host: the cut's archive publication and its recovery move into movesarchive.go, no behaviour change (Task 5 fix round 1, ruling 7)
A pure move of cutArchive, publishCutArchive, the journalled identity and the snapshot helpers, with the source-disk headroom (up to archive.Cap kept for the move's life, twice that during a rebuild; T6 pricing is a Plan 6 follow-up) documented in the file's header. movesScanFiles gains the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+283
−251
approved_moves_copy_test.go+1 −0internal/host/movesarchive.go+282 −0internal/host/movescut.go+0 −251
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go| index ba3a55ebb..a4d21b43a 100644 |
| --- a/approved_moves_copy_test.go |
| +++ b/approved_moves_copy_test.go |
| @@ -549,6 +549,7 @@ var movesScanFiles = []string{ |
| "internal/host/movesfloor.go", |
| "internal/host/movesfloor_linux.go", |
| "internal/host/movesfloor_other.go", |
| + "internal/host/movesarchive.go", |
| "internal/host/movescut.go", |
| "internal/host/movesguard.go", |
| "internal/host/movesheldenv.go", |
diff --git a/internal/host/movesarchive.go b/internal/host/movesarchive.go| new file mode 100644 |
| index 000000000..58b1da6b0 |
| --- /dev/null |
| +++ b/internal/host/movesarchive.go |
| @@ -0,0 +1,282 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "log" |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/lease" |
| + "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/moves/archive" |
| +) |
| + |
| +// The cut's archive (moves spec §7.4 staged, §8.3, codex 19, ruling G14): |
| +// how the stopped files directory's one sealed archive is identified, |
| +// published create-only into this box's move-snapshots/ and staged into the |
| +// lane's files/, and how a replay finishes exactly the identity an earlier |
| +// pass journalled. The cut's phase loop is in movescut.go; this file is the |
| +// archive's publication and its recovery, on their own so the recovery |
| +// rules can be read in one place (Task 5 fix round 1, ruling 7). |
| +// |
| +// Source-disk headroom. The preparing round keeps the newest built archive |
| +// on the source's data disk under <JournalDir>/archive/ for the life of |
| +// the move, so the cut can publish the very object the target verified |
| +// (G14). That costs up to archive.Cap (256 MiB) of the source's data disk |
| +// while a move is in flight, and a rebuild (the stopped files differ from |
| +// the verified archive) holds the old and the new one together until the |
| +// new one lands and pruneArchives removes the old: up to twice archive.Cap |
| +// for the length of one build. The target's T6 does not price this |
| +// source-side headroom; that is a Plan 6 follow-up. |
| +// cutArchiveStep is the archive's identity in the op journal, written |
| +// before any PUT so a replay publishes the same object (codex 19). Reuse |
| +// marks the preparing archive the target verified (G14): the seal names |
| +// its staged versions and nothing is staged again. |
| +type cutArchiveStep struct { |
| + builtArchive |
| + Reuse bool `json:"reuse,omitempty"` |
| +} |
| + |
| +// cutArchive publishes the cut's one sealed archive (§8.3) and returns the |
| +// seal's files: the sealed object and its manifest by version. |
| +// |
| +// Its identity is fixed before any PUT (sealing is randomized), in the op |
| +// journal. The first run reuses the preparing archive when its plaintext |
| +// digest is the stopped files' and its staged versions are pinned and its |
| +// sealed file is still on this disk (G14); otherwise it builds and seals |
| +// a new one. Then, from the journalled identity: |
| +// - the sealed file on disk: create-only into move-snapshots/ (an existing |
| +// object is this cut's earlier PUT), then staged into files/ unless it |
| +// is the reused archive, whose staged versions the ledger holds; |
| +// - no file, but both objects in move-snapshots/: staged into files/ by a |
| +// guarded server-side copy from there (or, reused, nothing to stage); |
| +// - neither: nothing was published under that identity, so the archive |
| +// is rebuilt and resealed and the journal entry overwritten. |
| +// |
| +// why is a recorded reason the stopped files cannot be archived. |
| +func (c *cutRun) cutArchive(ctx context.Context, plain string) (builtArchive, []moves.SealedObject, string, error) { |
| + cfg := c.cfg() |
| + host, id := c.rec.Host, c.g.ID |
| + own, vs, err := c.ownStores(ctx) |
| + if err != nil { |
| + return builtArchive{}, nil, "", err |
| + } |
| + ledger, err := readMoveLedger(ctx, own, cfg.OwnBucket, moves.LedgerKey(host, id)) |
| + if err != nil { |
| + return builtArchive{}, nil, "", err |
| + } |
| + j, err := c.journal() |
| + if err != nil { |
| + return builtArchive{}, nil, "", err |
| + } |
| + adir := archiveDir(cfg.DataDir, host, id) |
| + var step cutArchiveStep |
| + journalled := false |
| + if raw, ok := j.Steps[cutStepArchive]; ok && json.Unmarshal([]byte(raw), &step) == nil && step.Name == archiveName(step.Sealed) && step.Plain == plain { |
| + step.dir = adir |
| + journalled = true |
| + } |
| + snap := moves.SnapshotsPrefix(host, id) |
| + if journalled { |
| + switch onDisk := sealedOnDisk(step.builtArchive); { |
| + case onDisk: |
| + case c.snapshotPresent(ctx, vs, snap+step.Name): |
| + default: |
| + journalled = false // nothing published under it: rebuild |
| + } |
| + } |
| + if !journalled { |
| + reuse := ledger.ArchivePlain == plain && ledger.ArchiveVersions != nil && ledger.ArchiveVersions.Pinned() && ledger.ArchiveSealed != "" |
| + if reuse { |
| + kept := builtArchive{Name: archiveName(ledger.ArchiveSealed), Sealed: ledger.ArchiveSealed, Plain: plain, dir: adir} |
| + if m, ok := keptManifest(kept); ok && sealedOnDisk(kept) { |
| + kept.Bytes, kept.Entries = m.Bytes, len(m.Entries) |
| + if st, err := os.Stat(kept.sealedPath()); err == nil { |
| + kept.SealedSize = st.Size() |
| + } |
| + step = cutArchiveStep{builtArchive: kept, Reuse: true} |
| + } else { |
| + reuse = false |
| + } |
| + } |
| + if !reuse { |
| + a, why, retry, err := c.w.buildArchive(c.rec) |
| + if err != nil || why != "" { |
| + return builtArchive{}, nil, why, err |
| + } |
| + if retry { |
| + return builtArchive{}, nil, "", errors.New("a stopped file changed while it was archived") |
| + } |
| + if a.Plain != plain { |
| + return builtArchive{}, nil, "", fmt.Errorf("the stopped files' digest is %s, recorded %s", a.Plain, plain) |
| + } |
| + step = cutArchiveStep{builtArchive: a} |
| + } |
| + body, err := json.Marshal(step) |
| + if err != nil { |
| + return builtArchive{}, nil, "", err |
| + } |
| + j.Mark(cutStepArchive, string(body)) |
| + if err := j.Write(cfg.DataDir); err != nil { |
| + return builtArchive{}, nil, "", err |
| + } |
| + if err := c.fault("archive-journaled"); err != nil { |
| + return builtArchive{}, nil, "", err |
| + } |
| + } |
| + a := step.builtArchive |
| + files, err := c.publishCutArchive(ctx, own, vs, step, ledger) |
| + return a, files, "", err |
| +} |
| + |
| +// publishCutArchive is cutArchive's PUTs from a journalled identity. |
| +func (c *cutRun) publishCutArchive(ctx context.Context, own lease.MirrorStore, vs lease.VersionedStore, step cutArchiveStep, ledger moveLedger) ([]moves.SealedObject, error) { |
| + cfg := c.cfg() |
| + host, id := c.rec.Host, c.g.ID |
| + a := step.builtArchive |
| + snap := moves.SnapshotsPrefix(host, id) |
| + sealedSnap, manifestSnap := snap+a.Name+".sealed", snap+a.Name+".json" |
| + onDisk := sealedOnDisk(a) |
| + var manifest []byte |
| + if onDisk { |
| + var err error |
| + if manifest, err = os.ReadFile(a.manifestPath()); err != nil { |
| + return nil, err |
| + } |
| + fp, ok := vs.(lease.FilePutIfAbsent) |
| + if !ok { |
| + return nil, errors.New("this box cannot write the archive create-only") |
| + } |
| + if _, err := fp.PutFileIfAbsent(ctx, cfg.OwnBucket, sealedSnap, a.sealedPath()); err != nil && !errors.Is(err, lease.ErrExists) { |
| + return nil, fmt.Errorf("snapshot the sealed archive: %w", err) |
| + } |
| + if _, err := vs.PutIfAbsent(ctx, cfg.OwnBucket, manifestSnap, manifest); err != nil && !errors.Is(err, lease.ErrExists) { |
| + return nil, fmt.Errorf("snapshot the archive manifest: %w", err) |
| + } |
| + if err := c.fault("archive-own-put"); err != nil { |
| + return nil, err |
| + } |
| + } |
| + if step.Reuse { |
| + ref := ledger.ArchiveVersions |
| + if ref == nil || !ref.Pinned() || ledger.ArchiveSealed != a.Sealed { |
| + return nil, fmt.Errorf("the ledger no longer names the archive %s", a.Name) |
| + } |
| + msize := int64(len(manifest)) |
| + if !onDisk { |
| + msize = c.snapshotSize(ctx, vs, manifestSnap) |
| + } |
| + return []moves.SealedObject{ |
| + {Key: ref.SealedObjectKey, VersionID: ref.SealedVersion, Size: a.SealedSize}, |
| + {Key: ref.ManifestKey, VersionID: ref.ManifestVersion, Size: msize}, |
| + }, nil |
| + } |
| + prefix := moves.FilesPrefix(host, id) |
| + sealedKey, manifestKey := prefix+a.Name+".sealed", prefix+a.Name+".json" |
| + var sv, mv string |
| + var msize int64 |
| + var err error |
| + if onDisk { |
| + if sv, err = c.gm.PutFileVersioned(ctx, c.gm.dst, sealedKey, a.sealedPath(), a.SealedSize, digestHex(a.Sealed)); err != nil { |
| + return nil, fmt.Errorf("stage the sealed archive: %w", err) |
| + } |
| + if err := c.fault("archive-first-copy"); err != nil { |
| + return nil, err |
| + } |
| + if mv, err = c.gm.PutVersioned(ctx, c.gm.dst, manifestKey, manifest); err != nil { |
| + return nil, fmt.Errorf("stage the archive manifest: %w", err) |
| + } |
| + msize = int64(len(manifest)) |
| + } else { |
| + // The file is gone, the snapshot is there: a server-side copy from |
| + // it, guarded, as the replica lane copies. |
| + objs, err := own.List(ctx, cfg.OwnBucket, snap) |
| + if err != nil { |
| + return nil, fmt.Errorf("list the snapshots: %w", err) |
| + } |
| + so, sok := objs[sealedSnap] |
| + mo, mok := objs[manifestSnap] |
| + if !sok || !mok { |
| + return nil, fmt.Errorf("the snapshot of %s is incomplete", a.Name) |
| + } |
| + if _, sv, err = c.gm.CopyVersioned(ctx, cfg.OwnBucket, sealedSnap, so.ETag, c.gm.dst, sealedKey); err != nil { |
| + return nil, fmt.Errorf("stage the sealed archive: %w", err) |
| + } |
| + if err := c.fault("archive-first-copy"); err != nil { |
| + return nil, err |
| + } |
| + if _, mv, err = c.gm.CopyVersioned(ctx, cfg.OwnBucket, manifestSnap, mo.ETag, c.gm.dst, manifestKey); err != nil { |
| + return nil, fmt.Errorf("stage the archive manifest: %w", err) |
| + } |
| + a.SealedSize, msize = so.Size, mo.Size |
| + } |
| + if err := c.fault("archive-second-copy"); err != nil { |
| + return nil, err |
| + } |
| + if !lease.UsableVersionID(sv) || !lease.UsableVersionID(mv) { |
| + return nil, fmt.Errorf("the staged archive has no usable version ids (%q, %q)", sv, mv) |
| + } |
| + log.Printf("moves: %s: staged the cut's archive %s", host, a.Name) |
| + return []moves.SealedObject{{Key: sealedKey, VersionID: sv, Size: a.SealedSize}, {Key: manifestKey, VersionID: mv, Size: msize}}, nil |
| +} |
| + |
| +// sealedOnDisk reports whether the archive's sealed file and manifest are |
| +// on this disk and the sealed file hashes to its name. |
| +func sealedOnDisk(a builtArchive) bool { |
| + if _, err := os.Stat(a.manifestPath()); err != nil { |
| + return false |
| + } |
| + sum, err := fileSHA256(a.sealedPath()) |
| + return err == nil && sum == a.Sealed |
| +} |
| + |
| +// keptManifest reads the manifest of an archive kept on disk; it must |
| +// describe that archive. |
| +func keptManifest(a builtArchive) (archive.Manifest, bool) { |
| + var m archive.Manifest |
| + body, err := os.ReadFile(a.manifestPath()) |
| + if err != nil || json.Unmarshal(body, &m) != nil || m.Sealed != a.Sealed || m.Plain != a.Plain { |
| + return m, false |
| + } |
| + return m, true |
| +} |
| + |
| +// snapshotPresent reports whether move-snapshots/ holds both objects of the |
| +// archive named base (key without suffix), current and not deleted. |
| +func (c *cutRun) snapshotPresent(ctx context.Context, vs lease.VersionedStore, base string) bool { |
| + return c.snapshotSize(ctx, vs, base+".sealed") >= 0 && c.snapshotSize(ctx, vs, base+".json") >= 0 |
| +} |
| + |
| +// snapshotSize is the current size of key in this box's bucket, or -1. |
| +func (c *cutRun) snapshotSize(ctx context.Context, vs lease.VersionedStore, key string) int64 { |
| + vers, err := vs.ListVersions(ctx, c.cfg().OwnBucket, key) |
| + if err != nil { |
| + return -1 |
| + } |
| + for _, v := range vers { |
| + if v.Key == key && v.IsLatest && !v.DeleteMarker { |
| + return v.Size |
| + } |
| + } |
| + return -1 |
| +} |
| + |
| +// sealNamesArchive reports whether the seal's files are the archive with |
| +// the sealed digest given. |
| +func sealNamesArchive(seal moves.Seal, sealed string) bool { |
| + if sealed == "" { |
| + return false |
| + } |
| + name := archiveName(sealed) |
| + n := 0 |
| + for _, o := range seal.Files { |
| + base := filepath.Base(o.Key) |
| + if base == name+".sealed" || base == name+".json" { |
| + n++ |
| + } |
| + } |
| + return n == 2 && len(seal.Files) == 2 |
| +} |
diff --git a/internal/host/movescut.go b/internal/host/movescut.go| index 6c62b013c..d4373eea4 100644 |
| --- a/internal/host/movescut.go |
| +++ b/internal/host/movescut.go |
| @@ -20,7 +20,6 @@ import ( |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/lease" |
| "github.com/carlosframework/platform/internal/moves" |
| - "github.com/carlosframework/platform/internal/moves/archive" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -1102,239 +1101,6 @@ func (c *cutRun) sealArchive(ctx context.Context) (builtArchive, bool, error) { |
| return a, true, nil |
| } |
| -// cutArchiveStep is the archive's identity in the op journal, written |
| -// before any PUT so a replay publishes the same object (codex 19). Reuse |
| -// marks the preparing archive the target verified (G14): the seal names |
| -// its staged versions and nothing is staged again. |
| -type cutArchiveStep struct { |
| - builtArchive |
| - Reuse bool `json:"reuse,omitempty"` |
| -} |
| - |
| -// cutArchive publishes the cut's one sealed archive (§8.3) and returns the |
| -// seal's files: the sealed object and its manifest by version. |
| -// |
| -// Its identity is fixed before any PUT (sealing is randomized), in the op |
| -// journal. The first run reuses the preparing archive when its plaintext |
| -// digest is the stopped files' and its staged versions are pinned and its |
| -// sealed file is still on this disk (G14); otherwise it builds and seals |
| -// a new one. Then, from the journalled identity: |
| -// - the sealed file on disk: create-only into move-snapshots/ (an existing |
| -// object is this cut's earlier PUT), then staged into files/ unless it |
| -// is the reused archive, whose staged versions the ledger holds; |
| -// - no file, but both objects in move-snapshots/: staged into files/ by a |
| -// guarded server-side copy from there (or, reused, nothing to stage); |
| -// - neither: nothing was published under that identity, so the archive |
| -// is rebuilt and resealed and the journal entry overwritten. |
| -// |
| -// why is a recorded reason the stopped files cannot be archived. |
| -func (c *cutRun) cutArchive(ctx context.Context, plain string) (builtArchive, []moves.SealedObject, string, error) { |
| - cfg := c.cfg() |
| - host, id := c.rec.Host, c.g.ID |
| - own, vs, err := c.ownStores(ctx) |
| - if err != nil { |
| - return builtArchive{}, nil, "", err |
| - } |
| - ledger, err := readMoveLedger(ctx, own, cfg.OwnBucket, moves.LedgerKey(host, id)) |
| - if err != nil { |
| - return builtArchive{}, nil, "", err |
| - } |
| - j, err := c.journal() |
| - if err != nil { |
| - return builtArchive{}, nil, "", err |
| - } |
| - adir := archiveDir(cfg.DataDir, host, id) |
| - var step cutArchiveStep |
| - journalled := false |
| - if raw, ok := j.Steps[cutStepArchive]; ok && json.Unmarshal([]byte(raw), &step) == nil && step.Name == archiveName(step.Sealed) && step.Plain == plain { |
| - step.dir = adir |
| - journalled = true |
| - } |
| - snap := moves.SnapshotsPrefix(host, id) |
| - if journalled { |
| - switch onDisk := sealedOnDisk(step.builtArchive); { |
| - case onDisk: |
| - case c.snapshotPresent(ctx, vs, snap+step.Name): |
| - default: |
| - journalled = false // nothing published under it: rebuild |
| - } |
| - } |
| - if !journalled { |
| - reuse := ledger.ArchivePlain == plain && ledger.ArchiveVersions != nil && ledger.ArchiveVersions.Pinned() && ledger.ArchiveSealed != "" |
| - if reuse { |
| - kept := builtArchive{Name: archiveName(ledger.ArchiveSealed), Sealed: ledger.ArchiveSealed, Plain: plain, dir: adir} |
| - if m, ok := keptManifest(kept); ok && sealedOnDisk(kept) { |
| - kept.Bytes, kept.Entries = m.Bytes, len(m.Entries) |
| - if st, err := os.Stat(kept.sealedPath()); err == nil { |
| - kept.SealedSize = st.Size() |
| - } |
| - step = cutArchiveStep{builtArchive: kept, Reuse: true} |
| - } else { |
| - reuse = false |
| - } |
| - } |
| - if !reuse { |
| - a, why, retry, err := c.w.buildArchive(c.rec) |
| - if err != nil || why != "" { |
| - return builtArchive{}, nil, why, err |
| - } |
| - if retry { |
| - return builtArchive{}, nil, "", errors.New("a stopped file changed while it was archived") |
| - } |
| - if a.Plain != plain { |
| - return builtArchive{}, nil, "", fmt.Errorf("the stopped files' digest is %s, recorded %s", a.Plain, plain) |
| - } |
| - step = cutArchiveStep{builtArchive: a} |
| - } |
| - body, err := json.Marshal(step) |
| - if err != nil { |
| - return builtArchive{}, nil, "", err |
| - } |
| - j.Mark(cutStepArchive, string(body)) |
| - if err := j.Write(cfg.DataDir); err != nil { |
| - return builtArchive{}, nil, "", err |
| - } |
| - if err := c.fault("archive-journaled"); err != nil { |
| - return builtArchive{}, nil, "", err |
| - } |
| - } |
| - a := step.builtArchive |
| - files, err := c.publishCutArchive(ctx, own, vs, step, ledger) |
| - return a, files, "", err |
| -} |
| - |
| -// publishCutArchive is cutArchive's PUTs from a journalled identity. |
| -func (c *cutRun) publishCutArchive(ctx context.Context, own lease.MirrorStore, vs lease.VersionedStore, step cutArchiveStep, ledger moveLedger) ([]moves.SealedObject, error) { |
| - cfg := c.cfg() |
| - host, id := c.rec.Host, c.g.ID |
| - a := step.builtArchive |
| - snap := moves.SnapshotsPrefix(host, id) |
| - sealedSnap, manifestSnap := snap+a.Name+".sealed", snap+a.Name+".json" |
| - onDisk := sealedOnDisk(a) |
| - var manifest []byte |
| - if onDisk { |
| - var err error |
| - if manifest, err = os.ReadFile(a.manifestPath()); err != nil { |
| - return nil, err |
| - } |
| - fp, ok := vs.(lease.FilePutIfAbsent) |
| - if !ok { |
| - return nil, errors.New("this box cannot write the archive create-only") |
| - } |
| - if _, err := fp.PutFileIfAbsent(ctx, cfg.OwnBucket, sealedSnap, a.sealedPath()); err != nil && !errors.Is(err, lease.ErrExists) { |
| - return nil, fmt.Errorf("snapshot the sealed archive: %w", err) |
| - } |
| - if _, err := vs.PutIfAbsent(ctx, cfg.OwnBucket, manifestSnap, manifest); err != nil && !errors.Is(err, lease.ErrExists) { |
| - return nil, fmt.Errorf("snapshot the archive manifest: %w", err) |
| - } |
| - if err := c.fault("archive-own-put"); err != nil { |
| - return nil, err |
| - } |
| - } |
| - if step.Reuse { |
| - ref := ledger.ArchiveVersions |
| - if ref == nil || !ref.Pinned() || ledger.ArchiveSealed != a.Sealed { |
| - return nil, fmt.Errorf("the ledger no longer names the archive %s", a.Name) |
| - } |
| - msize := int64(len(manifest)) |
| - if !onDisk { |
| - msize = c.snapshotSize(ctx, vs, manifestSnap) |
| - } |
| - return []moves.SealedObject{ |
| - {Key: ref.SealedObjectKey, VersionID: ref.SealedVersion, Size: a.SealedSize}, |
| - {Key: ref.ManifestKey, VersionID: ref.ManifestVersion, Size: msize}, |
| - }, nil |
| - } |
| - prefix := moves.FilesPrefix(host, id) |
| - sealedKey, manifestKey := prefix+a.Name+".sealed", prefix+a.Name+".json" |
| - var sv, mv string |
| - var msize int64 |
| - var err error |
| - if onDisk { |
| - if sv, err = c.gm.PutFileVersioned(ctx, c.gm.dst, sealedKey, a.sealedPath(), a.SealedSize, digestHex(a.Sealed)); err != nil { |
| - return nil, fmt.Errorf("stage the sealed archive: %w", err) |
| - } |
| - if err := c.fault("archive-first-copy"); err != nil { |
| - return nil, err |
| - } |
| - if mv, err = c.gm.PutVersioned(ctx, c.gm.dst, manifestKey, manifest); err != nil { |
| - return nil, fmt.Errorf("stage the archive manifest: %w", err) |
| - } |
| - msize = int64(len(manifest)) |
| - } else { |
| - // The file is gone, the snapshot is there: a server-side copy from |
| - // it, guarded, as the replica lane copies. |
| - objs, err := own.List(ctx, cfg.OwnBucket, snap) |
| - if err != nil { |
| - return nil, fmt.Errorf("list the snapshots: %w", err) |
| - } |
| - so, sok := objs[sealedSnap] |
| - mo, mok := objs[manifestSnap] |
| - if !sok || !mok { |
| - return nil, fmt.Errorf("the snapshot of %s is incomplete", a.Name) |
| - } |
| - if _, sv, err = c.gm.CopyVersioned(ctx, cfg.OwnBucket, sealedSnap, so.ETag, c.gm.dst, sealedKey); err != nil { |
| - return nil, fmt.Errorf("stage the sealed archive: %w", err) |
| - } |
| - if err := c.fault("archive-first-copy"); err != nil { |
| - return nil, err |
| - } |
| - if _, mv, err = c.gm.CopyVersioned(ctx, cfg.OwnBucket, manifestSnap, mo.ETag, c.gm.dst, manifestKey); err != nil { |
| - return nil, fmt.Errorf("stage the archive manifest: %w", err) |
| - } |
| - a.SealedSize, msize = so.Size, mo.Size |
| - } |
| - if err := c.fault("archive-second-copy"); err != nil { |
| - return nil, err |
| - } |
| - if !lease.UsableVersionID(sv) || !lease.UsableVersionID(mv) { |
| - return nil, fmt.Errorf("the staged archive has no usable version ids (%q, %q)", sv, mv) |
| - } |
| - log.Printf("moves: %s: staged the cut's archive %s", host, a.Name) |
| - return []moves.SealedObject{{Key: sealedKey, VersionID: sv, Size: a.SealedSize}, {Key: manifestKey, VersionID: mv, Size: msize}}, nil |
| -} |
| - |
| -// sealedOnDisk reports whether the archive's sealed file and manifest are |
| -// on this disk and the sealed file hashes to its name. |
| -func sealedOnDisk(a builtArchive) bool { |
| - if _, err := os.Stat(a.manifestPath()); err != nil { |
| - return false |
| - } |
| - sum, err := fileSHA256(a.sealedPath()) |
| - return err == nil && sum == a.Sealed |
| -} |
| - |
| -// keptManifest reads the manifest of an archive kept on disk; it must |
| -// describe that archive. |
| -func keptManifest(a builtArchive) (archive.Manifest, bool) { |
| - var m archive.Manifest |
| - body, err := os.ReadFile(a.manifestPath()) |
| - if err != nil || json.Unmarshal(body, &m) != nil || m.Sealed != a.Sealed || m.Plain != a.Plain { |
| - return m, false |
| - } |
| - return m, true |
| -} |
| - |
| -// snapshotPresent reports whether move-snapshots/ holds both objects of the |
| -// archive named base (key without suffix), current and not deleted. |
| -func (c *cutRun) snapshotPresent(ctx context.Context, vs lease.VersionedStore, base string) bool { |
| - return c.snapshotSize(ctx, vs, base+".sealed") >= 0 && c.snapshotSize(ctx, vs, base+".json") >= 0 |
| -} |
| - |
| -// snapshotSize is the current size of key in this box's bucket, or -1. |
| -func (c *cutRun) snapshotSize(ctx context.Context, vs lease.VersionedStore, key string) int64 { |
| - vers, err := vs.ListVersions(ctx, c.cfg().OwnBucket, key) |
| - if err != nil { |
| - return -1 |
| - } |
| - for _, v := range vers { |
| - if v.Key == key && v.IsLatest && !v.DeleteMarker { |
| - return v.Size |
| - } |
| - } |
| - return -1 |
| -} |
| - |
| // archived (§7.4, §7.5): the flip. Its precondition is re-read from the |
| // claim: the seal names the archive at the record's N, and the record's |
| // digests are the seal's archive; otherwise this is a replay of staged's |
| @@ -1391,23 +1157,6 @@ func (c *cutRun) archived(ctx context.Context) (bool, error) { |
| return true, nil |
| } |
| -// sealNamesArchive reports whether the seal's files are the archive with |
| -// the sealed digest given. |
| -func sealNamesArchive(seal moves.Seal, sealed string) bool { |
| - if sealed == "" { |
| - return false |
| - } |
| - name := archiveName(sealed) |
| - n := 0 |
| - for _, o := range seal.Files { |
| - base := filepath.Base(o.Key) |
| - if base == name+".sealed" || base == name+".json" { |
| - n++ |
| - } |
| - } |
| - return n == 2 && len(seal.Files) == 2 |
| -} |
| - |
| // revertClaim is the claim half of a revert (§7.4 reverting, §7.5): once |
| // the source has given up execution, the claim goes to reverting under |
| // the source with the staging lane's park queued (hostclaim.Revert). While |