Table Of Contents
- Quick Fix: Basic File and Directory Deletion
- The Problem: Production-Safe Deletion with Backup and Protection
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!