Navigation

Node.js

How to Copy and Move Files

Copy and move files safely with progress tracking, atomic operations, and proper error handling in Node.js 2025

How to Copy and Move Files

Copy and move files safely with progress tracking, atomic operations, and proper error handling in Node.js 2025

Table Of Contents

Quick Fix: Basic File Copy and Move Operations

const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');

// Copy file (async)
async function copyFile(source, destination) {
  try {
    await fsPromises.copyFile(source, destination);
    console.log('File copied successfully');
  } catch (error) {
    console.error('Copy error:', error);
    throw error;
  }
}

// Copy file with flags
async function copyFileWithFlags(source, destination, flags = 0) {
  try {
    // fs.constants.COPYFILE_EXCL - fail if destination exists
    // fs.constants.COPYFILE_FICLONE - copy-on-write clone
    // fs.constants.COPYFILE_FICLONE_FORCE - force copy-on-write
    await fsPromises.copyFile(source, destination, flags);
    console.log('File copied with flags');
  } catch (error) {
    console.error('Copy with flags error:', error);
    throw error;
  }
}

// Move file (rename)
async function moveFile(source, destination) {
  try {
    await fsPromises.rename(source, destination);
    console.log('File moved successfully');
  } catch (error) {
    console.error('Move error:', error);
    throw error;
  }
}

// Copy file using streams (for large files)
async function copyFileStream(source, destination) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(source);
    const writeStream = fs.createWriteStream(destination);

    readStream.pipe(writeStream);

    writeStream.on('finish', () => {
      console.log('Stream copy completed');
      resolve();
    });

    writeStream.on('error', reject);
    readStream.on('error', reject);
  });
}

// Synchronous copy
function copyFileSync(source, destination) {
  try {
    fs.copyFileSync(source, destination);
    console.log('File copied synchronously');
  } catch (error) {
    console.error('Sync copy error:', error);
    throw error;
  }
}

// Copy directory recursively
async function copyDirectory(source, destination) {
  try {
    await fsPromises.mkdir(destination, { recursive: true });
    
    const entries = await fsPromises.readdir(source, { withFileTypes: true });
    
    for (const entry of entries) {
      const sourcePath = path.join(source, entry.name);
      const destinationPath = path.join(destination, entry.name);
      
      if (entry.isDirectory()) {
        await copyDirectory(sourcePath, destinationPath);
      } else {
        await fsPromises.copyFile(sourcePath, destinationPath);
      }
    }
    
    console.log('Directory copied recursively');
  } catch (error) {
    console.error('Directory copy error:', error);
    throw error;
  }
}

// Usage examples
async function examples() {
  // Basic file copy
  await copyFile('./source.txt', './destination.txt');
  
  // Copy with exclusive flag (fail if destination exists)
  await copyFileWithFlags('./source.txt', './new-file.txt', fs.constants.COPYFILE_EXCL);
  
  // Move file
  await moveFile('./old-location.txt', './new-location.txt');
  
  // Copy large file using streams
  await copyFileStream('./large-file.bin', './copy-of-large-file.bin');
  
  // Copy entire directory
  await copyDirectory('./source-directory', './destination-directory');
}

The Problem: Production File Operations with Advanced Features

const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const { pipeline } = require('stream');
const { promisify } = require('util');

const pipelineAsync = promisify(pipeline);

// Advanced file operations service
class FileOperationsService {
  constructor(options = {}) {
    this.enableBackup = options.enableBackup !== false;
    this.backupSuffix = options.backupSuffix || '.backup';
    this.enableProgress = options.enableProgress !== false;
    this.enableVerification = options.enableVerification !== false;
    this.chunkSize = options.chunkSize || 64 * 1024; // 64KB
    this.maxConcurrency = options.maxConcurrency || 10;
    this.preserveTimestamps = options.preserveTimestamps !== false;
    this.preservePermissions = options.preservePermissions !== false;
    this.enableAtomic = options.enableAtomic !== false;
    this.tempSuffix = '.tmp';
  }

  // Copy file with comprehensive options
  async copyFile(source, destination, options = {}) {
    const {
      overwrite = false,
      backup = this.enableBackup,
      verify = this.enableVerification,
      preserveTimestamps = this.preserveTimestamps,
      preservePermissions = this.preservePermissions,
      atomic = this.enableAtomic,
      onProgress = null
    } = options;

    try {
      const sourceInfo = await this.getFileInfo(source);
      
      if (!sourceInfo.exists) {
        throw new Error(`Source file does not exist: ${source}`);
      }

      // Check destination
      const destInfo = await this.getFileInfo(destination);
      
      if (destInfo.exists && !overwrite) {
        throw new Error(`Destination file exists: ${destination}`);
      }

      // Create backup if destination exists and backup is enabled
      let backupPath = null;
      if (destInfo.exists && backup) {
        backupPath = await this.createBackup(destination);
      }

      // Perform atomic copy if enabled
      const finalDestination = atomic 
        ? destination + this.tempSuffix 
        : destination;

      // Choose copy method based on file size
      let copyResult;
      if (sourceInfo.size > 100 * 1024 * 1024) { // > 100MB
        copyResult = await this.copyLargeFile(source, finalDestination, {
          sourceInfo,
          onProgress
        });
      } else {
        copyResult = await this.copySmallFile(source, finalDestination, {
          sourceInfo
        });
      }

      // Preserve file attributes
      if (preserveTimestamps || preservePermissions) {
        await this.preserveFileAttributes(source, finalDestination, {
          preserveTimestamps,
          preservePermissions
        });
      }

      // Verify copy if enabled
      if (verify) {
        const verificationResult = await this.verifyFileCopy(source, finalDestination);
        if (!verificationResult.match) {
          throw new Error('File verification failed: checksums do not match');
        }
      }

      // Atomic rename if enabled
      if (atomic) {
        await fsPromises.rename(finalDestination, destination);
      }

      return {
        success: true,
        source,
        destination,
        size: sourceInfo.size,
        backup: backupPath,
        verified: verify,
        atomic: atomic,
        preservedAttributes: preserveTimestamps || preservePermissions,
        ...copyResult
      };
    } catch (error) {
      return this.handleOperationError(error, 'copy', source, destination);
    }
  }

  // Move file with advanced options
  async moveFile(source, destination, options = {}) {
    const {
      overwrite = false,
      backup = this.enableBackup,
      crossDevice = false,
      atomic = this.enableAtomic
    } = options;

    try {
      const sourceInfo = await this.getFileInfo(source);
      
      if (!sourceInfo.exists) {
        throw new Error(`Source file does not exist: ${source}`);
      }

      const destInfo = await this.getFileInfo(destination);
      
      if (destInfo.exists && !overwrite) {
        throw new Error(`Destination file exists: ${destination}`);
      }

      // Create backup if destination exists
      let backupPath = null;
      if (destInfo.exists && backup) {
        backupPath = await this.createBackup(destination);
      }

      // Check if source and destination are on the same filesystem
      const sameDevice = await this.isSameDevice(source, destination);
      
      if (sameDevice && !crossDevice) {
        // Simple rename operation
        if (atomic && destInfo.exists) {
          const tempDest = destination + this.tempSuffix;
          await fsPromises.rename(source, tempDest);
          await fsPromises.rename(tempDest, destination);
        } else {
          await fsPromises.rename(source, destination);
        }
      } else {
        // Cross-device move: copy then delete
        const copyResult = await this.copyFile(source, destination, {
          ...options,
          atomic: false // Handle atomicity here
        });
        
        if (!copyResult.success) {
          throw new Error(`Copy failed during move: ${copyResult.error}`);
        }

        // Verify before deletion
        if (this.enableVerification) {
          const verificationResult = await this.verifyFileCopy(source, destination);
          if (!verificationResult.match) {
            await fsPromises.unlink(destination); // Clean up
            throw new Error('Move verification failed');
          }
        }

        // Delete source file
        await fsPromises.unlink(source);
      }

      return {
        success: true,
        source,
        destination,
        size: sourceInfo.size,
        backup: backupPath,
        crossDevice: !sameDevice,
        method: sameDevice ? 'rename' : 'copy-delete'
      };
    } catch (error) {
      return this.handleOperationError(error, 'move', source, destination);
    }
  }

  // Copy directory recursively with advanced options
  async copyDirectory(source, destination, options = {}) {
    const {
      overwrite = false,
      filter = null,
      preserveSymlinks = false,
      maxDepth = 100,
      onProgress = null,
      ignoreErrors = false
    } = options;

    try {
      const sourceInfo = await this.getFileInfo(source);
      
      if (!sourceInfo.exists || !sourceInfo.isDirectory) {
        throw new Error(`Source is not a directory: ${source}`);
      }

      const results = {
        filesProcessed: 0,
        directoriesCreated: 0,
        errors: [],
        skipped: []
      };

      await this.copyDirectoryRecursive(source, destination, {
        overwrite,
        filter,
        preserveSymlinks,
        maxDepth,
        currentDepth: 0,
        onProgress,
        ignoreErrors,
        results
      });

      return {
        success: true,
        source,
        destination,
        ...results,
        totalOperations: results.filesProcessed + results.directoriesCreated
      };
    } catch (error) {
      return this.handleOperationError(error, 'copy-directory', source, destination);
    }
  }

  // Batch file operations
  async batchOperations(operations, options = {}) {
    const { 
      concurrency = this.maxConcurrency,
      continueOnError = true,
      rollbackOnError = false 
    } = options;

    const results = [];
    const errors = [];
    const completed = [];

    try {
      // Process operations in batches
      for (let i = 0; i < operations.length; i += concurrency) {
        const batch = operations.slice(i, i + concurrency);
        
        const batchPromises = batch.map(async (operation, index) => {
          try {
            let result;
            
            switch (operation.type) {
              case 'copy':
                result = await this.copyFile(operation.source, operation.destination, operation.options);
                break;
              case 'move':
                result = await this.moveFile(operation.source, operation.destination, operation.options);
                break;
              case 'copy-directory':
                result = await this.copyDirectory(operation.source, operation.destination, operation.options);
                break;
              default:
                throw new Error(`Unknown operation type: ${operation.type}`);
            }

            return { index: i + index, operation, ...result };
          } catch (error) {
            const errorResult = {
              index: i + index,
              operation,
              success: false,
              error: error.message
            };
            
            if (continueOnError) {
              return errorResult;
            } else {
              throw errorResult;
            }
          }
        });

        const batchResults = await Promise.allSettled(batchPromises);
        
        batchResults.forEach(promiseResult => {
          if (promiseResult.status === 'fulfilled') {
            const result = promiseResult.value;
            
            if (result.error) {
              errors.push(result);
            } else {
              results.push(result);
              completed.push(result);
            }
          } else {
            errors.push({
              index: -1,
              operation: null,
              success: false,
              error: promiseResult.reason
            });
          }
        });

        // Check if we should rollback on error
        if (rollbackOnError && errors.length > 0) {
          await this.rollbackOperations(completed);
          throw new Error(`Batch operation failed, rolled back ${completed.length} operations`);
        }
      }

      return {
        success: errors.length === 0 || continueOnError,
        results: results.sort((a, b) => a.index - b.index),
        errors,
        total: operations.length,
        completed: results.length,
        failed: errors.length
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        results,
        errors,
        completed: completed.length
      };
    }
  }

  // Copy large file with progress tracking
  async copyLargeFile(source, destination, options = {}) {
    const { sourceInfo, onProgress } = options;
    
    return new Promise((resolve, reject) => {
      const readStream = fs.createReadStream(source, {
        highWaterMark: this.chunkSize
      });
      
      const writeStream = fs.createWriteStream(destination);
      
      let copiedBytes = 0;
      const totalBytes = sourceInfo.size;
      const startTime = Date.now();

      readStream.on('data', (chunk) => {
        copiedBytes += chunk.length;
        
        if (onProgress) {
          const progress = {
            copiedBytes,
            totalBytes,
            percentage: (copiedBytes / totalBytes) * 100,
            speed: copiedBytes / ((Date.now() - startTime) / 1000)
          };
          
          onProgress(progress);
        }
      });

      readStream.pipe(writeStream);

      writeStream.on('finish', () => {
        resolve({
          method: 'stream',
          copiedBytes,
          duration: Date.now() - startTime
        });
      });

      writeStream.on('error', reject);
      readStream.on('error', reject);
    });
  }

  // Copy small file using built-in method
  async copySmallFile(source, destination, options = {}) {
    const startTime = Date.now();
    
    await fsPromises.copyFile(source, destination);
    
    return {
      method: 'copyFile',
      duration: Date.now() - startTime
    };
  }

  // Verify file copy by comparing checksums
  async verifyFileCopy(source, destination) {
    try {
      const [sourceHash, destHash] = await Promise.all([
        this.calculateFileHash(source),
        this.calculateFileHash(destination)
      ]);

      return {
        match: sourceHash === destHash,
        sourceHash,
        destinationHash: destHash
      };
    } catch (error) {
      return {
        match: false,
        error: error.message
      };
    }
  }

  // Calculate file hash
  async calculateFileHash(filePath, algorithm = 'sha256') {
    return new Promise((resolve, reject) => {
      const hash = crypto.createHash(algorithm);
      const stream = fs.createReadStream(filePath);

      stream.on('data', data => hash.update(data));
      stream.on('end', () => resolve(hash.digest('hex')));
      stream.on('error', reject);
    });
  }

  // Preserve file attributes
  async preserveFileAttributes(source, destination, options = {}) {
    const { preserveTimestamps, preservePermissions } = options;
    
    try {
      const sourceStats = await fsPromises.stat(source);

      if (preserveTimestamps) {
        await fsPromises.utimes(destination, sourceStats.atime, sourceStats.mtime);
      }

      if (preservePermissions) {
        await fsPromises.chmod(destination, sourceStats.mode);
      }
    } catch (error) {
      // Don't fail the entire operation for attribute preservation errors
      console.warn('Failed to preserve file attributes:', error.message);
    }
  }

  // Copy directory recursively
  async copyDirectoryRecursive(source, destination, options) {
    const {
      overwrite,
      filter,
      preserveSymlinks,
      maxDepth,
      currentDepth,
      onProgress,
      ignoreErrors,
      results
    } = options;

    if (currentDepth >= maxDepth) {
      return;
    }

    try {
      // Create destination directory
      await fsPromises.mkdir(destination, { recursive: true });
      results.directoriesCreated++;

      const entries = await fsPromises.readdir(source, { withFileTypes: true });

      for (const entry of entries) {
        const sourcePath = path.join(source, entry.name);
        const destPath = path.join(destination, entry.name);

        // Apply filter if provided
        if (filter && !filter(entry, sourcePath)) {
          results.skipped.push(sourcePath);
          continue;
        }

        try {
          if (entry.isDirectory()) {
            await this.copyDirectoryRecursive(sourcePath, destPath, {
              ...options,
              currentDepth: currentDepth + 1
            });
          } else if (entry.isFile()) {
            await this.copyFile(sourcePath, destPath, { overwrite });
            results.filesProcessed++;
            
            if (onProgress) {
              onProgress({
                type: 'file',
                source: sourcePath,
                destination: destPath,
                processed: results.filesProcessed
              });
            }
          } else if (entry.isSymbolicLink() && preserveSymlinks) {
            const linkTarget = await fsPromises.readlink(sourcePath);
            await fsPromises.symlink(linkTarget, destPath);
            results.filesProcessed++;
          }
        } catch (error) {
          const errorInfo = {
            path: sourcePath,
            error: error.message
          };
          
          if (ignoreErrors) {
            results.errors.push(errorInfo);
          } else {
            throw error;
          }
        }
      }
    } catch (error) {
      if (!ignoreErrors) {
        throw error;
      }
      results.errors.push({
        path: source,
        error: error.message
      });
    }
  }

  // Helper methods
  async getFileInfo(filePath) {
    try {
      const stats = await fsPromises.stat(filePath);
      
      return {
        exists: true,
        size: stats.size,
        isFile: stats.isFile(),
        isDirectory: stats.isDirectory(),
        isSymlink: stats.isSymbolicLink(),
        mode: stats.mode,
        atime: stats.atime,
        mtime: stats.mtime,
        ctime: stats.ctime
      };
    } catch (error) {
      if (error.code === 'ENOENT') {
        return { exists: false };
      }
      throw error;
    }
  }

  async createBackup(filePath) {
    const backupPath = filePath + this.backupSuffix;
    await fsPromises.copyFile(filePath, backupPath);
    return backupPath;
  }

  async isSameDevice(path1, path2) {
    try {
      const [stats1, stats2] = await Promise.all([
        fsPromises.stat(path.dirname(path1)),
        fsPromises.stat(path.dirname(path2))
      ]);
      
      return stats1.dev === stats2.dev;
    } catch {
      return false;
    }
  }

  async rollbackOperations(completedOperations) {
    for (const operation of completedOperations.reverse()) {
      try {
        switch (operation.operation.type) {
          case 'copy':
          case 'copy-directory':
            await fsPromises.unlink(operation.operation.destination).catch(() => {});
            break;
          case 'move':
            // Attempt to move back
            await fsPromises.rename(operation.operation.destination, operation.operation.source).catch(() => {});
            break;
        }
      } catch {
        // Ignore rollback errors
      }
    }
  }

