UMBENENNEN gab es gar nicht -- verschieben ja, umbenennen nirgends. Jetzt fuer
Datei und Ordner. Die WARNUNG ist dabei der eigentliche Teil: beim Verschieben
wandert eine Datei, beim Umbenennen aendert sich IHRE ADRESSE, und die steht
womoeglich in einem Skript, das niemand mehr im Kopf hat. Der Dialog zeigt
deshalb die Abrufzahl und beim Ordner die Zahl der betroffenen Dateien, BEVOR
gedrueckt wird -- eine Datei mit viertausend Abrufen umzubenennen ist etwas
anderes als eine mit null, und der Dienst ist die einzige Stelle, die den
Unterschied kennt.
Am echten Fall geprueft, dem Kollisionsfund: items/coiloverss.png (hielt die
+-Fassung) -> items/coiloverss-plus.png, alte Adresse 404, neue 200 mit 4375 B,
Vorschau mitgewandert. Ordner: probe -> beispiele, 5 Dateien, alle Pfade in
einer Transaktion umgeschrieben.
Ein Fallstrick dabei: thumbPath haengt ".webp" an. Fuer eine Datei richtig, fuer
einen ORDNER Unsinn -- der Vorschauordner heisst thumbs/vehicles und nicht
thumbs/vehicles.webp. Dafuer gibt es jetzt moveFolder.
DIE 93 KOLLISIONEN SIND ERLEDIGT. tools/kollisionen.ps1 rechnet dieselbe
Zaehmung auf der Quelle nach und zeigt, was zusammenfaellt -- es aendert nichts,
damit die Entscheidung auf Zahlen steht. Ergebnis: 91 von 93 sind dieselbe Datei
zweimal (WEAPON_SMG.png und weapon_smg.png, byteweise gleich gross), da fehlte
nichts. Echt verloren waren ZWEI, beide mit + im Namen. Beide nachgeholt.
FIVEMANAGE-SPRACHE. POST /api/image|video|audio (v1) und POST /api/v3/file (v3),
Schluessel nackt im Authorization-Kopf. Auf einem laufenden Server stecken die
Fivemanage-Aufrufe in einem Dutzend fremder Ressourcen; sie alle umzuschreiben
tut niemand, und deshalb bliebe dieser Dienst ungenutzt daneben stehen. So ist
der Umzug eine Zeile je Skript: die Adresse.
Beide Formen sind AUS ECHTEM CODE abgelesen und nicht geraten -- fivemanage/sdk
fuer v3, Awleks/Devm-Camera fuer den aelteren Weg ueber screenshot-basic.
Gemessen: v1 -> {url,id,path}, v3 -> {status:"ok",data:{id,url}}, ohne
Schluessel 401, ein Video an /api/image -> 415 mit Grund.
Dabei bin ich in eine Falle gelaufen, vor der im eigenen Repo ein Kommentar
warnt: die Token-Wache hing an use('*'), und der Einhaengepunkt ist /api -- also
galt sie auch fuer /api/dash daneben. Das Dashboard bekam 401 auf die ANMELDUNG.
Wortwoertlich derselbe Fehler steht in upload.ts als Kommentar, weil er dort
schon einmal passiert ist. Gemerkt hat es der Gegentest, nicht der Kopf.
FREIGABE-LINKS. /s/<schluessel> zeigt einen Ordner ohne Anmeldung. Was dabei
ausdruecklich dabeisteht, in der Karte und in der Rueckfrage vor dem
Zurueckziehen: FREIGEGEBEN WIRD DIE LISTE, NICHT DER INHALT. Die Dateien sind
ohnehin oeffentlich; ein zurueckgezogener Link macht sie nicht wieder privat, er
nimmt nur die Uebersicht weg. Ohne diesen Satz zieht jemand einen Link zurueck
und glaubt, etwas sei verschwunden.
Die Antwort ist abgemessen: Name, Groesse, Art, Adresse, Laenge. NICHT Hash,
Token, Zeitpunkte, Abrufzahlen, IDs -- nichts davon braucht, wer einen Katalog
ansieht, und jedes davon waere eine Auskunft ueber den Betrieb. Ein unbekannter
Schluessel und ein zurueckgezogener geben dieselbe Antwort.
ZAHLEN, DIE SCHON DA WAREN. Sortieren nach Abrufen und nach "zuletzt geholt";
die Kachel zeigt dann auch diese Zahl statt Groesse und Datum, denn nach etwas
zu ordnen, das man nirgends sieht, ist eine Reihenfolge ohne Begruendung. Und je
Token, was damit abgelegt wurde: media.token_id wird seit dem ersten Tag
geschrieben und war NIRGENDS zu sehen. Gemessen: d4rk_photostudio haelt 810
Dateien / 99,5 MB, 3640 liegen ohne Token da.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
219 lines
8.7 KiB
TypeScript
219 lines
8.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { AlertTriangle, CheckCircle2, CloudUpload, Loader2, ShieldCheck, ShieldX } from 'lucide-react'
|
|
import { api, type Einstellungen as EinstellungenDaten } from '../api'
|
|
import { run, useStore } from '../store'
|
|
import { bytes, zeit } from '../format'
|
|
import { Feld, Karte, Knopf } from '../components/Bausteine'
|
|
import { DiscordKarte } from '../components/DiscordKarte'
|
|
import { VorlagenKarte } from '../components/VorlagenKarte'
|
|
import { FreigabenKarte } from '../components/FreigabenKarte'
|
|
import { MeldungenKarte } from '../components/MeldungenKarte'
|
|
|
|
/** Alles, was sich einstellen lässt, an einer Stelle.
|
|
*
|
|
* Bisher lag die Discord-Karte beim Konto und die Zugangsdaten der Sicherung
|
|
* in einer Textdatei auf der Platte. Beides ist jetzt hier — und das ist mehr
|
|
* als Aufräumen: eine Textdatei sagt niemandem, ob die Sicherung heute Nacht
|
|
* gelaufen ist. Diese Seite tut es. */
|
|
export function Einstellungen() {
|
|
const [urls, setUrls] = useState({ publicUrl: '', filePrefix: '' })
|
|
|
|
// Die Vorlagenkarte zeigt die FERTIGE Adresse und nicht nur das Muster —
|
|
// dafuer braucht sie den Ursprung, und der steht in der Uebersicht.
|
|
useEffect(() => {
|
|
api.uebersicht()
|
|
.then((u) => setUrls({ publicUrl: u.publicUrl, filePrefix: u.filePrefix }))
|
|
.catch(() => {})
|
|
}, [])
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<VorlagenKarte publicUrl={urls.publicUrl} filePrefix={urls.filePrefix} />
|
|
|
|
<FreigabenKarte />
|
|
<SicherungKarte />
|
|
<MeldungenKarte />
|
|
<DiscordKarte />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SicherungKarte() {
|
|
const note = useStore((s) => s.note)
|
|
const [daten, setDaten] = useState<EinstellungenDaten | null>(null)
|
|
const [laedt, setLaedt] = useState(true)
|
|
const [speichert, setSpeichert] = useState(false)
|
|
|
|
const [basis, setBasis] = useState('')
|
|
const [benutzer, setBenutzer] = useState('')
|
|
const [passwort, setPasswort] = useState('')
|
|
const [ordner, setOrdner] = useState('')
|
|
const [behalten, setBehalten] = useState('14')
|
|
|
|
async function laden() {
|
|
setLaedt(true)
|
|
try {
|
|
const e = await api.einstellungen()
|
|
setDaten(e)
|
|
setBasis(e.sicherung.basis)
|
|
setBenutzer(e.sicherung.benutzer)
|
|
setOrdner(e.sicherung.ordner)
|
|
setBehalten(String(e.sicherung.behalten))
|
|
} catch (err) {
|
|
note('fehler', err instanceof Error ? err.message : 'Unbekannt')
|
|
} finally {
|
|
setLaedt(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void laden()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
async function speichern() {
|
|
setSpeichert(true)
|
|
const ergebnis = await run(
|
|
() =>
|
|
api.sicherungSpeichern({
|
|
basis,
|
|
benutzer,
|
|
...(passwort ? { passwort } : {}),
|
|
ordner,
|
|
behalten: Number(behalten),
|
|
}),
|
|
(r) =>
|
|
r.bereit
|
|
? 'Gespeichert. Die nächste Sicherung geht in die Nextcloud.'
|
|
: `Gespeichert. Es fehlt noch: ${r.fehlt.join(', ')}.`,
|
|
)
|
|
setSpeichert(false)
|
|
if (ergebnis) {
|
|
setPasswort('')
|
|
void laden()
|
|
}
|
|
}
|
|
|
|
if (laedt) {
|
|
return (
|
|
<Karte titel="Sicherung">
|
|
<div className="flex justify-center py-6">
|
|
<Loader2 className="animate-spin text-schrift-leise" />
|
|
</div>
|
|
</Karte>
|
|
)
|
|
}
|
|
if (!daten) return null
|
|
|
|
const s = daten.sicherung
|
|
const l = s.letzte
|
|
|
|
return (
|
|
<Karte
|
|
titel="Sicherung in die Nextcloud"
|
|
rechts={
|
|
s.bereit ? (
|
|
<span className="flex items-center gap-1.5 text-xs text-gut">
|
|
<ShieldCheck size={14} /> eingerichtet
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1.5 text-xs text-schrift-leise">
|
|
<ShieldX size={14} /> nur lokal
|
|
</span>
|
|
)
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4">
|
|
{/* Das Wichtigste zuerst: LIEF sie? Eine Sicherung, die still
|
|
aufhoert zu laufen, ist der eigentliche Schaden. */}
|
|
<div
|
|
className={`flex items-start gap-2 rounded-md border px-3 py-2 text-sm ${
|
|
!l
|
|
? 'border-rand bg-grund text-schrift-leise'
|
|
: l.ok
|
|
? 'border-gut/40 bg-gut/10'
|
|
: 'border-schlecht/40 bg-schlecht/10'
|
|
}`}
|
|
>
|
|
{!l ? (
|
|
<AlertTriangle size={16} className="mt-0.5 shrink-0" />
|
|
) : l.ok ? (
|
|
<CheckCircle2 size={16} className="mt-0.5 shrink-0 text-gut" />
|
|
) : (
|
|
<AlertTriangle size={16} className="mt-0.5 shrink-0 text-schlecht" />
|
|
)}
|
|
<span className="text-schrift">
|
|
{!l ? (
|
|
'Noch keine Sicherung gemeldet. Sie läuft täglich um 04:30 als geplante Aufgabe.'
|
|
) : (
|
|
<>
|
|
Zuletzt {zeit(l.at)} — {l.ok ? 'erfolgreich' : 'FEHLGESCHLAGEN'}
|
|
{l.dateien != null && `, ${l.dateien} Dateien`}
|
|
{l.groesse != null && `, ${bytes(l.groesse)}`}
|
|
{l.hoch === true && ', in der Nextcloud'}
|
|
{l.hoch === false && ', nur lokal'}
|
|
{l.fehler && (
|
|
<span className="block text-xs text-schlecht">{l.fehler}</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<Feld
|
|
label="Nextcloud-Adresse"
|
|
value={basis}
|
|
placeholder="https://cdn.d4rkst3r.de"
|
|
onChange={(e) => setBasis(e.target.value)}
|
|
/>
|
|
<Feld
|
|
label="Benutzername"
|
|
value={benutzer}
|
|
placeholder="wie beim Anmelden in der Nextcloud"
|
|
onChange={(e) => setBenutzer(e.target.value)}
|
|
/>
|
|
<Feld
|
|
label="App-Passwort"
|
|
type="password"
|
|
value={passwort}
|
|
placeholder={s.hatPasswort ? '•••••••• (hinterlegt)' : 'noch keines'}
|
|
hinweis='leer lassen heißt: unverändert. Zum Entfernen ein „-" eintragen.'
|
|
onChange={(e) => setPasswort(e.target.value)}
|
|
/>
|
|
<Feld
|
|
label="Ordner"
|
|
value={ordner}
|
|
placeholder="Sicherungen/d4rk_media"
|
|
onChange={(e) => setOrdner(e.target.value)}
|
|
/>
|
|
<Feld
|
|
label="Wie viele behalten"
|
|
type="number"
|
|
min={1}
|
|
max={365}
|
|
value={behalten}
|
|
hinweis="gilt hier wie in der Nextcloud"
|
|
onChange={(e) => setBehalten(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<p className="flex items-start gap-2 text-xs text-schrift-leise">
|
|
<CloudUpload size={14} className="mt-0.5 shrink-0" />
|
|
<span>
|
|
Nimm ein <strong>App-Passwort</strong>, nicht dein echtes: Nextcloud →
|
|
Einstellungen → Sicherheit → „Neues App-Passwort erstellen". Es lässt
|
|
sich einzeln zurückziehen. Das Skript liest die Werte direkt aus der
|
|
Datenbank des Dienstes — über HTTP geht das Passwort nur hier beim
|
|
Eintragen, und das über TLS.
|
|
</span>
|
|
</p>
|
|
|
|
<div>
|
|
<Knopf art="haupt" wartet={speichert} onClick={speichern} kind="Speichern" />
|
|
</div>
|
|
</div>
|
|
</Karte>
|
|
)
|
|
}
|