fix: force OSS direct download even when traffic quota is enabled
This commit is contained in:
@@ -893,6 +893,63 @@ function applyDownloadTrafficUsage(userId, bytesToAdd) {
|
|||||||
return applyDownloadTrafficUsageTransaction(userId, Math.floor(parsedBytes));
|
return applyDownloadTrafficUsageTransaction(userId, Math.floor(parsedBytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reserveDirectDownloadTrafficTransaction = db.transaction((userId, bytesToReserve) => {
|
||||||
|
const policyState = enforceDownloadTrafficPolicyTransaction(userId, 'direct_download_reserve');
|
||||||
|
const user = policyState?.user || UserDB.findById(userId);
|
||||||
|
if (!user) {
|
||||||
|
return { ok: false, reason: 'user_not_found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const reserveBytes = Math.floor(Number(bytesToReserve));
|
||||||
|
if (!Number.isFinite(reserveBytes) || reserveBytes <= 0) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
quota: normalizeDownloadTrafficQuota(user.download_traffic_quota),
|
||||||
|
usedBefore: normalizeDownloadTrafficUsed(user.download_traffic_used, normalizeDownloadTrafficQuota(user.download_traffic_quota)),
|
||||||
|
usedAfter: normalizeDownloadTrafficUsed(user.download_traffic_used, normalizeDownloadTrafficQuota(user.download_traffic_quota)),
|
||||||
|
reserved: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const trafficState = getDownloadTrafficState(user);
|
||||||
|
if (!trafficState.isUnlimited && reserveBytes > trafficState.remaining) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'insufficient',
|
||||||
|
quota: trafficState.quota,
|
||||||
|
usedBefore: trafficState.used,
|
||||||
|
remaining: trafficState.remaining
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextUsed = trafficState.used + reserveBytes;
|
||||||
|
UserDB.update(userId, { download_traffic_used: nextUsed });
|
||||||
|
DownloadTrafficReportDB.addUsage(userId, reserveBytes, 1, new Date());
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
quota: trafficState.quota,
|
||||||
|
usedBefore: trafficState.used,
|
||||||
|
usedAfter: nextUsed,
|
||||||
|
reserved: reserveBytes
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function reserveDirectDownloadTraffic(userId, bytesToReserve) {
|
||||||
|
const parsedBytes = Number(bytesToReserve);
|
||||||
|
if (!Number.isFinite(parsedBytes) || parsedBytes <= 0) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
quota: 0,
|
||||||
|
usedBefore: 0,
|
||||||
|
usedAfter: 0,
|
||||||
|
reserved: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return reserveDirectDownloadTrafficTransaction(userId, Math.floor(parsedBytes));
|
||||||
|
}
|
||||||
|
|
||||||
function runDownloadTrafficPolicySweep(trigger = 'scheduled') {
|
function runDownloadTrafficPolicySweep(trigger = 'scheduled') {
|
||||||
try {
|
try {
|
||||||
const users = UserDB.getAll();
|
const users = UserDB.getAll();
|
||||||
@@ -4068,7 +4125,6 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开启下载流量配额后,禁止发放直连 URL(避免绕过后端计量)
|
|
||||||
const policyState = enforceDownloadTrafficPolicy(req.user.id, 'download_url');
|
const policyState = enforceDownloadTrafficPolicy(req.user.id, 'download_url');
|
||||||
const latestUser = policyState?.user || UserDB.findById(req.user.id);
|
const latestUser = policyState?.user || UserDB.findById(req.user.id);
|
||||||
if (!latestUser) {
|
if (!latestUser) {
|
||||||
@@ -4079,12 +4135,6 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trafficState = getDownloadTrafficState(latestUser);
|
const trafficState = getDownloadTrafficState(latestUser);
|
||||||
if (!trafficState.isUnlimited) {
|
|
||||||
return res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: '当前账号已启用下载流量限制,请使用系统下载入口'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户是否配置了 OSS(包括个人配置和系统级统一配置)
|
// 检查用户是否配置了 OSS(包括个人配置和系统级统一配置)
|
||||||
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
|
const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig();
|
||||||
@@ -4096,11 +4146,44 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
|
|
||||||
const { client, bucket, ossClient } = createS3ClientContextForUser(req.user);
|
const { client, bucket, ossClient } = createS3ClientContextForUser(latestUser);
|
||||||
const objectKey = ossClient.getObjectKey(normalizedPath);
|
const objectKey = ossClient.getObjectKey(normalizedPath);
|
||||||
|
let fileSize = 0;
|
||||||
|
|
||||||
|
// 启用下载流量限制时,签发前先校验文件大小与剩余额度
|
||||||
|
if (!trafficState.isUnlimited) {
|
||||||
|
let headResponse;
|
||||||
|
try {
|
||||||
|
headResponse = await client.send(new HeadObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: objectKey
|
||||||
|
}));
|
||||||
|
} catch (headError) {
|
||||||
|
const statusCode = headError?.$metadata?.httpStatusCode;
|
||||||
|
if (headError?.name === 'NotFound' || headError?.name === 'NoSuchKey' || statusCode === 404) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '文件不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw headError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = Number(headResponse?.ContentLength || 0);
|
||||||
|
fileSize = Number.isFinite(contentLength) && contentLength > 0
|
||||||
|
? Math.floor(contentLength)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (fileSize > trafficState.remaining) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: `下载流量不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(trafficState.remaining)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建 GetObject 命令
|
// 创建 GetObject 命令
|
||||||
const command = new GetObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
@@ -4112,10 +4195,24 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
// 生成签名 URL(1小时有效)
|
// 生成签名 URL(1小时有效)
|
||||||
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
||||||
|
|
||||||
|
// 直连模式下无法精确获知真实下载字节;限流时在签发前预扣文件大小
|
||||||
|
if (!trafficState.isUnlimited && fileSize > 0) {
|
||||||
|
const reserveResult = reserveDirectDownloadTraffic(latestUser.id, fileSize);
|
||||||
|
if (!reserveResult?.ok) {
|
||||||
|
const remaining = Number(reserveResult?.remaining || 0);
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: `下载流量不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(remaining)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
downloadUrl: signedUrl,
|
downloadUrl: signedUrl,
|
||||||
expiresIn: 3600
|
expiresIn: 3600,
|
||||||
|
direct: true,
|
||||||
|
quotaLimited: !trafficState.isUnlimited
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[OSS签名] 生成下载签名失败:', error);
|
console.error('[OSS签名] 生成下载签名失败:', error);
|
||||||
@@ -5371,8 +5468,8 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
|
|||||||
const storageType = share.storage_type || 'oss';
|
const storageType = share.storage_type || 'oss';
|
||||||
const ownerTrafficState = getDownloadTrafficState(shareOwner);
|
const ownerTrafficState = getDownloadTrafficState(shareOwner);
|
||||||
|
|
||||||
// 本地存储,或分享者启用了下载流量上限:统一走后端下载(便于计量)
|
// 本地存储:继续走后端下载
|
||||||
if (storageType !== 'oss' || !ownerTrafficState.isUnlimited) {
|
if (storageType !== 'oss') {
|
||||||
let downloadUrl = `${getSecureBaseUrl(req)}/api/share/${code}/download-file?path=${encodeURIComponent(normalizedFilePath)}`;
|
let downloadUrl = `${getSecureBaseUrl(req)}/api/share/${code}/download-file?path=${encodeURIComponent(normalizedFilePath)}`;
|
||||||
|
|
||||||
if (share.share_password) {
|
if (share.share_password) {
|
||||||
@@ -5401,11 +5498,43 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
|
|
||||||
const { client, bucket, ossClient } = createS3ClientContextForUser(shareOwner);
|
const { client, bucket, ossClient } = createS3ClientContextForUser(shareOwner);
|
||||||
const objectKey = ossClient.getObjectKey(normalizedFilePath);
|
const objectKey = ossClient.getObjectKey(normalizedFilePath);
|
||||||
|
let fileSize = 0;
|
||||||
|
|
||||||
|
if (!ownerTrafficState.isUnlimited) {
|
||||||
|
let headResponse;
|
||||||
|
try {
|
||||||
|
headResponse = await client.send(new HeadObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: objectKey
|
||||||
|
}));
|
||||||
|
} catch (headError) {
|
||||||
|
const statusCode = headError?.$metadata?.httpStatusCode;
|
||||||
|
if (headError?.name === 'NotFound' || headError?.name === 'NoSuchKey' || statusCode === 404) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '文件不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw headError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = Number(headResponse?.ContentLength || 0);
|
||||||
|
fileSize = Number.isFinite(contentLength) && contentLength > 0
|
||||||
|
? Math.floor(contentLength)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (fileSize > ownerTrafficState.remaining) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: `下载流量不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(ownerTrafficState.remaining)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建 GetObject 命令
|
// 创建 GetObject 命令
|
||||||
const command = new GetObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
@@ -5417,10 +5546,22 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
|
|||||||
// 生成签名 URL(1小时有效)
|
// 生成签名 URL(1小时有效)
|
||||||
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
const signedUrl = await getSignedUrl(client, command, { expiresIn: 3600 });
|
||||||
|
|
||||||
|
if (!ownerTrafficState.isUnlimited && fileSize > 0) {
|
||||||
|
const reserveResult = reserveDirectDownloadTraffic(shareOwner.id, fileSize);
|
||||||
|
if (!reserveResult?.ok) {
|
||||||
|
const remaining = Number(reserveResult?.remaining || 0);
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: `下载流量不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(remaining)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
downloadUrl: signedUrl,
|
downloadUrl: signedUrl,
|
||||||
direct: true,
|
direct: true,
|
||||||
|
quotaLimited: !ownerTrafficState.isUnlimited,
|
||||||
expiresIn: 3600
|
expiresIn: 3600
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1549,12 +1549,10 @@ handleDragLeave(e) {
|
|||||||
// 构建文件路径
|
// 构建文件路径
|
||||||
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
|
const filePath = this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
|
||||||
|
|
||||||
const hasDownloadTrafficLimit = Number(this.user?.download_traffic_quota || 0) > 0;
|
|
||||||
const canDirectOssDownload = this.storageType === 'oss'
|
const canDirectOssDownload = this.storageType === 'oss'
|
||||||
&& this.user?.oss_config_source !== 'none'
|
&& this.user?.oss_config_source !== 'none';
|
||||||
&& !hasDownloadTrafficLimit;
|
|
||||||
|
|
||||||
// OSS 且未启用下载限流:优先使用 OSS 直连下载(速度更快)
|
// OSS 下载优先使用直连(避免服务器中转导致带宽与费用双重损耗)
|
||||||
if (canDirectOssDownload) {
|
if (canDirectOssDownload) {
|
||||||
const directResult = await this.downloadFromOSS(filePath);
|
const directResult = await this.downloadFromOSS(filePath);
|
||||||
if (directResult) {
|
if (directResult) {
|
||||||
|
|||||||
Reference in New Issue
Block a user