213 lines
5.1 KiB
TypeScript
213 lines
5.1 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<void> {
|
||
|
|
await ensureDir(this.cacheDir);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all cache entries
|
||
|
|
*/
|
||
|
|
async list(): Promise<ICacheEntry[]> {
|
||
|
|
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<ICacheEntry | null> {
|
||
|
|
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<boolean> {
|
||
|
|
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<ICacheEntry> {
|
||
|
|
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<void> {
|
||
|
|
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<void> {
|
||
|
|
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<string | null> {
|
||
|
|
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<string> {
|
||
|
|
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<void> {
|
||
|
|
await ensureDir(this.cacheDir);
|
||
|
|
await Deno.writeTextFile(this.metadataPath, JSON.stringify(entries, null, 2));
|
||
|
|
}
|
||
|
|
}
|