Revert daily-log and queue-scope changes back to v1.7.112 state
Remove daily-log module entirely (caused UI freezes due to sync I/O even after async rewrite). Revert queue-scope stop() change (was for a different project). All source files now match v1.7.112. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a27d0ec8f2
commit
2d9fbb07ea
@ -24,27 +24,26 @@ 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 { 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";
|
||||
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";
|
||||
|
||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||
@ -74,23 +73,22 @@ export class AppController {
|
||||
|
||||
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
|
||||
|
||||
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
||||
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
||||
|
||||
private autoResumePending = false;
|
||||
private runtimeStatsTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
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);
|
||||
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);
|
||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||
login: this.settings.megaLogin,
|
||||
password: this.settings.megaPassword
|
||||
@ -100,31 +98,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) {
|
||||
@ -189,46 +187,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<string, unknown>): 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<string, unknown>): 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>): AppSettings {
|
||||
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
||||
@ -242,32 +240,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)}`);
|
||||
});
|
||||
@ -287,12 +285,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();
|
||||
@ -300,31 +298,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<void> {
|
||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
||||
await this.realDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
|
||||
public async openAllDebridLoginWindow(): Promise<void> {
|
||||
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
||||
await this.allDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
|
||||
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||
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<void> {
|
||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
||||
await this.realDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
|
||||
public async openAllDebridLoginWindow(): Promise<void> {
|
||||
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
||||
await this.allDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
|
||||
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||
const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
||||
this.audit("INFO", "BestDebrid Cookies importiert", {
|
||||
filePath,
|
||||
imported
|
||||
});
|
||||
return imported;
|
||||
}
|
||||
|
||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
||||
if (this.settings.allDebridUseWebLogin) {
|
||||
@ -369,38 +367,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<StartConflictEntry[]> {
|
||||
return this.manager.getStartConflicts();
|
||||
@ -410,163 +408,163 @@ export class AppController {
|
||||
return this.manager.resolveStartConflict(packageId, policy);
|
||||
}
|
||||
|
||||
public clearAll(): void {
|
||||
this.audit("WARN", "Queue komplett geleert");
|
||||
this.manager.clearAll();
|
||||
}
|
||||
public clearAll(): void {
|
||||
this.audit("WARN", "Queue komplett geleert");
|
||||
this.manager.clearAll();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
this.audit("INFO", "Session-Start ausgelöst");
|
||||
await this.manager.start();
|
||||
}
|
||||
|
||||
public async startPackages(packageIds: string[]): Promise<void> {
|
||||
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
||||
await this.manager.startPackages(packageIds);
|
||||
}
|
||||
|
||||
public async startItems(itemIds: string[]): Promise<void> {
|
||||
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 async start(): Promise<void> {
|
||||
this.audit("INFO", "Session-Start ausgelöst");
|
||||
await this.manager.start();
|
||||
}
|
||||
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 startPackages(packageIds: string[]): Promise<void> {
|
||||
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
||||
await this.manager.startPackages(packageIds);
|
||||
}
|
||||
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 startItems(itemIds: string[]): Promise<void> {
|
||||
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);
|
||||
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<string, unknown>;
|
||||
@ -588,21 +586,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<string, unknown>;
|
||||
const currentSettingsRecord = this.settings as unknown as Record<string, unknown>;
|
||||
const importedSettings = parsed.settings as AppSettings;
|
||||
const importedSettingsRecord = importedSettings as unknown as Record<string, unknown>;
|
||||
const currentSettingsRecord = this.settings as unknown as Record<string, unknown>;
|
||||
// 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);
|
||||
@ -631,94 +629,93 @@ 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();
|
||||
shutdownDailyLog();
|
||||
this.audit("INFO", "App beendet");
|
||||
shutdownTraceLog();
|
||||
shutdownAuditLog();
|
||||
if (this.settings.historyRetentionMode === "session") {
|
||||
clearHistory(this.storagePaths);
|
||||
}
|
||||
logger.info("App beendet");
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,235 +0,0 @@
|
||||
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;
|
||||
const FLUSH_INTERVAL_MS = 500;
|
||||
const BUFFER_LIMIT_CHARS = 500_000;
|
||||
|
||||
let dailyLogDir = "";
|
||||
let currentDayKey = "";
|
||||
let logListener: ((line: string) => void) | null = null;
|
||||
let cleanupTimer: NodeJS.Timeout | null = null;
|
||||
let lastCleanupAt = 0;
|
||||
|
||||
// Async buffered writes — never blocks the event loop
|
||||
let pendingLogLines: string[] = [];
|
||||
let pendingLogChars = 0;
|
||||
let pendingRenameLines: string[] = [];
|
||||
let pendingRenameChars = 0;
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
let flushInFlight = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function getDailyLogPath(dayKey: string): string {
|
||||
return path.join(dailyLogDir, getMonthDir(dayKey), `${dayKey}.log`);
|
||||
}
|
||||
|
||||
function getDailyRenameLogPath(dayKey: string): string {
|
||||
return path.join(dailyLogDir, getMonthDir(dayKey), `${dayKey}-rename.log`);
|
||||
}
|
||||
|
||||
function scheduleFlush(): void {
|
||||
if (flushTimer || flushInFlight) return;
|
||||
flushTimer = setTimeout(() => {
|
||||
flushTimer = null;
|
||||
void flushAsync();
|
||||
}, FLUSH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function flushAsync(): Promise<void> {
|
||||
if (flushInFlight) return;
|
||||
flushInFlight = true;
|
||||
|
||||
try {
|
||||
const dayKey = currentDayKey || getDayKey();
|
||||
|
||||
if (pendingLogLines.length > 0) {
|
||||
const chunk = pendingLogLines.join("");
|
||||
pendingLogLines = [];
|
||||
pendingLogChars = 0;
|
||||
const filePath = getDailyLogPath(dayKey);
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.promises.appendFile(filePath, chunk, "utf8");
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (pendingRenameLines.length > 0) {
|
||||
const chunk = pendingRenameLines.join("");
|
||||
pendingRenameLines = [];
|
||||
pendingRenameChars = 0;
|
||||
const filePath = getDailyRenameLogPath(dayKey);
|
||||
try {
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.promises.appendFile(filePath, chunk, "utf8");
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
} finally {
|
||||
flushInFlight = false;
|
||||
if (pendingLogLines.length > 0 || pendingRenameLines.length > 0) {
|
||||
scheduleFlush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flushSyncOnExit(): void {
|
||||
const dayKey = currentDayKey || getDayKey();
|
||||
|
||||
if (pendingLogLines.length > 0) {
|
||||
const chunk = pendingLogLines.join("");
|
||||
pendingLogLines = [];
|
||||
pendingLogChars = 0;
|
||||
try {
|
||||
const filePath = getDailyLogPath(dayKey);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.appendFileSync(filePath, chunk, "utf8");
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (pendingRenameLines.length > 0) {
|
||||
const chunk = pendingRenameLines.join("");
|
||||
pendingRenameLines = [];
|
||||
pendingRenameChars = 0;
|
||||
try {
|
||||
const filePath = getDailyRenameLogPath(dayKey);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.appendFileSync(filePath, chunk, "utf8");
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
function writeToDailyLog(line: string): void {
|
||||
if (!dailyLogDir) return;
|
||||
|
||||
const dayKey = getDayKey();
|
||||
if (dayKey !== currentDayKey) {
|
||||
// Day changed — flush previous day's buffer first
|
||||
if (currentDayKey && (pendingLogLines.length > 0 || pendingRenameLines.length > 0)) {
|
||||
void flushAsync();
|
||||
}
|
||||
currentDayKey = dayKey;
|
||||
}
|
||||
|
||||
pendingLogLines.push(line);
|
||||
pendingLogChars += line.length;
|
||||
|
||||
// Shed oldest lines if buffer too large
|
||||
while (pendingLogChars > BUFFER_LIMIT_CHARS && pendingLogLines.length > 1) {
|
||||
const removed = pendingLogLines.shift();
|
||||
if (removed) pendingLogChars -= removed.length;
|
||||
}
|
||||
|
||||
scheduleFlush();
|
||||
}
|
||||
|
||||
export function writeToDailyRenameLog(line: string): void {
|
||||
if (!dailyLogDir) return;
|
||||
|
||||
const dayKey = getDayKey();
|
||||
if (dayKey !== currentDayKey) {
|
||||
currentDayKey = dayKey;
|
||||
}
|
||||
|
||||
pendingRenameLines.push(line);
|
||||
pendingRenameChars += line.length;
|
||||
|
||||
while (pendingRenameChars > BUFFER_LIMIT_CHARS && pendingRenameLines.length > 1) {
|
||||
const removed = pendingRenameLines.shift();
|
||||
if (removed) pendingRenameChars -= removed.length;
|
||||
}
|
||||
|
||||
scheduleFlush();
|
||||
}
|
||||
|
||||
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 */ }
|
||||
}
|
||||
|
||||
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 */ }
|
||||
|
||||
currentDayKey = getDayKey();
|
||||
|
||||
logListener = (line: string) => writeToDailyLog(line);
|
||||
addLogListener(logListener);
|
||||
|
||||
cleanupOldDailyLogs();
|
||||
|
||||
cleanupTimer = setInterval(cleanupOldDailyLogs, CLEANUP_CHECK_INTERVAL_MS);
|
||||
if (cleanupTimer.unref) cleanupTimer.unref();
|
||||
|
||||
process.once("exit", flushSyncOnExit);
|
||||
}
|
||||
|
||||
export function shutdownDailyLog(): void {
|
||||
if (logListener) {
|
||||
removeLogListener(logListener);
|
||||
logListener = null;
|
||||
}
|
||||
if (cleanupTimer) {
|
||||
clearInterval(cleanupTimer);
|
||||
cleanupTimer = null;
|
||||
}
|
||||
if (flushTimer) {
|
||||
clearTimeout(flushTimer);
|
||||
flushTimer = null;
|
||||
}
|
||||
flushSyncOnExit();
|
||||
currentDayKey = "";
|
||||
}
|
||||
|
||||
export function getDailyLogDir(): string {
|
||||
return dailyLogDir;
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { writeToDailyRenameLog } from "./daily-log";
|
||||
|
||||
type RenameLogLevel = "INFO" | "WARN" | "ERROR";
|
||||
|
||||
@ -94,9 +93,11 @@ export function logRenameEvent(level: RenameLogLevel, message: string, fields?:
|
||||
if (!fs.existsSync(renameLogPath)) {
|
||||
fs.writeFileSync(renameLogPath, "", "utf8");
|
||||
}
|
||||
const line = `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`;
|
||||
fs.appendFileSync(renameLogPath, line, "utf8");
|
||||
writeToDailyRenameLog(line);
|
||||
fs.appendFileSync(
|
||||
renameLogPath,
|
||||
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
} catch {
|
||||
// ignore write errors
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user