@@ -1112,12 +1112,242 @@ function copySliceItem(params, callback) {
11121112 } ) ;
11131113}
11141114
1115+ // 分片下载文件
1116+ function downloadFile ( params , callback ) {
1117+ var self = this ;
1118+ var TaskId = params . TaskId || util . uuid ( ) ;
1119+ var Bucket = params . Bucket ;
1120+ var Region = params . Region ;
1121+ var Key = params . Key ;
1122+ var FilePath = params . FilePath ;
1123+ var FileSize ;
1124+ var FinishSize = 0 ;
1125+ var onProgress ;
1126+ var ChunkSize = params . ChunkSize || 1024 * 1024 ;
1127+ var ParallelLimit = params . ParallelLimit || 5 ;
1128+ var RetryTimes = params . RetryTimes || 3 ;
1129+ var ep = new EventProxy ( ) ;
1130+ var PartList ;
1131+ var aborted = false ;
1132+ var head = { } ;
1133+
1134+ ep . on ( 'error' , function ( err ) {
1135+ callback ( err ) ;
1136+ } ) ;
1137+
1138+ ep . on ( 'get_file_info' , function ( ) {
1139+ // 获取远端复制源文件的大小
1140+ self . headObject ( {
1141+ Bucket : Bucket ,
1142+ Region : Region ,
1143+ Key : Key ,
1144+ } , function ( err , data ) {
1145+ if ( err ) return ep . emit ( 'error' , err ) ;
1146+
1147+ // 获取文件大小
1148+ FileSize = params . FileSize = parseInt ( data . headers [ 'content-length' ] ) ;
1149+ if ( FileSize === undefined || ! FileSize ) {
1150+ callback ( util . error ( new Error ( 'get Content-Length error, please add "Content-Length" to CORS ExposeHeader setting.' ) ) ) ;
1151+ return ;
1152+ }
1153+
1154+ // 归档文件不支持下载
1155+ const resHeaders = data . headers ;
1156+ const storageClass = resHeaders [ 'x-cos-storage-class' ] || '' ;
1157+ const restoreStatus = resHeaders [ 'x-cos-restore' ] || '' ;
1158+ if (
1159+ [ 'DEEP_ARCHIVE' , 'ARCHIVE' ] . includes ( storageClass ) &&
1160+ ( ! restoreStatus || restoreStatus === 'ongoing-request="true"' )
1161+ ) {
1162+ return callback ( { statusCode, header : resHeaders , code : 'CannotDownload' , message : 'Archive object can not download, please restore to Standard storage class.' } ) ;
1163+ }
1164+
1165+ // 整理文件信息
1166+ head = {
1167+ ETag : data . ETag ,
1168+ size : FileSize ,
1169+ mtime : resHeaders [ 'last-modified' ] ,
1170+ crc64ecma : resHeaders [ 'x-cos-hash-crc64ecma' ] ,
1171+ } ;
1172+
1173+ // 处理进度反馈
1174+ onProgress = util . throttleOnProgress . call ( self , FileSize , function ( info ) {
1175+ if ( aborted ) return ;
1176+ params . onProgress ( info ) ;
1177+ } ) ;
1178+
1179+ if ( FileSize <= ChunkSize ) {
1180+ // 小文件直接单请求下载
1181+ self . getObject ( {
1182+ TaskId : TaskId ,
1183+ Bucket : Bucket ,
1184+ Region : Region ,
1185+ Key : Key ,
1186+ onProgress : onProgress ,
1187+ Output : fs . createWriteStream ( FilePath ) ,
1188+ } , function ( err , data ) {
1189+ if ( err ) {
1190+ onProgress ( null , true ) ;
1191+ return callback ( err ) ;
1192+ }
1193+ onProgress ( { loaded : FileSize , total : FileSize } , true ) ;
1194+ callback ( err , data ) ;
1195+ } ) ;
1196+ } else {
1197+ // 大文件分片下载
1198+ ep . emit ( 'calc_suitable_chunk_size' ) ;
1199+ }
1200+ } ) ;
1201+ } ) ;
1202+
1203+ // 计算合适的分片大小
1204+ ep . on ( 'calc_suitable_chunk_size' , function ( SourceHeaders ) {
1205+
1206+ // 控制分片大小
1207+ var SIZE = [ 1 , 2 , 4 , 8 , 16 , 32 , 64 , 128 , 256 , 512 , 1024 , 1024 * 2 , 1024 * 4 , 1024 * 5 ] ;
1208+ var AutoChunkSize = 1024 * 1024 ;
1209+ for ( var i = 0 ; i < SIZE . length ; i ++ ) {
1210+ AutoChunkSize = SIZE [ i ] * 1024 * 1024 ;
1211+ if ( FileSize / AutoChunkSize <= self . options . MaxPartNumber ) break ;
1212+ }
1213+ params . ChunkSize = ChunkSize = Math . max ( ChunkSize , AutoChunkSize ) ;
1214+
1215+ var ChunkCount = Math . ceil ( FileSize / ChunkSize ) ;
1216+
1217+ var list = [ ] ;
1218+ for ( var partNumber = 1 ; partNumber <= ChunkCount ; partNumber ++ ) {
1219+ var start = ( partNumber - 1 ) * ChunkSize ;
1220+ var end = partNumber * ChunkSize < FileSize ? ( partNumber * ChunkSize - 1 ) : FileSize - 1 ;
1221+ var item = {
1222+ PartNumber : partNumber ,
1223+ start : start ,
1224+ end : end ,
1225+ } ;
1226+ list . push ( item ) ;
1227+ }
1228+ PartList = list ;
1229+
1230+ ep . emit ( 'prepare_file' ) ;
1231+ } ) ;
1232+
1233+ // 准备要下载的空文件
1234+ ep . on ( 'prepare_file' , function ( SourceHeaders ) {
1235+ fs . writeFile ( FilePath , '' , err => {
1236+ if ( err ) {
1237+ ep . emit ( 'error' , err . code === 'EISDIR' ? { code : 'exist_same_dir' , message : FilePath } : err ) ;
1238+ } else {
1239+ ep . emit ( 'start_download_chunks' ) ;
1240+ }
1241+ } ) ;
1242+ } ) ;
1243+
1244+ // 计算合适的分片大小
1245+ var result ;
1246+ ep . on ( 'start_download_chunks' , function ( SourceHeaders ) {
1247+ onProgress ( { loaded : 0 , total : FileSize } , true ) ;
1248+ var maxPartNumber = PartList . length ;
1249+ Async . eachLimit ( PartList , ParallelLimit , function ( part , nextChunk ) {
1250+ if ( aborted ) return ;
1251+ Async . retry ( RetryTimes , function ( tryCallback ) {
1252+ if ( aborted ) return ;
1253+ // FinishSize
1254+ var Headers = util . clone ( params . Headers ) ;
1255+ Headers . Range = "bytes=" + part . start + "-" + part . end ;
1256+ const writeStream = fs . createWriteStream ( FilePath , {
1257+ start : part . start ,
1258+ flags : 'r+'
1259+ } ) ;
1260+ var preAddSize = 0 ;
1261+ var chunkReadSize = part . end - part . start ;
1262+ self . getObject ( {
1263+ TaskId : TaskId ,
1264+ Bucket : params . Bucket ,
1265+ Region : params . Region ,
1266+ Key : params . Key ,
1267+ Query : params . Query ,
1268+ Headers : Headers ,
1269+ onProgress : function ( data ) {
1270+ if ( aborted ) return ;
1271+ FinishSize += data . loaded - preAddSize ;
1272+ preAddSize = data . loaded ;
1273+ onProgress ( { loaded : FinishSize , total : FileSize } ) ;
1274+ } ,
1275+ Output : writeStream ,
1276+ } , function ( err , data ) {
1277+ if ( aborted ) return ;
1278+
1279+ // 处理错误和进度
1280+ if ( err ) {
1281+ FinishSize -= preAddSize ;
1282+ return tryCallback ( err ) ;
1283+ }
1284+
1285+ // 处理返回值
1286+ if ( part . PartNumber === maxPartNumber ) result = data ;
1287+ var chunkHeaders = data . headers || { } ;
1288+
1289+
1290+ var contentRanges = chunkHeaders [ 'content-range' ] || '' ; // content-range 格式:"bytes 3145728-4194303/68577051"
1291+ var totalSize = parseInt ( contentRanges . split ( '/' ) [ 1 ] || 0 ) ;
1292+
1293+ // 只校验文件大小和 crc64 是否有变更
1294+ var changed ;
1295+ if ( chunkHeaders [ 'x-cos-hash-crc64ecma' ] !== head . crc64ecma ) changed = 'download error, x-cos-hash-crc64ecma has changed.' ;
1296+ else if ( totalSize !== head . size ) changed = 'download error, Last-Modified has changed.' ;
1297+ // else if (data.ETag !== head.ETag) error = 'download error, ETag has changed.';
1298+ // else if (chunkHeaders['last-modified'] !== head.mtime) error = 'download error, Last-Modified has changed.';
1299+
1300+ // 如果
1301+ if ( changed ) {
1302+ FinishSize -= preAddSize ;
1303+ onProgress ( { loaded : FinishSize , total : FileSize } ) ;
1304+ ep . emit ( 'error' , {
1305+ code : 'ObjectHasChanged' ,
1306+ message : changed ,
1307+ statusCode : data . statusCode ,
1308+ header : chunkHeaders ,
1309+ } ) ;
1310+ self . emit ( 'inner-kill-task' , { TaskId : TaskId } ) ;
1311+ } else {
1312+ FinishSize += chunkReadSize - preAddSize ;
1313+ part . loaded = true ;
1314+ onProgress ( { loaded : FinishSize , total : FileSize } ) ;
1315+ tryCallback ( err , data ) ;
1316+ }
1317+ } ) ;
1318+ } , function ( err , data ) {
1319+ if ( aborted ) return ;
1320+ nextChunk ( err , data ) ;
1321+ } ) ;
1322+ } , function ( err , data ) {
1323+ if ( aborted ) return ;
1324+ onProgress ( { loaded : FileSize , total : FileSize } , true ) ;
1325+ if ( err ) return ep . emit ( 'error' , err ) ;
1326+ ep . emit ( 'download_chunks_complete' ) ;
1327+ } ) ;
1328+ } ) ;
1329+
1330+ // 下载已完成
1331+ ep . on ( 'download_chunks_complete' , function ( ) {
1332+ callback ( null , result ) ;
1333+ } ) ;
1334+
1335+ // 监听 取消任务
1336+ var killTask = function ( ) {
1337+ aborted = true ;
1338+ } ;
1339+ TaskId && self . on ( 'inner-kill-task' , killTask ) ;
1340+
1341+ ep . emit ( 'get_file_info' ) ;
1342+ }
1343+
11151344
11161345var API_MAP = {
11171346 sliceUploadFile : sliceUploadFile ,
11181347 abortUploadTask : abortUploadTask ,
11191348 uploadFiles : uploadFiles ,
11201349 sliceCopyFile : sliceCopyFile ,
1350+ downloadFile : downloadFile ,
11211351} ;
11221352
11231353module . exports . init = function ( COS , task ) {
0 commit comments