From 20c0b6b064c73a06706f37ca1d48ffdb27813fd4 Mon Sep 17 00:00:00 2001 From: Nikita Menkovich Date: Fri, 9 Oct 2026 10:14:22 +0000 Subject: [PATCH] fix: localize recipe photos on edit --- scripts/recipe-image-sync.test.js | 15 +++++++++++++++ server/routes/meals.js | 11 ++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/recipe-image-sync.test.js b/scripts/recipe-image-sync.test.js index 8fb8b465..52c53600 100644 --- a/scripts/recipe-image-sync.test.js +++ b/scripts/recipe-image-sync.test.js @@ -9,10 +9,12 @@ import { originalUrlForCachedImage } from '../src/lib/image-cache-map.js'; const temp = mkdtempSync(join(tmpdir(), 'nutritrace-recipe-sync-')); process.env.DB_PATH = join(temp, 'test.db'); +process.env.UPLOADS_PATH = join(temp, 'uploads'); process.env.NODE_ENV = 'test'; process.env.JWT_SECRET = 'recipe-sync-test-only'; const { default: db } = await import('../server/db.js'); +const { freshenItemImages } = await import('../server/lib/diary-helpers.js'); const { default: express } = await import('../server/node_modules/express/index.js'); const { default: meals } = await import('../server/routes/meals.js'); const { default: sync } = await import('../server/routes/sync.js'); @@ -125,3 +127,16 @@ test('explicit photo removal and unset servings still survive save and sync', as await f.pushChanges(); assert.deepEqual(f.row(), { img_url: null, servings: null }); }); + +test('a recipe photo supplied inline on edit is stored as a file and visible in the diary', async () => { + const f = fixture(); + const bytes = Buffer.concat([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=', 'base64'), Buffer.alloc(64)]); + await f.api.updateMeal(f.id, { + ...await f.api.getMeal(f.id), imgUrl: `data:image/png;base64,${bytes.toString('base64')}`, + }); + await f.requests[0].promise; + const image = f.row().img_url; + assert.match(image, /^\/uploads\//); + assert.deepEqual(readFileSync(join(temp, image)), bytes); + assert.equal(freshenItemImages([{ id: f.id, name: 'Bread test', is_recipe: true }])[0].imgUrl, image); +}); diff --git a/server/routes/meals.js b/server/routes/meals.js index 46c6faaf..bd847306 100644 --- a/server/routes/meals.js +++ b/server/routes/meals.js @@ -135,7 +135,7 @@ router.post('/', wrap(async (req, res) => { })); // ── PUT /:id ────────────────────────────────────────────────────────────── -router.put('/:id', wrap((req, res) => { +router.put('/:id', wrap(async (req, res) => { const u = uid(req); const existing = db.prepare('SELECT * FROM meals WHERE id = ?').get(req.params.id); if (!existing) return res.status(404).json({ error: 'Not found' }); @@ -151,10 +151,11 @@ router.put('/:id', wrap((req, res) => { // The old inline `img_url ?? existing.img_url` treated null as nullish and // preserved the existing image, so the MealEditor X-remove-photo button // silently failed to clear the photo on save. Parallel to the foods.js - // PUT fix for kilkalabs's report on #74 follow-up. Note: external-URL / - // data-URL localization on this route is a separate parity gap with the - // POST handler (line ~61) — out of scope for this fix. - const img = 'img_url' in req.body ? (img_url || null) : existing.img_url; + // PUT fix for kilkalabs's report on #74 follow-up. Localize supplied + // images just as POST does, so the diary can resolve edited recipe photos. + const img = 'img_url' in req.body + ? ((img_url && isExternalUrl(img_url)) ? await localizeImage(img_url, { allowPrivate: ownerOrOptIn(req, 'ALLOW_PRIVATE_IMAGE_URLS') }) : (img_url || null)) + : existing.img_url; db.prepare( `UPDATE meals SET name=?, nutrition=?, items=?, img_url=?, notes=?, is_recipe=?, portion=?, unit=?, servings=?, visibility=?, favorite=?, updated_at=datetime('now') WHERE id=?` ).run(name ?? existing.name, JSON.stringify(nutrition ?? JSON.parse(existing.nutrition || '{}')),