Skip to content

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.

  • 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 to browse mode 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/ (custom DatastoreErrorIntoResponse, manual TS types in the composable, no utoipa annotations — consistent with the module as it stands).
  • The per-VM domain.xml mirror already lives in /<pool>/vms/<slug>/ (written by vm_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/)”

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) ⇒ rename fails with EXDEV, 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 a job_id; smaller transfers run synchronously (job_id: null).

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.

POST /api/v1/datastores/:name/register-vm body { path }:

  • Resolve, require the entry to be a regular *.xml file 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 }.

NotAnXml → 400 E_DATASTORE_NOT_XML, RegisterFailed(String) → 502 E_DATASTORE_REGISTER_FAILED, JobNotFound → 404 E_DATASTORE_JOB_NOT_FOUND.

  • fetchEntries(name, path) — pure fetch returning DirectoryEntry[] without clobbering the shared entries ref (the tree needs per-node listings).
  • copyEntry / moveEntry gain an optional destination datastore.
  • registerVm(name, path) and getJob(name, jobId).
  • DatastoreTree.vue + recursive DatastoreTreeNode.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.

  • Installer/config.yaml seeding of [[datastores]] (still opt-in).
  • Backfill of domain.xml for pre-existing VMs.
  • Recursive folder size in the Properties drawer.