fix(logs): make log storage location safe and configurable

Adds an AppData/Desktop log storage selector with controlled migration of known log files only. Keeps runtime data and credentials in AppData, consolidates desktop rename logs into the chosen directory, and exposes the active log folder through the desktop UI.\n\nFlushes early recovery diagnostics into the runtime log before a location migration, preserves the active trace configuration as valid JSON during moves, and prevents backup imports from failing when the requested log location is unavailable. Adds regression coverage for desktop paths, legacy log migration, secret exclusion, and trace-config replacement.
This commit is contained in:
Sucukdeluxe
2026-08-12 11:59:16 +02:00
parent a02ffe4623
commit 56ff988a7c
17 changed files with 513 additions and 114 deletions
+131 -48
View File
@@ -38,7 +38,7 @@ import { applyAccountCommand } from "./account-commands";
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus, sanitizeDebridAccountStatuses } from "./account-status-sanitizer";
import { createRendererState } from "./renderer-state";
import { parseCollectorInput } from "./link-parser";
import { configureLogger, getLogFilePath, logger } from "./logger";
import { configureLogger, flushLoggerSync, getLogFilePath, logger } from "./logger";
import { AllDebridWebFallback } from "./all-debrid-web";
import { BestDebridWebFallback } from "./bestdebrid-web";
import { RealDebridWebFallback } from "./realdebrid-web";
@@ -60,13 +60,14 @@ import { runStartupHealthCheck } from "./startup-health-check";
import { getDebugSetupCheck } from "./debug-setup";
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
import { getDesktopRenameLogPath, initDesktopRenameLog, shutdownDesktopRenameLog } from "./desktop-rename-log";
import { getDesktopRenameLogPath, initDesktopRenameLogAt, shutdownDesktopRenameLog } from "./desktop-rename-log";
import { buildAccountSummary, diffAccountSummary } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
import { createOnlineBackup, downloadOnlineBackup, uploadOnlineBackup } from "./online-backup";
import { overlayLiveUsageCounters } from "./settings-live-overlay";
import { getLegacyDesktopLogDirectory, migrateLogDirectories, prepareLogDirectory, resolveLogDirectory } from "./log-storage";
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
@@ -94,33 +95,38 @@ export class AppController {
private lastUpdateCheckAt = 0;
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
private logDirectory = this.storagePaths.baseDir;
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
private autoResumePending = false;
private runtimeStatsTimer: NodeJS.Timeout | null = null;
private lastMemoryWarnAt = 0;
public constructor() {
configureLogger(this.storagePaths.baseDir);
initSessionLog(this.storagePaths.baseDir);
initPackageLogs(this.storagePaths.baseDir);
initItemLogs(this.storagePaths.baseDir);
initAuditLog(this.storagePaths.baseDir);
initAccountRotationLog(this.storagePaths.baseDir);
initConversionLog(this.storagePaths.baseDir);
initRenameLog(this.storagePaths.baseDir);
let desktopDir: string | null = null;
try {
desktopDir = app.getPath("desktop");
} catch {
desktopDir = null;
}
initDesktopRenameLog(desktopDir);
initTraceLog(this.storagePaths.baseDir);
this.settings = loadSettings(this.storagePaths);
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
private lastMemoryWarnAt = 0;
public constructor() {
configureLogger(this.storagePaths.baseDir);
this.settings = loadSettings(this.storagePaths);
flushLoggerSync();
const desktopDir = this.getDesktopDirectory();
const requestedLogDirectory = resolveLogDirectory(this.storagePaths.baseDir, desktopDir, this.settings.logStorageLocation);
if (!prepareLogDirectory(requestedLogDirectory)) {
this.settings = normalizeSettings({ ...this.settings, logStorageLocation: "appdata" });
saveSettings(this.storagePaths, this.settings);
this.logDirectory = this.storagePaths.baseDir;
} else {
this.logDirectory = requestedLogDirectory;
const legacyDirectory = this.settings.logStorageLocation === "desktop"
? getLegacyDesktopLogDirectory(desktopDir)
: null;
migrateLogDirectories(
[this.storagePaths.baseDir, ...(legacyDirectory ? [legacyDirectory] : [])],
this.logDirectory
);
}
this.initializeLogStorage();
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
const loadResult = loadSessionWithStatus(this.storagePaths);
const session = loadResult.session;
this.megaWebFallback = new MegaWebFallback(() => ({
@@ -430,7 +436,14 @@ export class AppController {
}
private applySettingsOnlyBackup(importedSettings: AppSettings, remoteDiagnostics?: unknown, restoreRemoteDiagnostics = false): void {
const restoredSettings = normalizeSettings(importedSettings);
let restoredSettings = normalizeSettings(importedSettings);
if (this.settings.logStorageLocation !== restoredSettings.logStorageLocation
&& !this.reconfigureLogStorage(restoredSettings.logStorageLocation)) {
restoredSettings = normalizeSettings({
...restoredSettings,
logStorageLocation: this.settings.logStorageLocation
});
}
this.overlayLiveUsageCounters(restoredSettings);
this.settings = restoredSettings;
saveSettings(this.storagePaths, this.settings);
@@ -443,16 +456,23 @@ export class AppController {
public updateSettings(partial: Partial<AppSettings>): AppSettings {
const sanitizedPatch = sanitizeSettingsPatch(partial);
const previousSettings = this.settings;
const nextSettings = normalizeSettings({
...previousSettings,
...sanitizedPatch
});
let nextSettings = normalizeSettings({
...previousSettings,
...sanitizedPatch
});
if (settingsFingerprint(nextSettings) === settingsFingerprint(previousSettings)) {
return previousSettings;
}
this.overlayLiveUsageCounters(nextSettings);
if (previousSettings.logStorageLocation !== nextSettings.logStorageLocation
&& !this.reconfigureLogStorage(nextSettings.logStorageLocation)) {
nextSettings = normalizeSettings({
...nextSettings,
logStorageLocation: previousSettings.logStorageLocation
});
}
this.overlayLiveUsageCounters(nextSettings);
const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
const historyLimitsChanged = previousSettings.historyMaxEntries !== nextSettings.historyMaxEntries
|| previousSettings.historyMaxAgeDays !== nextSettings.historyMaxAgeDays;
@@ -889,7 +909,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
importedSettingsRecord[key] = currentSettingsRecord[key];
}
}
const restoredSettings = normalizeSettings(importedSettings);
let restoredSettings = normalizeSettings(importedSettings);
// Settings-only backup: keep the running queue AND the live counters untouched.
// Overlay the live usage/status counters so they don't roll back to the backup's
@@ -909,8 +929,15 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
};
}
this.settings = restoredSettings;
saveSettings(this.storagePaths, this.settings);
if (this.settings.logStorageLocation !== restoredSettings.logStorageLocation
&& !this.reconfigureLogStorage(restoredSettings.logStorageLocation)) {
restoredSettings = normalizeSettings({
...restoredSettings,
logStorageLocation: this.settings.logStorageLocation
});
}
this.settings = restoredSettings;
saveSettings(this.storagePaths, this.settings);
this.manager.setSettings(this.settings);
this.manager.stop();
@@ -947,9 +974,13 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
return { restored: true, relaunch: true, message: "Backup wiederhergestellt App startet automatisch neu…" };
}
public getSessionLogPath(): string | null {
return getSessionLogPath();
}
public getSessionLogPath(): string | null {
return getSessionLogPath();
}
public getLogDirectory(): string {
return this.logDirectory;
}
public getPackageLogPath(packageId: string): string | null {
return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId);
@@ -959,7 +990,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId);
}
public shutdown(): void {
public shutdown(): void {
if (this.runtimeStatsTimer) {
clearInterval(this.runtimeStatsTimer);
this.runtimeStatsTimer = null;
@@ -972,21 +1003,73 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
this.realDebridWebFallback.dispose();
this.allDebridWebFallback.dispose();
this.bestDebridWebFallback.dispose();
shutdownSessionLog();
shutdownPackageLogs();
shutdownItemLogs();
shutdownRenameLog();
shutdownDesktopRenameLog();
this.audit("INFO", "App beendet");
shutdownTraceLog();
shutdownAccountRotationLog();
shutdownConversionLog();
shutdownAuditLog();
this.shutdownLogStorage();
this.audit("INFO", "App beendet");
shutdownTraceLog();
shutdownAccountRotationLog();
shutdownConversionLog();
shutdownAuditLog();
if (this.settings.historyRetentionMode === "session") {
clearHistory(this.storagePaths);
}
logger.info("App beendet");
}
}
private getDesktopDirectory(): string | null {
try {
return app.getPath("desktop");
} catch {
return null;
}
}
private initializeLogStorage(): void {
configureLogger(this.logDirectory);
initSessionLog(this.logDirectory);
initPackageLogs(this.logDirectory);
initItemLogs(this.logDirectory);
initAuditLog(this.logDirectory);
initAccountRotationLog(this.logDirectory);
initConversionLog(this.logDirectory);
initRenameLog(this.logDirectory);
initDesktopRenameLogAt(this.logDirectory);
initTraceLog(this.logDirectory);
}
private shutdownLogStorage(): void {
flushLoggerSync();
shutdownSessionLog();
shutdownPackageLogs();
shutdownItemLogs();
shutdownRenameLog();
shutdownDesktopRenameLog();
}
private reconfigureLogStorage(location: AppSettings["logStorageLocation"]): boolean {
const nextDirectory = resolveLogDirectory(this.storagePaths.baseDir, this.getDesktopDirectory(), location);
if (nextDirectory === this.logDirectory) {
return true;
}
if (!prepareLogDirectory(nextDirectory)) {
return false;
}
this.shutdownLogStorage();
shutdownTraceLog();
shutdownAccountRotationLog();
shutdownConversionLog();
shutdownAuditLog();
const legacyDirectory = location === "desktop"
? getLegacyDesktopLogDirectory(this.getDesktopDirectory())
: null;
migrateLogDirectories(
[this.logDirectory, ...(legacyDirectory ? [legacyDirectory] : [])],
nextDirectory
);
this.logDirectory = nextDirectory;
this.initializeLogStorage();
logger.info(`Log-Speicherort geändert: ${this.logDirectory}`);
return true;
}
private historyLimits(): { maxEntries: number; maxAgeDays: number } {
return { maxEntries: this.settings.historyMaxEntries, maxAgeDays: this.settings.historyMaxAgeDays };