Navigation

Node.js

How to Check if File or Directory Exists

Safely check file and directory existence with modern methods, handle race conditions, and validate file accessibility in Node.js 2025

Table Of Contents

Quick Fix: Basic Existence Checking

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

// Modern async method (recommended)
async function fileExists(filePath) {
  try {
    await fsPromises.access(filePath);
    return true;
  } catch {
    return false;
  }
}

// Check specific file permissions
async function fileExistsWithPermissions(filePath) {
  try {
    await fsPromises.access(filePath, fs.constants.F_OK | fs.constants.R_OK);
    return true;
  } catch {
    return false;
  }
}

// Synchronous check (blocking)
function fileExistsSync(filePath) {
  try {
    fs.accessSync(filePath);
    return true;
  } catch {
    return false;
  }
}

// Check if path is a file or directory
async function getPathType(filePath) {
  try {
    const stats = await fsPromises.stat(filePath);
    
    if (stats.isFile()) return 'file';
    if (stats.isDirectory()) return 'directory';
    if (stats.isSymbolicLink()) return 'symlink';
    if (stats.isSocket()) return 'socket';
    if (stats.isFIFO()) return 'fifo';
    if (stats.isBlockDevice()) return 'block-device';
    if (stats.isCharacterDevice()) return 'character-device';
    
    return 'unknown';
  } catch {
    return null;
  }
}

// Legacy method (deprecated but still works)
function legacyFileExists(filePath) {
  return fs.existsSync(filePath); // Don't use this in new code
}

// Usage examples
async function examples() {
  const testFile = './test.txt';
  const testDir = './test-directory';
  
  // Check if file exists
  const exists = await fileExists(testFile);
  console.log('File exists:', exists);
  
  // Check with permissions
  const readable = await fileExistsWithPermissions(testFile);
  console.log('File readable:', readable);
  
  // Get path type
  const type = await getPathType(testFile);
  console.log('Path type:', type);
  
  // Synchronous check
  const syncExists = fileExistsSync(testFile);
  console.log('Sync exists:', syncExists);
}

The Problem: Comprehensive File System Validation

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

// Advanced file system checker with comprehensive validation
class FileSystemChecker {
  constructor(options = {}) {
    this.followSymlinks = options.followSymlinks !== false;
    this.checkPermissions = options.checkPermissions !== false;
    this.enableCache = options.enableCache === true;
    this.cacheTimeout = options.cacheTimeout || 30000; // 30 seconds
    this.cache = new Map();
  }

  // Check if file or directory exists with detailed information
  async exists(targetPath, options = {}) {
    const {
      type = null, // 'file', 'directory', or null for any
      permissions = null, // fs.constants flags
      followSymlinks = this.followSymlinks,
      useCache = this.enableCache
    } = options;

    try {
      const absolutePath = path.resolve(targetPath);
      
      // Check cache first
      if (useCache) {
        const cached = this.getCached(absolutePath, type, permissions);
        if (cached !== null) {
          return cached;
        }
      }

      const result = await this.performExistenceCheck(absolutePath, {
        type,
        permissions,
        followSymlinks
      });

      // Cache the result
      if (useCache) {
        this.setCached(absolutePath, type, permissions, result);
      }

      return result;
    } catch (error) {
      return {
        exists: false,
        accessible: false,
        error: error.message,
        code: error.code,
        path: targetPath
      };
    }
  }

