From cacc0be49a4fe81c384180bdfd77820b6b3f3001 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 30 Jul 2025 15:24:29 +0000 Subject: [PATCH 01/10] feat: add CSEK to download (#2604) --- src/file.ts | 6 ++++++ test/file.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/file.ts b/src/file.ts index 8b51fcb26..68bbc1d10 100644 --- a/src/file.ts +++ b/src/file.ts @@ -403,6 +403,7 @@ export type DownloadCallback = ( export interface DownloadOptions extends CreateReadStreamOptions { destination?: string; + encryptionKey?: string | Buffer; } interface CopyQuery { @@ -2309,6 +2310,11 @@ class File extends ServiceObject { const destination = options.destination; delete options.destination; + if (options.encryptionKey) { + this.setEncryptionKey(options.encryptionKey); + delete options.encryptionKey; + } + const fileStream = this.createReadStream(options); let receivedData = false; diff --git a/test/file.ts b/test/file.ts index de4837ff7..d0fe7545b 100644 --- a/test/file.ts +++ b/test/file.ts @@ -2490,6 +2490,7 @@ describe('File', () => { describe('download', () => { let fileReadStream: Readable; + let originalSetEncryptionKey: Function; beforeEach(() => { fileReadStream = new Readable(); @@ -2502,6 +2503,13 @@ describe('File', () => { file.createReadStream = () => { return fileReadStream; }; + + originalSetEncryptionKey = file.setEncryptionKey; + file.setEncryptionKey = sinon.stub(); + }); + + afterEach(() => { + file.setEncryptionKey = originalSetEncryptionKey; }); it('should accept just a callback', done => { @@ -2532,6 +2540,31 @@ describe('File', () => { file.download(readOptions, assert.ifError); }); + it('should call setEncryptionKey with the provided key and not pass it to createReadStream', done => { + const encryptionKey = Buffer.from('encryption-key'); + const downloadOptions = { + encryptionKey: encryptionKey, + userProject: 'user-project-id', + }; + + file.createReadStream = (options: {}) => { + assert.deepStrictEqual(options, {userProject: 'user-project-id'}); + return fileReadStream; + }; + + file.download(downloadOptions, (err: Error) => { + assert.ifError(err); + // Verify that setEncryptionKey was called with the correct key + assert.ok( + (file.setEncryptionKey as sinon.SinonStub).calledWith(encryptionKey) + ); + done(); + }); + + fileReadStream.push('some data'); + fileReadStream.push(null); + }); + it('should only execute callback once', done => { Object.assign(fileReadStream, { _read(this: Readable) { From 7bb3430ab773c5f31d6c39a90c2562983369442c Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 30 Jul 2025 17:29:22 +0200 Subject: [PATCH 02/10] chore(deps): update dependency @grpc/proto-loader to ^0.8.0 (#2617) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de71121ed..78fc6c191 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@babel/core": "^7.22.11", "@google-cloud/pubsub": "^4.0.0", "@grpc/grpc-js": "^1.0.3", - "@grpc/proto-loader": "^0.7.0", + "@grpc/proto-loader": "^0.8.0", "@types/async-retry": "^1.4.3", "@types/duplexify": "^3.6.4", "@types/mime": "^3.0.0", From f6b68dc116a6e23fdda8495ea143b79f11a01698 Mon Sep 17 00:00:00 2001 From: Denis DelGrosso <85250797+ddelgrosso1@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:08:26 -0400 Subject: [PATCH 03/10] build: make docs check not required (#2623) --- .github/sync-repo-settings.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 28901a224..8bcfe112d 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -12,7 +12,6 @@ branchProtectionRules: requiredStatusCheckContexts: - "ci/kokoro: Samples test" - "ci/kokoro: System test" - - docs - lint - test (14) - test (16) From 9cae69cc280227737b5a1a1476eae1b2612b162b Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 30 Jul 2025 16:48:17 +0000 Subject: [PATCH 04/10] fix: typo correction (#2610) --- src/bucket.ts | 10 +++++----- src/file.ts | 2 +- src/storage.ts | 2 +- src/transfer-manager.ts | 8 ++++---- src/util.ts | 4 ++-- system-test/storage.ts | 10 +++++----- test/bucket.ts | 4 ++-- test/file.ts | 26 +++++++++++++------------- test/hmacKey.ts | 2 +- test/index.ts | 2 +- test/nodejs-common/service-object.ts | 2 +- test/nodejs-common/util.ts | 14 +++++++------- test/signer.ts | 2 +- test/transfer-manager.ts | 2 +- 14 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/bucket.ts b/src/bucket.ts index b8203c510..6169e3b28 100644 --- a/src/bucket.ts +++ b/src/bucket.ts @@ -1325,7 +1325,7 @@ class Bucket extends ServiceObject { * **Note**: For configuring a raw-formatted rule object to be passed as `action` * please refer to the [examples]{@link https://cloud.google.com/storage/docs/managing-lifecycles#configexamples}. * @param {object} rule.condition Condition a bucket must meet before the - * action occurson the bucket. Refer to followitn supported [conditions]{@link https://cloud.google.com/storage/docs/lifecycle#conditions}. + * action occurs on the bucket. Refer to following supported [conditions]{@link https://cloud.google.com/storage/docs/lifecycle#conditions}. * @param {string} [rule.storageClass] When using the `setStorageClass` * action, provide this option to dictate which storage class the object * should update to. @@ -1950,7 +1950,7 @@ class Bucket extends ServiceObject { * myBucket.createNotification('my-topic', callback); * * //- - * // Configure the nofiication by providing Notification metadata. + * // Configure the notification by providing Notification metadata. * //- * const metadata = { * objectNamePrefix: 'prefix-' @@ -3071,7 +3071,7 @@ class Bucket extends ServiceObject { * @property {boolean} [virtualHostedStyle=false] Use virtual hosted-style * URLs ('https://mybucket.storage.googleapis.com/...') instead of path-style * ('https://storage.googleapis.com/mybucket/...'). Virtual hosted-style URLs - * should generally be preferred instaed of path-style URL. + * should generally be preferred instead of path-style URL. * Currently defaults to `false` for path-style, although this may change in a * future major-version release. * @property {string} [cname] The cname for this bucket, i.e., @@ -3118,7 +3118,7 @@ class Bucket extends ServiceObject { * @param {boolean} [config.virtualHostedStyle=false] Use virtual hosted-style * URLs ('https://mybucket.storage.googleapis.com/...') instead of path-style * ('https://storage.googleapis.com/mybucket/...'). Virtual hosted-style URLs - * should generally be preferred instaed of path-style URL. + * should generally be preferred instead of path-style URL. * Currently defaults to `false` for path-style, although this may change in a * future major-version release. * @param {string} [config.cname] The cname for this bucket, i.e., @@ -3222,7 +3222,7 @@ class Bucket extends ServiceObject { * @throws {Error} if a metageneration is not provided. * * @param {number|string} metageneration The bucket's metageneration. This is - * accesssible from calling {@link File#getMetadata}. + * accessible from calling {@link File#getMetadata}. * @param {BucketLockCallback} [callback] Callback function. * @returns {Promise} * diff --git a/src/file.ts b/src/file.ts index 68bbc1d10..737299549 100644 --- a/src/file.ts +++ b/src/file.ts @@ -2982,7 +2982,7 @@ class File extends ServiceObject { * @param {boolean} [config.virtualHostedStyle=false] Use virtual hosted-style * URLs (e.g. 'https://mybucket.storage.googleapis.com/...') instead of path-style * (e.g. 'https://storage.googleapis.com/mybucket/...'). Virtual hosted-style URLs - * should generally be preferred instaed of path-style URL. + * should generally be preferred instead of path-style URL. * Currently defaults to `false` for path-style, although this may change in a * future major-version release. * @param {string} [config.cname] The cname for this bucket, i.e., diff --git a/src/storage.ts b/src/storage.ts index 88651f222..e923b32a0 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -896,7 +896,7 @@ export class Storage extends Service { * For more information, see {@link https://cloud.google.com/storage/docs/locations| Bucket Locations}. * @property {boolean} [dra=false] Specify the storage class as Durable Reduced * Availability. - * @property {boolean} [enableObjectRetention=false] Specifiy whether or not object retention should be enabled on this bucket. + * @property {boolean} [enableObjectRetention=false] Specify whether or not object retention should be enabled on this bucket. * @property {object} [hierarchicalNamespace.enabled=false] Specify whether or not to enable hierarchical namespace on this bucket. * @property {string} [location] Specify the bucket's location. If specifying * a dual-region, the `customPlacementConfig` property should be set in conjunction. diff --git a/src/transfer-manager.ts b/src/transfer-manager.ts index 234824d77..dd4e41eeb 100644 --- a/src/transfer-manager.ts +++ b/src/transfer-manager.ts @@ -416,7 +416,7 @@ export class TransferManager { * @typedef {object} UploadManyFilesOptions * @property {number} [concurrencyLimit] The number of concurrently executing promises * to use when uploading the files. - * @property {Function} [customDestinationBuilder] A fuction that will take the current path of a local file + * @property {Function} [customDestinationBuilder] A function that will take the current path of a local file * and return a string representing a custom path to be used to upload the file to GCS. * @property {boolean} [skipIfExists] Do not upload the file if it already exists in * the bucket. This will set the precondition ifGenerationMatch = 0. @@ -744,7 +744,7 @@ export class TransferManager { * @property {number} [concurrencyLimit] The number of concurrently executing promises * to use when uploading the file. * @property {number} [chunkSizeBytes] The size in bytes of each chunk to be uploaded. - * @property {string} [uploadName] Name of the file when saving to GCS. If ommitted the name is taken from the file path. + * @property {string} [uploadName] Name of the file when saving to GCS. If omitted the name is taken from the file path. * @property {number} [maxQueueSize] The number of chunks to be uploaded to hold in memory concurrently. If not specified * defaults to the specified concurrency limit. * @property {string} [uploadId] If specified attempts to resume a previous upload. @@ -757,14 +757,14 @@ export class TransferManager { * */ /** - * Upload a large file in chunks utilizing parallel upload opertions. If the upload fails, an uploadId and + * Upload a large file in chunks utilizing parallel upload operations. If the upload fails, an uploadId and * map containing all the successfully uploaded parts will be returned to the caller. These arguments can be used to * resume the upload. * * @param {string} [filePath] The path of the file to be uploaded * @param {UploadFileInChunksOptions} [options] Configuration options. * @param {MultiPartHelperGenerator} [generator] A function that will return a type that implements the MPU interface. Most users will not need to use this. - * @returns {Promise} If successful a promise resolving to void, otherwise a error containing the message, uploadid, and parts map. + * @returns {Promise} If successful a promise resolving to void, otherwise a error containing the message, uploadId, and parts map. * * @example * ``` diff --git a/src/util.ts b/src/util.ts index 896edc586..259f7c0f3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -148,7 +148,7 @@ export function convertObjKeysToSnakeCase(obj: object): object { * @param {boolean} includeTime flag to include hours, minutes, seconds in output. * @param {string} dateDelimiter delimiter between date components. * @param {string} timeDelimiter delimiter between time components. - * @returns {string} UTC ISO format of provided date obect. + * @returns {string} UTC ISO format of provided date object. */ export function formatAsUTCISO( dateTimeToFormat: Date, @@ -252,7 +252,7 @@ export class PassThroughShim extends PassThrough { this.emit('writing'); this.shouldEmitWriting = false; } - // Per the nodejs documention, callback must be invoked on the next tick + // Per the nodejs documentation, callback must be invoked on the next tick process.nextTick(() => { super._write(chunk, encoding, callback); }); diff --git a/system-test/storage.ts b/system-test/storage.ts index e630bb833..32190f65b 100644 --- a/system-test/storage.ts +++ b/system-test/storage.ts @@ -435,9 +435,9 @@ describe('storage', function () { resumable: false, }); const [metadata] = await file.getMetadata(); - const encyrptionAlgorithm = + const encryptionAlgorithm = metadata.customerEncryption?.encryptionAlgorithm; - assert.strictEqual(encyrptionAlgorithm, 'AES256'); + assert.strictEqual(encryptionAlgorithm, 'AES256'); }); it('should set custom encryption in a resumable upload', async () => { @@ -447,9 +447,9 @@ describe('storage', function () { resumable: true, }); const [metadata] = await file.getMetadata(); - const encyrptionAlgorithm = + const encryptionAlgorithm = metadata.customerEncryption?.encryptionAlgorithm; - assert.strictEqual(encyrptionAlgorithm, 'AES256'); + assert.strictEqual(encryptionAlgorithm, 'AES256'); }); it('should make a file public during the upload', async () => { @@ -2778,7 +2778,7 @@ describe('storage', function () { }); }); - it('should download from the encrytped file', async () => { + it('should download from the encrypted file', async () => { const [contents] = await file.download(); assert.strictEqual(contents.toString(), 'secret data'); }); diff --git a/test/bucket.ts b/test/bucket.ts index 5e0255e26..5b49fa518 100644 --- a/test/bucket.ts +++ b/test/bucket.ts @@ -197,7 +197,7 @@ describe('Bucket', () => { retryOptions: { autoRetry: true, maxRetries: 3, - retryDelayMultipier: 2, + retryDelayMultiplier: 2, totalTimeout: 600, maxRetryDelay: 60, retryableErrorFn: (err: HTTPError) => { @@ -2631,7 +2631,7 @@ describe('Bucket', () => { bucket.setStorageClass('hyphenated-class', OPTIONS, CALLBACK); }); - it('should call setMetdata correctly', () => { + it('should call setMetadata correctly', () => { bucket.setMetadata = ( metadata: BucketMetadata, options: {}, diff --git a/test/file.ts b/test/file.ts index d0fe7545b..9748836ae 100644 --- a/test/file.ts +++ b/test/file.ts @@ -237,7 +237,7 @@ describe('File', () => { retryOptions: { autoRetry: true, maxRetries: 3, - retryDelayMultipier: 2, + retryDelayMultiplier: 2, totalTimeout: 600, maxRetryDelay: 60, retryableErrorFn: (err: HTTPError) => { @@ -282,7 +282,7 @@ describe('File', () => { assert.strictEqual(file.storage, BUCKET.storage); }); - it('should set instanceRetryValue to the storage insance retryOptions.autoRetry value', () => { + it('should set instanceRetryValue to the storage instance retryOptions.autoRetry value', () => { assert.strictEqual( file.instanceRetryValue, STORAGE.retryOptions.autoRetry @@ -1815,7 +1815,7 @@ describe('File', () => { autoRetry: true, maxRetries: 3, maxRetryDelay: 60, - retryDelayMultipier: 2, + retryDelayMultiplier: 2, totalTimeout: 600, }, preconditionOpts: { @@ -1860,8 +1860,8 @@ describe('File', () => { options.retryOptions.maxRetryDelay ); assert.strictEqual( - opts.retryOptions.retryDelayMultipier, - options.retryOptions.retryDelayMultipier + opts.retryOptions.retryDelayMultiplier, + options.retryOptions.retryDelayMultiplier ); assert.strictEqual( opts.retryOptions.totalTimeout, @@ -1898,7 +1898,7 @@ describe('File', () => { autoRetry: true, maxRetries: 3, maxRetryDelay: 60, - retryDelayMultipier: 2, + retryDelayMultiplier: 2, totalTimeout: 600, }, }; @@ -1939,8 +1939,8 @@ describe('File', () => { options.retryOptions.maxRetryDelay ); assert.strictEqual( - opts.retryOptions.retryDelayMultipier, - options.retryOptions.retryDelayMultipier + opts.retryOptions.retryDelayMultiplier, + options.retryOptions.retryDelayMultiplier ); assert.strictEqual( opts.retryOptions.totalTimeout, @@ -1996,11 +1996,11 @@ describe('File', () => { 'Cannot provide an `offset` without providing a `uri`' ); - const opitons = { + const options = { offset: 1, isPartialUpload: true, }; - const writable = file.createWriteStream(opitons); + const writable = file.createWriteStream(options); writable.on('error', (err: RangeError) => { assert.deepEqual(err, error); @@ -2919,7 +2919,7 @@ describe('File', () => { ); }); - it('should add ACL condtion', done => { + it('should add ACL condition', done => { file.generateSignedPostPolicyV2( { expires: Date.now() + 2000, @@ -3151,7 +3151,7 @@ describe('File', () => { ); }); - it('should throw if prexif condition is not an array', () => { + it('should throw if prefix condition is not an array', () => { assert.throws(() => { file.generateSignedPostPolicyV2( { @@ -4558,7 +4558,7 @@ describe('File', () => { } } - describe('retry mulipart upload', () => { + describe('retry multipart upload', () => { it('should save a string with no errors', async () => { const options = {resumable: false}; file.createWriteStream = () => { diff --git a/test/hmacKey.ts b/test/hmacKey.ts index fc8e858f4..309b98835 100644 --- a/test/hmacKey.ts +++ b/test/hmacKey.ts @@ -60,7 +60,7 @@ describe('HmacKey', () => { retryOptions: { autoRetry: true, maxRetries: 3, - retryDelayMultipier: 2, + retryDelayMultiplier: 2, totalTimeout: 600, maxRetryDelay: 60, retryableErrorFn: (err: HTTPError) => { diff --git a/test/index.ts b/test/index.ts index 6d10c02a3..dec867bdc 100644 --- a/test/index.ts +++ b/test/index.ts @@ -500,7 +500,7 @@ describe('Storage', () => { ); }); - it('should be overriden by apiEndpoint', () => { + it('should be overridden by apiEndpoint', () => { const storage = new Storage({ projectId: PROJECT_ID, apiEndpoint: 'https://some.api.com', diff --git a/test/nodejs-common/service-object.ts b/test/nodejs-common/service-object.ts index b9dc994c1..3bba5f4fa 100644 --- a/test/nodejs-common/service-object.ts +++ b/test/nodejs-common/service-object.ts @@ -337,7 +337,7 @@ describe('ServiceObject', () => { serviceObject.delete(); }); - it('should respect ignoreNotFound opion', done => { + it('should respect ignoreNotFound option', done => { const options = {ignoreNotFound: true}; const error = new ApiError({code: 404, response: {} as r.Response}); sandbox.stub(ServiceObject.prototype, 'request').callsArgWith(1, error); diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index 55fe014c3..3efc73d11 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -412,22 +412,22 @@ describe('common/util', () => { }); it('should handle non-JSON body', done => { - const unparseableBody = 'Unparseable body.'; + const unparsableBody = 'Unparsable body.'; - util.handleResp(null, null, unparseableBody, (err, body) => { - assert(body.includes(unparseableBody)); + util.handleResp(null, null, unparsableBody, (err, body) => { + assert(body.includes(unparsableBody)); done(); }); }); it('should include the status code when the error body cannot be JSON-parsed', done => { - const unparseableBody = 'Bad gateway'; + const unparsableBody = 'Bad gateway'; const statusCode = 502; util.handleResp( null, - {body: unparseableBody, statusCode} as r.Response, - unparseableBody, + {body: unparsableBody, statusCode} as r.Response, + unparsableBody, err => { assert(err, 'there should be an error'); const apiError = err! as ApiError; @@ -437,7 +437,7 @@ describe('common/util', () => { if (!response) { assert.fail('there should be a response property on the error'); } else { - assert.strictEqual(response.body, unparseableBody); + assert.strictEqual(response.body, unparsableBody); } done(); diff --git a/test/signer.ts b/test/signer.ts index fd296cf76..6e840ac67 100644 --- a/test/signer.ts +++ b/test/signer.ts @@ -371,7 +371,7 @@ describe('signer', () => { .resolves(query) as sinon.SinonStub; }); - it('shuold insert user-provided queryParams', async () => { + it('should insert user-provided queryParams', async () => { CONFIG.queryParams = {key: 'AZ!*()*%/f'}; const url = await signer.getSignedUrl(CONFIG); diff --git a/test/transfer-manager.ts b/test/transfer-manager.ts index c2d0d750c..2582782fa 100644 --- a/test/transfer-manager.ts +++ b/test/transfer-manager.ts @@ -164,7 +164,7 @@ describe('Transfer Manager', () => { await transferManager.uploadManyFiles(paths, {prefix: 'hello/world'}); }); - it('replaces OS specfic separator with posix separator when calling bucket.upload', async () => { + it('replaces OS specific separator with posix separator when calling bucket.upload', async () => { const filePath = ['a', 'b', 'c'].join(path.sep); const expected = ['a', 'b', 'c'].join(path.posix.sep); From 712918635634485b83607e1f841befd32e40aced Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 30 Jul 2025 18:52:41 +0200 Subject: [PATCH 05/10] chore(deps): update dependency jsdoc-fresh to v4 (#2597) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78fc6c191..da63212b3 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "gapic-tools": "^0.4.0", "gts": "^5.0.0", "jsdoc": "^4.0.0", - "jsdoc-fresh": "^3.0.0", + "jsdoc-fresh": "^4.0.0", "jsdoc-region-tag": "^3.0.0", "linkinator": "^3.0.0", "mocha": "^9.2.2", From bcf58f08a0862218eeecc2a17f2c84aed8cfded8 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 1 Aug 2025 14:51:58 +0000 Subject: [PATCH 06/10] build: resolve issue with build process (#2619) * fix-build issue * Merge branch 'fix-build-issue' of https://github.com/googleapis/nodejs-storage into fix-build-issue * fix: Correct `setObjectRetentionPolicy` sample to extend retention period * build: Correct `setObjectRetentionPolicy` sample to extend retention period * Correct license year to 2024 --- samples/setObjectRetentionPolicy.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/samples/setObjectRetentionPolicy.js b/samples/setObjectRetentionPolicy.js index fc5c2ab1e..9cfe3444c 100644 --- a/samples/setObjectRetentionPolicy.js +++ b/samples/setObjectRetentionPolicy.js @@ -70,15 +70,18 @@ function main( ); // To modify an existing policy on an unlocked file object, pass in the override parameter - const newRetentionDate = new Date(); - retentionDate.setDate(retentionDate.getDate() + 9); - [metdata] = await file.setMetadata({ - retention: {retainUntilTime: newRetentionDate}, + const newRetentionDate = new Date(retentionDate.getDate()); + newRetentionDate.setDate(newRetentionDate.getDate() + 9); + const [newMetadata] = await file.setMetadata({ + retention: { + mode: 'Unlocked', + retainUntilTime: newRetentionDate, + }, overrideUnlockedRetention: true, }); console.log( - `Retention policy for file ${file.name} was updated to: ${metadata.retention.retainUntilTime}` + `Retention policy for file ${file.name} was updated to: ${newMetadata.retention.retainUntilTime}` ); } From a43b4904ecf2ebacde22bc6efbdcf97ac886e28d Mon Sep 17 00:00:00 2001 From: Wilfred van der Deijl Date: Mon, 11 Aug 2025 15:30:08 +0200 Subject: [PATCH 07/10] fix: propagate errors when using pipelines (#2560) (#2624) * fix: propagate errors when using pipelines (#2560) * fi test case * fix test case * fix(test): destroy uploadStream after error for node 14 close event --------- Co-authored-by: Thiyagu K --- src/file.ts | 7 ++++++ test/file.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/file.ts b/src/file.ts index 737299549..57f0645a1 100644 --- a/src/file.ts +++ b/src/file.ts @@ -2100,6 +2100,10 @@ class File extends ServiceObject { const emitStream = new PassThroughShim(); + // If `writeStream` is destroyed before the `writing` event, `emitStream` will not have any listeners. This prevents an unhandled error. + const noop = () => {}; + emitStream.on('error', noop); + let hashCalculatingStream: HashStreamValidator | null = null; if (crc32c || md5) { @@ -2138,6 +2142,9 @@ class File extends ServiceObject { this.startResumableUpload_(fileWriteStream, options); } + // remove temporary noop listener as we now create a pipeline that handles the errors + emitStream.removeListener('error', noop); + pipeline( emitStream, ...(transformStreams as [Transform]), diff --git a/test/file.ts b/test/file.ts index 9748836ae..564d10cff 100644 --- a/test/file.ts +++ b/test/file.ts @@ -23,7 +23,14 @@ import { } from '../src/nodejs-common/index.js'; import {describe, it, before, beforeEach, afterEach} from 'mocha'; import {PromisifyAllOptions} from '@google-cloud/promisify'; -import {Readable, PassThrough, Stream, Duplex, Transform} from 'stream'; +import { + Readable, + PassThrough, + Stream, + Duplex, + Transform, + pipeline, +} from 'stream'; import assert from 'assert'; import * as crypto from 'crypto'; import duplexify from 'duplexify'; @@ -2281,6 +2288,67 @@ describe('File', () => { writable.end('data'); }); + it('should close upstream when pipeline fails', done => { + const writable: Stream.Writable = file.createWriteStream(); + const error = new Error('My error'); + const uploadStream = new PassThrough(); + + let receivedBytes = 0; + const validateStream = new PassThrough(); + validateStream.on('data', (chunk: Buffer) => { + receivedBytes += chunk.length; + if (receivedBytes > 5) { + // this aborts the pipeline which should also close the internal pipeline within createWriteStream + pLine.destroy(error); + } + }); + + file.startResumableUpload_ = (dup: duplexify.Duplexify) => { + dup.setWritable(uploadStream); + // Emit an error so the pipeline's error-handling logic is triggered + uploadStream.emit('error', error); + // Explicitly destroy the stream so that the 'close' event is guaranteed to fire, + // even in Node v14 where autoDestroy defaults may prevent automatic closing + uploadStream.destroy(); + }; + + let closed = false; + uploadStream.on('close', () => { + closed = true; + }); + + const pLine = pipeline( + (function* () { + yield 'foo'; // write some data + yield 'foo'; // write some data + yield 'foo'; // write some data + })(), + validateStream, + writable, + (e: Error | null) => { + assert.strictEqual(e, error); + assert.strictEqual(closed, true); + done(); + } + ); + }); + + it('should error pipeline if source stream emits error before any data', done => { + const writable = file.createWriteStream(); + const error = new Error('Error before first chunk'); + pipeline( + // eslint-disable-next-line require-yield + (function* () { + throw error; + })(), + writable, + (e: Error | null) => { + assert.strictEqual(e, error); + done(); + } + ); + }); + describe('validation', () => { const data = 'test'; From 288e81ebb06118699ed1b7c5164ba0cad096023d Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 15 Aug 2025 00:40:58 +0530 Subject: [PATCH 08/10] chore(deps): update dependency form data to v4.0.4 (#2621) * fix-form-data-vuln * fix(deps): address form-data vulnerability GHSA-fjxv-7rqg-78g4 * update dependency jsdoc-fresh to v4 * trigger CI --------- Co-authored-by: Denis DelGrosso --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index da63212b3..9beec1a6a 100644 --- a/package.json +++ b/package.json @@ -108,10 +108,10 @@ "@types/uuid": "^8.0.0", "@types/yargs": "^17.0.10", "c8": "^9.0.0", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "gapic-tools": "^0.4.0", "gts": "^5.0.0", - "jsdoc": "^4.0.0", + "jsdoc": "^4.0.4", "jsdoc-fresh": "^4.0.0", "jsdoc-region-tag": "^3.0.0", "linkinator": "^3.0.0", From c2c7df6357b0ceb97d6a1e0e15590d959b728d35 Mon Sep 17 00:00:00 2001 From: Denis DelGrosso <85250797+ddelgrosso1@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:12:47 -0400 Subject: [PATCH 09/10] build: remove docs check from CI (#2634) * build: remove docs check from CI * add excluse * fix format --- .github/workflows/ci.yaml | 12 ------------ owlbot.py | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4892eb2c5..18ec4115b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,15 +46,3 @@ jobs: node-version: 14 - run: npm install - run: npm run lint - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 14 - - run: npm install - - run: npm run docs - - uses: JustinBeckwith/linkinator-action@v1 - with: - paths: docs/ diff --git a/owlbot.py b/owlbot.py index 8e2e3b940..3df451978 100644 --- a/owlbot.py +++ b/owlbot.py @@ -24,6 +24,7 @@ s.copy(templates, excludes=['.jsdoc.js', '.github/release-please.yml', '.github/sync-repo-settings.yaml', + '.github/workflows/ci.yaml', '.prettierrc.js', '.mocharc.js', '.kokoro/continuous/node14/system-test.cfg', From 7bcb04f4ae51f01ba3bf7244c12a842c1953b804 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:25:45 -0400 Subject: [PATCH 10/10] chore(main): release 7.17.0 (#2622) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1849703..221b624bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://www.npmjs.com/package/@google-cloud/storage?activeTab=versions +## [7.17.0](https://github.com/googleapis/nodejs-storage/compare/v7.16.0...v7.17.0) (2025-08-18) + + +### Features + +* Add CSEK to download ([#2604](https://github.com/googleapis/nodejs-storage/issues/2604)) ([cacc0be](https://github.com/googleapis/nodejs-storage/commit/cacc0be49a4fe81c384180bdfd77820b6b3f3001)) + + +### Bug Fixes + +* Propagate errors when using pipelines ([#2560](https://github.com/googleapis/nodejs-storage/issues/2560)) ([#2624](https://github.com/googleapis/nodejs-storage/issues/2624)) ([a43b490](https://github.com/googleapis/nodejs-storage/commit/a43b4904ecf2ebacde22bc6efbdcf97ac886e28d)) +* Typo correction ([#2610](https://github.com/googleapis/nodejs-storage/issues/2610)) ([9cae69c](https://github.com/googleapis/nodejs-storage/commit/9cae69cc280227737b5a1a1476eae1b2612b162b)) + ## [7.16.0](https://github.com/googleapis/nodejs-storage/compare/v7.15.2...v7.16.0) (2025-03-31) diff --git a/package.json b/package.json index 9beec1a6a..46dbf04d0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@google-cloud/storage", "description": "Cloud Storage Client Library for Node.js", - "version": "7.16.0", + "version": "7.17.0", "license": "Apache-2.0", "author": "Google Inc.", "engines": { diff --git a/samples/package.json b/samples/package.json index 93b0e6f23..401334cec 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@google-cloud/pubsub": "^4.0.0", - "@google-cloud/storage": "^7.16.0", + "@google-cloud/storage": "^7.17.0", "node-fetch": "^2.6.7", "uuid": "^8.0.0", "yargs": "^16.0.0"