The Transfer Manager provides high-level methods for bulk upload and download operations with parallel processing, progress tracking, and error handling for efficient data transfer.
class TransferManager {
constructor(bucket: Bucket);
// Properties
bucket: Bucket;
// Bulk upload methods
uploadManyFiles(filePathsOrDirectory: string[] | string, options?: UploadManyFilesOptions): Promise<UploadResponse[]>;
uploadFileInChunks(filePath: string, options?: UploadFileInChunksOptions): Promise<void>;
// Bulk download methods
downloadManyFiles(files: File[], options?: DownloadManyFilesOptions): Promise<void>;
downloadFileInChunks(file: File, options?: DownloadFileInChunksOptions): Promise<void>;
}// Create transfer manager for bucket
const transferManager = new TransferManager(bucket);
// Or import and create
import { TransferManager } from '@google-cloud/storage';
const bucket = storage.bucket('my-bucket');
const transferManager = new TransferManager(bucket);uploadManyFiles(filePathsOrDirectory: string[] | string, options?: UploadManyFilesOptions): Promise<UploadResponse[]>
interface UploadManyFilesOptions {
concurrencyLimit?: number;
customDestinationBuilder?: (filePath: string) => string;
skipIfExists?: boolean;
prefix?: string;
passthroughOptions?: UploadOptions;
}
type UploadResponse = [File, unknown]; // [file, apiResponse]
// Upload multiple files by path
const filePaths = [
'/local/documents/file1.pdf',
'/local/documents/file2.pdf',
'/local/images/photo1.jpg',
'/local/images/photo2.jpg'
];
const uploadResponses = await transferManager.uploadManyFiles(filePaths);
uploadResponses.forEach(([file]) => {
console.log(`Uploaded: ${file.name}`);
});
// Upload entire directory
const uploadResponses = await transferManager.uploadManyFiles('/local/documents');
// Upload with options
const uploadResponses = await transferManager.uploadManyFiles(filePaths, {
concurrencyLimit: 10,
skipIfExists: true,
prefix: 'uploaded/',
customDestinationBuilder: (filePath) => {
// Custom logic for destination path
const fileName = path.basename(filePath);
const timestamp = new Date().toISOString().replace(/:/g, '-');
return `uploads/${timestamp}-${fileName}`;
}
});
// Upload with passthrough options
const uploadResponses = await transferManager.uploadManyFiles(filePaths, {
concurrencyLimit: 5,
passthroughOptions: {
public: true,
metadata: {
cacheControl: 'public, max-age=3600'
},
resumable: true
}
});uploadFileInChunks(filePath: string, options?: UploadFileInChunksOptions): Promise<void>
interface UploadFileInChunksOptions {
chunkSizeBytes?: number;
concurrencyLimit?: number;
destination?: string;
encryptionKey?: string | Buffer;
kmsKeyName?: string;
maxRetries?: number;
preconditionOpts?: PreconditionOptions;
uploadId?: string;
passthroughOptions?: UploadOptions;
}
// Upload large file in chunks
await transferManager.uploadFileInChunks('/local/large-video.mp4');
// Upload with custom chunk size
await transferManager.uploadFileInChunks('/local/huge-dataset.zip', {
chunkSizeBytes: 32 * 1024 * 1024, // 32MB chunks
concurrencyLimit: 8,
destination: 'datasets/processed-data.zip'
});
// Upload with encryption
await transferManager.uploadFileInChunks('/local/sensitive-data.db', {
chunkSizeBytes: 16 * 1024 * 1024, // 16MB chunks
encryptionKey: crypto.randomBytes(32).toString('base64'),
maxRetries: 5
});
// Upload with KMS encryption
await transferManager.uploadFileInChunks('/local/confidential.pdf', {
kmsKeyName: 'projects/PROJECT_ID/locations/us/keyRings/ring/cryptoKeys/key',
destination: 'confidential/document.pdf'
});downloadManyFiles(files: File[], options?: DownloadManyFilesOptions): Promise<void>
interface DownloadManyFilesOptions {
concurrencyLimit?: number;
prefix?: string;
stripPrefix?: string;
passthroughOptions?: DownloadOptions;
}
// Download multiple files
const files = [
bucket.file('documents/file1.pdf'),
bucket.file('documents/file2.pdf'),
bucket.file('images/photo1.jpg')
];
await transferManager.downloadManyFiles(files, {
concurrencyLimit: 10,
prefix: '/local/downloads/'
});
// Get files and download with pattern
const [allFiles] = await bucket.getFiles({ prefix: 'reports/' });
await transferManager.downloadManyFiles(allFiles, {
concurrencyLimit: 5,
prefix: '/local/reports/',
stripPrefix: 'reports/',
passthroughOptions: {
validation: false // Skip integrity checking for faster downloads
}
});
// Download with custom local structure
const [files] = await bucket.getFiles({ prefix: 'backup/2023/' });
await transferManager.downloadManyFiles(files, {
prefix: '/local/restored/',
stripPrefix: 'backup/2023/',
concurrencyLimit: 8
});downloadFileInChunks(file: File, options?: DownloadFileInChunksOptions): Promise<void>
interface DownloadFileInChunksOptions {
chunkSizeBytes?: number;
concurrencyLimit?: number;
destination?: string;
validation?: boolean;
decompress?: boolean;
}
// Download large file in chunks
const largeFile = bucket.file('datasets/large-dataset.zip');
await transferManager.downloadFileInChunks(largeFile, {
destination: '/local/downloads/dataset.zip'
});
// Download with custom chunk size and concurrency
await transferManager.downloadFileInChunks(largeFile, {
chunkSizeBytes: 64 * 1024 * 1024, // 64MB chunks
concurrencyLimit: 12,
destination: '/local/fast-download/dataset.zip',
validation: false // Skip validation for speed
});
// Download compressed file with decompression
const compressedFile = bucket.file('logs/compressed-logs.gz');
await transferManager.downloadFileInChunks(compressedFile, {
destination: '/local/logs/decompressed.log',
decompress: true
});// Track upload progress
class UploadProgressTracker {
private totalFiles: number = 0;
private completedFiles: number = 0;
private totalBytes: number = 0;
private uploadedBytes: number = 0;
onUploadStart(files: string[]) {
this.totalFiles = files.length;
this.completedFiles = 0;
console.log(`Starting upload of ${this.totalFiles} files`);
}
onFileComplete(filePath: string, file: File) {
this.completedFiles++;
console.log(`Uploaded ${filePath} -> ${file.name} (${this.completedFiles}/${this.totalFiles})`);
}
onUploadComplete() {
console.log(`Upload complete: ${this.completedFiles} files uploaded`);
}
}
// Custom upload with progress tracking
async function uploadWithProgress(filePaths: string[]) {
const tracker = new UploadProgressTracker();
tracker.onUploadStart(filePaths);
const results = await transferManager.uploadManyFiles(filePaths, {
concurrencyLimit: 5
});
results.forEach(([file], index) => {
tracker.onFileComplete(filePaths[index], file);
});
tracker.onUploadComplete();
return results;
}// Track download progress
class DownloadProgressTracker {
private totalSize: number = 0;
private downloadedSize: number = 0;
private startTime: number = Date.now();
onDownloadStart(files: File[]) {
this.totalSize = files.reduce((sum, file) => {
return sum + (parseInt(file.metadata?.size || '0', 10) || 0);
}, 0);
console.log(`Starting download of ${files.length} files (${this.formatBytes(this.totalSize)})`);
}
onChunkDownloaded(bytes: number) {
this.downloadedSize += bytes;
const progress = (this.downloadedSize / this.totalSize) * 100;
const elapsed = (Date.now() - this.startTime) / 1000;
const speed = this.downloadedSize / elapsed;
console.log(`Progress: ${progress.toFixed(1)}% (${this.formatBytes(speed)}/s)`);
}
private formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}async function robustUpload(filePaths: string[]) {
const maxRetries = 3;
const results: Array<{ file: string; success: boolean; error?: Error }> = [];
for (const filePath of filePaths) {
let success = false;
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const [uploadResults] = await transferManager.uploadManyFiles([filePath], {
concurrencyLimit: 1,
passthroughOptions: {
timeout: 300000 // 5 minutes
}
});
results.push({ file: filePath, success: true });
success = true;
break;
} catch (error) {
lastError = error as Error;
console.warn(`Upload attempt ${attempt} failed for ${filePath}:`, error.message);
if (attempt < maxRetries) {
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
if (!success) {
results.push({ file: filePath, success: false, error: lastError! });
}
}
// Report results
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`Upload complete: ${successful.length} successful, ${failed.length} failed`);
if (failed.length > 0) {
console.error('Failed uploads:');
failed.forEach(result => {
console.error(` ${result.file}: ${result.error?.message}`);
});
}
return results;
}class ConcurrentTransferManager {
private bucket: Bucket;
private concurrencyLimit: number;
constructor(bucket: Bucket, concurrencyLimit = 10) {
this.bucket = bucket;
this.concurrencyLimit = concurrencyLimit;
}
async uploadDirectory(directoryPath: string, options?: UploadManyFilesOptions) {
// Get all files in directory
const files = await this.getFilesRecursively(directoryPath);
// Process in batches
const batches = this.createBatches(files, this.concurrencyLimit);
const results: UploadResponse[] = [];
for (const batch of batches) {
console.log(`Processing batch of ${batch.length} files`);
const batchPromises = batch.map(async (filePath) => {
const transferManager = new TransferManager(this.bucket);
return transferManager.uploadManyFiles([filePath], options);
});
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
results.push(...result.value);
console.log(`✓ Uploaded: ${batch[index]}`);
} else {
console.error(`✗ Failed: ${batch[index]} - ${result.reason}`);
}
});
}
return results;
}
private async getFilesRecursively(dir: string): Promise<string[]> {
const fs = require('fs').promises;
const path = require('path');
const files: string[] = [];
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
files.push(...await this.getFilesRecursively(itemPath));
} else {
files.push(itemPath);
}
}
return files;
}
private createBatches<T>(items: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
}async function incrementalBackup(localPath: string, remotePath: string) {
const fs = require('fs').promises;
const path = require('path');
// Get local files with metadata
const localFiles = await getLocalFilesWithMetadata(localPath);
// Get remote files
const [remoteFiles] = await bucket.getFiles({ prefix: remotePath });
const remoteFileMap = new Map(
remoteFiles.map(file => [file.name, file])
);
// Find files to upload (new or modified)
const filesToUpload: string[] = [];
for (const [relativePath, localStat] of localFiles) {
const remoteName = path.join(remotePath, relativePath).replace(/\\/g, '/');
const remoteFile = remoteFileMap.get(remoteName);
if (!remoteFile) {
// New file
filesToUpload.push(path.join(localPath, relativePath));
} else {
// Check if modified
const [remoteMetadata] = await remoteFile.getMetadata();
const remoteModified = new Date(remoteMetadata.updated!);
if (localStat.mtime > remoteModified) {
filesToUpload.push(path.join(localPath, relativePath));
}
}
}
console.log(`Found ${filesToUpload.length} files to upload`);
if (filesToUpload.length > 0) {
await transferManager.uploadManyFiles(filesToUpload, {
concurrencyLimit: 8,
customDestinationBuilder: (filePath) => {
const relativePath = path.relative(localPath, filePath);
return path.join(remotePath, relativePath).replace(/\\/g, '/');
}
});
}
return filesToUpload.length;
}
async function getLocalFilesWithMetadata(dirPath: string): Promise<Map<string, any>> {
const fs = require('fs').promises;
const path = require('path');
const files = new Map();
async function scan(currentPath: string, relativePath = '') {
const items = await fs.readdir(currentPath);
for (const item of items) {
const itemPath = path.join(currentPath, item);
const itemRelativePath = path.join(relativePath, item);
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
await scan(itemPath, itemRelativePath);
} else {
files.set(itemRelativePath, stat);
}
}
}
await scan(dirPath);
return files;
}async function migrateBetweenBuckets(
sourceBucket: Bucket,
destinationBucket: Bucket,
prefix?: string
) {
// Get all files from source bucket
const [files] = await sourceBucket.getFiles({ prefix });
console.log(`Migrating ${files.length} files from ${sourceBucket.name} to ${destinationBucket.name}`);
// Process in batches to avoid memory issues
const batchSize = 100;
let migrated = 0;
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const migrationPromises = batch.map(async (file) => {
try {
// Copy file to destination bucket
const [copiedFile] = await file.copy(destinationBucket.file(file.name));
// Verify copy succeeded
const [exists] = await copiedFile.exists();
if (exists) {
// Optionally delete original file
// await file.delete();
migrated++;
console.log(`Migrated: ${file.name}`);
}
} catch (error) {
console.error(`Failed to migrate ${file.name}:`, error);
}
});
await Promise.allSettled(migrationPromises);
console.log(`Progress: ${Math.min(i + batchSize, files.length)}/${files.length} files processed`);
}
console.log(`Migration complete: ${migrated}/${files.length} files migrated`);
return migrated;
}class ResumableTransferManager {
private bucket: Bucket;
private uploadStates = new Map<string, string>(); // file -> resumeUri
constructor(bucket: Bucket) {
this.bucket = bucket;
}
async resumableUpload(filePath: string, destination?: string) {
const fs = require('fs');
const path = require('path');
destination = destination || path.basename(filePath);
const file = this.bucket.file(destination);
// Check for existing upload session
let resumeUri = this.uploadStates.get(filePath);
if (!resumeUri) {
// Create new resumable upload session
[resumeUri] = await file.createResumableUpload({
metadata: {
contentType: this.getContentType(filePath)
}
});
this.uploadStates.set(filePath, resumeUri);
console.log(`Created upload session for ${filePath}`);
} else {
console.log(`Resuming upload session for ${filePath}`);
}
// Upload with resumable stream
return new Promise<void>((resolve, reject) => {
const readStream = fs.createReadStream(filePath);
const writeStream = file.createWriteStream({
uri: resumeUri,
resumable: true
});
writeStream.on('error', (error) => {
console.error(`Upload failed for ${filePath}:`, error);
// Keep resume URI for retry
reject(error);
});
writeStream.on('finish', () => {
console.log(`Upload completed for ${filePath}`);
this.uploadStates.delete(filePath); // Clear successful upload
resolve();
});
readStream.pipe(writeStream);
});
}
private getContentType(filePath: string): string {
const ext = require('path').extname(filePath).toLowerCase();
const contentTypes: { [key: string]: string } = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.json': 'application/json',
'.zip': 'application/zip'
};
return contentTypes[ext] || 'application/octet-stream';
}
}// Determine optimal concurrency based on file sizes and network
function getOptimalConcurrency(files: string[]): number {
const fs = require('fs');
// Calculate average file size
let totalSize = 0;
let fileCount = 0;
for (const file of files) {
try {
const stat = fs.statSync(file);
totalSize += stat.size;
fileCount++;
} catch (error) {
console.warn(`Could not stat file ${file}`);
}
}
if (fileCount === 0) return 5; // Default
const averageSize = totalSize / fileCount;
const sizeMB = averageSize / (1024 * 1024);
// Adjust concurrency based on file size
if (sizeMB < 1) return 20; // Small files: high concurrency
else if (sizeMB < 10) return 10; // Medium files: medium concurrency
else if (sizeMB < 100) return 5; // Large files: low concurrency
else return 2; // Very large files: minimal concurrency
}
// Usage
const filePaths = ['/path/to/files/*'];
const concurrency = getOptimalConcurrency(filePaths);
await transferManager.uploadManyFiles(filePaths, {
concurrencyLimit: concurrency
});import { ApiError } from '@google-cloud/storage';
try {
await transferManager.uploadManyFiles(filePaths);
} catch (error) {
if (error instanceof ApiError) {
if (error.code === 403) {
console.log('Permission denied - check bucket permissions');
} else if (error.code === 404) {
console.log('Bucket not found');
} else if (error.code === 413) {
console.log('File too large - consider chunked upload');
} else {
console.error(`Transfer Error ${error.code}: ${error.message}`);
}
} else {
console.error('Transfer error:', error);
}
}Transfer Manager methods are Promise-based, but you can use them with callbacks:
// Promise pattern (recommended)
const results = await transferManager.uploadManyFiles(filePaths);
// Callback wrapper
function uploadWithCallback(filePaths: string[], callback: (err: Error | null, results?: UploadResponse[]) => void) {
transferManager.uploadManyFiles(filePaths)
.then(results => callback(null, results))
.catch(error => callback(error));
}
// Usage
uploadWithCallback(filePaths, (err, results) => {
if (err) {
console.error('Upload error:', err);
return;
}
console.log(`Uploaded ${results!.length} files`);
});