From 740d005e3f0585e1ecb98f2b42025f8729d2c315 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 24 Jun 2026 05:59:44 +0000 Subject: [PATCH 1/6] feat(storage): support delete source objects on compose Updates compose file sample to support deleting source objects optionally. Fixes b/441557254 [Generated-by: AI] --- storage/composeFile.js | 72 +++++++++++++++++++++++++++++++ storage/system-test/files.test.js | 48 +++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 storage/composeFile.js diff --git a/storage/composeFile.js b/storage/composeFile.js new file mode 100644 index 0000000000..0121f07d21 --- /dev/null +++ b/storage/composeFile.js @@ -0,0 +1,72 @@ +// Copyright 2024 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. + +'use strict'; + +/** + * This application demonstrates how to perform basic operations on bucket and + * file Access Control Lists with the Google Cloud Storage API. + * + * For more information, see the README.md under /storage and the documentation + * at https://cloud.google.com/storage/docs. + */ + +function main( + bucketName = 'my-bucket', + firstFileName = 'first.txt', + secondFileName = 'second.txt', + destinationFileName = 'destination.txt', + deleteSourceObjects = false +) { + // [START storage_compose_file] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const bucketName = 'your-unique-bucket-name'; + // const firstFileName = 'first.txt'; + // const secondFileName = 'second.txt'; + // const destinationFileName = 'destination.txt'; + // const deleteSourceObjects = false; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function composeFile() { + const bucket = storage.bucket(bucketName); + const sources = [firstFileName, secondFileName]; + + await bucket.combine(sources, destinationFileName); + + console.log( + `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` + ); + + if (String(deleteSourceObjects) === 'true') { + await Promise.all( + sources.map(async source => { + await bucket.file(source).delete(); + }) + ); + console.log(`Deleted source objects: ${firstFileName}, ${secondFileName}`); + } + } + + composeFile().catch(console.error); + // [END storage_compose_file] +} + +main(...process.argv.slice(2)); diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js index a0147fa8a0..8d2ed77c14 100644 --- a/storage/system-test/files.test.js +++ b/storage/system-test/files.test.js @@ -146,6 +146,54 @@ describe('file', () => { await bucket.file(noContextFileName).delete(); }); }); + + describe('compose file', () => { + it('should combine multiple files into one new file and optionally delete source objects', async () => { + const firstFileName = 'first.txt'; + const secondFileName = 'second.txt'; + const destinationFileName = 'destination.txt'; + + // upload test.txt twice + await bucket.upload(filePath, {destination: firstFileName}); + await bucket.upload(filePath, {destination: secondFileName}); + + // Test with deleteSourceObjects = false + let output = execSync( + `node composeFile.js ${bucketName} ${firstFileName} ${secondFileName} ${destinationFileName} false` + ); + assert.include( + output, + `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` + ); + assert.notInclude(output, 'Deleted source objects'); + + let [destExists] = await bucket.file(destinationFileName).exists(); + assert.strictEqual(destExists, true); + let [firstExists] = await bucket.file(firstFileName).exists(); + assert.strictEqual(firstExists, true); + let [secondExists] = await bucket.file(secondFileName).exists(); + assert.strictEqual(secondExists, true); + + await bucket.file(destinationFileName).delete(); + + // Test with deleteSourceObjects = true + output = execSync( + `node composeFile.js ${bucketName} ${firstFileName} ${secondFileName} ${destinationFileName} true` + ); + assert.include( + output, + `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` + ); + assert.include(output, `Deleted source objects: ${firstFileName}, ${secondFileName}`); + + [destExists] = await bucket.file(destinationFileName).exists(); + assert.strictEqual(destExists, true); + [firstExists] = await bucket.file(firstFileName).exists(); + assert.strictEqual(firstExists, false); + [secondExists] = await bucket.file(secondFileName).exists(); + assert.strictEqual(secondExists, false); + }); + }); }); function generateName() { From 8e7c7c4b3595279bef456761044292c7a5a13edb Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 24 Jun 2026 09:41:30 +0000 Subject: [PATCH 2/6] chore: fix prettier linting issues in composeFile sample --- storage/composeFile.js | 4 +++- storage/system-test/files.test.js | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/storage/composeFile.js b/storage/composeFile.js index 0121f07d21..5d1f0435c5 100644 --- a/storage/composeFile.js +++ b/storage/composeFile.js @@ -61,7 +61,9 @@ function main( await bucket.file(source).delete(); }) ); - console.log(`Deleted source objects: ${firstFileName}, ${secondFileName}`); + console.log( + `Deleted source objects: ${firstFileName}, ${secondFileName}` + ); } } diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js index 8d2ed77c14..f3851c4b21 100644 --- a/storage/system-test/files.test.js +++ b/storage/system-test/files.test.js @@ -184,7 +184,10 @@ describe('file', () => { output, `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` ); - assert.include(output, `Deleted source objects: ${firstFileName}, ${secondFileName}`); + assert.include( + output, + `Deleted source objects: ${firstFileName}, ${secondFileName}` + ); [destExists] = await bucket.file(destinationFileName).exists(); assert.strictEqual(destExists, true); From ec55a0144b9b6ff121317f357ed741907f88ef18 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 24 Jun 2026 10:02:27 +0000 Subject: [PATCH 3/6] fix(storage): resolve PR comments for composeFile [Generated-by: AI] --- storage/composeFile.js | 10 ++++------ storage/system-test/files.test.js | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/storage/composeFile.js b/storage/composeFile.js index 5d1f0435c5..cf5a43c4b8 100644 --- a/storage/composeFile.js +++ b/storage/composeFile.js @@ -15,8 +15,8 @@ 'use strict'; /** - * This application demonstrates how to perform basic operations on bucket and - * file Access Control Lists with the Google Cloud Storage API. + * This application demonstrates how to compose/combine multiple files into a + * single destination file with the Google Cloud Storage API. * * For more information, see the README.md under /storage and the documentation * at https://cloud.google.com/storage/docs. @@ -52,7 +52,7 @@ function main( await bucket.combine(sources, destinationFileName); console.log( - `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` + `New composite file ${destinationFileName} was created by combining ${sources.join(', ')}` ); if (String(deleteSourceObjects) === 'true') { @@ -61,9 +61,7 @@ function main( await bucket.file(source).delete(); }) ); - console.log( - `Deleted source objects: ${firstFileName}, ${secondFileName}` - ); + console.log(`Deleted source objects: ${sources.join(', ')}`); } } diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js index f3851c4b21..c5105c65aa 100644 --- a/storage/system-test/files.test.js +++ b/storage/system-test/files.test.js @@ -163,7 +163,7 @@ describe('file', () => { ); assert.include( output, - `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` + `New composite file ${destinationFileName} was created by combining ${firstFileName}, ${secondFileName}` ); assert.notInclude(output, 'Deleted source objects'); @@ -182,7 +182,7 @@ describe('file', () => { ); assert.include( output, - `New composite file ${destinationFileName} was created by combining ${firstFileName} and ${secondFileName}` + `New composite file ${destinationFileName} was created by combining ${firstFileName}, ${secondFileName}` ); assert.include( output, From e2765c765afec2c7a8e8fa463cc845a9fdcf8273 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 29 Jun 2026 07:13:20 +0000 Subject: [PATCH 4/6] fix(storage): use native SDK option to delete source objects on compose Update composeFile sample to pass deleteSourceObjects option to combine method instead of deleting them manually. [Generated-by: AI] --- storage/composeFile.js | 20 +++++++++----------- storage/system-test/files.test.js | 9 ++------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/storage/composeFile.js b/storage/composeFile.js index cf5a43c4b8..9ef33555a3 100644 --- a/storage/composeFile.js +++ b/storage/composeFile.js @@ -49,20 +49,18 @@ function main( const bucket = storage.bucket(bucketName); const sources = [firstFileName, secondFileName]; - await bucket.combine(sources, destinationFileName); + const options = { + deleteSourceObjects: String(deleteSourceObjects) === 'true', + }; + await bucket.combine(sources, destinationFileName, options); + + const deletionMessage = options.deleteSourceObjects + ? ' and the source objects were deleted.' + : '.'; console.log( - `New composite file ${destinationFileName} was created by combining ${sources.join(', ')}` + `New composite file ${destinationFileName} was created by combining ${sources.join(', ')}${deletionMessage}` ); - - if (String(deleteSourceObjects) === 'true') { - await Promise.all( - sources.map(async source => { - await bucket.file(source).delete(); - }) - ); - console.log(`Deleted source objects: ${sources.join(', ')}`); - } } composeFile().catch(console.error); diff --git a/storage/system-test/files.test.js b/storage/system-test/files.test.js index c5105c65aa..0863af4913 100644 --- a/storage/system-test/files.test.js +++ b/storage/system-test/files.test.js @@ -163,9 +163,8 @@ describe('file', () => { ); assert.include( output, - `New composite file ${destinationFileName} was created by combining ${firstFileName}, ${secondFileName}` + `New composite file ${destinationFileName} was created by combining ${firstFileName}, ${secondFileName}.` ); - assert.notInclude(output, 'Deleted source objects'); let [destExists] = await bucket.file(destinationFileName).exists(); assert.strictEqual(destExists, true); @@ -182,11 +181,7 @@ describe('file', () => { ); assert.include( output, - `New composite file ${destinationFileName} was created by combining ${firstFileName}, ${secondFileName}` - ); - assert.include( - output, - `Deleted source objects: ${firstFileName}, ${secondFileName}` + `New composite file ${destinationFileName} was created by combining ${firstFileName}, ${secondFileName} and the source objects were deleted.` ); [destExists] = await bucket.file(destinationFileName).exists(); From 6dde5441282ad863c6afb92e81f42cc77863a7f3 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 29 Jun 2026 08:22:14 +0000 Subject: [PATCH 5/6] fix(storage): await async execution in composeFile sample Make main async and await composeFile to prevent premature process exit. Correctly set process.exitCode on error to propagate failures. [Generated-by: AI] --- storage/composeFile.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/storage/composeFile.js b/storage/composeFile.js index 9ef33555a3..e19aa4be2a 100644 --- a/storage/composeFile.js +++ b/storage/composeFile.js @@ -22,7 +22,7 @@ * at https://cloud.google.com/storage/docs. */ -function main( +async function main( bucketName = 'my-bucket', firstFileName = 'first.txt', secondFileName = 'second.txt', @@ -63,8 +63,11 @@ function main( ); } - composeFile().catch(console.error); + await composeFile(); // [END storage_compose_file] } -main(...process.argv.slice(2)); +main(...process.argv.slice(2)).catch(err => { + console.error(err); + process.exitCode = 1; +}); From b06ca04a5ffe22fd79a45a5bdf4c80574fb27b13 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 3 Aug 2026 07:51:12 +0000 Subject: [PATCH 6/6] fix(storage): ensure source objects are deleted in compose sample Modified the composeFile.js sample to manually delete source objects when requested. This fixes CI failures where source objects were not being deleted as expected by the tests. [Generated-by: AI] --- storage/composeFile.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/storage/composeFile.js b/storage/composeFile.js index e19aa4be2a..371a9d5b7f 100644 --- a/storage/composeFile.js +++ b/storage/composeFile.js @@ -49,13 +49,14 @@ async function main( const bucket = storage.bucket(bucketName); const sources = [firstFileName, secondFileName]; - const options = { - deleteSourceObjects: String(deleteSourceObjects) === 'true', - }; + await bucket.combine(sources, destinationFileName); - await bucket.combine(sources, destinationFileName, options); + const deleteSources = String(deleteSourceObjects) === 'true'; + if (deleteSources) { + await Promise.all(sources.map(name => bucket.file(name).delete())); + } - const deletionMessage = options.deleteSourceObjects + const deletionMessage = deleteSources ? ' and the source objects were deleted.' : '.'; console.log(