DER NAME KAM VIERMAL FEST AUS DEM QUELLTEXT: Kopfzeile, Anmeldeseite, Fusszeile
einer Freigabe, Tab-Titel -- und zwar als "d4rk_media", was der Name des
Repositorys ist und nicht der des Dienstes. Wer ihn aendern wollte, musste vier
Stellen finden und eine davon uebersehen.
Er kommt jetzt vom Server, aus der Einstellung marke.name -- DERSELBEN, die
schon in der Fusszeile der Discord-Meldungen steht ("D4RKST3R // MEDIA"). Ein
zweiter Name fuer die Oberflaeche waere ein zweiter, der beim naechsten Mal
anders lautet als der erste.
Ausgeliefert wird er ueber /api/dash/auth/config (oeffentlich, die
Anmeldeseite hat noch keine Sitzung) und in der Freigabe-Antwort (die Seite ist
oeffentlich und holt sonst nichts, was ihn kennen koennte). Angezeigt wird
"<marke> Media" -- die Marke allein waere falsch, sie steht ueber allem, was
hier laeuft.
EINE STELLE BLEIBT ABSICHTLICH: der Schluessel im Speicher des Browsers
(merker.ts) heisst weiter "d4rk_media.". Wer ihn umbenennt, wirft still alle
gemerkten Ansichtseinstellungen weg, und niemand versteht, warum die Galerie
ploetzlich wieder bei 60 Kacheln steht. Steht als Kommentar daneben.
---
UND DER NEUE NAME HAENGT HINTER CLOUDFLARE, was die WebP-Auslieferung bricht.
NPM ist sauber, sogar besser als vorgeschlagen: beide Namen auf DEMSELBEN Host,
die drei ^~-Bloecke gelten damit fuer beide. Der DNS-Eintrag geht aber ueber
Cloudflare (188.114.97.3, Server: cloudflare), waehrend fivemanage direkt auf
88.218.224.10 zeigt.
Gemessen, dieselbe Datei:
fivemanage (direkt) ohne Accept -> image/png 12295 B richtig
mit Accept -> image/webp 2270 B richtig
media (Cloudflare) 1. mit Accept -> image/webp 2270 B MISS
2. mit Accept -> image/webp 2270 B HIT
3. OHNE Accept -> image/webp 2270 B HIT FALSCH
Und andersherum genauso -- wer zuerst kommt, praegt den Zwischenspeicher fuer
alle. Es sind unsere eigenen Bytes (2270 = unsere WebP-Fassung), also kein
Polish, sondern ein Cache-Schluessel ohne Accept. Vary: Accept wird an den
Client durchgereicht, aber nicht zum Unterscheiden benutzt.
Fuer ein Lua-Skript heisst das: items/foo.png liefert WebP-Bytes unter einem
.png-Namen, und das faellt erst auf, wenn irgendwo ein Bild nicht laedt.
PUBLIC_URL ist deshalb NICHT umgestellt. Genau davor stand die Warnung im
letzten Commit, und sie war keine Vorsicht, sondern eine Vorhersage.
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; marke: string }>('/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}`),
|
|
}
|