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!