569 lines
14 KiB
JavaScript
Raw Normal View History

2025-04-24 17:03:28 +08:00
'use strict';
var debug = require('debug')('ali-oss:object');
var utility = require('utility');
// var crypto = require('crypto');
var fs = require('fs');
var is = require('is-type-of');
// var eoe = require('end-or-error');
var urlutil = require('url');
var copy = require('copy-to');
var path = require('path');
var mime = require('mime');
var callback = require('../common/callback');
// var assert = require('assert');
var 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 = function* (name, file, options) {
options = options || {};
if (options.position === undefined) options.position = '0';
options.subres = {
append: '',
position: options.position,
};
options.method = 'POST';
var result = yield 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 initiatiated
* {Object} options.callback.customValue Custom parameters are a map of key-values, e.g:
* customValue = {
* key1: 'value1',
* key2: 'value2'
* }
* @return {Object}
*/
proto.put = function* put(name, file, options) {
var content;
options = options || {};
if (is.buffer(file)) {
content = file;
} else if (is.string(file)) {
options.mime = options.mime || mime.getType(path.extname(file));
var stream = fs.createReadStream(file);
options.contentLength = yield this._getFileSize(file);
return yield this.putStream(name, stream, options);
} else if (is.readableStream(file)) {
return yield 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);
var method = options.method || 'PUT';
var params = this._objectRequestParams(method, name, options);
params.mime = options.mime;
params.content = content;
params.successStatuses = [200];
callback.encodeCallback(options);
var result = yield this.request(params);
var ret = {
name: name,
url: this._objectUrl(name),
res: result.res,
};
if (options.headers && options.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 = function* putStream(name, stream, options) {
options = options || {};
options.headers = options.headers || {};
if (options.contentLength) {
options.headers['Content-Length'] = options.contentLength;
} else {
options.headers['Transfer-Encoding'] = 'chunked';
}
this._convertMetaToHeaders(options.meta, options.headers);
var method = options.method || 'PUT';
var params = this._objectRequestParams(method, name, options);
params.mime = options.mime;
params.stream = stream;
params.successStatuses = [200];
callback.encodeCallback(options);
var result = yield this.request(params);
var ret = {
name: name,
url: this._objectUrl(name),
res: result.res,
};
if (options.headers && options.headers['x-oss-callback']) {
ret.data = JSON.parse(result.data.toString());
}
return ret;
};
proto.head = function* head(name, options) {
var params = this._objectRequestParams('HEAD', name, options);
params.successStatuses = [200, 304];
var result = yield this.request(params);
var data = {
meta: null,
res: result.res,
status: result.status
};
if (result.status === 200) {
for (var k in result.headers) {
if (k.indexOf('x-oss-meta-') === 0) {
if (!data.meta) {
data.meta = {};
}
data.meta[k.substring(11)] = result.headers[k];
}
}
}
return data;
};
proto.get = function* get(name, file, options) {
var writeStream = null;
var needDestroy = false;
if (is.writableStream(file)) {
writeStream = file;
} else if (is.string(file)) {
writeStream = fs.createWriteStream(file);
needDestroy = true;
} else {
// get(name, options)
options = file;
}
options = options || {};
if (options.process) {
options.subres = options.subres || {};
options.subres['x-oss-process'] = options.process;
}
var result;
try {
var params = this._objectRequestParams('GET', name, options);
params.writeStream = writeStream;
params.successStatuses = [200, 206, 304];
result = yield this.request(params);
if (needDestroy) {
writeStream.destroy();
}
} catch (err) {
if (needDestroy) {
writeStream.destroy();
// should delete the exists file before throw error
debug('get error: %s, delete the exists file %s', err, file);
yield this._deleteFileSafe(file);
}
throw err;
}
return {
res: result.res,
content: result.data
};
};
proto.getStream = function* getStream(name, options) {
options = options || {};
var params = this._objectRequestParams('GET', name, options);
params.customResponse = true;
params.successStatuses = [200, 206, 304];
var result = yield this.request(params);
return {
stream: result.res,
res: {
status: result.status,
headers: result.headers
}
};
};
proto.delete = function* _delete(name, options) {
var params = this._objectRequestParams('DELETE', name, options);
params.successStatuses = [204];
var result = yield this.request(params);
return {
res: result.res
};
};
proto.deleteMulti = function* deleteMulti(names, options) {
options = options || {};
var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<Delete>\n';
if (options.quiet) {
xml += ' <Quiet>true</Quiet>\n';
} else {
xml += ' <Quiet>false</Quiet>\n';
}
for (var i = 0; i < names.length; i++) {
xml += ' <Object><Key>' +
utility.escape(this._objectName(names[i])) + '</Key></Object>\n';
}
xml += '</Delete>';
debug('delete multi objects: %s', xml);
options.subres = 'delete';
var params = this._objectRequestParams('POST', '', options);
params.mime = 'xml';
params.content = xml;
params.xmlResponse = true;
params.successStatuses = [200];
var result = yield this.request(params);
var r = result.data;
var deleted = r && r.Deleted || null;
if (deleted) {
if (!Array.isArray(deleted)) {
deleted = [deleted];
}
deleted = deleted.map(function (item) {
return item.Key;
});
}
return {
res: result.res,
deleted: deleted
};
};
proto.copy = function* copy(name, sourceName, options) {
options = options || {};
options.headers = options.headers || {};
for (var k in options.headers) {
options.headers['x-oss-copy-source-' + k.toLowerCase()] = options.headers[k];
}
if (options.meta) {
options.headers['x-oss-metadata-directive'] = 'REPLACE';
}
this._convertMetaToHeaders(options.meta, options.headers);
if (sourceName[0] !== '/') {
// no specify bucket name
sourceName = '/' + this.options.bucket + '/' + encodeURIComponent(sourceName);
} else {
sourceName = '/' + encodeURIComponent(sourceName.slice(1));
}
options.headers['x-oss-copy-source'] = sourceName;
var params = this._objectRequestParams('PUT', name, options);
params.xmlResponse = true;
params.successStatuses = [200, 304];
var result = yield this.request(params);
var data = result.data;
if (data) {
data = {
etag: data.ETag,
lastModified: data.LastModified,
};
}
return {
data: data,
res: result.res
};
};
proto.putMeta = function* putMeta(name, meta, options) {
return yield this.copy(name, name, {
meta: meta || {},
timeout: options && options.timeout,
ctx: options && options.ctx,
});
};
proto.list = function* list(query, options) {
// prefix, marker, max-keys, delimiter
var params = this._objectRequestParams('GET', '', options);
params.query = query;
params.xmlResponse = true;
params.successStatuses = [200];
var result = yield this.request(params);
var objects = result.data.Contents;
var that = this;
if (objects) {
if (!Array.isArray(objects)) {
objects = [objects];
}
objects = objects.map(function (obj) {
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: {
id: obj.Owner.ID,
displayName: obj.Owner.DisplayName,
}
};
});
}
var prefixes = result.data.CommonPrefixes || null;
if (prefixes) {
if (!Array.isArray(prefixes)) {
prefixes = [prefixes];
}
prefixes = prefixes.map(function (item) {
return item.Prefix;
});
}
return {
res: result.res,
objects: objects,
prefixes: prefixes,
nextMarker: result.data.NextMarker || null,
isTruncated: result.data.IsTruncated === 'true'
};
};
/*
* Set object's ACL
* @param {String} name the object key
* @param {String} acl the object ACL
* @param {Object} options
*/
proto.putACL = function* putACL(name, acl, options) {
options = options || {};
options.subres = 'acl';
options.headers = options.headers || {};
options.headers['x-oss-object-acl'] = acl;
name = this._objectName(name);
var params = this._objectRequestParams('PUT', name, options);
params.successStatuses = [200];
var result = yield this.request(params);
return {
res: result.res
};
};
/*
* Get object's ACL
* @param {String} name the object key
* @param {Object} options
* @return {Object}
*/
proto.getACL = function* getACL(name, options) {
options = options || {};
options.subres = 'acl';
name = this._objectName(name);
var params = this._objectRequestParams('GET', name, options);
params.successStatuses = [200];
params.xmlResponse = true;
var result = yield this.request(params);
return {
acl: result.data.AccessControlList.Grant,
owner: {
id: result.data.Owner.ID,
displayName: result.data.Owner.DisplayName,
},
res: result.res
};
};
proto.signatureUrl = function (name, options) {
name = this._objectName(name);
var params = {
bucket: this.options.bucket,
object: name
};
options = options || {};
var expires = utility.timestamp() + (options.expires || 1800);
var resource = this._getResource(params);
var query = {};
var signList = [];
for (var k in options.response) {
var key = 'response-' + k.toLowerCase();
query[key] = options.response[k];
signList.push(key + '=' + options.response[k]);
}
if (this.options.stsToken) {
query['security-token'] = this.options.stsToken;
signList.push('security-token=' + this.options.stsToken);
}
if (options.process){
var processKeyword = 'x-oss-process';
query[processKeyword] = options.process;
var item = processKeyword + '=' + options.process;
signList.push(item);
}
if (signList.length > 0) {
signList.sort();
resource += '?' + signList.join('&');
}
var stringToSign = [
options.method || 'GET',
options['content-md5'] || '', // Content-MD5
options['content-type'] || '', // Content-Type
expires,
resource
].join('\n');
var signature = this.signature(stringToSign);
var url = urlutil.parse(this._getReqUrl(params));
url.query = {
OSSAccessKeyId: this.options.accessKeyId,
Expires: expires,
Signature: signature
};
copy(query).to(url.query);
return url.format();
};
/**
* Get Object url by name
* @param {String} name - object name
* @param {String} [baseUrl] - If provide `baseUrl`, will use `baseUrl` instead the default `endpoint`.
* @return {String} object url
*/
proto.getObjectUrl = function (name, baseUrl) {
if (!baseUrl) {
baseUrl = this.options.endpoint.format();
} else if (baseUrl[baseUrl.length - 1] !== '/') {
baseUrl += '/';
}
return baseUrl + this._escape(this._objectName(name));
};
proto._objectUrl = function (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) {
throw new Error('Please create a bucket first');
}
options = options || {};
name = this._objectName(name);
var params = {
object: name,
bucket: this.options.bucket,
method: method,
subres: options && options.subres,
timeout: options && options.timeout,
ctx: options && options.ctx,
};
if (options.headers) {
params.headers = options.headers;
}
return params;
};
proto._objectName = function (name) {
return name.replace(/^\/+/, '');
};
proto._statFile = function (filepath) {
return function (callback) {
fs.stat(filepath, callback);
};
};
proto._convertMetaToHeaders = function (meta, headers) {
if (!meta) {
return;
}
for (var k in meta) {
headers['x-oss-meta-' + k] = meta[k];
}
};
proto._deleteFileSafe = function (filepath) {
return function (callback) {
fs.exists(filepath, function (exists) {
if (!exists) {
return callback();
}
fs.unlink(filepath, function (err) {
if (err) {
debug('unlink %j error: %s', filepath, err);
}
callback();
});
});
};
};