Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds backup management capabilities to s3d by exposing snapshot list/delete operations through the admin API and wiring corresponding s3d backup CLI subcommands, building on the existing SQLite snapshot/backup implementation.
Changes:
- Extend the backend interface and Sia implementation to support listing and deleting recorded snapshot backups.
- Add new admin API endpoints to list backups and delete a backup snapshot; update OpenAPI accordingly.
- Add
s3d backup create|list|deleteCLI commands and a changeset entry documenting the feature.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
sia/sia.go |
Adds backend methods for listing/deleting snapshots and maps store snapshot records to admin API DTOs. |
s3/s3.go |
Extends admin backend interface and registers new admin API routes for backup listing/deletion. |
s3/s3_test.go |
Updates admin API tests to validate list/delete snapshot endpoints via HTTP. |
s3/admin.go |
Introduces Snapshot response type and handlers for listing/deleting snapshots. |
openapi.yml |
Documents the new /system/sqlite3/backups list and delete endpoints plus Snapshot schema. |
cmd/s3d/main.go |
Wires the new backup command and its subcommands into the CLI command tree. |
cmd/s3d/backup.go |
Implements the `s3d backup create |
.changeset/backup_commands.md |
Records the user-facing change for release tooling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cfd57b9 to
9569274
Compare
chris124567
left a comment
There was a problem hiding this comment.
LGTM except for what Copilot said about NewAdmin docstring
|
moved back into |
192d7a8 to
8f9de4c
Compare
8f9de4c to
8bb8684
Compare
Yeah I'm tired of rebasing it so I'll wait until the base PR gets merged :) |
8bb8684 to
e21bfc9
Compare
|
Should we consider calling the command |
There was a problem hiding this comment.
🟡 Changes recommended
s3d snapshots restore --force can fail on Windows because os.Rename won’t overwrite an existing database file unless it is removed first.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 24/24 changed files
- Comments generated: 1
- Review effort level: Lite
ca9a64c to
029a7c8
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The restore/remote-list flows currently require opening the local SQLite DB to obtain the app key, which undermines the stated “lost database” recovery story and should be addressed (e.g., alternate key source) before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 24/24 changed files
- Comments generated: 1
- Review effort level: Lite
029a7c8 to
2b6a64d
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical snapshot safety and consistency issues remain, along with API compatibility and response-ordering fixes.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
s3/admin.go:23
idwas part of the existing/snapshotsresponse and is removed from JSON here, silently breaking clients of the already-published POST endpoint. The new Sia object ID can be the cross-database address without removing the local ID; keep this field for wire compatibility (and update the OpenAPI schema), or explicitly version this breaking response change.
// ID is the local database row, needed because a snapshot row is inserted
// before its Sia object exists. It is deliberately not serialized:
// snapshots are addressed by their Sia object ID, and a row ID is
// meaningless outside the one database that issued it.
ID int64 `json:"-"`
s3/admin.go:125
- The empty result from
Store.ListSnapshotsis a nil slice, so encoding it here produces JSONnull, while this endpoint is documented as returning an array. Consumers that iterate the response must special-casenull; normalize an empty result to[]before encoding.
jc.Encode(snapshots)
- Files reviewed: 24/24 changed files
- Comments generated: 3
- Review effort level: Lite
2b6a64d to
b4054db
Compare
| // truncated database behind | ||
| tmp, err := os.CreateTemp(destDir, "restore-*.tmp") | ||
| checkFatalError("failed to create temporary file", err) | ||
| defer os.Remove(tmp.Name()) |
There was a problem hiding this comment.
nit: checkFatalError calls os.Exit which will result in the defer calls being skipped so to be thorough here you should make a helper that removes it then calls checkFatalError
2bb0ea9 to
afa3a15
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved snapshot consistency, restore-safety, and deletion-race issues include one critical finding.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
cmd/s3d/snapshots.go:240
- The existing
s3d.db-walis removed before the replacement database is renamed into place. If the second sidecar removal or the lateros.Renamefails, the old database remains but its WAL may already be gone, losing committed transactions and undermining the promise that a failed restore cannot damage the existing database. Stage or preserve the sidecars and only remove them after a replacement-safe commit.
for _, sidecar := range []string{dbPath + "-wal", dbPath + "-shm"} {
if err := os.Remove(sidecar); err != nil && !errors.Is(err, os.ErrNotExist) {
checkFatalRestoreError("failed to remove stale sidecar", err)
}
sia/sia.go:396
- Flushing before creating the snapshot does not make the backup self-contained because
store.Backupruns while writes remain allowed (the API docs explicitly promise this). A PUT that starts after this flush can leave a pending object row in the SQLite image whosefilenamepoints intouploads/; the snapshot only contains SQLite, so restoring it into a fresh directory has no file to upload and cannot recover that object. The snapshot path needs to quiesce/coordinate new writes or otherwise exclude and handle pending rows.
if err := s.FlushObjects(ctx); err != nil {
return s3.Snapshot{}, fmt.Errorf("failed to flush objects before snapshot: %w", err)
}
sia/sia.go:562
- A concurrent metadata sync can delete this row after
DeleteObjectsucceeds and beforeDeleteSnapshotsBySiaObjectruns. In that race,deleted == 0makes an already successful delete returnErrSnapshotNotFound(and skips the orphan-loop wake), so the CLI reports a failure. After the initialHasSnapshotObjectcheck, treat the local delete as idempotent instead of converting this race into 404.
} else if deleted == 0 {
return s3.ErrSnapshotNotFound
- Files reviewed: 24/24 changed files
- Comments generated: 1
- Review effort level: Lite
| // snapshot object, pins it, and marks the record pinned. On failure the | ||
| // snapshot is rolled back. | ||
| func (s *Sia) CreateSnapshot(ctx context.Context) (_ s3.Snapshot, err error) { | ||
| if err := s.FlushObjects(ctx); err != nil { |
This PR adds a CLI command to create, list, delete, and restore snapshots.
s3d snapshots create/list/deletewrap the admin API.list --remoteenumerates the snapshots stored on the Sia network, reading the app key from the local database rather than the admin API, so it works against a stopped daemon.restorefetches a backup from Sia and writes it to the data directory, which is what makes a lost database recoverable from the network alone.References #202