@@ -1112,12 +1112,242 @@ function copySliceItem(params, callback) {
1112
1112
} ) ;
1113
1113
}
1114
1114
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
+
1115
1344
1116
1345
var API_MAP = {
1117
1346
sliceUploadFile : sliceUploadFile ,
1118
1347
abortUploadTask : abortUploadTask ,
1119
1348
uploadFiles : uploadFiles ,
1120
1349
sliceCopyFile : sliceCopyFile ,
1350
+ downloadFile : downloadFile ,
1121
1351
} ;
1122
1352
1123
1353
module . exports . init = function ( COS , task ) {
0 commit comments