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>
533 lines
18 KiB
TypeScript
533 lines
18 KiB
TypeScript
// Der EINZIGE Ort, an dem fetch steht.
|
|
//
|
|
// Und die Stelle, an der der Fivemanage-Anlauf gescheitert ist: dessen
|
|
// http-util.ts warf jeden Fehlschlag als `new Error(…)`, die Hooks prüften auf
|
|
// `instanceof ApiError` — und damit wurde jeder Fehler lautlos verschluckt.
|
|
// Deshalb gibt es hier genau eine Fehlerklasse, und `request` wirft
|
|
// ausschließlich diese. Wer sie fängt, hat immer einen Text zum Anzeigen.
|
|
|
|
export class ApiError extends Error {
|
|
readonly status: number
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
/** Kein Netz, kein Server, DNS tot — alles, was nie beim Dienst ankam. */
|
|
export class NetworkError extends ApiError {
|
|
constructor(message: string) {
|
|
super(message, 0)
|
|
this.name = 'NetworkError'
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
let response: Response
|
|
try {
|
|
response = await fetch(path, {
|
|
// Das Sitzungs-Cookie ist httpOnly; ohne diese Zeile schickt der
|
|
// Browser es nicht mit, und jede Antwort wäre 401.
|
|
credentials: 'same-origin',
|
|
...init,
|
|
headers: {
|
|
...(init.body ? { 'content-type': 'application/json' } : {}),
|
|
...init.headers,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
// fetch wirft nur, wenn die Anfrage gar nicht stattgefunden hat. Das
|
|
// ist ein anderer Fall als "der Dienst sagt nein", und er verdient
|
|
// einen anderen Satz.
|
|
throw new NetworkError(
|
|
`Der Dienst ist nicht erreichbar (${err instanceof Error ? err.message : 'unbekannt'})`,
|
|
)
|
|
}
|
|
|
|
const text = await response.text()
|
|
let data: unknown = null
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text)
|
|
} catch {
|
|
// Der Dienst antwortet auf allen API-Wegen mit JSON. Kommt hier
|
|
// etwas anderes an, ist der Proxy davor oder die Sitzung schuld —
|
|
// dann steht der Anfang der Antwort in der Meldung, statt dass
|
|
// "unerwarteter Fehler" auf dem Bildschirm steht.
|
|
if (!response.ok) {
|
|
throw new ApiError(
|
|
`HTTP ${response.status}: ${text.slice(0, 140)}`,
|
|
response.status,
|
|
)
|
|
}
|
|
throw new ApiError('Die Antwort war kein JSON', response.status)
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const message =
|
|
data && typeof data === 'object' && typeof (data as any).error === 'string'
|
|
? (data as any).error
|
|
: `HTTP ${response.status}`
|
|
throw new ApiError(message, response.status)
|
|
}
|
|
|
|
return data as T
|
|
}
|
|
|
|
const get = <T,>(path: string) => request<T>(path)
|
|
const post = <T,>(path: string, body?: unknown) =>
|
|
request<T>(path, { method: 'POST', body: body === undefined ? undefined : JSON.stringify(body) })
|
|
const del = <T,>(path: string) => request<T>(path, { method: 'DELETE' })
|
|
|
|
// -- Die Formen, die der Dienst wirklich liefert -------------------------------
|
|
//
|
|
// Abgeschrieben aus routes/dash.ts, nicht erfunden. Wo der Server
|
|
// Datenbankspalten durchreicht, heißen sie hier genauso (can_delete,
|
|
// created_at) — ein hübscherer Name auf halbem Weg wäre eine Übersetzung, die
|
|
// beim nächsten Feld vergessen wird.
|
|
|
|
export type User = {
|
|
id: number
|
|
username: string
|
|
/** Kam dieses Konto ueber Discord herein? */
|
|
discord: boolean
|
|
discordName: string | null
|
|
/** Fertige Adresse vom Dienst — das URL-Schema von Discord steht dort und
|
|
* nicht hier. Null, wenn es kein Discord-Konto ist. */
|
|
avatar: string | null
|
|
}
|
|
|
|
export type MediaItem = {
|
|
id: number
|
|
path: string
|
|
size: number
|
|
sha256: string
|
|
mime: string
|
|
token_id: number | null
|
|
created_at: number
|
|
updated_at: number
|
|
url: string
|
|
/** Wie oft die Datei oeffentlich geholt wurde. */
|
|
abrufe: number
|
|
zuletzt_abgerufen: number | null
|
|
/** Laenge in Sekunden — nur bei Video und Ton, sonst null. */
|
|
dauer: number | null
|
|
}
|
|
|
|
export type MediaPage = {
|
|
total: number
|
|
limit: number
|
|
offset: number
|
|
items: MediaItem[]
|
|
}
|
|
|
|
export type Stats = {
|
|
files: number
|
|
bytes: number
|
|
tokens: number
|
|
folders: { folder: string; files: number; bytes: number }[]
|
|
publicUrl: string
|
|
}
|
|
|
|
export type PapierkorbEintrag = {
|
|
id: number
|
|
path: string
|
|
size: number
|
|
sha256: string
|
|
mime: string
|
|
geloescht: number
|
|
von: string
|
|
/** Liegt die Datei wirklich noch im Papierkorb? Der Eintrag steht in der
|
|
* Datenbank, die Datei daneben auf der Platte — zwei Wahrheiten, die
|
|
* auseinanderlaufen koennen. */
|
|
dateiDa: boolean
|
|
}
|
|
|
|
export type PapierkorbSeite = {
|
|
items: PapierkorbEintrag[]
|
|
n: number
|
|
bytes: number
|
|
haltbarTage: number
|
|
/** Eintraege ohne Datei. Sollte 0 sein. */
|
|
verwaist: number
|
|
}
|
|
|
|
export type Speicher = {
|
|
nachArt: { art: string; files: number; bytes: number }[]
|
|
groesste: MediaItem[]
|
|
aelteste: MediaItem[]
|
|
doppelte: { sha256: string; n: number; bytes: number; pfade: string }[]
|
|
verschwendet: number
|
|
stufen: { tage: number; files: number; bytes: number }[]
|
|
/** Was seit dem Einbau des Zaehlers NIE geholt wurde. */
|
|
nieGeholt: { n: number; bytes: number }
|
|
beliebteste: MediaItem[]
|
|
}
|
|
|
|
export type TokenRow = {
|
|
id: number
|
|
name: string
|
|
prefix: string
|
|
can_delete: number
|
|
created_at: number
|
|
last_used_at: number | null
|
|
/** Groesste erlaubte EINZELNE Datei. Null = die Grenze des Dienstes. */
|
|
max_bytes: number | null
|
|
/** Wieviel dieser Token INSGESAMT halten darf. Null = unbegrenzt. */
|
|
quota_bytes: number | null
|
|
/** Erlaubte Arten, mit Komma. Leer = alle. */
|
|
arten: string
|
|
expires_at: number | null
|
|
abgelaufen: boolean
|
|
/** Wieviel mit DIESEM Token abgelegt wurde. */
|
|
dateien: number
|
|
bytes: number
|
|
/** Wann zuletzt etwas dabei herauskam — anders als `last_used_at`, das
|
|
* auch ein Anklopfen ohne Ergebnis mitzaehlt. */
|
|
zuletztAbgelegt: number | null
|
|
}
|
|
|
|
/** Der Klartext kommt genau einmal — beim Anlegen. Danach steht nur noch der
|
|
* Hash in der Datenbank, und auch der Dienst kann ihn nicht mehr zeigen. */
|
|
export type NewToken = {
|
|
id: number
|
|
name: string
|
|
prefix: string
|
|
canDelete: boolean
|
|
token: string
|
|
}
|
|
|
|
/** Ein Ordner mit KUMULATIVEN Zahlen: "vehicles" enthält, was in
|
|
* "vehicles/showroom" liegt. `tiefe` ist die Einrückung im Baum. */
|
|
export type FolderRow = { folder: string; files: number; bytes: number; tiefe: number }
|
|
|
|
export type Folders = {
|
|
items: FolderRow[]
|
|
gesamt: { files: number; bytes: number }
|
|
wurzel: { files: number; bytes: number }
|
|
}
|
|
|
|
/** Der billige Fuehler fuer die automatische Auffrischung. */
|
|
export type Stand = { medien: number; neuest: number; ereignis: number }
|
|
|
|
export type Uebersicht = {
|
|
files: number
|
|
bytes: number
|
|
heuteHoch: number
|
|
wocheHoch: number
|
|
publicUrl: string
|
|
filePrefix: string
|
|
letzte: MediaEvent[]
|
|
bilder: MediaItem[]
|
|
ordner: { folder: string; files: number; bytes: number }[]
|
|
tokens: { id: number; name: string; prefix: string; can_delete: number; last_used_at: number | null }[]
|
|
discord: boolean
|
|
vorlagen: Vorlage[]
|
|
}
|
|
|
|
export type Vorlage = { name: string; muster: string }
|
|
|
|
/** Ein Konto. `weg` sagt, worueber jemand hereinkommt — die wichtigere Auskunft
|
|
* als der Name, denn ein Discord-Konto haengt an einer Rolle, die jemand
|
|
* anders vergibt. */
|
|
export type BenutzerRow = {
|
|
id: number
|
|
username: string
|
|
created_at: number
|
|
zuletzt_da: number | null
|
|
weg: 'discord' | 'passwort'
|
|
discordName: string | null
|
|
avatar: string | null
|
|
sitzungen: number
|
|
}
|
|
|
|
/** Eine Freigabe: ein Ordner, den man ohne Anmeldung ansehen kann.
|
|
*
|
|
* Freigegeben ist die LISTE, nicht der Inhalt — die Dateien sind ohnehin
|
|
* oeffentlich. Das steht so auch in der Karte, weil das Gegenteil zu glauben
|
|
* gefaehrlich waere. */
|
|
export type FreigabeRow = {
|
|
id: number
|
|
schluessel: string
|
|
ordner: string
|
|
titel: string
|
|
created_at: number
|
|
aufrufe: number
|
|
zuletzt: number | null
|
|
url: string
|
|
}
|
|
|
|
export type VerschiebeErgebnis = {
|
|
verschoben: number
|
|
fehlgeschlagen: number
|
|
ergebnisse: { path: string; ok: boolean; neu?: string; error?: string }[]
|
|
}
|
|
|
|
export type Art = '' | 'bild' | 'video' | 'ton' | 'dokument' | 'andere'
|
|
export type Arten = Record<string, number>
|
|
|
|
export type Sortierung = 'geaendert' | 'angelegt' | 'pfad' | 'groesse' | 'abrufe' | 'geholt'
|
|
export type Richtung = 'auf' | 'ab'
|
|
|
|
export type Verwaiste = {
|
|
dateiOhneDatensatz: { anzahl: number; beispiele: string[] }
|
|
datensatzOhneDatei: { anzahl: number; beispiele: string[] }
|
|
/** Vorschauen UND sparsame Fassungen, deren Original es nicht mehr gibt.
|
|
* Die Beispiele tragen den Ordner vorn — thumbs/… oder webp/…. */
|
|
abgeleitetOhneBild: { anzahl: number; beispiele: string[] }
|
|
}
|
|
|
|
/** Was EIN STAPEL „sparsame Fassungen nachziehen" zurueckmeldet.
|
|
*
|
|
* `fertig` ist der Punkt: der Server rechnet hoechstens ein paar hundert
|
|
* Stueck je Aufruf, weil der Proxy nach 90 Sekunden aufgibt. Solange `fertig`
|
|
* falsch ist, steht in `offen`, wieviele noch fehlen. */
|
|
export type WebpLauf = {
|
|
erzeugt: number
|
|
vorhanden: number
|
|
uebersprungen: number
|
|
lohntNicht: number
|
|
fehler: number
|
|
gesamt: number
|
|
offen: number
|
|
fertig: boolean
|
|
vorher: number
|
|
nachher: number
|
|
gespart: number
|
|
}
|
|
|
|
/** Was das Sicherungsskript nach jedem Lauf zurueckmeldet. */
|
|
export type LetzteSicherung = {
|
|
at: number
|
|
ok: boolean
|
|
dateien?: number
|
|
groesse?: number
|
|
/** true = in der Nextcloud, false = nur lokal, undefined = unbekannt. */
|
|
hoch?: boolean
|
|
fehler?: string
|
|
} | null
|
|
|
|
export type Einstellungen = {
|
|
vorlagen: Vorlage[]
|
|
meldungen: { hatWebhook: boolean; anlaesse: string[] }
|
|
sicherung: {
|
|
basis: string
|
|
benutzer: string
|
|
ordner: string
|
|
behalten: number
|
|
hatPasswort: boolean
|
|
bereit: boolean
|
|
letzte: LetzteSicherung
|
|
}
|
|
discord: {
|
|
clientId: string
|
|
guildId: string
|
|
/** Wer EINE davon hat, darf herein. */
|
|
roleIds: string[]
|
|
/** Nur OB eines hinterlegt ist — das Geheimnis selbst geht nie heraus. */
|
|
hatGeheimnis: boolean
|
|
bereit: boolean
|
|
redirectUri: string
|
|
ausDerUmgebung: {
|
|
clientId: boolean
|
|
clientSecret: boolean
|
|
guildId: boolean
|
|
roleIds: boolean
|
|
}
|
|
}
|
|
}
|
|
|
|
export type EventKind = 'upload' | 'replace' | 'delete' | 'move' | 'rename'
|
|
|
|
export type MediaEvent = {
|
|
id: number
|
|
at: number
|
|
kind: EventKind
|
|
path: string
|
|
size: number | null
|
|
actor_kind: 'token' | 'user' | 'unbekannt'
|
|
actor: string
|
|
/** Nur bei 'move': wo die Datei vorher lag. */
|
|
von: string | null
|
|
}
|
|
|
|
export type EventsSeite = {
|
|
total: number
|
|
limit: number
|
|
offset: number
|
|
items: MediaEvent[]
|
|
akteure: {
|
|
actor: string
|
|
actor_kind: string
|
|
n: number
|
|
zuletzt: number
|
|
avatar: string | null
|
|
}[]
|
|
/** Ab wann aufgezeichnet wird — null, solange nichts drinsteht. */
|
|
seit: number | null
|
|
}
|
|
|
|
export type UploadErgebnis = {
|
|
name: string
|
|
ok: boolean
|
|
path?: string
|
|
url?: string
|
|
size?: number
|
|
replaced?: boolean
|
|
error?: string
|
|
}
|
|
|
|
export type ThumbLauf = {
|
|
erzeugt: number
|
|
vorhanden: number
|
|
uebersprungen: number
|
|
fehler: number
|
|
gesamt: number
|
|
/** Wieviele noch fehlen. Siehe WebpLauf — derselbe Grund. */
|
|
offen: number
|
|
fertig: boolean
|
|
/** Ton ohne eingebettetes Titelbild. KEIN Fehler — der Normalfall. */
|
|
ohneBild: number
|
|
}
|
|
|
|
/** Die Adresse der Vorschau zu einem Pfad.
|
|
*
|
|
* Ob es sie gibt, weiss nur das Dateisystem — deshalb faellt die Galerie bei
|
|
* 404 auf das Vollbild zurueck, statt hier zu fragen. */
|
|
export const vorschauUrl = (path: string) =>
|
|
'/t/' + path.split('/').map(encodeURIComponent).join('/')
|
|
|
|
export const api = {
|
|
login: (username: string, password: string) =>
|
|
post<{ user: User }>('/api/dash/auth/login', { username, password }),
|
|
logout: () => post<{ ok: true }>('/api/dash/auth/logout'),
|
|
/** Was die ANMELDESEITE wissen darf, bevor jemand angemeldet ist. */
|
|
authConfig: () => get<{ discord: boolean }>('/api/dash/auth/config'),
|
|
me: () => get<{ user: User }>('/api/dash/auth/me'),
|
|
changePassword: (current: string, next: string) =>
|
|
post<{ ok: true }>('/api/dash/auth/password', { current, next }),
|
|
|
|
media: (
|
|
query: string,
|
|
folder: string,
|
|
art: Art,
|
|
limit: number,
|
|
offset: number,
|
|
sort: Sortierung = 'geaendert',
|
|
dir: Richtung = 'ab',
|
|
) =>
|
|
get<MediaPage>(
|
|
`/api/dash/media?query=${encodeURIComponent(query)}` +
|
|
`&folder=${encodeURIComponent(folder)}&art=${art}` +
|
|
`&limit=${limit}&offset=${offset}&sort=${sort}&dir=${dir}`,
|
|
),
|
|
/** Nur die IDs — fuer "alle Treffer waehlen", ohne 15 Seiten zu blaettern.
|
|
* Nimmt DIESELBEN Einschraenkungen wie die Liste. */
|
|
medienIds: (query: string, folder: string, art: Art) =>
|
|
get<{ ids: number[]; gekappt: boolean }>(
|
|
`/api/dash/media/ids?query=${encodeURIComponent(query)}` +
|
|
`&folder=${encodeURIComponent(folder)}&art=${art}`,
|
|
),
|
|
arten: (query: string, folder: string) =>
|
|
get<Arten>(
|
|
`/api/dash/arten?query=${encodeURIComponent(query)}&folder=${encodeURIComponent(folder)}`,
|
|
),
|
|
folders: () => get<Folders>('/api/dash/folders'),
|
|
verschieben: (ids: number[], ziel: string) =>
|
|
post<VerschiebeErgebnis>('/api/dash/media/move', { ids, ziel }),
|
|
ordnerAnlegen: (pfad: string) => post<{ ok: true; pfad: string }>('/api/dash/folders', { pfad }),
|
|
ordnerEntfernen: (pfad: string) =>
|
|
del<{ ok: true }>('/api/dash/folders/' + pfad.split('/').map(encodeURIComponent).join('/')),
|
|
deleteMedia: (id: number) => del<{ deleted: string }>(`/api/dash/media/${id}`),
|
|
deleteMediaBulk: (ids: number[]) =>
|
|
post<{ deleted: string[]; count: number }>('/api/dash/media/delete', { ids }),
|
|
|
|
events: (
|
|
kind: string,
|
|
actor: string,
|
|
query: string,
|
|
limit: number,
|
|
offset: number,
|
|
) =>
|
|
get<EventsSeite>(
|
|
`/api/dash/events?kind=${encodeURIComponent(kind)}` +
|
|
`&actor=${encodeURIComponent(actor)}` +
|
|
`&query=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}`,
|
|
),
|
|
|
|
einstellungen: () => get<Einstellungen>('/api/dash/settings'),
|
|
sicherungSpeichern: (sicherung: Record<string, string | number>) =>
|
|
post<{ ok: true; bereit: boolean; fehlt: string[] }>('/api/dash/settings', { sicherung }),
|
|
meldungenSpeichern: (meldungen: { webhook?: string; anlaesse?: string[] }) =>
|
|
post<{ ok: true; meldungen: { hatWebhook: boolean; anlaesse: string[] } }>(
|
|
'/api/dash/settings',
|
|
{ meldungen },
|
|
),
|
|
meldenTesten: () => post<{ ok: true }>('/api/dash/maintenance/melden'),
|
|
vorlagenSpeichern: (liste: Vorlage[]) =>
|
|
post<{ ok: true; vorlagen: Vorlage[] }>('/api/dash/settings', { vorlagen: liste }),
|
|
einstellungenSpeichern: (discord: Record<string, string>) =>
|
|
post<{ ok: true; bereit: boolean; fehlt: string[] }>('/api/dash/settings', { discord }),
|
|
|
|
uebersicht: () => get<Uebersicht>('/api/dash/uebersicht'),
|
|
stand: () => get<Stand>('/api/dash/stand'),
|
|
speicher: () => get<Speicher>('/api/dash/speicher'),
|
|
papierkorb: () => get<PapierkorbSeite>('/api/dash/papierkorb'),
|
|
papierkorbZurueck: (ids: number[]) =>
|
|
post<{
|
|
zurueck: number
|
|
fehlgeschlagen: number
|
|
ergebnisse: { path: string; ok: boolean; error?: string }[]
|
|
}>('/api/dash/papierkorb/zurueck', { ids }),
|
|
papierkorbLeeren: (ids?: number[]) =>
|
|
post<{ geleert: number }>('/api/dash/papierkorb/leeren', ids ? { ids } : {}),
|
|
stats: () => get<Stats>('/api/dash/stats'),
|
|
thumbsNachziehen: () => post<ThumbLauf>('/api/dash/maintenance/thumbs'),
|
|
webpNachziehen: () => post<WebpLauf>('/api/dash/maintenance/webp'),
|
|
verwaiste: () => get<Verwaiste>('/api/dash/maintenance/orphans'),
|
|
verwaisteBehandeln: (aktion: 'aufnehmen' | 'aufraeumen') =>
|
|
post<{ aktion: string; aufgenommen?: number; datensaetze?: number; vorschauen?: number }>(
|
|
'/api/dash/maintenance/orphans',
|
|
{ aktion },
|
|
),
|
|
|
|
benutzer: () => get<{ items: BenutzerRow[]; ich: number }>('/api/dash/benutzer'),
|
|
benutzerWeg: (id: number) => del<{ ok: true; username: string }>(`/api/dash/benutzer/${id}`),
|
|
|
|
freigaben: () => get<{ items: FreigabeRow[] }>('/api/dash/freigaben'),
|
|
freigabeAnlegen: (ordner: string, titel: string) =>
|
|
post<{ id: number; schluessel: string; ordner: string; dateien: number; url: string }>(
|
|
'/api/dash/freigaben',
|
|
{ ordner, titel },
|
|
),
|
|
freigabeWeg: (id: number) => del<{ ok: true }>(`/api/dash/freigaben/${id}`),
|
|
|
|
tokens: () =>
|
|
get<{ items: TokenRow[]; ohneToken: { dateien: number; bytes: number } }>(
|
|
'/api/dash/tokens',
|
|
),
|
|
umbenennen: (id: number, name: string) =>
|
|
post<{ alt: string; neu: string; url: string; abrufe: number }>(
|
|
'/api/dash/media/umbenennen',
|
|
{ id, name },
|
|
),
|
|
ordnerUmbenennen: (pfad: string, name: string) =>
|
|
post<{ alt: string; neu: string; dateien: number }>('/api/dash/folders/umbenennen', {
|
|
pfad,
|
|
name,
|
|
}),
|
|
createToken: (daten: {
|
|
name: string
|
|
prefix: string
|
|
canDelete: boolean
|
|
maxMb?: string
|
|
quotaMb?: string
|
|
arten?: string[]
|
|
gueltigTage?: string
|
|
}) => post<NewToken>('/api/dash/tokens', daten),
|
|
deleteToken: (id: number) => del<{ ok: true }>(`/api/dash/tokens/${id}`),
|
|
}
|