  // Perform the actual existence check
  async performExistenceCheck(absolutePath, options) {
    const { type, permissions, followSymlinks } = options;
    
    // Use lstat if not following symlinks, stat if following
    const statMethod = followSymlinks ? fsPromises.stat : fsPromises.lstat;
    
    try {
      // Check basic existence and get stats
      const stats = await statMethod(absolutePath);
      
      // Validate type if specified
      if (type) {
        const actualType = this.getFileType(stats);
        if (actualType !== type) {
          return {
            exists: true,
            accessible: false,
            expectedType: type,
            actualType,
            error: `Expected ${type} but found ${actualType}`,
            path: absolutePath
          };
        }
      }

      // Check permissions if specified
      let permissionsValid = true;
      let permissionError = null;
      
      if (permissions && this.checkPermissions) {
        try {
          await fsPromises.access(absolutePath, permissions);
        } catch (permError) {
          permissionsValid = false;
          permissionError = permError.message;
        }
      }

      return {
        exists: true,
        accessible: permissionsValid,
        type: this.getFileType(stats),
        size: stats.size,
        permissions: {
          readable: await this.checkPermission(absolutePath, fs.constants.R_OK),
          writable: await this.checkPermission(absolutePath, fs.constants.W_OK),
          executable: await this.checkPermission(absolutePath, fs.constants.X_OK)
        },
        timestamps: {
          created: stats.birthtime,
          modified: stats.mtime,
          accessed: stats.atime,
          changed: stats.ctime
        },
        isSymlink: stats.isSymbolicLink(),
        permissionError,
        path: absolutePath
      };
    } catch (error) {
      // Handle specific error cases
      if (error.code === 'ENOENT') {
        return {
          exists: false,
          accessible: false,
          error: 'File or directory not found',
          code: 'ENOENT',
          path: absolutePath
        };
      }
      
      if (error.code === 'EACCES') {
        return {
          exists: true,
          accessible: false,
          error: 'Permission denied',
          code: 'EACCES',
          path: absolutePath
        };
      }
      
      throw error;
    }
  }

  // Check specific file permission
  async checkPermission(filePath, permission) {
    try {
      await fsPromises.access(filePath, permission);
      return true;
    } catch {
      return false;
    }
  }

  // Get file type from stats
  getFileType(stats) {
    if (stats.isFile()) return 'file';
    if (stats.isDirectory()) return 'directory';
    if (stats.isSymbolicLink()) return 'symlink';
    if (stats.isSocket()) return 'socket';
    if (stats.isFIFO()) return 'fifo';
    if (stats.isBlockDevice()) return 'block-device';
    if (stats.isCharacterDevice()) return 'character-device';
    return 'unknown';
  }

  // Bulk existence checking for multiple paths
  async existsMultiple(paths, options = {}) {
    const { concurrency = 10, continueOnError = true } = options;
    
    const results = [];
    
    // Process in batches to control concurrency
    for (let i = 0; i < paths.length; i += concurrency) {
      const batch = paths.slice(i, i + concurrency);
      
      const batchPromises = batch.map(async (targetPath, index) => {
        try {
          const result = await this.exists(targetPath, options);
          return { index: i + index, path: targetPath, ...result };
        } catch (error) {
          if (continueOnError) {
            return {
              index: i + index,
              path: targetPath,
              exists: false,
              accessible: false,
              error: error.message
            };
          } else {
            throw error;
          }
        }
      });

      const batchResults = await Promise.allSettled(batchPromises);
      
      batchResults.forEach(promiseResult => {
        if (promiseResult.status === 'fulfilled') {
          results.push(promiseResult.value);
        } else if (continueOnError) {
          results.push({
            index: -1,
            path: 'unknown',
            exists: false,
            accessible: false,
            error: promiseResult.reason.message
          });
        }
      });
    }

    return {
      results: results.sort((a, b) => a.index - b.index),
      total: paths.length,
      existing: results.filter(r => r.exists).length,
      accessible: results.filter(r => r.accessible).length,
      errors: results.filter(r => r.error).length
    };
  }

