fix(api): Hochladen hält die Sperre des Logos bis zur zweiten Prüfung
Ab dem Anlegen steht die Zeile öffentlich in der Liste. Ein Herz, eine Meldung oder ein Löschen, das zwischen Anlegen, Festlegen der Dateirechte und zweiter Prüfung eintraf, lief an der Sperre `logo:<id>` vorbei: entfernte die Prüfung das Logo danach, blieb eine Herz- oder Meldungszeile ohne Logo zurück. - logos.js: Der Upload nimmt die Sperre `logo:<Zeilen-ID>` innerhalb der Kontosperre (Reihenfolge Konto, dann Logo; nur das Hochladen nimmt beide, nichts kann sich verklemmen) für Anlegen, Festlegen, zweite Prüfung und Rücknahme. - Tests mit Verzögerung (delayNext): Herz und Meldung, die in dieser Zeit eintreffen, warten und bekommen 404 ohne Reste (Beanstandung und Rücknahme nach einem Fehler); ein Löschen wartet, der Upload gelingt und danach ist das fertige Logo weg. - README: Sperre beim Hochladen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
738358fcac
commit
6134fd26d7
3 files changed
+129
-50
No files matched your search
+55
-48
@@ -22,7 +22,8 @@
|
||||
*
|
||||
* Tabellen `logos` (Zeilen-ID = hash32(`<Konto>|<Datei>`), siehe logoRowId; Altlogos haben noch Zeilen-ID = Datei-ID), `logo_likes`,
|
||||
* `logo_reports`, Bucket `logo-images`. `likeCount` und `reportCount` werden immer aus der Zahl der Zeilen gesetzt (countRows),
|
||||
* nie hoch- oder runtergezählt. Sperren: `logo-owner:<Nutzer>` beim Hochladen, `logo:<id>` für alles je Logo.
|
||||
* nie hoch- oder runtergezählt. Sperren: `logo-owner:<Nutzer>` beim Hochladen, `logo:<id>` für alles je Logo (auch beim Hochladen,
|
||||
* vom Anlegen der Zeile bis zur zweiten Prüfung, innerhalb der Sperre des Kontos).
|
||||
* Logs nennen nur Logo-IDs und Zahlen, nie Namen, IPs, Hashes oder JWTs; Herzen werden nicht geloggt.
|
||||
*/
|
||||
|
||||
@@ -247,54 +248,60 @@ function createLogoRoutes({ appwrite, salt, say, withLock, rateLimit, requireUse
|
||||
checkLogoContent(file);
|
||||
await checkLogoSize(data.fileId);
|
||||
|
||||
try {
|
||||
await appwrite.createRow(
|
||||
TABLE_LOGOS,
|
||||
rowId,
|
||||
{
|
||||
name: data.name,
|
||||
displayName: data.displayName,
|
||||
fileId: data.fileId,
|
||||
ownerId: user.id,
|
||||
mono: data.mono,
|
||||
likeCount: 0,
|
||||
hidden: false,
|
||||
reportCount: 0,
|
||||
},
|
||||
visibleRowPermissions(),
|
||||
);
|
||||
} catch (err) {
|
||||
// Eine Zeile mit dieser ID gibt es schon, steht aber nicht in der Liste des Kontos (ein Rest): das Logo ist vergeben.
|
||||
if (isDuplicate(err)) throw new HttpError(409, MSG.logoTaken);
|
||||
throw err;
|
||||
}
|
||||
// Ab dem Anlegen steht die Zeile öffentlich in der Liste. Solange die Prüfung das Logo noch entfernen kann, dürfen Herz, Meldung
|
||||
// und Löschen nicht dazwischenkommen (ihre Zeilen blieben ohne Logo zurück): der Upload hält die Sperre des Logos bis zum Ende.
|
||||
// Nur das Hochladen nimmt zwei Sperren, immer erst die des Kontos, dann die des Logos; alle anderen Anfragen nehmen nur die des
|
||||
// Logos, so kann sich nichts verklemmen.
|
||||
return withLogoLock(rowId, async () => {
|
||||
try {
|
||||
await appwrite.createRow(
|
||||
TABLE_LOGOS,
|
||||
rowId,
|
||||
{
|
||||
name: data.name,
|
||||
displayName: data.displayName,
|
||||
fileId: data.fileId,
|
||||
ownerId: user.id,
|
||||
mono: data.mono,
|
||||
likeCount: 0,
|
||||
hidden: false,
|
||||
reportCount: 0,
|
||||
},
|
||||
visibleRowPermissions(),
|
||||
);
|
||||
} catch (err) {
|
||||
// Eine Zeile mit dieser ID gibt es schon, steht aber nicht in der Liste des Kontos (ein Rest): das Logo ist vergeben.
|
||||
if (isDuplicate(err)) throw new HttpError(409, MSG.logoTaken);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 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, rowId).catch(() => {
|
||||
rolledBack = false;
|
||||
say(`rollback failed for logo ${rowId} (file ${data.fileId})`);
|
||||
});
|
||||
// Nur wenn keine Zeile mehr auf die Datei zeigt, bekommt sie delete(owner) zurück (der Client räumt damit auf).
|
||||
if (rolledBack) await appwrite.updateFilePermissions(BUCKET, data.fileId, uploadFilePermissions(user.id)).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
if (problem !== null) {
|
||||
await removeRejectedLogo(rowId, data.fileId, problem);
|
||||
throw problem;
|
||||
}
|
||||
say(`logo ${rowId} uploaded`);
|
||||
return { status: 201, body: { id: rowId } };
|
||||
// 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, rowId).catch(() => {
|
||||
rolledBack = false;
|
||||
say(`rollback failed for logo ${rowId} (file ${data.fileId})`);
|
||||
});
|
||||
// Nur wenn keine Zeile mehr auf die Datei zeigt, bekommt sie delete(owner) zurück (der Client räumt damit auf).
|
||||
if (rolledBack) await appwrite.updateFilePermissions(BUCKET, data.fileId, uploadFilePermissions(user.id)).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
if (problem !== null) {
|
||||
await removeRejectedLogo(rowId, data.fileId, problem);
|
||||
throw problem;
|
||||
}
|
||||
say(`logo ${rowId} uploaded`);
|
||||
return { status: 201, body: { id: rowId } };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user