feat(diagnostic): Auto-Versionierung fuer /shared/uploads/ + Versions-UI
Stefan-Wunsch: ARIA-Aenderungen an Dateien sollen vom System (nicht
von ARIA selbst) automatisch versioniert werden. Im Datei-Manager:
Versionen auflisten, einzelne downloaden, oder als neue aktive Version
setzen (Restore = non-destructive neuer Commit).
Implementierung (alles im diagnostic-Container, da der eh schon
File-Handling kann):
1. Dockerfile: apk add git
2. server.js — Auto-Commit-Loop:
- Beim Start: /shared/uploads als git-Repo initialisieren (idempotent;
bestehendes .git wird uebernommen)
- setInterval(30s): git status --porcelain → wenn dirty, add+commit
mit "auto: <ISO-Timestamp>"-Message
- Re-Entrancy-Guard fuer langsame git-Ops
3. server.js — drei neue HTTP-Routen:
GET /api/files-versions?path=X
→ [{hash, ts, subject, isCurrent}] aus git log --follow
GET /api/files-version-content?path=X&hash=Y
→ Binary-Stream der Datei aus diesem Commit (Content-Disposition
attachment mit "name@<short-hash>.ext" als Default-Dateiname)
POST /api/files-version-restore body={path, hash}
→ non-destructive: schreibt alten Inhalt als NEUE Version, neuer
Commit "restore: <path> <- <short>". Aktive Version damit
weiterhin rollback-bar.
4. index.html — Datei-Manager:
- Pro Datei zusaetzlich 🕒-Button neben ⬇/🗑
- Klick zeigt Modal mit Version-Liste (timestamp, short-hash,
'AKTIV'-Marker fuer den jeweils letzten)
- Pro Version: ⬇ Download + ⟲ Restore (mit Confirm)
- Restore broadcasted file_version_restored damit Browser refreshen
Path-Safety: alle Pfade muessen relative-to-uploads sein, kein '..',
kein '/', kein '.git/'. Hash muss [0-9a-f]{7,40}.
.gitignore zunaechst keine — uploads/ ist eh nur User-/ARIA-Dateien,
kein Log-Noise erwartet. Falls Disk explodiert: spaeter ergaenzen.
Step-2 (App-Side via RVS-Messages) folgt im naechsten Commit, sobald
das hier in Diagnostic funktioniert.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
# zip fuer Multi-Datei-Downloads (Brain-Export nutzt tar.gz, Datei-Manager zip)
|
||||
RUN apk add --no-cache zip
|
||||
# git fuer Auto-Versionierung von /shared/uploads/ (siehe server.js)
|
||||
RUN apk add --no-cache zip git
|
||||
COPY package.json ./
|
||||
RUN npm install --production
|
||||
COPY . .
|
||||
|
||||
@@ -4039,11 +4039,83 @@
|
||||
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
|
||||
</div>
|
||||
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen">⬇</button>
|
||||
<button class="btn secondary" onclick="showVersions('${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;" title="Versionen">🕒</button>
|
||||
<button class="btn secondary" onclick="deleteFile('${pathEsc}','${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Versions-Modal ──────────────────────────────────────
|
||||
async function showVersions(fileName) {
|
||||
// path-relative-to-/shared/uploads ist hier == fileName, weil unser
|
||||
// file-Manager-Verzeichnis flach ist
|
||||
const rel = fileName;
|
||||
const modal = document.getElementById('versions-modal');
|
||||
const title = document.getElementById('versions-title');
|
||||
const body = document.getElementById('versions-body');
|
||||
title.textContent = `Versionen — ${fileName}`;
|
||||
body.innerHTML = '<div style="color:#8888AA;text-align:center;padding:20px;">Lade...</div>';
|
||||
modal.style.display = 'flex';
|
||||
modal.dataset.path = rel;
|
||||
try {
|
||||
const r = await fetch('/api/files-versions?path=' + encodeURIComponent(rel));
|
||||
const d = await r.json();
|
||||
if (!d.ok) throw new Error(d.error || 'Fehler');
|
||||
if (!d.versions.length) {
|
||||
body.innerHTML = '<div style="color:#8888AA;text-align:center;padding:20px;">Noch keine Versions-Historie (Datei kommt erst nach naechstem Auto-Commit in den Index).</div>';
|
||||
return;
|
||||
}
|
||||
const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
body.innerHTML = d.versions.map(v => {
|
||||
const isCur = v.isCurrent
|
||||
? '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">AKTIV</span>'
|
||||
: '';
|
||||
const subjShort = (v.subject || '').slice(0, 60);
|
||||
return `<div style="padding:10px;border-bottom:1px solid #1E1E2E;display:flex;gap:8px;align-items:center;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="color:#E0E0F0;font-size:12px;">${isCur}<code style="color:#0096FF;">${v.hash.slice(0,7)}</code> · ${escapeHtml(subjShort)}</div>
|
||||
<div style="color:#555570;font-size:10px;">${fmtDate(v.ts)}</div>
|
||||
</div>
|
||||
<button class="btn secondary" onclick="downloadVersion('${escapeHtml(rel)}','${v.hash}')" style="padding:3px 10px;font-size:11px;">⬇ Download</button>
|
||||
${v.isCurrent ? '' : `<button class="btn" onclick="restoreVersion('${escapeHtml(rel)}','${v.hash}')" style="padding:3px 10px;font-size:11px;background:#0096FF;color:#fff;">⟲ Restore</button>`}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
body.innerHTML = `<div style="color:#FF6B6B;padding:20px;">${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeVersionsModal() {
|
||||
document.getElementById('versions-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
function downloadVersion(rel, hash) {
|
||||
const url = '/api/files-version-content?path=' + encodeURIComponent(rel) + '&hash=' + encodeURIComponent(hash);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = '';
|
||||
document.body.appendChild(a); a.click();
|
||||
setTimeout(() => a.remove(), 100);
|
||||
}
|
||||
|
||||
async function restoreVersion(rel, hash) {
|
||||
if (!confirm(`Diese Version (${hash.slice(0,7)}) als aktive Version setzen?\n\nDie aktuelle Version bleibt rollback-bar in der Historie.`)) return;
|
||||
try {
|
||||
const r = await fetch('/api/files-version-restore', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: rel, hash }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!d.ok) throw new Error(d.error || 'Fehler');
|
||||
// Modal neu laden mit aktualisierter Liste
|
||||
showVersions(rel);
|
||||
loadFiles();
|
||||
} catch (e) {
|
||||
alert('Restore fehlgeschlagen: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSelected() {
|
||||
const paths = [...filesSelected];
|
||||
if (!paths.length) return;
|
||||
@@ -5612,5 +5684,16 @@
|
||||
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
|
||||
loadAriaStreamHistory();
|
||||
</script>
|
||||
|
||||
<!-- Versions-Modal fuer Datei-Manager -->
|
||||
<div id="versions-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.75);z-index:1000;align-items:center;justify-content:center;" onclick="if(event.target===this)closeVersionsModal()">
|
||||
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;">
|
||||
<div style="padding:12px 16px;border-bottom:1px solid #1E1E2E;display:flex;align-items:center;gap:8px;">
|
||||
<strong id="versions-title" style="color:#E0E0F0;flex:1;font-size:13px;">Versionen</strong>
|
||||
<button class="btn secondary" onclick="closeVersionsModal()" style="padding:4px 10px;font-size:11px;">✕ Schliessen</button>
|
||||
</div>
|
||||
<div id="versions-body" style="overflow-y:auto;padding:4px 12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user