/** * ISO Cache Manager * Manages cached Ubuntu ISOs with multi-version support */ import { log } from '../logging.ts'; import { path } from '../plugins.ts'; import { getCacheDir, ensureDir } from '../paths.ts'; import { IsoDownloader } from './iso-downloader.ts'; export interface ICacheEntry { version: string; architecture: 'amd64' | 'arm64'; filename: string; path: string; size: number; downloadedAt: Date; } export class IsoCache { private cacheDir: string; private metadataPath: string; constructor() { this.cacheDir = getCacheDir(); this.metadataPath = path.join(this.cacheDir, 'metadata.json'); } /** * Initialize the cache directory */ async init(): Promise { await ensureDir(this.cacheDir); } /** * Get all cache entries */ async list(): Promise { try { const metadata = await Deno.readTextFile(this.metadataPath); return JSON.parse(metadata); } catch (err) { if (err instanceof Deno.errors.NotFound) { return []; } throw err; } } /** * Get a specific cache entry */ async get(version: string, architecture: 'amd64' | 'arm64'): Promise { const entries = await this.list(); return entries.find((e) => e.version === version && e.architecture === architecture) || null; } /** * Check if an ISO is cached */ async has(version: string, architecture: 'amd64' | 'arm64'): Promise { const entry = await this.get(version, architecture); if (!entry) return false; // Verify the file still exists try { const stat = await Deno.stat(entry.path); return stat.isFile; } catch { // File doesn't exist, remove from metadata await this.remove(version, architecture); return false; } } /** * Add an ISO to the cache */ async add( version: string, architecture: 'amd64' | 'arm64', sourcePath: string, ): Promise { await this.init(); const filename = IsoDownloader.getIsoFilename(version, architecture); const destPath = path.join(this.cacheDir, filename); // Copy file to cache await Deno.copyFile(sourcePath, destPath); // Get file size const stat = await Deno.stat(destPath); const entry: ICacheEntry = { version, architecture, filename, path: destPath, size: stat.size, downloadedAt: new Date(), }; // Update metadata const entries = await this.list(); const existingIndex = entries.findIndex( (e) => e.version === version && e.architecture === architecture, ); if (existingIndex >= 0) { entries[existingIndex] = entry; } else { entries.push(entry); } await this.saveMetadata(entries); log.success(`ISO cached: ${version} ${architecture}`); return entry; } /** * Remove an ISO from the cache */ async remove(version: string, architecture: 'amd64' | 'arm64'): Promise { const entry = await this.get(version, architecture); if (!entry) { log.warn(`ISO not found in cache: ${version} ${architecture}`); return; } // Remove file try { await Deno.remove(entry.path); } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } // Update metadata const entries = await this.list(); const filtered = entries.filter( (e) => !(e.version === version && e.architecture === architecture), ); await this.saveMetadata(filtered); log.success(`ISO removed from cache: ${version} ${architecture}`); } /** * Clean old cached ISOs */ async clean(olderThanDays?: number): Promise { const entries = await this.list(); const now = new Date(); for (const entry of entries) { const age = now.getTime() - new Date(entry.downloadedAt).getTime(); const ageDays = age / (1000 * 60 * 60 * 24); if (!olderThanDays || ageDays > olderThanDays) { await this.remove(entry.version, entry.architecture); } } } /** * Get the path to a cached ISO */ async getPath(version: string, architecture: 'amd64' | 'arm64'): Promise { const entry = await this.get(version, architecture); return entry?.path || null; } /** * Download and cache an ISO */ async downloadAndCache( version: string, architecture: 'amd64' | 'arm64', onProgress?: (downloaded: number, total: number) => void, ): Promise { await this.init(); const filename = IsoDownloader.getIsoFilename(version, architecture); const destPath = path.join(this.cacheDir, filename); const downloader = new IsoDownloader(); await downloader.downloadWithVerification({ ubuntuVersion: version, architecture, outputPath: destPath, onProgress, }); // Add to cache metadata await this.add(version, architecture, destPath); return destPath; } /** * Save metadata to disk */ private async saveMetadata(entries: ICacheEntry[]): Promise { await ensureDir(this.cacheDir); await Deno.writeTextFile(this.metadataPath, JSON.stringify(entries, null, 2)); } }