Skip to content

Commit 86086a6

Browse files
authored
feat(storage): enable CRC32C validation by default in transfer manager (#8350)
* feat: enable CRC32C validation by default in transfer manager and disable it for individual shard downloads * feat: update validation option to support boolean values and improve default handling logic * refactor: update validation property type definition to restrict string input to crc32c * refactor(storage): use sandbox stubs for file.download in transfer-manager tests
1 parent 08959de commit 86086a6

2 files changed

Lines changed: 54 additions & 18 deletions

File tree

handwritten/storage/src/transfer-manager.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export interface DownloadFileInChunksOptions {
117117
concurrencyLimit?: number;
118118
chunkSizeBytes?: number;
119119
destination?: string;
120-
validation?: 'crc32c' | false;
120+
validation?: 'crc32c' | boolean;
121121
noReturnData?: boolean;
122122
}
123123

@@ -736,7 +736,7 @@ export class TransferManager {
736736
* @property {number} [concurrencyLimit] The number of concurrently executing promises
737737
* to use when downloading the file.
738738
* @property {number} [chunkSizeBytes] The size in bytes of each chunk to be downloaded.
739-
* @property {string | boolean} [validation] Whether or not to perform a CRC32C validation check when download is complete.
739+
* @property {'crc32c' | boolean} [validation] Whether or not to perform a CRC32C validation check when download is complete. Defaults to 'crc32c'.
740740
* @property {boolean} [noReturnData] Whether or not to return the downloaded data. A `true` value here would be useful for files with a size that will not fit into memory.
741741
*
742742
*/
@@ -757,10 +757,19 @@ export class TransferManager {
757757
*
758758
* //-
759759
* // Download a large file in chunks utilizing parallel operations.
760+
* // CRC32C validation is performed by default.
760761
* //-
761762
* const response = await transferManager.downloadFileInChunks(bucket.file('large-file.txt');
762763
* // Your local directory now contains:
763764
* // - "large-file.txt" (with the contents from my-bucket.large-file.txt)
765+
*
766+
* //-
767+
* // To disable validation:
768+
* //-
769+
* const responseWithoutValidation = await transferManager.downloadFileInChunks(
770+
* bucket.file('large-file.txt'),
771+
* { validation: false }
772+
* );
764773
* ```
765774
*
766775
*/
@@ -780,6 +789,12 @@ export class TransferManager {
780789
? this.bucket.file(fileOrName)
781790
: fileOrName;
782791

792+
// Default validation to 'crc32c' if undefined or true, otherwise respect user's value
793+
const validation =
794+
options.validation === undefined || options.validation === true
795+
? 'crc32c'
796+
: options.validation;
797+
783798
const fileInfo = await file.get();
784799
const size = parseInt(fileInfo[0].metadata.size!.toString());
785800
// If the file size does not meet the threshold download it as a single chunk.
@@ -801,6 +816,7 @@ export class TransferManager {
801816
start: chunkStart,
802817
end: chunkEnd,
803818
[GCCL_GCS_CMD_KEY]: GCCL_GCS_CMD_FEATURE.DOWNLOAD_SHARDED,
819+
validation: false, // Disable validation on individual chunks
804820
});
805821
const result = await fileToWrite.write(
806822
resp[0],
@@ -823,7 +839,8 @@ export class TransferManager {
823839
await fileToWrite.close();
824840
}
825841

826-
if (options.validation === 'crc32c' && fileInfo[0].metadata.crc32c) {
842+
// Check against the defaulted validation option
843+
if (validation === 'crc32c' && fileInfo[0].metadata.crc32c) {
827844
const downloadedCrc32C = await CRC32C.fromFile(filePath);
828845
if (!downloadedCrc32C.validate(fileInfo[0].metadata.crc32c)) {
829846
const mismatchError = new RequestError(
@@ -833,6 +850,7 @@ export class TransferManager {
833850
throw mismatchError;
834851
}
835852
}
853+
836854
if (noReturnData) return;
837855
return [Buffer.concat(chunks as Buffer[], size)];
838856
}

handwritten/storage/test/transfer-manager.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -562,13 +562,16 @@ describe('Transfer Manager', () => {
562562

563563
describe('downloadFileInChunks', () => {
564564
let file: File;
565+
let fromFileStub: sinon.SinonStub;
565566

566567
beforeEach(() => {
567568
sandbox.stub(fsp, 'open').resolves({
568569
close: () => Promise.resolve(),
569570
write: (buffer: unknown) => Promise.resolve({buffer}),
570571
} as fsp.FileHandle);
571572

573+
fromFileStub = sandbox.stub(CRC32C, 'fromFile').resolves(new CRC32C(0));
574+
572575
file = new File(bucket, 'some-large-file');
573576
sandbox.stub(file, 'get').resolves([
574577
{
@@ -612,26 +615,41 @@ describe('Transfer Manager', () => {
612615
});
613616

614617
it('should call fromFile when validation is set to crc32c', async () => {
615-
let callCount = 0;
616-
file.download = () => {
617-
return Promise.resolve([Buffer.alloc(0)]) as Promise<DownloadResponse>;
618-
};
619-
CRC32C.fromFile = () => {
620-
callCount++;
621-
return Promise.resolve(new CRC32C(0));
622-
};
618+
sandbox.stub(file, 'download').resolves([Buffer.alloc(0)]);
623619

624620
await transferManager.downloadFileInChunks(file, {validation: 'crc32c'});
625-
assert.strictEqual(callCount, 1);
621+
assert.strictEqual(fromFileStub.callCount, 1);
622+
});
623+
624+
it('should perform CRC32C validation by default', async () => {
625+
sandbox.stub(file, 'download').resolves([Buffer.alloc(0)]);
626+
627+
await transferManager.downloadFileInChunks(file);
628+
assert.strictEqual(fromFileStub.callCount, 1);
629+
});
630+
631+
it('should not perform validation when validation is false', async () => {
632+
sandbox.stub(file, 'download').resolves([Buffer.alloc(0)]);
633+
634+
await transferManager.downloadFileInChunks(file, {validation: false});
635+
assert.strictEqual(fromFileStub.callCount, 0);
636+
});
637+
638+
it('should disable individual sharded chunk validation in download calls', async () => {
639+
let shardedValidationOption: any = undefined;
640+
sandbox.stub(file, 'download').callsFake(async options => {
641+
shardedValidationOption = (options as DownloadOptions).validation;
642+
return [Buffer.alloc(100)];
643+
});
644+
645+
await transferManager.downloadFileInChunks(file);
646+
assert.strictEqual(shardedValidationOption, false);
626647
});
627648

628649
it('should throw an error if crc32c validation fails', async () => {
629-
file.download = () => {
630-
return Promise.resolve([Buffer.alloc(0)]) as Promise<DownloadResponse>;
631-
};
632-
CRC32C.fromFile = () => {
633-
return Promise.resolve(new CRC32C(1)); // Set non-expected initial value
634-
};
650+
sandbox.stub(file, 'download').resolves([Buffer.alloc(0)]);
651+
652+
fromFileStub.resolves(new CRC32C(1)); // Set non-expected initial value
635653

636654
await assert.rejects(
637655
transferManager.downloadFileInChunks(file, {validation: 'crc32c'}),

0 commit comments

Comments
 (0)