Skip to content

File Handling

Overview

File operations are fundamental in Node.js applications. This chapter covers reading, writing, streaming, file system operations, and best practices for handling files efficiently and securely.

Basic File Operations

Reading Files

javascript
// file-reading.js
const fs = require('fs');
const path = require('path');

// Synchronous file reading (blocking)
function readFileSync(filePath) {
  try {
    const data = fs.readFileSync(filePath, 'utf8');
    console.log('File read synchronously:', data.length, 'characters');
    return data;
  } catch (error) {
    console.error('Sync read error:', error.message);
    throw error;
  }
}

// Asynchronous file reading with callbacks
function readFileCallback(filePath, callback) {
  fs.readFile(filePath, 'utf8', (error, data) => {
    if (error) {
      return callback(error, null);
    }
    console.log('File read with callback:', data.length, 'characters');
    callback(null, data);
  });
}

// Promise-based file reading
function readFilePromise(filePath) {
  return fs.promises.readFile(filePath, 'utf8')
    .then(data => {
      console.log('File read with promise:', data.length, 'characters');
      return data;
    })
    .catch(error => {
      console.error('Promise read error:', error.message);
      throw error;
    });
}

// Async/await file reading
async function readFileAsync(filePath) {
  try {
    const data = await fs.promises.readFile(filePath, 'utf8');
    console.log('File read with async/await:', data.length, 'characters');
    return data;
  } catch (error) {
    console.error('Async read error:', error.message);
    throw error;
  }
}

// Reading binary files
async function readBinaryFile(filePath) {
  try {
    const buffer = await fs.promises.readFile(filePath);
    console.log('Binary file read:', buffer.length, 'bytes');
    return buffer;
  } catch (error) {
    console.error('Binary read error:', error.message);
    throw error;
  }
}

// Reading files in chunks
function readFileInChunks(filePath, chunkSize = 1024) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    const stream = fs.createReadStream(filePath, { 
      encoding: 'utf8',
      highWaterMark: chunkSize 
    });

    stream.on('data', (chunk) => {
      console.log('Chunk received:', chunk.length, 'characters');
      chunks.push(chunk);
    });

    stream.on('end', () => {
      const data = chunks.join('');
      console.log('File read in chunks:', data.length, 'total characters');
      resolve(data);
    });

    stream.on('error', (error) => {
      console.error('Chunk read error:', error.message);
      reject(error);
    });
  });
}

// Usage examples
async function demonstrateReading() {
  const filePath = 'package.json';
  
  try {
    // Different reading methods
    await readFileAsync(filePath);
    await readFilePromise(filePath);
    await readFileInChunks(filePath, 512);
    
    readFileCallback(filePath, (error, data) => {
      if (error) {
        console.error('Callback error:', error.message);
      } else {
        console.log('Callback success');
      }
    });
  } catch (error) {
    console.error('Reading demonstration error:', error.message);
  }
}

Writing Files

javascript
// file-writing.js
const fs = require('fs');
const path = require('path');

// Synchronous file writing
function writeFileSync(filePath, data) {
  try {
    fs.writeFileSync(filePath, data, 'utf8');
    console.log('File written synchronously');
  } catch (error) {
    console.error('Sync write error:', error.message);
    throw error;
  }
}

// Asynchronous file writing
async function writeFileAsync(filePath, data) {
  try {
    await fs.promises.writeFile(filePath, data, 'utf8');
    console.log('File written asynchronously');
  } catch (error) {
    console.error('Async write error:', error.message);
    throw error;
  }
}

// Appending to files
async function appendToFile(filePath, data) {
  try {
    await fs.promises.appendFile(filePath, data, 'utf8');
    console.log('Data appended to file');
  } catch (error) {
    console.error('Append error:', error.message);
    throw error;
  }
}

// Writing JSON data
async function writeJSONFile(filePath, data) {
  try {
    const jsonString = JSON.stringify(data, null, 2);
    await fs.promises.writeFile(filePath, jsonString, 'utf8');
    console.log('JSON file written');
  } catch (error) {
    console.error('JSON write error:', error.message);
    throw error;
  }
}

// Writing with streams (for large files)
function writeFileStream(filePath, data) {
  return new Promise((resolve, reject) => {
    const stream = fs.createWriteStream(filePath, { encoding: 'utf8' });
    
    stream.on('error', (error) => {
      console.error('Stream write error:', error.message);
      reject(error);
    });
    
    stream.on('finish', () => {
      console.log('Stream write completed');
      resolve();
    });
    
    // Write data in chunks if it's large
    if (typeof data === 'string' && data.length > 1024 * 1024) {
      const chunkSize = 1024 * 1024; // 1MB chunks
      for (let i = 0; i < data.length; i += chunkSize) {
        const chunk = data.slice(i, i + chunkSize);
        stream.write(chunk);
      }
    } else {
      stream.write(data);
    }
    
    stream.end();
  });
}

