fix(kiwix): self-heal a missing or corrupt library XML on startup
Kiwix runs in library mode reading kiwix-library.xml via --monitorLibrary. Today a missing or corrupt XML is only repaired on the download path (rebuildFromDisk after a completed download), so if the file is lost or truncated outside that flow — storage relocation, an interrupted write, manual deletion — Kiwix comes up serving an empty library with no path to recovery. Add KiwixLibraryService.ensureLibraryXmlHealthy(): reads the XML, and if it's missing (ENOENT) or fails to parse / lacks a <library> root, rebuilds it from the ZIM files on disk. A well-formed but empty library is treated as valid (no spurious rebuild), and filesystem errors other than ENOENT are surfaced rather than masked. The boot provider calls it on the already-in-library-mode path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
026ab6df8f
commit
1b9f4f30f2
|
|
@ -201,6 +201,56 @@ export class KiwixLibraryService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the library XML parses and has a <library> root. A truncated or
|
||||
* corrupt file (e.g. an interrupted write) fails this even though it exists,
|
||||
* so the caller can rebuild rather than leave Kiwix serving a broken library.
|
||||
* An empty-but-well-formed library is considered valid (nothing to repair).
|
||||
*/
|
||||
private _isValidLibraryXml(xmlContent: string): boolean {
|
||||
try {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '@_',
|
||||
isArray: (name) => name === 'book',
|
||||
})
|
||||
const parsed = parser.parse(xmlContent)
|
||||
return parsed?.library !== undefined && parsed?.library !== null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot-time safety net: if the library XML is missing or unparseable, rebuild
|
||||
* it from the ZIM files on disk so Kiwix (running in library mode with
|
||||
* --monitorLibrary) doesn't come up serving an empty/broken library with no
|
||||
* path to recovery. This covers files lost or corrupted outside the normal
|
||||
* download flow (storage relocation, interrupted write, manual deletion).
|
||||
*
|
||||
* Returns true if a rebuild was performed. Filesystem errors other than
|
||||
* "not found" are surfaced rather than masked by a rebuild.
|
||||
*/
|
||||
async ensureLibraryXmlHealthy(): Promise<boolean> {
|
||||
let content: string
|
||||
try {
|
||||
content = await readFile(this.getLibraryFilePath(), 'utf-8')
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'ENOENT') {
|
||||
logger.warn('[KiwixLibraryService] Library XML missing on startup; rebuilding from disk.')
|
||||
await this.rebuildFromDisk()
|
||||
return true
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (this._isValidLibraryXml(content)) return false
|
||||
|
||||
logger.warn('[KiwixLibraryService] Library XML present but invalid; rebuilding from disk.')
|
||||
await this.rebuildFromDisk()
|
||||
return true
|
||||
}
|
||||
|
||||
async rebuildFromDisk(opts?: { excludeFilenames?: string[] }): Promise<number> {
|
||||
const dirPath = join(process.cwd(), ZIM_STORAGE_PATH)
|
||||
await ensureDirectoryExists(dirPath)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export default class KiwixMigrationProvider {
|
|||
const Service = (await import('#models/service')).default
|
||||
const { SERVICE_NAMES } = await import('../constants/service_names.js')
|
||||
const { DockerService } = await import('#services/docker_service')
|
||||
const { KiwixLibraryService } = await import('#services/kiwix_library_service')
|
||||
|
||||
const kiwixService = await Service.query()
|
||||
.where('service_name', SERVICE_NAMES.KIWIX)
|
||||
|
|
@ -36,7 +37,15 @@ export default class KiwixMigrationProvider {
|
|||
const isLegacy = await dockerService.isKiwixOnLegacyConfig()
|
||||
|
||||
if (!isLegacy) {
|
||||
logger.info('[KiwixMigrationProvider] Kiwix is already in library mode — no migration needed.')
|
||||
// Already in library mode. Make sure the library XML actually exists and
|
||||
// is parseable — if it was lost or corrupted outside the download flow,
|
||||
// rebuild it from disk so Kiwix isn't left serving an empty library.
|
||||
const rebuilt = await new KiwixLibraryService().ensureLibraryXmlHealthy()
|
||||
logger.info(
|
||||
rebuilt
|
||||
? '[KiwixMigrationProvider] Rebuilt missing/invalid Kiwix library XML on startup.'
|
||||
: '[KiwixMigrationProvider] Kiwix is already in library mode — no migration needed.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue