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 <noreply@anthropic.com>
This commit is contained in:
Ken Eucker 2026-08-14 14:17:13 -07:00
parent 40ee026938
commit e8d9406ba1
1 changed files with 40 additions and 2 deletions

View File

@ -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<string[]> {
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<void> {
/**
* 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',