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 { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus, sanitizeDebridAccountStatuses } from "./account-status-sanitizer";
import { createRendererState } from "./renderer-state"; import { createRendererState } from "./renderer-state";
import { parseCollectorInput } from "./link-parser"; 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 { AllDebridWebFallback } from "./all-debrid-web";
import { BestDebridWebFallback } from "./bestdebrid-web"; import { BestDebridWebFallback } from "./bestdebrid-web";
import { RealDebridWebFallback } from "./realdebrid-web"; import { RealDebridWebFallback } from "./realdebrid-web";
@@ -60,13 +60,14 @@ import { runStartupHealthCheck } from "./startup-health-check";
import { getDebugSetupCheck } from "./debug-setup"; import { getDebugSetupCheck } from "./debug-setup";
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log"; 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 { buildAccountSummary, diffAccountSummary } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle"; import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log"; import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types"; import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
import { createOnlineBackup, downloadOnlineBackup, uploadOnlineBackup } from "./online-backup"; import { createOnlineBackup, downloadOnlineBackup, uploadOnlineBackup } from "./online-backup";
import { overlayLiveUsageCounters } from "./settings-live-overlay"; import { overlayLiveUsageCounters } from "./settings-live-overlay";
import { getLegacyDesktopLogDirectory, migrateLogDirectories, prepareLogDirectory, resolveLogDirectory } from "./log-storage";
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> { function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
@@ -94,33 +95,38 @@ export class AppController {
private lastUpdateCheckAt = 0; 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 onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
private autoResumePending = false; private autoResumePending = false;
private runtimeStatsTimer: NodeJS.Timeout | null = null; private runtimeStatsTimer: NodeJS.Timeout | null = null;
private lastMemoryWarnAt = 0; private lastMemoryWarnAt = 0;
public constructor() { public constructor() {
configureLogger(this.storagePaths.baseDir); configureLogger(this.storagePaths.baseDir);
initSessionLog(this.storagePaths.baseDir); this.settings = loadSettings(this.storagePaths);
initPackageLogs(this.storagePaths.baseDir); flushLoggerSync();
initItemLogs(this.storagePaths.baseDir); const desktopDir = this.getDesktopDirectory();
initAuditLog(this.storagePaths.baseDir); const requestedLogDirectory = resolveLogDirectory(this.storagePaths.baseDir, desktopDir, this.settings.logStorageLocation);
initAccountRotationLog(this.storagePaths.baseDir); if (!prepareLogDirectory(requestedLogDirectory)) {
initConversionLog(this.storagePaths.baseDir); this.settings = normalizeSettings({ ...this.settings, logStorageLocation: "appdata" });
initRenameLog(this.storagePaths.baseDir); saveSettings(this.storagePaths, this.settings);
let desktopDir: string | null = null; this.logDirectory = this.storagePaths.baseDir;
try { } else {
desktopDir = app.getPath("desktop"); this.logDirectory = requestedLogDirectory;
} catch { const legacyDirectory = this.settings.logStorageLocation === "desktop"
desktopDir = null; ? getLegacyDesktopLogDirectory(desktopDir)
} : null;
initDesktopRenameLog(desktopDir); migrateLogDirectories(
initTraceLog(this.storagePaths.baseDir); [this.storagePaths.baseDir, ...(legacyDirectory ? [legacyDirectory] : [])],
this.settings = loadSettings(this.storagePaths); this.logDirectory
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); );
}
this.initializeLogStorage();
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
const loadResult = loadSessionWithStatus(this.storagePaths); const loadResult = loadSessionWithStatus(this.storagePaths);
const session = loadResult.session; const session = loadResult.session;
this.megaWebFallback = new MegaWebFallback(() => ({ this.megaWebFallback = new MegaWebFallback(() => ({
@@ -430,7 +436,14 @@ export class AppController {
} }
private applySettingsOnlyBackup(importedSettings: AppSettings, remoteDiagnostics?: unknown, restoreRemoteDiagnostics = false): void { 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.overlayLiveUsageCounters(restoredSettings);
this.settings = restoredSettings; this.settings = restoredSettings;
saveSettings(this.storagePaths, this.settings); saveSettings(this.storagePaths, this.settings);
@@ -443,16 +456,23 @@ export class AppController {
public updateSettings(partial: Partial<AppSettings>): AppSettings { public updateSettings(partial: Partial<AppSettings>): AppSettings {
const sanitizedPatch = sanitizeSettingsPatch(partial); const sanitizedPatch = sanitizeSettingsPatch(partial);
const previousSettings = this.settings; const previousSettings = this.settings;
const nextSettings = normalizeSettings({ let nextSettings = normalizeSettings({
...previousSettings, ...previousSettings,
...sanitizedPatch ...sanitizedPatch
}); });
if (settingsFingerprint(nextSettings) === settingsFingerprint(previousSettings)) { if (settingsFingerprint(nextSettings) === settingsFingerprint(previousSettings)) {
return 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 retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
const historyLimitsChanged = previousSettings.historyMaxEntries !== nextSettings.historyMaxEntries const historyLimitsChanged = previousSettings.historyMaxEntries !== nextSettings.historyMaxEntries
|| previousSettings.historyMaxAgeDays !== nextSettings.historyMaxAgeDays; || previousSettings.historyMaxAgeDays !== nextSettings.historyMaxAgeDays;
@@ -889,7 +909,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
importedSettingsRecord[key] = currentSettingsRecord[key]; importedSettingsRecord[key] = currentSettingsRecord[key];
} }
} }
const restoredSettings = normalizeSettings(importedSettings); let restoredSettings = normalizeSettings(importedSettings);
// Settings-only backup: keep the running queue AND the live counters untouched. // 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 // 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; if (this.settings.logStorageLocation !== restoredSettings.logStorageLocation
saveSettings(this.storagePaths, this.settings); && !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.setSettings(this.settings);
this.manager.stop(); this.manager.stop();
@@ -947,9 +974,13 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
return { restored: true, relaunch: true, message: "Backup wiederhergestellt App startet automatisch neu…" }; return { restored: true, relaunch: true, message: "Backup wiederhergestellt App startet automatisch neu…" };
} }
public getSessionLogPath(): string | null { public getSessionLogPath(): string | null {
return getSessionLogPath(); return getSessionLogPath();
} }
public getLogDirectory(): string {
return this.logDirectory;
}
public getPackageLogPath(packageId: string): string | null { public getPackageLogPath(packageId: string): string | null {
return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId); return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId);
@@ -959,7 +990,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId); return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId);
} }
public shutdown(): void { public shutdown(): void {
if (this.runtimeStatsTimer) { if (this.runtimeStatsTimer) {
clearInterval(this.runtimeStatsTimer); clearInterval(this.runtimeStatsTimer);
this.runtimeStatsTimer = null; this.runtimeStatsTimer = null;
@@ -972,21 +1003,73 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
this.realDebridWebFallback.dispose(); this.realDebridWebFallback.dispose();
this.allDebridWebFallback.dispose(); this.allDebridWebFallback.dispose();
this.bestDebridWebFallback.dispose(); this.bestDebridWebFallback.dispose();
shutdownSessionLog(); this.shutdownLogStorage();
shutdownPackageLogs(); this.audit("INFO", "App beendet");
shutdownItemLogs(); shutdownTraceLog();
shutdownRenameLog(); shutdownAccountRotationLog();
shutdownDesktopRenameLog(); shutdownConversionLog();
this.audit("INFO", "App beendet"); shutdownAuditLog();
shutdownTraceLog();
shutdownAccountRotationLog();
shutdownConversionLog();
shutdownAuditLog();
if (this.settings.historyRetentionMode === "session") { if (this.settings.historyRetentionMode === "session") {
clearHistory(this.storagePaths); clearHistory(this.storagePaths);
} }
logger.info("App beendet"); 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 } { private historyLimits(): { maxEntries: number; maxAgeDays: number } {
return { maxEntries: this.settings.historyMaxEntries, maxAgeDays: this.settings.historyMaxAgeDays }; return { maxEntries: this.settings.historyMaxEntries, maxAgeDays: this.settings.historyMaxAgeDays };
+4 -3
View File
@@ -101,9 +101,10 @@ export function defaultSettings(): AppSettings {
updateRepo: DEFAULT_UPDATE_REPO, updateRepo: DEFAULT_UPDATE_REPO,
autoUpdateCheck: true, autoUpdateCheck: true,
clipboardWatch: false, clipboardWatch: false,
minimizeToTray: false, minimizeToTray: false,
theme: "dark" as const, theme: "dark" as const,
collapseNewPackages: true, logStorageLocation: "appdata",
collapseNewPackages: true,
historyRetentionMode: "permanent", historyRetentionMode: "permanent",
historyMaxEntries: 500, historyMaxEntries: 500,
historyMaxAgeDays: 0, historyMaxAgeDays: 0,
+21 -12
View File
@@ -4,7 +4,7 @@ import { logTimestamp } from "./log-timestamp";
type DesktopRenameLevel = "INFO" | "WARN" | "ERROR"; type DesktopRenameLevel = "INFO" | "WARN" | "ERROR";
const FOLDER_NAME = "Downloader-Log"; const LEGACY_FOLDER_NAME = "Downloader-Log";
let logDir: string | null = null; let logDir: string | null = null;
let logFilePath: string | null = null; let logFilePath: string | null = null;
@@ -58,15 +58,15 @@ function ensureWritable(): boolean {
} }
} }
export function initDesktopRenameLog(desktopDir: string | null | undefined): void { function initializeRenameLog(directory: string | null | undefined): void {
try { try {
const base = String(desktopDir || "").trim(); const base = String(directory || "").trim();
if (!base) { if (!base) {
logDir = null; logDir = null;
logFilePath = null; logFilePath = null;
return; return;
} }
logDir = path.join(base, FOLDER_NAME); logDir = path.resolve(base);
logFilePath = path.join(logDir, `rename-session_${fileTimestamp()}.txt`); logFilePath = path.join(logDir, `rename-session_${fileTimestamp()}.txt`);
sessionHeader = `=== Rename-Session gestartet: ${logTimestamp()} ===\n` sessionHeader = `=== Rename-Session gestartet: ${logTimestamp()} ===\n`
+ "Diese Datei protokolliert JEDEN Umbenenn-/Verschiebevorgang dieser Programm-Sitzung\n" + "Diese Datei protokolliert JEDEN Umbenenn-/Verschiebevorgang dieser Programm-Sitzung\n"
@@ -77,8 +77,17 @@ export function initDesktopRenameLog(desktopDir: string | null | undefined): voi
} catch { } catch {
logDir = null; logDir = null;
logFilePath = null; logFilePath = null;
} }
} }
export function initDesktopRenameLog(desktopDir: string | null | undefined): void {
const base = String(desktopDir || "").trim();
initializeRenameLog(base ? path.join(base, LEGACY_FOLDER_NAME) : null);
}
export function initDesktopRenameLogAt(directory: string | null | undefined): void {
initializeRenameLog(directory);
}
export function logDesktopRename(level: DesktopRenameLevel, message: string, fields?: Record<string, unknown>): void { export function logDesktopRename(level: DesktopRenameLevel, message: string, fields?: Record<string, unknown>): void {
if (!ensureWritable() || !logFilePath) { if (!ensureWritable() || !logFilePath) {
+170
View File
@@ -0,0 +1,170 @@
import fs from "node:fs";
import path from "node:path";
import type { LogStorageLocation } from "../shared/types";
export const DESKTOP_LOG_DIRECTORY_NAME = "Downloader Log";
export const LEGACY_DESKTOP_LOG_DIRECTORY_NAME = "Downloader-Log";
const LOG_FILE_NAMES = new Set([
"rd_downloader.log",
"audit.log",
"account-rotation.log",
"conversion.log",
"rename.log",
"trace.log",
"trace_config.json"
]);
const LOG_DIRECTORY_NAMES = new Set(["session-logs", "package-logs", "item-logs"]);
export interface LogMigrationResult {
copiedFiles: number;
skippedFiles: number;
}
function samePath(left: string, right: string): boolean {
const normalizedLeft = path.resolve(left);
const normalizedRight = path.resolve(right);
return process.platform === "win32"
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
: normalizedLeft === normalizedRight;
}
function mergeTextFile(sourcePath: string, targetPath: string): boolean {
try {
const source = fs.readFileSync(sourcePath, "utf8");
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
if (!fs.existsSync(targetPath)) {
fs.writeFileSync(targetPath, source, "utf8");
return true;
}
const target = fs.readFileSync(targetPath, "utf8");
if (target === source || target.startsWith(source)) {
return false;
}
if (source.startsWith(target)) {
fs.appendFileSync(targetPath, source.slice(target.length), "utf8");
return true;
}
const separator = target.endsWith("\n") ? "" : "\n";
fs.appendFileSync(targetPath, `${separator}${source}`, "utf8");
return true;
} catch {
return false;
}
}
function copyTraceConfig(sourcePath: string, targetPath: string): boolean {
try {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
const source = fs.readFileSync(sourcePath, "utf8");
if (fs.existsSync(targetPath) && fs.readFileSync(targetPath, "utf8") === source) {
return false;
}
fs.writeFileSync(targetPath, source, "utf8");
return true;
} catch {
return false;
}
}
function isRootLogFile(fileName: string): boolean {
const baseName = fileName.endsWith(".old") ? fileName.slice(0, -4) : fileName;
return LOG_FILE_NAMES.has(baseName) || /^rename-session_.*\.txt$/i.test(fileName);
}
function copyAllowedEntries(sourceDirectory: string, targetDirectory: string, result: LogMigrationResult): void {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(sourceDirectory, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const sourcePath = path.join(sourceDirectory, entry.name);
const targetPath = path.join(targetDirectory, entry.name);
if (entry.isDirectory()) {
if (!LOG_DIRECTORY_NAMES.has(entry.name)) {
continue;
}
fs.mkdirSync(targetPath, { recursive: true });
copyDirectoryFiles(sourcePath, targetPath, result);
continue;
}
if (!entry.isFile() || !isRootLogFile(entry.name)) {
result.skippedFiles += 1;
continue;
}
const copied = entry.name === "trace_config.json"
? copyTraceConfig(sourcePath, targetPath)
: mergeTextFile(sourcePath, targetPath);
if (copied) {
result.copiedFiles += 1;
}
}
}
function copyDirectoryFiles(sourceDirectory: string, targetDirectory: string, result: LogMigrationResult): void {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(sourceDirectory, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const sourcePath = path.join(sourceDirectory, entry.name);
const targetPath = path.join(targetDirectory, entry.name);
if (entry.isDirectory()) {
fs.mkdirSync(targetPath, { recursive: true });
copyDirectoryFiles(sourcePath, targetPath, result);
continue;
}
if (!entry.isFile()) {
continue;
}
if (mergeTextFile(sourcePath, targetPath)) {
result.copiedFiles += 1;
}
}
}
export function resolveLogDirectory(
runtimeDirectory: string,
desktopDirectory: string | null | undefined,
location: LogStorageLocation
): string {
const runtimePath = path.resolve(runtimeDirectory);
const desktopPath = String(desktopDirectory || "").trim();
if (location === "desktop" && desktopPath) {
return path.resolve(desktopPath, DESKTOP_LOG_DIRECTORY_NAME);
}
return runtimePath;
}
export function getLegacyDesktopLogDirectory(desktopDirectory: string | null | undefined): string | null {
const desktopPath = String(desktopDirectory || "").trim();
return desktopPath ? path.resolve(desktopPath, LEGACY_DESKTOP_LOG_DIRECTORY_NAME) : null;
}
export function prepareLogDirectory(directory: string): boolean {
try {
fs.mkdirSync(directory, { recursive: true });
return fs.statSync(directory).isDirectory();
} catch {
return false;
}
}
export function migrateLogDirectories(sourceDirectories: readonly string[], targetDirectory: string): LogMigrationResult {
const result: LogMigrationResult = { copiedFiles: 0, skippedFiles: 0 };
if (!prepareLogDirectory(targetDirectory)) {
return result;
}
for (const sourceDirectory of sourceDirectories) {
if (!sourceDirectory || samePath(sourceDirectory, targetDirectory)) {
continue;
}
copyAllowedEntries(sourceDirectory, targetDirectory, result);
}
return result;
}
+32 -24
View File
@@ -57,11 +57,19 @@ export function removeLogListener(listener: LogListener): void {
} }
} }
export function configureLogger(baseDir: string): void { export function configureLogger(baseDir: string): void {
logFilePath = path.join(baseDir, "rd_downloader.log"); logFilePath = path.join(baseDir, "rd_downloader.log");
const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log"); const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log");
fallbackLogFilePath = cwdLogPath === logFilePath ? null : cwdLogPath; fallbackLogFilePath = cwdLogPath === logFilePath ? null : cwdLogPath;
} }
export function flushLoggerSync(): void {
if (flushTimer) {
clearTimeout(flushTimer);
flushTimer = null;
}
flushSyncPending();
}
function appendLine(filePath: string, line: string): { ok: boolean; errorText: string } { function appendLine(filePath: string, line: string): { ok: boolean; errorText: string } {
try { try {
@@ -99,15 +107,15 @@ function flushSyncPending(): void {
pendingLines = []; pendingLines = [];
pendingChars = 0; pendingChars = 0;
rotateIfNeeded(logFilePath); rotateIfNeeded(logFilePath);
const primary = appendLine(logFilePath, chunk); const primary = appendLine(logFilePath, chunk);
if (fallbackLogFilePath) { if (!primary.ok && fallbackLogFilePath) {
rotateIfNeeded(fallbackLogFilePath); rotateIfNeeded(fallbackLogFilePath);
const fallback = appendLine(fallbackLogFilePath, chunk); const fallback = appendLine(fallbackLogFilePath, chunk);
if (!primary.ok && !fallback.ok) { if (!fallback.ok) {
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`); writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
} }
return; return;
} }
if (!primary.ok) { if (!primary.ok) {
@@ -194,16 +202,16 @@ async function flushAsync(): Promise<void> {
const chunk = linesSnapshot.join(""); const chunk = linesSnapshot.join("");
try { try {
await rotateIfNeededAsync(logFilePath); await rotateIfNeededAsync(logFilePath);
const primary = await appendChunk(logFilePath, chunk); const primary = await appendChunk(logFilePath, chunk);
let wroteAny = primary.ok; let wroteAny = primary.ok;
if (fallbackLogFilePath) { if (!primary.ok && fallbackLogFilePath) {
await rotateIfNeededAsync(fallbackLogFilePath); await rotateIfNeededAsync(fallbackLogFilePath);
const fallback = await appendChunk(fallbackLogFilePath, chunk); const fallback = await appendChunk(fallbackLogFilePath, chunk);
wroteAny = wroteAny || fallback.ok; wroteAny = fallback.ok;
if (!primary.ok && !fallback.ok) { if (!fallback.ok) {
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`); writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
} }
} else if (!primary.ok) { } else if (!primary.ok) {
writeStderr(`LOGGER write failed: ${primary.errorText}\n`); writeStderr(`LOGGER write failed: ${primary.errorText}\n`);
} }
+10 -6
View File
@@ -673,12 +673,16 @@ function registerIpcHandlers(): void {
return { saved: true, filePath: result.filePath }; return { saved: true, filePath: result.filePath };
}); });
handleTrusted(IPC_CHANNELS.OPEN_LOG, async () => { handleTrusted(IPC_CHANNELS.OPEN_LOG, async () => {
const logPath = getLogFilePath(); const logPath = getLogFilePath();
await shell.openPath(logPath); await shell.openPath(logPath);
}); });
handleTrusted(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => { handleTrusted(IPC_CHANNELS.OPEN_LOG_DIRECTORY, async () => {
await shell.openPath(controller.getLogDirectory());
});
handleTrusted(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
const logPath = controller.getAuditLogPath(); const logPath = controller.getAuditLogPath();
if (logPath) { if (logPath) {
await shell.openPath(logPath); await shell.openPath(logPath);
+1
View File
@@ -179,6 +179,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
clipboardWatch: settings.clipboardWatch, clipboardWatch: settings.clipboardWatch,
minimizeToTray: settings.minimizeToTray, minimizeToTray: settings.minimizeToTray,
theme: settings.theme, theme: settings.theme,
logStorageLocation: settings.logStorageLocation,
collapseNewPackages: settings.collapseNewPackages, collapseNewPackages: settings.collapseNewPackages,
historyRetentionMode: settings.historyRetentionMode, historyRetentionMode: settings.historyRetentionMode,
historyMaxEntries: settings.historyMaxEntries, historyMaxEntries: settings.historyMaxEntries,
+8 -4
View File
@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys"; import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts"; import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types"; import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, LogStorageLocation, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { getProviderUsageDayKey } from "../shared/provider-daily-limits"; import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
import { defaultSettings } from "./constants"; import { defaultSettings } from "./constants";
import { needsPersistedSettingsRewrite, protectPersistedSettings, restorePersistedSettings } from "./credential-protection"; import { needsPersistedSettingsRewrite, protectPersistedSettings, restorePersistedSettings } from "./credential-protection";
@@ -18,7 +18,8 @@ const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "pack
const VALID_SPEED_MODES = new Set(["global", "per_download"]); const VALID_SPEED_MODES = new Set(["global", "per_download"]);
const VALID_THEMES = new Set(["dark", "light"]); const VALID_THEMES = new Set(["dark", "light"]);
const VALID_EXTRACT_CPU_PRIORITIES = new Set(["high", "middle", "low"]); const VALID_EXTRACT_CPU_PRIORITIES = new Set(["high", "middle", "low"]);
const VALID_HISTORY_RETENTION_MODES = new Set<HistoryRetentionMode>(["never", "session", "permanent"]); const VALID_HISTORY_RETENTION_MODES = new Set<HistoryRetentionMode>(["never", "session", "permanent"]);
const VALID_LOG_STORAGE_LOCATIONS = new Set<LogStorageLocation>(["appdata", "desktop"]);
const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]); const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]);
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([ const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled" "queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
@@ -516,8 +517,11 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
autoUpdateCheck: Boolean(settings.autoUpdateCheck), autoUpdateCheck: Boolean(settings.autoUpdateCheck),
updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo), updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo),
clipboardWatch: Boolean(settings.clipboardWatch), clipboardWatch: Boolean(settings.clipboardWatch),
minimizeToTray: Boolean(settings.minimizeToTray), minimizeToTray: Boolean(settings.minimizeToTray),
collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages, logStorageLocation: VALID_LOG_STORAGE_LOCATIONS.has(settings.logStorageLocation)
? settings.logStorageLocation
: defaults.logStorageLocation,
collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages,
historyRetentionMode: VALID_HISTORY_RETENTION_MODES.has(settings.historyRetentionMode) historyRetentionMode: VALID_HISTORY_RETENTION_MODES.has(settings.historyRetentionMode)
? settings.historyRetentionMode ? settings.historyRetentionMode
: defaults.historyRetentionMode, : defaults.historyRetentionMode,
+4 -3
View File
@@ -78,9 +78,10 @@ const api: ElectronApi = {
cancelBackupImport: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CANCEL_BACKUP_IMPORT), cancelBackupImport: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CANCEL_BACKUP_IMPORT),
exportOnlineBackup: (): Promise<{ key: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_ONLINE_BACKUP), exportOnlineBackup: (): Promise<{ key: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_ONLINE_BACKUP),
importOnlineBackup: (key: string): Promise<{ restored: boolean; relaunch: false; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_ONLINE_BACKUP, key), importOnlineBackup: (key: string): Promise<{ restored: boolean; relaunch: false; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_ONLINE_BACKUP, key),
exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE), exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE),
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG), openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
openAuditLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_AUDIT_LOG), openLogDirectory: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG_DIRECTORY),
openAuditLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_AUDIT_LOG),
openRenameLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_RENAME_LOG), openRenameLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_RENAME_LOG),
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG), openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG), openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
+8 -1
View File
@@ -821,7 +821,7 @@ const emptySnapshot = (): UiSnapshot => ({
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never", autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global", maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false, updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: false, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeRemoteDiagnostics: false, theme: "dark", logStorageLocation: "appdata", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: false, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeRemoteDiagnostics: false,
notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false, notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false,
accountListShowDetailedDebridLinkKeys: false, accountListShowDetailedDebridLinkKeys: false,
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0, bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
@@ -5075,6 +5075,12 @@ export function App(): ReactElement {
}); });
return; return;
} }
if (fieldId === "logStorageDirectory") {
void performQuickAction(async () => {
await window.rd.openLogDirectory();
});
return;
}
const targetKey = fieldId === "outputDir" ? "outputDir" : fieldId === "extractDir" ? "extractDir" : fieldId === "mkvLibraryDir" ? "mkvLibraryDir" : null; const targetKey = fieldId === "outputDir" ? "outputDir" : fieldId === "extractDir" ? "extractDir" : fieldId === "mkvLibraryDir" ? "mkvLibraryDir" : null;
if (targetKey) { if (targetKey) {
void performQuickAction(async () => { void performQuickAction(async () => {
@@ -5467,6 +5473,7 @@ export function App(): ReactElement {
className={`menu-submenu-dropdown${openSubmenu === "hilfe-log" ? " is-open" : ""}`} className={`menu-submenu-dropdown${openSubmenu === "hilfe-log" ? " is-open" : ""}`}
> >
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}><span>Haupt-Log</span></button> <button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}><span>Haupt-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLogDirectory().catch(() => {}); }}><span>Log-Ordner öffnen</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}><span>Audit-Log</span></button> <button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}><span>Audit-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}><span>Rename-Log</span></button> <button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openRenameLog().catch(() => {}); }}><span>Rename-Log</span></button>
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}><span>Session-Log</span></button> <button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}><span>Session-Log</span></button>
+13 -1
View File
@@ -466,7 +466,19 @@ export function buildSettingsFormViewModel({
title: "Speicherort", title: "Speicherort",
fields: [ fields: [
{ id: "outputDir", kind: "path", label: "Download-Ordner", value: settings.outputDir, actionLabel: "Wählen", help: "Zielordner für heruntergeladene Dateien." }, { id: "outputDir", kind: "path", label: "Download-Ordner", value: settings.outputDir, actionLabel: "Wählen", help: "Zielordner für heruntergeladene Dateien." },
{ id: "packageName", kind: "text", label: "Paketname (optional)", value: settings.packageName } { id: "packageName", kind: "text", label: "Paketname (optional)", value: settings.packageName },
{
id: "logStorageLocation",
kind: "select",
label: "Log-Speicherort",
value: settings.logStorageLocation,
options: [
{ value: "appdata", label: "AppData (empfohlen)" },
{ value: "desktop", label: "Desktop / Downloader Log" }
],
help: "Beim Wechsel werden vorhandene Log-Dateien übernommen. Zugangsdaten und App-Konfiguration bleiben in AppData."
},
{ id: "logStorageDirectory", kind: "action", label: "Log-Ordner", actionLabel: "Ordner öffnen" }
] ]
}, },
{ {
+3 -2
View File
@@ -47,8 +47,9 @@ export const IPC_CHANNELS = {
EXPORT_ONLINE_BACKUP: "app:export-online-backup", EXPORT_ONLINE_BACKUP: "app:export-online-backup",
IMPORT_ONLINE_BACKUP: "app:import-online-backup", IMPORT_ONLINE_BACKUP: "app:import-online-backup",
EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle", EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle",
OPEN_LOG: "app:open-log", OPEN_LOG: "app:open-log",
OPEN_AUDIT_LOG: "app:open-audit-log", OPEN_LOG_DIRECTORY: "app:open-log-directory",
OPEN_AUDIT_LOG: "app:open-audit-log",
OPEN_RENAME_LOG: "app:open-rename-log", OPEN_RENAME_LOG: "app:open-rename-log",
OPEN_SESSION_LOG: "app:open-session-log", OPEN_SESSION_LOG: "app:open-session-log",
OPEN_TRACE_LOG: "app:open-trace-log", OPEN_TRACE_LOG: "app:open-trace-log",
+4 -3
View File
@@ -75,9 +75,10 @@ export interface ElectronApi {
cancelBackupImport: () => Promise<void>; cancelBackupImport: () => Promise<void>;
exportOnlineBackup: () => Promise<{ key: string }>; exportOnlineBackup: () => Promise<{ key: string }>;
importOnlineBackup: (key: string) => Promise<{ restored: boolean; relaunch: false; message: string }>; importOnlineBackup: (key: string) => Promise<{ restored: boolean; relaunch: false; message: string }>;
exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>; exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>;
openLog: () => Promise<void>; openLog: () => Promise<void>;
openAuditLog: () => Promise<void>; openLogDirectory: () => Promise<void>;
openAuditLog: () => Promise<void>;
openRenameLog: () => Promise<void>; openRenameLog: () => Promise<void>;
openSessionLog: () => Promise<void>; openSessionLog: () => Promise<void>;
openTraceLog: () => Promise<void>; openTraceLog: () => Promise<void>;
+6 -3
View File
@@ -30,7 +30,8 @@ export type AppTheme = "dark" | "light";
export type AppLanguage = "en" | "de"; export type AppLanguage = "en" | "de";
export type PackagePriority = "high" | "normal" | "low"; export type PackagePriority = "high" | "normal" | "low";
export type ExtractCpuPriority = "high" | "middle" | "low"; export type ExtractCpuPriority = "high" | "middle" | "low";
export type HistoryRetentionMode = "never" | "session" | "permanent"; export type HistoryRetentionMode = "never" | "session" | "permanent";
export type LogStorageLocation = "appdata" | "desktop";
export interface BandwidthScheduleEntry { export interface BandwidthScheduleEntry {
id: string; id: string;
@@ -127,8 +128,9 @@ export interface AppSettings {
autoUpdateCheck: boolean; autoUpdateCheck: boolean;
clipboardWatch: boolean; clipboardWatch: boolean;
minimizeToTray: boolean; minimizeToTray: boolean;
theme: AppTheme; theme: AppTheme;
collapseNewPackages: boolean; logStorageLocation: LogStorageLocation;
collapseNewPackages: boolean;
historyRetentionMode: HistoryRetentionMode; historyRetentionMode: HistoryRetentionMode;
historyMaxEntries: number; historyMaxEntries: number;
historyMaxAgeDays: number; historyMaxAgeDays: number;
@@ -246,6 +248,7 @@ export interface RendererSettings {
clipboardWatch: boolean; clipboardWatch: boolean;
minimizeToTray: boolean; minimizeToTray: boolean;
theme: AppTheme; theme: AppTheme;
logStorageLocation: LogStorageLocation;
collapseNewPackages: boolean; collapseNewPackages: boolean;
historyRetentionMode: HistoryRetentionMode; historyRetentionMode: HistoryRetentionMode;
historyMaxEntries: number; historyMaxEntries: number;
+92
View File
@@ -0,0 +1,92 @@
import { afterEach, describe, expect, it } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
DESKTOP_LOG_DIRECTORY_NAME,
LEGACY_DESKTOP_LOG_DIRECTORY_NAME,
migrateLogDirectories,
prepareLogDirectory,
resolveLogDirectory
} from "../src/main/log-storage";
import { defaultSettings } from "../src/main/constants";
import { normalizeSettings } from "../src/main/storage";
const createdDirectories: string[] = [];
function createTempDirectory(): string {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-log-storage-"));
createdDirectories.push(directory);
return directory;
}
afterEach(() => {
for (const directory of createdDirectories) {
fs.rmSync(directory, { recursive: true, force: true });
}
createdDirectories.length = 0;
});
describe("log-storage", () => {
it("uses the runtime directory by default and the named desktop directory on request", () => {
const runtimeDirectory = path.join(createTempDirectory(), "runtime");
const desktopDirectory = path.join(createTempDirectory(), "Desktop");
expect(resolveLogDirectory(runtimeDirectory, desktopDirectory, "appdata")).toBe(runtimeDirectory);
expect(resolveLogDirectory(runtimeDirectory, desktopDirectory, "desktop")).toBe(
path.join(desktopDirectory, DESKTOP_LOG_DIRECTORY_NAME)
);
expect(resolveLogDirectory(runtimeDirectory, null, "desktop")).toBe(runtimeDirectory);
expect(defaultSettings().logStorageLocation).toBe("appdata");
expect(normalizeSettings({ ...defaultSettings(), logStorageLocation: "invalid" as "appdata" }).logStorageLocation).toBe("appdata");
});
it("creates the selected directory and carries only log files and log folders", () => {
const source = path.join(createTempDirectory(), "source");
const target = path.join(createTempDirectory(), "target");
fs.mkdirSync(path.join(source, "session-logs"), { recursive: true });
fs.mkdirSync(path.join(source, "package-logs"), { recursive: true });
fs.writeFileSync(path.join(source, "rd_downloader.log"), "main\n", "utf8");
fs.writeFileSync(path.join(source, "session-logs", "session_1.txt"), "session\n", "utf8");
fs.writeFileSync(path.join(source, "package-logs", "package_1.txt"), "package\n", "utf8");
fs.writeFileSync(path.join(source, "settings.json"), "secret\n", "utf8");
expect(prepareLogDirectory(target)).toBe(true);
const result = migrateLogDirectories([source], target);
expect(result.copiedFiles).toBe(3);
expect(fs.readFileSync(path.join(target, "rd_downloader.log"), "utf8")).toBe("main\n");
expect(fs.readFileSync(path.join(target, "session-logs", "session_1.txt"), "utf8")).toBe("session\n");
expect(fs.readFileSync(path.join(target, "package-logs", "package_1.txt"), "utf8")).toBe("package\n");
expect(fs.existsSync(path.join(target, "settings.json"))).toBe(false);
});
it("merges a legacy hyphenated desktop folder without exposing unrelated files", () => {
const root = createTempDirectory();
const legacy = path.join(root, LEGACY_DESKTOP_LOG_DIRECTORY_NAME);
const target = path.join(root, DESKTOP_LOG_DIRECTORY_NAME);
fs.mkdirSync(legacy, { recursive: true });
fs.writeFileSync(path.join(legacy, "rename-session_1.txt"), "rename\n", "utf8");
fs.writeFileSync(path.join(legacy, "credentials.json"), "secret\n", "utf8");
const result = migrateLogDirectories([legacy], target);
expect(result.copiedFiles).toBe(1);
expect(fs.readFileSync(path.join(target, "rename-session_1.txt"), "utf8")).toBe("rename\n");
expect(fs.existsSync(path.join(target, "credentials.json"))).toBe(false);
});
it("replaces trace configuration instead of concatenating JSON from an older location", () => {
const source = path.join(createTempDirectory(), "source");
const target = path.join(createTempDirectory(), "target");
fs.mkdirSync(source, { recursive: true });
fs.mkdirSync(target, { recursive: true });
fs.writeFileSync(path.join(source, "trace_config.json"), JSON.stringify({ enabled: true, expiresAt: 123 }), "utf8");
fs.writeFileSync(path.join(target, "trace_config.json"), JSON.stringify({ enabled: false, expiresAt: 456 }), "utf8");
const result = migrateLogDirectories([source], target);
expect(result.copiedFiles).toBe(1);
expect(JSON.parse(fs.readFileSync(path.join(target, "trace_config.json"), "utf8"))).toEqual({ enabled: true, expiresAt: 123 });
});
});
+5 -4
View File
@@ -112,10 +112,11 @@ function createSettings(): AppSettings {
speedLimitMode: "global", speedLimitMode: "global",
updateRepo: "Sucukdeluxe/multi-debrid-downloader", updateRepo: "Sucukdeluxe/multi-debrid-downloader",
autoUpdateCheck: true, autoUpdateCheck: true,
clipboardWatch: true, clipboardWatch: true,
minimizeToTray: false, minimizeToTray: false,
theme: "dark", theme: "dark",
collapseNewPackages: false, logStorageLocation: "appdata",
collapseNewPackages: false,
historyRetentionMode: "permanent", historyRetentionMode: "permanent",
historyMaxEntries: 500, historyMaxEntries: 500,
historyMaxAgeDays: 0, historyMaxAgeDays: 0,
+1
View File
@@ -182,6 +182,7 @@ export function createVisualElectronApi(
importOnlineBackup: async () => ({ restored: true, relaunch: false, message: "Visual online backup importiert" }), importOnlineBackup: async () => ({ restored: true, relaunch: false, message: "Visual online backup importiert" }),
exportSupportBundle: async () => ({ saved: true, filePath: "C:\\Visual\\Support\\support.zip" }), exportSupportBundle: async () => ({ saved: true, filePath: "C:\\Visual\\Support\\support.zip" }),
openLog: async () => {}, openLog: async () => {},
openLogDirectory: async () => {},
openAuditLog: async () => {}, openAuditLog: async () => {},
openRenameLog: async () => {}, openRenameLog: async () => {},
openSessionLog: async () => {}, openSessionLog: async () => {},