// Atomic file writing (write to temp file, then rename)
async function writeFileAtomic(filePath, data) {
  const tempPath = filePath + '.tmp';
  
  try {
    await fs.promises.writeFile(tempPath, data, 'utf8');
    await fs.promises.rename(tempPath, filePath);
    console.log('File written atomically');
  } catch (error) {
    // Clean up temp file if it exists
    try {
      await fs.promises.unlink(tempPath);
    } catch (cleanupError) {
      // Ignore cleanup errors
    }
    console.error('Atomic write error:', error.message);
    throw error;
  }
}

// Usage examples
async function demonstrateWriting() {
  const testData = {
    message: 'Hello, World!',
    timestamp: new Date().toISOString(),
    numbers: [1, 2, 3, 4, 5]
  };
  
  try {
    await writeJSONFile('test-output.json', testData);
    await appendToFile('test-log.txt', `Log entry: ${new Date()}\n`);
    await writeFileAtomic('atomic-test.txt', 'This was written atomically');
    await writeFileStream('stream-test.txt', 'This was written with streams');
  } catch (error) {
    console.error('Writing demonstration error:', error.message);
  }
}

File System Operations

Directory Operations

javascript
// directory-operations.js
const fs = require('fs');
const path = require('path');

class DirectoryManager {
  // Create directory (recursive)
  async createDirectory(dirPath) {
    try {
      await fs.promises.mkdir(dirPath, { recursive: true });
      console.log('Directory created:', dirPath);
    } catch (error) {
      console.error('Create directory error:', error.message);
      throw error;
    }
  }

  // List directory contents
  async listDirectory(dirPath, options = {}) {
    try {
      const items = await fs.promises.readdir(dirPath, { withFileTypes: true });
      
      const result = {
        files: [],
        directories: [],
        total: items.length
      };

      for (const item of items) {
        const itemPath = path.join(dirPath, item.name);
        const stats = await fs.promises.stat(itemPath);
        
        const itemInfo = {
          name: item.name,
          path: itemPath,
          size: stats.size,
          created: stats.birthtime,
          modified: stats.mtime,
          isDirectory: item.isDirectory(),
          isFile: item.isFile()
        };

        if (item.isDirectory()) {
          result.directories.push(itemInfo);
        } else {
          result.files.push(itemInfo);
        }
      }

      if (options.sortBy) {
        const sortFn = (a, b) => {
          const aValue = a[options.sortBy];
          const bValue = b[options.sortBy];
          return options.sortOrder === 'desc' ? bValue - aValue : aValue - bValue;
        };
        
        result.files.sort(sortFn);
        result.directories.sort(sortFn);
      }

      return result;
    } catch (error) {
      console.error('List directory error:', error.message);
      throw error;
    }
  }

  // Copy directory recursively
  async copyDirectory(source, destination) {
    try {
      await this.createDirectory(destination);
      const items = await fs.promises.readdir(source, { withFileTypes: true });

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

        if (item.isDirectory()) {
          await this.copyDirectory(sourcePath, destPath);
        } else {
          await fs.promises.copyFile(sourcePath, destPath);
        }
      }

      console.log('Directory copied:', source, '->', destination);
    } catch (error) {
      console.error('Copy directory error:', error.message);
      throw error;
    }
  }

  // Remove directory recursively
  async removeDirectory(dirPath) {
    try {
      await fs.promises.rm(dirPath, { recursive: true, force: true });
      console.log('Directory removed:', dirPath);
    } catch (error) {
      console.error('Remove directory error:', error.message);
      throw error;
    }
  }

  // Get directory size
  async getDirectorySize(dirPath) {
    try {
      let totalSize = 0;
      const items = await fs.promises.readdir(dirPath, { withFileTypes: true });

      for (const item of items) {
        const itemPath = path.join(dirPath, item.name);
        
        if (item.isDirectory()) {
          totalSize += await this.getDirectorySize(itemPath);
        } else {
          const stats = await fs.promises.stat(itemPath);
          totalSize += stats.size;
        }
      }

      return totalSize;
    } catch (error) {
      console.error('Get directory size error:', error.message);
      throw error;
    }
  }

  // Watch directory for changes
  watchDirectory(dirPath, callback) {
    try {
      const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
        callback({
          event: eventType,
          filename,
          path: path.join(dirPath, filename || ''),
          timestamp: new Date()
        });
      });

      console.log('Watching directory:', dirPath);
      return watcher;
    } catch (error) {
      console.error('Watch directory error:', error.message);
      throw error;
    }
  }
}

