This commit is contained in:
seb
2026-07-31 20:10:14 +02:00
parent 6074aa7304
commit c77337507f
7 changed files with 372 additions and 5 deletions

View File

@@ -72,6 +72,39 @@
* type: object
* description: Full article payload (unit, brand, categoryIds, photos, eans, prices, …)
* additionalProperties: true
* ArticleImage:
* type: object
* required: [picId, sort, full, thumb, hash, thumbHash]
* properties:
* picId:
* type: integer
* sort:
* type: integer
* full:
* type: object
* required: [w, h, size]
* properties:
* w:
* type: integer
* h:
* type: integer
* size:
* type: integer
* description: Byte length of full AVIF (as-is when already AVIF ≤1000×1000)
* thumb:
* type: object
* required: [w, h]
* properties:
* w:
* type: integer
* h:
* type: integer
* hash:
* type: string
* description: SHA-256 hex of full image bytes
* thumbHash:
* type: string
* description: Base64 thumbhash of thumb preview
*/
export {};

View File

@@ -1,5 +1,8 @@
import express from 'express';
import * as db from '../../db/article.js';
const AVIF_BODY_LIMIT = '20mb';
function parsePositiveInt(value, field) {
const id = Number(value);
if (!Number.isInteger(id) || id <= 0) {
@@ -16,6 +19,11 @@ function articleErrorStatus(error) {
return 500;
}
function isLikelyImageDecodeError(error) {
const msg = error?.message || '';
return /unsupported|corrupt|invalid|input buffer|vips/i.test(msg);
}
/**
* Registers article routes (requires auth middleware).
* @param {import('express').Application} app - Express app
@@ -256,4 +264,95 @@ export function register(app, basePath = '') {
res.status(status).json({ error: error.message });
}
});
/**
* @openapi
* /article/{sku}/image:
* post:
* tags: [Article]
* summary: Add article image
* description: |
* Accepts a full-size AVIF body (`Content-Type: image/avif`).
* Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.
* Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.
* Auth like GET by SKU.
* security:
* - cookieAuth: []
* x-markdown: article/add-image.md
* parameters:
* - in: path
* name: sku
* required: true
* schema:
* type: string
* description: Article SKU
* requestBody:
* required: true
* content:
* image/avif:
* schema:
* type: string
* format: binary
* responses:
* 201:
* description: Image stored and linked
* content:
* application/json:
* schema:
* type: object
* properties:
* image:
* $ref: '#/components/schemas/ArticleImage'
* 400:
* description: Missing body or invalid image
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Article not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
app.post(
`${basePath}/article/:sku/image`,
express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }),
async (req, res) => {
const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : '';
if (!sku) {
return res.status(400).json({ error: 'sku is required' });
}
const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null;
if (!imageBuffer?.length) {
return res.status(400).json({ error: 'AVIF image body is required' });
}
try {
const image = await db.addImage(req.user.id, decodeURIComponent(sku), imageBuffer);
res.status(201).json({ image });
} catch (error) {
if (isLikelyImageDecodeError(error)) {
return res.status(400).json({ error: 'Invalid image' });
}
const status = articleErrorStatus(error);
if (status === 500) console.error('Add article image failed:', error);
res.status(status).json({ error: error.message });
}
},
);
}

View File

@@ -14,3 +14,8 @@ export async function updateBySku(googleAuthId, sku, data) {
const store = await getStore();
return store.article.updateBySku(googleAuthId, sku, data);
}
export async function addImage(googleAuthId, sku, imageBuffer) {
const store = await getStore();
return store.article.addImage(googleAuthId, sku, imageBuffer);
}