mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
feat(webapp): old URLs follow a moved file, and share links follow the file (BEA-81) (#130)
There is no rename in beardrive: the scanner emits a put at the new path and a delete at the old, same device, same blob, one cycle. Everything keyed on a path therefore broke the moment a file moved — the viewer 404'd, history lost the file's own past versions, restore refused them, and a share link either 404'd or silently served whatever unrelated file later took its address. internal/webapp/moves.go derives the pairing from the ops the replay already walks, cached with the snapshot. Deliberately not a rename op: journal.Less and Replay are what every device converges to, and every already-shipped journal would still need the heuristic to read its own history. The two rules point in opposite directions on purpose. A viewer URL is an address, so a LIVE path always wins and only an empty one redirects. A share token is a promise about one file, so it follows the file even when a new one takes the old address — and 404s forever once the file is deleted. Nothing here writes an op or touches sync. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
aa33ba55cc
commit
e19fec0534
@@ -31,7 +31,9 @@ classDiagram
|
||||
class Browser {
|
||||
folder listing, file view
|
||||
per-view routes
|
||||
+moved: /resolve?path= on a tree miss only
|
||||
}
|
||||
note for Browser "A missing path is decided from /tree alone — the file is never fetched — so the X-Bdrive-Canonical-Path header /file answers with would never reach the browser, and a moved FOLDER has no content fetch to hang a header on. The not-found branch asks GET /resolve?path= instead, then replaceState-navigates to the destination and prints one Moved from … line above it (BEA-81)"
|
||||
|
||||
class router {
|
||||
+VIEW_ROUTES dashboard history install settings
|
||||
|
||||
@@ -37,7 +37,7 @@ classDiagram
|
||||
class volume {
|
||||
-source Source
|
||||
-refresh time.Duration
|
||||
-snap *snapshot
|
||||
-snap *snapshot (files + moves)
|
||||
+snapshot(ctx)
|
||||
+invalidate()
|
||||
}
|
||||
@@ -68,6 +68,30 @@ classDiagram
|
||||
}
|
||||
note for sourcedOp "An op's Device field is whatever the writer typed; From is the journal object it actually came out of, which the /store door gates. Attribution reads From — a peer cannot sign someone else's name on a change by editing its own journal"
|
||||
note for RemoteSource "OpenBlob is the single blob-read door: the sha must match blobRe, and verify re-hashes the bytes whenever the backend is a PutSigner — in direct-upload mode the server never saw the content, so the store is the only thing that could have swapped it. It stops re-hashing only once the object is PROVABLY immutable: both presign doors refuse a key that exists, so every URL for a blob was minted before its first PUT and dies at mint+PresignTTL; past that age the hub is the only writer left. That is what remote.Object.Modified is for"
|
||||
class MoveSource {
|
||||
<<interface>>
|
||||
+FilesWithMoves(ctx) files, moveIndex
|
||||
}
|
||||
class moveIndex {
|
||||
<<map path→[]pathEvent>>
|
||||
+buildMoveIndex(sorted ops)
|
||||
+resolveForward(idx, files, p) viewer
|
||||
+resolveShare(idx, files, p, since) /s/
|
||||
+chainSegments(idx, p) []segment
|
||||
+resolveFolder(idx, files, dir) all-or-nothing
|
||||
}
|
||||
class pathEvent {
|
||||
+At the delete that ended it
|
||||
+To "" = deleted, not moved
|
||||
+ToAt destination's create
|
||||
}
|
||||
class segment {
|
||||
+Path
|
||||
+From, To window it WAS the file
|
||||
}
|
||||
note for moveIndex "There is no rename op — a move is put(new) + delete(old), same device, same blob, one cycle — so the index is DERIVED inside the replay Files already runs and cached with the snapshot. Pairing needs same device, |Δt| ≤ 30s, B's first-ever put, and one-to-one both ways; anything ambiguous stays a plain deletion. Nothing here writes an op: journal.Less and Replay are untouched"
|
||||
note for segment "Time-bounded on purpose: a bare set of paths would make history?path=docs/a.md show the ops of the NEW a.md that took the old address"
|
||||
|
||||
class Uploader {
|
||||
<<interface>>
|
||||
+Upload(ctx, path, r, size, who, note)
|
||||
@@ -346,6 +370,11 @@ classDiagram
|
||||
|
||||
Source <|.. DirSource
|
||||
Source <|.. RemoteSource
|
||||
MoveSource <|.. RemoteSource : optional, like Uploader — DirSource has no journals, so no moves
|
||||
MoveSource ..> moveIndex
|
||||
volume o-- moveIndex : cached with the snapshot
|
||||
moveIndex *-- pathEvent
|
||||
moveIndex ..> segment : chainSegments
|
||||
Uploader <|-- DirectUploader
|
||||
DirectUploader <|.. RemoteSource
|
||||
RemoteSource o-- Backend : Prefixed(Root, projectID)
|
||||
|
||||
Reference in New Issue
Block a user