UNPKG

ali-oss-extra

Version:

Extend the official ali-oss with more convenient methods, such as listing, syncing or deleting a directory, put or delete a list of files etc.

579 lines (479 loc) 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _lodash = require("lodash"); var _async = _interopRequireDefault(require("async")); var _aliOss = _interopRequireDefault(require("ali-oss")); var _fsExtraPromise = _interopRequireDefault(require("fs-extra-promise")); var _isThere = _interopRequireDefault(require("is-there")); var _moment = _interopRequireDefault(require("moment")); var _walk = _interopRequireDefault(require("walk")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class OSSExtra extends _aliOss.default { async putList(fileList, { thread = 10, defaultHeader = {}, headersMap = new Map(), bigFile = 1024 * 500, partSize = 1024 * 500, timeout = 120 * 1000, ulimit = 1024, verbose = false } = {}, { putResultsMap = new Map(), checkPointMap = new Map(), uploadFilesMap = new Map() } = {}) { if (fileList.some(f => typeof f !== 'object' || !f.src || !f.dst || typeof f.src !== 'string' || typeof f.dst !== 'string' || typeof f.size !== 'number')) { throw new Error('putList: Incorrect input!'); } async function putFile(file) { if (putResultsMap.has(file.dst)) { return; } try { const headers = Object.assign({}, defaultHeader, headersMap.get(file.dst)); if (file.size >= bigFile) { const multiOptions = { headers, progress: function* (_, checkPoint) { checkPointMap.set(file.dst, checkPoint); } }; if (checkPointMap.has(file.dst)) { multiOptions.checkpoint = (0, _lodash.cloneDeep)(checkPointMap.get(file.dst)); } else { multiOptions.partSize = Math.max(Math.ceil(file.size / ulimit), partSize); } const result = await this.multipartUpload(file.dst, file.src, multiOptions); checkPointMap.delete(file.dst); uploadFilesMap.delete(file.dst); putResultsMap.set(file.dst, result); if (verbose) { console.log(`Uploaded: ${file.dst}, Remaining: ${uploadFilesMap.size}`); } } else { const result = await this.put(file.dst, file.src, { headers, timeout }); uploadFilesMap.delete(file.dst); putResultsMap.set(file.dst, result); if (verbose) { console.log(`Uploaded: ${file.dst}, Remaining: ${uploadFilesMap.size}`); } } } catch (err) { err.checkPointMap = checkPointMap; throw err; } } fileList = (0, _lodash.sortBy)(fileList, 'size'); await _async.default.mapLimit(fileList, thread, putFile.bind(this)); return [...putResultsMap.values()]; } async deleteList(fileList, { thread = 20, verbose = false } = {}, { deleteResults = [], deleteFilesMap = new Map() } = {}) { if (fileList.some(f => typeof f !== 'object' || !f.name || typeof f.name !== 'string')) { throw new Error('deleteList: Incorrect input!'); } async function deleteFile(file) { const result = await this.delete(file.name); deleteFilesMap.delete(file.name); deleteResults.push(result); if (verbose) { console.log(`Deleted: ${file.name}, Remaining: ${deleteFilesMap.size}`); } } await _async.default.mapLimit(fileList, thread, deleteFile.bind(this)); return deleteResults; } /** * Get a map of local files. */ async _getLocalFilesMap(directory, prefix, ignoreList = []) { function isIgnore(src) { return ignoreList.some(dir => src.startsWith(`${directory}/${dir}/`) || src === `${directory}/${dir}`); } return new Promise((resolve, reject) => { // a. check if directory exists if (!(0, _isThere.default)(directory)) { return reject(new Error(`Path ${directory} does not exist!`)); } // b. construct list of local files const localFiles = new Map(); const walker = _walk.default.walk(directory); walker.on('file', (root, stat, next) => { const dst = `${prefix}${root.substr(directory.length)}/${stat.name}`; const src = `${root}/${stat.name}`; if (!isIgnore(src)) { localFiles.set(dst, { dst, src, mtime: stat.mtime.toISOString(), size: stat.size }); } next(); }); walker.on('end', async () => { resolve(localFiles); }); }); } /** * Get a map of cloud files. */ async _getCloudFilesMap(prefix, options = { retryLimit: null }, meta = {}) { const cloudFiles = new Map(); const trial = (meta.trial || 0) + 1; try { const cloudFileList = await this.listDir(prefix, ['name', 'lastModified']); cloudFileList.forEach(f => cloudFiles.set(f.name, f)); return cloudFiles; } catch (err) { if (err && (err.name === 'ResponseTimeoutError' || err.name === 'ConnectionTimeoutError' || err.name === 'RequestError' || err.name === 'ResponseError')) { const { retryLimit } = options; if (retryLimit && Number.isInteger(retryLimit)) { if (trial < retryLimit) { return this._getCloudFilesMap(prefix, options, { trial }); } else { throw new Error('Retry limit exceeded!'); } } else { return this._getCloudFilesMap(prefix); } } else { throw err; } } } /** * As in: * s3 sync ${directory} s3://bucket/${prefix} --delete */ async syncDir(directory, prefix, { remove = true, ignoreList = [], defaultHeader = {}, headersMap = new Map(), retryLimit = null, thread = 10, timeout = 120 * 1000, ulimit = 1024, verbose = false } = {}, { retrying = false, putResultsMap = new Map(), deleteResults = [], checkPointMap = new Map(), uploadFilesMap = new Map(), deleteFilesMap = new Map(), trial = 0 } = {}) { const options = { remove, ignoreList, defaultHeader, headersMap, retryLimit, thread, timeout, ulimit, verbose }; if (typeof directory !== 'string' || typeof prefix !== 'string') { throw new Error('syncDir: Incorrect input!'); } if (retryLimit && Number.isInteger(retryLimit) && trial > retryLimit) { if (verbose) { console.error('syncDir: Retry limit exceeded!'); } throw new Error('Retry limit exceeded!'); } let localFilesMap = new Map(); let cloudFilesMap = new Map(); // 1. Get local and cloud files, if not retrying if (!retrying) { localFilesMap = await this._getLocalFilesMap(directory, prefix, ignoreList); cloudFilesMap = await this._getCloudFilesMap(prefix, options); if (verbose) { console.log(`Local files: ${localFilesMap.size}`); console.log(`Cloud files: ${cloudFilesMap.size}`); } // 2. Prepare a list of files to upload for (const f of localFilesMap.values()) { const existed = cloudFilesMap.get(f.dst); if (existed) { // sometimes, clocks in oss servers are 2s slower, // adding the round off error, so about 5s is needed if ((0, _moment.default)(f.mtime).isAfter((0, _moment.default)(existed.lastModified).add(5, 's'))) { uploadFilesMap.set(f.dst, f); } } else { uploadFilesMap.set(f.dst, f); } } // 3. Prepare a list of files to remove if (remove) { for (const f of cloudFilesMap.values()) { const existed = localFilesMap.get(f.name); if (!existed) { deleteFilesMap.set(f.name, f); } } } } // 3. Put a list of files to OSS if (verbose) { console.log(`Files to upload: ${uploadFilesMap.size}`); } try { await this.putList([...uploadFilesMap.values()], options, { putResultsMap, checkPointMap, uploadFilesMap }); } catch (err) { // catch the request or response or timeout errors, and re-try if (err && (err.name === 'ResponseTimeoutError' || err.name === 'ConnectionTimeoutError' || err.name === 'RequestError' || err.name === 'ResponseError' || err.name === 'NoSuchUploadError')) { if (verbose) { console.log(`Upload ${err.name}, retrying...`); } if (err.name === 'NoSuchUploadError') { err.checkPointMap.delete(err.params.object); } trial++; return this.syncDir(directory, prefix, options, { retrying: true, trial, putResultsMap, checkPointMap: err.checkPointMap, uploadFilesMap, deleteFilesMap }); } else { throw err; } } if (remove) { if (verbose) { console.log(`Files to delete: ${deleteFilesMap.size}`); } // 5. Delete a list of files from OSS try { await this.deleteList([...deleteFilesMap.values()], options, { deleteResults, deleteFilesMap }); } catch (err) { // catch the request or response or timeout errors, and re-try if (err && (err.name === 'ResponseTimeoutError' || err.name === 'ConnectionTimeoutError' || err.name === 'RequestError' || err.name === 'ResponseError')) { if (verbose) { console.log(`Delete ${err.name}, retrying...`); } trial++; return this.syncDir(directory, prefix, options, { retrying: true, trial, putResultsMap, deleteResults, checkPointMap, uploadFilesMap, deleteFilesMap }); } else { throw err; } } } return { put: (0, _lodash.uniqBy)([...putResultsMap.values()], 'name'), delete: (0, _lodash.uniqBy)(deleteResults, 'res.requestUrls[0]') }; } /** * Recursively download from the prefix and store all files in a local directory. */ async syncDirDown(prefix, directory, { remove = true, thread = 10, timeout = 120 * 1000, ulimit = 1024, verbose = false } = {}) { const options = { remove, thread, timeout, ulimit, verbose }; let localFilesMap = new Map(); let cloudFilesMap = new Map(); const downloadFilesMap = new Map(); const deleteFilesMap = new Map(); async function getFile(file) { const localFilePath = `${directory}${file.name.substr(prefix.length)}`; await _fsExtraPromise.default.ensureFileAsync(localFilePath); await this.get(file.name, localFilePath); if (verbose) { downloadFilesMap.delete(file.name); console.log(`Downloaded: ${localFilePath}, Remaining: ${downloadFilesMap.size}`); } return file.name; } async function deleteFile(file, done) { try { await _fsExtraPromise.default.removeAsync(file.src); if (verbose) { deleteFilesMap.delete(file.dst); console.log(`Deleted: ${file.src}, Remaining: ${deleteFilesMap.size}`); } done(null, file.src); } catch (err) { done(err); } } if (typeof directory !== 'string' || typeof prefix !== 'string') { throw new Error('syncDirDown: Incorrect input!'); } // 1. Get local and cloud files cloudFilesMap = await this._getCloudFilesMap(prefix, options); await _fsExtraPromise.default.ensureDirAsync(directory); localFilesMap = await this._getLocalFilesMap(directory, prefix); // 2. Prepare a list of cloud files to download for (const f of cloudFilesMap.values()) { const existed = localFilesMap.get(f.name); if (existed) { if ((0, _moment.default)(f.lastModified).isAfter((0, _moment.default)(existed.mtime).add(1, 's'))) { downloadFilesMap.set(f.name, f); } } else { downloadFilesMap.set(f.name, f); } } // 3. Prepare a list of local files to remove if (remove) { for (const f of localFilesMap.values()) { const existed = cloudFilesMap.get(f.dst); if (!existed) { deleteFilesMap.set(f.dst, f); } } } const downloadFiles = (0, _lodash.sortBy)([...downloadFilesMap.values()], 'name'); const getResults = await _async.default.mapLimit(downloadFiles, thread, getFile.bind(this)); if (remove) { const deleteResults = await _async.default.mapLimit(deleteFilesMap.values(), thread, deleteFile.bind(this)); return { get: getResults, delete: deleteResults }; } else { return { get: getResults }; } } /** * Get all the files of a directory recursively. * Return [] if not found. */ async listDir(prefix, projection = []) { if (typeof prefix !== 'string') { throw new Error('listDir: Incorrect input!'); } const query = { prefix, 'max-keys': 1000 }; let result = await this.list(query); if (!result.objects) { return []; } function project(files) { if (projection.length) { return files.map(f => (0, _lodash.pick)(f, projection)); } return files; } let allFiles = [...project(result.objects)]; while (result.nextMarker) { query.marker = result.nextMarker; result = await this.list(query); allFiles = [...allFiles, ...project(result.objects)]; } return allFiles; } /** * Delete a directory on OSS recursively. */ async deleteDir(prefix, { retryLimit = null } = {}, meta = {}) { if (typeof prefix !== 'string') { throw new Error('deleteDir: Incorrect input!'); } const trial = (meta.trial || 0) + 1; if (retryLimit && Number.isInteger(retryLimit) && trial > retryLimit) { throw new Error('Retry limit exceeded!'); } let objects = []; try { objects = (await this.listDir(prefix, ['name'])).map(x => x.name); } catch (err) { if (err && (err.name === 'ResponseTimeoutError' || err.name === 'ConnectionTimeoutError' || err.name === 'RequestError' || err.name === 'ResponseError')) { return this.deleteDir(prefix, { retryLimit }, { trial }); } else { throw err; } } let results = []; const cargo = _async.default.cargo(async (tasks, done) => { try { const data = await this.deleteMulti(tasks); results = [...results, ...data.deleted]; done(null, data); } catch (err) { if (err && (err.name === 'ResponseTimeoutError' || err.name === 'ConnectionTimeoutError' || err.name === 'RequestError' || err.name === 'ResponseError')) { return this.deleteDir(prefix, { retryLimit }, { trial }); } else { throw err; } } }, 1000); cargo.push(objects); await cargo.drain(); return results; } /** * Set the content-disposition header of a file. */ async setDownloadName(file, downloadName) { if (typeof file !== 'string' || typeof downloadName !== 'string') { throw new Error('setDownloadName: Incorrect input!'); } const options = { headers: { 'Content-Type': 'binary/octet-stream', 'Content-Disposition': `attachment; filename="${encodeURIComponent(downloadName)}"` } }; return this.copy(file, file, options); } } var _default = OSSExtra; exports.default = _default;