// Usage example
async function demonstrateDirectoryOperations() {
  const dirManager = new DirectoryManager();
  
  try {
    await dirManager.createDirectory('test-dir/sub-dir');
    
    const contents = await dirManager.listDirectory('.', { sortBy: 'modified', sortOrder: 'desc' });
    console.log('Directory contents:', contents);
    
    const size = await dirManager.getDirectorySize('.');
    console.log('Directory size:', size, 'bytes');
    
    // Watch for changes
    const watcher = dirManager.watchDirectory('.', (change) => {
      console.log('Directory change:', change);
    });
    
    // Stop watching after 10 seconds
    setTimeout(() => {
      watcher.close();
      console.log('Stopped watching directory');
    }, 10000);
    
  } catch (error) {
    console.error('Directory operations error:', error.message);
  }
}

module.exports = DirectoryManager;

File Streaming

Advanced Stream Operations

javascript
// file-streaming.js
const fs = require('fs');
const { pipeline, Transform } = require('stream');
const { promisify } = require('util');
const pipelineAsync = promisify(pipeline);

// Custom transform streams
class LineCounter extends Transform {
  constructor() {
    super({ objectMode: true });
    this.lineCount = 0;
  }

  _transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n');
    this.lineCount += lines.length - 1; // -1 because last split might not be complete line
    callback(null, chunk);
  }

  _flush(callback) {
    console.log('Total lines processed:', this.lineCount);
    callback();
  }
}

class DataProcessor extends Transform {
  constructor(processFn) {
    super();
    this.processFn = processFn;
  }

  _transform(chunk, encoding, callback) {
    try {
      const processed = this.processFn(chunk);
      callback(null, processed);
    } catch (error) {
      callback(error);
    }
  }
}

// File processing with streams
async function processLargeFile(inputPath, outputPath, processor) {
  try {
    const readStream = fs.createReadStream(inputPath);
    const writeStream = fs.createWriteStream(outputPath);
    const lineCounter = new LineCounter();
    const dataProcessor = new DataProcessor(processor);

    await pipelineAsync(
      readStream,
      lineCounter,
      dataProcessor,
      writeStream
    );

    console.log('File processing completed');
  } catch (error) {
    console.error('Stream processing error:', error.message);
    throw error;
  }
}

// CSV file processing
class CSVProcessor extends Transform {
  constructor(options = {}) {
    super({ objectMode: true });
    this.headers = null;
    this.delimiter = options.delimiter || ',';
    this.skipHeader = options.skipHeader || false;
    this.rowCount = 0;
  }

  _transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n');
    
    for (const line of lines) {
      if (!line.trim()) continue;
      
      const values = line.split(this.delimiter);
      
      if (!this.headers && !this.skipHeader) {
        this.headers = values.map(h => h.trim());
        continue;
      }
      
      if (!this.headers) {
        this.headers = values.map((_, i) => `column_${i}`);
      }
      
      const row = {};
      this.headers.forEach((header, index) => {
        row[header] = values[index]?.trim() || '';
      });
      
      this.rowCount++;
      this.push(JSON.stringify(row) + '\n');
    }
    
    callback();
  }

  _flush(callback) {
    console.log('CSV processing completed. Rows processed:', this.rowCount);
    callback();
  }
}

// Usage examples
async function demonstrateStreaming() {
  try {
    // Process text file (convert to uppercase)
    await processLargeFile(
      'input.txt',
      'output.txt',
      (chunk) => chunk.toString().toUpperCase()
    );

    // Process CSV file
    const csvProcessor = new CSVProcessor({ delimiter: ',' });
    const csvInput = fs.createReadStream('data.csv');
    const jsonOutput = fs.createWriteStream('data.json');

    await pipelineAsync(csvInput, csvProcessor, jsonOutput);
    
  } catch (error) {
    console.error('Streaming demonstration error:', error.message);
  }
}

module.exports = { processLargeFile, CSVProcessor, LineCounter, DataProcessor };

File Security and Validation

Secure File Operations

javascript
// file-security.js
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

class SecureFileHandler {
  constructor(options = {}) {
    this.allowedExtensions = options.allowedExtensions || ['.txt', '.json', '.csv'];
    this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
    this.uploadDir = options.uploadDir || './uploads';
    this.quarantineDir = options.quarantineDir || './quarantine';
  }

