Skip to content

Commit 9f62429

Browse files
fix: allow files in directories to be downloaded onto local machine (#2199)
* fix: allow files in nested folders to be downloaded onto local machine * fix: revert removal of options.prefix check * test: file.download should create directory paths recursively * fix: move the filtering of directory objects from TransferManager.downloadManyFiles to File.download * fix: handle windows path * chore: remove temporary comments * test: add scenarios where destination is set in Transfer Manager * chore: remove console.warn * fix: merge with main * fix: added transfer manager tests for prefix * fix: address PR comments * fix: paths for windows ci * fix: migrate mkdirsync to await fs/promises/mkdir * fix: var name --------- Co-authored-by: Vishwaraj Anand <[email protected]>
1 parent f2e1e0c commit 9f62429

File tree

4 files changed

+122
-4
lines changed

4 files changed

+122
-4
lines changed

src/file.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2288,12 +2288,13 @@ class File extends ServiceObject<File, FileMetadata> {
22882288

22892289
const fileStream = this.createReadStream(options);
22902290
let receivedData = false;
2291+
22912292
if (destination) {
22922293
fileStream
22932294
.on('error', callback)
22942295
.once('data', data => {
2295-
// We know that the file exists the server - now we can truncate/write to a file
22962296
receivedData = true;
2297+
// We know that the file exists the server - now we can truncate/write to a file
22972298
const writable = fs.createWriteStream(destination);
22982299
writable.write(data);
22992300
fileStream

src/transfer-manager.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,9 @@ export class TransferManager {
526526
*
527527
* @param {array | string} [filesOrFolder] An array of file name strings or file objects to be downloaded. If
528528
* a string is provided this will be treated as a GCS prefix and all files with that prefix will be downloaded.
529-
* @param {DownloadManyFilesOptions} [options] Configuration options.
529+
* @param {DownloadManyFilesOptions} [options] Configuration options. Setting options.prefix or options.stripPrefix
530+
* or options.passthroughOptions.destination will cause the downloaded files to be written to the file system
531+
* instead of being returned as a buffer.
530532
* @returns {Promise<DownloadResponse[]>}
531533
*
532534
* @example
@@ -587,7 +589,7 @@ export class TransferManager {
587589
[GCCL_GCS_CMD_KEY]: GCCL_GCS_CMD_FEATURE.DOWNLOAD_MANY,
588590
};
589591

590-
if (options.prefix) {
592+
if (options.prefix || passThroughOptionsCopy.destination) {
591593
passThroughOptionsCopy.destination = path.join(
592594
options.prefix || '',
593595
passThroughOptionsCopy.destination || '',
@@ -598,7 +600,19 @@ export class TransferManager {
598600
passThroughOptionsCopy.destination = file.name.replace(regex, '');
599601
}
600602

601-
promises.push(limit(() => file.download(passThroughOptionsCopy)));
603+
promises.push(
604+
limit(async () => {
605+
const destination = passThroughOptionsCopy.destination;
606+
if (destination && destination.endsWith(path.sep)) {
607+
await fsp.mkdir(destination, {recursive: true});
608+
return Promise.resolve([
609+
Buffer.alloc(0),
610+
]) as Promise<DownloadResponse>;
611+
}
612+
613+
return file.download(passThroughOptionsCopy);
614+
})
615+
);
602616
}
603617

604618
return Promise.all(promises);

test/file.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import assert from 'assert';
2828
import * as crypto from 'crypto';
2929
import duplexify from 'duplexify';
3030
import * as fs from 'fs';
31+
import * as path from 'path';
3132
import proxyquire from 'proxyquire';
3233
import * as resumableUpload from '../src/resumable-upload.js';
3334
import * as sinon from 'sinon';
@@ -2571,6 +2572,12 @@ describe('File', () => {
25712572
});
25722573

25732574
describe('with destination', () => {
2575+
const sandbox = sinon.createSandbox();
2576+
2577+
afterEach(() => {
2578+
sandbox.restore();
2579+
});
2580+
25742581
it('should write the file to a destination if provided', done => {
25752582
tmp.setGracefulCleanup();
25762583
tmp.file((err, tmpFilePath) => {
@@ -2694,6 +2701,29 @@ describe('File', () => {
26942701
});
26952702
});
26962703
});
2704+
2705+
it('should fail if provided destination directory does not exist', done => {
2706+
tmp.setGracefulCleanup();
2707+
tmp.dir(async (err, tmpDirPath) => {
2708+
assert.ifError(err);
2709+
2710+
const fileContents = 'nested-abcdefghijklmnopqrstuvwxyz';
2711+
2712+
Object.assign(fileReadStream, {
2713+
_read(this: Readable) {
2714+
this.push(fileContents);
2715+
this.push(null);
2716+
},
2717+
});
2718+
2719+
const nestedPath = path.join(tmpDirPath, 'a', 'b', 'c', 'file.txt');
2720+
2721+
file.download({destination: nestedPath}, (err: Error) => {
2722+
assert.ok(err);
2723+
done();
2724+
});
2725+
});
2726+
});
26972727
});
26982728
});
26992729

test/transfer-manager.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
Bucket,
2020
File,
2121
CRC32C,
22+
DownloadCallback,
2223
DownloadOptions,
2324
IdempotencyStrategy,
2425
MultiPartHelperGenerator,
@@ -233,6 +234,78 @@ describe('Transfer Manager', () => {
233234

234235
await transferManager.downloadManyFiles([file]);
235236
});
237+
238+
it('sets the destination correctly when provided a passthroughOptions.destination', async () => {
239+
const passthroughOptions = {
240+
destination: 'test-destination',
241+
};
242+
const filename = 'first.txt';
243+
const expectedDestination = path.normalize(
244+
`${passthroughOptions.destination}/${filename}`
245+
);
246+
const download = (optionsOrCb?: DownloadOptions | DownloadCallback) => {
247+
if (typeof optionsOrCb === 'function') {
248+
optionsOrCb(null, Buffer.alloc(0));
249+
} else if (optionsOrCb) {
250+
assert.strictEqual(optionsOrCb.destination, expectedDestination);
251+
}
252+
return Promise.resolve([Buffer.alloc(0)]) as Promise<DownloadResponse>;
253+
};
254+
255+
const file = new File(bucket, filename);
256+
file.download = download;
257+
await transferManager.downloadManyFiles([file], {passthroughOptions});
258+
});
259+
260+
it('does not set the destination when prefix, strip prefix and passthroughOptions.destination are not provided', async () => {
261+
const options = {};
262+
const filename = 'first.txt';
263+
const download = (optionsOrCb?: DownloadOptions | DownloadCallback) => {
264+
if (typeof optionsOrCb === 'function') {
265+
optionsOrCb(null, Buffer.alloc(0));
266+
} else if (optionsOrCb) {
267+
assert.strictEqual(optionsOrCb.destination, undefined);
268+
}
269+
return Promise.resolve([Buffer.alloc(0)]) as Promise<DownloadResponse>;
270+
};
271+
272+
const file = new File(bucket, filename);
273+
file.download = download;
274+
await transferManager.downloadManyFiles([file], options);
275+
});
276+
277+
it('should recursively create directory and write file contents if destination path is nested', async () => {
278+
const prefix = 'text-prefix';
279+
const folder = 'nestedFolder/';
280+
const file = 'first.txt';
281+
const filesOrFolder = [folder, path.join(folder, file)];
282+
const expectedFilePath = path.join(prefix, folder, file);
283+
const expectedDir = path.join(prefix, folder);
284+
const mkdirSpy = sandbox.spy(fsp, 'mkdir');
285+
const download = (optionsOrCb?: DownloadOptions | DownloadCallback) => {
286+
if (typeof optionsOrCb === 'function') {
287+
optionsOrCb(null, Buffer.alloc(0));
288+
} else if (optionsOrCb) {
289+
assert.strictEqual(optionsOrCb.destination, expectedFilePath);
290+
}
291+
return Promise.resolve([Buffer.alloc(0)]) as Promise<DownloadResponse>;
292+
};
293+
294+
sandbox.stub(bucket, 'file').callsFake(filename => {
295+
const file = new File(bucket, filename);
296+
file.download = download;
297+
return file;
298+
});
299+
await transferManager.downloadManyFiles(filesOrFolder, {
300+
prefix: prefix,
301+
});
302+
assert.strictEqual(
303+
mkdirSpy.calledOnceWith(expectedDir, {
304+
recursive: true,
305+
}),
306+
true
307+
);
308+
});
236309
});
237310

238311
describe('downloadFileInChunks', () => {

0 commit comments

Comments
 (0)