205 lines
5.5 KiB
JavaScript
205 lines
5.5 KiB
JavaScript
const fs = require('fs-extra');
|
|
const path = require('node:path');
|
|
const crypto = require('node:crypto');
|
|
|
|
/**
|
|
* File operations utility class
|
|
*/
|
|
class FileOps {
|
|
/**
|
|
* Copy a directory recursively
|
|
* @param {string} source - Source directory
|
|
* @param {string} dest - Destination directory
|
|
* @param {Object} options - Copy options
|
|
*/
|
|
async copyDirectory(source, dest, options = {}) {
|
|
const defaultOptions = {
|
|
overwrite: true,
|
|
errorOnExist: false,
|
|
filter: (src) => !this.shouldIgnore(src),
|
|
};
|
|
|
|
const copyOptions = { ...defaultOptions, ...options };
|
|
await fs.copy(source, dest, copyOptions);
|
|
}
|
|
|
|
/**
|
|
* Sync directory (selective copy preserving modifications)
|
|
* @param {string} source - Source directory
|
|
* @param {string} dest - Destination directory
|
|
*/
|
|
async syncDirectory(source, dest) {
|
|
const sourceFiles = await this.getFileList(source);
|
|
|
|
for (const file of sourceFiles) {
|
|
const sourceFile = path.join(source, file);
|
|
const destFile = path.join(dest, file);
|
|
|
|
// Check if destination file exists
|
|
if (await fs.pathExists(destFile)) {
|
|
// Compare checksums to see if file has been modified
|
|
const sourceHash = await this.getFileHash(sourceFile);
|
|
const destHash = await this.getFileHash(destFile);
|
|
|
|
if (sourceHash === destHash) {
|
|
// Files are identical, safe to update
|
|
await fs.copy(sourceFile, destFile, { overwrite: true });
|
|
} else {
|
|
// File has been modified, check timestamps
|
|
const sourceStats = await fs.stat(sourceFile);
|
|
const destStats = await fs.stat(destFile);
|
|
|
|
if (sourceStats.mtime > destStats.mtime) {
|
|
// Source is newer, update
|
|
await fs.copy(sourceFile, destFile, { overwrite: true });
|
|
}
|
|
// Otherwise, preserve user modifications
|
|
}
|
|
} else {
|
|
// New file, copy it
|
|
await fs.ensureDir(path.dirname(destFile));
|
|
await fs.copy(sourceFile, destFile);
|
|
}
|
|
}
|
|
|
|
// Remove files that no longer exist in source
|
|
const destFiles = await this.getFileList(dest);
|
|
for (const file of destFiles) {
|
|
const sourceFile = path.join(source, file);
|
|
const destFile = path.join(dest, file);
|
|
|
|
if (!(await fs.pathExists(sourceFile))) {
|
|
await fs.remove(destFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of all files in a directory
|
|
* @param {string} dir - Directory path
|
|
* @returns {Array} List of relative file paths
|
|
*/
|
|
async getFileList(dir) {
|
|
const files = [];
|
|
|
|
if (!(await fs.pathExists(dir))) {
|
|
return files;
|
|
}
|
|
|
|
const walk = async (currentDir, baseDir) => {
|
|
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(currentDir, entry.name);
|
|
|
|
if (entry.isDirectory() && !this.shouldIgnore(fullPath)) {
|
|
await walk(fullPath, baseDir);
|
|
} else if (entry.isFile() && !this.shouldIgnore(fullPath)) {
|
|
files.push(path.relative(baseDir, fullPath));
|
|
}
|
|
}
|
|
};
|
|
|
|
await walk(dir, dir);
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Get file hash for comparison
|
|
* @param {string} filePath - File path
|
|
* @returns {string} File hash
|
|
*/
|
|
async getFileHash(filePath) {
|
|
const hash = crypto.createHash('sha256');
|
|
const stream = fs.createReadStream(filePath);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
stream.on('data', (data) => hash.update(data));
|
|
stream.on('end', () => resolve(hash.digest('hex')));
|
|
stream.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if a path should be ignored
|
|
* @param {string} filePath - Path to check
|
|
* @returns {boolean} True if should be ignored
|
|
*/
|
|
shouldIgnore(filePath) {
|
|
const ignoredPatterns = ['.git', '.DS_Store', 'node_modules', '*.swp', '*.tmp', '.idea', '.vscode', '__pycache__', '*.pyc'];
|
|
|
|
const basename = path.basename(filePath);
|
|
|
|
for (const pattern of ignoredPatterns) {
|
|
if (pattern.includes('*')) {
|
|
// Simple glob pattern matching
|
|
const regex = new RegExp(pattern.replace('*', '.*'));
|
|
if (regex.test(basename)) {
|
|
return true;
|
|
}
|
|
} else if (basename === pattern) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Ensure directory exists
|
|
* @param {string} dir - Directory path
|
|
*/
|
|
async ensureDir(dir) {
|
|
await fs.ensureDir(dir);
|
|
}
|
|
|
|
/**
|
|
* Remove directory or file
|
|
* @param {string} targetPath - Path to remove
|
|
*/
|
|
async remove(targetPath) {
|
|
if (await fs.pathExists(targetPath)) {
|
|
await fs.remove(targetPath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read file content
|
|
* @param {string} filePath - File path
|
|
* @returns {string} File content
|
|
*/
|
|
async readFile(filePath) {
|
|
return await fs.readFile(filePath, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Write file content
|
|
* @param {string} filePath - File path
|
|
* @param {string} content - File content
|
|
*/
|
|
async writeFile(filePath, content) {
|
|
await fs.ensureDir(path.dirname(filePath));
|
|
await fs.writeFile(filePath, content, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Check if path exists
|
|
* @param {string} targetPath - Path to check
|
|
* @returns {boolean} True if exists
|
|
*/
|
|
async exists(targetPath) {
|
|
return await fs.pathExists(targetPath);
|
|
}
|
|
|
|
/**
|
|
* Get file or directory stats
|
|
* @param {string} targetPath - Path to check
|
|
* @returns {Object} File stats
|
|
*/
|
|
async stat(targetPath) {
|
|
return await fs.stat(targetPath);
|
|
}
|
|
}
|
|
|
|
module.exports = { FileOps };
|