| 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 { parseRestoreInfo } = require('./common/utils/parseRestoreInfo'); | |
| 
 | |
| 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 | |
|       }, | |
|       restoreInfo: parseRestoreInfo(obj.RestoreInfo) | |
|     })); | |
|   } | |
|   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, | |
|         restoreInfo: parseRestoreInfo(obj.RestoreInfo) | |
|       }; | |
|     }); | |
|   } | |
|   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); | |
|   const paramsXMLObj = { | |
|     RestoreRequest: { | |
|       Days: options.Days ? options.Days : 2 | |
|     } | |
|   }; | |
| 
 | |
|   if (options.type === 'ColdArchive' || options.type === 'DeepColdArchive') { | |
|     paramsXMLObj.RestoreRequest.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(); | |
|         }); | |
|       } | |
|     }); | |
|   }); | |
| };
 |