Compare commits
3 Commits
1f622c5cc2
...
72d3fe1e4e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72d3fe1e4e | ||
|
|
d720ba295a | ||
|
|
1c8514e127 |
@ -1,6 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const secretStore = require('./secret-store');
|
const secretStore = require('./secret-store');
|
||||||
|
const { normalizeLogMode } = require('./log-mode');
|
||||||
|
|
||||||
const HOSTER_SETTINGS_DEFAULTS = {
|
const HOSTER_SETTINGS_DEFAULTS = {
|
||||||
retries: 3,
|
retries: 3,
|
||||||
@ -56,7 +57,11 @@ const DEFAULTS = {
|
|||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart
|
shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart
|
||||||
logFilePath: '',
|
logFilePath: '',
|
||||||
sessionLog: false,
|
sessionLog: false, // legacy boolean (kept for back-compat reads); normalized into logMode on load
|
||||||
|
// NOTE: logMode is intentionally NOT in DEFAULTS. If it were, the deep-merge
|
||||||
|
// would seed logMode='single' for every load, which would beat (and silently
|
||||||
|
// erase) the legacy sessionLog:true → "daily" migration. normalizeLogMode in
|
||||||
|
// load() sets logMode after the merge, looking at the saved-only data.
|
||||||
resumeQueueOnLaunch: true,
|
resumeQueueOnLaunch: true,
|
||||||
parallelUploadCount: 0, // 0 = use per-hoster limits only
|
parallelUploadCount: 0, // 0 = use per-hoster limits only
|
||||||
scaleParallelUploads: false,
|
scaleParallelUploads: false,
|
||||||
@ -145,7 +150,11 @@ class ConfigStore {
|
|||||||
const backupPath = this.filePath + '.bak';
|
const backupPath = this.filePath + '.bak';
|
||||||
try { data = this._readAndParse(backupPath); } catch {}
|
try { data = this._readAndParse(backupPath); } catch {}
|
||||||
}
|
}
|
||||||
if (!data) return JSON.parse(JSON.stringify(DEFAULTS));
|
if (!data) {
|
||||||
|
const fresh = JSON.parse(JSON.stringify(DEFAULTS));
|
||||||
|
fresh.globalSettings.logMode = normalizeLogMode(fresh.globalSettings);
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate old single-object format to array format
|
// Migrate old single-object format to array format
|
||||||
for (const [name, val] of Object.entries(data.hosters || {})) {
|
for (const [name, val] of Object.entries(data.hosters || {})) {
|
||||||
@ -207,13 +216,20 @@ class ConfigStore {
|
|||||||
globalSettings[key] = { ...def, ...(savedGlobal[key] || {}) };
|
globalSettings[key] = { ...def, ...(savedGlobal[key] || {}) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Normalize logMode at this single boundary. Legacy sessionLog: true
|
||||||
|
// means *daily* (the old field was named after a misnomer); see log-mode.js.
|
||||||
|
// Downstream readers consume logMode only and must NOT derive from
|
||||||
|
// sessionLog at call sites.
|
||||||
|
globalSettings.logMode = normalizeLogMode(globalSettings);
|
||||||
const result = { hosters, hosterSettings, globalSettings, history: data.history || [] };
|
const result = { hosters, hosterSettings, globalSettings, history: data.history || [] };
|
||||||
// Decrypt credentials stored with safeStorage so the rest of the app
|
// Decrypt credentials stored with safeStorage so the rest of the app
|
||||||
// keeps working with plaintext in memory.
|
// keeps working with plaintext in memory.
|
||||||
secretStore.decryptCredentials(result);
|
secretStore.decryptCredentials(result);
|
||||||
return result;
|
return result;
|
||||||
} catch {
|
} catch {
|
||||||
return JSON.parse(JSON.stringify(DEFAULTS));
|
const fresh = JSON.parse(JSON.stringify(DEFAULTS));
|
||||||
|
fresh.globalSettings.logMode = normalizeLogMode(fresh.globalSettings);
|
||||||
|
return fresh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
85
lib/log-mode.js
Normal file
85
lib/log-mode.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Log-file mode resolution for fileuploader.log:
|
||||||
|
// - "single" → one file: fileuploader.log
|
||||||
|
// - "daily" → per-day: fileuploader-YYYY-MM-DD.log
|
||||||
|
// - "session" → per-launch: fileuploader-session-YYYY-MM-DD_HH-MM-SS-<pid>.log
|
||||||
|
//
|
||||||
|
// Pure functions only — no fs, no Date.now() at call time — so they unit-test
|
||||||
|
// cleanly and the main.js call sites pass in `new Date()` + the session stamp.
|
||||||
|
//
|
||||||
|
// MIGRATION TRAP this lib protects against: the legacy boolean was named
|
||||||
|
// `sessionLog` but actually toggled *daily* mode. A naive rename would silently
|
||||||
|
// flip every per-day user onto per-session. normalizeLogMode below maps the
|
||||||
|
// legacy `sessionLog: true` to "daily", NOT "session". Read logMode everywhere
|
||||||
|
// downstream; do not derive from sessionLog at call sites.
|
||||||
|
//
|
||||||
|
// Loaded both as CommonJS (main.js, tests) and as a browser global
|
||||||
|
// (renderer/app.js via index.html script tag) so a single implementation backs
|
||||||
|
// runtime and tests — same pattern as queue-prune.js / queue-dedup.js.
|
||||||
|
|
||||||
|
(function (root) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const VALID_MODES = new Set(['single', 'daily', 'session']);
|
||||||
|
|
||||||
|
function normalizeLogMode(globalSettings) {
|
||||||
|
const gs = globalSettings && typeof globalSettings === 'object' ? globalSettings : {};
|
||||||
|
if (typeof gs.logMode === 'string' && VALID_MODES.has(gs.logMode)) {
|
||||||
|
return gs.logMode;
|
||||||
|
}
|
||||||
|
// Legacy boolean migration: sessionLog *named* like "session" but actually
|
||||||
|
// implemented "daily" — preserve daily users on the migration path.
|
||||||
|
if (gs.sessionLog === true) return 'daily';
|
||||||
|
return 'single';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _two(n) { return String(n).padStart(2, '0'); }
|
||||||
|
|
||||||
|
function formatDateStamp(date) {
|
||||||
|
return `${date.getFullYear()}-${_two(date.getMonth() + 1)}-${_two(date.getDate())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSessionStamp(date, pid) {
|
||||||
|
const d = `${date.getFullYear()}-${_two(date.getMonth() + 1)}-${_two(date.getDate())}`;
|
||||||
|
const t = `${_two(date.getHours())}-${_two(date.getMinutes())}-${_two(date.getSeconds())}`;
|
||||||
|
// PID disambiguates a same-second close→reopen — a human can't but two
|
||||||
|
// automated runs might. Cheap belt to a suspenders-not-required problem.
|
||||||
|
const pidStr = pid !== undefined && pid !== null ? `-${pid}` : '';
|
||||||
|
return `${d}_${t}${pidStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the log filename for the given mode + clock.
|
||||||
|
* @param {Object} args
|
||||||
|
* @param {string} args.baseName e.g. "fileuploader"
|
||||||
|
* @param {string} args.ext e.g. ".log"
|
||||||
|
* @param {string} args.mode "single" | "daily" | "session"
|
||||||
|
* @param {Date} args.date current timestamp
|
||||||
|
* @param {string} [args.sessionId] required when mode === "session"
|
||||||
|
* @returns {string} the bare filename (no directory)
|
||||||
|
*/
|
||||||
|
function resolveLogFileName(args) {
|
||||||
|
const a = args || {};
|
||||||
|
const base = String(a.baseName || 'fileuploader');
|
||||||
|
const ext = String(a.ext || '.log');
|
||||||
|
const mode = VALID_MODES.has(a.mode) ? a.mode : 'single';
|
||||||
|
if (mode === 'single') return `${base}${ext}`;
|
||||||
|
if (mode === 'daily') {
|
||||||
|
const date = a.date instanceof Date ? a.date : new Date();
|
||||||
|
return `${base}-${formatDateStamp(date)}${ext}`;
|
||||||
|
}
|
||||||
|
// session
|
||||||
|
const sid = a.sessionId && String(a.sessionId).trim();
|
||||||
|
if (sid) return `${base}-session-${sid}${ext}`;
|
||||||
|
// Defensive: if a session-id wasn't passed, fall back to single rather
|
||||||
|
// than emit a malformed name. main.js always supplies one.
|
||||||
|
return `${base}${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp, VALID_MODES };
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = api;
|
||||||
|
} else if (root) {
|
||||||
|
root.LogMode = api;
|
||||||
|
}
|
||||||
|
})(typeof window !== 'undefined' ? window : this);
|
||||||
56
main.js
56
main.js
@ -240,40 +240,41 @@ function getBaseLogFilePath() {
|
|||||||
return customPath || getDefaultLogFilePath();
|
return customPath || getDefaultLogFilePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daily log: one file per day, reused across sessions on the same day
|
// Log-mode bookkeeping. Three modes (see lib/log-mode.js): single, daily, session.
|
||||||
let _dailyLogPath = null;
|
// The session-id is stamped ONCE at main-process startup so every write of a
|
||||||
let _dailyLogDate = null;
|
// given session lands in the same file. A close→reopen of the app starts a new
|
||||||
|
// main process, so a new SESSION_ID, so a new session file. PID is appended as
|
||||||
|
// a cheap hedge against same-second restart collisions.
|
||||||
|
const { resolveLogFileName, formatSessionStamp, formatDateStamp } = require('./lib/log-mode');
|
||||||
|
const SESSION_ID = formatSessionStamp(new Date(), process.pid);
|
||||||
|
let _activeLogKey = null; // remembers (mode + date-or-session) so cache rolls correctly
|
||||||
|
let _activeLogPath = null;
|
||||||
|
|
||||||
function getLogFilePath() {
|
function getLogFilePath() {
|
||||||
const config = configStore.load();
|
const config = configStore.load();
|
||||||
const useDailyLog = config && config.globalSettings && config.globalSettings.sessionLog;
|
const mode = (config && config.globalSettings && config.globalSettings.logMode) || 'single';
|
||||||
if (!useDailyLog) return getBaseLogFilePath();
|
const base = getBaseLogFilePath();
|
||||||
|
const dir = path.dirname(base);
|
||||||
|
const ext = path.extname(base);
|
||||||
|
const name = path.basename(base, ext);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
// Cache key changes when the user toggles mode mid-run OR when the daily date
|
||||||
const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
// rolls over at midnight — so the cached path can't be served stale.
|
||||||
|
const datePart = mode === 'daily' ? formatDateStamp(now) : '';
|
||||||
// Reuse path if same day, otherwise generate new
|
const key = `${mode}|${datePart}|${SESSION_ID}|${base}`;
|
||||||
if (_dailyLogDate !== today) {
|
if (_activeLogKey !== key) {
|
||||||
const base = getBaseLogFilePath();
|
_activeLogPath = path.join(dir, resolveLogFileName({ baseName: name, ext, mode, date: now, sessionId: SESSION_ID }));
|
||||||
const dir = path.dirname(base);
|
_activeLogKey = key;
|
||||||
const ext = path.extname(base);
|
|
||||||
const name = path.basename(base, ext);
|
|
||||||
_dailyLogPath = path.join(dir, `${name}-${today}${ext}`);
|
|
||||||
_dailyLogDate = today;
|
|
||||||
}
|
}
|
||||||
return _dailyLogPath;
|
return _activeLogPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFallbackLogName(dir) {
|
function buildFallbackLogName(dir) {
|
||||||
// Match the daily-log naming when enabled, so fallback files stay consistent.
|
// Match the active log-mode's naming so the fallback file is consistent with
|
||||||
|
// what the primary write would have produced.
|
||||||
const config = configStore.load();
|
const config = configStore.load();
|
||||||
const useDailyLog = config && config.globalSettings && config.globalSettings.sessionLog;
|
const mode = (config && config.globalSettings && config.globalSettings.logMode) || 'single';
|
||||||
if (!useDailyLog) return path.join(dir, 'fileuploader.log');
|
return path.join(dir, resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode, date: new Date(), sessionId: SESSION_ID }));
|
||||||
const now = new Date();
|
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
|
||||||
const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
||||||
return path.join(dir, `fileuploader-${today}.log`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSafeDesktopDir() {
|
function getSafeDesktopDir() {
|
||||||
@ -305,7 +306,10 @@ function _invalidateUploadLogTargetCache() {
|
|||||||
|
|
||||||
function _resolveUploadLogTarget() {
|
function _resolveUploadLogTarget() {
|
||||||
const primary = getLogFilePath();
|
const primary = getLogFilePath();
|
||||||
const key = `${primary}|${_dailyLogDate || ''}`;
|
// The primary path already encodes the mode + date/session, so it changes
|
||||||
|
// when the user toggles mode, daily rolls at midnight, or this is a new
|
||||||
|
// process — cache invalidates naturally on path change.
|
||||||
|
const key = primary;
|
||||||
if (_cachedUploadLogKey === key && _cachedUploadLogTarget) return _cachedUploadLogTarget;
|
if (_cachedUploadLogKey === key && _cachedUploadLogTarget) return _cachedUploadLogTarget;
|
||||||
|
|
||||||
const commit = (t) => {
|
const commit = (t) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "3.3.34",
|
"version": "3.3.35",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -2552,8 +2552,13 @@ function renderSettings() {
|
|||||||
<button class="btn btn-xs btn-secondary" id="openLogFolderBtn" title="Log-Ordner im Explorer öffnen">Öffnen</button>
|
<button class="btn btn-xs btn-secondary" id="openLogFolderBtn" title="Log-Ordner im Explorer öffnen">Öffnen</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Neues Log pro Tag</label>
|
<label>Log-Datei-Modus</label>
|
||||||
<input type="checkbox" class="settings-autosave" id="sessionLogInput" ${globalSettings.sessionLog ? 'checked' : ''}>
|
<select class="hs-input settings-autosave" id="logModeInput">
|
||||||
|
<option value="single" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'single' ? 'selected' : ''}>Eine Datei</option>
|
||||||
|
<option value="daily" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'daily' ? 'selected' : ''}>Pro Tag</option>
|
||||||
|
<option value="session" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'session' ? 'selected' : ''}>Pro Session</option>
|
||||||
|
</select>
|
||||||
|
<span class="hint">Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -2897,7 +2902,10 @@ async function saveSettings(options = {}) {
|
|||||||
const globalSettings = {
|
const globalSettings = {
|
||||||
...(config.globalSettings || {}),
|
...(config.globalSettings || {}),
|
||||||
logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
|
logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
|
||||||
sessionLog: !!document.getElementById('sessionLogInput')?.checked,
|
logMode: (() => {
|
||||||
|
const v = document.getElementById('logModeInput')?.value;
|
||||||
|
return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single';
|
||||||
|
})(),
|
||||||
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
|
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
|
||||||
parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
|
parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
|
||||||
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
|
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
|
||||||
|
|||||||
@ -332,6 +332,7 @@
|
|||||||
|
|
||||||
<script src="../lib/queue-prune.js"></script>
|
<script src="../lib/queue-prune.js"></script>
|
||||||
<script src="../lib/queue-dedup.js"></script>
|
<script src="../lib/queue-dedup.js"></script>
|
||||||
|
<script src="../lib/log-mode.js"></script>
|
||||||
<script src="../lib/throttled-cache.js"></script>
|
<script src="../lib/throttled-cache.js"></script>
|
||||||
<script src="../lib/coalesced-set.js"></script>
|
<script src="../lib/coalesced-set.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|||||||
@ -54,3 +54,11 @@
|
|||||||
**Fix:** Die offizielle doodapi.co JSON-API nutzen, wenn ein API-Key da ist — sie liefert result[0].filecode DIREKT in JSON (kein HTML-Formular) und nutzt einen persistenten api_key (kein alternder sess_id). Git-Historie: die API war der ORIGINAL-Pfad (initial commit); Web-Login kam später nur "als Alternative zum API-Key" — Key-Bevorzugung stellt also den gedachten Primärpfad wieder her, kämpft nicht gegen eine bewusste Entscheidung.
|
**Fix:** Die offizielle doodapi.co JSON-API nutzen, wenn ein API-Key da ist — sie liefert result[0].filecode DIREKT in JSON (kein HTML-Formular) und nutzt einen persistenten api_key (kein alternder sess_id). Git-Historie: die API war der ORIGINAL-Pfad (initial commit); Web-Login kam später nur "als Alternative zum API-Key" — Key-Bevorzugung stellt also den gedachten Primärpfad wieder her, kämpft nicht gegen eine bewusste Entscheidung.
|
||||||
**Regel:** Bei Hoster-Integrationen die offizielle API der Web-Scraping-Ebene vorziehen wo möglich. Empty-form/codeless-2xx = Hoster-Backend-Flake (hosterTransient), Account NICHT als tot markieren — auf BEIDEN Pfaden (Web + API) gleich klassifizieren.
|
**Regel:** Bei Hoster-Integrationen die offizielle API der Web-Scraping-Ebene vorziehen wo möglich. Empty-form/codeless-2xx = Hoster-Backend-Flake (hosterTransient), Account NICHT als tot markieren — auf BEIDEN Pfaden (Web + API) gleich klassifizieren.
|
||||||
**Voraussetzung:** Engagiert nur wenn der Doodstream-Account einen gültigen API-Key hat (doodstream.com/settings). Keyless-Accounts bleiben beim Web-Pfad.
|
**Voraussetzung:** Engagiert nur wenn der Doodstream-Account einen gültigen API-Key hat (doodstream.com/settings). Keyless-Accounts bleiben beim Web-Pfad.
|
||||||
|
|
||||||
|
## 2026-05-28 — Doodstream empty-form: live diagnosis confirmed API path is the fix
|
||||||
|
**Verifiziert mit echtem Account-Key (read-only API-Calls):**
|
||||||
|
- account/info → status 200, Key gültig, Storage unlimited. Premium ABGELAUFEN (2025-10-03) — Uploads gehen TROTZDEM.
|
||||||
|
- upload/server → liefert gültigen Node (cv1130ed.cloudatacdn.com) auch ohne Premium → API-Upload-Pfad nutzbar.
|
||||||
|
- file/list → 90.548 Dateien; Uploads landen server-seitig INTERMITTIEREND (viele Burn-Notice-Folgen genau im "Fehler"-Zeitfenster vorhanden). Das leere Formular ist also nicht "immer kaputt", sondern manchmal — der Web-Form-Registrierungs-Callback (fs-public.intconnect.net) timeoutet sporadisch.
|
||||||
|
**Konsequenz:** API-Weg (result[0].filecode inline) umgeht den failenden Callback → richtiger Fix. file/list-Recovery ist NICHT tote Last (Dateien erscheinen ja) — aber bei 90k-Accounts MUSS man sort=created&order=desc erzwingen, sonst ist die frische Datei nicht auf Seite 1.
|
||||||
|
**Regel:** Bei "geht manchmal/manchmal nicht" + Hoster mit offizieller API: erst per read-only API-Call (account/info, file/list) gegen den ECHTEN Account verifizieren statt am Client weiterzuraten. Das beendet Spekulations-Schleifen.
|
||||||
|
|||||||
@ -56,6 +56,28 @@ describe('ConfigStore', () => {
|
|||||||
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'test-key-123');
|
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'test-key-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('default logMode is "single"', () => {
|
||||||
|
const config = store.load();
|
||||||
|
assert.equal(config.globalSettings.logMode, 'single');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('regression: legacy sessionLog:true on disk normalizes to logMode "daily" (NOT "session")', async () => {
|
||||||
|
// Write a config with the legacy boolean only (what an existing user has).
|
||||||
|
await store.save({ globalSettings: { sessionLog: true } });
|
||||||
|
const config = store.load();
|
||||||
|
// The misnamed legacy field MUST map to daily — mapping to "session" would
|
||||||
|
// silently change every per-day user's behaviour on upgrade.
|
||||||
|
assert.equal(config.globalSettings.logMode, 'daily');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logMode round-trips for all three values', async () => {
|
||||||
|
for (const mode of ['single', 'daily', 'session']) {
|
||||||
|
await store.save({ globalSettings: { logMode: mode } });
|
||||||
|
const config = store.load();
|
||||||
|
assert.equal(config.globalSettings.logMode, mode, `mode ${mode}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('load merges with defaults for missing hosters', () => {
|
it('load merges with defaults for missing hosters', () => {
|
||||||
// Write partial config in old single-object format (triggers migration)
|
// Write partial config in old single-object format (triggers migration)
|
||||||
fs.writeFileSync(store.filePath, JSON.stringify({
|
fs.writeFileSync(store.filePath, JSON.stringify({
|
||||||
|
|||||||
96
tests/log-mode.test.js
Normal file
96
tests/log-mode.test.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp } = require('../lib/log-mode');
|
||||||
|
|
||||||
|
// --- normalizeLogMode ---
|
||||||
|
|
||||||
|
test('normalizeLogMode: default for empty/null/undefined is "single"', () => {
|
||||||
|
assert.equal(normalizeLogMode(), 'single');
|
||||||
|
assert.equal(normalizeLogMode(null), 'single');
|
||||||
|
assert.equal(normalizeLogMode({}), 'single');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeLogMode: explicit logMode wins for all three valid values', () => {
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'single' }), 'single');
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'daily' }), 'daily');
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'session' }), 'session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('regression: legacy sessionLog:true maps to "daily", NOT "session"', () => {
|
||||||
|
// The legacy boolean field was named after a misnomer — it actually toggled
|
||||||
|
// per-day logging. Mapping it to "session" would silently flip every existing
|
||||||
|
// per-day user onto per-session, which is exactly the bug the migration trap
|
||||||
|
// exists to prevent.
|
||||||
|
assert.equal(normalizeLogMode({ sessionLog: true }), 'daily');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeLogMode: sessionLog:false / missing maps to "single"', () => {
|
||||||
|
assert.equal(normalizeLogMode({ sessionLog: false }), 'single');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeLogMode: explicit logMode beats the legacy sessionLog field', () => {
|
||||||
|
// Once a user picks a mode in 3.3.35+, the legacy boolean must NOT override.
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'session', sessionLog: true }), 'session');
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'single', sessionLog: true }), 'single');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeLogMode: invalid logMode strings fall through to single (or legacy if present)', () => {
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'lolnope' }), 'single');
|
||||||
|
assert.equal(normalizeLogMode({ logMode: '' }), 'single');
|
||||||
|
assert.equal(normalizeLogMode({ logMode: 'lolnope', sessionLog: true }), 'daily');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- resolveLogFileName ---
|
||||||
|
|
||||||
|
test('resolveLogFileName: single mode → bare basename + ext', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'single' }),
|
||||||
|
'fileuploader.log'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveLogFileName: daily mode → fileuploader-YYYY-MM-DD.log', () => {
|
||||||
|
const d = new Date(2026, 4, 28); // May 28, 2026 — month is 0-indexed
|
||||||
|
assert.equal(
|
||||||
|
resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'daily', date: d }),
|
||||||
|
'fileuploader-2026-05-28.log'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveLogFileName: session mode → fileuploader-session-<id>.log', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'session', sessionId: '2026-05-28_22-44-52-12345' }),
|
||||||
|
'fileuploader-session-2026-05-28_22-44-52-12345.log'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveLogFileName: session mode with missing sessionId falls back to single (never emits malformed name)', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'session' }),
|
||||||
|
'fileuploader.log'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveLogFileName: unknown mode is treated as single', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveLogFileName({ baseName: 'fileuploader', ext: '.log', mode: 'lolnope' }),
|
||||||
|
'fileuploader.log'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- format helpers ---
|
||||||
|
|
||||||
|
test('formatDateStamp: zero-pads month and day', () => {
|
||||||
|
assert.equal(formatDateStamp(new Date(2026, 0, 3)), '2026-01-03');
|
||||||
|
assert.equal(formatDateStamp(new Date(2026, 11, 31)), '2026-12-31');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatSessionStamp: produces YYYY-MM-DD_HH-MM-SS-pid', () => {
|
||||||
|
const d = new Date(2026, 4, 28, 7, 9, 5);
|
||||||
|
assert.equal(formatSessionStamp(d, 12345), '2026-05-28_07-09-05-12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatSessionStamp: omits the pid suffix when none provided', () => {
|
||||||
|
const d = new Date(2026, 4, 28, 22, 44, 52);
|
||||||
|
assert.equal(formatSessionStamp(d), '2026-05-28_22-44-52');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user