Files
tsdocker/ts/classes.tsdockercache.ts

109 lines
3.3 KiB
TypeScript

import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as plugins from './tsdocker.plugins.js';
import * as paths from './tsdocker.paths.js';
import { logger } from './tsdocker.logging.js';
import type { ICacheData, ICacheEntry } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
/**
* Manages content-hash-based build caching for Dockerfiles.
* Cache is stored in .nogit/tsdocker_support.json.
*/
export class TsDockerCache {
private cacheFilePath: string;
private data: ICacheData;
constructor() {
this.cacheFilePath = path.join(paths.cwd, '.nogit', 'tsdocker_support.json');
this.data = { version: 1, entries: {} };
}
/**
* Loads cache data from disk. Falls back to empty cache on missing/corrupt file.
*/
public load(): void {
try {
const raw = fs.readFileSync(this.cacheFilePath, 'utf-8');
const parsed = JSON.parse(raw);
if (parsed && parsed.version === 1 && parsed.entries) {
this.data = parsed;
} else {
logger.log('warn', '[cache] Cache file has unexpected format, starting fresh');
this.data = { version: 1, entries: {} };
}
} catch {
// Missing or corrupt file — start fresh
this.data = { version: 1, entries: {} };
}
}
/**
* Saves cache data to disk. Creates .nogit directory if needed.
*/
public save(): void {
const dir = path.dirname(this.cacheFilePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.data, null, 2), 'utf-8');
}
/**
* Computes SHA-256 hash of Dockerfile content.
*/
public computeContentHash(content: string): string {
return crypto.createHash('sha256').update(content).digest('hex');
}
/**
* Checks whether a build can be skipped for the given Dockerfile.
* Logs detailed diagnostics and returns true if the build should be skipped.
*/
public async shouldSkipBuild(cleanTag: string, content: string): Promise<boolean> {
const contentHash = this.computeContentHash(content);
const entry = this.data.entries[cleanTag];
if (!entry) {
logger.log('info', `[cache] ${cleanTag}: no cached entry, will build`);
return false;
}
const hashMatch = entry.contentHash === contentHash;
logger.log('info', `[cache] ${cleanTag}: hash ${hashMatch ? 'matches' : 'changed'}`);
if (!hashMatch) {
logger.log('info', `[cache] ${cleanTag}: content changed, will build`);
return false;
}
// Hash matches — verify the image still exists locally
const inspectResult = await smartshellInstance.exec(
`docker image inspect ${entry.imageId} > /dev/null 2>&1`
);
const available = inspectResult.exitCode === 0;
if (available) {
logger.log('info', `[cache] ${cleanTag}: cache hit, skipping build`);
return true;
}
logger.log('info', `[cache] ${cleanTag}: image no longer available, will build`);
return false;
}
/**
* Records a successful build in the cache.
*/
public recordBuild(cleanTag: string, content: string, imageId: string, buildTag: string): void {
this.data.entries[cleanTag] = {
contentHash: this.computeContentHash(content),
imageId,
buildTag,
timestamp: Date.now(),
};
}
}