Skip to content

macOS dataless follow-up: opt-in hydrate / hydrate-evict modes to capture cloud-only files that changed remotely #185

Description

@hexsprite

Summary

Follow-up to #118. The v1 dataless handling (detect SF_DATALESS, propagate from parent snapshot on (size, mtime, inode) match, never open) works well: warm-at-any-point files stay in every snapshot, no bandwidth is burned, and the warning noise is gone.

What remains is the gap called out as deferred in the #118 resolution: a dataless file that is new, or changed remotely since it was last warm, is never captured. For iCloud-heavy homes ("Desktop & Documents" is one checkbox), that can be tens of GB of user documents that exist in exactly one place — Apple's cloud. Accidental-edit protection, the main reason to back these files up at all, is exactly the case where the local copy is dataless and the remote copy is the one that changed.

Key observation: no download needed to detect change

There is no public API to get a content checksum of a cloud-only file without hydrating it. But none is needed: fileproviderd keeps the placeholder's stat metadata current — when the remote content changes, the dataless inode's size/mtime are updated without materialization.

The existing parent-match identity (size, mtime, inode) is therefore already a remote-change detector:

Dataless file state Action Bandwidth
identity matches parent snapshot propagate ChunkRefs (shipped in #118) none
identity differs / no parent entry hydrate this one file, chunk it, optionally evict that file only

Steady-state cost per backup is the remote delta only. The first run in a hydrate mode pays a one-time cost for files never previously captured.

Proposed configuration

Per-source setting with a global default, as sketched in #118:

sources:
  - path: /Users/jbb
    dataless: skip            # current behavior, stays the default
Mode Behavior
skip (default) current v1 behavior: parent-propagate or omit
hydrate on identity mismatch, read the file normally (open triggers materialization); file stays warm afterwards
hydrate-evict as hydrate, then return the file to dataless after the snapshot commits, via NSFileProviderManager.evictItem

Implementation notes

Hydration read path. Opening a dataless file starts async materialization; size/blocks change mid-read, which today trips the drift guard. Cleanest is materialize-then-read: after open(), poll fstat until SF_DATALESS clears (with backoff and a per-file timeout), then chunk through the existing unchanged path so the drift guard keeps its semantics. This also sidesteps the EDEADLK class of problems (#183) on the file read path.

Eviction safety (hydrate-evict).

  • Only evict files vykar itself hydrated this run — never files that were already warm (the user may be working on them).
  • Re-stat immediately before evicting; if mtime advanced since hydration, the user touched it — skip eviction.
  • Eviction is best-effort: log failures (file in use, provider busy) and continue; never fail the backup.
  • Needs a small Swift/Obj-C bridge for NSFileProviderManager.evictItem(identifier:) (macOS 11+), behind #[cfg(target_os = "macos")].

Guards.

  • Pre-flight: sum the sizes of files that will hydrate this run and refuse with a clear error if it exceeds free disk space, rather than ENOSPC mid-backup.
  • hydrate_parallel: N knob to bound concurrent hydrations (head-of-line blocking on one multi-GB file is otherwise easy to hit).
  • End-of-source summary line: hydrated N files / M bytes, evicted K.

Interaction with the file cache. The ctime-ignoring dataless lookup from #118 already covers the evict-rehydrate ctime churn, so hydrate-evict does not re-download unchanged files on subsequent runs — that was the prerequisite for this mode being viable at all.

Restore semantics are unchanged from #118: restored files are warm, and FileProvider xattrs should be stripped on restore (still open as its own deferred item).

Sequencing

Two PRs would keep review small:

  1. dataless: skip|hydrate — config plumbing, materialize-then-read, guards, tests. Pure Rust, no bridge.
  2. hydrate-evict — the Swift/Obj-C bridge and eviction tracking.

Happy to build both, pending direction on the approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions