Navigation

Node.js

How to Delete Files and Directories

Safely remove files and directories with proper error handling, backup options, and protection against accidental deletion in Node.js 2025

Table Of Contents

Quick Fix: Basic File and Directory Deletion

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

// Delete a single file (async)
async function deleteFile(filePath) {
  try {
    await fsPromises.unlink(filePath);
    console.log('File deleted successfully');
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.log('File does not exist');
    } else {
      console.error('Error deleting file:', error);
      throw error;
    }
  }
}

// Delete directory recursively (async)
async function deleteDirectory(dirPath) {
  try {
    await fsPromises.rm(dirPath, { recursive: true, force: true });
    console.log('Directory deleted successfully');
  } catch (error) {
    console.error('Error deleting directory:', error);
    throw error;
  }
}

// Synchronous file deletion
function deleteFileSync(filePath) {
  try {
    fs.unlinkSync(filePath);
    console.log('File deleted synchronously');
  } catch (error) {
    if (error.code !== 'ENOENT') {
      console.error('Error deleting file:', error);
      throw error;
    }
  }
}

// Synchronous directory deletion
function deleteDirectorySync(dirPath) {
  try {
    fs.rmSync(dirPath, { recursive: true, force: true });
    console.log('Directory deleted synchronously');
  } catch (error) {
    console.error('Error deleting directory:', error);
    throw error;
  }
}

// Safe deletion with existence check
async function safeDelete(targetPath) {
  try {
    const stats = await fsPromises.stat(targetPath);
    
    if (stats.isFile()) {
      await fsPromises.unlink(targetPath);
      console.log('File deleted safely');
    } else if (stats.isDirectory()) {
      await fsPromises.rm(targetPath, { recursive: true });
      console.log('Directory deleted safely');
    }
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.log('Target does not exist');
    } else {
      throw error;
    }
  }
}

// Usage examples
async function examples() {
  // Delete files
  await deleteFile('./temp-file.txt');
  
  // Delete directories
  await deleteDirectory('./temp-directory');
  
  // Safe deletion
  await safeDelete('./unknown-target');
  
  // Synchronous operations
  deleteFileSync('./sync-file.txt');
  deleteDirectorySync('./sync-directory');
}

The Problem: Production-Safe Deletion with Backup and Protection

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

// Advanced file deletion service with safety features
class FileDeletionService {
  constructor(options = {}) {
    this.enableBackup = options.enableBackup !== false;
    this.backupDirectory = options.backupDirectory || './backups';
    this.dryRun = options.dryRun === true;
    this.protectedPaths = new Set(options.protectedPaths || []);
    this.maxDepth = options.maxDepth || 50;
    this.enableLogging = options.enableLogging !== false;
    this.confirmDeletion = options.confirmDeletion === true;
    this.retentionDays = options.retentionDays || 30;
    this.safeModeEnabled = options.safeModeEnabled !== false;
  }

  // Delete file or directory with comprehensive safety checks
  async delete(targetPath, options = {}) {
    const {
      force = false,
      backup = this.enableBackup,
      recursive = true,
      preserveRoot = true,
      maxRetries = 3,
      retryDelay = 1000
    } = options;

    try {
      const absolutePath = path.resolve(targetPath);
      
      // Safety validations
      await this.validateDeletion(absolutePath, { preserveRoot, force });
      
      // Check existence and get metadata
      const targetInfo = await this.getTargetInfo(absolutePath);
      
      if (!targetInfo.exists) {
        return {
          success: true,
          path: absolutePath,
          action: 'skipped',
          reason: 'Target does not exist',
          alreadyDeleted: true
        };
      }

      // Log operation if enabled
      if (this.enableLogging) {
        console.log(`Preparing to delete: ${absolutePath} (${targetInfo.type})`);
      }

      // Handle dry run
      if (this.dryRun) {
        return this.simulateDeletion(absolutePath, targetInfo, options);
      }

      // Request confirmation if enabled
      if (this.confirmDeletion) {
        const confirmed = await this.requestConfirmation(absolutePath, targetInfo);
        if (!confirmed) {
          return {
            success: false,
            path: absolutePath,
            action: 'cancelled',
            reason: 'User cancelled deletion'
          };
        }
      }

      // Create backup if enabled
      let backupPath = null;
      if (backup) {
        backupPath = await this.createBackup(absolutePath, targetInfo);
      }

      // Perform deletion with retries
      const deletionResult = await this.performDeletion(absolutePath, targetInfo, {
        recursive,
        maxRetries,
        retryDelay
      });

      return {
        success: true,
        path: absolutePath,
        type: targetInfo.type,
        size: targetInfo.size,
        backup: backupPath,
        action: 'deleted',
        timestamp: new Date(),
        ...deletionResult
      };
    } catch (error) {
      return this.handleDeletionError(error, targetPath);
    }
  }

