@google-cloud/storage
Version:
Cloud Storage Client Library for Node.js
1,171 lines • 143 kB
JavaScript
"use strict";
// Copyright 2019 Google LLC
//
// 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _File_instances, _File_validateIntegrity;
Object.defineProperty(exports, "__esModule", { value: true });
exports.File = exports.FileExceptionMessages = exports.RequestError = exports.STORAGE_POST_POLICY_BASE_URL = exports.ActionToHTTPMethod = void 0;
const index_js_1 = require("./nodejs-common/index.js");
const promisify_1 = require("@google-cloud/promisify");
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const mime_1 = __importDefault(require("mime"));
const resumableUpload = __importStar(require("./resumable-upload.js"));
const stream_1 = require("stream");
const zlib = __importStar(require("zlib"));
const storage_js_1 = require("./storage.js");
const bucket_js_1 = require("./bucket.js");
const acl_js_1 = require("./acl.js");
const signer_js_1 = require("./signer.js");
const util_js_1 = require("./nodejs-common/util.js");
const duplexify_1 = __importDefault(require("duplexify"));
const util_js_2 = require("./util.js");
const crc32c_js_1 = require("./crc32c.js");
const hash_stream_validator_js_1 = require("./hash-stream-validator.js");
const async_retry_1 = __importDefault(require("async-retry"));
var ActionToHTTPMethod;
(function (ActionToHTTPMethod) {
ActionToHTTPMethod["read"] = "GET";
ActionToHTTPMethod["write"] = "PUT";
ActionToHTTPMethod["delete"] = "DELETE";
ActionToHTTPMethod["resumable"] = "POST";
})(ActionToHTTPMethod || (exports.ActionToHTTPMethod = ActionToHTTPMethod = {}));
/**
* @deprecated - no longer used
*/
exports.STORAGE_POST_POLICY_BASE_URL = 'https://storage.googleapis.com';
/**
* @private
*/
const GS_URL_REGEXP = /^gs:\/\/([a-z0-9_.-]+)\/(.+)$/;
/**
* @private
* This regex will match compressible content types. These are primarily text/*, +json, +text, +xml content types.
* This was based off of mime-db and may periodically need to be updated if new compressible content types become
* standards.
*/
const COMPRESSIBLE_MIME_REGEX = new RegExp([
/^text\/|application\/ecmascript|application\/javascript|application\/json/,
/|application\/postscript|application\/rtf|application\/toml|application\/vnd.dart/,
/|application\/vnd.ms-fontobject|application\/wasm|application\/x-httpd-php|application\/x-ns-proxy-autoconfig/,
/|application\/x-sh(?!ockwave-flash)|application\/x-tar|application\/x-virtualbox-hdd|application\/x-virtualbox-ova|application\/x-virtualbox-ovf/,
/|^application\/x-virtualbox-vbox$|application\/x-virtualbox-vdi|application\/x-virtualbox-vhd|application\/x-virtualbox-vmdk/,
/|application\/xml|application\/xml-dtd|font\/otf|font\/ttf|image\/bmp|image\/vnd.adobe.photoshop|image\/vnd.microsoft.icon/,
/|image\/vnd.ms-dds|image\/x-icon|image\/x-ms-bmp|message\/rfc822|model\/gltf-binary|\+json|\+text|\+xml|\+yaml/,
]
.map(r => r.source)
.join(''), 'i');
class RequestError extends Error {
}
exports.RequestError = RequestError;
const SEVEN_DAYS = 7 * 24 * 60 * 60;
const GS_UTIL_URL_REGEX = /(gs):\/\/([a-z0-9_.-]+)\/(.+)/g;
const HTTPS_PUBLIC_URL_REGEX = /(https):\/\/(storage\.googleapis\.com)\/([a-z0-9_.-]+)\/(.+)/g;
var FileExceptionMessages;
(function (FileExceptionMessages) {
FileExceptionMessages["EXPIRATION_TIME_NA"] = "An expiration time is not available.";
FileExceptionMessages["DESTINATION_NO_NAME"] = "Destination file should have a name.";
FileExceptionMessages["INVALID_VALIDATION_FILE_RANGE"] = "Cannot use validation with file ranges (start/end).";
FileExceptionMessages["MD5_NOT_AVAILABLE"] = "MD5 verification was specified, but is not available for the requested object. MD5 is not available for composite objects.";
FileExceptionMessages["EQUALS_CONDITION_TWO_ELEMENTS"] = "Equals condition must be an array of 2 elements.";
FileExceptionMessages["STARTS_WITH_TWO_ELEMENTS"] = "StartsWith condition must be an array of 2 elements.";
FileExceptionMessages["CONTENT_LENGTH_RANGE_MIN_MAX"] = "ContentLengthRange must have numeric min & max fields.";
FileExceptionMessages["DOWNLOAD_MISMATCH"] = "The downloaded data did not match the data from the server. To be sure the content is the same, you should download the file again.";
FileExceptionMessages["UPLOAD_MISMATCH_DELETE_FAIL"] = "The uploaded data did not match the data from the server.\n As a precaution, we attempted to delete the file, but it was not successful.\n To be sure the content is the same, you should try removing the file manually,\n then uploading the file again.\n \n\nThe delete attempt failed with this message:\n\n ";
FileExceptionMessages["UPLOAD_MISMATCH"] = "The uploaded data did not match the data from the server.\n As a precaution, the file has been deleted.\n To be sure the content is the same, you should try uploading the file again.";
FileExceptionMessages["MD5_RESUMED_UPLOAD"] = "MD5 cannot be used with a continued resumable upload as MD5 cannot be extended from an existing value";
FileExceptionMessages["MISSING_RESUME_CRC32C_FINAL_UPLOAD"] = "The CRC32C is missing for the final portion of a resumed upload, which is required for validation. Please provide `resumeCRC32C` if validation is required, or disable `validation`.";
})(FileExceptionMessages || (exports.FileExceptionMessages = FileExceptionMessages = {}));
/**
* A File object is created from your {@link Bucket} object using
* {@link Bucket#file}.
*
* @class
*/
class File extends index_js_1.ServiceObject {
/**
* Cloud Storage uses access control lists (ACLs) to manage object and
* bucket access. ACLs are the mechanism you use to share objects with other
* users and allow other users to access your buckets and objects.
*
* An ACL consists of one or more entries, where each entry grants permissions
* to an entity. Permissions define the actions that can be performed against
* an object or bucket (for example, `READ` or `WRITE`); the entity defines
* who the permission applies to (for example, a specific user or group of
* users).
*
* The `acl` object on a File instance provides methods to get you a list of
* the ACLs defined on your bucket, as well as set, update, and delete them.
*
* See {@link http://goo.gl/6qBBPO| About Access Control lists}
*
* @name File#acl
* @mixes Acl
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
* //-
* // Make a file publicly readable.
* //-
* const options = {
* entity: 'allUsers',
* role: storage.acl.READER_ROLE
* };
*
* file.acl.add(options, function(err, aclObject) {});
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.acl.add(options).then(function(data) {
* const aclObject = data[0];
* const apiResponse = data[1];
* });
* ```
*/
/**
* The API-formatted resource description of the file.
*
* Note: This is not guaranteed to be up-to-date when accessed. To get the
* latest record, call the `getMetadata()` method.
*
* @name File#metadata
* @type {object}
*/
/**
* The file's name.
* @name File#name
* @type {string}
*/
/**
* @callback Crc32cGeneratorToStringCallback
* A method returning the CRC32C as a base64-encoded string.
*
* @returns {string}
*
* @example
* Hashing the string 'data' should return 'rth90Q=='
*
* ```js
* const buffer = Buffer.from('data');
* crc32c.update(buffer);
* crc32c.toString(); // 'rth90Q=='
* ```
**/
/**
* @callback Crc32cGeneratorValidateCallback
* A method validating a base64-encoded CRC32C string.
*
* @param {string} [value] base64-encoded CRC32C string to validate
* @returns {boolean}
*
* @example
* Should return `true` if the value matches, `false` otherwise
*
* ```js
* const buffer = Buffer.from('data');
* crc32c.update(buffer);
* crc32c.validate('DkjKuA=='); // false
* crc32c.validate('rth90Q=='); // true
* ```
**/
/**
* @callback Crc32cGeneratorUpdateCallback
* A method for passing `Buffer`s for CRC32C generation.
*
* @param {Buffer} [data] data to update CRC32C value with
* @returns {undefined}
*
* @example
* Hashing buffers from 'some ' and 'text\n'
*
* ```js
* const buffer1 = Buffer.from('some ');
* crc32c.update(buffer1);
*
* const buffer2 = Buffer.from('text\n');
* crc32c.update(buffer2);
*
* crc32c.toString(); // 'DkjKuA=='
* ```
**/
/**
* @typedef {object} CRC32CValidator
* @property {Crc32cGeneratorToStringCallback}
* @property {Crc32cGeneratorValidateCallback}
* @property {Crc32cGeneratorUpdateCallback}
*/
/**
* @callback Crc32cGeneratorCallback
* @returns {CRC32CValidator}
*/
/**
* @typedef {object} FileOptions Options passed to the File constructor.
* @property {string} [encryptionKey] A custom encryption key.
* @property {number} [generation] Generation to scope the file to.
* @property {string} [kmsKeyName] Cloud KMS Key used to encrypt this
* object, if the object is encrypted by such a key. Limited availability;
* usable only by enabled projects.
* @property {string} [userProject] The ID of the project which will be
* billed for all requests made from File object.
* @property {Crc32cGeneratorCallback} [callback] A function that generates a CRC32C Validator. Defaults to {@link CRC32C}
*/
/**
* Constructs a file object.
*
* @param {Bucket} bucket The Bucket instance this file is
* attached to.
* @param {string} name The name of the remote file.
* @param {FileOptions} [options] Configuration options.
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
* ```
*/
constructor(bucket, name, options = {}) {
var _a, _b;
const requestQueryObject = {};
let generation;
if (options.generation !== null) {
if (typeof options.generation === 'string') {
generation = Number(options.generation);
}
else {
generation = options.generation;
}
if (!isNaN(generation)) {
requestQueryObject.generation = generation;
}
}
Object.assign(requestQueryObject, options.preconditionOpts);
const userProject = options.userProject || bucket.userProject;
if (typeof userProject === 'string') {
requestQueryObject.userProject = userProject;
}
const methods = {
/**
* @typedef {array} DeleteFileResponse
* @property {object} 0 The full API response.
*/
/**
* @callback DeleteFileCallback
* @param {?Error} err Request error, if any.
* @param {object} apiResponse The full API response.
*/
/**
* Delete the file.
*
* See {@link https://cloud.google.com/storage/docs/json_api/v1/objects/delete| Objects: delete API Documentation}
*
* @method File#delete
* @param {object} [options] Configuration options.
* @param {boolean} [options.ignoreNotFound = false] Ignore an error if
* the file does not exist.
* @param {string} [options.userProject] The ID of the project which will be
* billed for the request.
* @param {DeleteFileCallback} [callback] Callback function.
* @returns {Promise<DeleteFileResponse>}
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
* file.delete(function(err, apiResponse) {});
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.delete().then(function(data) {
* const apiResponse = data[0];
* });
*
* ```
* @example <caption>include:samples/files.js</caption>
* region_tag:storage_delete_file
* Another example:
*/
delete: {
reqOpts: {
qs: requestQueryObject,
},
},
/**
* @typedef {array} FileExistsResponse
* @property {boolean} 0 Whether the {@link File} exists.
*/
/**
* @callback FileExistsCallback
* @param {?Error} err Request error, if any.
* @param {boolean} exists Whether the {@link File} exists.
*/
/**
* Check if the file exists.
*
* @method File#exists
* @param {options} [options] Configuration options.
* @param {string} [options.userProject] The ID of the project which will be
* billed for the request.
* @param {FileExistsCallback} [callback] Callback function.
* @returns {Promise<FileExistsResponse>}
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
*
* file.exists(function(err, exists) {});
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.exists().then(function(data) {
* const exists = data[0];
* });
* ```
*/
exists: {
reqOpts: {
qs: requestQueryObject,
},
},
/**
* @typedef {array} GetFileResponse
* @property {File} 0 The {@link File}.
* @property {object} 1 The full API response.
*/
/**
* @callback GetFileCallback
* @param {?Error} err Request error, if any.
* @param {File} file The {@link File}.
* @param {object} apiResponse The full API response.
*/
/**
* Get a file object and its metadata if it exists.
*
* @method File#get
* @param {options} [options] Configuration options.
* @param {string} [options.userProject] The ID of the project which will be
* billed for the request.
* @param {number} [options.generation] The generation number to get
* @param {boolean} [options.softDeleted] If true, returns the soft-deleted object.
Object `generation` is required if `softDeleted` is set to True.
* @param {GetFileCallback} [callback] Callback function.
* @returns {Promise<GetFileResponse>}
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
*
* file.get(function(err, file, apiResponse) {
* // file.metadata` has been populated.
* });
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.get().then(function(data) {
* const file = data[0];
* const apiResponse = data[1];
* });
* ```
*/
get: {
reqOpts: {
qs: requestQueryObject,
},
},
/**
* @typedef {array} GetFileMetadataResponse
* @property {object} 0 The {@link File} metadata.
* @property {object} 1 The full API response.
*/
/**
* @callback GetFileMetadataCallback
* @param {?Error} err Request error, if any.
* @param {object} metadata The {@link File} metadata.
* @param {object} apiResponse The full API response.
*/
/**
* Get the file's metadata.
*
* See {@link https://cloud.google.com/storage/docs/json_api/v1/objects/get| Objects: get API Documentation}
*
* @method File#getMetadata
* @param {object} [options] Configuration options.
* @param {string} [options.userProject] The ID of the project which will be
* billed for the request.
* @param {GetFileMetadataCallback} [callback] Callback function.
* @returns {Promise<GetFileMetadataResponse>}
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
*
* file.getMetadata(function(err, metadata, apiResponse) {});
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.getMetadata().then(function(data) {
* const metadata = data[0];
* const apiResponse = data[1];
* });
*
* ```
* @example <caption>include:samples/files.js</caption>
* region_tag:storage_get_metadata
* Another example:
*/
getMetadata: {
reqOpts: {
qs: requestQueryObject,
},
},
/**
* @typedef {object} SetFileMetadataOptions Configuration options for File#setMetadata().
* @param {string} [userProject] The ID of the project which will be billed for the request.
*/
/**
* @callback SetFileMetadataCallback
* @param {?Error} err Request error, if any.
* @param {object} apiResponse The full API response.
*/
/**
* @typedef {array} SetFileMetadataResponse
* @property {object} 0 The full API response.
*/
/**
* Merge the given metadata with the current remote file's metadata. This
* will set metadata if it was previously unset or update previously set
* metadata. To unset previously set metadata, set its value to null.
*
* You can set custom key/value pairs in the metadata key of the given
* object, however the other properties outside of this object must adhere
* to the {@link https://goo.gl/BOnnCK| official API documentation}.
*
*
* See the examples below for more information.
*
* See {@link https://cloud.google.com/storage/docs/json_api/v1/objects/patch| Objects: patch API Documentation}
*
* @method File#setMetadata
* @param {object} [metadata] The metadata you wish to update.
* @param {SetFileMetadataOptions} [options] Configuration options.
* @param {SetFileMetadataCallback} [callback] Callback function.
* @returns {Promise<SetFileMetadataResponse>}
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const myBucket = storage.bucket('my-bucket');
*
* const file = myBucket.file('my-file');
*
* const metadata = {
* contentType: 'application/x-font-ttf',
* metadata: {
* my: 'custom',
* properties: 'go here'
* }
* };
*
* file.setMetadata(metadata, function(err, apiResponse) {});
*
* // Assuming current metadata = { hello: 'world', unsetMe: 'will do' }
* file.setMetadata({
* metadata: {
* abc: '123', // will be set.
* unsetMe: null, // will be unset (deleted).
* hello: 'goodbye' // will be updated from 'world' to 'goodbye'.
* }
* }, function(err, apiResponse) {
* // metadata should now be { abc: '123', hello: 'goodbye' }
* });
*
* //-
* // Set a temporary hold on this file from its bucket's retention period
* // configuration.
* //
* file.setMetadata({
* temporaryHold: true
* }, function(err, apiResponse) {});
*
* //-
* // Alternatively, you may set a temporary hold. This will follow the
* // same behavior as an event-based hold, with the exception that the
* // bucket's retention policy will not renew for this file from the time
* // the hold is released.
* //-
* file.setMetadata({
* eventBasedHold: true
* }, function(err, apiResponse) {});
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.setMetadata(metadata).then(function(data) {
* const apiResponse = data[0];
* });
* ```
*/
setMetadata: {
reqOpts: {
qs: requestQueryObject,
},
},
};
super({
parent: bucket,
baseUrl: '/o',
id: encodeURIComponent(name),
methods,
});
_File_instances.add(this);
this.bucket = bucket;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.storage = bucket.parent;
// @TODO Can this duplicate code from above be avoided?
if (options.generation !== null) {
let generation;
if (typeof options.generation === 'string') {
generation = Number(options.generation);
}
else {
generation = options.generation;
}
if (!isNaN(generation)) {
this.generation = generation;
}
}
this.kmsKeyName = options.kmsKeyName;
this.userProject = userProject;
this.name = name;
if (options.encryptionKey) {
this.setEncryptionKey(options.encryptionKey);
}
this.acl = new acl_js_1.Acl({
request: this.request.bind(this),
pathPrefix: '/acl',
});
this.crc32cGenerator =
options.crc32cGenerator || this.bucket.crc32cGenerator;
this.instanceRetryValue = (_b = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.retryOptions) === null || _b === void 0 ? void 0 : _b.autoRetry;
this.instancePreconditionOpts = options === null || options === void 0 ? void 0 : options.preconditionOpts;
}
/**
* The object's Cloud Storage URI (`gs://`)
*
* @example
* ```ts
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const bucket = storage.bucket('my-bucket');
* const file = bucket.file('image.png');
*
* // `gs://my-bucket/image.png`
* const href = file.cloudStorageURI.href;
* ```
*/
get cloudStorageURI() {
const uri = this.bucket.cloudStorageURI;
uri.pathname = this.name;
return uri;
}
/**
* A helper method for determining if a request should be retried based on preconditions.
* This should only be used for methods where the idempotency is determined by
* `ifGenerationMatch`
* @private
*
* A request should not be retried under the following conditions:
* - if precondition option `ifGenerationMatch` is not set OR
* - if `idempotencyStrategy` is set to `RetryNever`
*/
shouldRetryBasedOnPreconditionAndIdempotencyStrat(options) {
var _a;
return !(((options === null || options === void 0 ? void 0 : options.ifGenerationMatch) === undefined &&
((_a = this.instancePreconditionOpts) === null || _a === void 0 ? void 0 : _a.ifGenerationMatch) === undefined &&
this.storage.retryOptions.idempotencyStrategy ===
storage_js_1.IdempotencyStrategy.RetryConditional) ||
this.storage.retryOptions.idempotencyStrategy ===
storage_js_1.IdempotencyStrategy.RetryNever);
}
/**
* @typedef {array} CopyResponse
* @property {File} 0 The copied {@link File}.
* @property {object} 1 The full API response.
*/
/**
* @callback CopyCallback
* @param {?Error} err Request error, if any.
* @param {File} copiedFile The copied {@link File}.
* @param {object} apiResponse The full API response.
*/
/**
* @typedef {object} CopyOptions Configuration options for File#copy(). See an
* {@link https://cloud.google.com/storage/docs/json_api/v1/objects#resource| Object resource}.
* @property {string} [cacheControl] The cacheControl setting for the new file.
* @property {string} [contentEncoding] The contentEncoding setting for the new file.
* @property {string} [contentType] The contentType setting for the new file.
* @property {string} [destinationKmsKeyName] Resource name of the Cloud
* KMS key, of the form
* `projects/my-project/locations/location/keyRings/my-kr/cryptoKeys/my-key`,
* that will be used to encrypt the object. Overwrites the object
* metadata's `kms_key_name` value, if any.
* @property {Metadata} [metadata] Metadata to specify on the copied file.
* @property {string} [predefinedAcl] Set the ACL for the new file.
* @property {string} [token] A previously-returned `rewriteToken` from an
* unfinished rewrite request.
* @property {string} [userProject] The ID of the project which will be
* billed for the request.
*/
/**
* Copy this file to another file. By default, this will copy the file to the
* same bucket, but you can choose to copy it to another Bucket by providing
* a Bucket or File object or a URL starting with "gs://".
* The generation of the file will not be preserved.
*
* See {@link https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite| Objects: rewrite API Documentation}
*
* @throws {Error} If the destination file is not provided.
*
* @param {string|Bucket|File} destination Destination file.
* @param {CopyOptions} [options] Configuration options. See an
* @param {CopyCallback} [callback] Callback function.
* @returns {Promise<CopyResponse>}
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
*
* //-
* // You can pass in a variety of types for the destination.
* //
* // For all of the below examples, assume we are working with the following
* // Bucket and File objects.
* //-
* const bucket = storage.bucket('my-bucket');
* const file = bucket.file('my-image.png');
*
* //-
* // If you pass in a string for the destination, the file is copied to its
* // current bucket, under the new name provided.
* //-
* file.copy('my-image-copy.png', function(err, copiedFile, apiResponse) {
* // `my-bucket` now contains:
* // - "my-image.png"
* // - "my-image-copy.png"
*
* // `copiedFile` is an instance of a File object that refers to your new
* // file.
* });
*
* //-
* // If you pass in a string starting with "gs://" for the destination, the
* // file is copied to the other bucket and under the new name provided.
* //-
* const newLocation = 'gs://another-bucket/my-image-copy.png';
* file.copy(newLocation, function(err, copiedFile, apiResponse) {
* // `my-bucket` still contains:
* // - "my-image.png"
* //
* // `another-bucket` now contains:
* // - "my-image-copy.png"
*
* // `copiedFile` is an instance of a File object that refers to your new
* // file.
* });
*
* //-
* // If you pass in a Bucket object, the file will be copied to that bucket
* // using the same name.
* //-
* const anotherBucket = storage.bucket('another-bucket');
* file.copy(anotherBucket, function(err, copiedFile, apiResponse) {
* // `my-bucket` still contains:
* // - "my-image.png"
* //
* // `another-bucket` now contains:
* // - "my-image.png"
*
* // `copiedFile` is an instance of a File object that refers to your new
* // file.
* });
*
* //-
* // If you pass in a File object, you have complete control over the new
* // bucket and filename.
* //-
* const anotherFile = anotherBucket.file('my-awesome-image.png');
* file.copy(anotherFile, function(err, copiedFile, apiResponse) {
* // `my-bucket` still contains:
* // - "my-image.png"
* //
* // `another-bucket` now contains:
* // - "my-awesome-image.png"
*
* // Note:
* // The `copiedFile` parameter is equal to `anotherFile`.
* });
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* file.copy(newLocation).then(function(data) {
* const newFile = data[0];
* const apiResponse = data[1];
* });
*
* ```
* @example <caption>include:samples/files.js</caption>
* region_tag:storage_copy_file
* Another example:
*/
copy(destination, optionsOrCallback, callback) {
var _a, _b;
const noDestinationError = new Error(FileExceptionMessages.DESTINATION_NO_NAME);
if (!destination) {
throw noDestinationError;
}
let options = {};
if (typeof optionsOrCallback === 'function') {
callback = optionsOrCallback;
}
else if (optionsOrCallback) {
options = { ...optionsOrCallback };
}
callback = callback || index_js_1.util.noop;
let destBucket;
let destName;
let newFile;
if (typeof destination === 'string') {
const parsedDestination = GS_URL_REGEXP.exec(destination);
if (parsedDestination !== null && parsedDestination.length === 3) {
destBucket = this.storage.bucket(parsedDestination[1]);
destName = parsedDestination[2];
}
else {
destBucket = this.bucket;
destName = destination;
}
}
else if (destination instanceof bucket_js_1.Bucket) {
destBucket = destination;
destName = this.name;
}
else if (destination instanceof File) {
destBucket = destination.bucket;
destName = destination.name;
newFile = destination;
}
else {
throw noDestinationError;
}
const query = {};
if (this.generation !== undefined) {
query.sourceGeneration = this.generation;
}
if (options.token !== undefined) {
query.rewriteToken = options.token;
}
if (options.userProject !== undefined) {
query.userProject = options.userProject;
delete options.userProject;
}
if (options.predefinedAcl !== undefined) {
query.destinationPredefinedAcl = options.predefinedAcl;
delete options.predefinedAcl;
}
newFile = newFile || destBucket.file(destName);
const headers = {};
if (this.encryptionKey !== undefined) {
headers['x-goog-copy-source-encryption-algorithm'] = 'AES256';
headers['x-goog-copy-source-encryption-key'] = this.encryptionKeyBase64;
headers['x-goog-copy-source-encryption-key-sha256'] =
this.encryptionKeyHash;
}
if (newFile.encryptionKey !== undefined) {
this.setEncryptionKey(newFile.encryptionKey);
}
else if (options.destinationKmsKeyName !== undefined) {
query.destinationKmsKeyName = options.destinationKmsKeyName;
delete options.destinationKmsKeyName;
}
else if (newFile.kmsKeyName !== undefined) {
query.destinationKmsKeyName = newFile.kmsKeyName;
}
if (query.destinationKmsKeyName) {
this.kmsKeyName = query.destinationKmsKeyName;
const keyIndex = this.interceptors.indexOf(this.encryptionKeyInterceptor);
if (keyIndex > -1) {
this.interceptors.splice(keyIndex, 1);
}
}
if (!this.shouldRetryBasedOnPreconditionAndIdempotencyStrat(options === null || options === void 0 ? void 0 : options.preconditionOpts)) {
this.storage.retryOptions.autoRetry = false;
}
if (((_a = options.preconditionOpts) === null || _a === void 0 ? void 0 : _a.ifGenerationMatch) !== undefined) {
query.ifGenerationMatch = (_b = options.preconditionOpts) === null || _b === void 0 ? void 0 : _b.ifGenerationMatch;
delete options.preconditionOpts;
}
this.request({
method: 'POST',
uri: `/rewriteTo/b/${destBucket.name}/o/${encodeURIComponent(newFile.name)}`,
qs: query,
json: options,
headers,
}, (err, resp) => {
this.storage.retryOptions.autoRetry = this.instanceRetryValue;
if (err) {
callback(err, null, resp);
return;
}
if (resp.rewriteToken) {
const options = {
token: resp.rewriteToken,
};
if (query.userProject) {
options.userProject = query.userProject;
}
if (query.destinationKmsKeyName) {
options.destinationKmsKeyName = query.destinationKmsKeyName;
}
this.copy(newFile, options, callback);
return;
}
callback(null, newFile, resp);
});
}
/**
* @typedef {object} CreateReadStreamOptions Configuration options for File#createReadStream.
* @property {string} [userProject] The ID of the project which will be
* billed for the request.
* @property {string|boolean} [validation] Possible values: `"md5"`,
* `"crc32c"`, or `false`. By default, data integrity is validated with a
* CRC32c checksum. You may use MD5 if preferred, but that hash is not
* supported for composite objects. An error will be raised if MD5 is
* specified but is not available. You may also choose to skip validation
* completely, however this is **not recommended**.
* @property {number} [start] A byte offset to begin the file's download
* from. Default is 0. NOTE: Byte ranges are inclusive; that is,
* `options.start = 0` and `options.end = 999` represent the first 1000
* bytes in a file or object. NOTE: when specifying a byte range, data
* integrity is not available.
* @property {number} [end] A byte offset to stop reading the file at.
* NOTE: Byte ranges are inclusive; that is, `options.start = 0` and
* `options.end = 999` represent the first 1000 bytes in a file or object.
* NOTE: when specifying a byte range, data integrity is not available.
* @property {boolean} [decompress=true] Disable auto decompression of the
* received data. By default this option is set to `true`.
* Applicable in cases where the data was uploaded with
* `gzip: true` option. See {@link File#createWriteStream}.
*/
/**
* Create a readable stream to read the contents of the remote file. It can be
* piped to a writable stream or listened to for 'data' events to read a
* file's contents.
*
* In the unlikely event there is a mismatch between what you downloaded and
* the version in your Bucket, your error handler will receive an error with
* code "CONTENT_DOWNLOAD_MISMATCH". If you receive this error, the best
* recourse is to try downloading the file again.
*
* NOTE: Readable streams will emit the `end` event when the file is fully
* downloaded.
*
* @param {CreateReadStreamOptions} [options] Configuration options.
* @returns {ReadableStream}
*
* @example
* ```
* //-
* // <h4>Downloading a File</h4>
* //
* // The example below demonstrates how we can reference a remote file, then
* // pipe its contents to a local file. This is effectively creating a local
* // backup of your remote data.
* //-
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const bucket = storage.bucket('my-bucket');
*
* const fs = require('fs');
* const remoteFile = bucket.file('image.png');
* const localFilename = '/Users/stephen/Photos/image.png';
*
* remoteFile.createReadStream()
* .on('error', function(err) {})
* .on('response', function(response) {
* // Server connected and responded with the specified status and headers.
* })
* .on('end', function() {
* // The file is fully downloaded.
* })
* .pipe(fs.createWriteStream(localFilename));
*
* //-
* // To limit the downloaded data to only a byte range, pass an options
* // object.
* //-
* const logFile = myBucket.file('access_log');
* logFile.createReadStream({
* start: 10000,
* end: 20000
* })
* .on('error', function(err) {})
* .pipe(fs.createWriteStream('/Users/stephen/logfile.txt'));
*
* //-
* // To read a tail byte range, specify only `options.end` as a negative
* // number.
* //-
* const logFile = myBucket.file('access_log');
* logFile.createReadStream({
* end: -100
* })
* .on('error', function(err) {})
* .pipe(fs.createWriteStream('/Users/stephen/logfile.txt'));
* ```
*/
createReadStream(options = {}) {
options = Object.assign({ decompress: true }, options);
const rangeRequest = typeof options.start === 'number' || typeof options.end === 'number';
const tailRequest = options.end < 0;
let validateStream = undefined;
let request = undefined;
const throughStream = new util_js_2.PassThroughShim();
let crc32c = true;
let md5 = false;
if (typeof options.validation === 'string') {
const value = options.validation.toLowerCase().trim();
crc32c = value === 'crc32c';
md5 = value === 'md5';
}
else if (options.validation === false) {
crc32c = false;
}
const shouldRunValidation = !rangeRequest && (crc32c || md5);
if (rangeRequest) {
if (typeof options.validation === 'string' ||
options.validation === true) {
throw new Error(FileExceptionMessages.INVALID_VALIDATION_FILE_RANGE);
}
// Range requests can't receive data integrity checks.
crc32c = false;
md5 = false;
}
const onComplete = (err) => {
if (err) {
// There is an issue with node-fetch 2.x that if the stream errors the underlying socket connection is not closed.
// This causes a memory leak, so cleanup the sockets manually here by destroying the agent.
if (request === null || request === void 0 ? void 0 : request.agent) {
request.agent.destroy();
}
throughStream.destroy(err);
}
};
// We listen to the response event from the request stream so that we
// can...
//
// 1) Intercept any data from going to the user if an error occurred.
// 2) Calculate the hashes from the http.IncomingMessage response
// stream,
// which will return the bytes from the source without decompressing
// gzip'd content. We then send it through decompressed, if
// applicable, to the user.
const onResponse = (err, _body, rawResponseStream) => {
if (err) {
// Get error message from the body.
this.getBufferFromReadable(rawResponseStream).then(body => {
err.message = body.toString('utf8');
throughStream.destroy(err);
});
return;
}
request = rawResponseStream.request;
const headers = rawResponseStream.toJSON().headers;
const isCompressed = headers['content-encoding'] === 'gzip';
const hashes = {};
// The object is safe to validate if:
// 1. It was stored gzip and returned to us gzip OR
// 2. It was never stored as gzip
const safeToValidate = (headers['x-goog-stored-content-encoding'] === 'gzip' &&
isCompressed) ||
headers['x-goog-stored-content-encoding'] === 'identity';
const transformStreams = [];
if (shouldRunValidation) {
// The x-goog-hash header should be set with a crc32c and md5 hash.
// ex: headers['x-goog-hash'] = 'crc32c=xxxx,md5=xxxx'
if (typeof headers['x-goog-hash'] === 'string') {
headers['x-goog-hash']
.split(',')
.forEach((hashKeyValPair) => {
const delimiterIndex = hashKeyValPair.indexOf('=');
const hashType = hashKeyValPair.substring(0, delimiterIndex);
const hashValue = hashKeyValPair.substring(delimiterIndex + 1);
hashes[hashType] = hashValue;
});
}
validateStream = new hash_stream_validator_js_1.HashStreamValidator({
crc32c,
md5,
crc32cGenerator: this.crc32cGenerator,
crc32cExpected: hashes.crc32c,
md5Expected: hashes.md5,
});
}
if (md5 && !hashes.md5) {
const hashError = new RequestError(FileExceptionMessages.MD5_NOT_AVAILABLE);
hashError.code = 'MD5_NOT_AVAILABLE';
throughStream.destroy(hashError);
return;
}
if (safeToValidate && shouldRunValidation && validateStream) {
transformStreams.push(validateStream);
}
if (isCompressed && options.decompress) {
transformStreams.push(zlib.createGunzip());
}
(0, stream_1.pipeline)(rawResponseStream, ...transformStreams, throughStream, onComplete);
};
// Authenticate the request, then pipe the remote API request to the stream
// returned to the user.
const makeRequest = () => {
const query = { alt: 'media' };
if (this.generation) {
query.generation = this.generation;
}
if (options.userProject) {
query.userProject = options.userProject;
}
const headers = {
'Accept-Encoding': 'gzip',
'Cache-Control': 'no-store',
};
if (rangeRequest) {
const start = typeof options.start === 'number' ? options.start : '0';
const end = typeof options.end === 'number' ? options.end : '';
headers.Range = `bytes=${tailRequest ? end : `${start}-${end}`}`;
}
const reqOpts = {
uri: '',
headers,
qs: query,
};
if (options[util_js_1.GCCL_GCS_CMD_KEY]) {
reqOpts[util_js_1.GCCL_GCS_CMD_KEY] = options[util_js_1.GCCL_GCS_CMD_KEY];
}
this.requestStream(reqOpts)
.on('error', err => {
throughStream.destroy(err);
})
.on('response', res => {
throughStream.emit('response', res);
index_js_1.util.handleResp(null, res, null, onResponse);
})
.resume();
};
throughStream.on('reading', makeRequest);
return throughStream;
}
/**
* @callback CreateResumableUploadCallback
* @param {?Error} err Request error, if any.
* @param {string} uri The resumable upload's unique session URI.
*/
/**
* @typedef {array} CreateResumableUploadResponse
* @property {string} 0 The resumable upload's unique session URI.
*/
/**
* @typedef {object} CreateResumableUploadOptions
* @property {object} [metadata] Metadata to set on the file.
* @property {number} [offset] The starting byte of the upload stream for resuming an interrupted upload.
* @property {string} [origin] Origin header to set for the upload.
* @property {string} [predefinedAcl] Apply a predefined set of access
* controls to this object.
*
* Acceptable values are:
* - **`authenticatedRead`** - Object owner gets `OWNER` access, and
* `allAuthenticatedUsers` get `READER` access.
*
* - **`bucketOwnerFullControl`** - Object owner gets `OWNER` access, and
* project team owners get `OWNER` access.
*
* - **`bucketOwnerRead`** - Object owner gets `OWNER` access, and project
* team owners get `READER` access.
*
* - **`private`** - Object owner gets `OWNER` access.
*
* - **`projectPrivate`** - Object owner gets `OWNER` access, and project
* team members get access according to their roles.
*
* - **`publicRead`** - Object owner gets `OWNER` access, and `allUsers`
* get `READER` access.
* @property {boolean} [private] Make the uploaded file private. (Al