pgit-cli
Version:
Private file tracking with dual git repositories
467 lines • 17.3 kB
JavaScript
;
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