  // Delete multiple files/directories
  async deleteMultiple(targetPaths, options = {}) {
    const {
      concurrency = 5,
      continueOnError = true,
      sharedOptions = {}
    } = options;

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

    // Process targets in batches
    for (let i = 0; i < targetPaths.length; i += concurrency) {
      const batch = targetPaths.slice(i, i + concurrency);
      
      const batchPromises = batch.map(async (targetPath, index) => {
        try {
          const result = await this.delete(targetPath, sharedOptions);
          return { index: i + index, path: targetPath, ...result };
        } catch (error) {
          const errorResult = {
            index: i + index,
            path: targetPath,
            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);
          }
        } else {
          errors.push({
            index: -1,
            path: 'unknown',
            success: false,
            error: promiseResult.reason
          });
        }
      });
    }

    return {
      success: errors.length === 0 || continueOnError,
      results: results.sort((a, b) => a.index - b.index),
      errors,
      total: targetPaths.length,
      deleted: results.filter(r => r.action === 'deleted').length,
      skipped: results.filter(r => r.action === 'skipped').length,
      failed: errors.length
    };
  }

  // Delete files by pattern
  async deleteByPattern(baseDirectory, pattern, options = {}) {
    const {
      recursive = true,
      maxDepth = this.maxDepth,
      includeDirectories = false,
      testMode = this.dryRun
    } = options;

    try {
      const matches = await this.findMatches(baseDirectory, pattern, {
        recursive,
        maxDepth,
        includeDirectories
      });

      if (matches.length === 0) {
        return {
          success: true,
          matched: 0,
          deleted: 0,
          message: 'No files matched the pattern'
        };
      }

      // Sort by depth (deepest first) to avoid deleting parent before children
      const sortedMatches = matches.sort((a, b) => {
        const depthA = a.split(path.sep).length;
        const depthB = b.split(path.sep).length;
        return depthB - depthA;
      });

      const deleteOptions = { ...options, dryRun: testMode };
      const result = await this.deleteMultiple(sortedMatches, { sharedOptions: deleteOptions });

      return {
        success: result.success,
        pattern,
        baseDirectory,
        matched: matches.length,
        deleted: result.deleted,
        skipped: result.skipped,
        failed: result.failed,
        results: result.results,
        errors: result.errors
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        pattern,
        baseDirectory
      };
    }
  }

  // Clean up old files based on age
  async cleanupOldFiles(directory, options = {}) {
    const {
      maxAge = 30 * 24 * 60 * 60 * 1000, // 30 days
      pattern = null,
      recursive = true,
      dryRun = this.dryRun
    } = options;

    try {
      const cutoffDate = new Date(Date.now() - maxAge);
      const oldFiles = [];

      await this.findOldFiles(directory, cutoffDate, {
        pattern,
        recursive,
        oldFiles
      });

      if (oldFiles.length === 0) {
        return {
          success: true,
          directory,
          cutoffDate,
          found: 0,
          deleted: 0,
          message: 'No old files found'
        };
      }

      const deleteOptions = { dryRun, backup: false }; // Don't backup old files by default
      const result = await this.deleteMultiple(oldFiles, { sharedOptions: deleteOptions });

      return {
        success: result.success,
        directory,
        cutoffDate,
        found: oldFiles.length,
        deleted: result.deleted,
        failed: result.failed,
        results: result.results
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        directory
      };
    }
  }

  // Secure deletion (overwrite before delete)
  async secureDelete(filePath, options = {}) {
    const {
      passes = 3,
      backup = false,
      verifyDeletion = true
    } = options;

    try {
      const absolutePath = path.resolve(filePath);
      
      // Validate and get file info
      const fileInfo = await this.getTargetInfo(absolutePath);
      
      if (!fileInfo.exists) {
        return {
          success: true,
          path: absolutePath,
          action: 'skipped',
          reason: 'File does not exist'
        };
      }

      if (fileInfo.type !== 'file') {
        throw new Error('Secure deletion only supports files');
      }

      // Create backup if requested
      let backupPath = null;
      if (backup) {
        backupPath = await this.createBackup(absolutePath, fileInfo);
      }

      // Overwrite file content multiple times
      await this.overwriteFile(absolutePath, fileInfo.size, passes);

      // Delete the file
      await fsPromises.unlink(absolutePath);

      // Verify deletion
      if (verifyDeletion) {
        const stillExists = await this.fileExists(absolutePath);
        if (stillExists) {
          throw new Error('File still exists after secure deletion');
        }
      }

      return {
        success: true,
        path: absolutePath,
        action: 'securely_deleted',
        passes,
        backup: backupPath,
        originalSize: fileInfo.size,
        timestamp: new Date()
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
        path: filePath
      };
    }
  }

  // Helper methods
  async validateDeletion(absolutePath, options) {
    const { preserveRoot, force } = options;
    
    // Check if path is protected
    if (this.protectedPaths.has(absolutePath)) {
      throw new Error(`Path is protected from deletion: ${absolutePath}`);
    }

    // Prevent deletion of root directory
    if (preserveRoot) {
      const root = path.parse(absolutePath).root;
      if (absolutePath === root) {
        throw new Error('Cannot delete root directory');
      }
    }

    // Check depth to prevent excessive recursion
    const depth = absolutePath.split(path.sep).length;
    if (depth > this.maxDepth) {
      throw new Error(`Path depth exceeds maximum allowed: ${this.maxDepth}`);
    }

    // Safe mode additional checks
    if (this.safeModeEnabled && !force) {
      await this.performSafeModeChecks(absolutePath);
    }
  }

  async performSafeModeChecks(absolutePath) {
    // Check if path contains system directories
    const systemPaths = ['/bin', '/sbin', '/usr', '/lib', '/etc', 'C:\\Windows', 'C:\\Program Files'];
    
    if (systemPaths.some(sysPath => absolutePath.startsWith(path.resolve(sysPath)))) {
      throw new Error('Safe mode: Cannot delete system directories');
    }

    // Check if path contains many files (potential accident)
    try {
      const stats = await fsPromises.stat(absolutePath);
      if (stats.isDirectory()) {
        const entries = await fsPromises.readdir(absolutePath);
        if (entries.length > 1000) {
          throw new Error(`Safe mode: Directory contains ${entries.length} entries. Use force option if intentional.`);
        }
      }
    } catch (error) {
      // Ignore stat errors
    }
  }

  async getTargetInfo(targetPath) {
    try {
      const stats = await fsPromises.stat(targetPath);
      
      return {
        exists: true,
        type: stats.isFile() ? 'file' : stats.isDirectory() ? 'directory' : 'other',
        size: stats.size,
        modified: stats.mtime,
        permissions: stats.mode,
        isSymlink: stats.isSymbolicLink()
      };
    } catch (error) {
      if (error.code === 'ENOENT') {
        return { exists: false };
      }
      throw error;
    }
  }

  async performDeletion(absolutePath, targetInfo, options) {
    const { recursive, maxRetries, retryDelay } = options;
    
    let lastError;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        if (targetInfo.type === 'file') {
          await fsPromises.unlink(absolutePath);
        } else if (targetInfo.type === 'directory') {
          await fsPromises.rm(absolutePath, { recursive, force: true });
        } else {
          // Handle special files (symlinks, etc.)
          await fsPromises.unlink(absolutePath);
        }
        
        return {
          attempts: attempt + 1,
          retried: attempt > 0
        };
      } catch (error) {
        lastError = error;
        
        if (attempt < maxRetries && this.isRetryableError(error)) {
          await this.sleep(retryDelay * Math.pow(2, attempt));
          continue;
        }
        
        break;
      }
    }
    
    throw lastError;
  }

  async createBackup(targetPath, targetInfo) {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const fileName = path.basename(targetPath);
    const backupName = `${fileName}_backup_${timestamp}`;
    const backupPath = path.join(this.backupDirectory, backupName);
    
    // Ensure backup directory exists
    await fsPromises.mkdir(this.backupDirectory, { recursive: true });
    
    if (targetInfo.type === 'file') {
      await fsPromises.copyFile(targetPath, backupPath);
    } else if (targetInfo.type === 'directory') {
      await this.copyDirectory(targetPath, backupPath);
    }
    
    return backupPath;
  }

  async copyDirectory(source, destination) {
    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 destPath = path.join(destination, entry.name);
      
      if (entry.isDirectory()) {
        await this.copyDirectory(sourcePath, destPath);
      } else {
        await fsPromises.copyFile(sourcePath, destPath);
      }
    }
  }

  async overwriteFile(filePath, fileSize, passes) {
    const fileHandle = await fsPromises.open(filePath, 'r+');
    
    try {
      for (let pass = 0; pass < passes; pass++) {
        // Generate random data for this pass
        const chunkSize = 64 * 1024; // 64KB chunks
        let position = 0;
        
        while (position < fileSize) {
          const remainingBytes = Math.min(chunkSize, fileSize - position);
          const randomData = crypto.randomBytes(remainingBytes);
          
          await fileHandle.write(randomData, 0, remainingBytes, position);
          position += remainingBytes;
        }
        
        // Ensure data is written to disk
        await fileHandle.sync();
      }
    } finally {
      await fileHandle.close();
    }
  }

  async findMatches(baseDirectory, pattern, options) {
    const { recursive, maxDepth, includeDirectories } = options;
    const matches = [];
    const regex = new RegExp(pattern);
    
    await this.searchDirectory(baseDirectory, regex, {
      recursive,
      maxDepth,
      includeDirectories,
      currentDepth: 0,
      matches
    });
    
    return matches;
  }

  async searchDirectory(dirPath, pattern, options) {
    const { recursive, maxDepth, includeDirectories, currentDepth, matches } = options;
    
    if (currentDepth >= maxDepth) return;
    
    try {
      const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
      
      for (const entry of entries) {
        const fullPath = path.join(dirPath, entry.name);
        
        if (entry.isFile() || (includeDirectories && entry.isDirectory())) {
          if (pattern.test(entry.name)) {
            matches.push(fullPath);
          }
        }
        
        if (entry.isDirectory() && recursive) {
          await this.searchDirectory(fullPath, pattern, {
            ...options,
            currentDepth: currentDepth + 1
          });
        }
      }
    } catch (error) {
      // Continue search even if one directory fails
      console.warn(`Failed to search directory ${dirPath}:`, error.message);
    }
  }

  async findOldFiles(directory, cutoffDate, options) {
    const { pattern, recursive, oldFiles } = options;
    const regex = pattern ? new RegExp(pattern) : null;
    
    try {
      const entries = await fsPromises.readdir(directory, { withFileTypes: true });
      
      for (const entry of entries) {
        const fullPath = path.join(directory, entry.name);
        
        if (entry.isFile()) {
          if (!regex || regex.test(entry.name)) {
            const stats = await fsPromises.stat(fullPath);
            if (stats.mtime < cutoffDate) {
              oldFiles.push(fullPath);
            }
          }
        } else if (entry.isDirectory() && recursive) {
          await this.findOldFiles(fullPath, cutoffDate, options);
        }
      }
    } catch (error) {
      console.warn(`Failed to process directory ${directory}:`, error.message);
    }
  }

  simulateDeletion(absolutePath, targetInfo, options) {
    return {
      success: true,
      path: absolutePath,
      type: targetInfo.type,
      size: targetInfo.size,
      action: 'simulated',
      dryRun: true,
      wouldDelete: true,
      options
    };
  }

  async requestConfirmation(absolutePath, targetInfo) {
    // In a real application, this would show a UI prompt
    // For demo purposes, we'll assume confirmation
    console.log(`Confirm deletion of ${targetInfo.type}: ${absolutePath}`);
    return true;
  }

  handleDeletionError(error, targetPath) {
    let errorCode = error.code || 'UNKNOWN';
    let errorMessage = error.message;
    
    switch (error.code) {
      case 'ENOENT':
        errorMessage = `Target does not exist: ${targetPath}`;
        break;
      case 'EACCES':
        errorMessage = `Permission denied: ${targetPath}`;
        break;
      case 'EBUSY':
        errorMessage = `Target is busy: ${targetPath}`;
        break;
      case 'ENOTEMPTY':
        errorMessage = `Directory not empty: ${targetPath}`;
        break;
    }
    
    return {
      success: false,
      error: errorMessage,
      code: errorCode,
      path: targetPath,
      timestamp: new Date()
    };
  }

  isRetryableError(error) {
    const retryableCodes = ['EBUSY', 'EMFILE', 'ENFILE', 'EAGAIN'];
    return retryableCodes.includes(error.code);
  }

  async fileExists(filePath) {
    try {
      await fsPromises.access(filePath);
      return true;
    } catch {
      return false;
    }
  }

  async sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Express middleware for file deletion
