const debug = require('debug')('ali-oss:object'); const fs = require('fs'); const is = require('is-type-of'); const copy = require('copy-to'); const path = require('path'); const mime = require('mime'); const callback = require('./common/callback'); const { Transform } = require('stream'); const pump = require('pump'); const { isBuffer } = require('./common/utils/isBuffer'); const { retry } = require('./common/utils/retry'); const { obj2xml } = require('./common/utils/obj2xml'); const proto = exports; /** * Object operations */ /** * append an object from String(file path)/Buffer/ReadableStream * @param {String} name the object key * @param {Mixed} file String(file path)/Buffer/ReadableStream * @param {Object} options * @return {Object} */ proto.append = async function append(name, file, options) { options = options || {}; if (options.position === undefined) options.position = '0'; options.subres = { append: '', position: options.position }; options.method = 'POST'; const result = await this.put(name, file, options); result.nextAppendPosition = result.res.headers['x-oss-next-append-position']; return result; }; /** * put an object from String(file path)/Buffer/ReadableStream * @param {String} name the object key * @param {Mixed} file String(file path)/Buffer/ReadableStream * @param {Object} options * {Object} [options.callback] The callback parameter is composed of a JSON string encoded in Base64 * {String} options.callback.url the OSS sends a callback request to this URL * {String} [options.callback.host] The host header value for initiating callback requests * {String} options.callback.body The value of the request body when a callback is initiated * {String} [options.callback.contentType] The Content-Type of the callback requests initiated * {Boolean} [options.callback.callbackSNI] Whether OSS sends SNI to the origin address specified by callbackUrl when a callback request is initiated from the client * {Object} [options.callback.customValue] Custom parameters are a map of key-values, e.g: * customValue = { * key1: 'value1', * key2: 'value2' * } * @return {Object} */ proto.put = async function put(name, file, options) { let content; options = options || {}; name = this._objectName(name); if (isBuffer(file)) { content = file; } else if (is.string(file)) { const stats = fs.statSync(file); if (!stats.isFile()) { throw new Error(`${file} is not file`); } options.mime = options.mime || mime.getType(path.extname(file)); options.contentLength = await this._getFileSize(file); const getStream = () => fs.createReadStream(file); const putStreamStb = (objectName, makeStream, configOption) => { return this.putStream(objectName, makeStream(), configOption); }; return await retry(putStreamStb, this.options.retryMax, { errorHandler: err => { const _errHandle = _err => { const statusErr = [-1, -2].includes(_err.status); const requestErrorRetryHandle = this.options.requestErrorRetryHandle || (() => true); return statusErr && requestErrorRetryHandle(_err); }; if (_errHandle(err)) return true; return false; } })(name, getStream, options); } else if (is.readableStream(file)) { return await this.putStream(name, file, options); } else { throw new TypeError('Must provide String/Buffer/ReadableStream for put.'); } options.headers = options.headers || {}; this._convertMetaToHeaders(options.meta, options.headers); const method = options.method || 'PUT'; const params = this._objectRequestParams(method, name, options); callback.encodeCallback(params, options); params.mime = options.mime; params.content = content; params.successStatuses = [200]; const result = await this.request(params); const ret = { name, url: this._objectUrl(name), res: result.res }; if (params.headers && params.headers['x-oss-callback']) { ret.data = JSON.parse(result.data.toString()); } return ret; }; /** * put an object from ReadableStream. If `options.contentLength` is * not provided, chunked encoding is used. * @param {String} name the object key * @param {Readable} stream the ReadableStream * @param {Object} options * @return {Object} */ proto.putStream = async function putStream(name, stream, options) { options = options || {}; options.headers = options.headers || {}; name = this._objectName(name); if (options.contentLength) { options.headers['Content-Length'] = options.contentLength; } else { options.headers['Transfer-Encoding'] = 'chunked'; } this._convertMetaToHeaders(options.meta, options.headers); const method = options.method || 'PUT'; const params = this._objectRequestParams(method, name, options); callback.encodeCallback(params, options); params.mime = options.mime; const transform = new Transform(); // must remove http stream header for signature transform._transform = function _transform(chunk, encoding, done) { this.push(chunk); done(); }; params.stream = pump(stream, transform); params.successStatuses = [200]; const result = await this.request(params); const ret = { name, url: this._objectUrl(name), res: result.res }; if (params.headers && params.headers['x-oss-callback']) { ret.data = JSON.parse(result.data.toString()); } return ret; }; proto.getStream = async function getStream(name, options) { options = options || {}; if (options.process) { options.subres = options.subres || {}; options.subres['x-oss-process'] = options.process; } const params = this._objectRequestParams('GET', name, options); params.customResponse = true; params.successStatuses = [200, 206, 304]; const result = await this.request(params); return { stream: result.res, res: { status: result.status, headers: result.headers } }; }; proto.putMeta = async function putMeta(name, meta, options) { return await this.copy(name, name, { meta: meta || {}, timeout: options && options.timeout, ctx: options && options.ctx }); }; proto.list = async function list(query, options) { // prefix, marker, max-keys, delimiter const params = this._objectRequestParams('GET', '', options); params.query = query; params.xmlResponse = true; params.successStatuses = [200]; const result = await this.request(params); let objects = result.data.Contents || []; const that = this; if (objects) { if (!Array.isArray(objects)) { objects = [objects]; } objects = objects.map(obj => ({ name: obj.Key, url: that._objectUrl(obj.Key), lastModified: obj.LastModified, etag: obj.ETag, type: obj.Type, size: Number(obj.Size), storageClass: obj.StorageClass, owner: { id: obj.Owner.ID, displayName: obj.Owner.DisplayName } })); } let prefixes = result.data.CommonPrefixes || null; if (prefixes) { if (!Array.isArray(prefixes)) { prefixes = [prefixes]; } prefixes = prefixes.map(item => item.Prefix); } return { res: result.res, objects, prefixes, nextMarker: result.data.NextMarker || null, isTruncated: result.data.IsTruncated === 'true' }; }; proto.listV2 = async function listV2(query = {}, options = {}) { const continuation_token = query['continuation-token'] || query.continuationToken; delete query['continuation-token']; delete query.continuationToken; if (continuation_token) { options.subres = Object.assign( { 'continuation-token': continuation_token }, options.subres ); } const params = this._objectRequestParams('GET', '', options); params.query = Object.assign({ 'list-type': 2 }, query); delete params.query['continuation-token']; delete query.continuationToken; params.xmlResponse = true; params.successStatuses = [200]; const result = await this.request(params); let objects = result.data.Contents || []; const that = this; if (objects) { if (!Array.isArray(objects)) { objects = [objects]; } objects = objects.map(obj => { let owner = null; if (obj.Owner) { owner = { id: obj.Owner.ID, displayName: obj.Owner.DisplayName }; } return { name: obj.Key, url: that._objectUrl(obj.Key), lastModified: obj.LastModified, etag: obj.ETag, type: obj.Type, size: Number(obj.Size), storageClass: obj.StorageClass, owner }; }); } let prefixes = result.data.CommonPrefixes || null; if (prefixes) { if (!Array.isArray(prefixes)) { prefixes = [prefixes]; } prefixes = prefixes.map(item => item.Prefix); } return { res: result.res, objects, prefixes, isTruncated: result.data.IsTruncated === 'true', keyCount: +result.data.KeyCount, continuationToken: result.data.ContinuationToken || null, nextContinuationToken: result.data.NextContinuationToken || null }; }; /** * Restore Object * @param {String} name the object key * @param {Object} options {type : Archive or ColdArchive} * @returns {{res}} */ proto.restore = async function restore(name, options = { type: 'Archive' }) { options = options || {}; options.subres = Object.assign({ restore: '' }, options.subres); if (options.versionId) { options.subres.versionId = options.versionId; } const params = this._objectRequestParams('POST', name, options); if (options.type === 'ColdArchive') { const paramsXMLObj = { RestoreRequest: { Days: options.Days ? options.Days : 2, JobParameters: { Tier: options.JobParameters ? options.JobParameters : 'Standard' } } }; params.content = obj2xml(paramsXMLObj, { headers: true }); params.mime = 'xml'; } params.successStatuses = [202]; const result = await this.request(params); return { res: result.res }; }; proto._objectUrl = function _objectUrl(name) { return this._getReqUrl({ bucket: this.options.bucket, object: name }); }; /** * generator request params * @return {Object} params * * @api private */ proto._objectRequestParams = function (method, name, options) { if (!this.options.bucket && !this.options.cname) { throw new Error('Please create a bucket first'); } options = options || {}; name = this._objectName(name); const params = { object: name, bucket: this.options.bucket, method, subres: options && options.subres, additionalHeaders: options && options.additionalHeaders, timeout: options && options.timeout, ctx: options && options.ctx }; if (options.headers) { params.headers = {}; copy(options.headers).to(params.headers); } return params; }; proto._objectName = function (name) { return name.replace(/^\/+/, ''); }; proto._statFile = function (filepath) { return new Promise((resolve, reject) => { fs.stat(filepath, (err, stats) => { if (err) { reject(err); } else { resolve(stats); } }); }); }; proto._convertMetaToHeaders = function (meta, headers) { if (!meta) { return; } Object.keys(meta).forEach(k => { headers[`x-oss-meta-${k}`] = meta[k]; }); }; proto._deleteFileSafe = function (filepath) { return new Promise(resolve => { fs.exists(filepath, exists => { if (!exists) { resolve(); } else { fs.unlink(filepath, err => { if (err) { debug('unlink %j error: %s', filepath, err); } resolve(); }); } }); }); };