From 0896720bc03c29ebcdb5a3717396def46f822a90 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 1 Sep 2026 15:27:12 +0200 Subject: [PATCH] Create work directories when settings are saved --- CHANGELOG.md | 5 ++ PROJECT_MEMORY.md | 13 +++- src/main/app-controller.ts | 52 +++++++++++----- src/renderer/i18n.ts | 2 +- src/renderer/views/settings/settings-model.ts | 2 +- tests/app-controller.test.ts | 61 +++++++++++++++++++ tests/i18n.test.ts | 2 + tests/settings-view.test.tsx | 3 +- 8 files changed, 121 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee23a12..323a91d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to Multi-Debrid Downloader are documented in this file. ## [Unreleased] +### Work folders + +- Create missing configured work folders immediately when the enabled startup-folder setting is saved, without requiring an application restart. +- Preserve existing contents and continue isolating an unavailable target from the remaining configured folders. + ## [2.0.83] - 2026-09-01 ### Bounded CRC recovery diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 8ce1938..bc0073f 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -10,8 +10,8 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält - Verifiziert am: 1. September 2026, Europe/Berlin - Lokaler Pfad: `C:\Users\Sascha\Desktop\Claude & ChatGPT Projekte\Multi-Debrid-Downloader` -- Arbeitsbranch: `release/v2.0.83` -- Quellbasis: `release/v2.0.82` +- Arbeitsbranch: `fix/work-directories-on-save` +- Quellbasis: `release/v2.0.83` - Release-Tag: `v2.0.83` - Baseline-Commit: `f3f2f60ece9322bfaddeb8f47d07830bb8fe273a` - Hotfix-Basis: `f3f2f60ece9322bfaddeb8f47d07830bb8fe273a` @@ -194,6 +194,12 @@ Diese Datei hält den verifizierten technischen Arbeitsstand fest. Sie enthält - Der Einmal-Schutz wird pro Item in der Session gespeichert und überlebt einen Programmneustart. Liefert der Upstream erneut einen CRC-Fehler, bleibt der zweite Fehler sichtbar, statt eine Endlosschleife zu erzeugen. Ein manueller Item- oder Paket-Reset löscht den Schutz und erlaubt einen neuen bewussten Versuch. - Echte Passwortfehler bei Dateien mit korrekter Größe und gültiger Archivsignatur lösen weiterhin keinen unnötigen Neudownload aus. Die Entpack-, Passwort- und Fallbacklogik selbst wurde nicht verändert. +## Unveröffentlichte Änderungen + +- Ist „Fehlende Arbeitsordner beim Start anlegen“ aktiviert, werden fehlende konfigurierte Arbeitsordner nun bereits unmittelbar beim Speichern angelegt; ein Neustart ist dafür nicht mehr erforderlich. +- Derselbe bestehende Ordnerpfad bleibt für Programmstart und Speichern zuständig. Optionale Ziele, vorhandene Inhalte und die Fehlerisolation zwischen mehreren Zielpfaden behalten ihr bisheriges Verhalten. +- Download-, Entpack-, Passwort- und Nachbearbeitungslogik wurden nicht verändert. + ## Start-, Build- und Testbefehle ```powershell @@ -225,6 +231,8 @@ npm exec -- tsc --noEmit ## Verifizierungen vom 1. September 2026 +- Unveröffentlichte Sofortanlage der Arbeitsordner: 344 von 344 fokussierten AppController-, Ordner-, Einstellungsansichts- und Übersetzungstests erfolgreich. Abgedeckt sind die unmittelbare Anlage aller aktivierten Ziele beim Speichern, ein erneuter unveränderter Speichervorgang bei bereits aktiver Option und das Ausbleiben der Ordneranlage, wenn die Einstellungen nicht persistiert werden können. TypeScript, Main-Build, Renderer-Produktionsbuild und Node-Self-Check sind erfolgreich; die bekannte Vite-Warnung betrifft den rund 581 KiB großen Renderer-Chunk. +- Vollständiger Clientlauf nach der Sofortanlage: 143 Testdateien erfolgreich, 1 optionale JVM-Testdatei übersprungen; 2.728 Tests erfolgreich und 4 übersprungen. Ausschließlich die zwei bekannten Windows-Symlink-Fixtures scheiterten mangels Berechtigung vor ihrer Produktassertion mit `EPERM`. Alle 265 Download-Manager- und 84 Extractor-Tests sind weiterhin vollständig grün. - Kandidat für `v2.0.83`: 392 von 392 fokussierten Download-Manager- und Storage-Tests erfolgreich. Die neuen Regressionen beweisen den einmaligen Neudownload bei CRC trotz gültiger RAR-Signatur, den persistenten Schutz vor einem zweiten automatischen Neudownload, die unveränderte Passwortfehler-Abgrenzung sowie das Löschen des Schutzes beim manuellen Reset. - Vollständiger Clientlauf des Kandidaten: 143 Testdateien erfolgreich, 1 optionale JVM-Testdatei übersprungen; 2.725 Tests erfolgreich und 4 übersprungen. Nur die zwei bekannten Windows-Symlink-Fixtures endeten mangels Berechtigung vor ihrer Produktassertion mit `EPERM`. Alle 265 Download-Manager- und 84 Extractor-Tests sind vollständig grün. - TypeScript-Prüfung, Main-Build, Renderer-Produktionsbuild, Node-Self-Check und alle 16 Backup-API-Tests sind erfolgreich. Die bekannte Vite-Warnung betrifft weiterhin den rund 581 KiB großen Renderer-Chunk. @@ -335,6 +343,7 @@ npm exec -- tsc --noEmit - Die Account-Übersicht unterstützt `Strg+A`; Proxy-only-Accountfehler werden verständlich aufgelöst und Online-Sicherungen können die Proxy-Liste verschlüsselt portieren. `v2.0.81` ist auf GitHub und Forgejo veröffentlicht. Eine produktive Installation oder ein Neustart wurde nicht vorgenommen. - Die optionale Arbeitsordner-Erstellung, der neue Kategoriename und die feinere TiB-Restanzeige sind als `v2.0.82` auf GitHub und Forgejo veröffentlicht. Eine produktive Installation oder ein Neustart ist nicht Bestandteil des Releases. - Die einmalige CRC-Neudownload-Wiederherstellung ist als `v2.0.83` auf GitHub und Forgejo veröffentlicht. Entpack-, Passwort- und Extractor-Fallbacklogik blieben unverändert; eine produktive Installation oder ein Neustart ist nicht Bestandteil des Releases. +- Die unmittelbare Arbeitsordner-Anlage beim Speichern ist auf `fix/work-directories-on-save` implementiert und verifiziert, aber noch nicht veröffentlicht. ## Nächste sinnvolle Schritte diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 97b452f..dfb626d 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -102,6 +102,19 @@ function settingsFingerprint(settings: AppSettings): string { return JSON.stringify(normalizeSettings(settings)); } +const WORK_DIRECTORY_SETTING_KEYS = new Set([ + "createWorkDirectoriesOnStartup", + "outputDir", + "autoExtract", + "extractDir", + "collectMkvToLibrary", + "mkvLibraryDir" +]); + +function touchesWorkDirectorySettings(partial: Partial): boolean { + return Object.keys(partial).some((key) => WORK_DIRECTORY_SETTING_KEYS.has(key as keyof AppSettings)); +} + type PendingRealDebridWebAccount = { generation: number; dailyLimitBytes: number; @@ -174,13 +187,7 @@ export class AppController { } this.applyNetworkProxyConfiguration(); this.initializeLogStorage(); - const workDirectories = ensureStartupWorkDirectories(this.settings); - if (workDirectories.created.length > 0) { - logger.info(`Arbeitsordner beim Start angelegt: ${workDirectories.created.map((entry) => entry.kind).join(", ")}`); - } - for (const failure of workDirectories.failures) { - logger.warn(`Arbeitsordner konnte beim Start nicht angelegt werden (${failure.kind}): ${failure.error}`); - } + this.ensureConfiguredWorkDirectories(this.settings, "beim Start"); this.runHistoryLifecycleCleanup("Start", () => resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode)); const loadResult = loadSessionWithStatus(this.storagePaths); const session = loadResult.session; @@ -702,17 +709,31 @@ export class AppController { return { proxyListRestored, proxyOnlyDisabled }; } + private ensureConfiguredWorkDirectories(settings: AppSettings, trigger: "beim Start" | "nach dem Speichern"): void { + const workDirectories = ensureStartupWorkDirectories(settings); + if (workDirectories.created.length > 0) { + logger.info(`Arbeitsordner ${trigger} angelegt: ${workDirectories.created.map((entry) => entry.kind).join(", ")}`); + } + for (const failure of workDirectories.failures) { + logger.warn(`Arbeitsordner konnte ${trigger} nicht angelegt werden (${failure.kind}): ${failure.error}`); + } + } + public updateSettings(partial: Partial): AppSettings { - const sanitizedPatch = sanitizeSettingsPatch(partial); - const previousSettings = this.settings; + const sanitizedPatch = sanitizeSettingsPatch(partial); + const previousSettings = this.settings; + const ensureWorkDirectoriesAfterSave = touchesWorkDirectorySettings(sanitizedPatch); let nextSettings = normalizeSettings({ ...previousSettings, ...sanitizedPatch }); - - if (settingsFingerprint(nextSettings) === settingsFingerprint(previousSettings)) { - return previousSettings; - } + + if (settingsFingerprint(nextSettings) === settingsFingerprint(previousSettings)) { + if (ensureWorkDirectoriesAfterSave) { + this.ensureConfiguredWorkDirectories(previousSettings, "nach dem Speichern"); + } + return previousSettings; + } if (previousSettings.logStorageLocation !== nextSettings.logStorageLocation && !this.reconfigureLogStorage(nextSettings.logStorageLocation)) { @@ -743,7 +764,10 @@ export class AppController { this.settings = nextSettings; this.applyNetworkProxyConfiguration(); this.manager.setSettings(this.settings); - this.audit("INFO", "Einstellungen aktualisiert", { + if (ensureWorkDirectoriesAfterSave) { + this.ensureConfiguredWorkDirectories(this.settings, "nach dem Speichern"); + } + this.audit("INFO", "Einstellungen aktualisiert", { changedKeys: Object.keys(sanitizedPatch), accountChanges: diffAccountSummary(previousSettings, this.settings) }); diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index b121cc9..49e0f24 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -10,7 +10,7 @@ const pairs = [ ["Sprache", "Language"], ["Speicherort", "Storage location"], ["Download-Verhalten", "Download behavior"], ["Oberfläche und Bedienung", "Interface and controls"], ["Discord-Benachrichtigungen", "Discord notifications"], ["Speicherort, Download-Verhalten, Verlauf, Oberfläche und Benachrichtigungen.", "Storage location, download behavior, history, interface and notifications."], ["Hell", "Light"], ["Dunkel", "Dark"], ["Download-Ordner", "Download folder"], ["Paketname (optional)", "Package name (optional)"], ["Max. gleichzeitige Downloads", "Max. concurrent downloads"], ["Automatische Wiederholungen", "Automatic retries"], - ["Zielordner für heruntergeladene Dateien.", "Destination folder for downloaded files."], ["Fehlende Arbeitsordner beim Start anlegen", "Create missing work folders on startup"], ["Erstellt den Download-Ordner sowie bei aktiver Funktion den Entpack- und Videosammelordner neu. Vorhandene Ordner und Inhalte bleiben unverändert.", "Creates the download folder and, when enabled, the extraction and video library folders. Existing folders and contents remain unchanged."], + ["Zielordner für heruntergeladene Dateien.", "Destination folder for downloaded files."], ["Fehlende Arbeitsordner beim Start anlegen", "Create missing work folders on startup"], ["Erstellt fehlende Download-, Entpack- und Videosammelordner sofort beim Speichern sowie bei jedem Programmstart. Optionale Ordner werden nur bei aktiver Funktion angelegt; vorhandene Inhalte bleiben unverändert.", "Creates missing download, extraction, and video library folders immediately when settings are saved and on every application start. Optional folders are created only when their feature is enabled; existing contents remain unchanged."], ["Beim Start automatisch fortsetzen", "Resume automatically on startup"], ["Zwischenablage überwachen", "Monitor clipboard"], ["Verlauf speichern", "Save history"], ["Nur aktuelle Session", "Current session only"], ["Nur letzte 100 Einträge", "Last 100 entries only"], ["Nur letzte 250 Einträge", "Last 250 entries only"], ["Dauerhaft", "Permanent"], ["Maximale Verlauf-Einträge", "Maximum history entries"], ["Einträge löschen älter als (Tage)", "Delete entries older than (days)"], ["Neue Pakete eingeklappt zeigen", "Show new packages collapsed"], ["Animationen", "Animations"], ["In den Infobereich minimieren", "Minimize to tray"], ["Vor dem Löschen nachfragen", "Confirm before deleting"], ["Download-Liste mitsichern", "Include download list in backup"], diff --git a/src/renderer/views/settings/settings-model.ts b/src/renderer/views/settings/settings-model.ts index 9fe0f32..71426d4 100644 --- a/src/renderer/views/settings/settings-model.ts +++ b/src/renderer/views/settings/settings-model.ts @@ -588,7 +588,7 @@ export function buildSettingsFormViewModel({ kind: "switch", label: "Fehlende Arbeitsordner beim Start anlegen", value: settings.createWorkDirectoriesOnStartup, - help: "Erstellt den Download-Ordner sowie bei aktiver Funktion den Entpack- und Videosammelordner neu. Vorhandene Ordner und Inhalte bleiben unverändert." + help: "Erstellt fehlende Download-, Entpack- und Videosammelordner sofort beim Speichern sowie bei jedem Programmstart. Optionale Ordner werden nur bei aktiver Funktion angelegt; vorhandene Inhalte bleiben unverändert." }, { id: "packageName", kind: "text", label: "Paketname (optional)", value: settings.packageName }, { diff --git a/tests/app-controller.test.ts b/tests/app-controller.test.ts index 43ecd81..368eee2 100644 --- a/tests/app-controller.test.ts +++ b/tests/app-controller.test.ts @@ -735,6 +735,67 @@ describe("AppController history retention updates", () => { }); }); +describe("AppController work directory settings updates", () => { + it("creates all enabled missing work directories immediately after saving the option", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-work-dirs-")); + tempDirs.push(root); + const outputDir = path.join(root, "downloads"); + const extractDir = path.join(root, "extracted"); + const mkvLibraryDir = path.join(root, "videos"); + const controller = createController({ + ...defaultSettings(), + outputDir, + extractDir, + mkvLibraryDir, + autoExtract: true, + collectMkvToLibrary: true, + createWorkDirectoriesOnStartup: false + }); + + controller.updateSettings({ createWorkDirectoriesOnStartup: true }); + + expect(fs.statSync(outputDir).isDirectory()).toBe(true); + expect(fs.statSync(extractDir).isDirectory()).toBe(true); + expect(fs.statSync(mkvLibraryDir).isDirectory()).toBe(true); + }); + + it("recreates a missing directory when enabled work-directory settings are saved unchanged", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-work-dirs-")); + tempDirs.push(root); + const outputDir = path.join(root, "downloads"); + const controller = createController({ + ...defaultSettings(), + outputDir, + autoExtract: false, + collectMkvToLibrary: false, + createWorkDirectoriesOnStartup: true + }); + + controller.updateSettings({ createWorkDirectoriesOnStartup: true }); + + expect(fs.statSync(outputDir).isDirectory()).toBe(true); + expect((controller as any).manager.setSettings).not.toHaveBeenCalled(); + }); + + it("does not create directories when the settings cannot be persisted", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-work-dirs-")); + tempDirs.push(root); + const outputDir = path.join(root, "downloads"); + const controller = createController({ + ...defaultSettings(), + outputDir, + createWorkDirectoriesOnStartup: false + }); + bootStorage.settingsSaveError = new Error("settings work directories locked"); + + expect(() => controller.updateSettings({ createWorkDirectoriesOnStartup: true })) + .toThrow("settings work directories locked"); + + expect(fs.existsSync(outputDir)).toBe(false); + expect(controller.getSettings().createWorkDirectoriesOnStartup).toBe(false); + }); +}); + describe("AppController provider quota reset statistics", () => { it("persists quota settings before rebasing statistics and applying runtime settings", () => { const settings = { diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 84555a2..477f1f4 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -20,6 +20,8 @@ describe("renderer localization", () => { expect(translateUiText("Animations", "de")).toBe("Animationen"); expect(translateUiText("Geschwindigkeit & Proxy", "en")).toBe("Speed & Proxy"); expect(translateUiText("Fehlende Arbeitsordner beim Start anlegen", "en")).toBe("Create missing work folders on startup"); + expect(translateUiText("Erstellt fehlende Download-, Entpack- und Videosammelordner sofort beim Speichern sowie bei jedem Programmstart. Optionale Ordner werden nur bei aktiver Funktion angelegt; vorhandene Inhalte bleiben unverändert.", "en")) + .toBe("Creates missing download, extraction, and video library folders immediately when settings are saved and on every application start. Optional folders are created only when their feature is enabled; existing contents remain unchanged."); }); it.each([ diff --git a/tests/settings-view.test.tsx b/tests/settings-view.test.tsx index aaed55b..a6cb722 100644 --- a/tests/settings-view.test.tsx +++ b/tests/settings-view.test.tsx @@ -655,7 +655,8 @@ describe("settings views", () => { expect(field).toEqual(expect.objectContaining({ kind: "switch", label: "Fehlende Arbeitsordner beim Start anlegen", - value: false + value: false, + help: "Erstellt fehlende Download-, Entpack- und Videosammelordner sofort beim Speichern sowie bei jedem Programmstart. Optionale Ordner werden nur bei aktiver Funktion angelegt; vorhandene Inhalte bleiben unverändert." })); });