Skip to content

feat(file-lock): add lease-backed checkout authority - #1

Merged
opendev9 merged 4 commits into
devfrom
feature/fileop-lifecycle
Aug 6, 2026
Merged

feat(file-lock): add lease-backed checkout authority#1
opendev9 merged 4 commits into
devfrom
feature/fileop-lifecycle

Conversation

@opendev9

@opendev9 opendev9 commented Aug 5, 2026

Copy link
Copy Markdown

What changed

  • Adds the CE lease-backed lock authority and RPC bindings.
  • Enforces checkout state through the shared write lifecycle.

Validation

  • ./tests/cf-fileop/run.sh

opendev9 and others added 4 commits July 26, 2026 14:39
CE has nowhere to ask the two questions the write side needs: may this
write happen, and what happened. cf-ext.h only has read-side seams, and
Hub's register_file_op_hook has no upstream caller. Six capabilities were
blocked on that -- file lock, checkout, OnlyOffice write-back, properties,
tags, and metadata following a rename.

Building the lock first would have produced two of these: the lock's veto
point and the file_op event source, covering the same write entry points.
So the contract comes first and the lock registers into it.

common/cf-fileop.{c,h} defines PREPARE (first refusal wins, nothing
persisted yet), COMMITTED (immutable, exactly once per successful
operation) and ABORTED (best effort -- a provider needing reserve/release
must carry its own lease, which is why the lock is built on heartbeats).
Both it and common/cf-path.c depend on nothing but glib, so the vocabulary,
the path rules and the dispatcher all compile into a standalone test.

The seam goes in server/repo-op.c, not the already-registered
rpc-service.c: upload-file.c, the virtual repo merge and copy-mgr all reach
seaf_repo_manager_* directly, and an adjudication point with a way around
it is not one. That takes the upstream patch count from 33 to 35.

seafdav needs no patch -- its writes go through seafile_api.* into
repo-op.c, so it inherits the C answer. A second Python implementation
would only be a second thing to drift.

The Go fileserver does need its own seam, because it chunks, writes
objects, commits and updates the branch without entering C. It asks over
RPC rather than reimplementing. Unlike cf_ext.go it never caches the
verdict: caching "no provider registered" would leave a window, after an
operator enables a capability and restarts, in which every upload bypasses
it. What is cached is only which of three worlds we are in, to pick the
failure mode -- fail open with nothing registered, fail closed with
something registered.

Path normalization moves from cf-acl-resolve.c down to common/cf-path.c
with a forwarder left behind. The ACL keys rules by path and the lock keys
leases by path; if they normalized differently, a rule on /a/b and a lock
on /a/b/ would be about different objects.

Evidence: 144 C checks, 6 Go cross-language contract checks, 50 call sites
type-checked against the real header, 9 mutations all caught. None of that
proves the seam is reached at runtime -- that needs the Linux stack, and
the fake-provider veto matrix is not written yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, both found by taking the exit criteria literally.

The defect: copy and move shipped COMMITTED facts with no commit_id. Both
commit through put_dirent_and_commit and move_file_same_repo, two static
helpers that discarded the id gen_new_commit hands back. A fact without a
version cannot be lined up against the repo-update stream, which is where
the file change itself is recorded, and the omission is invisible -- the
consumer just gets an empty string. Both helpers now take an out param.
check-call-sites.py grew a rule for it: every COMMITTED but upload-blocks
must carry .commit_id, verified by mutation.

The gap: P0.5's one unmet exit criterion was "with no lock implemented
yet, a fake provider can already refuse at every write entry point". So
here is the fake provider, and the gate that drives it.

cf-fileop-test.c journals every event and refuses any operation whose
subject or source path has a marked component. It is gated at runtime, not
compiled out, because a build flag would mean the gate exercises an image
that is not the one shipped -- the same mistake that let a hand-written
settings fixture pass while the generated file raised NameError and
silently discarded every CloudFile setting. It is deliberately not a
CF_ENABLE_* switch: that list is product capabilities an operator may
reasonably turn on, and this can refuse writes.

Matching is component-wise, not substring or prefix. A substring makes
"notes-secret.txt" match a component of "secret", so a refusal meant for
one object quietly covers others; a prefix cannot be seeded, because
seeding means creating the marked directory and creating it is one of the
operations under test. The rule lives in cf-path.c with 15 shared cases
rather than private to the provider, since whether phase 2 of the gate
passes for the right reason depends on it.

The matrix runs in two phases because the fixtures can only be built while
refusal is off, and because a provider that refuses everything -- or a
broken service -- would make "every entry point refuses" trivially true.
Hence the positive controls.

159 C checks, 6 Go contract checks, 50 call sites type-checked, 11
mutations all caught. The end-to-end gate itself has never run: it needs
Linux. A written gate is not a green one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@opendev9
opendev9 merged commit b8e8637 into dev Aug 6, 2026
5 of 6 checks passed
@opendev9
opendev9 deleted the feature/fileop-lifecycle branch August 20, 2026 03:32
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