173 lines
4.8 KiB
TypeScript
173 lines
4.8 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
}
|