mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 14:56:27 +00:00
feat(stacks): browse and edit mounted volume files in the explorer (#1403)
* feat(stacks): browse and edit mounted volume files in the explorer Reposition the stack file explorer around runtime configuration access: discover a stack's declared mounts and expose each as a safe, stack-scoped file root. The explorer opens on a Volumes group (bind mounts and named Docker volumes) by default, with the stack source directory as a secondary group, on a "Files & Volumes" tab. - Discover roots from the rendered effective compose model; resolve named volumes to their Docker name and browse/edit them through the hardened helper container, with bind mounts handled directly when reachable. - Re-derive the allowed roots server-side on every file operation and match the client root id against them, so a request can never address a path the stack did not declare. Block dangerous host mounts and binds that overlap Sencho's managed directories; reject writes to read-only mounts. - Thread an optional root id through the existing file endpoints and an opaque, parseable optimistic-concurrency token through read, conflict, and write, for both filesystem and helper backends. - Keep compose and env file protection on the stack source root only. * fix(stacks): theme the Files & Volumes root switcher Replace the raw native select in the file-root switcher with the design system Select component. The native control did not honour the dark theme, so the panel rendered white with unreadable text. The themed Select gives a dark popover with grouped Volumes / Stack source labels and disabled items. * fix(stacks): contain the bind-root probe and de-taint the file-op error log Gate the volume-root bind probe's realpath/stat behind a compose-base containment check (mirroring the storage host-path probe) so they never run on an unvalidated host path; a source outside the compose dir is unreachable in the containerized deployment anyway and is reported non-accessible without touching the filesystem. Log the helper-backed file-op failure through a constant format string with sanitized arguments instead of an interpolated template literal. * fix(stacks): inline the bind-probe containment guard at the fs sinks The wrapped containment predicate was not recognized as a path barrier, so the bind probe's realpath/stat still flagged as uncontrolled-data-in-path. Inline the path.resolve + startsWith check directly at each filesystem sink (and re-check the resolved canonical before stat, so a within-base symlink that resolves outside the compose dir is also rejected). * fix(stacks): harden file-root lifecycle, upload race, and helper errors Address review findings on the Files & Volumes feature: - Invalidate the file-root allowlist on stack create/delete/import/from-git (wire StackFileRootsService.invalidateNode into invalidateNodeCaches), so a stack deleted and recreated under the same name cannot serve the old stack's roots from the 15s cache. - Use the atomic exclusive write for a non-overwrite upload so a file created by another writer after the existence check is not silently clobbered. - Let the helper's real cd errno through and map permission failures to 403 consistently across list/stat/read/write/mkdir/delete/pathKind, instead of reporting EACCES as 404/500; pathKind no longer reports a permission-denied parent as absent. - Document the realpath-then-open TOCTOU as a known, pre-existing limitation of every file op (O_NOFOLLOW is not viable because config volumes legitimately contain symlinks); the bind root is contained to the compose dir and the op requires stack:edit. - Docs: drop a missing screenshot reference and correct the protected-file delete behavior (stack-root compose/.env cannot be deleted via the explorer).
This commit is contained in:
@@ -21,6 +21,8 @@ interface MoveFileDialogProps {
|
||||
relPath: string;
|
||||
/** The entry being moved (null until a source is chosen). */
|
||||
entry: FileEntry | null;
|
||||
/** The selected file root; the destination tree is loaded within it. */
|
||||
rootId?: string;
|
||||
/** Relocate `fromRel` into `destDir` (''=stack root). Resolves true only when
|
||||
* the entry actually moved, so the dialog stays open on a blocked/failed move. */
|
||||
onMove: (fromRel: string, entryName: string, destDir: string) => boolean | Promise<boolean>;
|
||||
@@ -32,6 +34,7 @@ export function MoveFileDialog({
|
||||
stackName,
|
||||
relPath,
|
||||
entry,
|
||||
rootId,
|
||||
onMove,
|
||||
}: MoveFileDialogProps) {
|
||||
// Loaded directory children, keyed by directory rel path ('' = stack root).
|
||||
@@ -66,7 +69,7 @@ export function MoveFileDialog({
|
||||
next.delete(dir);
|
||||
return next;
|
||||
});
|
||||
listStackDirectory(stackName, dir)
|
||||
listStackDirectory(stackName, dir, rootId)
|
||||
.then((entries) => {
|
||||
if (requestSeqRef.current !== seq) return;
|
||||
setDirChildren((prev) => new Map(prev).set(dir, entries.filter((e) => e.type === 'directory')));
|
||||
|
||||
Reference in New Issue
Block a user