feat: Konten sichtbar, Kontingent je Token, Plattenwaechter, Umbenennen im Verlauf
KONTEN SICHTBAR. Es gab zwei -- "admin" und eines aus Discord -- und KEINE
Stelle, an der man sie sehen konnte. Weil jeder mit der Rolle auf dem
Discord-Server beim ersten Anmelden still ein Konto bekommt, waeren aus zwei
unbemerkt zwanzig geworden: man erfaehrt es nicht, und wegnehmen ging auch
nicht.
Jetzt eine Liste mit Weg (Discord/Passwort), angelegt, zuletzt da und offenen
Sitzungen. Die Spalte "Weg" ist dabei die wichtigere Auskunft als der Name: ein
Discord-Konto haengt an einer Rolle, die jemand anders vergibt.
Zwei Sperren, beide mit Grund: das EIGENE Konto laesst sich hier nicht
entfernen -- das ist keine Bevormundung, sondern die Vermeidung des einen
Klicks, nach dem niemand mehr hereinkommt -- und das LETZTE auch nicht. Die
Rueckfrage sagt bei einem Discord-Konto dazu, dass die naechste Anmeldung ein
neues anlegt, solange die Rolle bleibt; sonst haelt jemand das Entfernen fuer
eine Aussperrung.
"zuletzt da" wird hoechstens einmal je Stunde geschrieben. Bei jedem Aufruf
waere es ein UPDATE je Anfrage, und das Dashboard stellt beim Blaettern durch
3600 Bilder eine Menge davon.
KONTINGENT JE TOKEN. max_bytes gab es laengst, aber es galt JE DATEI: ein Token
mit "hoechstens 2 MB je Bild" konnte trotzdem die Platte fuellen, es brauchte
nur genug Bilder. quota_bytes gilt fuer alles zusammen, gezaehlt ueber
media.token_id -- also ueber das, was wirklich liegt, statt ueber einen
mitlaufenden Zaehler, der auseinanderlaufen kann.
Auf BEIDEN Wegen geprueft, eigene API und Fivemanage-Weg: eine Grenze, die nur
an einer von zwei Tueren haengt, ist keine.
Die erste Fassung meldete "haelt 0.0 MB von 0.0 MB, und diese Datei braucht 0.0
MB" -- formal richtig, praktisch die Meldung, die genau die Frage nicht
beantwortet, wegen der man sie liest. Die Einheit waechst jetzt mit: "haelt 12
KB von 20 KB, und diese Datei braucht 12 KB."
PLATTENWAECHTER. Meldet bei 80, 90 und 95 Prozent, je Stufe genau einmal; wird
wieder Platz frei, meldet der naechste Engpass erneut. Der Grund steht in der
Meldung: eine volle Platte ist der eine Zustand, in dem gleichzeitig nichts mehr
hereinkommt UND die Sicherung nicht mehr schreiben kann.
Gemessen, und die Abweichung zu df ist gewollt:
gesamt 1006,9 GB
frei (bavail) 909,5 GB -> 9,7 % belegt <- was der Waechter sieht
frei (bfree) 960,7 GB -> 4,6 % belegt
df sagt 5 %
bavail zieht die ext4-Reserve ab, an die der Dienst als Nicht-root ohnehin nicht
herankommt. Er warnt damit etwas frueher -- die richtige Richtung. Wer spaeter
den Unterschied zu df sucht, findet ihn als Kommentar an Ort und Stelle.
Dabei fiel ein SCHLAFENDER FEHLER auf: der Merker gegen Wiederholungen lag unter
EINEM Schluessel fuer alle Wachhunde. Mit zwei davon haetten sie sich
gegenseitig ueberschrieben -- einer meldet, der andere haelt daraufhin seinen
eigenen Zustand fuer neu und meldet auch, oder schweigt, obwohl sich etwas
geaendert hat. Der Merker haengt jetzt am Anlass.
UMBENENNEN IM VERLAUF. Stand dort als "verschoben". Technisch stimmt das, fuer
den Leser nicht: verschoben heisst "liegt woanders", umbenannt heisst "heisst
anders, liegt noch da" -- und genau diesen Unterschied schlaegt man im Verlauf
nach. Eigene Art mit eigenem Zeichen. Die Filterleiste kennt jetzt auch move und
rename, die vorher gar nicht filterbar waren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
pruneEvents,
|
||||
pruneSessions,
|
||||
type Media,
|
||||
type BenutzerZeile,
|
||||
type Freigabe,
|
||||
type MediaEvent,
|
||||
type PapierkorbEintrag,
|
||||
@@ -947,7 +948,7 @@ dashRoutes.get('/events', (c) => {
|
||||
|
||||
const bedingungen: string[] = []
|
||||
const params: unknown[] = []
|
||||
if (kind && ['upload', 'replace', 'delete'].includes(kind)) {
|
||||
if (kind && ['upload', 'replace', 'delete', 'move', 'rename'].includes(kind)) {
|
||||
bedingungen.push('kind = ?')
|
||||
params.push(kind)
|
||||
}
|
||||
@@ -1469,7 +1470,7 @@ dashRoutes.post('/media/umbenennen', async (c) => {
|
||||
|
||||
await moveFile(row.path, neu)
|
||||
db.prepare('UPDATE media SET path = ?, updated_at = ? WHERE id = ?').run(neu, now(), row.id)
|
||||
logEvent('move', neu, row.size, 'user', c.get('user').username, row.path)
|
||||
logEvent('rename', neu, row.size, 'user', c.get('user').username, row.path)
|
||||
|
||||
return c.json({ alt: row.path, neu, url: publicUrlFor(neu), abrufe: row.abrufe })
|
||||
})
|
||||
@@ -1548,7 +1549,7 @@ dashRoutes.post('/folders/umbenennen', async (c) => {
|
||||
|
||||
// EIN Eintrag im Verlauf und nicht 3576. Wer nachliest, was passiert ist,
|
||||
// will "items -> gegenstaende" lesen und nicht dreitausend Zeilen davon.
|
||||
logEvent('move', neu, 0, 'user', wer, alt)
|
||||
logEvent('rename', neu, 0, 'user', wer, alt)
|
||||
|
||||
return c.json({ alt, neu, dateien: betroffen.length })
|
||||
})
|
||||
@@ -1868,6 +1869,71 @@ dashRoutes.post('/papierkorb/leeren', async (c) => {
|
||||
return c.json({ geleert: weg })
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Benutzer
|
||||
//
|
||||
// WOZU. Es gab zwei Konten und keine Stelle, an der man sie sehen konnte --
|
||||
// "admin" und eines aus Discord. Und weil jeder mit der Rolle auf dem Server
|
||||
// beim ersten Anmelden STILL ein Konto bekommt, waere aus zwei unbemerkt
|
||||
// zwanzig geworden. Wer hereinkommt, gehoert auf einen Bildschirm.
|
||||
|
||||
dashRoutes.get('/benutzer', (c) => {
|
||||
const items = (
|
||||
db
|
||||
.prepare(
|
||||
`SELECT id, username, created_at, zuletzt_da, discord_id, discord_name,
|
||||
discord_avatar
|
||||
FROM users ORDER BY created_at`,
|
||||
)
|
||||
.all() as BenutzerZeile[]
|
||||
).map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
created_at: u.created_at,
|
||||
zuletzt_da: u.zuletzt_da,
|
||||
// Ueber welchen Weg jemand hereinkommt, ist die wichtigere Auskunft als
|
||||
// sein Name: ein Discord-Konto haengt an einer Rolle, die jemand anders
|
||||
// vergibt.
|
||||
weg: u.discord_id ? 'discord' : 'passwort',
|
||||
discordName: u.discord_name,
|
||||
avatar: avatarUrl(u.discord_id, u.discord_avatar, 32),
|
||||
// Wieviele Sitzungen gerade offen sind -- die Zahl, die nach dem
|
||||
// Entfernen auf 0 steht.
|
||||
sitzungen: (
|
||||
db
|
||||
.prepare('SELECT COUNT(*) AS n FROM sessions WHERE user_id = ? AND expires_at > ?')
|
||||
.get(u.id, now()) as { n: number }
|
||||
).n,
|
||||
}))
|
||||
return c.json({ items, ich: c.get('user').id })
|
||||
})
|
||||
|
||||
/** Ein Konto entfernen.
|
||||
*
|
||||
* SICH SELBST NICHT -- das ist keine Bevormundung, sondern die Vermeidung des
|
||||
* einen Klicks, nach dem niemand mehr hereinkommt. Und den LETZTEN nicht: ein
|
||||
* Dienst ohne Konto laesst sich nur noch ueber die Datenbank reparieren.
|
||||
*
|
||||
* Die Sitzungen gehen mit (ON DELETE CASCADE), der Zugang ist also sofort weg
|
||||
* und nicht erst in 30 Tagen. */
|
||||
dashRoutes.delete('/benutzer/:id', (c) => {
|
||||
const id = Number(c.req.param('id'))
|
||||
if (id === c.get('user').id) {
|
||||
return c.json({ error: 'Das eigene Konto laesst sich hier nicht entfernen.' }, 400)
|
||||
}
|
||||
const wieviele = (db.prepare('SELECT COUNT(*) AS n FROM users').get() as { n: number }).n
|
||||
if (wieviele <= 1) {
|
||||
return c.json({ error: 'Das letzte Konto bleibt — sonst kommt niemand mehr herein.' }, 400)
|
||||
}
|
||||
|
||||
const wer = db.prepare('SELECT username FROM users WHERE id = ?').get(id) as
|
||||
| { username: string }
|
||||
| undefined
|
||||
if (!wer) return c.json({ error: 'nicht gefunden' }, 404)
|
||||
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(id)
|
||||
return c.json({ ok: true, username: wer.username })
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------- Freigaben
|
||||
//
|
||||
// Was hier verwaltet wird, ist die LISTE eines Ordners -- die Dateien selbst
|
||||
@@ -2032,7 +2098,7 @@ dashRoutes.get('/tokens', (c) => {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, name, prefix, can_delete, created_at, last_used_at,
|
||||
max_bytes, arten, expires_at
|
||||
max_bytes, quota_bytes, arten, expires_at
|
||||
FROM tokens ORDER BY created_at DESC`,
|
||||
)
|
||||
.all() as Omit<Token, 'hash'>[]
|
||||
@@ -2068,6 +2134,7 @@ dashRoutes.post('/tokens', async (c) => {
|
||||
prefix?: string
|
||||
canDelete?: boolean
|
||||
maxMb?: number | string
|
||||
quotaMb?: number | string
|
||||
arten?: string[]
|
||||
gueltigTage?: number | string
|
||||
}>(c)
|
||||
@@ -2096,6 +2163,18 @@ dashRoutes.post('/tokens', async (c) => {
|
||||
maxBytes = Math.round(mb * 1024 * 1024)
|
||||
}
|
||||
|
||||
// DAS GESAMTKONTINGENT -- etwas anderes als die Groesse EINER Datei, und
|
||||
// deshalb ohne Deckelung durch config.maxUploadBytes: "hoechstens 2 MB je
|
||||
// Bild, aber insgesamt 5 GB" ist eine voellig sinnvolle Angabe.
|
||||
let quotaBytes: number | null = null
|
||||
if (body.quotaMb !== undefined && String(body.quotaMb).trim() !== '') {
|
||||
const mb = Number(body.quotaMb)
|
||||
if (!Number.isFinite(mb) || mb <= 0) {
|
||||
return c.json({ error: 'Kontingent: eine Zahl groesser als 0' }, 400)
|
||||
}
|
||||
quotaBytes = Math.round(mb * 1024 * 1024)
|
||||
}
|
||||
|
||||
const ERLAUBTE_ARTEN = ['bild', 'video', 'ton', 'dokument', 'andere']
|
||||
let arten = ''
|
||||
if (Array.isArray(body.arten) && body.arten.length > 0) {
|
||||
@@ -2131,8 +2210,8 @@ dashRoutes.post('/tokens', async (c) => {
|
||||
const info = db
|
||||
.prepare(
|
||||
`INSERT INTO tokens (name, hash, prefix, can_delete, created_at,
|
||||
max_bytes, arten, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
max_bytes, quota_bytes, arten, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
name.trim(),
|
||||
@@ -2141,6 +2220,7 @@ dashRoutes.post('/tokens', async (c) => {
|
||||
canDelete ? 1 : 0,
|
||||
now(),
|
||||
maxBytes,
|
||||
quotaBytes,
|
||||
arten,
|
||||
expiresAt,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user