Files
cli/ts/mod_format/classes.changecache.ts

229 lines
6.5 KiB
TypeScript

import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
export interface IFileCache {
path: string;
checksum: string;
modified: number;
size: number;
}
export interface ICacheManifest {
version: string;
lastFormat: number;
files: IFileCache[];
}
export class ChangeCache {
private cacheDir: string;
private manifestPath: string;
private cacheVersion = '1.0.0';
constructor() {
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
}
async initialize(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.cacheDir);
}
async getManifest(): Promise<ICacheManifest> {
const defaultManifest: ICacheManifest = {
version: this.cacheVersion,
lastFormat: 0,
files: []
};
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) {
return defaultManifest;
}
try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn('Invalid manifest structure, returning default manifest');
return defaultManifest;
}
} catch (error) {
console.warn(`Failed to read cache manifest: ${error.message}, returning default manifest`);
// Try to delete the corrupted file
try {
await plugins.smartfile.fs.remove(this.manifestPath);
} catch (removeError) {
// Ignore removal errors
}
return defaultManifest;
}
}
async saveManifest(manifest: ICacheManifest): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid manifest structure, cannot save');
}
// Use atomic write: write to temp file, then move it
const tempPath = `${this.manifestPath}.tmp`;
try {
// Write to temporary file
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, tempPath);
// Move temp file to actual manifest (atomic-like operation)
// Since smartfile doesn't have rename, we copy and delete
await plugins.smartfile.fs.copy(tempPath, this.manifestPath);
await plugins.smartfile.fs.remove(tempPath);
} catch (error) {
// Clean up temp file if it exists
try {
await plugins.smartfile.fs.remove(tempPath);
} catch (removeError) {
// Ignore removal errors
}
throw error;
}
}
async hasFileChanged(filePath: string): Promise<boolean> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) {
return true; // File doesn't exist, so it's "changed" (will be created)
}
// Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories
if (stats.isDirectory()) {
return false; // Directories are not processed
}
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const currentChecksum = this.calculateChecksum(content);
// Get cached info
const manifest = await this.getManifest();
const cachedFile = manifest.files.find(f => f.path === filePath);
if (!cachedFile) {
return true; // Not in cache, so it's changed
}
// Compare checksums
return cachedFile.checksum !== currentChecksum ||
cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtimeMs;
}
async updateFileCache(filePath: string): Promise<void> {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(paths.cwd, filePath);
// Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories
if (stats.isDirectory()) {
return; // Don't cache directories
}
const content = plugins.smartfile.fs.toStringSync(absolutePath);
const checksum = this.calculateChecksum(content);
// Update manifest
const manifest = await this.getManifest();
const existingIndex = manifest.files.findIndex(f => f.path === filePath);
const cacheEntry: IFileCache = {
path: filePath,
checksum,
modified: stats.mtimeMs,
size: stats.size
};
if (existingIndex !== -1) {
manifest.files[existingIndex] = cacheEntry;
} else {
manifest.files.push(cacheEntry);
}
manifest.lastFormat = Date.now();
await this.saveManifest(manifest);
}
async getChangedFiles(filePaths: string[]): Promise<string[]> {
const changedFiles: string[] = [];
for (const filePath of filePaths) {
if (await this.hasFileChanged(filePath)) {
changedFiles.push(filePath);
}
}
return changedFiles;
}
async clean(): Promise<void> {
const manifest = await this.getManifest();
const validFiles: IFileCache[] = [];
// Remove entries for files that no longer exist
for (const file of manifest.files) {
const absolutePath = plugins.path.isAbsolute(file.path)
? file.path
: plugins.path.join(paths.cwd, file.path);
if (await plugins.smartfile.fs.fileExists(absolutePath)) {
validFiles.push(file);
}
}
manifest.files = validFiles;
await this.saveManifest(manifest);
}
private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex');
}
private isValidManifest(manifest: any): manifest is ICacheManifest {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (typeof manifest.version !== 'string' ||
typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)) {
return false;
}
// Check each file entry
for (const file of manifest.files) {
if (!file || typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number') {
return false;
}
}
return true;
}
}