From d7149829ead09f3fb5659e3601ef0731c08c6c1e Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 24 Mar 2026 09:16:24 +0100 Subject: [PATCH] Add daily log rotation with monthly folder structure All log output is now additionally written to daily log files: daily-logs/YYYY-MM/YYYY-MM-DD.log (main log) daily-logs/YYYY-MM/YYYY-MM-DD-rename.log (rename log) Automatic cleanup of daily logs older than 30 days. The existing rd_downloader.log and rename.log continue to work as before. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/app-controller.ts | 849 +++++++++++++++++++------------------ src/main/daily-log.ts | 193 +++++++++ src/main/rename-log.ts | 9 +- 3 files changed, 623 insertions(+), 428 deletions(-) create mode 100644 src/main/daily-log.ts diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index e1763b5..a5f6c47 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -24,26 +24,27 @@ import { APP_VERSION } from "./constants"; import { DownloadManager } from "./download-manager"; import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid"; import { parseCollectorInput } from "./link-parser"; -import { configureLogger, getLogFilePath, logger } from "./logger"; -import { AllDebridWebFallback } from "./all-debrid-web"; -import { BestDebridWebFallback } from "./bestdebrid-web"; -import { RealDebridWebFallback } from "./realdebrid-web"; -import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log"; -import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log"; -import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log"; +import { configureLogger, getLogFilePath, logger } from "./logger"; +import { AllDebridWebFallback } from "./all-debrid-web"; +import { BestDebridWebFallback } from "./bestdebrid-web"; +import { RealDebridWebFallback } from "./realdebrid-web"; +import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log"; +import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log"; +import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log"; import { MegaWebFallback } from "./mega-web-fallback"; -import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage"; -import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; -import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server"; -import { encryptBackup, decryptBackup } from "./backup-crypto"; -import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log"; -import { getDebugSetupCheck } from "./debug-setup"; -import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; -import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./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 { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage"; +import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; +import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server"; +import { encryptBackup, decryptBackup } from "./backup-crypto"; +import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log"; +import { getDebugSetupCheck } from "./debug-setup"; +import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; +import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log"; +import { initDailyLog, shutdownDailyLog } from "./daily-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"; function sanitizeSettingsPatch(partial: Partial): Partial { const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); @@ -73,22 +74,23 @@ export class AppController { private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime")); - private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null; - - private autoResumePending = false; - private runtimeStatsTimer: NodeJS.Timeout | null = null; + private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null; - public constructor() { - configureLogger(this.storagePaths.baseDir); - initSessionLog(this.storagePaths.baseDir); - initPackageLogs(this.storagePaths.baseDir); - initItemLogs(this.storagePaths.baseDir); - initAuditLog(this.storagePaths.baseDir); - initRenameLog(this.storagePaths.baseDir); - initTraceLog(this.storagePaths.baseDir); - this.settings = loadSettings(this.storagePaths); - resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - const session = loadSession(this.storagePaths); + private autoResumePending = false; + private runtimeStatsTimer: NodeJS.Timeout | null = null; + + public constructor() { + configureLogger(this.storagePaths.baseDir); + initSessionLog(this.storagePaths.baseDir); + initPackageLogs(this.storagePaths.baseDir); + initItemLogs(this.storagePaths.baseDir); + initAuditLog(this.storagePaths.baseDir); + initRenameLog(this.storagePaths.baseDir); + initDailyLog(this.storagePaths.baseDir); + initTraceLog(this.storagePaths.baseDir); + this.settings = loadSettings(this.storagePaths); + resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + const session = loadSession(this.storagePaths); this.megaWebFallback = new MegaWebFallback(() => ({ login: this.settings.megaLogin, password: this.settings.megaPassword @@ -98,31 +100,31 @@ export class AppController { this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken); this.manager = new DownloadManager(this.settings, session, this.storagePaths, { megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal), - allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal), - realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal), - bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal), - invalidateMegaSession: () => this.megaWebFallback.invalidateSession(), - onHistoryEntry: (entry: HistoryEntry) => { - addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry); - } - }); + allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal), + realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal), + bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal), + invalidateMegaSession: () => this.megaWebFallback.invalidateSession(), + onHistoryEntry: (entry: HistoryEntry) => { + addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry); + } + }); this.manager.on("state", (snapshot: UiSnapshot) => { this.onStateHandler?.(snapshot); - }); - logger.info(`App gestartet v${APP_VERSION}`); - logger.info(`Log-Datei: ${getLogFilePath()}`); - logAuditEvent("INFO", "App gestartet", { - appVersion: APP_VERSION, - runtimeDir: this.storagePaths.baseDir - }); - startDebugServer(this.manager, this.storagePaths.baseDir); - this.runtimeStatsTimer = setInterval(() => { - this.manager.persistRuntimeStats(); - this.settings = this.manager.getSettings(); - }, 60_000); - this.runtimeStatsTimer.unref?.(); - - if (this.settings.autoResumeOnStart) { + }); + logger.info(`App gestartet v${APP_VERSION}`); + logger.info(`Log-Datei: ${getLogFilePath()}`); + logAuditEvent("INFO", "App gestartet", { + appVersion: APP_VERSION, + runtimeDir: this.storagePaths.baseDir + }); + startDebugServer(this.manager, this.storagePaths.baseDir); + this.runtimeStatsTimer = setInterval(() => { + this.manager.persistRuntimeStats(); + this.settings = this.manager.getSettings(); + }, 60_000); + this.runtimeStatsTimer.unref?.(); + + if (this.settings.autoResumeOnStart) { const snapshot = this.manager.getSnapshot(); const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait"); if (hasPending) { @@ -187,46 +189,46 @@ export class AppController { return APP_VERSION; } - public getSettings(): AppSettings { - return this.settings; - } - - public getAuditLogPath(): string | null { - return getAuditLogPath(); - } - - public getRenameLogPath(): string | null { - return getRenameLogPath(); - } - - public getTraceLogPath(): string | null { - return getTraceLogPath(); - } - - public getTraceConfig(): SupportTraceConfig { - return getTraceConfig(); - } - - public rotateDebugToken(): { path: string; token: string } { - const rotated = rotateDebugToken(this.storagePaths.baseDir); - this.audit("WARN", "Debug-Token rotiert", { path: rotated.path }); - return rotated; - } - - public getDebugSetupCheck(): DebugSetupCheckResult { - return getDebugSetupCheck(this.storagePaths.baseDir); - } - - private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record): void { - logAuditEvent(level, message, fields); - logTraceEvent(level, "audit", message, fields); - } - - public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig { - const next = setTraceEnabled(enabled, note, durationMs); - this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note }); - return next; - } + public getSettings(): AppSettings { + return this.settings; + } + + public getAuditLogPath(): string | null { + return getAuditLogPath(); + } + + public getRenameLogPath(): string | null { + return getRenameLogPath(); + } + + public getTraceLogPath(): string | null { + return getTraceLogPath(); + } + + public getTraceConfig(): SupportTraceConfig { + return getTraceConfig(); + } + + public rotateDebugToken(): { path: string; token: string } { + const rotated = rotateDebugToken(this.storagePaths.baseDir); + this.audit("WARN", "Debug-Token rotiert", { path: rotated.path }); + return rotated; + } + + public getDebugSetupCheck(): DebugSetupCheckResult { + return getDebugSetupCheck(this.storagePaths.baseDir); + } + + private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record): void { + logAuditEvent(level, message, fields); + logTraceEvent(level, "audit", message, fields); + } + + public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig { + const next = setTraceEnabled(enabled, note, durationMs); + this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note }); + return next; + } public updateSettings(partial: Partial): AppSettings { const sanitizedPatch = sanitizeSettingsPatch(partial); @@ -240,32 +242,32 @@ export class AppController { return previousSettings; } - // Preserve the live all-time counters from the download manager - const liveSettings = this.manager.getSettings(); - nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0); - nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0); - nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs()); - nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay; - nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; - nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; - nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries( - Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) - ); - nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries( - Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) - ); - const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode; - this.settings = nextSettings; - if (retentionChanged) { - resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - } - saveSettings(this.storagePaths, this.settings); - this.manager.setSettings(this.settings); - this.audit("INFO", "Einstellungen aktualisiert", { - changedKeys: Object.keys(sanitizedPatch), - accountChanges: diffAccountSummary(previousSettings, this.settings) - }); - if (previousSettings.rememberToken && !this.settings.rememberToken) { + // Preserve the live all-time counters from the download manager + const liveSettings = this.manager.getSettings(); + nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0); + nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0); + nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs()); + nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay; + nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; + nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; + nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries( + Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) + ); + nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries( + Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) + ); + const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode; + this.settings = nextSettings; + if (retentionChanged) { + resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + } + saveSettings(this.storagePaths, this.settings); + this.manager.setSettings(this.settings); + this.audit("INFO", "Einstellungen aktualisiert", { + changedKeys: Object.keys(sanitizedPatch), + accountChanges: diffAccountSummary(previousSettings, this.settings) + }); + if (previousSettings.rememberToken && !this.settings.rememberToken) { void this.realDebridWebFallback.clearSessions().catch((error) => { logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`); }); @@ -285,12 +287,12 @@ export class AppController { ...liveSettings, ...resetProviderDailyUsage(liveSettings, provider) }); - this.settings = nextSettings; - saveSettings(this.storagePaths, this.settings); - this.manager.setSettings(this.settings); - this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider }); - return this.settings; - } + this.settings = nextSettings; + saveSettings(this.storagePaths, this.settings); + this.manager.setSettings(this.settings); + this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider }); + return this.settings; + } public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings { const liveSettings = this.manager.getSettings(); @@ -298,31 +300,31 @@ export class AppController { ...liveSettings, ...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId) }); - this.settings = nextSettings; - saveSettings(this.storagePaths, this.settings); - this.manager.setSettings(this.settings); - this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId }); - return this.settings; - } - - public async openRealDebridLoginWindow(): Promise { - this.audit("INFO", "Real-Debrid Login-Fenster geöffnet"); - await this.realDebridWebFallback.openLoginWindow(); - } - - public async openAllDebridLoginWindow(): Promise { - this.audit("INFO", "AllDebrid Login-Fenster geöffnet"); - await this.allDebridWebFallback.openLoginWindow(); - } - - public async importBestDebridCookies(filePath: string): Promise { - const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath); - this.audit("INFO", "BestDebrid Cookies importiert", { - filePath, - imported - }); - return imported; - } + this.settings = nextSettings; + saveSettings(this.storagePaths, this.settings); + this.manager.setSettings(this.settings); + this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId }); + return this.settings; + } + + public async openRealDebridLoginWindow(): Promise { + this.audit("INFO", "Real-Debrid Login-Fenster geöffnet"); + await this.realDebridWebFallback.openLoginWindow(); + } + + public async openAllDebridLoginWindow(): Promise { + this.audit("INFO", "AllDebrid Login-Fenster geöffnet"); + await this.allDebridWebFallback.openLoginWindow(); + } + + public async importBestDebridCookies(filePath: string): Promise { + const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath); + this.audit("INFO", "BestDebrid Cookies importiert", { + filePath, + imported + }); + return imported; + } public async getAllDebridHostInfo(host = "rapidgator"): Promise { if (this.settings.allDebridUseWebLogin) { @@ -367,38 +369,38 @@ export class AppController { return result; } - public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } { - const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName); - if (parsed.length === 0) { - this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", { - hasPackageName: Boolean(payload.packageName) - }); - return { addedPackages: 0, addedLinks: 0, invalidCount: 1 }; - } - const result = this.manager.addPackages(parsed); - this.audit("INFO", "Links hinzugefügt", { - addedPackages: result.addedPackages, - addedLinks: result.addedLinks, - requestedPackages: parsed.length - }); - return { ...result, invalidCount: 0 }; - } + public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } { + const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName); + if (parsed.length === 0) { + this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", { + hasPackageName: Boolean(payload.packageName) + }); + return { addedPackages: 0, addedLinks: 0, invalidCount: 1 }; + } + const result = this.manager.addPackages(parsed); + this.audit("INFO", "Links hinzugefügt", { + addedPackages: result.addedPackages, + addedLinks: result.addedLinks, + requestedPackages: parsed.length + }); + return { ...result, invalidCount: 0 }; + } public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> { const packages = await importDlcContainers(filePaths); - const merged: ParsedPackageInput[] = packages.map((pkg) => ({ - name: pkg.name, - links: pkg.links, - ...(pkg.fileNames ? { fileNames: pkg.fileNames } : {}) - })); - const result = this.manager.addPackages(merged); - this.audit("INFO", "Container importiert", { - files: filePaths.length, - addedPackages: result.addedPackages, - addedLinks: result.addedLinks - }); - return result; - } + const merged: ParsedPackageInput[] = packages.map((pkg) => ({ + name: pkg.name, + links: pkg.links, + ...(pkg.fileNames ? { fileNames: pkg.fileNames } : {}) + })); + const result = this.manager.addPackages(merged); + this.audit("INFO", "Container importiert", { + files: filePaths.length, + addedPackages: result.addedPackages, + addedLinks: result.addedLinks + }); + return result; + } public async getStartConflicts(): Promise { return this.manager.getStartConflicts(); @@ -408,163 +410,163 @@ export class AppController { return this.manager.resolveStartConflict(packageId, policy); } - public clearAll(): void { - this.audit("WARN", "Queue komplett geleert"); - this.manager.clearAll(); - } - - public async start(): Promise { - this.audit("INFO", "Session-Start ausgelöst"); - await this.manager.start(); - } - - public async startPackages(packageIds: string[]): Promise { - this.audit("INFO", "Paket-Start ausgelöst", { packageIds }); - await this.manager.startPackages(packageIds); - } - - public async startItems(itemIds: string[]): Promise { - this.audit("INFO", "Item-Start ausgelöst", { itemIds }); - await this.manager.startItems(itemIds); - } - - public stop(): void { - this.audit("INFO", "Session-Stopp ausgelöst"); - this.manager.stop(); - } - - public togglePause(): boolean { - const paused = this.manager.togglePause(); - this.audit("INFO", "Pause umgeschaltet", { paused }); - return paused; - } - - public retryExtraction(packageId: string): void { - this.audit("INFO", "Extraktion manuell wiederholt", { packageId }); - this.manager.retryExtraction(packageId); - } - - public extractNow(packageId: string): void { - this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId }); - this.manager.extractNow(packageId); - } - - public resetPackage(packageId: string): void { - this.audit("INFO", "Paket zurückgesetzt", { packageId }); - this.manager.resetPackage(packageId); - } - - public cancelPackage(packageId: string): void { - this.audit("WARN", "Paket abgebrochen", { packageId }); - this.manager.cancelPackage(packageId); - } - - public renamePackage(packageId: string, newName: string): void { - this.audit("INFO", "Paket umbenannt", { packageId, newName }); - this.manager.renamePackage(packageId, newName); - } - - public reorderPackages(packageIds: string[]): void { - this.audit("INFO", "Paketreihenfolge geändert", { packageIds }); - this.manager.reorderPackages(packageIds); - } - - public removeItem(itemId: string): void { - this.audit("WARN", "Item entfernt", { itemId }); - this.manager.removeItem(itemId); - } + public clearAll(): void { + this.audit("WARN", "Queue komplett geleert"); + this.manager.clearAll(); + } - public togglePackage(packageId: string): void { - this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId }); - this.manager.togglePackage(packageId); - } - - public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { - const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []); - this.audit("INFO", "Paket-Auswahl exportiert", { - packageCount: selection.packageCount, - linkCount: selection.linkCount, - packageIds - }); - return { - text: serializeLinkExportText(selection.packages), - defaultFileName: selection.defaultFileName, - packageCount: selection.packageCount, - linkCount: selection.linkCount - }; - } - - public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { - const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds); - this.audit("INFO", "Item-Auswahl exportiert", { - packageCount: selection.packageCount, - linkCount: selection.linkCount, - itemIds - }); - return { - text: serializeLinkExportText(selection.packages), - defaultFileName: selection.defaultFileName, - packageCount: selection.packageCount, - linkCount: selection.linkCount - }; - } - - public exportQueue(): string { - return this.manager.exportQueue(); - } - - public importQueue(json: string): { addedPackages: number; addedLinks: number } { - const result = this.manager.importQueue(json); - this.audit("INFO", "Import-Datei verarbeitet", result); - return result; - } + public async start(): Promise { + this.audit("INFO", "Session-Start ausgelöst"); + await this.manager.start(); + } - public getSessionStats(): SessionStats { - return this.manager.getSessionStats(); - } - - public resetSessionStats(): void { - this.audit("INFO", "Session-Statistik zurückgesetzt"); - this.manager.resetSessionStats(); - } - - public resetDownloadStats(): void { - this.manager.resetDownloadStats(); - this.settings = this.manager.getSettings(); - this.audit("INFO", "Download-Statistik zurückgesetzt"); - } + public async startPackages(packageIds: string[]): Promise { + this.audit("INFO", "Paket-Start ausgelöst", { packageIds }); + await this.manager.startPackages(packageIds); + } - public exportBackup(): Buffer { - const settings = { ...this.settings }; - const session = this.manager.getSession(); - const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + public async startItems(itemIds: string[]): Promise { + this.audit("INFO", "Item-Start ausgelöst", { itemIds }); + await this.manager.startItems(itemIds); + } + + public stop(): void { + this.audit("INFO", "Session-Stopp ausgelöst"); + this.manager.stop(); + } + + public togglePause(): boolean { + const paused = this.manager.togglePause(); + this.audit("INFO", "Pause umgeschaltet", { paused }); + return paused; + } + + public retryExtraction(packageId: string): void { + this.audit("INFO", "Extraktion manuell wiederholt", { packageId }); + this.manager.retryExtraction(packageId); + } + + public extractNow(packageId: string): void { + this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId }); + this.manager.extractNow(packageId); + } + + public resetPackage(packageId: string): void { + this.audit("INFO", "Paket zurückgesetzt", { packageId }); + this.manager.resetPackage(packageId); + } + + public cancelPackage(packageId: string): void { + this.audit("WARN", "Paket abgebrochen", { packageId }); + this.manager.cancelPackage(packageId); + } + + public renamePackage(packageId: string, newName: string): void { + this.audit("INFO", "Paket umbenannt", { packageId, newName }); + this.manager.renamePackage(packageId, newName); + } + + public reorderPackages(packageIds: string[]): void { + this.audit("INFO", "Paketreihenfolge geändert", { packageIds }); + this.manager.reorderPackages(packageIds); + } + + public removeItem(itemId: string): void { + this.audit("WARN", "Item entfernt", { itemId }); + this.manager.removeItem(itemId); + } + + public togglePackage(packageId: string): void { + this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId }); + this.manager.togglePackage(packageId); + } + + public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { + const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []); + this.audit("INFO", "Paket-Auswahl exportiert", { + packageCount: selection.packageCount, + linkCount: selection.linkCount, + packageIds + }); + return { + text: serializeLinkExportText(selection.packages), + defaultFileName: selection.defaultFileName, + packageCount: selection.packageCount, + linkCount: selection.linkCount + }; + } + + public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { + const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds); + this.audit("INFO", "Item-Auswahl exportiert", { + packageCount: selection.packageCount, + linkCount: selection.linkCount, + itemIds + }); + return { + text: serializeLinkExportText(selection.packages), + defaultFileName: selection.defaultFileName, + packageCount: selection.packageCount, + linkCount: selection.linkCount + }; + } + + public exportQueue(): string { + return this.manager.exportQueue(); + } + + public importQueue(json: string): { addedPackages: number; addedLinks: number } { + const result = this.manager.importQueue(json); + this.audit("INFO", "Import-Datei verarbeitet", result); + return result; + } + + public getSessionStats(): SessionStats { + return this.manager.getSessionStats(); + } + + public resetSessionStats(): void { + this.audit("INFO", "Session-Statistik zurückgesetzt"); + this.manager.resetSessionStats(); + } + + public resetDownloadStats(): void { + this.manager.resetDownloadStats(); + this.settings = this.manager.getSettings(); + this.audit("INFO", "Download-Statistik zurückgesetzt"); + } + + public exportBackup(): Buffer { + const settings = { ...this.settings }; + const session = this.manager.getSession(); + const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); const payload = JSON.stringify({ version: 2, appVersion: APP_VERSION, exportedAt: new Date().toISOString(), - settings, - session, - history - }); - this.audit("INFO", "Backup exportiert", { - historyEntries: history.length, - sessionItems: Object.keys(session.items).length, - sessionPackages: Object.keys(session.packages).length - }); - return encryptBackup(payload); - } - - public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } { - this.audit("INFO", "Support-Bundle exportiert"); - logTraceEvent("INFO", "support", "Support-Bundle erstellt", { - packageCount: Object.keys(this.manager.getSnapshot().session.packages).length, - itemCount: Object.keys(this.manager.getSnapshot().session.items).length - }); - return { - buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir), - defaultFileName: getSupportBundleDefaultFileName() - }; - } + settings, + session, + history + }); + this.audit("INFO", "Backup exportiert", { + historyEntries: history.length, + sessionItems: Object.keys(session.items).length, + sessionPackages: Object.keys(session.packages).length + }); + return encryptBackup(payload); + } + + public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } { + this.audit("INFO", "Support-Bundle exportiert"); + logTraceEvent("INFO", "support", "Support-Bundle erstellt", { + packageCount: Object.keys(this.manager.getSnapshot().session.packages).length, + itemCount: Object.keys(this.manager.getSnapshot().session.items).length + }); + return { + buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir), + defaultFileName: getSupportBundleDefaultFileName() + }; + } public importBackup(data: Buffer): { restored: boolean; message: string } { let parsed: Record; @@ -586,21 +588,21 @@ export class AppController { } // Restore settings — ALL credentials are included (no more masking) - const importedSettings = parsed.settings as AppSettings; - const importedSettingsRecord = importedSettings as unknown as Record; - const currentSettingsRecord = this.settings as unknown as Record; + const importedSettings = parsed.settings as AppSettings; + const importedSettingsRecord = importedSettings as unknown as Record; + const currentSettingsRecord = this.settings as unknown as Record; // Legacy backup compatibility: if credentials were masked with ***, keep current values const SENSITIVE_KEYS: (keyof AppSettings)[] = [ "token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword", "oneFichierApiKey", "debridLinkApiKeys", "linkSnappyLogin", "linkSnappyPassword" ]; - for (const key of SENSITIVE_KEYS) { - const val = importedSettingsRecord[key]; - if (typeof val === "string" && val.startsWith("***")) { - importedSettingsRecord[key] = currentSettingsRecord[key]; - } - } + for (const key of SENSITIVE_KEYS) { + const val = importedSettingsRecord[key]; + if (typeof val === "string" && val.startsWith("***")) { + importedSettingsRecord[key] = currentSettingsRecord[key]; + } + } const restoredSettings = normalizeSettings(importedSettings); this.settings = restoredSettings; saveSettings(this.storagePaths, this.settings); @@ -629,93 +631,94 @@ export class AppController { } } - resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - - // Prevent prepareForShutdown from overwriting the restored data + resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + + // Prevent prepareForShutdown from overwriting the restored data this.manager.skipShutdownPersist = true; this.manager.blockAllPersistence = true; - logger.info("Backup wiederhergestellt (verschlüsseltes Format)"); - this.audit("WARN", "Backup importiert", { - historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0, - accountSummary: buildAccountSummary(this.settings) - }); - return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; - } + logger.info("Backup wiederhergestellt (verschlüsseltes Format)"); + this.audit("WARN", "Backup importiert", { + historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0, + accountSummary: buildAccountSummary(this.settings) + }); + return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; + } - public getSessionLogPath(): string | null { - return getSessionLogPath(); - } - - public getPackageLogPath(packageId: string): string | null { - return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId); - } - - public getItemLogPath(itemId: string): string | null { - return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId); - } - - public shutdown(): void { - if (this.runtimeStatsTimer) { - clearInterval(this.runtimeStatsTimer); - this.runtimeStatsTimer = null; - } - stopDebugServer(); + public getSessionLogPath(): string | null { + return getSessionLogPath(); + } + + public getPackageLogPath(packageId: string): string | null { + return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId); + } + + public getItemLogPath(itemId: string): string | null { + return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId); + } + + public shutdown(): void { + if (this.runtimeStatsTimer) { + clearInterval(this.runtimeStatsTimer); + this.runtimeStatsTimer = null; + } + stopDebugServer(); abortActiveUpdateDownload(); this.manager.prepareForShutdown(); this.megaWebFallback.dispose(); this.realDebridWebFallback.dispose(); - this.allDebridWebFallback.dispose(); - this.bestDebridWebFallback.dispose(); - shutdownSessionLog(); - shutdownPackageLogs(); - shutdownItemLogs(); - shutdownRenameLog(); - this.audit("INFO", "App beendet"); - shutdownTraceLog(); - shutdownAuditLog(); - if (this.settings.historyRetentionMode === "session") { - clearHistory(this.storagePaths); - } - logger.info("App beendet"); - } - - public getHistory(): HistoryEntry[] { - return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - } + this.allDebridWebFallback.dispose(); + this.bestDebridWebFallback.dispose(); + shutdownSessionLog(); + shutdownPackageLogs(); + shutdownItemLogs(); + shutdownRenameLog(); + shutdownDailyLog(); + this.audit("INFO", "App beendet"); + shutdownTraceLog(); + shutdownAuditLog(); + if (this.settings.historyRetentionMode === "session") { + clearHistory(this.storagePaths); + } + logger.info("App beendet"); + } - public clearHistory(): void { - this.audit("WARN", "Verlauf geleert"); - clearHistory(this.storagePaths); - } - - public setPackagePriority(packageId: string, priority: PackagePriority): void { - this.audit("INFO", "Paket-Priorität geändert", { packageId, priority }); - this.manager.setPackagePriority(packageId, priority); - } - - public skipItems(itemIds: string[]): void { - this.audit("INFO", "Items übersprungen", { itemIds }); - this.manager.skipItems(itemIds); - } - - public resetItems(itemIds: string[]): void { - this.audit("INFO", "Items zurückgesetzt", { itemIds }); - this.manager.resetItems(itemIds); - } - - public removeHistoryEntry(entryId: string): void { - this.audit("INFO", "Verlaufseintrag entfernt", { entryId }); - removeHistoryEntry(this.storagePaths, entryId); - } - - public addToHistory(entry: HistoryEntry): void { - this.audit("INFO", "Verlaufseintrag hinzugefügt", { - id: entry.id, - name: entry.name, - status: entry.status, - provider: entry.provider, - fileCount: entry.fileCount - }); - addHistoryEntry(this.storagePaths, entry); - } + public getHistory(): HistoryEntry[] { + return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + } + + public clearHistory(): void { + this.audit("WARN", "Verlauf geleert"); + clearHistory(this.storagePaths); + } + + public setPackagePriority(packageId: string, priority: PackagePriority): void { + this.audit("INFO", "Paket-Priorität geändert", { packageId, priority }); + this.manager.setPackagePriority(packageId, priority); + } + + public skipItems(itemIds: string[]): void { + this.audit("INFO", "Items übersprungen", { itemIds }); + this.manager.skipItems(itemIds); + } + + public resetItems(itemIds: string[]): void { + this.audit("INFO", "Items zurückgesetzt", { itemIds }); + this.manager.resetItems(itemIds); + } + + public removeHistoryEntry(entryId: string): void { + this.audit("INFO", "Verlaufseintrag entfernt", { entryId }); + removeHistoryEntry(this.storagePaths, entryId); + } + + public addToHistory(entry: HistoryEntry): void { + this.audit("INFO", "Verlaufseintrag hinzugefügt", { + id: entry.id, + name: entry.name, + status: entry.status, + provider: entry.provider, + fileCount: entry.fileCount + }); + addHistoryEntry(this.storagePaths, entry); + } } diff --git a/src/main/daily-log.ts b/src/main/daily-log.ts new file mode 100644 index 0000000..0450954 --- /dev/null +++ b/src/main/daily-log.ts @@ -0,0 +1,193 @@ +import fs from "node:fs"; +import path from "node:path"; +import { addLogListener, removeLogListener } from "./logger"; + +const DAILY_LOG_RETENTION_DAYS = 30; +const CLEANUP_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // every 6 hours + +let dailyLogDir = ""; +let currentDayKey = ""; +let currentLogFd: number | null = null; +let currentRenameFd: number | null = null; +let logListener: ((line: string) => void) | null = null; +let cleanupTimer: NodeJS.Timeout | null = null; +let lastCleanupAt = 0; + +function getDayKey(now = new Date()): string { + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function getMonthDir(dayKey: string): string { + return dayKey.slice(0, 7); // "YYYY-MM" +} + +function ensureDayFile(dayKey: string): number | null { + if (currentDayKey === dayKey && currentLogFd !== null) { + return currentLogFd; + } + + // Close previous day's fd + if (currentLogFd !== null) { + try { fs.closeSync(currentLogFd); } catch { /* ignore */ } + currentLogFd = null; + } + if (currentRenameFd !== null) { + try { fs.closeSync(currentRenameFd); } catch { /* ignore */ } + currentRenameFd = null; + } + + currentDayKey = dayKey; + const monthDir = path.join(dailyLogDir, getMonthDir(dayKey)); + + try { + fs.mkdirSync(monthDir, { recursive: true }); + const filePath = path.join(monthDir, `${dayKey}.log`); + currentLogFd = fs.openSync(filePath, "a"); + return currentLogFd; + } catch { + return null; + } +} + +function ensureRenameFd(dayKey: string): number | null { + if (currentDayKey === dayKey && currentRenameFd !== null) { + return currentRenameFd; + } + + // ensureDayFile handles day transitions + if (currentDayKey !== dayKey) { + ensureDayFile(dayKey); + } + + if (currentRenameFd !== null) { + return currentRenameFd; + } + + const monthDir = path.join(dailyLogDir, getMonthDir(dayKey)); + try { + fs.mkdirSync(monthDir, { recursive: true }); + const filePath = path.join(monthDir, `${dayKey}-rename.log`); + currentRenameFd = fs.openSync(filePath, "a"); + return currentRenameFd; + } catch { + return null; + } +} + +function writeToDailyLog(line: string): void { + if (!dailyLogDir) return; + + const dayKey = getDayKey(); + const fd = ensureDayFile(dayKey); + if (fd === null) return; + + try { + fs.writeSync(fd, line); + } catch { + // Close and retry on next write + try { fs.closeSync(fd); } catch { /* ignore */ } + currentLogFd = null; + } +} + +export function writeToDailyRenameLog(line: string): void { + if (!dailyLogDir) return; + + const dayKey = getDayKey(); + const fd = ensureRenameFd(dayKey); + if (fd === null) return; + + try { + fs.writeSync(fd, line); + } catch { + try { fs.closeSync(fd); } catch { /* ignore */ } + currentRenameFd = null; + } +} + +function cleanupOldDailyLogs(): void { + if (!dailyLogDir) return; + + const now = Date.now(); + if (now - lastCleanupAt < CLEANUP_CHECK_INTERVAL_MS) return; + lastCleanupAt = now; + + const cutoffMs = now - DAILY_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; + + try { + const monthDirs = fs.readdirSync(dailyLogDir, { withFileTypes: true }) + .filter((e) => e.isDirectory() && /^\d{4}-\d{2}$/.test(e.name)); + + for (const monthDir of monthDirs) { + const monthPath = path.join(dailyLogDir, monthDir.name); + const files = fs.readdirSync(monthPath, { withFileTypes: true }) + .filter((e) => e.isFile() && /^\d{4}-\d{2}-\d{2}/.test(e.name)); + + for (const file of files) { + const filePath = path.join(monthPath, file.name); + try { + const stat = fs.statSync(filePath); + if (stat.mtimeMs < cutoffMs) { + fs.rmSync(filePath, { force: true }); + } + } catch { /* ignore */ } + } + + // Remove empty month dirs + try { + const remaining = fs.readdirSync(monthPath); + if (remaining.length === 0) { + fs.rmdirSync(monthPath); + } + } catch { /* ignore */ } + } + } catch { + // ignore cleanup errors + } +} + +export function initDailyLog(baseDir: string): void { + dailyLogDir = path.join(baseDir, "daily-logs"); + + try { + fs.mkdirSync(dailyLogDir, { recursive: true }); + } catch { /* ignore */ } + + // Attach listener to main logger + logListener = (line: string) => writeToDailyLog(line); + addLogListener(logListener); + + // Initial cleanup + cleanupOldDailyLogs(); + + // Periodic cleanup + cleanupTimer = setInterval(cleanupOldDailyLogs, CLEANUP_CHECK_INTERVAL_MS); + if (cleanupTimer.unref) cleanupTimer.unref(); +} + +export function shutdownDailyLog(): void { + if (logListener) { + removeLogListener(logListener); + logListener = null; + } + if (cleanupTimer) { + clearInterval(cleanupTimer); + cleanupTimer = null; + } + if (currentLogFd !== null) { + try { fs.closeSync(currentLogFd); } catch { /* ignore */ } + currentLogFd = null; + } + if (currentRenameFd !== null) { + try { fs.closeSync(currentRenameFd); } catch { /* ignore */ } + currentRenameFd = null; + } + currentDayKey = ""; +} + +export function getDailyLogDir(): string { + return dailyLogDir; +} diff --git a/src/main/rename-log.ts b/src/main/rename-log.ts index 72f5102..c03325d 100644 --- a/src/main/rename-log.ts +++ b/src/main/rename-log.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { writeToDailyRenameLog } from "./daily-log"; type RenameLogLevel = "INFO" | "WARN" | "ERROR"; @@ -93,11 +94,9 @@ export function logRenameEvent(level: RenameLogLevel, message: string, fields?: if (!fs.existsSync(renameLogPath)) { fs.writeFileSync(renameLogPath, "", "utf8"); } - fs.appendFileSync( - renameLogPath, - `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`, - "utf8" - ); + const line = `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`; + fs.appendFileSync(renameLogPath, line, "utf8"); + writeToDailyRenameLog(line); } catch { // ignore write errors }