  // Validate file path (prevent directory traversal)
  validatePath(filePath) {
    const normalizedPath = path.normalize(filePath);
    const resolvedPath = path.resolve(normalizedPath);
    const allowedDir = path.resolve(this.uploadDir);

    if (!resolvedPath.startsWith(allowedDir)) {
      throw new Error('Invalid file path: Directory traversal detected');
    }

    return resolvedPath;
  }

  // Validate file extension
  validateExtension(filename) {
    const ext = path.extname(filename).toLowerCase();
    
    if (!this.allowedExtensions.includes(ext)) {
      throw new Error(`Invalid file extension: ${ext}. Allowed: ${this.allowedExtensions.join(', ')}`);
    }

    return true;
  }

  // Validate file size
  async validateFileSize(filePath) {
    try {
      const stats = await fs.promises.stat(filePath);
      
      if (stats.size > this.maxFileSize) {
        throw new Error(`File too large: ${stats.size} bytes. Max allowed: ${this.maxFileSize} bytes`);
      }

      return stats.size;
    } catch (error) {
      throw new Error(`Cannot validate file size: ${error.message}`);
    }
  }

  // Generate secure filename
  generateSecureFilename(originalName) {
    const ext = path.extname(originalName);
    const timestamp = Date.now();
    const random = crypto.randomBytes(8).toString('hex');
    
    return `${timestamp}_${random}${ext}`;
  }

  // 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', (error) => {
        reject(error);
      });
    });
  }

  // Scan file content for malicious patterns
  async scanFileContent(filePath) {
    try {
      const content = await fs.promises.readFile(filePath, 'utf8');
      
      // Basic malicious pattern detection
      const maliciousPatterns = [
        /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
        /javascript:/gi,
        /vbscript:/gi,
        /onload\s*=/gi,
        /onerror\s*=/gi
      ];

      for (const pattern of maliciousPatterns) {
        if (pattern.test(content)) {
          throw new Error('Malicious content detected');
        }
      }

      return true;
    } catch (error) {
      if (error.message === 'Malicious content detected') {
        throw error;
      }
      // If file is not text, skip content scanning
      return true;
    }
  }

  // Secure file upload
  async secureUpload(sourceFile, originalName) {
    try {
      // Validate extension
      this.validateExtension(originalName);

      // Generate secure filename
      const secureFilename = this.generateSecureFilename(originalName);
      const targetPath = path.join(this.uploadDir, secureFilename);

      // Ensure upload directory exists
      await fs.promises.mkdir(this.uploadDir, { recursive: true });

      // Copy file to secure location
      await fs.promises.copyFile(sourceFile, targetPath);

      // Validate file size
      const fileSize = await this.validateFileSize(targetPath);

      // Scan for malicious content
      await this.scanFileContent(targetPath);

      // Calculate file hash for integrity
      const fileHash = await this.calculateFileHash(targetPath);

      console.log('File uploaded securely:', {
        originalName,
        secureFilename,
        size: fileSize,
        hash: fileHash
      });

      return {
        filename: secureFilename,
        path: targetPath,
        size: fileSize,
        hash: fileHash,
        uploadedAt: new Date()
      };

    } catch (error) {
      // Move suspicious files to quarantine
      if (error.message.includes('Malicious content')) {
        await this.quarantineFile(sourceFile, originalName);
      }
      
      console.error('Secure upload error:', error.message);
      throw error;
    }
  }

  // Quarantine suspicious files
  async quarantineFile(filePath, originalName) {
    try {
      await fs.promises.mkdir(this.quarantineDir, { recursive: true });
      
      const quarantineFilename = `${Date.now()}_${originalName}`;
      const quarantinePath = path.join(this.quarantineDir, quarantineFilename);
      
      await fs.promises.copyFile(filePath, quarantinePath);
      
      console.log('File quarantined:', quarantineFilename);
    } catch (error) {
      console.error('Quarantine error:', error.message);
    }
  }
}

module.exports = SecureFileHandler;

Next Steps

In the next chapter, we'll explore advanced features of Node.js including clustering, worker threads, and performance optimization.

Key Takeaways

  • Use asynchronous file operations to avoid blocking the event loop
  • Streams are efficient for processing large files
  • Always validate file paths to prevent security vulnerabilities
  • Implement proper error handling for file operations
  • Use atomic operations for critical file writes
  • Monitor file system operations for performance optimization

Content is for learning and research only.