diff --git a/package.json b/package.json index 50e4d24..5f2edc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/zim-utils", - "version": "0.3.4", + "version": "0.3.5", "description": "Native, dependency-light ZIM archive reader/searcher and Kiwix catalog downloader for Node.js", "author": "Zak Timson", "license": "MIT", diff --git a/src/manager.js b/src/manager.js index 39a3ab6..988dff5 100644 --- a/src/manager.js +++ b/src/manager.js @@ -90,9 +90,9 @@ export class ZimManager { /** Downloads/updates a single ZIM by direct href, matching against any existing local copy by name. */ async download(href, {force = false} = {}) { - await fs.promises.mkdir(this.#dir, {recursive: true}); const {url: finalUrl} = await this.#resolveUrl(href); const filename = path.basename(new URL(finalUrl).pathname).replace(/\.meta4$/i, ''); + await fs.promises.mkdir(this.#dir, {recursive: true}); const name = filename.replace(/\.zim$/i, '').replace(/_\d{4}-\d{2}(?:_\d+)?$/, ''); const local = await this.list(); diff --git a/src/server.js b/src/server.js index 38565bb..e46283c 100644 --- a/src/server.js +++ b/src/server.js @@ -27,6 +27,8 @@ function findFreePort() { /** Owns a kiwix-serve process's full lifecycle: library.xml, start/stop/reload, content + search access. */ export class KiwixServer { + static #empty = '\n\n'; + #dir; #host; #port; @@ -49,6 +51,12 @@ export class KiwixServer { this.#binDir = binDir; this.#libraryPath = path.join(dir, 'library.xml'); this.#remote = url ? url.replace(/\/$/, '') : null; + if (!this.#remote) this.#ensureLocalStore(); + } + + #ensureLocalStore() { + fs.mkdirSync(this.#dir, {recursive: true}); + if (!fs.existsSync(this.#libraryPath)) fs.writeFileSync(this.#libraryPath, KiwixServer.#empty); } #assertRunning() { @@ -61,15 +69,22 @@ export class KiwixServer { /** Reads library.xml from disk if we own the server, or over HTTP if attached to a remote one. */ async #fetchLibraryXml() { - if (this.#remote) return (await fetch(`${this.#remote}/library.xml`).catch(() => null))?.text?.() ?? ''; - return fs.promises.readFile(this.#libraryPath, 'utf8').catch(() => ''); + if (!this.#remote) return fs.promises.readFile(this.#libraryPath, 'utf8').catch(() => ''); + try { + const res = await fetch(`${this.#remote}/library.xml`); + return res.ok ? await res.text() : ''; + } catch { + return ''; + } } /** Rebuilds library.xml from scratch by scanning `dir` for .zim files - no-op if attached to a remote server. */ async #rebuildLibrary() { if (this.#remote) return; await fs.promises.rm(this.#libraryPath, {force: true}); - for (const f of await this.#zimFiles()) await execFileAsync(this.#bin('kiwix-manage'), [this.#libraryPath, 'add', path.join(this.#dir, f)]); + const files = await this.#zimFiles(); + if (!files.length) return fs.promises.writeFile(this.#libraryPath, KiwixServer.#empty); + for (const f of files) await execFileAsync(this.#bin('kiwix-manage'), [this.#libraryPath, 'add', path.join(this.#dir, f)]); } async #waitUntilReady() { @@ -133,6 +148,7 @@ export class KiwixServer { async list() { this.#assertRunning(); const xml = await this.#fetchLibraryXml(); + if (!xml) return []; const entries = fromXml(xml); return (entries?.library?.book || []).map(e => { const tags = e.tags.split(';');