Skip to content

Cloud sync: cooperate with file-on-demand providers (WIP)#67

Draft
hatton wants to merge 16 commits into
masterfrom
OneDrive
Draft

Cloud sync: cooperate with file-on-demand providers (WIP)#67
hatton wants to merge 16 commits into
masterfrom
OneDrive

Conversation

@hatton

@hatton hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member

Draft / WIP. This is the long-running OneDrive branch for making lameta cooperate with cloud file-on-demand sync providers (OneDrive, Dropbox, Nextcloud, …). Opened as a draft so the review bots run against the branch.

Most recent preflight run focused on handling a broken/unreachable cloud provider at project load:

  • A per-load circuit breaker (cloudReadGuard) so a down/rate-limiting provider is hit ~once, not once-per-file.
  • Metadata reads soft-fail instead of spamming toasts + provider modals or aborting the whole load.
  • A single "couldn't download N files from " banner with Retry (Project.retryFailedCloudReads).
  • save() now refuses to overwrite a file whose real metadata was never read (prevents empty-content data loss syncing back to the cloud).

Plus a small pre-existing refactor already on the branch: a shared revealInFolderLabel() and a "reveal in folder" button on the cloud status card.

🤖 Generated with Claude Code

hatton and others added 13 commits July 8, 2026 13:04
…wn' polling

- sleep() left its AbortSignal "abort" listener attached after a normal
  (non-abort) timer fire; {once: true} only clears it when abort actually
  fires. Over a multi-hour hydration (one sleep() per poll, same long-lived
  signal) this accumulated one listener per poll and would eventually trigger
  MaxListenersExceededWarning. Now removed explicitly on normal resolution.
- hydrateFile polled forever if getCloudFileStatus kept returning "unknown"
  (e.g. fswin failing to load/read), since hydrateFile intentionally has no
  default timeout. Now gives up and rejects after 5 consecutive "unknown"
  polls, while a transient "unknown" that recovers does not trip it.
addCustomKeys() exports every non-blacklisted file property into the
IMDI <Keys> element, including the internal mediaStatsCache cache
blob (probed ffprobe/ExifReader stats + size/mtime), which is
bookkeeping, not archival metadata. Export the property key constant
from File.ts and blacklist it. RO-Crate export was checked and does
not have the same issue (it only iterates folder.metadataFile
properties filtered by isCustom, never enumerates arbitrary File
properties). CustomFieldsTable already excludes it too, since it also
filters on isCustom.
… never blocks

Real testing against a genuine OneDrive Files-On-Demand folder showed the old
hydrateFile was broken on real placeholders: it triggered hydration with an
async fs.promises read, which throws "UNKNOWN: unknown error, read"
(errno -4094) on a real placeholder -- libuv's overlapped I/O can't satisfy the
cloud recall. The error escaped before polling even began, so
File.makeAvailableOffline() failed immediately with a cryptic message. The unit
tests missed it because they read a real *local* temp file, never an actual
placeholder.

Only a synchronous, blocking read reliably triggers the recall and blocks until
the bytes arrive -- but for a multi-GB file on a slow link that blocks for
hours, so it must never run on the renderer/UI thread. hydrateFile now runs that
read inside a worker_thread (eval Worker, no separate bundled entry):

- Reads the whole file in 4 MB chunks with fs.readSync, so completion guarantees
  the file is fully local, and reports real byte progress via onProgress.
- Retries the transient cloud-recall UNKNOWN/-4094 error with exponential
  backoff (6 tries) then fails cleanly, instead of crashing. A slow-but-working
  download never throws (readSync just blocks), so there is still no default
  timeout.
- Main thread only awaits worker messages; timeoutMs/signal terminate the worker
  and reject with a timeout/AbortError.

Validated live: main thread stays responsive during the worker read (~3000
heartbeat ticks, max gap ~100ms over a 180s read); detection, timeout, abort and
the UNKNOWN-retry failure paths all behave correctly.

Adds an injectable setHydrationRunnerForTests() seam so specs don't need a real
worker; options shape is now { timeoutMs?, signal?, onProgress? } (dropped
pollIntervalMs -- there is no more attribute polling).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ckmarks; user doc

