feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
This commit is contained in:
212
ts/classes/iso-cache.ts
Normal file
212
ts/classes/iso-cache.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user