Skip to content

Commit 8e922d3

Browse files
committed
add downloadFile
1 parent 9d4645f commit 8e922d3

File tree

3 files changed

+258
-1
lines changed

3 files changed

+258
-1
lines changed

demo/demo.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,31 @@ function deleteFolder() {
13831383
});
13841384
}
13851385

1386+
function downloadFile() {
1387+
// 单文件分片并发下载
1388+
var Key = 'windows_7_ultimate_x64.iso';
1389+
cos.downloadFile({
1390+
Bucket: config.Bucket,
1391+
Region: config.Region,
1392+
Key: Key,
1393+
FilePath: './' + Key,
1394+
ChunkSize: 1024 * 1024 * 8, // 文件大于 8MB 用分片下载
1395+
ParallelLimit: 5, // 分片并发数
1396+
RetryTimes: 3, // 分片失败重试次数
1397+
onTaskReady: function (taskId) {
1398+
console.log(taskId);
1399+
},
1400+
onProgress: function (progressData) {
1401+
console.log(JSON.stringify(progressData));
1402+
},
1403+
}, function (err, data) {
1404+
console.log(err || data);
1405+
});
1406+
1407+
// 取消下载任务
1408+
// cos.emit('inner-kill-task', {TaskId: '123'});
1409+
}
1410+
13861411
function request() {
13871412
// 对云上数据进行图片处理
13881413
var filename = 'exampleImage.png';
@@ -1478,3 +1503,5 @@ function request() {
14781503
// uploadFolder();
14791504
// listFolder();
14801505
// deleteFolder();
1506+
// downloadFile();
1507+
// request();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cos-nodejs-sdk-v5",
3-
"version": "2.9.13",
3+
"version": "2.9.14",
44
"description": "cos nodejs sdk v5",
55
"main": "index.js",
66
"types": "types",

sdk/advance.js

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

11161345
var API_MAP = {
11171346
sliceUploadFile: sliceUploadFile,
11181347
abortUploadTask: abortUploadTask,
11191348
uploadFiles: uploadFiles,
11201349
sliceCopyFile: sliceCopyFile,
1350+
downloadFile: downloadFile,
11211351
};
11221352

11231353
module.exports.init = function (COS, task) {

0 commit comments

Comments
 (0)