- Refresh cloudStatus right after readMetadataFile()'s sync read hydrates a
  dehydrated metadata file, so the row stops showing the cloud icon/grey name
  the moment the session becomes readable (with regression test).
- Comment out (not remove) the Explorer-style green checkmarks for
  locally-available files, per John -- may be re-enabled later.
- Add readme-onedrive.md: plain-language user documentation for the OneDrive
  status icons, the request-file checkbox, auto-fetch setting, and offline
  behavior.
- Locale catalogs cleaned of strings removed during the UI iterations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add readme-images/ with four captures from the live app: the file-list
status icons (online-only, downloading, local), the OneDrive Status card
in its resting, Waiting, and offline states. Embed each in the matching
section of readme-onedrive.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract revealInFolderLabel() so the file-list context menu and the cloud
status card use the same platform-appropriate wording, and add a reveal-in-
folder button to the cloud status card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a cloud sync provider (OneDrive/Dropbox/Nextcloud) is down or
rate-limiting, lameta's load-time metadata reads fail: File.readMetadataFile()
does a synchronous read of each .session/.person/.meta sidecar to force
on-demand hydration, and on a broken provider that read throws
(errno -4094 / code UNKNOWN). Previously each failure produced two lameta
toasts plus the provider's own native modal per file, and could abort the
whole project load.

Now:
- cloudReadGuard: a per-load circuit breaker. On the first failed cloud read
  it trips and skips both pinning and reading of the remaining placeholders,
  so we stop hammering a broken provider (~1 provider modal instead of N).
- readMetadataFile soft-fails on a cloud read failure: record the file, mark
  it cloudMetadataUnavailable, no per-file toast, and don't throw -- the
  project keeps loading. Genuine (non-cloud) file errors keep the original
  notify-and-rethrow behavior.
- CloudUnavailableBanner: one persistent, dismissible banner
  ("couldn't download N files from <provider>") with Retry, which re-reads
  just the failed files via Project.retryFailedCloudReads().
- save() refuses to overwrite a file whose real metadata we never read, so a
  soft-failed placeholder can't be clobbered with empty content (and synced
  back to the cloud) on the next blur/quit save.

Also: prefer-const cleanup in normalizeIncomingDateString.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/components/MediaStats.tsx Outdated
Comment thread locale/en/fields.po
@hatton

hatton commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8] Consulted Devin on 2026-07-10 14:49 MDT up to commit e624901c64606ec46b5b3201fb413554f98bd8df.

Result: 0 bugs, 2 Investigate flags, 3 informational (skipped). The 2 Investigate flags were mirrored as review threads above:

  • src/components/MediaStats.tsx — unhandled ffprobe promise rejection (more likely with the new cloud flow)
  • locale/en/fields.po / vocabularies.po — message catalogs emptied (translation-catalog concern, spans the wider WIP branch)

Note: Devin reviewed the entire 13-commit OneDrive WIP branch (51 files), not just the latest cloud broken-provider work.

hatton and others added 3 commits July 10, 2026 15:36
…logs

E (MediaStats.tsx): the media-stats probe called getStatsFromFileAsync().then()
with no .catch(). The new cloud flow re-fires the probe when a file hydrates, so
a failing ffprobe (corrupt file, unsupported codec, failed hydration) left the
panel stuck on "Processing..." forever. Add a .catch() that logs and shows a
"could not read media information" status instead of swallowing the rejection.
Also drop a dead `eslint-disable react-hooks/exhaustive-deps` directive (the
plugin is not registered in eslint.config.mjs, so it errored).

F (locale/en/fields.po, vocabularies.po): these were accidentally emptied in an
earlier commit. They are generated build artifacts (yarn strings:extract-json,
from archive-configurations/**/*.json5), so regenerate them from the current
JSON source of truth rather than restoring stale content: 118 field strings and
49 vocabulary strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement the macOS side of cloud-sync support and harden the whole
feature against the boundary conditions cloud placeholders introduce.

