- Add bun setup and web frontend build steps to server-build job in
release workflow (rust-embed needs apps/web/dist/ at compile time)
- Fix rustfmt violation in volume detection
- Fix storage dialog callback to use sdPath instead of location_id
- Navigate to explorer path after adding storage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- cargo fmt across all modified files
- Add Vite client types and Window.__SPACEDRIVE__ declaration
- Fix @sd/interface/platform import to @sd/interface
- Align @types/react versions between tauri and interface packages
- Remove unused imports/vars in useDropZone, DragOverlay, ContextMenuWindow
- Fix WebviewWindow.location references to use globalThis
- Exclude updater.example.ts from typecheck
- Sort `pub mod adapters` alphabetically in ops/mod.rs
- Wrap long line in volume/fs/refs.rs
- Handle empty TARGET_TRIPLE env var in xtask setup
- Replace `link:@spacedrive/*` with published `^0.2.3` versions
Volume GUIDs (\?\Volume{...}\) and other verbatim forms are invalid
without the prefix. Only strip when followed by a drive letter (X:\).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract duplicated Windows extended path normalization into shared
`common::utils::strip_windows_extended_prefix()` helper (was copy-pasted
in location/manager.rs, locations/add/action.rs, volume/fs/refs.rs)
- Add `unwatch_location()` call in LocationRemoveAction to stop the
filesystem watcher when a location is deleted (symmetric with the
`watch_location()` added in LocationAddAction)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the query/response approach from #3037 and fixes the actual bugs
that caused empty ephemeral directories:
- directory_listing.rs: Restore async indexer dispatch (return empty,
populate via events). Subdirectories from a parent's shallow index now
correctly fall through to trigger their own indexer job.
- subscriptionManager.ts: Pre-register initial listener before calling
transport.subscribe() so buffer replay events aren't broadcast to an
empty listener Set.
- useNormalizedQuery.ts: Seed TanStack Query cache when oldData is
undefined, so events arriving before the query response aren't silently
dropped by the setQueryData updater.
Adds bridge test (Rust harness + TS integration) that reproduces the
ephemeral event streaming flow end-to-end.
addressing.rs: when resolving SdPath::Physical, entries with parent_id=None
produced a relative path like "file.txt" which violates the SdPath::Physical
contract (must be absolute). Now returns an error instead. In practice this
case shouldn't occur (root entries are directories), but it's correct to
fail explicitly rather than silently produce invalid paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
canonicalize() on Windows can produce \?\UNC\server\share\... for network
paths. The previous strip_prefix(r"\?\") would produce UNC\server\share\...
which is invalid. Now handles both forms:
- \?\UNC\server\share\... → \server\share\... (network UNC)
- \?\C:\... → C:\... (local drive)
Applied in both manager.rs (add_location) and add/action.rs (watcher
registration) to ensure consistency. Same normalization as
volume/fs/refs.rs:contains_path().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two upstream bugs fixed:
1. LocationManager::add_location() stored paths as-is without
canonicalization. Relative paths (e.g. from cwd-dependent contexts)
broke the watcher, volume manager, and indexer pipelines.
Now calls tokio::fs::canonicalize() on local physical paths before
storing, with UNC prefix stripping on Windows.
2. LocationAddAction::execute() never registered new locations with the
FsWatcherService. The watcher only discovered locations at startup
via load_library_locations(). Any location added at runtime had no
filesystem monitoring — creates, deletes, and renames went undetected.
Now calls fs_watcher.watch_location() after successful creation.
- Extract delete logic into useDeleteFiles hook (DRY: used by both
useExplorerKeyboard and useFileContextMenu)
- Add isPending guard to prevent double-deletion on rapid key presses
- Remove dead DeleteProgress struct from delete job
- Use Progress::Indeterminate for intermediate phases instead of
misleading percentage values that get overridden by with_completion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DeleteJob now emits GenericProgress at 4 phases: Preparing (validation),
Preparing (path resolution), Deleting (strategy execution), and
Complete (with final counts and timing). Fixes the UI showing 0%
throughout delete operations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DeleteJob now calls resolve_in_job() on each target path before strategy
selection, converting SdPath::Content to SdPath::Physical via DB lookup.
This follows the same pattern established by CopyJob (upstream).
Also replaces unimplemented!() panic in PathResolver::resolve() for
Content paths with a proper error return.
Note: RemoteDeleteStrategy still uses deprecated device_id() — fixing
this requires device_slug resolution infrastructure (separate concern).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clamp pool_size to at least 1 and min_connections to min(pool_size, 5)
to prevent panic when pool_size < 5
- Use dynamic buffer size (1024) for GetVolumePathNameW in generic.rs
to handle folder-mounted volumes with long paths (same fix as fs/mod.rs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use root_buf.len() and guid_buf.len() instead of hardcoded literals
to avoid size mismatches. Increased root_buf to 1024 to handle
folder-mounted volumes whose paths can exceed MAX_PATH.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Commit e6b9a630d correctly injects DB UUIDs into the VolumeManager
cache during refresh, but the volumes.list query was still returning
ephemeral Uuid::new_v4() IDs from the initial startup detection
(which runs before the library is loaded). This caused a UUID mismatch:
the frontend cached ephemeral IDs, then received ResourceChanged events
with DB UUIDs — no match by ID — volumes appended instead of updated.
Fix: when volumes.list merges a tracked DB volume with its live cache
counterpart, override live_vol.id with the stable DB UUID (tracked_vol.uuid).
This ensures the query response and subsequent events use the same ID.
Note: the VolumeManager cache still holds ephemeral UUIDs between
startup and the first refresh after library load (~30s). A future
improvement could sync DB UUIDs into the cache immediately after
library loading, but this is not necessary since volumes.list is a
LibraryQuery and always runs after library load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a tracked volume gets updated in the cache (mount status change,
space change, etc.), line 741 was overwriting the ID with existing.id
which could be an ephemeral Uuid::new_v4() if the cache was populated
before DB metadata was merged (startup race condition). This defeats
the stable UUID fix from 4f387eede.
Now prefers the DB UUID from tracked_volumes_map when available,
falling back to existing.id only for untracked volumes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BatteryFlag: use bitmask check instead of exact equality (0x80 = no battery)
- SystemDrive: use %SystemDrive% env var instead of hardcoded C:\
- SQLite PRAGMAs: apply per-connection via SqliteConnectOptions (not one-time exec)
- System volume detection: check for Windows\System32 dir instead of drive letter
- Volume serial: use GetVolumePathNameW to resolve actual mount point
- ReFS: proper IOCTL version detection, real volume GUID, capability storage
- Volume GUID: extract shared volume_guid() from ntfs.rs to fs/mod.rs
- Device manager: reuse reg_read_hklm() instead of duplicated registry code
- VolumesGroup: remove 'local' fallback for device slug
- useNormalizedQuery: case-insensitive path matching, catch subscription errors
- Volume struct: add supports_block_cloning field for ReFS CoW routing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- transport.ts: clean up event listener if subscribe_to_events throws
- is_hidden_path: use FILE_ATTRIBUTE_HIDDEN only on Windows, dot-prefix
only on Unix (no cross-over that would mark .gitignore as hidden)
- reg_read_hklm: two-pass query to handle arbitrarily long REG_SZ values
- classify_volume: pass total_space instead of hardcoded 0
- ntfs volume_guid: unwrap_or(guid_buf.len()) instead of unwrap_or(0)
- wait_for_indexing: poll every 25ms instead of 5ms
- main.rs: accept both JsonOk and result keys for daemon compat
- VolumesGroup.tsx: remove unnecessary <any> type parameter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs caused ephemeral volume browsing to either show nothing or require
navigation away and back before content appeared.
1. Transport race condition (packages/ts-client/src/transport.ts)
listen("core-event") was called AFTER invoke("subscribe_to_events"), so any
events replayed from the 5s EventBuffer during subscription setup were emitted
before the listener existed and were silently dropped. Fixed by swapping the
order: listen first, then subscribe.
2. Event filter: direct children not matched (core/src/infra/event/mod.rs)
affects_path() with include_descendants=false only matched exact path equality
(file_path == scope_path). A directory listing subscribes with the directory as
scope, so files inside it never matched. Fixed by also checking
file_path.parent() == scope_path (direct children).
3. Synchronous ephemeral directory listing (core/src/ops/files/query/directory_listing.rs)
query_ephemeral_directory_impl() dispatched the indexer job and immediately
returned empty, relying on events to update the UI. Since ephemeral indexing
takes <500ms, it is better to wait for completion and return results directly.
The function now polls cache.is_indexing() every 5ms (10s timeout) and reads
from cache before returning. Concurrent requests also wait instead of returning
empty. Subdirectories with no indexed children now trigger their own indexer
job instead of returning empty (was: parent index has entry but no children →
treated as "has results").
Code refactored into read_ephemeral_listing() and wait_for_indexing() helpers
to eliminate triplication.
4. Removed is_explicitly_indexed() duplicate (core/src/ops/indexing/ephemeral/cache.rs)
Identical to is_indexed(); callers now use is_indexed() directly.
5. Debug logging infrastructure (packages/ts-client/src/hooks/useNormalizedQuery.ts)
Added debug: boolean option to useNormalizedQuery. When true, logs subscription
lifecycle, raw events, batch filter counts, and cache updates to the console.
Off by default.
remove_location() was calling delete_subtree() with library.db().conn()
while holding a transaction, causing SQLITE_BUSY (database is locked).
Use delete_subtree_in_txn(&txn) instead to run within the same transaction.
The frontend VolumesGroup was sending volume.device_id (UUID) instead
of device.slug when constructing Physical SdPaths. This caused
as_local_path() and is_local() to return false, breaking ephemeral
volume browsing ("Location root path is not local").
Backend: SdPath::is_current_device() now accepts slug, UUID, or "local".
Frontend: VolumesGroup.tsx looks up the Device by ID and uses device.slug.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
detect_hardware_model() in manager.rs still used wmic to get the
computer model. Replace with direct registry read from
HKLM\HARDWARE\DESCRIPTION\System\BIOS\SystemProductName.
- detect_hardware_model: registry HKLM\HARDWARE\DESCRIPTION\System\BIOS\SystemProductName
- detect_manufacturer: registry HKLM\HARDWARE\DESCRIPTION\System\BIOS\SystemManufacturer
- detect_form_factor: GetSystemPowerStatus (BatteryFlag 128 = no battery = desktop)
- detect_gpu_models: registry enumeration of display adapter class {4d36e968...}\DriverDesc
- detect_boot_disk_type: sysinfo::Disks on C:\ mount point
- detect_boot_disk_capacity: sysinfo::Disks total_space() on C:\
Adds Win32_System_Registry and Win32_System_Power features to windows-sys.
Remove supports_hardlinks(), supports_junctions(), get_ntfs_features(),
resolve_ntfs_path() and NtfsFeatures — none are called outside the module.
Simplify enhance_volume() to a no-op. Drop now-unused PathBuf and
tracing::debug imports.
get_ntfs_features() spawned PowerShell to query compression/encryption
flags, but all modern NTFS volumes support these features unconditionally.
Replace with a function returning hardcoded NtfsFeatures.
resolve_ntfs_path() used PowerShell Resolve-Path to canonicalize paths.
Replace with std::fs::canonicalize() which handles symlinks and junctions
natively without spawning a shell.
supports_hardlinks() and supports_junctions() called PowerShell via
get_volume_info() to verify the filesystem type. Since NtfsHandler is
only used for NTFS volumes, both always return true unconditionally.
Remove get_volume_info(), NtfsVolumeInfo, parse_volume_info(),
parse_ntfs_features(), extract_json_* helpers, and their tests.
Remove unused imports: tokio::task, tracing::warn.
path.starts_with(PathBuf::from("C:\\")) returns true for every path
on drive C:, causing OneDrive and other user folders to be flagged as
system directories during location validation.
Extend the root path detection to cover Windows drive roots (C:\) in
addition to the Unix root ("/") already handled. Use d.parent().is_none()
which is true for both Unix "/" and Windows "C:\" — root paths must
match exactly, not via starts_with.
GenericFilesystemHandler::same_physical_storage() returned hardcoded
false on Windows, causing same-volume file copies to always fall back
to streaming instead of atomic/fast-copy operations.
generic.rs: add volume_serial() helper using GetVolumeInformationW
to retrieve the 32-bit volume serial number from the drive-letter root
(e.g. C:\). Compare serial numbers to determine same-device status.
ntfs.rs: replace PowerShell Get-Volume with native Win32 calls.
Add volume_guid() helper using GetVolumePathNameW (mount point root)
+ GetVolumeNameForVolumeMountPointW (stable \?\Volume{guid}identifier). Compare GUIDs for reliable same-volume detection.
Both helpers are #[cfg(windows)] so non-Windows builds are unaffected.
On Windows, files can be hidden via the FILE_ATTRIBUTE_HIDDEN attribute
without using a dot-prefix. The existing code only checked for dot-prefix
(Unix convention), causing Windows hidden files to appear in listings.
Add is_hidden_path() helper in database_storage with a #[cfg(windows)]
branch that calls GetFileAttributesW() and checks FILE_ATTRIBUTE_HIDDEN.
Falls back to dot-prefix check if the API call fails. Non-Windows
platforms keep the original dot-prefix logic unchanged.
Replace all 10 occurrences across 4 files (database_storage.rs,
ephemeral/index.rs, ephemeral/writer.rs, job.rs).
SQLite returns SQLITE_BUSY immediately when another connection holds a
write lock. This is triggered during location removal when delete_subtree()
opens a nested transaction while the outer removal transaction is active,
and additionally when the indexer/watcher writes concurrently.
Adding busy_timeout=5000 makes SQLite retry for up to 5 seconds before
failing, which is sufficient for the nested transaction pattern to resolve.
Applied in both Database::create() and Database::open() to cover all
database instances.
Note: the root cause (nested transaction in remove_location()) should
be fixed separately for full correctness.
Resolves Issue #8 (partial - eliminates the SQLITE_BUSY symptom).
PathBuf is serialized with native OS separators (backslash on Windows).
When a PullRequest is sent from Windows to macOS/Linux, the path
C:\Users\alice\file.txt arrives as a string with backslashes which the
receiving OS cannot resolve (backslash is a valid filename character
on Unix, not a separator).
Sender (strategy.rs): normalize source_path to forward slashes before
building PullRequest, so the wire format is always OS-agnostic.
Receiver (file_transfer.rs): convert forward slashes back to the native
MAIN_SEPARATOR at the start of handle_pull_request, before any path
operations (canonicalize, metadata, checksum, streaming).
During startup, locations are loaded from the database before the
FsWatcher service connects. This triggers a warn!() for every location
saying "FsWatcher not connected". However, PersistentEventHandler::start()
has a recovery mechanism that re-registers all locations once the watcher
is connected, so this is expected behavior, not an actual error.
Change warn!() to debug!() and clarify the message to indicate the
deferred registration will happen on start().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PowerShell Get-Volume | ConvertTo-Json fails on many Windows systems:
it returns non-JSON output (BOM, encoding artifacts, or error messages)
while still exiting with code 0, causing volume detection to abort
entirely with "Failed to parse PowerShell JSON object".
Replace the entire PowerShell + wmic detection stack with sysinfo crate
(v0.31, already a dependency) which uses native Win32 APIs underneath:
- Enumerate disks via sysinfo::Disks::new_with_refreshed_list()
- Map disk.kind() directly to DiskType (SSD/HDD) instead of Unknown
- Pass disk.is_removable() to the classifier for better volume typing
- No shell process spawning, no JSON parsing, no encoding issues
The same sysinfo approach was already proven in refs.rs for ReFS
detection. This makes all volume detection on Windows shell-free.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two places hardcoded Unix-only /tmp paths that break on Windows:
- file_transfer.rs: TransferSession::destination_path defaulted to "/tmp"
which is not a valid path on Windows. Changed to std::env::temp_dir()
converted to a String via to_string_lossy().
- file_sharing.rs: SharingOptions::default() used PathBuf::from("/tmp/spacedrive")
as destination. Changed to std::env::temp_dir().join("spacedrive") which
correctly resolves to %TEMP%\spacedrive on Windows and /tmp/spacedrive on Unix.
Fixes audit Issue #11 from memory-bank/windows_audit_2026_03.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three Windows incompatibilities in the ephemeral path index:
1. count_entries_under_path() only checked for b'/' as child separator.
On Windows, paths use backslashes so "C:\foo" would never be found
as an ancestor of "C:\foo\bar". Added check for b'\' alongside b'/'.
2. remove_directory_tree() used format!("{}/", prefix) to find children.
Same issue — "C:\foo\" never matches "C:\foo\bar". Added a parallel
check with format!("{}\\", prefix).
3. The fallback display name for root nodes was hardcoded to "/" instead
of the path itself. On Windows, drive roots like "C:\" have no
file_name() component, so now falls back to the full path string
(e.g. "C:\\") instead of the misleading "/" label.
Fixes audit Issue #10 from memory-bank/windows_audit_2026_03.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Volume UUIDs were randomly generated (Uuid::new_v4()) on every startup,
causing the frontend to flash/reset on every app launch because React
uses volume.id as component keys (DevicePanel.tsx, VolumesGroup.tsx).
Fix by including db_vol.uuid in tracked_volumes_map and assigning it
to detected.id during the DB metadata merge step in reconcile_volumes().
Volumes recognized by fingerprint now retain their stable database UUID
across restarts, while new volumes still get a fresh UUID until tracked.
Fixes audit Issue #9 from memory-bank/windows_audit_2026_03.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The upstream tests had assertions that didn't match the implementation:
- completion.completed/total used phase-specific values but the impl
uses total_found.files + total_found.dirs (real file counts, total=0)
- percentage used exact f32 equality (fails due to float precision)
Fix assertions to use abs() comparisons for floats and expect the
actual total_found-based completion values.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous Windows implementation moved files to a temp directory
(spacedrive_trash) rather than the actual Recycle Bin. The macOS/Linux
implementations were also custom-rolled with manual collision handling.
Replace all platform-specific move_to_trash implementations with the
`trash` crate (v3.3), which uses native OS APIs:
- Windows: SHFileOperation → actual Recycle Bin
- macOS: NSFileManager → Trash
- Linux: XDG trash specification
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PowerShell process spawning for ReFS block-cloning checks caused visible
latency on every volume operation. Replace with:
- FSCTL_GET_REFS_VOLUME_DATA DeviceIoControl call to detect ReFS v2+
(Win10 1703+) block cloning support instantly, with a static cache so
each volume is queried at most once per process lifetime.
- sysinfo-based volume enumeration in get_volume_info/get_all_refs_volumes
(consistent with ntfs.rs, no shell process).
- contains_path strips the \?\ extended-path prefix produced by
canonicalize() so mount point comparisons work correctly.
Add Win32_System_Ioctl and Win32_System_IO features to windows-sys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Windows and macOS, volume paths are case-insensitive but the OS can
return them in different cases (e.g. "C:\" vs "c:\"). Without normalization,
the same physical volume could generate different fingerprints across
detection cycles, causing duplicate volume entries in the UI.
Also trims trailing slashes for consistency while preserving root paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>