feat(file-lock): add lease-backed checkout authority - #1
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Validation