macOS provider (cloudFileStatus.ts):
- MacCloudFileProvider detects dataless placeholders (size>0, blocks==0)
  under a sync root; one-shot materialization via brctl (iCloud) or a
  reader child process (FileProvider). capabilities gains canFetch
  (a real provider exists) distinct from canPin (durable pin; Windows
  only). Sync-root discovery from ~/Library/CloudStorage/*, home-dir
  symlink aliases, and iCloud Drive incl. Desktop & Documents sync.
- Path compares fold to NFC so decomposed macOS names still match.
- macOS Download-button UI (no durable pin), disabled while offline;
  auto-fetch skipped while offline.

Load reliability & UX:
- Parallel prefetch of cloud-evicted metadata at project load, so a
  fully-evicted project opens in ~seconds instead of minutes of serial
  on-demand hydration (cloudMetadataPrefetch.ts).
- All project-open paths route through the async loader + modal
  progress dialog ("Getting project information from <provider>...");
  the trigger now also fires for cloud-evicted projects, not just large
  ones, so the renderer no longer freezes on open.
- Corrupt/truncated metadata (interrupted sync) no longer risks
  overwriting recoverable bytes: a failed parse sets metadataReadFailed
  and blocks the save. Sync-conflict duplicate files load harmlessly.

Testing:
- FakeCloudFileProvider (E2E_FAKE_CLOUD_PROVIDER manifest) + e2e/
  cloudSync.e2e.ts exercise the cloud UI on any machine, no account.
- Unit tests for the mac provider, prefetch, corrupt metadata, offline
  behavior, and read-failure signatures.
- readme-cloud-testing.md: manual test plan for human testers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename reliability (PatientFS):
- Roll back per-file renames when the final folder rename exhausts its
  retry budget, so a failed rename always leaves a fully consistent
  old state (previously: folder kept the old name while files inside
  carried the new prefix, and the next load silently reverted the
  user's rename).
- Remove the rename-out-and-back "trial run": torture testing showed it
  could strand the folder under the NEW name when a handle opened
  mid-flight blocked the rename-back. With rollback in place the
  precheck was redundant as well as risky; the directory is now renamed
  exactly once.
- Drop the graceful-fs monkey-patch and dependency: its Windows rename
  retry only wraps the async fs.rename, and PatientFS is all-sync, so
  it contributed nothing. Comments now document the verified
  EBUSY/EPERM contention semantics.
- Fix zombie metadata repair: the single-zombie case was skipped
  (`> 1` off-by-one) and the rename got a bare filename instead of a
  full path, so the repair had never worked.
- Fix the .maybeLostInfoHere rescue path (Path.join misuse) and an
  unfinished error-dialog sentence.

Vanished files must not wound the app:
- New File.fileMissing soft-fail flag: saving a file whose folder was
  deleted/renamed outside lameta (e.g. a collaborator's change applied
  by OneDrive/Dropbox) now warns once and skips, instead of popping an
  ENOENT toast on every save trigger; self-heals if the folder returns.
- Detail-pane ErrorBoundary gets resetKeys so a per-file render error
  recovers on selection change instead of wedging the pane until
  restart; selection changes survive a failed save of the outgoing
  folder.

Never silently clobber a collaborator's edit:
- File.save() now tracks each metadata file's on-disk mtime+size from
  read and after every write. If the file changed underneath us
  (another machine's sync, an external editor), the disk version is
  moved aside as "<name> (changed on another computer <date>).<ext>"
  before writing ours, with a single clear warning. Byte-identical
  mtime churn is ignored. This closes the verified silent-data-loss
  vector where B's stale in-memory save reverted A's synced-down edit
  with no trace.

Tests: LockedFileScenarios (rollback, proven failing-without-fix),
ZombieMetadataRepair, VanishedFile, ExternallyChangedFile unit specs;
renameContention + vanishedSession e2e; RenameContention.stress.spec
(opt-in LAMETA_STRESS torture harness with a lock-meddler process, plus
real-provider mode) and twoUserSync e2e (opt-in LAMETA_EXPERIMENT
two-user hazard harness).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant