From dea76e0cd14ecea20ee62b882f717c52203e17bf Mon Sep 17 00:00:00 2001 From: Jason MacNamara Date: Wed, 23 Sep 2026 22:53:05 +0100 Subject: [PATCH] fix: server: pick the requested Graphic when a folder holds several MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A folder MAY hold several manifest files. The specification is explicit that they are then "interpreted as multiple, independent Graphics", and that this "can be useful for example when having a package of multiple OGraf graphics, which then might share resources such as images, fonts, etc." findManifestFile() returned the first manifest file readdir() happened to yield, which is not an answer to "which Graphic is in this folder". getGraphicInfo() then did the right thing and rejected the folder, because the manifest it had been handed declared a different id: Folder name graphic- does not match manifest id So of a package of three Graphics sharing one folder, exactly one loaded and the other two returned 500 for every load. Which one worked depended on readdir order, so it was not even stable between machines or filesystems. findManifestFile() now takes the id the caller is asking for and returns the manifest carrying it, falling back to the previous behaviour when the id is not known or no manifest matches — the callers that check the id themselves are unchanged. Both id-known call sites, getGraphicInfo() and getGraphicManifest(), pass it. The legacy folder-rename scan has no id to match and keeps taking the first manifest, which is correct for the single-manifest folders it deals with. Verified against a storage root holding a three-manifest package: all three ids now resolve to their own manifest and their own name, where previously only the alphabetically-first one did. (cherry picked from commit 40e10954d24bfaf50a8d881e14590b194a557473) --- packages/server/src/managers/GraphicsStore.ts | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/server/src/managers/GraphicsStore.ts b/packages/server/src/managers/GraphicsStore.ts index e12f35c..e9ca691 100644 --- a/packages/server/src/managers/GraphicsStore.ts +++ b/packages/server/src/managers/GraphicsStore.ts @@ -36,11 +36,24 @@ export class GraphicsStoreNS { this.destroyed = true if (this.checkInterVal !== undefined) clearInterval(this.checkInterVal) } - /** Find a manifest file in a folder */ - private async findManifestFile(graphicsFolder: string): Promise { + /** + * Find a manifest file in a folder. + * + * A folder MAY hold several manifest files, and the specification is explicit + * that they are then "interpreted as multiple, independent Graphics" — a + * package of Graphics sharing resources such as images and fonts. So the first + * file readdir() happens to return is not an acceptable answer: it hands every + * caller whichever Graphic sorts first, and the id check in getGraphicInfo() + * then rejects the folder for every other Graphic in it. + * + * When the caller knows which Graphic it wants, match on the manifest id. + */ + private async findManifestFile(graphicsFolder: string, wantedId?: string): Promise { const files = await fs.promises.readdir(graphicsFolder, { withFileTypes: true, }) + + const candidates: string[] = [] for (const file of files) { if ( file.isFile() && @@ -48,11 +61,27 @@ export class GraphicsStoreNS { file.name.endsWith('.ograf') || // File name from 2025-06-13 to 2025-07-13 file.name === 'manifest.json') // Legacy, initial manifest file name ) { - return path.join(graphicsFolder, file.name) + candidates.push(path.join(graphicsFolder, file.name)) + } + } + + if (candidates.length === 0) { + throw new Error(`No OGraf manifest found in folder ${graphicsFolder}`) + } + + if (wantedId !== undefined) { + for (const candidate of candidates) { + try { + const manifest = JSON.parse(await fs.promises.readFile(candidate, 'utf8')) as GraphicsManifest + if (manifest?.id === wantedId) return candidate + } catch { + // An unreadable or malformed manifest is not the one being looked for. + // It may still be the right answer for a caller that did not ask by id. + } } } - throw new Error(`No OGraf manifest found in folder ${graphicsFolder}`) + return candidates[0] } async listGraphics(config: ConfigOptions): Promise { const graphics: ServerApi.components['schemas']['GraphicListInfo'][] = [] @@ -103,7 +132,9 @@ export class GraphicsStoreNS { return undefined } - const manifestFilePath = await this.findManifestFile(fullFolderPath) + // Several Graphics may share this folder, so ask for the one that was + // actually requested rather than whichever manifest is found first. + const manifestFilePath = await this.findManifestFile(fullFolderPath, id) const pStat = fs.promises.stat(manifestFilePath)