fix(api): Logo-Datei nach dem Festlegen der Rechte noch einmal prüfen

Der Hochlader behält delete auf seiner Datei, bis die API sie auf read("any") festlegt. Zwischen der ersten Prüfung
(Typ, Größe, PNG-Kopf, Maße) und dem Festlegen ließ sich die Datei unter derselben ID löschen und mit anderem Inhalt
neu anlegen (z. B. 4000 × 4000). Jetzt liest die API den Dateianfang nach dem Festlegen noch einmal, wenn der
Hochlader die Datei nicht mehr ändern kann:

- PNG-Kopf und Maße 8 × 8 bis 720 × 250 werden erneut geprüft. Fällt die Datei durch oder fehlt sie, löscht die API die
  Datei (Server-Key) und die Zeile (beides wird versucht) und antwortet 400 logoFormat bzw. fileMissing. Log:
  "logo <id> removed after upload check (<code>)", bleibt etwas stehen: "rollback failed for logo <id>".
- Ein Appwrite-Fehler beim zweiten Lesen wird wie ein Fehler beim Festlegen behandelt (502, Zeile zurückgenommen, die
  Datei bekommt delete(owner) zurück).
- fake-appwrite.js: Testhilfe swapContentAfterPermissions (der Inhalt ändert sich nach dem nächsten Rechte-Update).
- Tests: 7 neue in logos.test.js (Austausch auf 4000 × 4000, weitere ungültige Inhalte, Datei weg, Appwrite-Fehler,
  Aufräumen scheitert, Ablauffolge, weiterhin gültiger Inhalt), der Logtest prüft das neue Ereignis mit. Der
  Karten-Ablauf ist unverändert (app.js und common.js nicht angefasst).
- Doku: api/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 19:28:12 +00:00
1 parent dc5de99611
commit fc9e5f5577
4 files changed
+198 -6

No files matched your search

+17
View File
@@ -18,6 +18,8 @@
* Test auffallen.
* Dateien werden nach ID gesucht und liefern ihren echten `bucketId` zurück (der Bucket in der URL zählt nicht),
* so lässt sich der Fall „Datei liegt in einem anderen Bucket“ nachstellen.
* Testhilfen: failNext/hangNext/delayNext (Aufruf scheitert, hängt, wird verzögert) und swapContentAfterPermissions
* (Dateiinhalt ändert sich nach dem nächsten Rechte-Update).
*/
const http = require('node:http');
@@ -125,6 +127,7 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
const users = new Map();
const calls = [];
const injections = [];
const swaps = new Map();
let sequence = 0;
function addUser(jwt, { id, labels = [] }) {
@@ -180,6 +183,15 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
injections.push({ predicate, delayMs });
}
/**
* Nach dem nächsten Rechte-Update (PUT) der Datei `id` hat sie den Inhalt `content` (einmalig). So lässt sich eine Datei
* nachstellen, die zwischen der ersten Prüfung und dem Festlegen der Rechte ausgetauscht wurde: der Anfang ist beim ersten
* Lesen gültig und danach ein anderer.
*/
function swapContentAfterPermissions(id, content) {
swaps.set(id, content);
}
function validateData(table, data, { partial }) {
const columns = COLUMNS[table];
for (const [name, value] of Object.entries(data)) {
@@ -323,6 +335,10 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
const body = call.body || {};
if (!Array.isArray(body.permissions)) return error(400, 'general_argument_invalid');
file.$permissions = body.permissions;
if (swaps.has(segments[5])) {
contents.set(segments[5], swaps.get(segments[5]));
swaps.delete(segments[5]);
}
return reply(200, file);
}
}
@@ -348,6 +364,7 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
failNext,
hangNext,
delayNext,
swapContentAfterPermissions,
row: (table, id) => tables[table].get(id) || null,
rows: (table) => [...tables[table].values()],
file: (id) => files.get(id) || null,