Der Kratzplatz räumt sich auf
39,1 GB in FLARO_TRANSCODE, und das war kein Normalzustand, sondern ein
Leck: für jede Mitnahme baut der Server eine MP4 in Quellgrösse und löschte
sie nie. Die einzige Löschung, die es gab, räumte ältere Fassungen
derselben Datei weg. Stromordner verschwanden nur beim geordneten
Herunterfahren, Untertitel gar nicht.
**Zuerst: fertig heisst fertig.** ffmpeg schrieb direkt auf den Endnamen.
Nach einem harten Abbruch lag dort eine abgeschnittene Datei, `IsReady`
meldete „fertig", und sie ging so hinaus — und ein Aufräumer konnte
vollständig und abgeschnitten nicht unterscheiden. Gebaut wird jetzt unter
`.teil`, umbenannt erst nach Erfolg. Dabei muss das Format ausdrücklich
gesetzt werden: ffmpeg wählt den Muxer an der Endung und antwortet auf
`.teil` mit „Unable to choose an output format". Das hätte jedes Umpacken
zerlegt; die Prüfung hat es gefunden, bevor es jemand zu spüren bekam.
**Zwei Stufen für die Pakete.** Verwaiste gehen bedingungslos: unleserlicher
Name, fremde Bauart, kein Eintrag in der Mediathek, oder der Zeitstempel im
Namen passt nicht mehr. Der Rest fällt unter Obergrenze (20 GB) und Alter
(7 Tage), Ältestes zuerst.
Beides, und nicht eines: ein Alter allein löst den Fall nicht, denn die 39
GB waren gültige, aktuelle Dateien, jede für sich berechtigt. Eine
Obergrenze allein ist zu grob — ein Topf knapp darunter, in dem seit Wochen
nichts gebraucht wurde, besetzt ein Volume, das anderes braucht.
„Zuletzt gebraucht" ist die Änderungszeit der Paketdatei selbst, bei jeder
Auslieferung neu gestempelt. Die Zugriffszeit wäre das Naheliegende, nur
führt sie nicht jedes Dateisystem — mit `noatime` steht sie still, und der
Aufräumer hielte alles für uralt.
Ein laufender Download wird nie gelöscht: ein Benutzungszähler hält die
Datei fest, solange die Antwort läuft, dazu eine Schonfrist von 30 Minuten.
Der Zähler ist auch unter Linux nötig — der laufende Strom überlebt ein
Entfernen, die Fortsetzung einer Bereichsanfrage nicht, und der Client finge
von vorn an.
**Stromordner** werden beim Start bedingungslos geleert — nach einem Start
gibt es keine einzige Sitzung, also ist jeder Ordner verwaist. Im Betrieb
nur die ohne passende Sitzung, deren Änderungszeit älter ist als die
Geduld; das fängt auch den Fall, dass `Abraeumen` an einem IOException
scheitert und der Ordner für immer liegenbleibt.
**Untertitel** nur, wenn sie verwaist sind. Keine Obergrenze: eine .vtt ist
Kilobytes, und eine gültige wird wieder gebraucht — sie nach Platz zu
löschen hiesse, Minuten Lesearbeit gegen Megabytes zu tauschen. Eine leere
bleibt ausdrücklich liegen: sie ist das Ergebnis „diese Spur hat keinen
Text", und wer sie wegräumt, lässt bei jedem Start eine vierzig Gigabyte
grosse Datei neu durchlesen.
**Vier Anlässe, kein Dienst.** Start, vor jedem Umpacken, nach einem Scan,
auf Knopfdruck — wie alles Wiederkehrende in diesem Server. Einer zur Zeit;
wer nicht hineinkommt, geht weiter.
Gemessen am laufenden Server mit Attrappen:
vorher packages 17408 KB (4), untertitel 5 KB (3), 1 Stromordner
nachher 14 MB freigegeben, 4 Dateien
gültiges Paket blieb, verwaistes/veraltetes/Teilstück weg,
verwaiste .vtt weg, gültige blieben, Stromordner weg
Obergrenze 1 MB, Datei in der Schonfrist -> bleibt
Obergrenze 1 MB, Schonfrist vorbei -> 3072 KB freigegeben
In der Übersicht steht die Belegung je Unterordner mit Dateizahl und
daneben „Jetzt aufräumen"; die drei Zahlen stehen unter Wiedergabe, wo
Transcoder-Stufe und Schwellen schon stehen — der Kratzplatz ist, was die
Wiedergabe hinterlässt.
714 Tests grün, davon 36 neue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1b4784f1db
commit
c4af182d31
@@ -1016,11 +1016,35 @@ export const statistik = (wochen = 12) =>
|
||||
export type Speicherort = {
|
||||
name: string; variable: string; pfad: string;
|
||||
belegt: number; frei: number | null; bilder: number | null;
|
||||
/** Nur beim Kratzplatz: die Unterordner einzeln. */
|
||||
teile: { name: string; frei: number; dateien: number }[] | null;
|
||||
};
|
||||
|
||||
/** Wo der Server seine Dateien ablegt und wie viel davon belegt ist. */
|
||||
export const speicherorte = () => anfrage<Speicherort[]>('/system/storage');
|
||||
|
||||
export type Aufraeumbericht = {
|
||||
frei: number; dateien: number;
|
||||
teile: { name: string; frei: number; dateien: number }[];
|
||||
};
|
||||
|
||||
/** Räumt den Kratzplatz auf — Verwaistes immer, der Rest nach der Obergrenze.
|
||||
*
|
||||
* POST und nicht DELETE wie beim Bildspeicher: dort wird ein Ordner geleert,
|
||||
* hier eine Regel angewandt. */
|
||||
export const kratzplatzAufraeumen = () =>
|
||||
anfrage<Aufraeumbericht>('/system/storage/transcode/cleanup', { method: 'POST' });
|
||||
|
||||
export type Wartungseinstellung = { capGb: number; unusedDays: number; graceMinutes: number };
|
||||
|
||||
export const wartungseinstellung = () =>
|
||||
anfrage<Wartungseinstellung>('/settings/maintenance');
|
||||
|
||||
export const wartungSetzen = (wunsch: Partial<Wartungseinstellung>) =>
|
||||
anfrage<Wartungseinstellung>('/settings/maintenance', {
|
||||
method: 'PUT', body: JSON.stringify(wunsch),
|
||||
});
|
||||
|
||||
/** Leert den Bildspeicher. Ungefährlich — Bilder werden bei Bedarf neu geholt. */
|
||||
export const bildspeicherLeeren = () =>
|
||||
anfrage<{ freed: number }>('/system/storage/images', { method: 'DELETE' });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
doppelteZusammenlegen, metadatenEinstellung, metadatenSetzen, servernameSetzen, aufmacherhoeheSetzen,
|
||||
systemauskunft, laufendeWiedergaben, serverzustand, bildAdresse, speicherorte,
|
||||
bildspeicherLeeren, type Wiedergabe,
|
||||
bildspeicherLeeren, kratzplatzAufraeumen, type Wiedergabe,
|
||||
} from '../../api';
|
||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../../util';
|
||||
import { dialog } from '../../dialog';
|
||||
@@ -296,6 +296,38 @@ export function speicherbereich(): HTMLElement {
|
||||
+ `<span class="winzig">Wird gesetzt über ${esc(o.variable)}.</span>`;
|
||||
oben.appendChild(text);
|
||||
|
||||
// **Beim Kratzplatz aufgeschlüsselt.** „39 GB belegt" sagt nicht, ob
|
||||
// das Segmente, umgepackte Filme oder Untertitel sind — und davon
|
||||
// hängt ab, ob es ein Problem ist. Die Dateizahl steht dabei, weil
|
||||
// bei den Untertiteln die Anzahl das Problem ist und nicht die Grösse.
|
||||
if (o.teile) {
|
||||
const auf = document.createElement('span');
|
||||
auf.className = 'winzig';
|
||||
auf.textContent = o.teile
|
||||
.map(t => `${t.name} ${groesse(t.frei)} (${t.dateien})`)
|
||||
.join(' · ');
|
||||
text.appendChild(auf);
|
||||
|
||||
const raeumen = knopf('Jetzt aufräumen', 'btn');
|
||||
raeumen.onclick = async () => {
|
||||
raeumen.disabled = true;
|
||||
try {
|
||||
const bericht = await kratzplatzAufraeumen();
|
||||
await laden();
|
||||
const gemeldet = document.createElement('p');
|
||||
gemeldet.className = 'hinweis';
|
||||
gemeldet.textContent = bericht.frei > 0
|
||||
? `${groesse(bericht.frei)} freigegeben (${bericht.dateien} Datei(en)).`
|
||||
: 'Es gab nichts wegzuräumen.';
|
||||
liste.prepend(gemeldet);
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile(meldung(fehler)));
|
||||
raeumen.disabled = false;
|
||||
}
|
||||
};
|
||||
oben.appendChild(raeumen);
|
||||
}
|
||||
|
||||
// Leeren gibt es nur dort, wo es gefahrlos ist.
|
||||
if (o.bilder !== null) {
|
||||
const leeren = knopf('Bildspeicher leeren', 'btn');
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { wiedergabeEinstellung, wiedergabeSetzen } from '../../api';
|
||||
import {
|
||||
wiedergabeEinstellung, wiedergabeSetzen, wartungseinstellung, wartungSetzen,
|
||||
} from '../../api';
|
||||
import { fehlerzeile, meldung, hinweisZeigen } from '../../util';
|
||||
import { dialog } from '../../dialog';
|
||||
|
||||
@@ -47,8 +49,9 @@ export function wiedergabebereich(): HTMLElement {
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
let stand;
|
||||
let wartung;
|
||||
try {
|
||||
stand = await wiedergabeEinstellung();
|
||||
[stand, wartung] = await Promise.all([wiedergabeEinstellung(), wartungseinstellung()]);
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
@@ -106,6 +109,55 @@ export function wiedergabebereich(): HTMLElement {
|
||||
wert: stand.encoder,
|
||||
setzen: v => wiedergabeSetzen({ encoder: v }),
|
||||
})));
|
||||
|
||||
/* ---------- Was die Wiedergabe auf der Platte hinterlässt ---------- */
|
||||
|
||||
// **Hier und nicht unter einem eigenen Punkt.** Der Kratzplatz ist, was
|
||||
// die Wiedergabe hinterlässt: umgepackte Dateien, Segmente, Untertitel.
|
||||
// Drei Felder rechtfertigen keine eigene Seite in der Leiste.
|
||||
liste.appendChild(abschnitt('Kratzplatz'));
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Obergrenze',
|
||||
wartung.capGb > 0 ? `${wartung.capGb} GB` : 'Unbegrenzt',
|
||||
'Wird sie überschritten, geht das am längsten Ungenutzte zuerst. '
|
||||
+ 'Was gerade heruntergeladen wird, bleibt in jedem Fall. 0 heisst unbegrenzt.',
|
||||
() => zahlFragen('Obergrenze', 'Gigabyte', String(wartung!.capGb),
|
||||
v => wartungSetzen({ capGb: Number(v) }))));
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Unbenutzt löschen nach',
|
||||
wartung.unusedDays > 0 ? `${wartung.unusedDays} Tagen` : 'Nie',
|
||||
'Gilt auch unterhalb der Obergrenze: ein Topf, in dem seit Wochen '
|
||||
+ 'nichts gebraucht wurde, besetzt ein Volume, das anderes braucht. '
|
||||
+ '0 heisst: nur die Obergrenze zählt.',
|
||||
() => zahlFragen('Unbenutzt löschen nach', 'Tagen', String(wartung!.unusedDays),
|
||||
v => wartungSetzen({ unusedDays: Number(v) }))));
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Schonfrist',
|
||||
`${wartung.graceMinutes} Minuten`,
|
||||
'So lange bleibt eine frisch gebrauchte Datei in jedem Fall liegen. '
|
||||
+ 'Weniger als fünf Minuten lässt der Server nicht zu — das löschte '
|
||||
+ 'eine Datei mitten im Herunterladen weg.',
|
||||
() => zahlFragen('Schonfrist', 'Minuten', String(wartung!.graceMinutes),
|
||||
v => wartungSetzen({ graceMinutes: Number(v) }))));
|
||||
}
|
||||
|
||||
/** Ein Dialog für eine einzelne Zahl. */
|
||||
function zahlFragen(
|
||||
titel: string, beschriftung: string, wert: string, setzen: (wert: string) => Promise<unknown>,
|
||||
): void {
|
||||
dialog({
|
||||
titel,
|
||||
felder: [{ name: 'wert', beschriftung, wert, art: 'number' }],
|
||||
bestaetigen: 'Übernehmen',
|
||||
tun: async lesen => {
|
||||
await setzen(lesen('wert'));
|
||||
hinweisZeigen('Übernommen.');
|
||||
await laden();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Ein Auswahldialog, der danach meldet, was er umgeworfen hat. */
|
||||
|
||||
Reference in New Issue
Block a user