added move/copy support
This commit is contained in:
@@ -352,7 +352,7 @@ function renderFileManagerPage(title, body) {
|
||||
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.actions button { font-size: 0.9rem; padding: 7px 10px; }
|
||||
dialog { border: none; border-radius: 16px; padding: 0; width: min(480px, 92vw); }
|
||||
dialog { border: none; border-radius: 16px; padding: 0; width: min(520px, 92vw); }
|
||||
dialog::backdrop { background: rgba(15, 23, 42, 0.4); }
|
||||
.dialog-card { padding: 18px; display: grid; gap: 12px; }
|
||||
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
@@ -990,6 +990,8 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
<td>${modifiedAt ? formatTimestamp(modifiedAt) : '—'}</td>
|
||||
<td class="actions">
|
||||
<button type="button" class="secondary rename-trigger" data-path="${escapedPath}" data-name="${escapedName}">Umbenennen</button>
|
||||
<button type="button" class="secondary move-trigger" data-path="${escapedPath}" data-name="${escapedName}">Verschieben</button>
|
||||
<button type="button" class="secondary copy-trigger" data-path="${escapedPath}" data-name="${escapedName}">Kopieren</button>
|
||||
<button type="button" class="secondary delete-trigger" data-path="${escapedPath}" data-name="${escapedName}">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1106,14 +1108,56 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="move-dialog">
|
||||
<form method="post" action="${baseUrl('/admin/files/move')}" class="dialog-card">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<input type="hidden" name="path" id="move-path" />
|
||||
<h2>Verschieben</h2>
|
||||
<div class="muted" id="move-current"></div>
|
||||
<label>
|
||||
Zielpfad (relativ)
|
||||
<input name="targetPath" id="move-target" placeholder="z.B. archiv/ordner" required />
|
||||
</label>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="secondary dialog-close">Abbrechen</button>
|
||||
<button type="submit">Verschieben</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="copy-dialog">
|
||||
<form method="post" action="${baseUrl('/admin/files/copy')}" class="dialog-card">
|
||||
${csrfField(res.locals.csrfToken)}
|
||||
<input type="hidden" name="path" id="copy-path" />
|
||||
<h2>Kopieren</h2>
|
||||
<div class="muted" id="copy-current"></div>
|
||||
<label>
|
||||
Zielpfad (relativ)
|
||||
<input name="targetPath" id="copy-target" placeholder="z.B. archiv/ordner" required />
|
||||
</label>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="secondary dialog-close">Abbrechen</button>
|
||||
<button type="submit">Kopieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
const renameDialog = document.getElementById('rename-dialog');
|
||||
const deleteDialog = document.getElementById('delete-dialog');
|
||||
const moveDialog = document.getElementById('move-dialog');
|
||||
const copyDialog = document.getElementById('copy-dialog');
|
||||
const renamePath = document.getElementById('rename-path');
|
||||
const renameName = document.getElementById('rename-name');
|
||||
const renameCurrent = document.getElementById('rename-current');
|
||||
const deletePath = document.getElementById('delete-path');
|
||||
const deleteCurrent = document.getElementById('delete-current');
|
||||
const movePath = document.getElementById('move-path');
|
||||
const moveTarget = document.getElementById('move-target');
|
||||
const moveCurrent = document.getElementById('move-current');
|
||||
const copyPath = document.getElementById('copy-path');
|
||||
const copyTarget = document.getElementById('copy-target');
|
||||
const copyCurrent = document.getElementById('copy-current');
|
||||
|
||||
document.querySelectorAll('.rename-trigger').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -1124,6 +1168,24 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.move-trigger').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
movePath.value = btn.dataset.path || '';
|
||||
moveTarget.value = '';
|
||||
moveCurrent.textContent = 'Quelle: ' + (btn.dataset.name || '');
|
||||
moveDialog.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.copy-trigger').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
copyPath.value = btn.dataset.path || '';
|
||||
copyTarget.value = '';
|
||||
copyCurrent.textContent = 'Quelle: ' + (btn.dataset.name || '');
|
||||
copyDialog.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-trigger').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
deletePath.value = btn.dataset.path || '';
|
||||
@@ -1136,6 +1198,8 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
||||
btn.addEventListener('click', () => {
|
||||
renameDialog.close();
|
||||
deleteDialog.close();
|
||||
moveDialog.close();
|
||||
copyDialog.close();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1235,6 +1299,73 @@ app.post(`${basePath}/admin/files/delete`, requireAdminPage, async (req, res) =>
|
||||
res.redirect(baseUrl(`/admin/files?path=${encodeURIComponent(nextPath)}`));
|
||||
});
|
||||
|
||||
app.post(`${basePath}/admin/files/move`, requireAdminPage, async (req, res) => {
|
||||
const relativePath = String(req.body.path || '').replace(/^\/+/, '');
|
||||
const targetPath = String(req.body.targetPath || '').replace(/^\/+/, '');
|
||||
if (!relativePath || !targetPath) {
|
||||
res.status(400).send(renderFileManagerPage('Admin-Dateien', '<p class="card">Ungültige Eingabe.</p>'));
|
||||
return;
|
||||
}
|
||||
const source = resolveAdminPath(relativePath);
|
||||
const targetBase = resolveAdminPath(targetPath);
|
||||
if (!source || !targetBase) {
|
||||
res.status(400).send(renderFileManagerPage('Admin-Dateien', '<p class="card">Ungültiger Pfad.</p>'));
|
||||
return;
|
||||
}
|
||||
|
||||
let target = targetBase;
|
||||
try {
|
||||
const stat = await fs.promises.stat(targetBase);
|
||||
if (stat.isDirectory()) {
|
||||
target = path.join(targetBase, path.basename(source));
|
||||
}
|
||||
} catch (err) {
|
||||
// targetBase does not exist; treat as file/dir path.
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.rename(source, target);
|
||||
} catch (err) {
|
||||
if (err.code === 'EXDEV') {
|
||||
await fs.promises.cp(source, target, { recursive: true, force: false });
|
||||
await fs.promises.rm(source, { recursive: true, force: true });
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await logEvent('admin_move', 'admin', { from: relativePath, to: targetPath });
|
||||
res.redirect(baseUrl('/admin/files'));
|
||||
});
|
||||
|
||||
app.post(`${basePath}/admin/files/copy`, requireAdminPage, async (req, res) => {
|
||||
const relativePath = String(req.body.path || '').replace(/^\/+/, '');
|
||||
const targetPath = String(req.body.targetPath || '').replace(/^\/+/, '');
|
||||
if (!relativePath || !targetPath) {
|
||||
res.status(400).send(renderFileManagerPage('Admin-Dateien', '<p class="card">Ungültige Eingabe.</p>'));
|
||||
return;
|
||||
}
|
||||
const source = resolveAdminPath(relativePath);
|
||||
const targetBase = resolveAdminPath(targetPath);
|
||||
if (!source || !targetBase) {
|
||||
res.status(400).send(renderFileManagerPage('Admin-Dateien', '<p class="card">Ungültiger Pfad.</p>'));
|
||||
return;
|
||||
}
|
||||
|
||||
let target = targetBase;
|
||||
try {
|
||||
const stat = await fs.promises.stat(targetBase);
|
||||
if (stat.isDirectory()) {
|
||||
target = path.join(targetBase, path.basename(source));
|
||||
}
|
||||
} catch (err) {
|
||||
// targetBase does not exist; treat as file/dir path.
|
||||
}
|
||||
|
||||
await fs.promises.cp(source, target, { recursive: true, force: false });
|
||||
await logEvent('admin_copy', 'admin', { from: relativePath, to: targetPath });
|
||||
res.redirect(baseUrl('/admin/files'));
|
||||
});
|
||||
|
||||
app.post(`${basePath}/admin/files/:id/delete`, requireAdminPage, async (req, res) => {
|
||||
const uploadEntry = await get('SELECT id, stored_path FROM uploads WHERE id = ?', [req.params.id]);
|
||||
if (!uploadEntry) {
|
||||
|
||||
Reference in New Issue
Block a user