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

224 lines
6.0 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');
}
// Ensure directory exists
await plugins.smartfile.fs.ensureDir(this.cacheDir);
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
}
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;
}
}