or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

access-control.mdauthentication.mdbucket-operations.mdfile-operations.mdindex.mdnotifications.mdstorage-client.mdtransfer-manager.mdutilities.md
tile.json

transfer-manager.mddocs/

Transfer Manager

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.

TransferManager Class

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>;
}

Creating Transfer Manager

// 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);

Bulk Upload Operations

Upload Many Files

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
  }
});

Upload File in Chunks

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'
});

Bulk Download Operations

Download Many Files

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
});

Download File in Chunks

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
});

Progress Tracking and Events

Upload Progress Monitoring

// 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;
}

Download Progress Monitoring

// 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]}`;
  }
}

Error Handling and Retry Logic

Robust Upload with Error Handling

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;
}

Parallel Processing with Concurrency Control

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;
  }
}

Advanced Transfer Scenarios

Incremental Backup

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;
}

Data Migration Between Buckets

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;
}

Resumable Large File Transfer

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';
  }
}

Performance Optimization

Optimal Concurrency Settings

// 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
});

Error Handling

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);
  }
}

Callback Support

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`);
});