From e8d9406ba1942a46cd948e9adfe5dc832c47abcd Mon Sep 17 00:00:00 2001 From: Ken Eucker Date: Fri, 14 Aug 2026 14:17:13 -0700 Subject: [PATCH] fix(kiwix): skip the ZIM download when storage already has one The Kiwix pre-install requirement is "at least one ZIM file present", but the check that enforces it sits behind a HEAD request in the downloader. On a host with no connectivity that request fails, so installing Kiwix fails even when a ZIM is already sitting in storage. Check the storage directory first and skip the download when it already holds a .zim, rebuilding the library XML from disk instead. This mirrors how Calibre-Web seeds its bundled library. Online installs are unaffected: with no ZIM present the existing download path still runs. Co-Authored-By: Claude Opus 5 --- admin/app/services/docker_service.ts | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index bba8532..61bcec3 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -21,7 +21,7 @@ import { KiwixLibraryService } from './kiwix_library_service.js' import { SERVICE_NAMES } from '../../constants/service_names.js' import { exec } from 'child_process' import { promisify } from 'util' -import { readFile, mkdir, copyFile, chown, chmod, access, writeFile } from 'node:fs/promises' +import { readFile, mkdir, copyFile, chown, chmod, access, writeFile, readdir } from 'node:fs/promises' import { randomBytes } from 'node:crypto' import KVStore from '#models/kv_store' import { BROADCAST_CHANNELS } from '../../constants/broadcast.js' @@ -949,6 +949,21 @@ export class DockerService { } } + /** + * ZIM files already sitting in storage, e.g. seeded from an offline artifact + * bundle or left by a previous install. Never throws — an unreadable or + * missing directory simply means "none present". + */ + private async _listExistingZimFiles(): Promise { + try { + const zimDir = join(process.cwd(), ZIM_STORAGE_PATH) + const entries = await readdir(zimDir) + return entries.filter((entry) => entry.toLowerCase().endsWith('.zim')) + } catch { + return [] + } + } + private async _runPreinstallActions__KiwixServe(): Promise { /** * At least one .zim file must be available before we can start the kiwix container. @@ -958,13 +973,36 @@ export class DockerService { 'https://github.com/Crosstalk-Solutions/project-nomad/raw/refs/heads/main/install/wikipedia_en_100_mini_2026-01.zim' const filename = 'wikipedia_en_100_mini_2026-01.zim' const filepath = join(process.cwd(), ZIM_STORAGE_PATH, filename) - logger.info(`[DockerService] Kiwix Serve pre-install: Downloading ZIM file to ${filepath}`) this._broadcast( SERVICE_NAMES.KIWIX, 'preinstall', `Running pre-install actions for Kiwix Serve...` ) + + // The requirement is "at least one ZIM present", so if storage already has + // one there is nothing to fetch. Checking here matters for offline installs: + // the downloader's own idempotency check sits behind a HEAD request, so + // without this an air-gapped host fails even with the ZIM already in place. + // Mirrors how Calibre-Web seeds its bundled library below. + const existingZims = await this._listExistingZimFiles() + if (existingZims.length > 0) { + logger.info( + `[DockerService] Kiwix Serve pre-install: ${existingZims.length} ZIM file(s) already present, skipping download` + ) + this._broadcast( + SERVICE_NAMES.KIWIX, + 'preinstall', + `Found ${existingZims.length} existing ZIM file(s); skipping download.` + ) + + const kiwixLibraryService = new KiwixLibraryService() + await kiwixLibraryService.rebuildFromDisk() + this._broadcast(SERVICE_NAMES.KIWIX, 'preinstall', 'Generated kiwix library XML.') + return + } + + logger.info(`[DockerService] Kiwix Serve pre-install: Downloading ZIM file to ${filepath}`) this._broadcast( SERVICE_NAMES.KIWIX, 'preinstall',