fix(recipes): build deploy against .env_prod; harden image save

Recipe image upload failed in prod with ENOENT writing the full image.
Root cause: the deploy built against the dev .env, whose relative
IMAGE_DIR="./imgs/" resolves under the service's dist/ working dir
instead of the real served image directory — and `$env/static/private`
is inlined at build time, so dev values shipped to prod.

- deploy.sh: source .env_prod (overridable via PROD_ENV) into the env
  before `pnpm build`, so prod values win over .env for the whole build
  lifecycle; abort if it's missing rather than ship a dev-env build.
- .gitignore: ignore .env_* so .env_prod (prod secrets) isn't committed
  (the existing .env.* dot pattern didn't match the underscore form).
- imageProcessing: mkdir -p the full/thumb dirs before writing. The
  WebP passthrough writes the full image with fs.writeFile, which (unlike
  sharp's toFile) does not create parent dirs.
- recipeFormHelpers: add serializableFormValues() and use it in the add/
  edit actions' fail() returns. Returning raw formData (now containing the
  recipe_image File) crashed the action response with a non-POJO devalue
  error, masking the real failure with an opaque 500.
This commit is contained in:
2026-05-31 13:49:04 +02:00
parent 9fe9d95e36
commit 9b5cfe5e49
7 changed files with 61 additions and 19 deletions
+12 -5
View File
@@ -1,5 +1,5 @@
import path from 'path';
import { writeFile } from 'fs/promises';
import { writeFile, mkdir } from 'fs/promises';
import sharp from 'sharp';
import { generateImageHashFromBuffer, getHashedFilename } from '$utils/imageHash';
import { validateImageFile } from '$utils/imageValidation';
@@ -138,8 +138,15 @@ export async function processAndSaveRecipeImage(
// the size the user saw in the editor — re-encoding through sharp would silently
// re-compress and discard their quality/size choice.
// Fallback (non-webp upload, e.g. editor bypassed): re-encode to WebP q90 as before.
const fullHashedPath = path.join(imageDir, 'rezepte', 'full', hashedFilename);
const fullUnhashedPath = path.join(imageDir, 'rezepte', 'full', unhashedFilename);
const fullDir = path.join(imageDir, 'rezepte', 'full');
const thumbDir = path.join(imageDir, 'rezepte', 'thumb');
// fs.writeFile (unlike sharp's toFile) does not create parent dirs, so ensure
// both target directories exist before writing.
await mkdir(fullDir, { recursive: true });
await mkdir(thumbDir, { recursive: true });
const fullHashedPath = path.join(fullDir, hashedFilename);
const fullUnhashedPath = path.join(fullDir, unhashedFilename);
let fullBuffer: Buffer;
if (file.type === 'image/webp') {
@@ -165,8 +172,8 @@ export async function processAndSaveRecipeImage(
.toBuffer();
console.log('[ImageProcessing] Thumbnail buffer created, size:', thumbBuffer.length, 'bytes');
const thumbHashedPath = path.join(imageDir, 'rezepte', 'thumb', hashedFilename);
const thumbUnhashedPath = path.join(imageDir, 'rezepte', 'thumb', unhashedFilename);
const thumbHashedPath = path.join(thumbDir, hashedFilename);
const thumbUnhashedPath = path.join(thumbDir, unhashedFilename);
console.log('[ImageProcessing] Saving thumbnail to:', { thumbHashedPath, thumbUnhashedPath });
await sharp(thumbBuffer).toFile(thumbHashedPath);