import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import https from 'node:https'; import {fileURLToPath} from 'node:url'; import AdmZip from 'adm-zip'; import * as tar from 'tar'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const VERSION = '3.8.1'; const BASE_URL = 'https://download.kiwix.org/release/kiwix-tools'; const MAX_REDIRECTS = 5; const BIN_DIR = path.join(__dirname, '..', 'bin'); const PLATFORM_MAP = { linux: {name: 'linux', ext: 'tar.gz'}, darwin: {name: 'macos', ext: 'tar.gz'}, win32: {name: 'win', ext: 'zip'} }; const ARCH_MAP = { linux: { x64: 'x86_64', arm64: 'aarch64', arm: 'armhf', ia32: 'i586' }, darwin: { x64: 'x86_64', arm64: 'arm64' }, win32: { x64: 'x86_64', ia32: 'i686' } }; function download(url, dest, redirectsLeft = MAX_REDIRECTS) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); const agent = new https.Agent({maxFreeSockets: 20, maxTotalSockets: 50}); const cleanupAndReject = err => { file.close(); fs.unlink(dest, () => reject(err)); }; const req = https.get(url, {agent}, res => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); // discard body file.close(); if (redirectsLeft <= 0) { reject(new Error(`Too many redirects while downloading ${url}`)); return; } // Location can be relative, so resolve it against the current URL. const nextUrl = new URL(res.headers.location, url).toString(); resolve(download(nextUrl, dest, redirectsLeft - 1)); return; } if (!res.statusCode || res.statusCode >= 400) { res.resume(); cleanupAndReject(new Error(`HTTP ${res.statusCode} - ${res.statusMessage}`)); return; } res.pipe(file); file.on('finish', () => { file.close(resolve); }); file.on('error', cleanupAndReject); }); req.on('error', cleanupAndReject); }); } async function flattenExtractedDir(srcDir, destDir) { let entries = await fs.promises.readdir(srcDir, {withFileTypes: true}); let sourceDir = srcDir; if (entries.length === 1 && entries[0].isDirectory()) { sourceDir = path.join(srcDir, entries[0].name); entries = await fs.promises.readdir(sourceDir, {withFileTypes: true}); } for (const entry of entries) { const src = path.join(sourceDir, entry.name); const dest = path.join(destDir, entry.name); await fs.promises.rename(src, dest); } } async function extractTarGz(archivePath, destDir) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiwix-extract-')); await tar.extract({ file: archivePath, cwd: tmpDir }); await flattenExtractedDir(tmpDir, destDir); await fs.promises.rm(tmpDir, {recursive: true, force: true}); } async function extractZip(archivePath, destDir) { const zip = new AdmZip(archivePath); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiwix-extract-')); await new Promise((resolve, reject) => { zip.extractAllToAsync(tmpDir, true, false, err => { if (err) reject(err); else resolve(); }); }); await flattenExtractedDir(tmpDir, destDir); await fs.promises.rm(tmpDir, {recursive: true, force: true}); } function resolveArchiveName() { const platformInfo = PLATFORM_MAP[process.platform]; if (!platformInfo) { throw new Error(`Unsupported platform: ${process.platform}`); } const archName = ARCH_MAP[process.platform]?.[process.arch]; if (!archName) { const supported = Object.keys(ARCH_MAP[process.platform] || {}).join(', '); throw new Error( `Unsupported architecture "${process.arch}" for platform "${process.platform}". ` + `Supported architectures: ${supported}` ); } return { archiveName: `kiwix-tools_${platformInfo.name}-${archName}-${VERSION}.${platformInfo.ext}`, ext: platformInfo.ext }; } async function install() { const {archiveName, ext} = resolveArchiveName(); const url = `${BASE_URL}/${archiveName}`; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiwix-install-')); const archivePath = path.join(tmpDir, archiveName); console.log(`Platform: ${process.platform}/${process.arch}`); console.log(`Downloading kiwix-tools: ${url}`); await download(url, archivePath); console.log('Download complete!'); console.log('Extracting...'); await fs.promises.mkdir(BIN_DIR, {recursive: true}); if (ext === 'zip') { await extractZip(archivePath, BIN_DIR); } else { await extractTarGz(archivePath, BIN_DIR); } for (const entry of await fs.promises.readdir(BIN_DIR)) { if (entry.startsWith('kiwix-')) { const fullPath = path.join(BIN_DIR, entry); const stat = await fs.promises.stat(fullPath); if (stat.isFile()) { await fs.promises.chmod(fullPath, 0o755); } } } console.log(`Installed to: ${BIN_DIR}`); await fs.promises.rm(tmpDir, {recursive: true, force: true}); } install().catch(err => { console.error('Install failed:', err.message); process.exit(1); });