mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
feat(files): delete a file from the Files panel
A delete button on each file row of the Files panel (tree and search results) and a DELETE /api/sessions/:id/files?path= route that removes one regular file from the session workspace, refused for remote (SSH) sessions and behind the same sensitive-path, attachment-guard and .git checks as the edit route. Salvaged from #529.
This commit is contained in:
1 parent
f62c1f7e2f
commit
dfe1af2e5f
3 files changed
+86
No files matched your search
@@ -3799,6 +3799,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
const downloadBtn = !isDir
|
||||
? `<a class="file-tree-download" href="${escapeHtml(CodemanBase.url(`/api/sessions/${encodeURIComponent(owner)}/file-raw?path=${encodeURIComponent(node.path)}&download=true`))}" title="Download" onclick="event.stopPropagation()">⬇</a>`
|
||||
: '';
|
||||
const deleteBtn = !isDir
|
||||
? `<button class="file-tree-delete" title="Delete" aria-label="Delete ${escapeHtml(node.name)}" onclick="event.stopPropagation(); app.deleteFileFromBrowser(decodeURIComponent('${encodeURIComponent(owner)}'), decodeURIComponent('${encodeURIComponent(node.path)}'))">×</button>`
|
||||
: '';
|
||||
|
||||
html.push(`
|
||||
<div class="file-tree-item" data-path="${escapeHtml(node.path)}" data-type="${escapeHtml(node.type)}" data-depth="${depth}">
|
||||
@@ -3807,6 +3810,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
<span class="${nameClass}">${escapeHtml(node.name)}</span>
|
||||
${sizeStr}
|
||||
${downloadBtn}
|
||||
${deleteBtn}
|
||||
</div>
|
||||
`);
|
||||
|
||||
@@ -3981,6 +3985,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
const downloadBtn = !isDir
|
||||
? `<a class="file-tree-download" href="${escapeHtml(CodemanBase.url(`/api/sessions/${ownerPath}/file-raw?path=${encodeURIComponent(match.path)}&download=true`))}" title="Download" onclick="event.stopPropagation()">⬇</a>`
|
||||
: '';
|
||||
const deleteBtn = !isDir
|
||||
? `<button class="file-tree-delete" title="Delete" aria-label="Delete ${escapeHtml(match.name)}" onclick="event.stopPropagation(); app.deleteFileFromBrowser(decodeURIComponent('${encodeURIComponent(ownerSessionId)}'), decodeURIComponent('${encodeURIComponent(match.path)}'))">×</button>`
|
||||
: '';
|
||||
return `
|
||||
<div class="file-tree-item" data-path="${escapeHtml(match.path)}" data-type="${escapeHtml(match.type)}" data-owner="${escapeHtml(ownerSessionId)}">
|
||||
<span class="file-tree-expand"></span>
|
||||
@@ -3988,6 +3995,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
<span class="${nameClass}">${escapeHtml(match.name)}</span>
|
||||
${sizeStr}
|
||||
${downloadBtn}
|
||||
${deleteBtn}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
@@ -4012,6 +4020,24 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
async deleteFileFromBrowser(sessionId, filePath) {
|
||||
if (!sessionId || !filePath) return;
|
||||
if (!window.confirm(`Delete ${filePath}?`)) return;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/sessions/${encodeURIComponent(sessionId)}/files?path=${encodeURIComponent(filePath)}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
const result = await response.json().catch(() => null);
|
||||
if (!response.ok || !result?.success) throw new Error(result?.error || 'Delete failed');
|
||||
this.showToast(`Deleted: ${filePath}`, 'success');
|
||||
await this.loadFileBrowser(sessionId, { force: true });
|
||||
} catch (error) {
|
||||
console.error('File browser delete failed:', error);
|
||||
this.showToast(error?.message || 'Delete failed', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
_findFileBrowserDirectory(nodes, targetPath, ancestors = []) {
|
||||
if (!Array.isArray(nodes)) return null;
|
||||
for (const node of nodes) {
|
||||
|
||||
@@ -12386,6 +12386,24 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
||||
.file-tree-download:hover {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
.file-tree-delete {
|
||||
display: none;
|
||||
margin-left: 2px;
|
||||
padding: 0 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-tree-item:hover .file-tree-delete {
|
||||
display: inline;
|
||||
}
|
||||
.file-tree-delete:hover {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
/* File tree indent levels */
|
||||
.file-tree-item[data-depth="0"] { padding-left: 0.5rem; }
|
||||
|
||||
@@ -2000,6 +2000,48 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
};
|
||||
});
|
||||
|
||||
// Delete a regular file from the current session workspace. The same
|
||||
// confinement and sensitive-path checks used by previews apply here.
|
||||
app.delete('/api/sessions/:id/files', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { path: filePath } = req.query as { path?: string };
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
if (session.remote) {
|
||||
reply.code(400);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Deleting is not supported for remote (SSH) sessions');
|
||||
}
|
||||
if (!filePath) {
|
||||
reply.code(400);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Missing path parameter');
|
||||
}
|
||||
const validated = validateSessionFilePath(session.workingDir, filePath);
|
||||
if (!validated) {
|
||||
reply.code(404);
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
|
||||
}
|
||||
const { resolvedPath, relativePath } = validated;
|
||||
const guard = await loadAttachmentGuardConfig();
|
||||
if (
|
||||
isSensitivePath(resolvedPath) ||
|
||||
isBlockedAttachmentPath(resolvedPath, guard.blockedTrees) ||
|
||||
isDeniedEditRelativePath(relativePath)
|
||||
) {
|
||||
reply.code(403);
|
||||
return createErrorResponse(ApiErrorCode.FORBIDDEN, 'Deleting this file is blocked');
|
||||
}
|
||||
const stat = await fs.lstat(resolvedPath).catch(() => undefined);
|
||||
if (!stat) {
|
||||
reply.code(404);
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'File not found');
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
reply.code(400);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Only regular files can be deleted');
|
||||
}
|
||||
await fs.unlink(resolvedPath);
|
||||
return { success: true, data: { path: filePath } };
|
||||
});
|
||||
|
||||
// Get file content for preview (File Browser)
|
||||
app.get('/api/sessions/:id/file-content', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
Reference in new issue
Block a user