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:
1 parent
dc5de99611
commit
fc9e5f5577
4 files changed
+198
-6
No files matched your search
+39
-4
@@ -9,6 +9,8 @@
|
||||
* 401 · 400 Validierung, Datei fehlt, kein PNG, größer als 1 MiB, Maße nicht 8 × 8 bis 720 × 250 · 403 Datei nicht
|
||||
* meine / falscher Bucket / Datei gesperrt (kein read("any")) · 409 Datei schon für ein Logo verwendet · 429 schon
|
||||
* 30 Logos · 201 {id} (id = Datei-ID = Zeilen-ID)
|
||||
* Nach dem Festlegen der Datei auf read("any") wird ihr Anfang noch einmal geprüft (der Hochlader kann sie dann nicht
|
||||
* mehr ändern): fällt sie durch oder fehlt sie, werden Datei und Zeile gelöscht und die Antwort ist 400.
|
||||
* POST /api/logos/herz Nutzer-JWT {id, herz}
|
||||
* 401 · 400 · 404 fehlt oder ausgeblendet · 200 {herz, likeCount}
|
||||
* POST /api/logos/melden ohne Anmeldung {id, reason} (wie /api/melden, Rate-Limit gemeinsam)
|
||||
@@ -180,11 +182,35 @@ function createLogoRoutes({ appwrite, salt, say, withLock, rateLimit, requireUse
|
||||
}
|
||||
}
|
||||
|
||||
/** Liest den Anfang der Datei und verlangt einen PNG-Kopf mit Maßen von 8 × 8 bis 720 × 250. */
|
||||
async function checkLogoSize(fileId) {
|
||||
/**
|
||||
* Liest den Anfang der Datei und verlangt einen PNG-Kopf mit Maßen von 8 × 8 bis 720 × 250.
|
||||
* @returns {Promise<HttpError | null>} das Urteil (400 fileMissing bzw. logoFormat), null = in Ordnung; Appwrite-Fehler werfen
|
||||
*/
|
||||
async function logoSizeProblem(fileId) {
|
||||
const head = await appwrite.readFileHead(BUCKET, fileId, IMAGE_HEAD_BYTES);
|
||||
if (head === null) throw new HttpError(400, MSG.fileMissing);
|
||||
if (!isLogoSize(head)) throw new HttpError(400, MSG.logoFormat);
|
||||
if (head === null) return new HttpError(400, MSG.fileMissing);
|
||||
return isLogoSize(head) ? null : new HttpError(400, MSG.logoFormat);
|
||||
}
|
||||
|
||||
async function checkLogoSize(fileId) {
|
||||
const problem = await logoSizeProblem(fileId);
|
||||
if (problem !== null) throw problem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Logo ist nach dem Festlegen der Rechte durchgefallen (Inhalt ausgetauscht oder Datei weg): die Datei mit dem
|
||||
* Server-Key löschen, dann die Zeile. Beides wird versucht, auch wenn das erste scheitert: die Zeile macht das Logo für
|
||||
* andere sichtbar. Bleibt etwas stehen, steht `rollback failed for logo <id>` im Log.
|
||||
*/
|
||||
async function removeRejectedLogo(id, problem) {
|
||||
let clean = true;
|
||||
await appwrite.deleteFile(BUCKET, id).catch(() => {
|
||||
clean = false;
|
||||
});
|
||||
await appwrite.deleteRow(TABLE_LOGOS, id).catch(() => {
|
||||
clean = false;
|
||||
});
|
||||
say(clean ? `logo ${id} removed after upload check (${problem.code})` : `rollback failed for logo ${id}`);
|
||||
}
|
||||
|
||||
async function uploadLogo({ req, raw }) {
|
||||
@@ -231,9 +257,14 @@ function createLogoRoutes({ appwrite, salt, say, withLock, rateLimit, requireUse
|
||||
|
||||
// Kanonischer Zustand: die Datei trägt genau read("any") (kein delete(owner): sonst ließe sich das Bild tauschen).
|
||||
// Erst nach dem Anlegen der Zeile: ein 409 fasst die Datei nie an. Scheitert das Festlegen, wird die Zeile zurückgenommen.
|
||||
let problem = null;
|
||||
try {
|
||||
const pinned = await appwrite.updateFilePermissions(BUCKET, data.fileId, visibleFilePermissions());
|
||||
if (pinned === null) throw new HttpError(400, MSG.fileMissing);
|
||||
// Jetzt hat der Hochlader kein delete mehr und kann die Datei nicht mehr ändern: den Anfang noch einmal prüfen. So
|
||||
// lässt sich die Datei nicht zwischen der ersten Prüfung und dem Festlegen unter derselben ID austauschen. Ein
|
||||
// Appwrite-Fehler hier wird wie ein Fehler beim Festlegen behandelt (Rücknahme unten, nichts bleibt veröffentlicht).
|
||||
problem = await logoSizeProblem(data.fileId);
|
||||
} catch (err) {
|
||||
let rolledBack = true;
|
||||
await appwrite.deleteRow(TABLE_LOGOS, data.fileId).catch(() => {
|
||||
@@ -244,6 +275,10 @@ function createLogoRoutes({ appwrite, salt, say, withLock, rateLimit, requireUse
|
||||
if (rolledBack) await appwrite.updateFilePermissions(BUCKET, data.fileId, uploadFilePermissions(user.id)).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
if (problem !== null) {
|
||||
await removeRejectedLogo(data.fileId, problem);
|
||||
throw problem;
|
||||
}
|
||||
say(`logo ${data.fileId} uploaded`);
|
||||
return { status: 201, body: { id: data.fileId } };
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user