mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-11-03 13:55:30 -05:00
nightly
1340
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fc1798b05a |
Windows unit job gates main; daemon raises its fd limit; sd-version marker test (#3153)
* CI: the Windows unit job gates main Core Tests (windows unit) was green twice on main ( |
||
|
|
e4b4cb71f5 |
Capture time and place in listings and search, read from the image facet on both backends (#3151)
* Capture time and place in listings and search, read from the image facet on both backends search.files sorts by CapturedAt and filters by a CapturedAt range and has_location; files.directory_listing sorts by datetaken; the media listing's datetaken reads the facet. The store joins facet_image into every entry read, guarded by the row's content hash, and the arena path reads the facet from the store ahead of its sort and filter, so both backends hand the same File to one predicate and one order (R8 row 19, new row 29). File.image_media_data is filled from the facet on every path; the inspector shows the capture time and the coordinates; Date Taken is in every explorer view's sort menu. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * search.media judges the capture filters on its rows and leaves replicas out under them; a directory listing reads facets ahead of the sort only for a capture order Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * search.media judges a replica's rows under a capture filter as search.files does instead of dropping the partition Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
15d7df1808 |
Windows: build and unit-test the daemon and CLI on a Windows runner, fix what it found, publish Windows nightlies (#3148)
* ci: build and unit-test sd-core and sd-cli on a Windows runner The job is the first Windows build since the rewrite and the evidence for the cross-platform release gate. It runs on GitHub's hosted windows-latest (Blacksmith has no Windows), builds sd-cli and sd-daemon in debug, then runs the sd-core lib tests and the sd-cli tests. continue-on-error keeps it from gating main until it has been green twice. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * cli: name the daemon binary and replace binaries the way Windows allows The CLI built the daemon's path as sd-daemon everywhere, which does not exist on Windows; sd_client::daemon_binary_name appends the platform's executable suffix. sd update renamed the staged file over the running binary, which Windows refuses for a loaded executable: it now moves the current binary to .bak first and renames the new one into place, removing the backup where the platform lets it and on the next update otherwise. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * indexing: keep the hidden bit in the arena instead of re-reading it on every read The arena rebuilt is_hidden from the path on every entry read. On Unix that is a name check; on Windows it is a GetFileAttributesW call per entry, and it answers false for anything not on disk at that moment, so search returned hidden files from a snapshot or a detached volume. PackedMetadata now carries a hidden bit (size drops to 59 bits), the arena records what the walk saw, and the writer judges a change by the metadata it carries. Snapshot restore recomputes the bit from names where names decide. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * volume: tell a Windows mount point from the directory a drive left behind is_mount_point answered by existence on Windows, so the empty folder an unplugged mapped drive leaves passed as mounted and the detached volume's source could be dispatched at it. GetVolumePathNameW names the mount point of the volume holding a path; a directory that is its own volume root is a mount point, anything else resolves to a parent volume. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * core: let three path-shaped unit tests read the same on Windows The compare and media tests compared displayed paths against slash-written expectations, and the track action test offered a Unix absolute path that Windows reads as relative. The tests now normalise separators and pick an absolute root the platform accepts; what they assert is unchanged. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * files: delete a moved source when the filesystem still has it, not when the volume index guesses After a streaming copy in move mode the source was deleted only when the volume manager said the two paths were on different volumes. On Windows it answered same-volume for a copy the router had already decided to stream, so the file stayed in both places and the move reported success. The job now checks whether the source still exists after the strategy ran and removes it then, which is the condition the comment described. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * ci: save the Windows rust cache on a red run too Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * ci: publish Windows x86_64 nightly binaries The nightly matrix gains x86_64-pc-windows-msvc on windows-latest, producing sd-windows-x86_64.exe and sd-daemon-windows-x86_64.exe with checksums. The build and prepare steps run under bash on every host, and the asset names carry .exe, which the updater already strips when matching. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * docs: Windows gap report and register row docs/plans/2026-10-09-windows-gap.md records the first Windows build since the rewrite: what compiled, what passed, each failure with its cause and fix, what the integration, FDA, R8 and locked volumes suites need to run on Windows, and the beta.1 recommendation with release notes text. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * review: judge mount points on the path as given, mark placeholder directories hidden, keep the rename-over update on Unix GetVolumePathNameW judges the string it is handed, so a subst or mapped drive letter must not be canonicalized to its target first or its own root reads as not a mount point. Arena::ensure_directory creates directory placeholders the walk has not statted; they now carry the hidden answer the path gives until the walk corrects them. The updater moves the running binary aside only on Windows, which needs it; elsewhere a .bak copy and a rename over the target keep a working file at the path at every instant. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
6dc6c2bef8 |
Housekeeping: drop sources triggers, remove libraries.export stub, prune restore-trash (#3143)
* ci: drop the sources branch from nightly and core tests triggers Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * core: remove the libraries.export stub left behind by backup/restore Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * core: prune restore-trash after a successful library restore A restore --replace parks the displaced library and store files under restore-trash/<library>-<time>/ and never removed them. Once the restored library is open, keep the newest backup.restore_trash_keep entries for that library (default 2, settable with sd config set) and remove the rest, never the one the restore just parked. Removed paths are logged and returned in the restore output. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * tests: add the backup field to the sync harness AppConfig literal Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
731744277e |
Add to Library step 4: keep an offline copy of an on-source catalog (#3142)
* Add to Library step 4: keep an offline copy of an on-source catalog as a library replica Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Say where a detached source's catalog is: offline copy, or on the drive Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Retire only a reader on the offline copy when publishing; drop the unused apply_setting Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Fill the new field in the CLI's source row test Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Fill offline_copy in sources.get; show the catalog hint on the card whether or not the drive has a badge Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
a19553f6f1 |
Locked volumes L5: volume state in the app, CLI and peer volume facts (#3141)
* Locked volumes L5: volume state in the app, CLI and peer volume facts RemoteVolumeInfo carries VolumeState and a device publishes its tracked unmounted and locked volumes, so a peer lists the owner's dataset as locked instead of a replica that stopped updating. RemoteSourceInfo carries each source's volume_state and sources.get reads the live index. VolumeMountChanged carries the state and fires on any state change, including a key unloaded from an already unmounted dataset. The CLI event printer names a lock; sd sources list has a State column and sd volumes list a State line. The sidebar, Storage cards, source detail and Overview show Locked, Unmounted or Offline and say the index stays browsable. The Add to Library modal refuses a locked or unmounted drive with the reason. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Refuse an add only for this device's away drives; printer falls back to is_mounted without a state Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Say why RemoteVolumeInfo.state stays last Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Hold the Add to Library refusal until the device list resolves Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
26b67ff0d1 |
Extension file kinds K3 to K5: client renderer registry, viewer bundles, photos viewer (#3136)
* Extension file kinds K3 to K5: client renderer registry, viewer bundles, photos viewer Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Review: allow viewer bundle imports under the desktop CSP, clear the bundle host on unmount Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
0e76551463 |
Nightly release: edit in place instead of delete and recreate (#3137)
* Nightly release: edit in place instead of delete and recreate Every gh release create notified everyone watching the repository for releases, about ten times a day. The publish job now moves the nightly tag with git, edits the release title and notes, uploads assets with --clobber and removes assets whose platform disappeared, creating the release only when it is missing. sd update reads the commit from the git ref API (annotated tags dereferenced) with the nightly.sha asset as the rate-limit fallback, because target_commitish is frozen at creation. A release without nightly.sha is recreated one last time so updaters built before the switch still see a newer build. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly publish: upload assets before moving the tag; treat only 'release not found' as missing Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly publish: upload nightly.sha after the binaries it names Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly publish: key the one-time recreate on the release notes, written before any upload Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly publish: one marker variable for the notes and the recreate probe Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
cbdc1db3fa |
sd mcp: MCP server over the operation registry (#3131)
* sd mcp: MCP server over the operation registry Every registered query, action, validate and preview is an MCP tool served over stdio with rmcp, with input schemas walked from the ops' specta types and a jobs.wait tool to follow jobs. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * docs: note that dotted MCP tool names are deliberate Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * sd mcp: repair a stale library selection; run the suite in the acceptance job Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * sd mcp: jobs.wait finds the job in the library that owns it Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * sd mcp: resolve jobs.wait candidates once and pin the owning library Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
30b4f19ba4 |
Scan the installed @spacedrive/primitives for Tailwind classes when spaceui is not beside the repo (#3127)
* Scan the installed @spacedrive/primitives for Tailwind classes when spaceui is not checked out beside the repo Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Also scan the repo-root node_modules for @spacedrive/primitives, for bun's hoisted linker Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
f69c1fee89 |
Add to Library core: defaults and overrides, store placement, volume registration, portable identity, removal keeps the catalog (#3121)
* Add to Library defaults: library settings with per-add overrides Every entry point used to carry its own notion of what a source starts as: volumes.track defaulted external drives to unfiltered, folder tracking to filtered, and the Explorer sent unfiltered: false by hand. The library now holds the defaults under settings.adding (store placement in library, an offline copy kept, filtered capture, content identified), config.library.get reports them and config.library.update changes them for later adds only. sources.track and volumes.track take the same AddOverrides on top; an absent field is the default and an override never writes back. The effective settings are the source's SourceConfig, persisted with the registration and returned in the output together with the saved name and the store path. A name, settings or volume row that fails to persist now fails the add instead of reporting a tracked source. A source added without content identification gets its walk and nothing after. volumes.track keeps the archival behavior by passing an explicit unfiltered override for an external drive unless the caller set one. The CLI gains --on-source, --in-library, --keep-offline-copy and --no-identify on sd source track. Generated TypeScript types regenerated. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Resolve store placement per library in the source layer Placement is intent recorded in each source's config column; the serving device resolves it. VolumeIndex::store_dir is the one resolver: in library under <data>/sources/<id>, on source under <root>/.spacedrive/sources/<id>, following the source's current root so an on-source store tracks its volume across remounts and resolves to nothing while the drive is away. Writer opens, read-only opens, freezes, the status surface and sources.list all go through it, and SourceStore::open takes the resolved stores directory rather than the data-directory layout. On source is refused for network and cloud volumes, whose WAL database cannot live on the share. The volume index also gains register_source_with, which registers under an identity a store on disk already carries, and forget_source, which drops a registration and retires its writer and reader handles without touching the directory. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Adding a path tracks its volume ensure_volume_in_db wrote the row but skipped the sync publication and the VolumeTracked event that track_volume emits, and the add logged its failure and carried on. It now syncs and emits on insert, the add fails when the row cannot be written, and the volume index maps the drive at the same moment so the mount point check and the detached flag apply to the new source. A second folder on the same drive finds the row already there. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Portable source identity and managed-directory exclusions Every store now carries source.json beside data.db: a versioned descriptor with the source id, the library that wrote it, its volume anchor and path within the volume, and its settings. Adding a scope first looks for a store whose descriptor binds to this library, this volume and this path, and registers under that identity, so a removed folder re-added or a drive plugged in with its catalog on it reopens the catalog instead of minting a fresh source over an empty one. A descriptor from another library or another drive is ignored and a new source starts beside it. The own-data-directory check becomes crate::config::is_managed: the data directory under every spelling, any .spacedrive directory and what is under it, and the volume identity marker. It is a path-component check rather than a registry lookup, so it holds for an unfiltered source whose rules are off, for an enclosing source that was never told about the inner store, for a store on a drive nobody here tracks, and across remounts and aliases. The walk, the watcher and the writer all use it. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Removing a source keeps its catalog unless asked to delete it sources.delete used to delete the store directory through the adapter engine before unregistering, which would have turned removal of an on-source store into destruction of the portable catalog, assertions included. It now retires the source's handles, drops the registration and leaves the store where it is; delete_catalog is an explicit second choice. The volume row is never touched: removing the last folder on a drive does not forget the hardware. sd source untrack wraps the action. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Add to Library acceptance suite and status One Core over temporary directories tracks a folder with defaults and one with overrides, proves one volume row for both, finds the on-source store excluded from its own and an enclosing source, removes a source and sees its catalog and record identities survive the re-add, and sees deletion start over. The suite joins the acceptance group of xtask test-core; docs/core/acceptance/add-to-library.md maps the plan's cases to tests and names the rows the core half cannot prove. PROJECT_STATUS and the plan's status line record what landed and what the modal thread builds next. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Review fixes: placement-aware reads and backup, re-track keeps settings, adoption across placements The library's per-source read queries (sources.list_items, list_records, media_listing) opened the store through the engine's in-library layout, so an on-source catalog was unreachable from the explorer; they now resolve a registered filesystem source through the volume index and fall through to the engine only for adapter sources. libraries.backup reads each store from where placement put it. A re-track carries the previous value of every setting the add did not name instead of the library's current defaults, and never changes placement. portable_identity searches both placements so a drive arriving with its catalog is adopted whatever placement the add asked for, and the adopted catalog keeps the placement it was found under. SourceConfig::default agrees with its serde defaults. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Restore names an on-source catalog it cannot put back The archive now holds a store backed up from its drive, but a restore lays every store in the in-library layout, where an on-source registration does not read. The manifest records each source's placement; a restore leaves an on-source catalog where it landed, keeps it out of the restored count, and names it in on_source_catalogs with its path, so the caller knows what was not put back. Moving it onto the drive is relocation. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * catalog_reused means an identity was kept, not that a file exists The acceptance run on CI saw a fresh add report its catalog reused: the content identity job of an enclosing source opened the new source's store between registration and the file probe. The flag now answers from the registration (it existed, or a descriptor-bound catalog was adopted), which is what it claims. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Regenerate TypeScript types for the catalog_reused doc Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Volume tracking test follows the one-library-per-path rule volumes.track now surfaces a source setup failure instead of logging it, so tracking a volume another open library holds is refused. The multi-library test asserts the refusal, then the handover once the first library untracks the drive and removes its source. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
92dea58a46 |
ci: enforce the docs build, generated types, secrets scan, and license gates (#3118)
* docs: escape the <name> placeholder in library-sync-setup so MDX compiles Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * tauri: regenerate capability schemas for the updater plugin and pinned tauri-build Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * ci: enforce the docs build, generated types, secrets scan, and license gates on every PR Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * docs: name the ci.yml job behind each repository release gate Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * tauri: keep desktop-schema.json in its committed key order Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * ci: compare Tauri schemas as JSON since key order follows feature unification Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
08eb622e29 |
Library backup and restore with manifest verification (#3115)
* Library backup and restore with manifest verification libraries.backup copies library.db, sync.db, every owned source store and its sidecars with VACUUM INTO while the daemon runs, hashes each file with blake3 and writes manifest.json to a directory or .tar.zst archive. libraries.backup.verify rehashes a backup without restoring it. libraries.restore verifies every hash, refuses a backup from a newer schema, stages under the data dir, swaps files into place with the open store and sidecar pools released first, parks the displaced state in restore-trash/, and reopens the library. Replacing a library with other member devices needs force, since their device rows and sync watermarks would diverge. CLI: sd library backup --to, sd library backup verify --from, sd library restore --from [--as-new | --replace] [--force]. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * CLI help: restore-trash is kept, not pruned Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Restore: keep stores closed across the swap, clear the arena, serialize library.json Review findings on #3115: the volume index kept the pre-restore arena and drive snapshots after a replace, a watcher event could reopen the old data.db between the release and the rename, and library.json was copied byte for byte while the daemon rewrites it in place. quiesce_stores takes every affected source's open gate and holds it until the renames are done, closes the cached handles, detaches the library's registrations and partitions, and removes the drive snapshots so the reopened library rebuilds from the restored stores. Backup serializes the open library's config instead of copying the file, and restore parses the staged config before touching the data directory. A failure after the first displacement now names the restore-trash directory. The test feeds source b's watcher during the replace and proves a later write reaches the restored file on disk. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Restore: load the staged config as LibraryConfig, hold sidecars across the swap Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
e58507a1ec |
R8 fixes F1, F2, F3, F5: salvage repair, snapshot quarantine, refused watch state, nested-store search duplicates (#3114)
* Store: unsettle a salvaged-away row so the next observation rewrites it R8 row 3 (F1). When a batch loses a row to a database failure the ledger kept the binding it made before the commit, so the next walk resolved the same file Unchanged and the row stayed missing for as long as its size and mtime held. commit now takes the ledger and unsettles each row it could not land: the uuid keeps its path, the size and mtime evidence is cleared, and the next observation resolves Changed and writes the whole row under the identity the arena already holds. Un-ignores a_repeat_observation_repairs_a_failed_write. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Snapshot: move an unreadable artifact aside instead of deleting it R8 row 10 (F2). load_snapshot_impl removed a snapshot it could not read, decode, or accept the version of, so the artifact was gone before anyone could inspect it. It now moves to <name>.corrupt-<unix seconds> beside the slot with a WARN naming both paths; the slot is clear for the next save and no launch parses it again. Un-ignores an_invalid_snapshot_is_retained_for_diagnosis, which now asserts the bytes survive at the aside path. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Watcher: register a root only after the OS accepts, report and retry refusals R8 row 11 (F3). watch_root registered the root with the volume index before subscribing at the OS, so a refused subscription left the root in watched_paths and is_watched and core.index_status reported a watch that did not exist. The OS subscription now comes first; a refusal is recorded on the volume index with its reason, surfaces as IndexStatus::refused_watches (and in sd index status), and is retried every watch_retry_interval (30 s) while the service runs. A retry the OS accepts moves the root to watched_paths. Un-ignores a_refused_watch_is_not_reported_active and extends it with the status report and the reconciling retry. Regenerates ts-client types. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Search: answer a file under nested sources once from the stores R8 row 25 (F5). With an outer and an inner source both walked, the file under the inner root sits in both stores, and after a restart with no snapshot search_every_index queried each store in turn and returned it twice. The store loop now reads sources innermost first and drops a hit whose local path an earlier partition (an inner store or a drive's arena) already answered, before it is counted, so total_found stays the number of distinct files. Un-ignores a_file_under_nested_sources_is_one_hit_from_the_stores. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Docs: R8 rows 3, 10, 11 and 25 fixed; row 17 remains the R4 brief Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Review: one retained corrupt snapshot, full-arena dedupe seed, reset clears refusals A recurring load failure no longer retains an artifact per launch: when a .corrupt-* sibling exists the slot is removed instead, keeping the oldest copy as the mismatched-root path does. The store de-duplication seed is collected from each arena's full match set before the page window narrows it, so total_found stays exact beyond the page. clear_all forgets refused watches along with the watched paths. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
5bc474e722 |
CLI and daemon housekeeping from the live Mac runs (#3111)
* Default the replication block in core.status so a newer CLI reads an older daemon A |
||
|
|
10dbc18d0f |
Replica fetch: resumable .part transfers, bandwidth cap, pause, and progress (#3109)
* Resume interrupted replica fetches from a .part file A replica transfer now lands in <source>.<generation>.<blake3 prefix>.part and stays there when the stream breaks or the daemon stops. The next attempt asks the owner for the artifact's tail from the part's length over the new FetchSnapshotFrom and FetchDatabaseFrom requests, whose header still names the whole artifact; a header that names different bytes discards the part and starts over, and the finished file is validated against the header checksum before it replaces the live replica as before. Parts are removed only when the owner's listing no longer carries their generation. A nonzero offset serves the on-disk snapshot without the pre-fetch save, so a resumed copy finishes against the same bytes. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Cap replica transfer bandwidth with a persisted daemon setting replication.max_bytes_per_sec in spacedrive.json bounds every replica artifact served and received through one token bucket (zero is unlimited); the cap applies to transfers already in flight. It is set through config.app.update and sd config set replication.max_bytes_per_sec (bytes, or K/M/G suffixes), read back through sd config get and show. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Pause and resume replica fetching sd sources replication pause stops this device starting replica transfers and halts any in flight at its next chunk, keeping the partial file; resume continues from it and runs a replication pass against every connected device at once. The switch is replication.paused in the daemon config, so a paused daemon stays paused across a restart, and a pass that runs while paused still refreshes listings, owner facts and fleet totals. A pause does not count as a fetch failure, so no backoff accrues. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Show replica fetch progress in sources list, status and events Every fetch in flight is tracked with bytes on disk, total and a recent rate. sources.list carries it on the replica's row (or a replica_fetching row for a first copy that has no share yet), core.status and the new mounts.replication_status query summarize it with the pause switch and cap, and the daemon emits a typed ReplicationProgress event about once a second per transfer plus a finished one when it stops. sd sources list and sd status print the figures; ts-client types regenerated. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Document resumable replica fetch, the cap, pause and progress Source replication gets its own section in networking.mdx with the .part contract, the wire change, the cap, the pause switch and the progress fields; file-sync.mdx points there; the replication plan marks its cleanup item superseded on the replica side and distinct from the restart refetch; PROJECT_STATUS.md gains the register row. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Keep the part when a resumed request gets no header; cap sidecar pages A resumed request that fails outright (dropped link, or an owner too old to know the offset variants) says nothing about the bytes on disk, so the part now stays and the whole-file retry lands on the same name. Sidecar pages count against the bandwidth cap on both ends, as the setting's contract says. Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Add the replication field to the test harness config Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Check the pause switch after the cap's wait, not before Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
f9bdf724b6 |
Library join: fix inverted backfill, catch-up loop, reverse device registration, member totals (#3110)
* Networking: serve streams on connections a protocol dials
The connection cache is shared between the dial side and the accept side,
so once a protocol dials a peer, the peer's own send path finds that
connection and opens its streams on it. Nothing read those streams: only
connections the event loop accepted, or those the reconnect loop handed
over, had a stream handler. The peer's request then sat unanswered until
its 30 s timeout while requests in the other direction answered in
milliseconds. This is the reverse RegisterDeviceRequest timeout SPAC-12
saw after join-remote.
get_or_create_connection now hands every connection it dials to the
event loop as TrackOutboundConnection, which spawns the same stream
handler an inbound connection gets. Messaging, sync, and job activity
pass their event loop handle; pairing keeps its own connection handling.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sync: the device that joins a library is the one that backfills
Every sync service started Uninitialized and backfilled from the first
connected partner, whatever state it already held. When the Mac Studio
joined the MacBook's library the MacBook was the one with a partner in
its device table, so it went Uninitialized -> Backfilling{Studio} and
applied the empty copy's snapshot over its own rows (LWW on the
deterministic default-space UUID). The Studio never backfilled because
the reverse registration had timed out and its device table held only
itself.
A copy of a library created from another device's library is the only
thing that may start Uninitialized. create_library_with_id now writes an
initial_backfill_pending marker into sync.db; PeerSync reads it on open
and starts Ready otherwise. A Ready device never requests a snapshot
(SharedChangeRequest.include_snapshot = false on catch-up) and repeated
catch-up failures no longer escalate to a full backfill. A copy still
waiting for its own first backfill refuses to serve as a join target.
The join carries the rule explicitly: RegisterDeviceRequest has
needs_initial_state, and RegisterDeviceResponse returns the responder's
own device record so the joiner's table is complete when the command
returns, replacing the spawned reverse request. sync_setup fails
instead of reporting devices_registered: true when that record is
missing.
Catch-up completion is recorded per peer in sync.db even when no rows
arrived; the Ready loop catches up once with a peer it has no record
for and no longer re-polls every peer whose watermark is 60 s old. A
missing watermark means never synced, not ancient, so the forced full
sync warning only fires for a real watermark past retention.
library_join_test pairs two daemons for real, joins Bob to Alice's
library, and asserts Alice's rows are unchanged, Bob holds Alice's
device row and spaces, and Alice runs no further catch-ups.
Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com>
* Sync: test that catch-up with an empty peer runs once
Alice holds a volume, Bob holds nothing of his own, so Alice never
writes a per-resource watermark for Bob. Before the per-peer catch-up
record, that made Bob a new peer on every loop iteration and Alice
caught up with him forever (2 -> 5 sessions in 16 s on
|
||
|
|
5e285d3c97 |
Nightly CLI channel: rolling prerelease workflow, sha-aware self-update, update timer (#3107)
* Add nightly CLI channel: rolling prerelease workflow, sha-aware self-update, update timer Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * TEMP: trigger nightly on PR branch Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Raise the launchd daemon file-descriptor limit to 8192 Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Remove temporary nightly trigger for the PR branch Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly: save the rust cache, note why Linux builds on 22.04 Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Address review: semver-ordered stable updates, assert nightly target commit, quote systemd ExecStart Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly: skip the xtask daemon build, retry release creation Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * TEMP: trigger nightly on PR branch Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Nightly: remove the runner's libunwind before setup.sh Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> * Remove temporary nightly trigger for the PR branch Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai-staging[bot] <305915985+capy-ai-staging[bot]@users.noreply.github.com> |
||
|
|
61dea60558 |
CI green on sources: runner toolchain, clippy, typecheck, tests compile (#3102)
* ci: install rustfmt and clippy components on every runner
Self-hosted runners have rustc preinstalled, so setup-rust skipped the
dtolnay/rust-toolchain step that added the components and the Rust
Formatting job died with 'cargo-fmt is not installed'. Add the components
to rust-toolchain.toml and run rustup component add unconditionally.
* setup: install FFmpeg development libraries on Linux and macOS
crates/ffmpeg is a default workspace member that links libav* through
ffmpeg-sys-next, so a fresh machine fails the first cargo build with
'libavutil.pc not found'. Install the libav dev packages and pkg-config
from scripts/setup.sh for every supported package manager, warn from
xtask setup when pkg-config cannot find them, and document the requirement
in the README build section.
* test: call the trash directly in delete_strategy_test
|
||
|
|
9929d22c72 | Land MacBook work: sources.track fix, source hashing test, addressing and locked-volumes plans | ||
|
|
fec139fbdf |
Remove Spacebot and the voice overlay
Spacebot ships separately. This drops its UI, the voice overlay and the Alt+Space shortcut that opened it, the spacebot app config and window, the @spacebot/api-client and @spacedrive/ai wiring, and the Spacebot and Console design docs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
3cd589bc87 |
Regenerate the Tauri capability schemas
Key order only, from a build. |
||
|
|
4e78b40e8e |
Follow folders on other devices in Photos and preview them in Quick Look
Photos takes a focus on another device's folder and lists it through search.media. Cells keep their SdPath, so thumbnail requests go to the device that owns the files. Quick Look on a cell asks the data plane for the path on this machine: its own for a file here, answered in process, and its path inside the mounted share for one elsewhere, from files.local_path. Only the latest answer reaches the panel, so a cursor that has moved on is not pulled back. The device slug no longer holds a listing back. |
||
|
|
e752cb460b |
Preview and open files from other devices in the desktop app
Quick Preview reads every original through useOriginalUrl: the asset protocol for a file on this device, and files.stream_url for one on another, so a remote video plays and seeks without a download. It replaces five copies of the URL code across the renderers and the mesh viewer. Open and double-click hand another device's file to the opener at the path files.local_path returns. Open With, Show in Finder and Share, in the menu and in the inspector, take only files on this device, since another device's path names a different file here or none. The CSP allows media from loopback. |
||
|
|
a733bca637 |
CLI: rename, undo, mirror, organize, flatten, archive, extract, attributes, link, trash
sd file rename (--to or rules), undo, organize, flatten, archive, extract, attributes, link and trash list|restore|empty, plus sd job journal and --remove-extras on merge. Every one shows its findings and plan before the prompt through one run_with_preflight. |
||
|
|
e1e73f2b38 |
Dedupe as a delete target: first copy or chosen copies, read in full before removal
files.delete takes a Duplicates target: a scope with keep first (walk order) or a list of copies to keep with every other copy of their content going. The job reads each surplus copy and its keeper in full before removing, records the hashes it learns, and checkpoints the compare cursor so it resumes. Preflight validates the scope and keep list and previews keepers as skips and surplus copies as deletes. Store gains duplicated_contents_beneath and copies_beneath. files.duplicates is registered as a core query and reports each copy's SdPath. CLI: sd file dedupe with --keep, --keep-under, --min-size, --dry-run. |
||
|
|
3cd44f2daa |
Add preflight to actions and build folder merge on it
Two opt-in methods per action, validate: and preview:, over the exact input the action takes. Validation answers findings with stable codes and the facts of the execution, runs again at dispatch and refuses on an error. Preview answers an FsPlan read from the index, kept under a handle so files.directory_listing can serve the directory as it would look after the operation. Copy, move, merge and delete answer both. Merge is new: a resumable job that recurses into matching folders, reads both sides before skipping a duplicate, applies a per-leaf conflict policy, leaves conflicts alone and can consume its sources. Delete's validation counts, across every store, which files are the last copy of their content. The ValidationResult and ConfirmationRequest hook is gone, and so is the CLI's local conflict detection. file copy, merge and delete render preflight before the prompt and take --dry-run. |
||
|
|
903c8f2110 |
Delete from a folder what a comparison names
files.delete takes a comparison as its targets alongside a path list, so the client sends five fields and the daemon derives the set from the index as the job runs. The job drains the matcher the compare query pages, checkpoints its cursor after each batch, and for `both` reads whichever side lacks an integrity hash before removing a file, writing what it learned back to the store. Pairs whose bytes differ, or whose copy in B is gone by then, stay and are reported. The CLI gains `sd file delete A --against B --show both` behind a y/N prompt, plus the plain paths form. Compare matched on content uuids, which re-derive from the integrity hash once a copy is verified, so a copy verified on one side stopped matching its unread twin. It now matches on the sampled hash every store keys a content by, with integrity hashes deciding where both sides have them; FsEntry carries both hashes for that. |
||
|
|
16325fc5c1 |
Name compared folders A and B, and their sets after a Venn diagram
Left and right only meant first and second argument, and nothing on screen said which folder was which. "Same" meant different things per mode, "changed" implied a direction compare can't know, and "location" collided with the retired locations. The output now names both folders as A and B up front, says how files matched, then counts Only in A, Only in B, In both, and Different (by path only). --by location becomes --by path and the --show values follow, as do the API types, generated TS types, skill, and docs. Single-side listings get Size and Modified columns under a title naming the set, and --help explains each value. |
||
|
|
ac49fc1f09 |
Walk every page of a folder comparison in the CLI
files compare printed one page and handed back an --after cursor to paste. The CLI now follows the cursor itself: totals first, then each set's files streaming one per line as pages of 5000 arrive. --limit caps the total and says N of M listed when it cuts a set short, a closed pipe like head ends the walk quietly, and --format json emits one document holding the totals and every entry. The skill doc follows. |
||
|
|
e5e6749c26 |
Compare two folders over the source stores
paths.compare answers what differs between two folders from the source stores at query time. By location it merges both sides in index order into only-left, only-right, changed and same. By content it streams one side and asks the other side's stores which content ids they hold, so a renamed or moved copy still matches. Pages resume from a cursor and the first page carries every set's count. Unhashed pairs fall back to size and mtime and get counted, so a page says how much rests on that. sd-cli file compare wraps it. On the store side, files_beneath takes a Start, including one that resumes a nested store at its first directory, and contents_beneath answers the content lookup a batch at a time. Working out which stores a path reaches moved into ops/paths/reach.rs, shared with search.media and paths.context. No catalog.db: the P7 row, the filesystem-intelligence plan, the previs and the design docs now treat cross-source questions as store queries. |
||
|
|
b16816b73b |
Page Photos from the source stores and follow searches
Photos listed only a folder's direct children and never saw the explorer's search, so searching from a folder of folders showed no photos. search.media pages images and videos straight from source SQLite. Files come out in the order the directory_path and sibling indexes already keep, so a page reads its own rows and resumes from a (source, directory, name) cursor. Extension, kind and hidden narrow in SQL; name, size, date and tags are judged per row the way the store backend of search.files judges them. A folder reaches its innermost source and any sources nested beneath it. The navigation focus now carries the running search, and Photos lists it in place of the folder. Folders list everything beneath them. Pages grow the grid as they land, and tile identities name their drive's cache, so a library-wide search across drives renders. |
||
|
|
8f12892c12 |
Let a daemon start without the default places
The first client to connect starts automatic discovery: it tracks the system volume, adds the home folder as a source and walks every attached drive. For a library meant to hold only what you add, like one whose data directory lives on the drive it catalogs, that indexes the Mac into it. sd start --no-default-sources, and the same flag on restart and on sd-daemon, now leaves those three out. The pass still restores snapshots, heals coverage for sources already tracked and hashes them, so a source you added keeps working across restarts. sd-server keeps the defaults. |
||
|
|
0062a34d58 |
Open Quick Look outside gpui's app borrow
Opening the panel or handing it a new file loads the file before the call returns, and the panel spins the event loop while it waits. Called from a key action, that let gpui's main-thread tasks run while the action still held the app, and the grid's frame watchdog panicked on the second borrow, which aborts inside an FFI callback. Requests now record the wanted file and the panel catches up on a later turn of the main thread, with no gpui or RefCell borrow held across a panel call. Posting real key events reproduced the abort on the old code and runs clean now: Space opens, arrows follow, Esc closes, Space reopens. |
||
|
|
e89141a4c6 |
Preview originals in Photos with Quick Look
Space opens the photo under the cursor in the system Quick Look panel, so HEIC, RAW, and video open at full size straight from the file. Space or Esc closes it. A controller hung off the window's next responder takes control of the panel and serves it the one file on show. Keys the panel doesn't use go back to the gpui view, so the arrows still move the cursor and the preview follows, and the number keys still tag in tag mode. On drop the controller leaves the chain and the panel looks again, so it never holds a controller that's gone. |
||
|
|
338e052ca5 |
Add selection and tag mode to Photos
Photos now selects the way the explorer does: click, cmd-click to toggle, shift-click for a run, cmd-A, arrows, Esc to clear. T opens the same tag bar the explorer has, with the library's first ten tags on the number keys. A key takes its tag off the selection when every selected photo carries it and puts it on all of them otherwise. Tagged tiles get dots in the corner. The listing now carries each cell's record and tags, and the plane follows the file rows the daemon announces. Indexing announces rows without their tags, so a row naming none is believed only for records this window is changing and read back with files.by_id otherwise. Tag changes queue on a per-folder channel from the moment the folder is listed. |
||
|
|
90e2f3ba3a |
Make Apps > Photos open its window
Nothing built spacedrive-native, so the Apps menu looked beside the desktop binary, found nothing, and logged the error to the dev terminal only. just dev-desktop now builds sd-native first. A failed build only warns, since sd-native is not a default member and can trail core. A launch that fails now shows a dialog. Photos quits when its window closes, and launching it while it runs brings its window forward through NSRunningApplication, which the launcher's doc already claimed it did. |
||
|
|
4e58ddca55 |
Publish device facts over the peer plane
Paired devices answer a new DeviceFacts request with their hardware and mounted volumes, under the ids their sources anchor to. The peer fetches them on every sync beside the device summary and keeps them in the replica manifest with its own receipt time, so an offline device still describes itself as it last did. volumes.list lists those volumes under their owner, mounted only while it's connected, and devices.list fills in a paired device's hardware. The overview folds replicas into their owner's volume bars the way local sources fold into theirs. Another device's volumes show when they were last seen and hide the local volume actions on desktop and mobile. Volume classifications travel as names, so one a peer's build doesn't know reads as unknown instead of failing the response. |
||
|
|
8a878a3c9b |
Clear location and entry residue from the code
The error variants, the permissions flag, the sync fixtures, and a long tail of comments still named locations and entries after both left the product. LocationNotFound goes from the query, action, and CLI errors; can_manage_locations becomes can_manage_sources; ChangeConfig's location_root becomes root; and the sync tests key their watermarks, checkpoints, dependency parsing, and messages on models that still sync. The transaction manager's stubbed pre-HLC methods and the bulk operation types only they used are removed, and the spaces invalidation arm stops matching a location resource nothing emits. File transfers and deletes describe their allow-list as the source roots it is. The CLI no longer points a new cloud volume at sd location add. In the clients, mobile loses its location search scope and empty Locations section, an unused overview card mock is deleted, and empty states and copy say folder, place, or source instead of location. |
||
|
|
cc5ffc15d3 |
Retire the ephemeral qualifier
The word marked a contrast with the persistent entries index, and that index is gone. ops/indexing/ephemeral folds into ops/indexing, and the names now say what each piece is, keeping the in-memory arena apart from the durable registry and stores: EphemeralIndexCache, ephemeral_cache() -> VolumeIndex, volume_index() VolumeIndex (one drive's slot) -> Partition EphemeralIndex -> Arena EphemeralEventHandler -> FsEventHandler watch_ephemeral / unwatch_ephemeral -> watch_root / unwatch_root IndexerJobConfig::ephemeral_browse -> IndexerJobConfig::new set_ephemeral_index, from_ephemeral -> set_arena, from_arena ops/search/ephemeral_search -> arena_search core.ephemeral_status / ephemeral_reset -> core.index_status / index_reset The CLI's index subcommands become status and reset. Status output loses its always-empty legacy fields, EntryRef loses an id that was always zero, the indexer output loses a field nothing read, and module docs that described browsing outside managed locations now describe the volume index. On-disk snapshots keep the ephemeral.snapshot name: renaming the file would orphan every snapshot and re-walk every drive. The job system's ephemeral jobs, Spacedrop's ephemeral shares, and UI state that is never persisted keep the word, since it means what it says there. |
||
|
|
9374855db2 |
Retire the persistent-era search and event surface
Search reports one index, so IndexType and the index_type field are gone, along with the Locations filter kind, the locations filter and facet, the constructors that only existed to stamp Persistent, and the Entry-based enhanced output. Tags and hidden files are advertised filters with no caveat, since both backends apply them. Fifteen Event variants that nothing emits are removed: the Entry events, FsRawChange, the location-keyed Indexing events, and the legacy location, thumbnail, file-operation, and files-modified events, with their helper types. events.list now derives its names from the enum instead of a list that had drifted from it. The CLI printer, the Tauri and ts-client default subscriptions, and the client-side filter types follow, and an uncompiled stale copy of type_extraction.rs goes with them. |
||
|
|
318ab3891a |
Replace the library schema and remove locations
FD4 of the final entries drop. One migration drops 29 retired tables inside its own transaction, children before parents: with foreign keys on, dropping a parent makes SQLite parse every child's constraints, and the first attempt failed on sidecar, whose other parent entries was already gone. That attempt also showed Migrator::up leaves SQLite migrations unwrapped, which is why the migration opens its own transaction. Every library now converges on 14 tables. Proven on a copy of the live library, on a library the pre-drop build left half-migrated, on a brand-new library, and on the live Mac library after a backup; every dropped table held zero rows there apart from the seeded kind lookup. Locations leave with the schema: ops, domain type, entity, CLI domain, and every consumer in the web and mobile clients. PathBar pins become space-level Path items listed under Pinned, the sidebar's Places come from a new paths.system_folders query, Add Storage tracks sources and validates through sources.validate_path, device views list sources, and library statistics count sources as source_count. Content kind statistics read the source stores instead of a lookup table. |
||
|
|
7d81966c34 |
Move tags onto source stores end to end
tags.create stages a definition in library.db until first application adopts it into a store; apply resolves targets through the arena with the stores as the floor, so a cold daemon and a detached source both answer. Every claim is stamped by a real HLC seeded from DeviceManager; the library-id-as-device placeholder is gone. File targets carry the content key when the store knows it, so tagging a copy reaches the bytes without the client choosing. Writes against a remote-owned source author their rows durably in an assertion outbox and deliver to the owner over remote ops when it answers: sources.assertions.merge commits before replying, the reply is the ack, and redelivery is absorbed by the assertion primary key. Pending claims overlay replica listings immediately. Reads decorate from the stores everywhere: directory listings on both the arena and store paths, files.by_id, files.by_path, replica listings, and files.by_tag, which no longer needs a loaded arena. File::announce decorates its payload too, which is what makes tagging reactive: the event used to carry empty tags and the client faithfully rendered that. Deleted: TagManager, TagClosureService, TagConflictResolver, TagUsageAnalyzer, TaggingFacade, UserMetadataManager, ops/metadata, the validation module, and the semantic-tag field zoo. Tag is uuid, path, name, color, icon. The XOR-era UI gates (the needs-indexing toast and the inspector's isEphemeral message) are gone; the one honest refusal left is a path outside every tracked source. The CLI gains tag unapply and delete. No data migration: the legacy tables were verified empty, and their entities wait for FD3 and FD4. |
||
|
|
25fd868a2f |
Add path-scoped thumbnail generation
From a parallel session. thumbs.generate accepts a file or an indexed folder with missing, stale, and force modes, runs in resumable batches, and versions video tiles against host FFmpeg availability so a tile regenerated with better tooling replaces its predecessor in place. File and folder context menus and the new sd thumbs CLI domain call the same operation. paths.context now reports whether the exact path has a committed source record, and the path bar surfaces it with a refresh affordance. Coverage and resume semantics are documented in docs/core/thumbnail-generation.mdx. |
||
|
|
b653676e80 |
Docs, register, and interface polish from parallel sessions
README and CONTRIBUTING updates, the working register brought current, locations and data-model pages reconciled, redundancy and overview polish, media view tweaks, regenerated schemas, and the new Spacedrive icon asset. |
||
|
|
c32768f2e4 |
Make startup discovery explicit and permission-aware
Library loading no longer dispatches a filesystem discovery pass. The desktop requests one idempotent pass after the connected shell renders, a bare daemon initiates nothing, and sd index start --defaults gives CLI and server operators the same explicit path. Restored volume snapshots prevent repeat full walks. |
||
|
|
4e53d58dbb |
Keep baked thumbnails through remounts, fix Quick Preview originals
Hot-tier bake completion epochs survive virtualized cell unmounts, and a thumbnail hides its fallback only after the current image element loads its current URL. The Tauri asset boundary maps canonical macOS APFS data-volume paths back to the scoped /Users and /Volumes aliases so originals load without a 403, with tests over the alias mapping. |
||
|
|
f3a1ede2f8 |
Discover host media tools, bake video thumbstrips on demand
A machine-scoped registry discovers host FFmpeg and companion tools, surfaced in Services settings. The thumbnail service uses them for video posters and on-demand scrub sheets: thumbstrips bake per request into the volume hot tier and serve over the local HTTP servers with immutable content-versioned URLs, wired to the timeline scrubber. |
||
|
|
f0347a3179 |
Fix hot-thumb 404s and silence browse indexer jobs
The Tauri hot-thumb handler still read sources/<id>/thumbs.pvcache after the cache moved to volumes/<id>/, so every tile request 404'd. Directory browsing dispatches an ephemeral indexer per first visit; since the entries teardown dropped the ephemeral persistence guard, those jobs persisted a row and emitted start/completion events, so the desktop app played the job-done sound on every navigation. Browse jobs now run in the background: the listing already reaches the UI through ResourceChanged events, and completion is marked inside the job, not from its events. User-initiated indexing and volume mapping keep their events. |
||
|
|
b1b0d28b72 |
Refresh project context and working registers
README rewritten for the 2.0 release framing. AGENTS.md points at product-direction.mdx for the product boundary and makes PROJECT_STATUS.md the project-wide working register (the entries teardown table stays authoritative for that track). CLAUDE.md and PROJECT_STATUS.md are now tracked. Spacebot VISION marked retired. justfile gains dev-docs/build-docs and splits dev-desktop-watch out of dev-desktop. Tauri gen schemas regenerated. |