  handleOperationError(error, operation, source, destination) {
    let errorCode = error.code || 'UNKNOWN';
    let errorMessage = error.message;
    
    switch (error.code) {
      case 'ENOENT':
        errorMessage = `File not found: ${source}`;
        break;
      case 'EACCES':
        errorMessage = `Permission denied: ${source} or ${destination}`;
        break;
      case 'EEXIST':
        errorMessage = `Destination already exists: ${destination}`;
        break;
      case 'ENOSPC':
        errorMessage = 'No space left on device';
        break;
      case 'EXDEV':
        errorMessage = 'Cross-device operation not supported';
        break;
    }
    
    return {
      success: false,
      operation,
      source,
      destination,
      error: errorMessage,
      code: errorCode
    };
  }
}

// Express application for file operations
const express = require('express');
const app = express();

// Initialize file operations service
const fileOps = new FileOperationsService({
  enableBackup: true,
  enableProgress: true,
  enableVerification: true
});

app.use(express.json());

// Routes
app.post('/api/file/copy', async (req, res) => {
  try {
    const { source, destination, options = {} } = req.body;
    
    if (!source || !destination) {
      return res.status(400).json({
        error: 'Source and destination paths required',
        code: 'PATHS_MISSING'
      });
    }

    const result = await fileOps.copyFile(source, destination, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({
      error: error.message,
      code: 'COPY_ERROR'
    });
  }
});

app.post('/api/file/move', async (req, res) => {
  try {
    const { source, destination, options = {} } = req.body;
    
    if (!source || !destination) {
      return res.status(400).json({
        error: 'Source and destination paths required',
        code: 'PATHS_MISSING'
      });
    }

    const result = await fileOps.moveFile(source, destination, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({
      error: error.message,
      code: 'MOVE_ERROR'
    });
  }
});

app.post('/api/directory/copy', async (req, res) => {
  try {
    const { source, destination, options = {} } = req.body;
    
    if (!source || !destination) {
      return res.status(400).json({
        error: 'Source and destination paths required',
        code: 'PATHS_MISSING'
      });
    }

    const result = await fileOps.copyDirectory(source, destination, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({
      error: error.message,
      code: 'COPY_DIRECTORY_ERROR'
    });
  }
});

app.post('/api/batch', async (req, res) => {
  try {
    const { operations, options = {} } = req.body;
    
    if (!Array.isArray(operations)) {
      return res.status(400).json({
        error: 'Operations array required',
        code: 'INVALID_INPUT'
      });
    }

    const result = await fileOps.batchOperations(operations, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({
      error: error.message,
      code: 'BATCH_ERROR'
    });
  }
});

// Demonstration
async function demonstrateUsage() {
  try {
    // Copy single file with verification
    const copyResult = await fileOps.copyFile('./source.txt', './destination.txt', {
      verify: true,
      preserveTimestamps: true,
      backup: true
    });
    console.log('Copy result:', copyResult.success);

    // Move file with atomic operation
    const moveResult = await fileOps.moveFile('./temp.txt', './moved.txt', {
      atomic: true,
      overwrite: true
    });
    console.log('Move result:', moveResult.success);

    // Copy directory with filtering
    const dirCopyResult = await fileOps.copyDirectory('./source-dir', './dest-dir', {
      filter: (entry) => !entry.name.startsWith('.'),
      preserveSymlinks: true,
      onProgress: (progress) => {
        console.log(`Processed: ${progress.processed} files`);
      }
    });
    console.log('Directory copy result:', dirCopyResult.success);

    // Batch operations
    const batchResult = await fileOps.batchOperations([
      { type: 'copy', source: './file1.txt', destination: './copy1.txt' },
      { type: 'copy', source: './file2.txt', destination: './copy2.txt' },
      { type: 'move', source: './file3.txt', destination: './moved3.txt' }
    ], {
      concurrency: 2,
      continueOnError: true
    });
    console.log('Batch operations:', batchResult.completed, 'completed');
  } catch (error) {
    console.error('Demonstration error:', error);
  }
}

module.exports = {
  FileOperationsService,
  app
};

File copy and move operations solve "file management", "data migration", and "backup automation" issues. Use fs.copyFile() for small files, streams for large files, implement atomic operations for safety. Support progress tracking, verification, batch operations, and proper error handling. Handle cross-device moves, preserve file attributes, and provide rollback capabilities. Alternative: shell commands, rsync, cloud storage APIs.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js