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; }
|
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
|
||||||
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||||
.actions button { font-size: 0.9rem; padding: 7px 10px; }
|
.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::backdrop { background: rgba(15, 23, 42, 0.4); }
|
||||||
.dialog-card { padding: 18px; display: grid; gap: 12px; }
|
.dialog-card { padding: 18px; display: grid; gap: 12px; }
|
||||||
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
.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>${modifiedAt ? formatTimestamp(modifiedAt) : '—'}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button type="button" class="secondary rename-trigger" data-path="${escapedPath}" data-name="${escapedName}">Umbenennen</button>
|
<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>
|
<button type="button" class="secondary delete-trigger" data-path="${escapedPath}" data-name="${escapedName}">Löschen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1106,14 +1108,56 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</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>
|
<script>
|
||||||
const renameDialog = document.getElementById('rename-dialog');
|
const renameDialog = document.getElementById('rename-dialog');
|
||||||
const deleteDialog = document.getElementById('delete-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 renamePath = document.getElementById('rename-path');
|
||||||
const renameName = document.getElementById('rename-name');
|
const renameName = document.getElementById('rename-name');
|
||||||
const renameCurrent = document.getElementById('rename-current');
|
const renameCurrent = document.getElementById('rename-current');
|
||||||
const deletePath = document.getElementById('delete-path');
|
const deletePath = document.getElementById('delete-path');
|
||||||
const deleteCurrent = document.getElementById('delete-current');
|
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) => {
|
document.querySelectorAll('.rename-trigger').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
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) => {
|
document.querySelectorAll('.delete-trigger').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
deletePath.value = btn.dataset.path || '';
|
deletePath.value = btn.dataset.path || '';
|
||||||
@@ -1136,6 +1198,8 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
renameDialog.close();
|
renameDialog.close();
|
||||||
deleteDialog.close();
|
deleteDialog.close();
|
||||||
|
moveDialog.close();
|
||||||
|
copyDialog.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -1235,6 +1299,73 @@ app.post(`${basePath}/admin/files/delete`, requireAdminPage, async (req, res) =>
|
|||||||
res.redirect(baseUrl(`/admin/files?path=${encodeURIComponent(nextPath)}`));
|
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) => {
|
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]);
|
const uploadEntry = await get('SELECT id, stored_path FROM uploads WHERE id = ?', [req.params.id]);
|
||||||
if (!uploadEntry) {
|
if (!uploadEntry) {
|
||||||
|
|||||||
Reference in New Issue
Block a user