e87c61435c
Security and robustness: - EXIF orientation is now applied before any geometry. Phone photos carry the rotation only as metadata; sharp was cropping the unrotated raster, so a portrait shot came out of the printer sideways and wrongly framed. - renderCell no longer materialises the padded image at source resolution. It is one extract-resize-extend chain now, which is also sharp's internal order. A panorama into a narrow contain target used to build a ~960 MB intermediate and then fail; it is 90 ms and a few MB now. - Target size is capped (300 Mpx) and bleedMm is clamped in /api/print/single, which had no bound at all. - The delivery gallery is validated before use - posixpath.join let a crafted name escape the target's base folder and create directories there. - Sheet requests are capped at 500 pieces and the packer has a step budget, so a degenerate request cannot block the single-threaded server. - Print presets: delete only your own (admins all), config size limit, count limit, and by_name honours anonymous_generations. - Telegram callbacks require an active pairing, like every other path. - Error responses no longer leak storage paths or delivery hostnames. Correctness: - allowRotate:undefined now means allowed, consistently with the packer. - The many-formats shortcut no longer drops a format that only fits rotated. - unplaced names the format that is actually missing, not the first one. - Crop marks never sit inside the printed bleed - the offset is raised. - capacity() computes the grid instead of probing with 200 copies. - Image keys in the sheet cannot collide with a cell literally named x::rot. - labelMm keeps real decimals; parseSizeMm reads a:b as width:height, so 3:4/15 is portrait and 4:3/15 is landscape. - The footer is skipped when there is no free space at the bottom. - The UI warns when corner marks do not fit the margin, and when continuous guides are used with mixed sizes. Tests: 21 -> 31, each finding has a regression test.
47 lines
2.0 KiB
TypeScript
47 lines
2.0 KiB
TypeScript
// Auflösen der Bildquellen für den Druckbogen: frisch hochgeladen oder aus der Bibliothek.
|
|
import { one } from './db';
|
|
import { getObject } from './storage';
|
|
|
|
export type PrintSource =
|
|
| { kind: 'upload'; path: string }
|
|
| { kind: 'item'; id: string };
|
|
|
|
export interface SessionUser { uid: string | null; role: string }
|
|
|
|
/** Lädt die Bilddaten und prüft dabei die Zugriffsrechte wie /api/items/:id/file. */
|
|
export async function loadSource(src: PrintSource, user: SessionUser): Promise<Buffer> {
|
|
if (!src || typeof src !== 'object') throw new Error('Quelle fehlt.');
|
|
|
|
if (src.kind === 'upload') {
|
|
// Nur der Upload-Bereich ist erreichbar — kein Weg zu results/ oder /etc.
|
|
const p = String(src.path || '');
|
|
// \n bewusst ausschließen: JS-„$" matcht auch vor einem abschließenden Zeilenumbruch.
|
|
if (/[\r\n]/.test(p) || !/^sources\/[0-9]{4}\/[A-Za-z0-9_-]+\.[A-Za-z0-9]{2,5}$/.test(p))
|
|
throw new Error('Ungültige Quelle.');
|
|
return getObject(p);
|
|
}
|
|
|
|
if (src.kind === 'item') {
|
|
const item = await one<any>(
|
|
`SELECT i.result_path, i.filename, j.created_by, j.private
|
|
FROM items i JOIN jobs j ON j.id = i.job_id WHERE i.id = $1`, [src.id]);
|
|
if (!item?.result_path) throw new Error('Bild nicht gefunden.');
|
|
const isAdmin = user.role === 'admin';
|
|
const own = item.created_by === user.uid;
|
|
if (!isAdmin && !own) {
|
|
const s = await one<{ library_visibility: string }>('SELECT library_visibility FROM settings WHERE id=1');
|
|
if (item.private || s?.library_visibility !== 'shared') throw new Error('Kein Zugriff auf dieses Bild.');
|
|
}
|
|
return getObject(item.result_path);
|
|
}
|
|
|
|
throw new Error('Unbekannte Quellenart.');
|
|
}
|
|
|
|
/** Sprechender Dateiname für den Download. */
|
|
export function sheetFilename(prefix: string, ext: string): string {
|
|
const date = new Date().toISOString().slice(0, 10);
|
|
const slug = (prefix || 'druckbogen').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
return `${date}_${slug || 'druckbogen'}.${ext}`;
|
|
}
|