function createDeletionMiddleware(deletionService) {
  return async (req, res, next) => {
    try {
      const { path: targetPath, options = {} } = req.body;
      
      if (!targetPath) {
        return res.status(400).json({
          error: 'Target path required',
          code: 'PATH_MISSING'
        });
      }

      const result = await deletionService.delete(targetPath, options);
      res.json(result);
    } catch (error) {
      console.error('Deletion middleware error:', error);
      res.status(500).json({
        error: 'Deletion failed',
        code: 'DELETION_ERROR'
      });
    }
  };
}

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

// Initialize deletion service
const deletionService = new FileDeletionService({
  enableBackup: true,
  backupDirectory: './backups',
  safeModeEnabled: true,
  protectedPaths: ['/etc', '/usr', '/bin', '/sbin']
});

app.use(express.json());

// Routes
app.delete('/api/delete', createDeletionMiddleware(deletionService));

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

    const result = await deletionService.deleteMultiple(paths, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/cleanup-old', async (req, res) => {
  try {
    const { directory, maxAge, pattern } = req.body;
    
    const result = await deletionService.cleanupOldFiles(directory, {
      maxAge: maxAge || 30 * 24 * 60 * 60 * 1000,
      pattern
    });
    
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

module.exports = {
  FileDeletionService,
  createDeletionMiddleware,
  app
};

File and directory deletion solves "cleanup automation", "storage management", and "secure data removal" issues. Implement safe deletion with validation, backup creation, and protection against accidental removal. Support pattern-based deletion, old file cleanup, secure overwriting. Handle retries for busy files and provide comprehensive error information. Alternative: trash/recycle bins, soft deletion, cloud storage APIs.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js