@@ -112,7 +112,7 @@ const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max(
10 ,
Math . min ( 3600 , Number ( process . env . DOWNLOAD _SIGNED _URL _EXPIRES _SECONDS || 30 ) )
) ;
const DEFAULT _DESKTOP _VERSION = process . env . DESKTOP _LATEST _VERSION || '0.1.31 ' ;
const DEFAULT _DESKTOP _VERSION = process . env . DESKTOP _LATEST _VERSION || '0.1.38 ' ;
const DEFAULT _DESKTOP _INSTALLER _URL = process . env . DESKTOP _INSTALLER _URL || '' ;
const DEFAULT _DESKTOP _INSTALLER _SHA256 = String ( process . env . DESKTOP _INSTALLER _SHA256 || '' ) . trim ( ) . toLowerCase ( ) ;
const DEFAULT _DESKTOP _INSTALLER _SIZE = Math . max ( 0 , Number ( process . env . DESKTOP _INSTALLER _SIZE || 0 ) ) ;
@@ -2562,7 +2562,13 @@ const MULTER_UPLOAD_MAX_BYTES = Math.max(
) ;
const upload = multer ( {
dest : path . join ( _ _dirname , 'uploads' ) ,
limits : { fileSize : MULTER _UPLOAD _MAX _BYTES }
limits : {
fileSize : MULTER _UPLOAD _MAX _BYTES ,
fields : 20 ,
parts : 22 ,
fieldNameSize : 100 ,
fieldSize : 1024 * 1024
}
} ) ;
// ===== TTL缓存类 =====
@@ -5719,11 +5725,63 @@ app.get('/api/files/search', authMiddleware, async (req, res) => {
}
} ) ;
function rewritePersistedPathsAfterRename ( userId , storageType , oldPath , newPath , isDirectory ) {
const oldPrefix = ` ${ oldPath } / ` ;
const rewritePath = ( value ) => {
const current = normalizeVirtualPath ( value || '' ) ;
if ( ! current ) return null ;
if ( current === oldPath ) return newPath ;
if ( isDirectory && current . startsWith ( oldPrefix ) ) {
return ` ${ newPath } ${ current . slice ( oldPath . length ) } ` ;
}
return null ;
} ;
db . transaction ( ( ) => {
const shares = db . prepare ( `
SELECT id, share_path FROM shares
WHERE user_id = ? AND COALESCE(storage_type, 'oss') = ?
AND (share_path = ? OR share_path LIKE ?)
` ) . all ( userId , storageType , oldPath , ` ${ oldPrefix } % ` ) ;
const updateShare = db . prepare ( 'UPDATE shares SET share_path = ? WHERE id = ?' ) ;
for ( const share of shares ) {
const rewritten = rewritePath ( share . share _path ) ;
if ( rewritten ) updateShare . run ( rewritten , share . id ) ;
}
const links = db . prepare ( `
SELECT id, file_path FROM direct_links
WHERE user_id = ? AND COALESCE(storage_type, 'oss') = ?
AND (file_path = ? OR file_path LIKE ?)
` ) . all ( userId , storageType , oldPath , ` ${ oldPrefix } % ` ) ;
const updateLink = db . prepare ( 'UPDATE direct_links SET file_path = ?, file_name = ? WHERE id = ?' ) ;
for ( const link of links ) {
const rewritten = rewritePath ( link . file _path ) ;
if ( rewritten ) updateLink . run ( rewritten , rewritten . split ( '/' ) . pop ( ) || null , link . id ) ;
}
const hashes = db . prepare ( `
SELECT id, file_path FROM user_file_hash_index
WHERE user_id = ? AND storage_type = ?
AND (file_path = ? OR file_path LIKE ?)
` ) . all ( userId , storageType , oldPath , ` ${ oldPrefix } % ` ) ;
const updateHash = db . prepare ( `
UPDATE user_file_hash_index
SET file_path = ?, object_key = NULL, updated_at = datetime('now', 'localtime')
WHERE id = ?
` ) ;
for ( const row of hashes ) {
const rewritten = rewritePath ( row . file _path ) ;
if ( rewritten ) updateHash . run ( rewritten , row . id ) ;
}
} ) ( ) ;
}
// 重命名文件
app . post ( '/api/files/rename' , authMiddleware , async ( req , res ) => {
const oldName = decodeHtmlEntities ( req . body . oldName ) ;
const newName = decodeHtmlEntities ( req . body . newName ) ;
const path = decodeHtmlEntities ( req . body . path ) || '/' ;
const parentPa th = normalizeVirtualPath ( decodeHtmlEntities ( req . body . path ) || '/' ) ;
let storage ;
if ( ! oldName || ! newName ) {
@@ -5733,14 +5791,22 @@ app.post('/api/files/rename', authMiddleware, async (req, res) => {
} ) ;
}
if ( ! parentPath || ! isSafePathSegment ( oldName ) || ! isSafePathSegment ( newName ) ) {
return res . status ( 400 ) . json ( {
success : false ,
message : '文件名或路径包含非法字符'
} ) ;
}
try {
// 使用统一存储接口
const { StorageInterface } = require ( './storage' ) ;
const storageInterface = new StorageInterface ( req . user ) ;
storage = await storageInterface . connect ( ) ;
const oldPath = path === '/' ? ` / ${ oldName } ` : ` ${ path } / ${ oldName } ` ;
const newPath = path === '/' ? ` / ${ newName } ` : ` ${ path } / ${ newName } ` ;
const oldPath = parentPa th === '/' ? ` / ${ oldName } ` : ` ${ parentPa th } / ${ oldName } ` ;
const newPath = parentPa th === '/' ? ` / ${ newName } ` : ` ${ parentPa th } / ${ newName } ` ;
const sourceStats = await storage . stat ( oldPath ) ;
await storage . rename ( oldPath , newPath ) ;
@@ -5748,18 +5814,22 @@ app.post('/api/files/rename', authMiddleware, async (req, res) => {
const normalizedOldPath = normalizeVirtualPath ( oldPath ) ;
const normalizedNewPath = normalizeVirtualPath ( newPath ) ;
if ( normalizedOldPath && normalizedNewPath ) {
const oldHashRow = FileHashIndexDB . getByPath ( req . user . id , normalizedStorageType , normalizedOldPath ) ;
if ( oldHashRow ) {
FileHashIndexDB . up sert ( {
userId : req . user . id ,
storageType : normalizedStorageType ,
fileHash : oldHashRow . file _has h,
fileSize : oldHashRow . file _size ,
filePath : normalizedNewPath ,
objectKey : oldHashRow . object _key || null
} ) ;
try {
rewritePersistedPathsAfterRename (
req . user . id ,
normalizedStorageType ,
normalizedOldPath ,
normalizedNewPat h,
! ! sourceStats ? . isDirectory
) ;
} catch ( metadataError ) {
try {
await storage . rename ( newPath , oldPath ) ;
} catch ( rollbackError ) {
console . error ( '重命名元数据失败且存储回滚失败:' , rollbackError ) ;
}
throw metadataError ;
}
FileHashIndexDB . deleteByPath ( req . user . id , normalizedStorageType , normalizedOldPath ) ;
}
// 清除 OSS 使用情况缓存(如果用户使用 OSS)
@@ -7140,6 +7210,28 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
const trafficState = getDownloadTrafficState ( latestUser ) ;
if ( ( latestUser . current _storage _type || 'oss' ) === 'local' ) {
const localMode = isPreviewMode ? 'preview' : 'download' ;
const downloadToken = signEphemeralToken ( {
type : 'local_download' ,
userId : latestUser . id ,
path : normalizedPath ,
mode : localMode
} , 15 * 60 ) ;
const downloadUrl = ` ${ getSecureBaseUrl ( req ) } /api/files/download ` +
` ?path= ${ encodeURIComponent ( normalizedPath ) } ` +
` &mode= ${ encodeURIComponent ( localMode ) } ` +
` &token= ${ encodeURIComponent ( downloadToken ) } ` ;
return res . json ( {
success : true ,
downloadUrl ,
expiresIn : 15 * 60 ,
direct : false ,
storageType : 'local' ,
quotaLimited : ! trafficState . isUnlimited
} ) ;
}
// 检查用户是否配置了 OSS( 包括个人配置和系统级统一配置)
const hasUnifiedConfig = SettingsDB . hasUnifiedOssConfig ( ) ;
if ( ! req . user . has _oss _config && ! hasUnifiedConfig ) {
@@ -7469,8 +7561,50 @@ app.get('/api/files/download-check', authMiddleware, async (req, res) => {
}
} ) ;
function parseSingleByteRange ( rangeHeader , fileSize ) {
if ( ! rangeHeader ) return null ;
const match = /^bytes=(\d*)-(\d*)$/ . exec ( String ( rangeHeader ) . trim ( ) ) ;
if ( ! match || ( ! match [ 1 ] && ! match [ 2 ] ) || fileSize <= 0 ) return { invalid : true } ;
let start ;
let end ;
if ( ! match [ 1 ] ) {
const suffixLength = Number ( match [ 2 ] ) ;
if ( ! Number . isSafeInteger ( suffixLength ) || suffixLength <= 0 ) return { invalid : true } ;
start = Math . max ( 0 , fileSize - suffixLength ) ;
end = fileSize - 1 ;
} else {
start = Number ( match [ 1 ] ) ;
end = match [ 2 ] ? Number ( match [ 2 ] ) : fileSize - 1 ;
if ( ! Number . isSafeInteger ( start ) || ! Number . isSafeInteger ( end ) || start < 0 || start >= fileSize || end < start ) {
return { invalid : true } ;
}
end = Math . min ( end , fileSize - 1 ) ;
}
return { invalid : false , start , end , length : end - start + 1 } ;
}
function authOrLocalDownloadToken ( req , res , next ) {
const tokenResult = verifyEphemeralToken ( req . query ? . token , 'local_download' ) ;
if ( tokenResult . valid ) {
const payload = tokenResult . payload || { } ;
const requestedPath = normalizeVirtualPath ( req . query ? . path || '' ) ;
const requestedMode = req . query ? . mode === 'preview' ? 'preview' : 'download' ;
if ( requestedPath && payload . path === requestedPath && payload . mode === requestedMode ) {
const user = UserDB . findById ( Number ( payload . userId ) ) ;
if ( user && user . is _active && ! user . is _banned ) {
req . user = user ;
req . localDownloadTokenAuthenticated = true ;
return next ( ) ;
}
}
}
return authMiddleware ( req , res , next ) ;
}
// 下载文件
app . get ( '/api/files/download' , authMiddleware , async ( req , res ) => {
app . get ( '/api/files/download' , authOrLocalDownloadToken , async ( req , res ) => {
const filePath = req . query . path ;
let storage ;
let storageEnded = false ; // 防止重复关闭
@@ -7545,6 +7679,37 @@ app.get('/api/files/download', authMiddleware, async (req, res) => {
} ) ;
}
// Legacy pages download with the session cookie only. Redirect once to a
// short-lived URL so Android's external download manager can resume without it.
if ( ! req . localDownloadTokenAuthenticated ) {
const securityResult = evaluateDownloadSecurityPolicy ( req , {
ownerUserId : req . user . id ,
filePath : normalizedPath ,
source : 'user_download_legacy_redirect'
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
ownerUserId : req . user . id ,
filePath : normalizedPath ,
source : 'user_download_legacy_redirect'
} ) ;
}
const downloadMode = req . query ? . mode === 'preview' ? 'preview' : 'download' ;
const downloadToken = signEphemeralToken ( {
type : 'local_download' ,
userId : req . user . id ,
path : normalizedPath ,
mode : downloadMode
} , 15 * 60 ) ;
const redirectUrl = '/api/files/download' +
` ?path= ${ encodeURIComponent ( normalizedPath ) } ` +
` &mode= ${ encodeURIComponent ( downloadMode ) } ` +
` &token= ${ encodeURIComponent ( downloadToken ) } ` ;
return res . redirect ( 302 , redirectUrl ) ;
}
try {
const policyState = enforceDownloadTrafficPolicy ( req . user . id , 'download' ) ;
const latestUser = policyState ? . user || UserDB . findById ( req . user . id ) ;
@@ -7555,18 +7720,22 @@ app.get('/api/files/download', authMiddleware, async (req, res) => {
} ) ;
}
const securityResult = evaluateDownloadSecurityPolicy ( req , {
ownerUserId : latestUser . id ,
filePath : normalizedPath ,
source : 'user_download_stream'
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
// The security policy is evaluated when the short-lived token is issued.
// Do not count the browser probes and DownloadManager requests again.
if ( req . localDownloadTokenAuthenticated !== true ) {
const securityResult = evaluateDownloadSecurityPolicy ( req , {
ownerUserId : latestUser . id ,
filePath : normalizedPath ,
source : 'user_download_stream'
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
ownerUserId : latestUser . id ,
filePath : normalizedPath ,
source : 'user_download_stream'
} ) ;
}
}
const trafficState = getDownloadTrafficState ( latestUser ) ;
@@ -7582,29 +7751,49 @@ app.get('/api/files/download', authMiddleware, async (req, res) => {
// 先获取文件信息(获取文件大小)
const fileStats = await storage . stat ( normalizedPath ) ;
const fileSize = Math . max ( 0 , Number ( fileStats ? . size ) || 0 ) ;
const isLocalStorage = ( latestUser . current _storage _type || 'oss' ) === 'local' ;
const byteRange = isLocalStorage ? parseSingleByteRange ( req . headers . range , fileSize ) : null ;
console . log ( '[下载] 文件: ' + fileName + ', 大小: ' + fileSize + ' 字节' ) ;
if ( ! trafficState . isUnlimited && fileSize > trafficState . remaining ) {
if ( byteRange ? . invalid ) {
res . setHeader ( 'Content-Range' , ` bytes */ ${ fileSize } ` ) ;
await safeEndStorage ( ) ;
return res . status ( 416 ) . end ( ) ;
}
const responseSize = byteRange ? byteRange . length : fileSize ;
if ( ! trafficState . isUnlimited && responseSize > trafficState . remaining ) {
await safeEndStorage ( ) ;
return res . status ( 403 ) . json ( {
success : false ,
message : ` 下载流量不足:文件 ${ formatFileSize ( fil eSize) } ,剩余 ${ formatFileSize ( trafficState . remaining ) } `
message : ` 下载流量不足:本次需要 ${ formatFileSize ( respons eSize) } ,剩余 ${ formatFileSize ( trafficState . remaining ) } `
} ) ;
}
// 设置响应头(包含文件大小,浏览器可显示下载进度)
res . setHeader ( 'Content-Type' , 'application/octet-stream' ) ;
res . setHeader ( 'Content-Length' , fil eSize) ;
res . setHeader ( 'Content-Length' , respons eSize) ;
if ( isLocalStorage ) {
res . setHeader ( 'Accept-Ranges' , 'bytes' ) ;
}
if ( byteRange ) {
res . status ( 206 ) ;
res . setHeader ( 'Content-Range' , ` bytes ${ byteRange . start } - ${ byteRange . end } / ${ fileSize } ` ) ;
}
// 关闭 Nginx 代理缓冲,避免上游提前读完整文件导致流量计量失真
res . setHeader ( 'X-Accel-Buffering' , 'no' ) ;
res . setHeader ( 'Content-Disposition' , 'attachment; filename="' + encodeURIComponent ( fileName ) + '"; filename*=UTF-8\'\' ' + encodeURIComponent ( fileName ) ) ;
const disposition = req . query ? . mode === 'preview' ? 'inline ' : 'attachment' ;
res . setHeader ( 'Content-Disposition' , disposition + '; filename="' + encodeURIComponent ( fileName ) + '"; filename*=UTF-8\'\'' + encodeURIComponent ( fileName ) ) ;
if ( typeof res . flushHeaders === 'function' ) {
res . flushHeaders ( ) ;
}
responseBodyStartSocketBytes = Number ( res . socket ? . bytesWritten ) || 0 ;
// 创建文件流并传输(流式下载,服务器不保存临时文件)
const stream = await storage . createReadStream ( normalizedPath ) ;
const stream = await storage . createReadStream (
normalizedPath ,
byteRange ? { start : byteRange . start , end : byteRange . end } : undefined
) ;
stream . on ( 'data' , ( chunk ) => {
if ( ! chunk ) return ;
@@ -8865,7 +9054,7 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
} ) ;
}
cons t normalizedFilePath = normalizeVirtualPath ( filePath ) ;
le t normalizedFilePath = normalizeVirtualPath ( filePath ) ;
if ( ! normalizedFilePath ) {
return res . status ( 400 ) . json ( {
success : false ,
@@ -8901,6 +9090,12 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
}
}
// Older cached share pages may request "/" for single-file shares.
// Map that back to the immutable share path before the strict scope check.
if ( share . share _type === 'file' && normalizedFilePath === '/' ) {
normalizedFilePath = normalizeVirtualPath ( share . share _path ) ;
}
// 安全验证:检查请求路径是否在分享范围内
if ( ! isPathWithinShare ( normalizedFilePath , share ) ) {
return res . status ( 403 ) . json ( {
@@ -8909,9 +9104,10 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
} ) ;
}
const isResumeRequest = /^bytes=[1-9]\d*-/i . test ( String ( req . headers . range || '' ) . trim ( ) ) ;
const accessPolicy = evaluateShareSecurityPolicy ( share , req , {
action : 'download' ,
enforceDownloadLimit : true
enforceDownloadLimit : ! isResumeRequest
} ) ;
if ( ! accessPolicy . allowed ) {
return res . status ( 403 ) . json ( {
@@ -8934,20 +9130,21 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
const storageType = share . storage _type || 'oss' ;
const ownerTrafficState = getDownloadTrafficState ( shareOwner ) ;
if ( storageType === 'oss' ) {
const securityResult = evaluateDownloadSecurityPolicy ( req , {
const downloadSecuritySource = storageType === 'oss'
? 'share_download_url_oss'
: 'share_download_url_local' ;
const securityResult = evaluateDownloadSecurityPolicy ( req , {
ownerUserId : shareOwner . id ,
filePath : normalizedFilePath ,
source : downloadSecuritySource
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
ownerUserId : shareOwner . id ,
filePath : normalizedFilePath ,
source : 'share_download_url_oss'
source : downloadSecuritySource
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
ownerUserId : shareOwner . id ,
filePath : normalizedFilePath ,
source : 'share_download_url_oss'
} ) ;
}
}
// 本地存储:继续走后端下载
@@ -8990,16 +9187,17 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
}
}
le t downloadUrl = ` ${ getSecureBaseUrl ( req ) } /api/share/ ${ code } /download-file?path= ${ encodeURIComponent ( normalizedFilePath ) } ` ;
cons t downloadToken = signEphemeralToken ( {
type : 'share_download' ,
code ,
path : normalizedFilePath
} , 15 * 60 ) ;
const downloadUrl = ` ${ getSecureBaseUrl ( req ) } /api/share/ ${ code } /download-file ` +
` ?path= ${ encodeURIComponent ( normalizedFilePath ) } ` +
` &token= ${ encodeURIComponent ( downloadToken ) } ` ;
if ( share . share _password ) {
const downloadToken = signEphemeralToken ( {
type : 'share_download' ,
code ,
path : normalizedFilePath
} , 15 * 60 ) ;
downloadUrl += ` &token= ${ encodeURIComponent ( downloadToken ) } ` ;
}
// Count one user-initiated download, not every browser probe or range request.
ShareDB . incrementDownloadCount ( code ) ;
return res . json ( {
success : true ,
@@ -9105,7 +9303,7 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
const { code } = req . params ;
const rawFilePath = typeof req . query ? . path === 'string' ? req . query . path : '' ;
const { password , token } = req . query ;
cons t filePath = normalizeVirtualPath ( rawFilePath ) ;
le t filePath = normalizeVirtualPath ( rawFilePath ) ;
let storage ;
let storageEnded = false ; // 防止重复关闭
let transferFinalized = false ; // 防止重复结算
@@ -9181,21 +9379,24 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
} ) ;
}
// Older cached share pages may request "/" for single-file shares.
// Map that back to the immutable share path before validating the token.
if ( share . share _type === 'file' && filePath === '/' ) {
filePath = normalizeVirtualPath ( share . share _path ) ;
}
let verifiedByShareToken = false ;
if ( token ) {
const tokenResult = verifyEphemeralToken ( token , 'share_download' ) ;
if ( tokenResult . valid ) {
const tokenPayload = tokenResult . payload || { } ;
verifiedByShareToken = tokenPayload . code === code && tokenPayload . path === filePath ;
}
}
// 验证密码(如果需要),支持短期下载 token( 避免密码出现在 URL)
if ( share . share _password ) {
let verifiedByToken = false ;
if ( token ) {
const tokenResult = verifyEphemeralToken ( token , 'share_download' ) ;
if ( tokenResult . valid ) {
const tokenPayload = tokenResult . payload || { } ;
if ( tokenPayload . code === code && tokenPayload . path === filePath ) {
verifiedByToken = true ;
}
}
}
if ( ! verifiedByToken ) {
if ( ! verifiedByShare Token ) {
if ( ! password || ! ShareDB . verifyPassword ( password , share . share _password ) ) {
// 只在密码错误时记录失败
if ( req . shareRateLimitKey ) {
@@ -9224,9 +9425,10 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
} ) ;
}
const isResumeRequest = /^bytes=[1-9]\d*-/i . test ( String ( req . headers . range || '' ) . trim ( ) ) ;
const accessPolicy = evaluateShareSecurityPolicy ( share , req , {
action : 'download' ,
enforceDownloadLimit : true
enforceDownloadLimit : ! verifiedByShareToken && ! isResumeRequest
} ) ;
if ( ! accessPolicy . allowed ) {
return res . status ( 403 ) . json ( {
@@ -9246,18 +9448,20 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
}
shareOwnerId = shareOwner . id ;
const securityResult = evaluateDownloadSecurityPolicy ( req , {
ownerUserId : shareOwner . id ,
filePath ,
source : 'share_download_stream'
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
if ( ! verifiedByShareToken ) {
const securityResult = evaluateDownloadSecurityPolicy ( req , {
ownerUserId : shareOwner . id ,
filePath ,
source : 'share_download_stream'
} ) ;
if ( ! securityResult . allowed ) {
return handleDownloadSecurityBlock ( req , res , securityResult , {
statusCode : 503 ,
ownerUserId : shareOwner . id ,
filePath ,
source : 'share_download_stream'
} ) ;
}
}
const ownerTrafficState = getDownloadTrafficState ( shareOwner ) ;
@@ -9283,22 +9487,36 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
// 获取文件信息(获取文件大小)
const fileStats = await storage . stat ( filePath ) ;
const fileSize = fileStats . size ;
const isLocalStorage = storageType === 'local' ;
const byteRange = isLocalStorage ? parseSingleByteRange ( req . headers . range , fileSize ) : null ;
console . log ( ` [分享下载] 文件: ${ fileName } , 大小: ${ fileSize } 字节 ` ) ;
if ( ! ownerTrafficState . isUnlimited && fileSize > ownerTrafficState . remaining ) {
if ( byteRange ? . invalid ) {
res . setHeader ( 'Content-Range' , ` bytes */ ${ fileSize } ` ) ;
await safeEndStorage ( ) ;
return res . status ( 416 ) . end ( ) ;
}
const responseSize = byteRange ? byteRange . length : fileSize ;
if ( ! ownerTrafficState . isUnlimited && responseSize > ownerTrafficState . remaining ) {
await safeEndStorage ( ) ;
return res . status ( 403 ) . json ( {
success : false ,
message : ` 分享者下载流量不足:文件 ${ formatFileSize ( fil eSize) } ,剩余 ${ formatFileSize ( ownerTrafficState . remaining ) } `
message : ` 分享者下载流量不足:本次需要 ${ formatFileSize ( respons eSize) } ,剩余 ${ formatFileSize ( ownerTrafficState . remaining ) } `
} ) ;
}
// 增加下载次数
ShareDB . incrementDownloadCount ( code ) ;
if ( ! verifiedByShareToken && ! isResumeRequest ) ShareDB . incrementDownloadCount ( code ) ;
// 设置响应头(包含文件大小,浏览器可显示下载进度)
res . setHeader ( 'Content-Type' , 'application/octet-stream' ) ;
res . setHeader ( 'Content-Length' , fil eSize) ;
res . setHeader ( 'Content-Length' , respons eSize) ;
if ( isLocalStorage ) res . setHeader ( 'Accept-Ranges' , 'bytes' ) ;
if ( byteRange ) {
res . status ( 206 ) ;
res . setHeader ( 'Content-Range' , ` bytes ${ byteRange . start } - ${ byteRange . end } / ${ fileSize } ` ) ;
}
// 关闭 Nginx 代理缓冲,减少代理预读导致的计量偏差
res . setHeader ( 'X-Accel-Buffering' , 'no' ) ;
res . setHeader ( 'Content-Disposition' , ` attachment; filename=" ${ encodeURIComponent ( fileName ) } "; filename*=UTF-8'' ${ encodeURIComponent ( fileName ) } ` ) ;
@@ -9308,7 +9526,10 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
responseBodyStartSocketBytes = Number ( res . socket ? . bytesWritten ) || 0 ;
// 创建文件流并传输(流式下载,服务器不保存临时文件)
const stream = await storage . createReadStream ( filePath ) ;
const stream = await storage . createReadStream (
filePath ,
byteRange ? { start : byteRange . start , end : byteRange . end } : undefined
) ;
stream . on ( 'data' , ( chunk ) => {
if ( ! chunk ) return ;
@@ -11466,6 +11687,7 @@ app.get('/d/:code', async (req, res) => {
const fileStats = await storage . stat ( normalizedPath ) ;
const fileSize = Number ( fileStats ? . size || 0 ) ;
const byteRange = parseSingleByteRange ( req . headers . range , fileSize ) ;
if ( fileStats ? . isDirectory ) {
await safeEndStorage ( ) ;
@@ -11477,15 +11699,27 @@ app.get('/d/:code', async (req, res) => {
return sendPlainTextError ( res , 404 , '文件不存在' ) ;
}
if ( ! ownerTrafficState . isUnlimited && fileSize > ownerTrafficState . remaining ) {
if ( byteRange ? . invalid ) {
res . setHeader ( 'Content-Range' , ` bytes */ ${ fileSize } ` ) ;
await safeEndStorage ( ) ;
return res . status ( 416 ) . end ( ) ;
}
const responseSize = byteRange ? byteRange . length : fileSize ;
if ( ! ownerTrafficState . isUnlimited && responseSize > ownerTrafficState . remaining ) {
await safeEndStorage ( ) ;
return sendPlainTextError ( res , 503 , getBusyDownloadMessage ( ) ) ;
}
DirectLinkDB . incrementDownloadCount ( code ) ;
if ( ! byteRange || byteRange . start === 0 ) DirectLinkDB . incrementDownloadCount ( code ) ;
res . setHeader ( 'Content-Type' , 'application/octet-stream' ) ;
res . setHeader ( 'Content-Length' , fil eSize) ;
res . setHeader ( 'Content-Length' , respons eSize) ;
res . setHeader ( 'Accept-Ranges' , 'bytes' ) ;
if ( byteRange ) {
res . status ( 206 ) ;
res . setHeader ( 'Content-Range' , ` bytes ${ byteRange . start } - ${ byteRange . end } / ${ fileSize } ` ) ;
}
res . setHeader ( 'X-Accel-Buffering' , 'no' ) ;
res . setHeader ( 'Content-Disposition' , ` attachment; filename=" ${ encodeURIComponent ( directFileName ) } "; filename*=UTF-8'' ${ encodeURIComponent ( directFileName ) } ` ) ;
if ( typeof res . flushHeaders === 'function' ) {
@@ -11493,7 +11727,10 @@ app.get('/d/:code', async (req, res) => {
}
responseBodyStartSocketBytes = Number ( res . socket ? . bytesWritten ) || 0 ;
const stream = await storage . createReadStream ( normalizedPath ) ;
const stream = await storage . createReadStream (
normalizedPath ,
byteRange ? { start : byteRange . start , end : byteRange . end } : undefined
) ;
stream . on ( 'data' , ( chunk ) => {
if ( ! chunk ) return ;
downloadedBytes += Buffer . isBuffer ( chunk ) ? chunk . length : Buffer . byteLength ( chunk ) ;