feat(recent): export all recent uploads (name+link+hoster+time)

Adds an 'Exportieren' button next to 'Alle entfernen' that writes a
pipe-delimited log of every row currently shown in the recent-uploads
panel — so session data doesn't get lost if the log file path is wrong.

Also fixes appendUploadLog silently failing: if the configured path is
unwritable (e.g. C:/Users/<nonexistent>/...), entries now go to
<userData>/fileuploader-fallback.log and the renderer warns once.
This commit is contained in:
Administrator 2026-04-19 11:35:41 +02:00
parent 299fa8a4e5
commit 9a7354fc55
4 changed files with 80 additions and 8 deletions

52
main.js
View File

@ -113,16 +113,37 @@ function getLogFilePath() {
return _dailyLogPath; return _dailyLogPath;
} }
let _uploadLogFallbackWarned = false;
function appendUploadLog(hoster, link, fileName) { function appendUploadLog(hoster, link, fileName) {
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const line = `${dateStr}|${hoster}|${link}||${fileName}|\n`;
const tryWrite = (p) => {
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.appendFileSync(p, line, 'utf-8');
};
try { try {
const logPath = getLogFilePath(); tryWrite(getLogFilePath());
fs.mkdirSync(path.dirname(logPath), { recursive: true }); return;
const now = new Date(); } catch (err) {
const pad = (n) => String(n).padStart(2, '0'); debugLog(`appendUploadLog primary failed (${err.message}); using fallback`);
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; }
const line = `${dateStr}|${hoster}|${link}||${fileName}|\n`;
fs.appendFileSync(logPath, line, 'utf-8'); try {
} catch {} const fallbackPath = path.join(app.getPath('userData'), 'fileuploader-fallback.log');
tryWrite(fallbackPath);
if (!_uploadLogFallbackWarned) {
_uploadLogFallbackWarned = true;
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('upload-log-fallback', { fallbackPath });
}
}
} catch (err) {
debugLog(`appendUploadLog fallback also failed: ${err.message}`);
}
} }
function flattenHistoryForExport(history) { function flattenHistoryForExport(history) {
@ -681,6 +702,21 @@ ipcMain.handle('get-history', () => {
return configStore.loadHistory(); return configStore.loadHistory();
}); });
ipcMain.handle('save-text-file', async (_event, defaultName, content, filters) => {
const safeName = String(defaultName || `export-${new Date().toISOString().slice(0, 10)}.txt`);
const safeFilters = Array.isArray(filters) && filters.length
? filters
: [{ name: 'Textdatei', extensions: ['txt', 'csv', 'log'] }];
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Speichern unter',
defaultPath: safeName,
filters: safeFilters
});
if (canceled || !filePath) return { ok: false, canceled: true };
fs.writeFileSync(filePath, String(content === null || content === undefined ? '' : content), 'utf-8');
return { ok: true, path: filePath };
});
ipcMain.handle('export-history', async (_event, format) => { ipcMain.handle('export-history', async (_event, format) => {
const normalizedFormat = String(format || 'csv').toLowerCase() === 'json' ? 'json' : 'csv'; const normalizedFormat = String(format || 'csv').toLowerCase() === 'json' ? 'json' : 'csv';
const history = configStore.loadHistory(); const history = configStore.loadHistory();

View File

@ -7,6 +7,7 @@ contextBridge.exposeInMainWorld('api', {
getHistory: () => ipcRenderer.invoke('get-history'), getHistory: () => ipcRenderer.invoke('get-history'),
clearHistory: () => ipcRenderer.invoke('clear-history'), clearHistory: () => ipcRenderer.invoke('clear-history'),
exportHistory: (format) => ipcRenderer.invoke('export-history', format), exportHistory: (format) => ipcRenderer.invoke('export-history', format),
saveTextFile: (defaultName, content, filters) => ipcRenderer.invoke('save-text-file', defaultName, content, filters),
// Hoster settings // Hoster settings
getHosterSettings: () => ipcRenderer.invoke('get-hoster-settings'), getHosterSettings: () => ipcRenderer.invoke('get-hoster-settings'),
@ -100,6 +101,9 @@ contextBridge.exposeInMainWorld('api', {
onShutdownCountdown: (callback) => { onShutdownCountdown: (callback) => {
ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data)); ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data));
}, },
onUploadLogFallback: (callback) => {
ipcRenderer.on('upload-log-fallback', (_event, data) => callback(data));
},
// Remote Control // Remote Control
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'), remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings), remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),

View File

@ -104,6 +104,9 @@ async function init() {
handleStats(data); handleStats(data);
}); });
window.api.onShutdownCountdown(handleShutdownCountdown); window.api.onShutdownCountdown(handleShutdownCountdown);
window.api.onUploadLogFallback((data) => {
alert('Der konfigurierte Log-Pfad konnte nicht beschrieben werden.\n\nNeue Einträge werden zwischenzeitlich hier gespeichert:\n' + (data && data.fallbackPath ? data.fallbackPath : '(Fallback)') + '\n\nBitte in den Einstellungen einen gültigen Pfad setzen.');
});
// Folder monitor: auto-queue new files // Folder monitor: auto-queue new files
window.api.onFolderMonitorNewFiles((files) => { window.api.onFolderMonitorNewFiles((files) => {
@ -1122,6 +1125,33 @@ function clearAllRecentFiles() {
renderRecentUploadsPanel(); renderRecentUploadsPanel();
} }
async function exportAllRecentFiles() {
if (sessionFilesData.length === 0) {
alert('Keine Einträge zum Exportieren.');
return;
}
const rows = sortRecentFiles(sessionFilesData);
const header = 'timestamp|hoster|link|filename|status';
const lines = rows.map(r => {
const ts = r.timestamp || r.time || '';
const host = r.host || r.hoster || '';
const link = r.link || '';
const name = r.filename || '';
const status = r.isError ? 'error' : 'ok';
return [ts, host, link, name, status].map(v => String(v).replace(/[\r\n|]/g, ' ')).join('|');
});
const content = [header, ...lines].join('\n') + '\n';
const defaultName = `uploads-${new Date().toISOString().slice(0, 10)}.log`;
try {
const result = await window.api.saveTextFile(defaultName, content, [
{ name: 'Log-Datei', extensions: ['log', 'txt', 'csv'] }
]);
if (result && result.ok) showCopyToast(`${rows.length} Einträge exportiert`);
} catch (err) {
alert('Export fehlgeschlagen: ' + (err.message || err));
}
}
function copySelectedRecentLinks() { function copySelectedRecentLinks() {
const links = sessionFilesData const links = sessionFilesData
.filter(r => selectedRecentIds.has(r.order) && !r.isError) .filter(r => selectedRecentIds.has(r.order) && !r.isError)
@ -3241,6 +3271,7 @@ function setupListeners() {
document.getElementById('accountsRunHealthCheckBtn').addEventListener('click', () => runHealthCheck('manual')); document.getElementById('accountsRunHealthCheckBtn').addEventListener('click', () => runHealthCheck('manual'));
document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks); document.getElementById('copyAllLinksBtn').addEventListener('click', copyAllLinks);
document.getElementById('clearRecentFilesBtn').addEventListener('click', clearAllRecentFiles); document.getElementById('clearRecentFilesBtn').addEventListener('click', clearAllRecentFiles);
document.getElementById('exportRecentFilesBtn').addEventListener('click', exportAllRecentFiles);
document.getElementById('retryFailedBtn').addEventListener('click', () => { document.getElementById('retryFailedBtn').addEventListener('click', () => {
queueJobs.forEach(j => { if (j.status === 'error') selectedJobIds.add(j.id); }); queueJobs.forEach(j => { if (j.status === 'error') selectedJobIds.add(j.id); });
retrySelectedJobs(); retrySelectedJobs();

View File

@ -105,6 +105,7 @@
<button class="recent-tab" data-panel="statsTab">Stats</button> <button class="recent-tab" data-panel="statsTab">Stats</button>
</div> </div>
<span class="recent-files-hint" id="recentFilesHint">Zuletzt erzeugte Upload-Links</span> <span class="recent-files-hint" id="recentFilesHint">Zuletzt erzeugte Upload-Links</span>
<button class="btn btn-xs btn-secondary" id="exportRecentFilesBtn" title="Alle Zeilen als Datei exportieren (Zeit, Hoster, Link, Dateiname)">Exportieren</button>
<button class="btn btn-xs btn-danger" id="clearRecentFilesBtn" title="Alle Links aus diesem Panel entfernen">Alle entfernen</button> <button class="btn btn-xs btn-danger" id="clearRecentFilesBtn" title="Alle Links aus diesem Panel entfernen">Alle entfernen</button>
</div> </div>
<div class="recent-tab-body active" id="filesTab"> <div class="recent-tab-body active" id="filesTab">