UNPKG

pgit-cli

Version:

Private file tracking with dual git repositories

467 lines 17.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.FileSystemService = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); const platform_detector_1 = require("../utils/platform.detector"); const filesystem_error_1 = require("../errors/filesystem.error"); /** * Atomic file system operations with rollback capability */ class FileSystemService { constructor() { this.rollbackActions = []; } /** * Move file or directory atomically with rollback support */ async moveFileAtomic(source, target) { await this.validatePath(source); await this.validateTargetPath(target); const backupPath = await this.createBackup(source); this.addRollbackAction(async () => { if (await fs.pathExists(backupPath)) { // Remove existing source first if it exists during rollback if (await fs.pathExists(source)) { await fs.remove(source); } await fs.move(backupPath, source); } }); try { await fs.ensureDir(path.dirname(target)); // Use a more robust move operation to handle fs-extra quirks let moveSuccessful = false; let retryCount = 0; const maxRetries = 3; while (!moveSuccessful && retryCount < maxRetries) { try { // Remove target file if it exists to avoid 'dest already exists' error if (await fs.pathExists(target)) { await fs.remove(target); // Add a small delay to ensure file is fully removed await new Promise(resolve => setTimeout(resolve, 10)); } await fs.move(source, target); moveSuccessful = true; } catch (moveError) { retryCount++; if (retryCount >= maxRetries) { throw moveError; } // Wait a bit before retrying await new Promise(resolve => setTimeout(resolve, 50 * retryCount)); } } // Clean up backup on success if (await fs.pathExists(backupPath)) { await fs.remove(backupPath); } } catch (error) { await this.rollback(); throw new filesystem_error_1.AtomicOperationError(`Failed to move ${source} to ${target}`, error instanceof Error ? error.message : String(error)); } } /** * Copy file or directory atomically */ async copyFileAtomic(source, target) { await this.validatePath(source); await this.validateTargetPath(target); try { await fs.ensureDir(path.dirname(target)); await fs.copy(source, target); } catch (error) { throw new filesystem_error_1.AtomicOperationError(`Failed to copy ${source} to ${target}`, error instanceof Error ? error.message : String(error)); } } /** * Create directory with proper permissions */ async createDirectory(dirPath) { this.validatePathString(dirPath); try { await fs.ensureDir(dirPath); // Set appropriate permissions (readable/writable by owner only) if (platform_detector_1.PlatformDetector.isUnix()) { await fs.chmod(dirPath, 0o700); } } catch (error) { throw new filesystem_error_1.FileSystemError(`Failed to create directory ${dirPath}`, error instanceof Error ? error.message : String(error)); } } /** * Ensure directory exists (alias for createDirectory) */ async ensureDirectoryExists(dirPath) { return this.createDirectory(dirPath); } /** * Write file atomically with backup */ async writeFileAtomic(filePath, content) { this.validatePathString(filePath); const tempPath = `${filePath}.tmp.${Date.now()}`; const backupPath = await this.createBackupIfExists(filePath); if (backupPath) { this.addRollbackAction(async () => { if (await fs.pathExists(backupPath)) { // Remove existing file first if it exists during rollback if (await fs.pathExists(filePath)) { await fs.remove(filePath); } await fs.move(backupPath, filePath); } }); } try { await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(tempPath, content, 'utf8'); // Remove target file if it exists to avoid 'dest already exists' error if (await fs.pathExists(filePath)) { await fs.remove(filePath); } // Use a more robust move operation to handle fs-extra quirks let moveSuccessful = false; let retryCount = 0; const maxRetries = 3; while (!moveSuccessful && retryCount < maxRetries) { try { // Remove target file if it exists to avoid 'dest already exists' error if (await fs.pathExists(filePath)) { await fs.remove(filePath); // Add a small delay to ensure file is fully removed await new Promise(resolve => setTimeout(resolve, 10)); } await fs.move(tempPath, filePath); moveSuccessful = true; } catch (moveError) { retryCount++; if (retryCount >= maxRetries) { throw moveError; } // Wait a bit before retrying await new Promise(resolve => setTimeout(resolve, 50 * retryCount)); } } // Set appropriate permissions if (platform_detector_1.PlatformDetector.isUnix()) { await fs.chmod(filePath, 0o600); } // Clean up backup on success if (backupPath && (await fs.pathExists(backupPath))) { await fs.remove(backupPath); } } catch (error) { // Clean up temp file if (await fs.pathExists(tempPath)) { await fs.remove(tempPath); } await this.rollback(); throw new filesystem_error_1.AtomicOperationError(`Failed to write file ${filePath}`, error instanceof Error ? error.message : String(error)); } } /** * Write file content (convenience method) */ async writeFile(filePath, content) { this.validatePathString(filePath); try { await fs.writeFile(filePath, content, 'utf8'); } catch (error) { throw new filesystem_error_1.FileSystemError(`Failed to write file ${filePath}`, error instanceof Error ? error.message : String(error)); } } /** * Read file safely */ async readFile(filePath) { await this.validatePath(filePath); try { return await fs.readFile(filePath, 'utf8'); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { throw new filesystem_error_1.FileNotFoundError(`File not found: ${filePath}`); } throw new filesystem_error_1.FileSystemError(`Failed to read file ${filePath}`, error instanceof Error ? error.message : String(error)); } } /** * Remove file or directory safely */ async remove(targetPath) { await this.validatePath(targetPath); const backupPath = await this.createBackup(targetPath); this.addRollbackAction(async () => { if (await fs.pathExists(backupPath)) { // Remove existing path first if it exists during rollback if (await fs.pathExists(targetPath)) { await fs.remove(targetPath); } await fs.move(backupPath, targetPath); } }); try { await fs.remove(targetPath); } catch (error) { await this.rollback(); throw new filesystem_error_1.FileSystemError(`Failed to remove ${targetPath}`, error instanceof Error ? error.message : String(error)); } } /** * Check if path exists */ async pathExists(targetPath) { this.validatePathString(targetPath); return fs.pathExists(targetPath); } /** * Get file/directory statistics */ async getStats(targetPath) { await this.validatePath(targetPath); try { return await fs.stat(targetPath); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { throw new filesystem_error_1.FileNotFoundError(`Path not found: ${targetPath}`); } throw new filesystem_error_1.FileSystemError(`Failed to get stats for ${targetPath}`, error instanceof Error ? error.message : String(error)); } } /** * Get link statistics (for symbolic links, returns stats of the link itself, not the target) */ async getLinkStats(targetPath) { this.validatePathString(targetPath); try { return await fs.lstat(targetPath); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { throw new filesystem_error_1.FileNotFoundError(`Path not found: ${targetPath}`); } throw new filesystem_error_1.FileSystemError(`Failed to get link stats for ${targetPath}`, error instanceof Error ? error.message : String(error)); } } /** * Check if path is a directory */ async isDirectory(targetPath) { try { const stats = await this.getStats(targetPath); return stats.isDirectory(); } catch (error) { if (error instanceof filesystem_error_1.FileNotFoundError) { return false; } throw error; } } /** * Check if path is a file */ async isFile(targetPath) { try { const stats = await this.getStats(targetPath); return stats.isFile(); } catch (error) { if (error instanceof filesystem_error_1.FileNotFoundError) { return false; } throw error; } } /** * Validate path for security and accessibility */ async validatePath(targetPath) { this.validatePathString(targetPath); // Check if path exists if (!(await fs.pathExists(targetPath))) { throw new filesystem_error_1.FileNotFoundError(`Path does not exist: ${targetPath}`); } // Check permissions const permissions = await platform_detector_1.PlatformDetector.checkPermissions(targetPath); if (!permissions.readable) { throw new filesystem_error_1.PermissionError(`Cannot read path: ${targetPath}`); } } /** * Validate path string for security issues */ validatePathString(targetPath) { if (!targetPath || typeof targetPath !== 'string') { throw new filesystem_error_1.InvalidPathError('Path must be a non-empty string'); } if (targetPath.includes('\x00')) { throw new filesystem_error_1.InvalidPathError('Path contains invalid characters'); } if (targetPath.length > 4096) { throw new filesystem_error_1.InvalidPathError('Path is too long'); } // Prevent path traversal attacks const normalizedPath = path.normalize(targetPath); if (normalizedPath.includes('..')) { throw new filesystem_error_1.InvalidPathError(`Path traversal detected: ${targetPath}`); } // Prevent access to system files const systemPaths = ['.git', 'node_modules', '.npm', '.cache']; const pathParts = normalizedPath.split(path.sep); for (const systemPath of systemPaths) { if (pathParts.includes(systemPath) && !pathParts.includes('.private-storage')) { throw new filesystem_error_1.InvalidPathError(`Access to system path not allowed: ${targetPath}`); } } } /** * Validate target path for write operations */ async validateTargetPath(targetPath) { this.validatePathString(targetPath); const targetDir = path.dirname(targetPath); // Check if parent directory exists and is writable if (await fs.pathExists(targetDir)) { const permissions = await platform_detector_1.PlatformDetector.checkPermissions(targetDir); if (!permissions.writable) { throw new filesystem_error_1.PermissionError(`Cannot write to directory: ${targetDir}`); } } } /** * Create backup of file/directory */ async createBackup(targetPath) { const backupPath = `${targetPath}.backup.${Date.now()}.${this.generateId()}`; if (await fs.pathExists(targetPath)) { await fs.copy(targetPath, backupPath); } return backupPath; } /** * Create backup only if file exists */ async createBackupIfExists(targetPath) { if (await fs.pathExists(targetPath)) { return this.createBackup(targetPath); } return null; } /** * Add rollback action */ addRollbackAction(action) { this.rollbackActions.push(action); } /** * Execute rollback actions in reverse order */ async rollback() { const actions = [...this.rollbackActions].reverse(); this.rollbackActions.length = 0; // Clear actions for (const action of actions) { try { await action(); } catch (error) { // Log rollback errors but don't throw to avoid masking original error console.error('Rollback action failed:', error); // Continue with other rollback actions even if one fails } } } /** * Generate unique identifier */ generateId() { return crypto.randomBytes(8).toString('hex'); } /** * Clear rollback actions (call after successful operation) */ clearRollbackActions() { this.rollbackActions.length = 0; } /** * Get safe file name for current platform */ static getSafeFileName(name) { // Remove or replace unsafe characters let safeName = name.replace(/[<>:"/\\|?*]/g, '_'); // Ensure it's not too long if (safeName.length > 255) { safeName = safeName.substring(0, 255); } // Ensure it's not empty if (!safeName.trim()) { safeName = 'unnamed'; } return safeName; } /** * Get relative path between two paths */ static getRelativePath(from, to) { return path.relative(from, to); } /** * Join paths in platform-appropriate way */ static joinPaths(...paths) { return path.join(...paths); } /** * Resolve path to absolute path */ static resolvePath(targetPath) { return path.resolve(targetPath); } } exports.FileSystemService = FileSystemService; //# sourceMappingURL=filesystem.service.js.map