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:
+131
-48
@@ -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 };
|
||||
|
||||
@@ -101,9 +101,10 @@ export function defaultSettings(): AppSettings {
|
||||
updateRepo: DEFAULT_UPDATE_REPO,
|
||||
autoUpdateCheck: true,
|
||||
clipboardWatch: false,
|
||||
minimizeToTray: false,
|
||||
theme: "dark" as const,
|
||||
collapseNewPackages: true,
|
||||
minimizeToTray: false,
|
||||
theme: "dark" as const,
|
||||
logStorageLocation: "appdata",
|
||||
collapseNewPackages: true,
|
||||
historyRetentionMode: "permanent",
|
||||
historyMaxEntries: 500,
|
||||
historyMaxAgeDays: 0,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { logTimestamp } from "./log-timestamp";
|
||||
|
||||
type DesktopRenameLevel = "INFO" | "WARN" | "ERROR";
|
||||
|
||||
const FOLDER_NAME = "Downloader-Log";
|
||||
const LEGACY_FOLDER_NAME = "Downloader-Log";
|
||||
|
||||
let logDir: string | null = null;
|
||||
let logFilePath: string | null = null;
|
||||
@@ -58,15 +58,15 @@ function ensureWritable(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function initDesktopRenameLog(desktopDir: string | null | undefined): void {
|
||||
try {
|
||||
const base = String(desktopDir || "").trim();
|
||||
if (!base) {
|
||||
logDir = null;
|
||||
logFilePath = null;
|
||||
return;
|
||||
}
|
||||
logDir = path.join(base, FOLDER_NAME);
|
||||
function initializeRenameLog(directory: string | null | undefined): void {
|
||||
try {
|
||||
const base = String(directory || "").trim();
|
||||
if (!base) {
|
||||
logDir = null;
|
||||
logFilePath = null;
|
||||
return;
|
||||
}
|
||||
logDir = path.resolve(base);
|
||||
logFilePath = path.join(logDir, `rename-session_${fileTimestamp()}.txt`);
|
||||
sessionHeader = `=== Rename-Session gestartet: ${logTimestamp()} ===\n`
|
||||
+ "Diese Datei protokolliert JEDEN Umbenenn-/Verschiebevorgang dieser Programm-Sitzung\n"
|
||||
@@ -77,8 +77,17 @@ export function initDesktopRenameLog(desktopDir: string | null | undefined): voi
|
||||
} catch {
|
||||
logDir = 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 {
|
||||
if (!ensureWritable() || !logFilePath) {
|
||||
|
||||
@@ -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
@@ -57,11 +57,19 @@ export function removeLogListener(listener: LogListener): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function configureLogger(baseDir: string): void {
|
||||
logFilePath = path.join(baseDir, "rd_downloader.log");
|
||||
const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log");
|
||||
fallbackLogFilePath = cwdLogPath === logFilePath ? null : cwdLogPath;
|
||||
}
|
||||
export function configureLogger(baseDir: string): void {
|
||||
logFilePath = path.join(baseDir, "rd_downloader.log");
|
||||
const cwdLogPath = path.resolve(process.cwd(), "rd_downloader.log");
|
||||
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 } {
|
||||
try {
|
||||
@@ -99,15 +107,15 @@ function flushSyncPending(): void {
|
||||
pendingLines = [];
|
||||
pendingChars = 0;
|
||||
|
||||
rotateIfNeeded(logFilePath);
|
||||
const primary = appendLine(logFilePath, chunk);
|
||||
if (fallbackLogFilePath) {
|
||||
rotateIfNeeded(fallbackLogFilePath);
|
||||
const fallback = appendLine(fallbackLogFilePath, chunk);
|
||||
if (!primary.ok && !fallback.ok) {
|
||||
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
}
|
||||
return;
|
||||
rotateIfNeeded(logFilePath);
|
||||
const primary = appendLine(logFilePath, chunk);
|
||||
if (!primary.ok && fallbackLogFilePath) {
|
||||
rotateIfNeeded(fallbackLogFilePath);
|
||||
const fallback = appendLine(fallbackLogFilePath, chunk);
|
||||
if (!fallback.ok) {
|
||||
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!primary.ok) {
|
||||
@@ -194,16 +202,16 @@ async function flushAsync(): Promise<void> {
|
||||
const chunk = linesSnapshot.join("");
|
||||
|
||||
try {
|
||||
await rotateIfNeededAsync(logFilePath);
|
||||
const primary = await appendChunk(logFilePath, chunk);
|
||||
let wroteAny = primary.ok;
|
||||
if (fallbackLogFilePath) {
|
||||
await rotateIfNeededAsync(fallbackLogFilePath);
|
||||
const fallback = await appendChunk(fallbackLogFilePath, chunk);
|
||||
wroteAny = wroteAny || fallback.ok;
|
||||
if (!primary.ok && !fallback.ok) {
|
||||
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
}
|
||||
await rotateIfNeededAsync(logFilePath);
|
||||
const primary = await appendChunk(logFilePath, chunk);
|
||||
let wroteAny = primary.ok;
|
||||
if (!primary.ok && fallbackLogFilePath) {
|
||||
await rotateIfNeededAsync(fallbackLogFilePath);
|
||||
const fallback = await appendChunk(fallbackLogFilePath, chunk);
|
||||
wroteAny = fallback.ok;
|
||||
if (!fallback.ok) {
|
||||
writeStderr(`LOGGER write failed (primary+fallback): ${primary.errorText} | ${fallback.errorText}\n`);
|
||||
}
|
||||
} else if (!primary.ok) {
|
||||
writeStderr(`LOGGER write failed: ${primary.errorText}\n`);
|
||||
}
|
||||
|
||||
+10
-6
@@ -673,12 +673,16 @@ function registerIpcHandlers(): void {
|
||||
return { saved: true, filePath: result.filePath };
|
||||
});
|
||||
|
||||
handleTrusted(IPC_CHANNELS.OPEN_LOG, async () => {
|
||||
const logPath = getLogFilePath();
|
||||
await shell.openPath(logPath);
|
||||
});
|
||||
|
||||
handleTrusted(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
|
||||
handleTrusted(IPC_CHANNELS.OPEN_LOG, async () => {
|
||||
const logPath = getLogFilePath();
|
||||
await shell.openPath(logPath);
|
||||
});
|
||||
|
||||
handleTrusted(IPC_CHANNELS.OPEN_LOG_DIRECTORY, async () => {
|
||||
await shell.openPath(controller.getLogDirectory());
|
||||
});
|
||||
|
||||
handleTrusted(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
|
||||
const logPath = controller.getAuditLogPath();
|
||||
if (logPath) {
|
||||
await shell.openPath(logPath);
|
||||
|
||||
@@ -179,6 +179,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
|
||||
clipboardWatch: settings.clipboardWatch,
|
||||
minimizeToTray: settings.minimizeToTray,
|
||||
theme: settings.theme,
|
||||
logStorageLocation: settings.logStorageLocation,
|
||||
collapseNewPackages: settings.collapseNewPackages,
|
||||
historyRetentionMode: settings.historyRetentionMode,
|
||||
historyMaxEntries: settings.historyMaxEntries,
|
||||
|
||||
+8
-4
@@ -4,7 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||
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 { defaultSettings } from "./constants";
|
||||
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_THEMES = new Set(["dark", "light"]);
|
||||
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_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
|
||||
"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),
|
||||
updateRepo: migrateUpdateRepo(asText(settings.updateRepo), defaults.updateRepo),
|
||||
clipboardWatch: Boolean(settings.clipboardWatch),
|
||||
minimizeToTray: Boolean(settings.minimizeToTray),
|
||||
collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages,
|
||||
minimizeToTray: Boolean(settings.minimizeToTray),
|
||||
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)
|
||||
? settings.historyRetentionMode
|
||||
: defaults.historyRetentionMode,
|
||||
|
||||
@@ -78,9 +78,10 @@ const api: ElectronApi = {
|
||||
cancelBackupImport: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.CANCEL_BACKUP_IMPORT),
|
||||
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),
|
||||
exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE),
|
||||
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
|
||||
openAuditLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_AUDIT_LOG),
|
||||
exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE),
|
||||
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_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),
|
||||
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
|
||||
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
|
||||
|
||||
@@ -821,7 +821,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
||||
maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global",
|
||||
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,
|
||||
accountListShowDetailedDebridLinkKeys: false,
|
||||
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
|
||||
@@ -5075,6 +5075,12 @@ export function App(): ReactElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (fieldId === "logStorageDirectory") {
|
||||
void performQuickAction(async () => {
|
||||
await window.rd.openLogDirectory();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const targetKey = fieldId === "outputDir" ? "outputDir" : fieldId === "extractDir" ? "extractDir" : fieldId === "mkvLibraryDir" ? "mkvLibraryDir" : null;
|
||||
if (targetKey) {
|
||||
void performQuickAction(async () => {
|
||||
@@ -5467,6 +5473,7 @@ export function App(): ReactElement {
|
||||
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.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.openRenameLog().catch(() => {}); }}><span>Rename-Log</span></button>
|
||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}><span>Session-Log</span></button>
|
||||
|
||||
@@ -466,7 +466,19 @@ export function buildSettingsFormViewModel({
|
||||
title: "Speicherort",
|
||||
fields: [
|
||||
{ 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
@@ -47,8 +47,9 @@ export const IPC_CHANNELS = {
|
||||
EXPORT_ONLINE_BACKUP: "app:export-online-backup",
|
||||
IMPORT_ONLINE_BACKUP: "app:import-online-backup",
|
||||
EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle",
|
||||
OPEN_LOG: "app:open-log",
|
||||
OPEN_AUDIT_LOG: "app:open-audit-log",
|
||||
OPEN_LOG: "app:open-log",
|
||||
OPEN_LOG_DIRECTORY: "app:open-log-directory",
|
||||
OPEN_AUDIT_LOG: "app:open-audit-log",
|
||||
OPEN_RENAME_LOG: "app:open-rename-log",
|
||||
OPEN_SESSION_LOG: "app:open-session-log",
|
||||
OPEN_TRACE_LOG: "app:open-trace-log",
|
||||
|
||||
@@ -75,9 +75,10 @@ export interface ElectronApi {
|
||||
cancelBackupImport: () => Promise<void>;
|
||||
exportOnlineBackup: () => Promise<{ key: string }>;
|
||||
importOnlineBackup: (key: string) => Promise<{ restored: boolean; relaunch: false; message: string }>;
|
||||
exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>;
|
||||
openLog: () => Promise<void>;
|
||||
openAuditLog: () => Promise<void>;
|
||||
exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>;
|
||||
openLog: () => Promise<void>;
|
||||
openLogDirectory: () => Promise<void>;
|
||||
openAuditLog: () => Promise<void>;
|
||||
openRenameLog: () => Promise<void>;
|
||||
openSessionLog: () => Promise<void>;
|
||||
openTraceLog: () => Promise<void>;
|
||||
|
||||
+6
-3
@@ -30,7 +30,8 @@ export type AppTheme = "dark" | "light";
|
||||
export type AppLanguage = "en" | "de";
|
||||
export type PackagePriority = "high" | "normal" | "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 {
|
||||
id: string;
|
||||
@@ -127,8 +128,9 @@ export interface AppSettings {
|
||||
autoUpdateCheck: boolean;
|
||||
clipboardWatch: boolean;
|
||||
minimizeToTray: boolean;
|
||||
theme: AppTheme;
|
||||
collapseNewPackages: boolean;
|
||||
theme: AppTheme;
|
||||
logStorageLocation: LogStorageLocation;
|
||||
collapseNewPackages: boolean;
|
||||
historyRetentionMode: HistoryRetentionMode;
|
||||
historyMaxEntries: number;
|
||||
historyMaxAgeDays: number;
|
||||
@@ -246,6 +248,7 @@ export interface RendererSettings {
|
||||
clipboardWatch: boolean;
|
||||
minimizeToTray: boolean;
|
||||
theme: AppTheme;
|
||||
logStorageLocation: LogStorageLocation;
|
||||
collapseNewPackages: boolean;
|
||||
historyRetentionMode: HistoryRetentionMode;
|
||||
historyMaxEntries: number;
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -112,10 +112,11 @@ function createSettings(): AppSettings {
|
||||
speedLimitMode: "global",
|
||||
updateRepo: "Sucukdeluxe/multi-debrid-downloader",
|
||||
autoUpdateCheck: true,
|
||||
clipboardWatch: true,
|
||||
minimizeToTray: false,
|
||||
theme: "dark",
|
||||
collapseNewPackages: false,
|
||||
clipboardWatch: true,
|
||||
minimizeToTray: false,
|
||||
theme: "dark",
|
||||
logStorageLocation: "appdata",
|
||||
collapseNewPackages: false,
|
||||
historyRetentionMode: "permanent",
|
||||
historyMaxEntries: 500,
|
||||
historyMaxAgeDays: 0,
|
||||
|
||||
@@ -182,6 +182,7 @@ export function createVisualElectronApi(
|
||||
importOnlineBackup: async () => ({ restored: true, relaunch: false, message: "Visual online backup importiert" }),
|
||||
exportSupportBundle: async () => ({ saved: true, filePath: "C:\\Visual\\Support\\support.zip" }),
|
||||
openLog: async () => {},
|
||||
openLogDirectory: async () => {},
|
||||
openAuditLog: async () => {},
|
||||
openRenameLog: async () => {},
|
||||
openSessionLog: async () => {},
|
||||
|
||||
Reference in New Issue
Block a user