UNPKG

10.4 kBJavaScriptView Raw
1const debug = require('debug')('ali-oss:object');
2const fs = require('fs');
3const is = require('is-type-of');
4const copy = require('copy-to');
5const path = require('path');
6const mime = require('mime');
7const callback = require('./common/callback');
8const { Transform } = require('stream');
9const pump = require('pump');
10const { isBuffer } = require('./common/utils/isBuffer');
11
12const proto = exports;
13
14/**
15 * Object operations
16 */
17
18/**
19 * append an object from String(file path)/Buffer/ReadableStream
20 * @param {String} name the object key
21 * @param {Mixed} file String(file path)/Buffer/ReadableStream
22 * @param {Object} options
23 * @return {Object}
24 */
25proto.append = async function append(name, file, options) {
26 options = options || {};
27 if (options.position === undefined) options.position = '0';
28 options.subres = {
29 append: '',
30 position: options.position
31 };
32 options.method = 'POST';
33
34 const result = await this.put(name, file, options);
35 result.nextAppendPosition = result.res.headers['x-oss-next-append-position'];
36 return result;
37};
38
39/**
40 * put an object from String(file path)/Buffer/ReadableStream
41 * @param {String} name the object key
42 * @param {Mixed} file String(file path)/Buffer/ReadableStream
43 * @param {Object} options
44 * {Object} options.callback The callback parameter is composed of a JSON string encoded in Base64
45 * {String} options.callback.url the OSS sends a callback request to this URL
46 * {String} options.callback.host The host header value for initiating callback requests
47 * {String} options.callback.body The value of the request body when a callback is initiated
48 * {String} options.callback.contentType The Content-Type of the callback requests initiatiated
49 * {Object} options.callback.customValue Custom parameters are a map of key-values, e.g:
50 * customValue = {
51 * key1: 'value1',
52 * key2: 'value2'
53 * }
54 * @return {Object}
55 */
56proto.put = async function put(name, file, options) {
57 let content;
58 options = options || {};
59 name = this._objectName(name);
60
61 if (isBuffer(file)) {
62 content = file;
63 } else if (is.string(file)) {
64 const stats = fs.statSync(file);
65 if (!stats.isFile()) {
66 throw new Error(`${file} is not file`);
67 }
68 options.mime = options.mime || mime.getType(path.extname(file));
69 const stream = fs.createReadStream(file);
70 options.contentLength = await this._getFileSize(file);
71 return await this.putStream(name, stream, options);
72 } else if (is.readableStream(file)) {
73 return await this.putStream(name, file, options);
74 } else {
75 throw new TypeError('Must provide String/Buffer/ReadableStream for put.');
76 }
77
78 options.headers = options.headers || {};
79 this._convertMetaToHeaders(options.meta, options.headers);
80
81 const method = options.method || 'PUT';
82 const params = this._objectRequestParams(method, name, options);
83
84 callback.encodeCallback(params, options);
85
86 params.mime = options.mime;
87 params.content = content;
88 params.successStatuses = [200];
89
90 const result = await this.request(params);
91
92 const ret = {
93 name,
94 url: this._objectUrl(name),
95 res: result.res
96 };
97
98 if (params.headers && params.headers['x-oss-callback']) {
99 ret.data = JSON.parse(result.data.toString());
100 }
101
102 return ret;
103};
104
105/**
106 * put an object from ReadableStream. If `options.contentLength` is
107 * not provided, chunked encoding is used.
108 * @param {String} name the object key
109 * @param {Readable} stream the ReadableStream
110 * @param {Object} options
111 * @return {Object}
112 */
113proto.putStream = async function putStream(name, stream, options) {
114 options = options || {};
115 options.headers = options.headers || {};
116 name = this._objectName(name);
117 if (options.contentLength) {
118 options.headers['Content-Length'] = options.contentLength;
119 } else {
120 options.headers['Transfer-Encoding'] = 'chunked';
121 }
122 this._convertMetaToHeaders(options.meta, options.headers);
123
124 const method = options.method || 'PUT';
125 const params = this._objectRequestParams(method, name, options);
126 callback.encodeCallback(params, options);
127 params.mime = options.mime;
128 const transform = new Transform();
129 // must remove http stream header for signature
130 transform._transform = function _transform(chunk, encoding, done) {
131 this.push(chunk);
132 done();
133 };
134 params.stream = pump(stream, transform);
135 params.successStatuses = [200];
136
137 const result = await this.request(params);
138
139 const ret = {
140 name,
141 url: this._objectUrl(name),
142 res: result.res
143 };
144
145 if (params.headers && params.headers['x-oss-callback']) {
146 ret.data = JSON.parse(result.data.toString());
147 }
148
149 return ret;
150};
151
152proto.getStream = async function getStream(name, options) {
153 options = options || {};
154
155 if (options.process) {
156 options.subres = options.subres || {};
157 options.subres['x-oss-process'] = options.process;
158 }
159
160 const params = this._objectRequestParams('GET', name, options);
161 params.customResponse = true;
162 params.successStatuses = [200, 206, 304];
163
164 const result = await this.request(params);
165
166 return {
167 stream: result.res,
168 res: {
169 status: result.status,
170 headers: result.headers
171 }
172 };
173};
174
175proto.putMeta = async function putMeta(name, meta, options) {
176 return await this.copy(name, name, {
177 meta: meta || {},
178 timeout: options && options.timeout,
179 ctx: options && options.ctx
180 });
181};
182
183proto.list = async function list(query, options) {
184 // prefix, marker, max-keys, delimiter
185
186 const params = this._objectRequestParams('GET', '', options);
187 params.query = query;
188 params.xmlResponse = true;
189 params.successStatuses = [200];
190
191 const result = await this.request(params);
192 let objects = result.data.Contents;
193 const that = this;
194 if (objects) {
195 if (!Array.isArray(objects)) {
196 objects = [objects];
197 }
198 objects = objects.map(obj => ({
199 name: obj.Key,
200 url: that._objectUrl(obj.Key),
201 lastModified: obj.LastModified,
202 etag: obj.ETag,
203 type: obj.Type,
204 size: Number(obj.Size),
205 storageClass: obj.StorageClass,
206 owner: {
207 id: obj.Owner.ID,
208 displayName: obj.Owner.DisplayName
209 }
210 }));
211 }
212 let prefixes = result.data.CommonPrefixes || null;
213 if (prefixes) {
214 if (!Array.isArray(prefixes)) {
215 prefixes = [prefixes];
216 }
217 prefixes = prefixes.map(item => item.Prefix);
218 }
219 return {
220 res: result.res,
221 objects,
222 prefixes,
223 nextMarker: result.data.NextMarker || null,
224 isTruncated: result.data.IsTruncated === 'true'
225 };
226};
227
228proto.listV2 = async function listV2(query, options = {}) {
229 const continuation_token = query['continuation-token'] || query.continuationToken;
230 delete query['continuation-token'];
231 delete query.continuationToken;
232 if (continuation_token) {
233 options.subres = Object.assign(
234 {
235 'continuation-token': continuation_token
236 },
237 options.subres
238 );
239 }
240 const params = this._objectRequestParams('GET', '', options);
241 params.query = {
242 'list-type': 2,
243 ...query
244 };
245 params.xmlResponse = true;
246 params.successStatuses = [200];
247
248 const result = await this.request(params);
249 let objects = result.data.Contents;
250 const that = this;
251 if (objects) {
252 if (!Array.isArray(objects)) {
253 objects = [objects];
254 }
255 objects = objects.map(obj => ({
256 name: obj.Key,
257 url: that._objectUrl(obj.Key),
258 lastModified: obj.LastModified,
259 etag: obj.ETag,
260 type: obj.Type,
261 size: Number(obj.Size),
262 storageClass: obj.StorageClass,
263 owner: obj.Owner
264 ? {
265 id: obj.Owner.ID,
266 displayName: obj.Owner.DisplayName
267 }
268 : null
269 }));
270 }
271 let prefixes = result.data.CommonPrefixes || null;
272 if (prefixes) {
273 if (!Array.isArray(prefixes)) {
274 prefixes = [prefixes];
275 }
276 prefixes = prefixes.map(item => item.Prefix);
277 }
278 return {
279 res: result.res,
280 objects,
281 prefixes,
282 isTruncated: result.data.IsTruncated === 'true',
283 keyCount: +result.data.KeyCount,
284 continuationToken: result.data.ContinuationToken || null,
285 nextContinuationToken: result.data.NextContinuationToken || null
286 };
287};
288
289/**
290 * Restore Object
291 * @param {String} name the object key
292 * @param {Object} options
293 * @returns {{res}}
294 */
295proto.restore = async function restore(name, options) {
296 options = options || {};
297 options.subres = Object.assign({ restore: '' }, options.subres);
298 if (options.versionId) {
299 options.subres.versionId = options.versionId;
300 }
301 const params = this._objectRequestParams('POST', name, options);
302 params.successStatuses = [202];
303
304 const result = await this.request(params);
305
306 return {
307 res: result.res
308 };
309};
310
311proto._objectUrl = function _objectUrl(name) {
312 return this._getReqUrl({ bucket: this.options.bucket, object: name });
313};
314
315/**
316 * generator request params
317 * @return {Object} params
318 *
319 * @api private
320 */
321
322proto._objectRequestParams = function (method, name, options) {
323 if (!this.options.bucket && !this.options.cname) {
324 throw new Error('Please create a bucket first');
325 }
326
327 options = options || {};
328 name = this._objectName(name);
329 const params = {
330 object: name,
331 bucket: this.options.bucket,
332 method,
333 subres: options && options.subres,
334 timeout: options && options.timeout,
335 ctx: options && options.ctx
336 };
337
338 if (options.headers) {
339 params.headers = {};
340 copy(options.headers).to(params.headers);
341 }
342 return params;
343};
344
345proto._objectName = function (name) {
346 return name.replace(/^\/+/, '');
347};
348
349proto._statFile = function (filepath) {
350 return new Promise((resolve, reject) => {
351 fs.stat(filepath, (err, stats) => {
352 if (err) {
353 reject(err);
354 } else {
355 resolve(stats);
356 }
357 });
358 });
359};
360
361proto._convertMetaToHeaders = function (meta, headers) {
362 if (!meta) {
363 return;
364 }
365
366 Object.keys(meta).forEach(k => {
367 headers[`x-oss-meta-${k}`] = meta[k];
368 });
369};
370
371proto._deleteFileSafe = function (filepath) {
372 return new Promise(resolve => {
373 fs.exists(filepath, exists => {
374 if (!exists) {
375 resolve();
376 } else {
377 fs.unlink(filepath, err => {
378 if (err) {
379 debug('unlink %j error: %s', filepath, err);
380 }
381 resolve();
382 });
383 }
384 });
385 });
386};