UNPKG

minio

Version:

S3 Compatible Cloud Storage client

1,461 lines (1,154 loc) 144 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _exportNames = { Client: true, CopyConditions: true, PostPolicy: true }; exports.PostPolicy = exports.CopyConditions = exports.Client = void 0; var _fs = _interopRequireDefault(require("fs")); var _http = _interopRequireDefault(require("http")); var _https = _interopRequireDefault(require("https")); var _stream = _interopRequireDefault(require("stream")); var _blockStream = _interopRequireDefault(require("block-stream2")); var _xml = _interopRequireDefault(require("xml")); var _xml2js = _interopRequireDefault(require("xml2js")); var _async = _interopRequireDefault(require("async")); var _queryString = _interopRequireDefault(require("query-string")); var _mkdirp = _interopRequireDefault(require("mkdirp")); var _path = _interopRequireDefault(require("path")); var _lodash = _interopRequireDefault(require("lodash")); var _webEncoding = require("web-encoding"); var _helpers = require("./helpers.js"); var _signing = require("./signing.js"); var _objectUploader = _interopRequireDefault(require("./object-uploader")); var transformers = _interopRequireWildcard(require("./transformers")); var errors = _interopRequireWildcard(require("./errors.js")); var _s3Endpoints = require("./s3-endpoints.js"); var _notification = require("./notification"); Object.keys(_notification).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; if (key in exports && exports[key] === _notification[key]) return; Object.defineProperty(exports, key, { enumerable: true, get: function () { return _notification[key]; } }); }); var _extensions = _interopRequireDefault(require("./extensions")); var _CredentialProvider = _interopRequireDefault(require("./CredentialProvider")); var _xmlParsers = require("./xml-parsers"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* * MinIO Javascript Library for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var Package = require('../../package.json'); class Client { constructor(params) { if (typeof params.secure !== 'undefined') throw new Error('"secure" option deprecated, "useSSL" should be used instead'); // Default values if not specified. if (typeof params.useSSL === 'undefined') params.useSSL = true; if (!params.port) params.port = 0; // Validate input params. if (!(0, _helpers.isValidEndpoint)(params.endPoint)) { throw new errors.InvalidEndpointError(`Invalid endPoint : ${params.endPoint}`); } if (!(0, _helpers.isValidPort)(params.port)) { throw new errors.InvalidArgumentError(`Invalid port : ${params.port}`); } if (!(0, _helpers.isBoolean)(params.useSSL)) { throw new errors.InvalidArgumentError(`Invalid useSSL flag type : ${params.useSSL}, expected to be of type "boolean"`); } // Validate region only if its set. if (params.region) { if (!(0, _helpers.isString)(params.region)) { throw new errors.InvalidArgumentError(`Invalid region : ${params.region}`); } } var host = params.endPoint.toLowerCase(); var port = params.port; var protocol = ''; var transport; // Validate if configuration is not using SSL // for constructing relevant endpoints. if (params.useSSL === false) { transport = _http.default; protocol = 'http:'; if (port === 0) { port = 80; } } else { // Defaults to secure. transport = _https.default; protocol = 'https:'; if (port === 0) { port = 443; } } // if custom transport is set, use it. if (params.transport) { if (!(0, _helpers.isObject)(params.transport)) { throw new errors.InvalidArgumentError('Invalid transport type : ${params.transport}, expected to be type "object"'); } transport = params.transport; } // User Agent should always following the below style. // Please open an issue to discuss any new changes here. // // MinIO (OS; ARCH) LIB/VER APP/VER // var libraryComments = `(${process.platform}; ${process.arch})`; var libraryAgent = `MinIO ${libraryComments} minio-js/${Package.version}`; // User agent block ends. this.transport = transport; this.host = host; this.port = port; this.protocol = protocol; this.accessKey = params.accessKey; this.secretKey = params.secretKey; this.sessionToken = params.sessionToken; this.userAgent = `${libraryAgent}`; // Default path style is true if (params.pathStyle === undefined) { this.pathStyle = true; } else { this.pathStyle = params.pathStyle; } if (!this.accessKey) this.accessKey = ''; if (!this.secretKey) this.secretKey = ''; this.anonymous = !this.accessKey || !this.secretKey; if (params.credentialsProvider) { this.credentialsProvider = params.credentialsProvider; this.checkAndRefreshCreds(); } this.regionMap = {}; if (params.region) { this.region = params.region; } this.partSize = 64 * 1024 * 1024; if (params.partSize) { this.partSize = params.partSize; this.overRidePartSize = true; } if (this.partSize < 5 * 1024 * 1024) { throw new errors.InvalidArgumentError(`Part size should be greater than 5MB`); } if (this.partSize > 5 * 1024 * 1024 * 1024) { throw new errors.InvalidArgumentError(`Part size should be less than 5GB`); } this.maximumPartSize = 5 * 1024 * 1024 * 1024; this.maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024; // SHA256 is enabled only for authenticated http requests. If the request is authenticated // and the connection is https we use x-amz-content-sha256=UNSIGNED-PAYLOAD // header for signature calculation. this.enableSHA256 = !this.anonymous && !params.useSSL; this.s3AccelerateEndpoint = params.s3AccelerateEndpoint || null; this.reqOptions = {}; } // This is s3 Specific and does not hold validity in any other Object storage. getAccelerateEndPointIfSet(bucketName, objectName) { if (!_lodash.default.isEmpty(this.s3AccelerateEndpoint) && !_lodash.default.isEmpty(bucketName) && !_lodash.default.isEmpty(objectName)) { // http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html // Disable transfer acceleration for non-compliant bucket names. if (bucketName.indexOf(".") !== -1) { throw new Error(`Transfer Acceleration is not supported for non compliant bucket:${bucketName}`); } // If transfer acceleration is requested set new host. // For more details about enabling transfer acceleration read here. // http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html return this.s3AccelerateEndpoint; } return false; } /** * @param endPoint _string_ valid S3 acceleration end point */ setS3TransferAccelerate(endPoint) { this.s3AccelerateEndpoint = endPoint; } // Sets the supported request options. setRequestOptions(options) { if (!(0, _helpers.isObject)(options)) { throw new TypeError('request options should be of type "object"'); } this.reqOptions = _lodash.default.pick(options, ['agent', 'ca', 'cert', 'ciphers', 'clientCertEngine', 'crl', 'dhparam', 'ecdhCurve', 'family', 'honorCipherOrder', 'key', 'passphrase', 'pfx', 'rejectUnauthorized', 'secureOptions', 'secureProtocol', 'servername', 'sessionIdContext']); } // returns *options* object that can be used with http.request() // Takes care of constructing virtual-host-style or path-style hostname getRequestOptions(opts) { var method = opts.method; var region = opts.region; var bucketName = opts.bucketName; var objectName = opts.objectName; var headers = opts.headers; var query = opts.query; var reqOptions = { method }; reqOptions.headers = {}; // Verify if virtual host supported. var virtualHostStyle; if (bucketName) { virtualHostStyle = (0, _helpers.isVirtualHostStyle)(this.host, this.protocol, bucketName, this.pathStyle); } if (this.port) reqOptions.port = this.port; reqOptions.protocol = this.protocol; if (objectName) { objectName = `${(0, _helpers.uriResourceEscape)(objectName)}`; } reqOptions.path = '/'; // Save host. reqOptions.host = this.host; // For Amazon S3 endpoint, get endpoint based on region. if ((0, _helpers.isAmazonEndpoint)(reqOptions.host)) { const accelerateEndPoint = this.getAccelerateEndPointIfSet(bucketName, objectName); if (accelerateEndPoint) { reqOptions.host = `${accelerateEndPoint}`; } else { reqOptions.host = (0, _s3Endpoints.getS3Endpoint)(region); } } if (virtualHostStyle && !opts.pathStyle) { // For all hosts which support virtual host style, `bucketName` // is part of the hostname in the following format: // // var host = 'bucketName.example.com' // if (bucketName) reqOptions.host = `${bucketName}.${reqOptions.host}`; if (objectName) reqOptions.path = `/${objectName}`; } else { // For all S3 compatible storage services we will fallback to // path style requests, where `bucketName` is part of the URI // path. if (bucketName) reqOptions.path = `/${bucketName}`; if (objectName) reqOptions.path = `/${bucketName}/${objectName}`; } if (query) reqOptions.path += `?${query}`; reqOptions.headers.host = reqOptions.host; if (reqOptions.protocol === 'http:' && reqOptions.port !== 80 || reqOptions.protocol === 'https:' && reqOptions.port !== 443) { reqOptions.headers.host = `${reqOptions.host}:${reqOptions.port}`; } reqOptions.headers['user-agent'] = this.userAgent; if (headers) { // have all header keys in lower case - to make signing easy _lodash.default.map(headers, (v, k) => reqOptions.headers[k.toLowerCase()] = v); } // Use any request option specified in minioClient.setRequestOptions() reqOptions = Object.assign({}, this.reqOptions, reqOptions); return reqOptions; } // Set application specific information. // // Generates User-Agent in the following style. // // MinIO (OS; ARCH) LIB/VER APP/VER // // __Arguments__ // * `appName` _string_ - Application name. // * `appVersion` _string_ - Application version. setAppInfo(appName, appVersion) { if (!(0, _helpers.isString)(appName)) { throw new TypeError(`Invalid appName: ${appName}`); } if (appName.trim() === '') { throw new errors.InvalidArgumentError('Input appName cannot be empty.'); } if (!(0, _helpers.isString)(appVersion)) { throw new TypeError(`Invalid appVersion: ${appVersion}`); } if (appVersion.trim() === '') { throw new errors.InvalidArgumentError('Input appVersion cannot be empty.'); } this.userAgent = `${this.userAgent} ${appName}/${appVersion}`; } // Calculate part size given the object size. Part size will be atleast this.partSize calculatePartSize(size) { if (!(0, _helpers.isNumber)(size)) { throw new TypeError('size should be of type "number"'); } if (size > this.maxObjectSize) { throw new TypeError(`size should not be more than ${this.maxObjectSize}`); } if (this.overRidePartSize) { return this.partSize; } var partSize = this.partSize; for (;;) { // while(true) {...} throws linting error. // If partSize is big enough to accomodate the object size, then use it. if (partSize * 10000 > size) { return partSize; } // Try part sizes as 64MB, 80MB, 96MB etc. partSize += 16 * 1024 * 1024; } } // log the request, response, error logHTTP(reqOptions, response, err) { // if no logstreamer available return. if (!this.logStream) return; if (!(0, _helpers.isObject)(reqOptions)) { throw new TypeError('reqOptions should be of type "object"'); } if (response && !(0, _helpers.isReadableStream)(response)) { throw new TypeError('response should be of type "Stream"'); } if (err && !(err instanceof Error)) { throw new TypeError('err should be of type "Error"'); } var logHeaders = headers => { _lodash.default.forEach(headers, (v, k) => { if (k == 'authorization') { var redacter = new RegExp('Signature=([0-9a-f]+)'); v = v.replace(redacter, 'Signature=**REDACTED**'); } this.logStream.write(`${k}: ${v}\n`); }); this.logStream.write('\n'); }; this.logStream.write(`REQUEST: ${reqOptions.method} ${reqOptions.path}\n`); logHeaders(reqOptions.headers); if (response) { this.logStream.write(`RESPONSE: ${response.statusCode}\n`); logHeaders(response.headers); } if (err) { this.logStream.write('ERROR BODY:\n'); var errJSON = JSON.stringify(err, null, '\t'); this.logStream.write(`${errJSON}\n`); } } // Enable tracing traceOn(stream) { if (!stream) stream = process.stdout; this.logStream = stream; } // Disable tracing traceOff() { this.logStream = null; } // makeRequest is the primitive used by the apis for making S3 requests. // payload can be empty string in case of no payload. // statusCode is the expected statusCode. If response.statusCode does not match // we parse the XML error and call the callback with the error message. // A valid region is passed by the calls - listBuckets, makeBucket and // getBucketRegion. makeRequest(options, payload, statusCodes, region, returnResponse, cb) { if (!(0, _helpers.isObject)(options)) { throw new TypeError('options should be of type "object"'); } if (!(0, _helpers.isString)(payload) && !(0, _helpers.isObject)(payload)) { // Buffer is of type 'object' throw new TypeError('payload should be of type "string" or "Buffer"'); } statusCodes.forEach(statusCode => { if (!(0, _helpers.isNumber)(statusCode)) { throw new TypeError('statusCode should be of type "number"'); } }); if (!(0, _helpers.isString)(region)) { throw new TypeError('region should be of type "string"'); } if (!(0, _helpers.isBoolean)(returnResponse)) { throw new TypeError('returnResponse should be of type "boolean"'); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } if (!options.headers) options.headers = {}; if (options.method === 'POST' || options.method === 'PUT' || options.method === 'DELETE') { options.headers['content-length'] = payload.length; } var sha256sum = ''; if (this.enableSHA256) sha256sum = (0, _helpers.toSha256)(payload); var stream = (0, _helpers.readableStream)(payload); this.makeRequestStream(options, stream, sha256sum, statusCodes, region, returnResponse, cb); } // makeRequestStream will be used directly instead of makeRequest in case the payload // is available as a stream. for ex. putObject makeRequestStream(options, stream, sha256sum, statusCodes, region, returnResponse, cb) { if (!(0, _helpers.isObject)(options)) { throw new TypeError('options should be of type "object"'); } if (!(0, _helpers.isReadableStream)(stream)) { throw new errors.InvalidArgumentError('stream should be a readable Stream'); } if (!(0, _helpers.isString)(sha256sum)) { throw new TypeError('sha256sum should be of type "string"'); } statusCodes.forEach(statusCode => { if (!(0, _helpers.isNumber)(statusCode)) { throw new TypeError('statusCode should be of type "number"'); } }); if (!(0, _helpers.isString)(region)) { throw new TypeError('region should be of type "string"'); } if (!(0, _helpers.isBoolean)(returnResponse)) { throw new TypeError('returnResponse should be of type "boolean"'); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } // sha256sum will be empty for anonymous or https requests if (!this.enableSHA256 && sha256sum.length !== 0) { throw new errors.InvalidArgumentError(`sha256sum expected to be empty for anonymous or https requests`); } // sha256sum should be valid for non-anonymous http requests. if (this.enableSHA256 && sha256sum.length !== 64) { throw new errors.InvalidArgumentError(`Invalid sha256sum : ${sha256sum}`); } var _makeRequest = (e, region) => { if (e) return cb(e); options.region = region; var reqOptions = this.getRequestOptions(options); if (!this.anonymous) { // For non-anonymous https requests sha256sum is 'UNSIGNED-PAYLOAD' for signature calculation. if (!this.enableSHA256) sha256sum = 'UNSIGNED-PAYLOAD'; let date = new Date(); reqOptions.headers['x-amz-date'] = (0, _helpers.makeDateLong)(date); reqOptions.headers['x-amz-content-sha256'] = sha256sum; if (this.sessionToken) { reqOptions.headers['x-amz-security-token'] = this.sessionToken; } this.checkAndRefreshCreds(); var authorization = (0, _signing.signV4)(reqOptions, this.accessKey, this.secretKey, region, date); reqOptions.headers.authorization = authorization; } var req = this.transport.request(reqOptions, response => { if (!statusCodes.includes(response.statusCode)) { // For an incorrect region, S3 server always sends back 400. // But we will do cache invalidation for all errors so that, // in future, if AWS S3 decides to send a different status code or // XML error code we will still work fine. delete this.regionMap[options.bucketName]; var errorTransformer = transformers.getErrorTransformer(response); (0, _helpers.pipesetup)(response, errorTransformer).on('error', e => { this.logHTTP(reqOptions, response, e); cb(e); }); return; } this.logHTTP(reqOptions, response); if (returnResponse) return cb(null, response); // We drain the socket so that the connection gets closed. Note that this // is not expensive as the socket will not have any data. response.on('data', () => {}); cb(null); }); let pipe = (0, _helpers.pipesetup)(stream, req); pipe.on('error', e => { this.logHTTP(reqOptions, null, e); cb(e); }); }; if (region) return _makeRequest(null, region); this.getBucketRegion(options.bucketName, _makeRequest); } // gets the region of the bucket getBucketRegion(bucketName, cb) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError(`Invalid bucket name : ${bucketName}`); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('cb should be of type "function"'); } // Region is set with constructor, return the region right here. if (this.region) return cb(null, this.region); if (this.regionMap[bucketName]) return cb(null, this.regionMap[bucketName]); var extractRegion = response => { var transformer = transformers.getBucketRegionTransformer(); var region = _helpers.DEFAULT_REGION; (0, _helpers.pipesetup)(response, transformer).on('error', cb).on('data', data => { if (data) region = data; }).on('end', () => { this.regionMap[bucketName] = region; cb(null, region); }); }; var method = 'GET'; var query = 'location'; // `getBucketLocation` behaves differently in following ways for // different environments. // // - For nodejs env we default to path style requests. // - For browser env path style requests on buckets yields CORS // error. To circumvent this problem we make a virtual host // style request signed with 'us-east-1'. This request fails // with an error 'AuthorizationHeaderMalformed', additionally // the error XML also provides Region of the bucket. To validate // this region is proper we retry the same request with the newly // obtained region. var pathStyle = this.pathStyle && typeof window === 'undefined'; this.makeRequest({ method, bucketName, query, pathStyle }, '', [200], _helpers.DEFAULT_REGION, true, (e, response) => { if (e) { if (e.name === 'AuthorizationHeaderMalformed') { var region = e.Region; if (!region) return cb(e); this.makeRequest({ method, bucketName, query }, '', [200], region, true, (e, response) => { if (e) return cb(e); extractRegion(response); }); return; } return cb(e); } extractRegion(response); }); } // Creates the bucket `bucketName`. // // __Arguments__ // * `bucketName` _string_ - Name of the bucket // * `region` _string_ - region valid values are _us-west-1_, _us-west-2_, _eu-west-1_, _eu-central-1_, _ap-southeast-1_, _ap-northeast-1_, _ap-southeast-2_, _sa-east-1_. // * `makeOpts` _object_ - Options to create a bucket. e.g {ObjectLocking:true} (Optional) // * `callback(err)` _function_ - callback function with `err` as the error argument. `err` is null if the bucket is successfully created. makeBucket(bucketName, region, makeOpts = {}, cb) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } // Backward Compatibility if ((0, _helpers.isObject)(region)) { cb = makeOpts; makeOpts = region; region = ''; } if ((0, _helpers.isFunction)(region)) { cb = region; region = ''; makeOpts = {}; } if ((0, _helpers.isFunction)(makeOpts)) { cb = makeOpts; makeOpts = {}; } if (!(0, _helpers.isString)(region)) { throw new TypeError('region should be of type "string"'); } if (!(0, _helpers.isObject)(makeOpts)) { throw new TypeError('makeOpts should be of type "object"'); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } var payload = ''; // Region already set in constructor, validate if // caller requested bucket location is same. if (region && this.region) { if (region !== this.region) { throw new errors.InvalidArgumentError(`Configured region ${this.region}, requested ${region}`); } } // sending makeBucket request with XML containing 'us-east-1' fails. For // default region server expects the request without body if (region && region !== _helpers.DEFAULT_REGION) { var createBucketConfiguration = []; createBucketConfiguration.push({ _attr: { xmlns: 'http://s3.amazonaws.com/doc/2006-03-01/' } }); createBucketConfiguration.push({ LocationConstraint: region }); var payloadObject = { CreateBucketConfiguration: createBucketConfiguration }; payload = (0, _xml.default)(payloadObject); } var method = 'PUT'; var headers = {}; if (makeOpts.ObjectLocking) { headers["x-amz-bucket-object-lock-enabled"] = true; } if (!region) region = _helpers.DEFAULT_REGION; const processWithRetry = err => { if (err && (region === "" || region === _helpers.DEFAULT_REGION)) { if (err.code === "AuthorizationHeaderMalformed" && err.region !== "") { // Retry with region returned as part of error this.makeRequest({ method, bucketName, headers }, payload, [200], err.region, false, cb); } else { return cb && cb(err); } } return cb && cb(err); }; this.makeRequest({ method, bucketName, headers }, payload, [200], region, false, processWithRetry); } // List of buckets created. // // __Arguments__ // * `callback(err, buckets)` _function_ - callback function with error as the first argument. `buckets` is an array of bucket information // // `buckets` array element: // * `bucket.name` _string_ : bucket name // * `bucket.creationDate` _Date_: date when bucket was created listBuckets(cb) { if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } var method = 'GET'; this.makeRequest({ method }, '', [200], _helpers.DEFAULT_REGION, true, (e, response) => { if (e) return cb(e); var transformer = transformers.getListBucketTransformer(); var buckets; (0, _helpers.pipesetup)(response, transformer).on('data', result => buckets = result).on('error', e => cb(e)).on('end', () => cb(null, buckets)); }); } // Returns a stream that emits objects that are partially uploaded. // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `prefix` _string_: prefix of the object names that are partially uploaded (optional, default `''`) // * `recursive` _bool_: directory style listing when false, recursive listing when true (optional, default `false`) // // __Return Value__ // * `stream` _Stream_ : emits objects of the format: // * `object.key` _string_: name of the object // * `object.uploadId` _string_: upload ID of the object // * `object.size` _Integer_: size of the partially uploaded object listIncompleteUploads(bucket, prefix, recursive) { if (prefix === undefined) prefix = ''; if (recursive === undefined) recursive = false; if (!(0, _helpers.isValidBucketName)(bucket)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucket); } if (!(0, _helpers.isValidPrefix)(prefix)) { throw new errors.InvalidPrefixError(`Invalid prefix : ${prefix}`); } if (!(0, _helpers.isBoolean)(recursive)) { throw new TypeError('recursive should be of type "boolean"'); } var delimiter = recursive ? '' : '/'; var keyMarker = ''; var uploadIdMarker = ''; var uploads = []; var ended = false; var readStream = _stream.default.Readable({ objectMode: true }); readStream._read = () => { // push one upload info per _read() if (uploads.length) { return readStream.push(uploads.shift()); } if (ended) return readStream.push(null); this.listIncompleteUploadsQuery(bucket, prefix, keyMarker, uploadIdMarker, delimiter).on('error', e => readStream.emit('error', e)).on('data', result => { result.prefixes.forEach(prefix => uploads.push(prefix)); _async.default.eachSeries(result.uploads, (upload, cb) => { // for each incomplete upload add the sizes of its uploaded parts this.listParts(bucket, upload.key, upload.uploadId, (err, parts) => { if (err) return cb(err); upload.size = parts.reduce((acc, item) => acc + item.size, 0); uploads.push(upload); cb(); }); }, err => { if (err) { readStream.emit('error', err); return; } if (result.isTruncated) { keyMarker = result.nextKeyMarker; uploadIdMarker = result.nextUploadIdMarker; } else { ended = true; } readStream._read(); }); }); }; return readStream; } // To check if a bucket already exists. // // __Arguments__ // * `bucketName` _string_ : name of the bucket // * `callback(err)` _function_ : `err` is `null` if the bucket exists bucketExists(bucketName, cb) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } var method = 'HEAD'; this.makeRequest({ method, bucketName }, '', [200], '', false, err => { if (err) { if (err.code == 'NoSuchBucket' || err.code == 'NotFound') return cb(null, false); return cb(err); } cb(null, true); }); } // Remove a bucket. // // __Arguments__ // * `bucketName` _string_ : name of the bucket // * `callback(err)` _function_ : `err` is `null` if the bucket is removed successfully. removeBucket(bucketName, cb) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } var method = 'DELETE'; this.makeRequest({ method, bucketName }, '', [204], '', false, e => { // If the bucket was successfully removed, remove the region map entry. if (!e) delete this.regionMap[bucketName]; cb(e); }); } // Remove the partially uploaded object. // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `callback(err)` _function_: callback function is called with non `null` value in case of error removeIncompleteUpload(bucketName, objectName, cb) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.isValidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } var removeUploadId; _async.default.during(cb => { this.findUploadId(bucketName, objectName, (e, uploadId) => { if (e) return cb(e); removeUploadId = uploadId; cb(null, uploadId); }); }, cb => { var method = 'DELETE'; var query = `uploadId=${removeUploadId}`; this.makeRequest({ method, bucketName, objectName, query }, '', [204], '', false, e => cb(e)); }, cb); } // Callback is called with `error` in case of error or `null` in case of success // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `filePath` _string_: path to which the object data will be written to // * `getOpts` _object_: Version of the object in the form `{versionId:'my-uuid'}`. Default is `{}`. (optional) // * `callback(err)` _function_: callback is called with `err` in case of error. fGetObject(bucketName, objectName, filePath, getOpts = {}, cb) { // Input validation. if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } if (!(0, _helpers.isString)(filePath)) { throw new TypeError('filePath should be of type "string"'); } // Backward Compatibility if ((0, _helpers.isFunction)(getOpts)) { cb = getOpts; getOpts = {}; } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } // Internal data. var partFile; var partFileStream; var objStat; // Rename wrapper. var rename = err => { if (err) return cb(err); _fs.default.rename(partFile, filePath, cb); }; _async.default.waterfall([cb => this.statObject(bucketName, objectName, getOpts, cb), (result, cb) => { objStat = result; // Create any missing top level directories. (0, _mkdirp.default)(_path.default.dirname(filePath), cb); }, (ignore, cb) => { partFile = `${filePath}.${objStat.etag}.part.minio`; _fs.default.stat(partFile, (e, stats) => { var offset = 0; if (e) { partFileStream = _fs.default.createWriteStream(partFile, { flags: 'w' }); } else { if (objStat.size === stats.size) return rename(); offset = stats.size; partFileStream = _fs.default.createWriteStream(partFile, { flags: 'a' }); } this.getPartialObject(bucketName, objectName, offset, 0, getOpts, cb); }); }, (downloadStream, cb) => { (0, _helpers.pipesetup)(downloadStream, partFileStream).on('error', e => cb(e)).on('finish', cb); }, cb => _fs.default.stat(partFile, cb), (stats, cb) => { if (stats.size === objStat.size) return cb(); cb(new Error('Size mismatch between downloaded file and the object')); }], rename); } // Callback is called with readable stream of the object content. // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `getOpts` _object_: Version of the object in the form `{versionId:'my-uuid'}`. Default is `{}`. (optional) // * `callback(err, stream)` _function_: callback is called with `err` in case of error. `stream` is the object content stream getObject(bucketName, objectName, getOpts = {}, cb) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } // Backward Compatibility if ((0, _helpers.isFunction)(getOpts)) { cb = getOpts; getOpts = {}; } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } this.getPartialObject(bucketName, objectName, 0, 0, getOpts, cb); } // Callback is called with readable stream of the partial object content. // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `offset` _number_: offset of the object from where the stream will start // * `length` _number_: length of the object that will be read in the stream (optional, if not specified we read the rest of the file from the offset) // * `getOpts` _object_: Version of the object in the form `{versionId:'my-uuid'}`. Default is `{}`. (optional) // * `callback(err, stream)` _function_: callback is called with `err` in case of error. `stream` is the object content stream getPartialObject(bucketName, objectName, offset, length, getOpts = {}, cb) { if ((0, _helpers.isFunction)(length)) { cb = length; length = 0; } if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } if (!(0, _helpers.isNumber)(offset)) { throw new TypeError('offset should be of type "number"'); } if (!(0, _helpers.isNumber)(length)) { throw new TypeError('length should be of type "number"'); } // Backward Compatibility if ((0, _helpers.isFunction)(getOpts)) { cb = getOpts; getOpts = {}; } if (!(0, _helpers.isFunction)(cb)) { throw new TypeError('callback should be of type "function"'); } var range = ''; if (offset || length) { if (offset) { range = `bytes=${+offset}-`; } else { range = 'bytes=0-'; offset = 0; } if (length) { range += `${+length + offset - 1}`; } } var headers = {}; if (range !== '') { headers.range = range; } var expectedStatusCodes = [200]; if (range) { expectedStatusCodes.push(206); } var method = 'GET'; var query = _queryString.default.stringify(getOpts); this.makeRequest({ method, bucketName, objectName, headers, query }, '', expectedStatusCodes, '', true, cb); } // Uploads the object using contents from a file // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `filePath` _string_: file path of the file to be uploaded // * `metaData` _Javascript Object_: metaData assosciated with the object // * `callback(err, objInfo)` _function_: non null `err` indicates error, `objInfo` _object_ which contains versionId and etag. fPutObject(bucketName, objectName, filePath, metaData, callback) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } if (!(0, _helpers.isString)(filePath)) { throw new TypeError('filePath should be of type "string"'); } if ((0, _helpers.isFunction)(metaData)) { callback = metaData; metaData = {}; // Set metaData empty if no metaData provided. } if (!(0, _helpers.isObject)(metaData)) { throw new TypeError('metaData should be of type "object"'); } // Inserts correct `content-type` attribute based on metaData and filePath metaData = (0, _helpers.insertContentType)(metaData, filePath); // Updates metaData to have the correct prefix if needed metaData = (0, _helpers.prependXAMZMeta)(metaData); var size; var partSize; _async.default.waterfall([cb => _fs.default.stat(filePath, cb), (stats, cb) => { size = stats.size; var cbTriggered = false; var origCb = cb; cb = function () { if (cbTriggered) { return; } cbTriggered = true; return origCb.apply(this, arguments); }; if (size > this.maxObjectSize) { return cb(new Error(`${filePath} size : ${stats.size}, max allowed size : 5TB`)); } if (size <= this.partSize) { // simple PUT request, no multipart var multipart = false; var uploader = this.getUploader(bucketName, objectName, metaData, multipart); var hash = transformers.getHashSummer(this.enableSHA256); var start = 0; var end = size - 1; var autoClose = true; if (size === 0) end = 0; var options = { start, end, autoClose }; (0, _helpers.pipesetup)(_fs.default.createReadStream(filePath, options), hash).on('data', data => { var md5sum = data.md5sum; var sha256sum = data.sha256sum; var stream = _fs.default.createReadStream(filePath, options); uploader(stream, size, sha256sum, md5sum, (err, objInfo) => { callback(err, objInfo); cb(true); }); }).on('error', e => cb(e)); return; } this.findUploadId(bucketName, objectName, cb); }, (uploadId, cb) => { // if there was a previous incomplete upload, fetch all its uploaded parts info if (uploadId) return this.listParts(bucketName, objectName, uploadId, (e, etags) => cb(e, uploadId, etags)); // there was no previous upload, initiate a new one this.initiateNewMultipartUpload(bucketName, objectName, metaData, (e, uploadId) => cb(e, uploadId, [])); }, (uploadId, etags, cb) => { partSize = this.calculatePartSize(size); var multipart = true; var uploader = this.getUploader(bucketName, objectName, metaData, multipart); // convert array to object to make things easy var parts = etags.reduce(function (acc, item) { if (!acc[item.part]) { acc[item.part] = item; } return acc; }, {}); var partsDone = []; var partNumber = 1; var uploadedSize = 0; _async.default.whilst(cb => { cb(null, uploadedSize < size); }, cb => { var cbTriggered = false; var origCb = cb; cb = function () { if (cbTriggered) { return; } cbTriggered = true; return origCb.apply(this, arguments); }; var part = parts[partNumber]; var hash = transformers.getHashSummer(this.enableSHA256); var length = partSize; if (length > size - uploadedSize) { length = size - uploadedSize; } var start = uploadedSize; var end = uploadedSize + length - 1; var autoClose = true; var options = { autoClose, start, end }; // verify md5sum of each part (0, _helpers.pipesetup)(_fs.default.createReadStream(filePath, options), hash).on('data', data => { var md5sumHex = Buffer.from(data.md5sum, 'base64').toString('hex'); if (part && md5sumHex === part.etag) { // md5 matches, chunk already uploaded partsDone.push({ part: partNumber, etag: part.etag }); partNumber++; uploadedSize += length; return cb(); } // part is not uploaded yet, or md5 mismatch var stream = _fs.default.createReadStream(filePath, options); uploader(uploadId, partNumber, stream, length, data.sha256sum, data.md5sum, (e, objInfo) => { if (e) return cb(e); partsDone.push({ part: partNumber, etag: objInfo.etag }); partNumber++; uploadedSize += length; return cb(); }); }).on('error', e => cb(e)); }, e => { if (e) return cb(e); cb(null, partsDone, uploadId); }); }, // all parts uploaded, complete the multipart upload (etags, uploadId, cb) => this.completeMultipartUpload(bucketName, objectName, uploadId, etags, cb)], (err, ...rest) => { if (err === true) return; callback(err, ...rest); }); } // Uploads the object. // // Uploading a stream // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `stream` _Stream_: Readable stream // * `size` _number_: size of the object (optional) // * `callback(err, etag)` _function_: non null `err` indicates error, `etag` _string_ is the etag of the object uploaded. // // Uploading "Buffer" or "string" // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `string or Buffer` _string_ or _Buffer_: string or buffer // * `callback(err, objInfo)` _function_: `err` is `null` in case of success and `info` will have the following object details: // * `etag` _string_: etag of the object // * `versionId` _string_: versionId of the object putObject(bucketName, objectName, stream, size, metaData, callback) { if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } // We'll need to shift arguments to the left because of size and metaData. if ((0, _helpers.isFunction)(size)) { callback = size; metaData = {}; } else if ((0, _helpers.isFunction)(metaData)) { callback = metaData; metaData = {}; } // We'll need to shift arguments to the left because of metaData // and size being optional. if ((0, _helpers.isObject)(size)) { metaData = size; } // Ensures Metadata has appropriate prefix for A3 API metaData = (0, _helpers.prependXAMZMeta)(metaData); if (typeof stream === 'string' || stream instanceof Buffer) { // Adapts the non-stream interface into a stream. size = stream.length; stream = (0, _helpers.readableStream)(stream); } else if (!(0, _helpers.isReadableStream)(stream)) { throw new TypeError('third argument should be of type "stream.Readable" or "Buffer" or "string"'); } if (!(0, _helpers.isFunction)(callback)) { throw new TypeError('callback should be of type "function"'); } if ((0, _helpers.isNumber)(size) && size < 0) { throw new errors.InvalidArgumentError(`size cannot be negative, given size: ${size}`); } // Get the part size and forward that to the BlockStream. Default to the // largest block size possible if necessary. if (!(0, _helpers.isNumber)(size)) size = this.maxObjectSize; size = this.calculatePartSize(size); // s3 requires that all non-end chunks be at least `this.partSize`, // so we chunk the stream until we hit either that size or the end before // we flush it to s3. let chunker = new _blockStream.default({ size, zeroPadding: false }); // This is a Writable stream that can be written to in order to upload // to the specified bucket and object automatically. let uploader = new _objectUploader.default(this, bucketName, objectName, size, metaData, callback); // stream => chunker => uploader stream.pipe(chunker).pipe(uploader); } // Copy the object. // // __Arguments__ // * `bucketName` _string_: name of the bucket // * `objectName` _string_: name of the object // * `srcObject` _string_: path of the source object to be copied // * `conditions` _CopyConditions_: copy conditions that needs to be satisfied (optional, default `null`) // * `callback(err, {etag, lastModified})` _function_: non null `err` indicates error, `etag` _string_ and `listModifed` _Date_ are respectively the etag and the last modified date of the newly copied object copyObjectV1(arg1, arg2, arg3, arg4, arg5) { var bucketName = arg1; var objectName = arg2; var srcObject = arg3; var conditions, cb; if (typeof arg4 == 'function' && arg5 === undefined) { conditions = null; cb = arg4; } else { conditions = arg4; cb = arg5; } if (!(0, _helpers.isValidBucketName)(bucketName)) { throw new errors.InvalidBucketNameError('Invalid bucket name: ' + bucketName); } if (!(0, _helpers.isValidObjectName)(objectName)) { throw new errors.InvalidObjectNameError(`Invalid object name: ${objectName}`); } if (!(0, _helpers.isString)(srcObject)) { throw new TypeError('srcObject should be of type "string"'); } if (srcObject === "") { throw new errors.InvalidPrefixError(`Empty source prefix`); } if (conditions !== null && !(conditions instanceof CopyConditions)) { throw new TypeError('conditions should be of type "CopyConditions"'); } var headers = {}; headers['x-amz-copy-source'] = (0, _helpers.uriResourceEscape)(srcObject); if (conditions !== null) { if (conditions.modified !== "") { headers['x-amz-copy-source-if-modified-since'] = conditions.modified; } if (conditions.unmodified !== "") { headers['x-amz-copy-source-if-unmodified-since'] = conditions.unmodified; } if (conditions.matchETag !== "") { headers['x-amz-copy-source-if-match'] = conditions.matchETag; } if (conditions.matchEtagExcept !== "") { headers['x-amz-copy-source-if-none-match'] = conditions.matchETagExcept; } } var method = 'PUT'; this.makeRequest({ method, bucketName, objectName, headers }, '', [200], '', true, (e, response) => { if (e) return cb(e); var transformer = transformers.getCopyObjectTransformer(); (0, _helpers.pipesetup)(response, transformer).on('error', e => cb(e)).on('data', data => cb(null, data)); }); } /** * Internal Method to perform copy of an object. *