feat(core): Initial project scaffold and implementation: Deno CLI, ISO tooling, cloud-init generation, packaging and installer scripts
This commit is contained in:
172
ts/classes/iso-downloader.ts
Normal file
172
ts/classes/iso-downloader.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* ISO Downloader
|
||||
* Downloads Ubuntu Server ISOs from official mirrors
|
||||
*/
|
||||
|
||||
import { log } from '../logging.ts';
|
||||
import { path } from '../plugins.ts';
|
||||
|
||||
export interface IDownloadOptions {
|
||||
ubuntuVersion: string; // e.g., "24.04", "22.04"
|
||||
architecture: 'amd64' | 'arm64';
|
||||
outputPath: string;
|
||||
onProgress?: (downloaded: number, total: number) => void;
|
||||
}
|
||||
|
||||
export class IsoDownloader {
|
||||
/**
|
||||
* Ubuntu release URLs
|
||||
*/
|
||||
private static readonly UBUNTU_MIRROR = 'https://releases.ubuntu.com';
|
||||
|
||||
/**
|
||||
* Get the ISO filename for a given version and architecture
|
||||
*/
|
||||
static getIsoFilename(version: string, arch: 'amd64' | 'arm64'): string {
|
||||
// Ubuntu Server ISO naming pattern
|
||||
if (arch === 'amd64') {
|
||||
return `ubuntu-${version}-live-server-${arch}.iso`;
|
||||
} else {
|
||||
// ARM64 uses preinstalled server images
|
||||
return `ubuntu-${version}-preinstalled-server-${arch}.img.xz`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download URL for a given Ubuntu version and architecture
|
||||
*/
|
||||
static getDownloadUrl(version: string, arch: 'amd64' | 'arm64'): string {
|
||||
const filename = this.getIsoFilename(version, arch);
|
||||
return `${this.UBUNTU_MIRROR}/${version}/${filename}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checksum URL for verification
|
||||
*/
|
||||
static getChecksumUrl(version: string): string {
|
||||
return `${this.UBUNTU_MIRROR}/${version}/SHA256SUMS`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an Ubuntu ISO
|
||||
*/
|
||||
async download(options: IDownloadOptions): Promise<void> {
|
||||
const { ubuntuVersion, architecture, outputPath, onProgress } = options;
|
||||
const url = IsoDownloader.getDownloadUrl(ubuntuVersion, architecture);
|
||||
|
||||
log.info(`Downloading Ubuntu ${ubuntuVersion} ${architecture} from ${url}`);
|
||||
|
||||
// Download the ISO
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ISO: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
|
||||
let downloadedSize = 0;
|
||||
|
||||
// Open file for writing
|
||||
const file = await Deno.open(outputPath, { write: true, create: true, truncate: true });
|
||||
|
||||
try {
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
throw new Error('Failed to get response body reader');
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
await file.write(value);
|
||||
downloadedSize += value.length;
|
||||
|
||||
if (onProgress && totalSize > 0) {
|
||||
onProgress(downloadedSize, totalSize);
|
||||
}
|
||||
}
|
||||
|
||||
log.success(`ISO downloaded successfully to ${outputPath}`);
|
||||
} finally {
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and verify checksum
|
||||
*/
|
||||
async downloadWithVerification(options: IDownloadOptions): Promise<void> {
|
||||
const { ubuntuVersion, architecture, outputPath } = options;
|
||||
|
||||
// Download the ISO
|
||||
await this.download(options);
|
||||
|
||||
// Download checksums
|
||||
log.info('Downloading checksums for verification...');
|
||||
const checksumUrl = IsoDownloader.getChecksumUrl(ubuntuVersion);
|
||||
const checksumResponse = await fetch(checksumUrl);
|
||||
|
||||
if (!checksumResponse.ok) {
|
||||
log.warn('Could not download checksums for verification');
|
||||
return;
|
||||
}
|
||||
|
||||
const checksumText = await checksumResponse.text();
|
||||
const filename = path.basename(outputPath);
|
||||
|
||||
// Find the checksum for our file
|
||||
const lines = checksumText.split('\n');
|
||||
let expectedChecksum: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(filename)) {
|
||||
expectedChecksum = line.split(/\s+/)[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!expectedChecksum) {
|
||||
log.warn(`No checksum found for ${filename}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the checksum
|
||||
log.info('Verifying checksum...');
|
||||
const actualChecksum = await this.calculateSha256(outputPath);
|
||||
|
||||
if (actualChecksum === expectedChecksum) {
|
||||
log.success('Checksum verified successfully ✓');
|
||||
} else {
|
||||
throw new Error(`Checksum mismatch!\nExpected: ${expectedChecksum}\nActual: ${actualChecksum}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate SHA256 checksum of a file
|
||||
*/
|
||||
private async calculateSha256(filePath: string): Promise<string> {
|
||||
const file = await Deno.open(filePath, { read: true });
|
||||
const buffer = new Uint8Array(8192);
|
||||
const hasher = await crypto.subtle.digest('SHA-256', new Uint8Array(0)); // Initialize
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const bytesRead = await file.read(buffer);
|
||||
if (bytesRead === null) break;
|
||||
|
||||
// Hash the chunk
|
||||
const chunk = buffer.slice(0, bytesRead);
|
||||
await crypto.subtle.digest('SHA-256', chunk);
|
||||
}
|
||||
} finally {
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Convert to hex string
|
||||
const hashArray = Array.from(new Uint8Array(hasher));
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
return hashHex;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user