file.ts•6.17 kB
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from './logger';
const logger = createLogger('FileUtil');
/**
* Utility functions for file operations with security hardening
*/
// Maximum file size for reads (10MB)
const MAX_FILE_SIZE = 10 * 1024 * 1024;
// Get the working directory (can be overridden by environment variable)
const WORKING_DIR = process.env.QUICKBASE_WORKING_DIR || process.cwd();
/**
* Validate and sanitize a file path to prevent directory traversal
* @param filePath The file path to validate
* @returns Sanitized absolute path or null if invalid
*/
function sanitizePath(filePath: string): string | null {
try {
// Resolve to absolute path
const absolutePath = path.resolve(WORKING_DIR, filePath);
// Ensure the path is within the working directory
const relative = path.relative(WORKING_DIR, absolutePath);
// Check for directory traversal attempts
if (relative.startsWith('..') || path.isAbsolute(relative)) {
logger.error('Path traversal attempt detected', {
filePath,
absolutePath,
relative,
workingDir: WORKING_DIR
});
return null;
}
return absolutePath;
} catch (error) {
logger.error('Error sanitizing path', { filePath, error });
return null;
}
}
/**
* Check if a file exists
* @param filePath File path to check
* @returns True if the file exists
*/
export function fileExists(filePath: string): boolean {
try {
const safePath = sanitizePath(filePath);
if (!safePath) {
return false;
}
return fs.existsSync(safePath) && fs.statSync(safePath).isFile();
} catch (error) {
logger.error('Error checking if file exists', { filePath, error });
return false;
}
}
/**
* Ensure a directory exists, creating it if necessary
* @param dirPath Directory path to ensure
* @returns True if the directory exists or was created
*/
export function ensureDirectoryExists(dirPath: string): boolean {
try {
const safePath = sanitizePath(dirPath);
if (!safePath) {
return false;
}
if (fs.existsSync(safePath)) {
return fs.statSync(safePath).isDirectory();
}
// Create the directory
fs.mkdirSync(safePath, { recursive: true });
return true;
} catch (error) {
logger.error('Error ensuring directory exists', { dirPath, error });
return false;
}
}
/**
* Get information about a file
* @param filePath File path
* @returns File information or null if the file doesn't exist
*/
export function getFileInfo(filePath: string): {
name: string;
size: number;
extension: string;
mimeType: string;
lastModified: Date
} | null {
try {
const safePath = sanitizePath(filePath);
if (!safePath || !fileExists(filePath)) {
return null;
}
const stats = fs.statSync(safePath);
const ext = path.extname(filePath).toLowerCase();
// Simple mime type mapping
const mimeTypes: Record<string, string> = {
'.txt': 'text/plain',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.xml': 'application/xml',
'.pdf': 'application/pdf',
'.zip': 'application/zip',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.xls': 'application/vnd.ms-excel',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.ppt': 'application/vnd.ms-powerpoint',
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
};
return {
name: path.basename(filePath),
size: stats.size,
extension: ext,
mimeType: mimeTypes[ext] || 'application/octet-stream',
lastModified: stats.mtime
};
} catch (error) {
logger.error('Error getting file info', { filePath, error });
return null;
}
}
/**
* Read a file as a Buffer
* @param filePath File path
* @returns File contents as Buffer or null if an error occurs
*/
export function readFileAsBuffer(filePath: string): Buffer | null {
try {
const safePath = sanitizePath(filePath);
if (!safePath) {
logger.error('Invalid file path', { filePath });
return null;
}
if (!fileExists(filePath)) {
logger.error('File does not exist', { filePath });
return null;
}
// Check file size before reading
const stats = fs.statSync(safePath);
if (stats.size > MAX_FILE_SIZE) {
logger.error('File too large', {
filePath,
size: stats.size,
maxSize: MAX_FILE_SIZE
});
return null;
}
return fs.readFileSync(safePath);
} catch (error) {
logger.error('Error reading file', { filePath, error });
return null;
}
}
/**
* Write data to a file
* @param filePath File path to write to
* @param data Data to write
* @returns True if the file was written successfully
*/
export function writeFile(filePath: string, data: Buffer | string): boolean {
try {
const safePath = sanitizePath(filePath);
if (!safePath) {
logger.error('Invalid file path', { filePath });
return false;
}
const dirPath = path.dirname(safePath);
const safeDirPath = sanitizePath(dirPath);
if (!safeDirPath || !ensureDirectoryExists(safeDirPath)) {
logger.error('Could not create directory for file', { dirPath: safeDirPath });
return false;
}
// Check data size limit
const dataSize = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
if (dataSize > MAX_FILE_SIZE) {
logger.error('Data too large to write', {
filePath,
size: dataSize,
maxSize: MAX_FILE_SIZE
});
return false;
}
fs.writeFileSync(safePath, data);
return true;
} catch (error) {
logger.error('Error writing file', { filePath, error });
return false;
}
}