Datastore browser — two-pane file manager redesign
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/superpowers/specs/2026-06-03-datastore-browser-two-pane-design.md. Edit at the source, not here.
Datastore browser — two-pane file manager redesign
Section titled “Datastore browser — two-pane file manager redesign”Date: 2026-06-03
Branch: feature/datastore-browser-vsphere
Status: approved, in implementation
Bring the datastore browser (browse mode) up to a full operator-grade
file manager: a folder tree on the left, a file list on the right, and
an icon action toolbar carrying, in order:
New Folder · Upload Files · Upload Folder │ Register VM │ Download · Copy To · Move To · Rename · Delete
The first group acts on the current folder; Register VM and everything
after it act on the selected entry.
Constraints / invariants
Section titled “Constraints / invariants”mode: 'select'is consumed by the Create-VM wizard and the VM-detail “browse for ISO” flow. Its contract (selectableTypes,extensionFilter,emit('select'), single-pane layout) must not change. The two-pane redesign and the new toolbar apply tobrowsemode only.- English-only UI strings, no third-party product names (describe by
behavior). Lucide icons, brand tokens, TS strict, ESLint
--max-warnings 0. - Backend keeps the existing plain-handler style of
api/datastores/(customDatastoreError→IntoResponse, manual TS types in the composable, no utoipa annotations — consistent with the module as it stands). - The per-VM
domain.xmlmirror already lives in/<pool>/vms/<slug>/(written byvm_layout), so “Register VM” reads an existing XML and defines it — no new XML-persistence code is needed.
Backend (crates/daemon/src/api/datastores/)
Section titled “Backend (crates/daemon/src/api/datastores/)”Cross-datastore Copy / Move
Section titled “Cross-datastore Copy / Move”Extend EntryCopy / EntryMove with an optional to_datastore: Option<String>
(absent ⇒ same datastore, current behavior preserved). Destination subpath is
resolved against the destination datastore’s root via the same
resolve_safe_path; read_only on the destination short-circuits with 405.
- Same datastore + same filesystem ⇒
rename(move) /fs::copy(copy), as today. - Different datastore (different filesystem) ⇒
renamefails withEXDEV, so:- Copy = stream copy (recursive for folders).
- Move = stream copy, then delete the source on success.
- Files/folders ≥
ASYNC_COPY_THRESHOLD(100 MiB) run in a background task and return ajob_id; smaller transfers run synchronously (job_id: null).
Job registry + status
Section titled “Job registry + status”A process-lived registry (std::sync::OnceLock<Mutex<HashMap<String, Arc<JobState>>>>)
tracks { kind, total_bytes, done_bytes (AtomicU64), phase }. New endpoint:
GET /api/v1/datastores/:name/jobs/:job_id → { kind, state: running|done|failed, done_bytes, total_bytes, error? }. The frontend polls it to drive a progress
toast for large Copy/Move ops. Closes the long-standing job_id: None stub.
Register VM
Section titled “Register VM”POST /api/v1/datastores/:name/register-vm body { path }:
- Resolve, require the entry to be a regular
*.xmlfile under a small cap. - Read it and call
synvirt_libvirt::virsh::define_with_stdin(xml). - Parse
<name>from the XML for the response; return{ vm_name }.
New error variants (error.rs)
Section titled “New error variants (error.rs)”NotAnXml → 400 E_DATASTORE_NOT_XML, RegisterFailed(String) → 502
E_DATASTORE_REGISTER_FAILED, JobNotFound → 404 E_DATASTORE_JOB_NOT_FOUND.
Frontend (crates/web-ux-v2/)
Section titled “Frontend (crates/web-ux-v2/)”Composable (composables/useDatastores.ts)
Section titled “Composable (composables/useDatastores.ts)”fetchEntries(name, path)— pure fetch returningDirectoryEntry[]without clobbering the sharedentriesref (the tree needs per-node listings).copyEntry/moveEntrygain an optional destination datastore.registerVm(name, path)andgetJob(name, jobId).
New components (components/datastores/)
Section titled “New components (components/datastores/)”DatastoreTree.vue+ recursiveDatastoreTreeNode.vue— lazy folder tree of the active datastore; expand fetches children; selecting a node drives the right pane.DatastoreDestinationPicker.vue— Modal that navigates datastores + folders and returns{ datastore, path }for Copy To / Move To.
DatastoreBrowser.vue (rewrite of browse mode)
Section titled “DatastoreBrowser.vue (rewrite of browse mode)”Two-pane shell: tree left, right pane = icon toolbar + breadcrumbs + table.
Single-row selection state drives the selection-scoped toolbar actions
(enabled only with a compatible selection: Download/Register VM = file, Register
VM additionally .xml; Copy/Move/Rename/Delete = file or folder). Upload Folder
uses <input webkitdirectory>: create missing subfolders, then upload each file
with a combined N/M progress strip. Large Copy/Move polls getJob for progress.
select mode keeps its current single-pane code path untouched.
Out of scope
Section titled “Out of scope”- Installer/
config.yamlseeding of[[datastores]](still opt-in). - Backfill of
domain.xmlfor pre-existing VMs. - Recursive folder size in the Properties drawer.