Storage panel: say what is mounted at /data and warn about Docker's anonymous volume
build-container / build (push) Successful in 2m8s
build-container / build (push) Successful in 2m8s
"mounted" was true even when /data was the hidden anonymous volume Docker creates for the Dockerfile's VOLUME, so the app looked fine while the user's host folder stayed empty and the data was lost when the container was recreated. Read the mount source and report host folder, named volume or anonymous volume (flagged), in Settings and the startup log. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e22d26bf17
commit
ea882cafbf
2 files changed
+18
-6
No files matched your search
+6
-2
@@ -1088,9 +1088,13 @@ function viewSettings() {
|
|||||||
if (st.signedIn) { const b = $('#plexout'); b.hidden = false; b.onclick = async () => { await jfetch('/api/plex/logout', { method: 'POST' }); pl.a.pause(); pl.q = []; pl.i = -1; renderMini(); toast('Signed out of Plex'); viewSettings(); }; }
|
if (st.signedIn) { const b = $('#plexout'); b.hidden = false; b.onclick = async () => { await jfetch('/api/plex/logout', { method: 'POST' }); pl.a.pause(); pl.q = []; pl.i = -1; renderMini(); toast('Signed out of Plex'); viewSettings(); }; }
|
||||||
}).catch(() => { const el = $('#plexst'); if (el) el.textContent = 'Unavailable'; });
|
}).catch(() => { const el = $('#plexst'); if (el) el.textContent = 'Unavailable'; });
|
||||||
jfetch('/api/system').then(i => {
|
jfetch('/api/system').then(i => {
|
||||||
const ok = i.mounted !== false && i.writable, row = (t, d, bad) => `<div class="row"><span class="grow"><b>${t}</b><small style="white-space:normal;${bad ? 'color:var(--danger)' : ''}">${d}</small></span></div>`;
|
const anon = i.source && i.source.kind === 'anonymous', ok = i.mounted !== false && !anon && i.writable;
|
||||||
|
const row = (t, d, bad) => `<div class="row"><span class="grow"><b>${t}</b><small style="white-space:normal;${bad ? 'color:var(--danger)' : ''}">${d}</small></span></div>`;
|
||||||
|
const where = !i.source ? '' : i.source.kind === 'folder' ? ' · host folder ' + esc(i.source.name) : i.source.kind === 'named' ? ' · volume "' + esc(i.source.name) + '"' : '';
|
||||||
$('#stor').innerHTML =
|
$('#stor').innerHTML =
|
||||||
row(ok ? 'Data is saved on a volume' : 'Data is NOT being kept', i.mounted === false ? 'The data folder is not a mounted volume, so passkeys, calendars and sign-ins are lost when the container is recreated. Mount a volume or a host folder at ' + esc(i.dir) + '.' : !i.writable ? 'The data folder is not writable by the app.' : esc(i.dir) + (i.mounted ? ' (mounted)' : ''), !ok)
|
row(ok ? 'Data is saved on a volume' : 'Data is NOT being kept safely', anon ? 'This is a temporary Docker volume (' + esc(i.source.name) + '…), NOT your host folder. It is deleted when the container is recreated, and your folder stays empty. Check that your stack mounts the host folder at ' + esc(i.dir) + '.'
|
||||||
|
: i.mounted === false ? 'The data folder is not a mounted volume, so passkeys, calendars and sign-ins are lost when the container is recreated. Mount a volume or a host folder at ' + esc(i.dir) + '.'
|
||||||
|
: !i.writable ? 'The data folder is not writable by the app.' : esc(i.dir) + ' (mounted' + where + ')', !ok)
|
||||||
+ row('What is stored', `${i.passkeys} passkey${i.passkeys === 1 ? '' : 's'} · ${i.events} NE Hub event${i.events === 1 ? '' : 's'} · ${i.calendarLinks} calendar link${i.calendarLinks === 1 ? '' : 's'} · ${i.settings || 0} saved setting${i.settings === 1 ? '' : 's'}${i.icloud ? ' · iCloud connected' : ''}`)
|
+ row('What is stored', `${i.passkeys} passkey${i.passkeys === 1 ? '' : 's'} · ${i.events} NE Hub event${i.events === 1 ? '' : 's'} · ${i.calendarLinks} calendar link${i.calendarLinks === 1 ? '' : 's'} · ${i.settings || 0} saved setting${i.settings === 1 ? '' : 's'}${i.icloud ? ' · iCloud connected' : ''}`)
|
||||||
+ row('Session secret', i.secretFromEnv ? 'Set in the stack (sign-ins survive restarts)' : 'Generated and kept in the data folder', false)
|
+ row('Session secret', i.secretFromEnv ? 'Set in the stack (sign-ins survive restarts)' : 'Generated and kept in the data folder', false)
|
||||||
+ row('Running since', new Date(i.startedAt).toLocaleString(), false);
|
+ row('Running since', new Date(i.startedAt).toLocaleString(), false);
|
||||||
|
|||||||
@@ -267,19 +267,27 @@ async function plexProxy(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Storage health: is /data a real mounted volume, and what is in it? (so a wiped volume is obvious)
|
// ---- Storage health: is /data a real mounted volume, and what is in it? (so a wiped volume is obvious)
|
||||||
|
/** What is mounted at the data folder? Docker's own anonymous volumes look "mounted" but are lost when the container is recreated. */
|
||||||
|
function classifyMount(mountinfoLine) {
|
||||||
|
const root = (mountinfoLine || '').split(' ')[3] || '';
|
||||||
|
const vol = /\/volumes\/([^/]+)\/_data/.exec(root);
|
||||||
|
if (vol) return /^[0-9a-f]{64}$/.test(vol[1]) ? { kind: 'anonymous', name: vol[1].slice(0, 12) } : { kind: 'named', name: vol[1] };
|
||||||
|
return { kind: 'folder', name: root };
|
||||||
|
}
|
||||||
function storageInfo() {
|
function storageInfo() {
|
||||||
const fs = require('fs'), path = require('path');
|
const fs = require('fs'), path = require('path');
|
||||||
const dir = path.resolve(process.env.DATA_DIR || path.join(__dirname, 'data'));
|
const dir = path.resolve(process.env.DATA_DIR || path.join(__dirname, 'data'));
|
||||||
let mounted = null; // null = cannot tell (not Linux)
|
let mounted = null, source = null; // null = cannot tell (not Linux)
|
||||||
try { mounted = fs.readFileSync('/proc/self/mountinfo', 'utf8').split('\n').some(l => l.split(' ')[4] === dir); } catch {}
|
try { const l = fs.readFileSync('/proc/self/mountinfo', 'utf8').split('\n').find(x => x.split(' ')[4] === dir); mounted = !!l; if (l) source = classifyMount(l); } catch {}
|
||||||
let writable = true; try { const f = path.join(dir, '.write-test'); fs.writeFileSync(f, 'x'); fs.unlinkSync(f); } catch { writable = false; }
|
let writable = true; try { const f = path.join(dir, '.write-test'); fs.writeFileSync(f, 'x'); fs.unlinkSync(f); } catch { writable = false; }
|
||||||
const count = (f, pick) => { try { return pick(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } catch { return 0; } };
|
const count = (f, pick) => { try { return pick(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } catch { return 0; } };
|
||||||
return { dir, mounted, writable, passkeys: count('passkeys.json', j => (j.creds || []).length), calendarLinks: count('calendars.json', j => j.length),
|
return { dir, mounted, source, writable, passkeys: count('passkeys.json', j => (j.creds || []).length), calendarLinks: count('calendars.json', j => j.length),
|
||||||
events: count('events.json', j => j.length), settings: count('settings.json', j => Object.keys(j).length), icloud: fs.existsSync(path.join(dir, 'icloud.json')), secretFromEnv: !!process.env.COOKIE_SECRET,
|
events: count('events.json', j => j.length), settings: count('settings.json', j => Object.keys(j).length), icloud: fs.existsSync(path.join(dir, 'icloud.json')), secretFromEnv: !!process.env.COOKIE_SECRET,
|
||||||
startedAt: STARTED };
|
startedAt: STARTED };
|
||||||
}
|
}
|
||||||
const STARTED = Date.now();
|
const STARTED = Date.now();
|
||||||
{ const i = storageInfo(); console.log(`[data] ${i.dir}: ${i.passkeys} passkeys, ${i.calendarLinks} calendar links, ${i.events} events; mounted volume: ${i.mounted === null ? 'unknown' : i.mounted ? 'yes' : 'NO (data is lost on redeploy)'}; writable: ${i.writable}; COOKIE_SECRET from env: ${i.secretFromEnv}`);
|
{ const i = storageInfo(); console.log(`[data] ${i.dir}: ${i.passkeys} passkeys, ${i.calendarLinks} calendar links, ${i.events} events; mounted: ${i.mounted === null ? 'unknown' : i.mounted ? (i.source.kind + ' ' + i.source.name) : 'NO (data is lost on redeploy)'}; writable: ${i.writable}; COOKIE_SECRET from env: ${i.secretFromEnv}`);
|
||||||
|
if (i.source && i.source.kind === 'anonymous') console.warn('[data] WARNING: /data is an anonymous Docker volume, not your host folder. It is deleted when the container is recreated. Check that your stack mounts the host folder at /data.');
|
||||||
if (i.mounted === false) console.warn('[data] WARNING: the data folder is not a mounted volume. Passkeys, calendars and sessions will be lost whenever the container is recreated. Mount a volume or host folder at /data.'); }
|
if (i.mounted === false) console.warn('[data] WARNING: the data folder is not a mounted volume. Passkeys, calendars and sessions will be lost whenever the container is recreated. Mount a volume or host folder at /data.'); }
|
||||||
|
|
||||||
if (!JWT_SECRET) console.warn('WARNING: ONLYOFFICE_JWT_SECRET not set. In-app editing only works if the Document Server has JWT disabled, and save callbacks are unauthenticated. Testing only.');
|
if (!JWT_SECRET) console.warn('WARNING: ONLYOFFICE_JWT_SECRET not set. In-app editing only works if the Document Server has JWT disabled, and save callbacks are unauthenticated. Testing only.');
|
||||||
|
|||||||
Reference in new issue
Block a user