fix(api): Bildmaße prüfen, Dateien ohne delete(owner), Limits, Melder-Netz
- Beim Veröffentlichen liest die API die ersten 64 Bytes beider Dateien (PNG-IHDR, WebP VP8/VP8L/VP8X) und verlangt Bild 1536x969, Vorschau 480x302-304, sonst 400 "Das Bild hat nicht das Kartenformat." - Veröffentlichte und ausgeblendete Dateien tragen kein delete(owner) mehr (Abweichung von Spec §7): kein Löschen und Neuanlegen derselben Datei-ID nach dem Veröffentlichen; scheitert das Festlegen, bekommen die Dateien delete(owner) für den Client-Aufräumer zurück - Melder-Identität aus dem IPv4-/24 bzw. IPv6-/48; das Rate-Limit zählt weiter je Anschluss - Höchstens 20 Veröffentlichungen je Konto (429); sourceCardId muss eine eigene Zeile in `cards` sein (403) - Steuer-, Nullbreiten- und Bidi-Zeichen in Titel, Herausgeber, Anzeigename und Tags abgelehnt - Tags als ",a,b," gespeichert (exakter Tag-Filter) - fehlgeschlagenes Zurücknehmen wird mit `rollback failed for card <id>` protokolliert - README: Endpunkt-Tabelle, Regeln, Scopes (documents.*), akzeptiertes Risiko X-Real-Ip im Docker-Netz Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87b71ec71f
commit
4c90ee49e8
6 files changed
+743
-163
No files matched your search
+35
-2
@@ -11,6 +11,8 @@
|
||||
* updateRow(table, id, data, permissions?) Rechte nur ändern, wenn übergeben
|
||||
* deleteRow(table, id) true, false bei 404 row_not_found (wiederholbar)
|
||||
* getFile(bucket, id) Datei-Metadaten ($permissions, bucketId, mimeType, sizeOriginal) oder null
|
||||
* readFileHead(bucket, id, maxBytes) die ersten Bytes des Dateiinhalts (Buffer) oder null; liest nie mehr als
|
||||
* nötig und bricht den Rest der Übertragung ab
|
||||
* updateFilePermissions(bucket, id, permissions) Datei oder null (404 storage_file_not_found)
|
||||
* deleteFile(bucket, id) true, false bei 404 storage_file_not_found
|
||||
* accountFromJwt(jwt) Nutzer ($id, labels) oder null (401); OHNE Server-Key
|
||||
@@ -28,6 +30,7 @@
|
||||
*/
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||
const DEFAULT_HEAD_BYTES = 64;
|
||||
const ROW_NOT_FOUND = 'row_not_found';
|
||||
const FILE_NOT_FOUND = 'storage_file_not_found';
|
||||
|
||||
@@ -72,8 +75,30 @@ function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch,
|
||||
const rowsPath = (table) => `/tablesdb/${segment(database)}/tables/${segment(table)}/rows`;
|
||||
const filePath = (bucket, id) => `/storage/buckets/${segment(bucket)}/files/${segment(id)}`;
|
||||
|
||||
/** Ein HTTP-Aufruf; liefert { status, body } für 2xx und für ein 404 mit einem Typ aus `missingTypes`, sonst wirft er. */
|
||||
async function send(method, path, { body, headers = {}, withKey = true, missingTypes = [] } = {}) {
|
||||
/** Liest höchstens `limit` Bytes vom Anfang der Antwort und bricht den Rest ab (Dateien können 5 MiB groß sein). */
|
||||
async function readHead(response, limit) {
|
||||
if (!response.body) return Buffer.from(await response.arrayBuffer()).subarray(0, limit);
|
||||
const reader = response.body.getReader();
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
try {
|
||||
while (size < limit) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(Buffer.from(value));
|
||||
size += value.length;
|
||||
}
|
||||
} finally {
|
||||
await reader.cancel().catch(() => {});
|
||||
}
|
||||
return Buffer.concat(chunks).subarray(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein HTTP-Aufruf; liefert { status, body } für 2xx und für ein 404 mit einem Typ aus `missingTypes`, sonst wirft er.
|
||||
* Mit `headBytes` ist `body` bei 2xx ein Buffer mit den ersten Bytes der Antwort statt geparstem JSON.
|
||||
*/
|
||||
async function send(method, path, { body, headers = {}, withKey = true, missingTypes = [], headBytes = 0 } = {}) {
|
||||
const requestHeaders = { 'X-Appwrite-Project': project, ...headers };
|
||||
if (withKey) requestHeaders['X-Appwrite-Key'] = key;
|
||||
const init = { method, headers: requestHeaders, redirect: 'error', signal: AbortSignal.timeout(timeoutMs) };
|
||||
@@ -86,6 +111,9 @@ function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch,
|
||||
let text;
|
||||
try {
|
||||
response = await fetchImpl(`${base}${path}`, init);
|
||||
if (headBytes > 0 && response.status >= 200 && response.status < 300) {
|
||||
return { status: response.status, body: await readHead(response, headBytes) };
|
||||
}
|
||||
text = await response.text();
|
||||
} catch (err) {
|
||||
// Fehlertexte von fetch bleiben draußen (sie könnten Adresse und Header enthalten).
|
||||
@@ -147,6 +175,11 @@ function createAppwrite({ endpoint, project, key, fetchImpl = globalThis.fetch,
|
||||
return result.status === 404 ? null : result.body;
|
||||
},
|
||||
|
||||
async readFileHead(bucket, id, maxBytes = DEFAULT_HEAD_BYTES) {
|
||||
const result = await send('GET', `${filePath(bucket, id)}/view`, { missingTypes: [FILE_NOT_FOUND], headBytes: maxBytes });
|
||||
return result.status === 404 ? null : result.body;
|
||||
},
|
||||
|
||||
async updateFilePermissions(bucket, id, permissions) {
|
||||
const result = await send('PUT', filePath(bucket, id), { body: { permissions }, missingTypes: [FILE_NOT_FOUND] });
|
||||
return result.status === 404 ? null : result.body;
|
||||
|
||||
Reference in new issue
Block a user