  // Race-condition safe file operations
  async safeFileOperation(filePath, operation, options = {}) {
    const maxRetries = options.maxRetries || 3;
    const retryDelay = options.retryDelay || 100;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        // Check existence before operation
        const existsResult = await this.exists(filePath, options);
        
        if (!existsResult.exists && operation.requiresExistence) {
          throw new Error(`File does not exist: ${filePath}`);
        }
        
        if (existsResult.exists && operation.requiresNonExistence) {
          throw new Error(`File already exists: ${filePath}`);
        }
        
        // Perform the operation
        const result = await operation.execute(filePath, existsResult);
        
        return {
          success: true,
          result,
          attempts: attempt + 1,
          fileInfo: existsResult
        };
      } catch (error) {
        if (attempt === maxRetries || !this.isRetryableError(error)) {
          throw error;
        }
        
        // Wait before retry
        await new Promise(resolve => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
      }
    }
  }

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

  // Directory traversal with existence checking
  async findExisting(searchPaths, options = {}) {
    const {
      type = null,
      pattern = null,
      maxDepth = 3,
      followSymlinks = this.followSymlinks
    } = options;

    const found = [];
    const processed = new Set();

    for (const searchPath of searchPaths) {
      await this.traverseDirectory(searchPath, {
        type,
        pattern,
        maxDepth,
        followSymlinks,
        currentDepth: 0,
        found,
        processed
      });
    }

    return found;
  }

  // Traverse directory recursively
  async traverseDirectory(dirPath, options) {
    const { type, pattern, maxDepth, currentDepth, found, processed, followSymlinks } = options;
    
    if (currentDepth >= maxDepth) return;
    
    try {
      const absolutePath = path.resolve(dirPath);
      
      // Avoid infinite loops with symlinks
      if (processed.has(absolutePath)) return;
      processed.add(absolutePath);

      const dirResult = await this.exists(absolutePath, { type: 'directory', followSymlinks });
      
      if (!dirResult.exists || !dirResult.accessible) return;

      const entries = await fsPromises.readdir(absolutePath);
      
      for (const entry of entries) {
        const entryPath = path.join(absolutePath, entry);
        const entryResult = await this.exists(entryPath, { followSymlinks });
        
        if (!entryResult.exists) continue;

        // Check type filter
        if (type && entryResult.type !== type) {
          // Recurse into directories even if we're looking for files
          if (entryResult.type === 'directory') {
            await this.traverseDirectory(entryPath, {
              ...options,
              currentDepth: currentDepth + 1
            });
          }
          continue;
        }

        // Check pattern filter
        if (pattern) {
          const regex = new RegExp(pattern);
          if (!regex.test(entry)) {
            // Recurse into directories even if name doesn't match
            if (entryResult.type === 'directory') {
              await this.traverseDirectory(entryPath, {
                ...options,
                currentDepth: currentDepth + 1
              });
            }
            continue;
          }
        }

        found.push({
          path: entryPath,
          name: entry,
          ...entryResult
        });

        // Recurse into directories
        if (entryResult.type === 'directory') {
          await this.traverseDirectory(entryPath, {
            ...options,
            currentDepth: currentDepth + 1
          });
        }
      }
    } catch (error) {
      // Continue traversal even if one directory fails
      console.warn(`Failed to traverse ${dirPath}:`, error.message);
    }
  }

  // Synchronous existence checking
  existsSync(targetPath, options = {}) {
    const { type = null, permissions = null } = options;
    
    try {
      const absolutePath = path.resolve(targetPath);
      
      // Check basic existence
      fs.accessSync(absolutePath);
      
      // Get stats for type checking
      const stats = fs.statSync(absolutePath);
      
      // Validate type if specified
      if (type) {
        const actualType = this.getFileType(stats);
        if (actualType !== type) {
          return {
            exists: true,
            accessible: false,
            expectedType: type,
            actualType,
            error: `Expected ${type} but found ${actualType}`
          };
        }
      }
      
      // Check permissions if specified
      if (permissions) {
        try {
          fs.accessSync(absolutePath, permissions);
        } catch {
          return {
            exists: true,
            accessible: false,
            error: 'Permission denied'
          };
        }
      }
      
      return {
        exists: true,
        accessible: true,
        type: this.getFileType(stats),
        size: stats.size,
        path: absolutePath
      };
    } catch (error) {
      return {
        exists: false,
        accessible: false,
        error: error.message,
        code: error.code
      };
    }
  }

  // Cache management
  getCached(path, type, permissions) {
    const key = `${path}:${type}:${permissions}`;
    const cached = this.cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return cached.result;
    }
    
    if (cached) {
      this.cache.delete(key);
    }
    
    return null;
  }

  setCached(path, type, permissions, result) {
    const key = `${path}:${type}:${permissions}`;
    this.cache.set(key, {
      result,
      timestamp: Date.now()
    });
  }

  clearCache() {
    this.cache.clear();
  }
}

// Express middleware for path validation
function createPathValidationMiddleware(checker) {
  return async (req, res, next) => {
    try {
      const { path: targetPath, type, permissions } = req.query;
      
      if (!targetPath) {
        return res.status(400).json({
          error: 'Path parameter required',
          code: 'PATH_MISSING'
        });
      }

      // Security: prevent path traversal
      const safePath = path.resolve(targetPath);
      const allowedDir = path.resolve('./allowed-files');
      
      if (!safePath.startsWith(allowedDir)) {
        return res.status(403).json({
          error: 'Access denied: path outside allowed directory',
          code: 'PATH_FORBIDDEN'
        });
      }

      const result = await checker.exists(safePath, {
        type,
        permissions: permissions ? parseInt(permissions) : null
      });

      res.json(result);
    } catch (error) {
      console.error('Path validation middleware error:', error);
      res.status(500).json({
        error: 'Path validation failed',
        code: 'VALIDATION_ERROR'
      });
    }
  };
}

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

// Initialize file system checker
const fsChecker = new FileSystemChecker({
  followSymlinks: true,
  checkPermissions: true,
  enableCache: true
});

app.use(express.json());

// Routes
app.get('/api/exists', createPathValidationMiddleware(fsChecker));

app.post('/api/batch-check', 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 fsChecker.existsMultiple(paths, options);
    res.json(result);
  } catch (error) {
    res.status(500).json({
      error: error.message,
      code: 'BATCH_CHECK_ERROR'
    });
  }
});

app.get('/api/find', async (req, res) => {
  try {
    const { searchPaths, type, pattern, maxDepth } = req.query;
    
    const paths = Array.isArray(searchPaths) ? searchPaths : [searchPaths];
    
    const found = await fsChecker.findExisting(paths, {
      type,
      pattern,
      maxDepth: maxDepth ? parseInt(maxDepth) : 3
    });
    
    res.json({
      found: found.length,
      results: found
    });
  } catch (error) {
    res.status(500).json({
      error: error.message,
      code: 'FIND_ERROR'
    });
  }
});

// Demonstration
async function demonstrateUsage() {
  try {
    // Basic existence check
    const exists = await fsChecker.exists('./package.json');
    console.log('Package.json exists:', exists.exists);

    // Check with type validation
    const dirExists = await fsChecker.exists('./', { type: 'directory' });
    console.log('Current directory valid:', dirExists.accessible);

    // Check multiple files
    const multiResult = await fsChecker.existsMultiple([
      './package.json',
      './README.md',
      './nonexistent.txt'
    ]);
    console.log('Multi-check results:', multiResult.existing, 'of', multiResult.total);

    // Find files with pattern
    const foundFiles = await fsChecker.findExisting(['.'], {
      type: 'file',
      pattern: '\\.js$',
      maxDepth: 2
    });
    console.log('Found JS files:', foundFiles.length);

    // Safe operation example
    const safeOp = await fsChecker.safeFileOperation('./test.txt', {
      requiresNonExistence: true,
      execute: async (filePath) => {
        await fsPromises.writeFile(filePath, 'test content');
        return 'File created';
      }
    });
    console.log('Safe operation:', safeOp.success);
  } catch (error) {
    console.error('Demonstration error:', error);
  }
}

module.exports = {
  FileSystemChecker,
  createPathValidationMiddleware,
  app
};

File and directory existence checking solves "path validation", "race condition prevention", and "permission verification" issues. Use modern fs.access() over deprecated fs.exists(), handle permissions properly, avoid race conditions. Support bulk operations, directory traversal, caching for performance. Implement security validation and proper error handling. Alternative: try-catch with file operations, stat-based checking, third-party file utilities.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js