Skip to content

Commit

Permalink
feat: add repo stats to restic package
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Dec 24, 2023
1 parent b8c2e81 commit 26d4724
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 27 deletions.
2 changes: 1 addition & 1 deletion backrest.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func main() {
// Create and serve the HTTP gateway
apiServer := api.NewServer(
configStore,
orchestrator, // TODO: eliminate default config
orchestrator,
oplog,
)

Expand Down
119 changes: 113 additions & 6 deletions gen/go/v1/restic.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cache

import (
"fmt"

v1 "github.com/garethgeorge/backrest/gen/go/v1"
"go.etcd.io/bbolt"
)

type Cache struct {
db *bbolt.DB
}

func NewCache(databasePath string) (*Cache, error) {
db, err := bbolt.Open(databasePath, 0600, nil)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
return &Cache{db: db}, nil
}

func (c *Cache) Close() error {
return c.db.Close()
}

func (c *Cache) SetRepoStats(repo *v1.Repo, stats *v1.RepoStats) error {
return c.db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("repo_stats"))
if err != nil {
return fmt.Errorf("create bucket: %w", err)
}
return b.Put([]byte(repo.Id), protoutil.MustMarshal(stats))
})
}
26 changes: 6 additions & 20 deletions internal/oplog/oplog.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"path"
"sync"
"sync/atomic"
"time"

v1 "github.com/garethgeorge/backrest/gen/go/v1"
Expand All @@ -30,12 +29,11 @@ const (
var ErrNotExist = errors.New("operation does not exist")

var (
SystemBucket = []byte("oplog.system") // system stores metadata
OpLogBucket = []byte("oplog.log") // oplog stores existant operations.
OpLogSoftDeleteBucket = []byte("oplog.log_soft_delete") // oplog_soft_delete stores soft deleted operations
RepoIndexBucket = []byte("oplog.repo_idx") // repo_index tracks IDs of operations affecting a given repo
PlanIndexBucket = []byte("oplog.plan_idx") // plan_index tracks IDs of operations affecting a given plan
SnapshotIndexBucket = []byte("oplog.snapshot_idx") // snapshot_index tracks IDs of operations affecting a given snapshot
SystemBucket = []byte("oplog.system") // system stores metadata
OpLogBucket = []byte("oplog.log") // oplog stores existant operations.
RepoIndexBucket = []byte("oplog.repo_idx") // repo_index tracks IDs of operations affecting a given repo
PlanIndexBucket = []byte("oplog.plan_idx") // plan_index tracks IDs of operations affecting a given plan
SnapshotIndexBucket = []byte("oplog.snapshot_idx") // snapshot_index tracks IDs of operations affecting a given snapshot
)

// OpLog represents a log of operations performed.
Expand All @@ -45,7 +43,6 @@ type OpLog struct {

subscribersMu sync.RWMutex
subscribers []*func(*v1.Operation, *v1.Operation)
nextId atomic.Int64
}

func NewOpLog(databasePath string) (*OpLog, error) {
Expand All @@ -61,12 +58,11 @@ func NewOpLog(databasePath string) (*OpLog, error) {
o := &OpLog{
db: db,
}
o.nextId.Store(1)

if err := db.Update(func(tx *bolt.Tx) error {
// Create the buckets if they don't exist
for _, bucket := range [][]byte{
SystemBucket, OpLogBucket, OpLogSoftDeleteBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket,
SystemBucket, OpLogBucket, RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket,
} {
if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
return fmt.Errorf("creating bucket %s: %s", string(bucket), err)
Expand Down Expand Up @@ -194,17 +190,7 @@ func (o *OpLog) Delete(ids ...int64) error {
return fmt.Errorf("deleting operation %v: %w", id, err)
}
removedOps = append(removedOps, removed)

b := tx.Bucket(OpLogSoftDeleteBucket)
val, err := proto.Marshal(removed)
if err != nil {
return fmt.Errorf("marshalling operation %v: %w", id, err)
}
if err := b.Put(serializationutil.Itob(id), val); err != nil {
return fmt.Errorf("putting operation %v into soft delete bucket: %w", id, err)
}
}

return nil
})
if err == nil {
Expand Down
10 changes: 10 additions & 0 deletions internal/protoutil/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,13 @@ func RestoreProgressEntryToProto(p *restic.RestoreProgressEntry) *v1.RestoreProg
PercentDone: p.PercentDone,
}
}

func RepoStatsToProto(s *restic.RepoStats) *v1.RepoStats {
return &v1.RepoStats{
TotalSize: int64(s.TotalSize),
TotalUncompressedSize: int64(s.TotalUncompressedSize),
CompressionRatio: s.CompressionRatio,
TotalBlobCount: int64(s.TotalBlobCount),
SnapshotCount: int64(s.SnapshotsCount),
}
}
42 changes: 42 additions & 0 deletions internal/protoutil/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,45 @@ func TestBackupProgressEntryToProto(t *testing.T) {
})
}
}

func TestRepoStatsToProto(t *testing.T) {
cases := []struct {
name string
stats *restic.RepoStats
want *v1.RepoStats
}{
{
name: "no stats",
stats: &restic.RepoStats{},
want: &v1.RepoStats{},
},
{
name: "with stats",
stats: &restic.RepoStats{
TotalSize: 1,
TotalUncompressedSize: 2,
CompressionRatio: 3,
TotalBlobCount: 5,
CompressionProgress: 6,
CompressionSpaceSaving: 7,
SnapshotsCount: 8,
},
want: &v1.RepoStats{
TotalSize: 1,
TotalUncompressedSize: 2,
CompressionRatio: 3,
TotalBlobCount: 5,
SnapshotCount: 8,
},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := RepoStatsToProto(c.stats)
if !proto.Equal(got, c.want) {
t.Errorf("wanted: %+v, got: %+v", c.want, got)
}
})
}
}
10 changes: 10 additions & 0 deletions pkg/restic/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,13 @@ func ValidateSnapshotId(id string) error {
}
return nil
}

type RepoStats struct {
TotalSize int64 `json:"total_size"`
TotalUncompressedSize int64 `json:"total_uncompressed_size"`
CompressionRatio float64 `json:"compression_ratio"`
CompressionProgress int64 `json:"compression_progress"`
CompressionSpaceSaving float64 `json:"compression_space_saving"`
TotalBlobCount int64 `json:"total_blob_count"`
SnapshotsCount int64 `json:"snapshots_count"`
}
24 changes: 24 additions & 0 deletions pkg/restic/restic.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,30 @@ func (r *Repo) Unlock(ctx context.Context, opts ...GenericOption) error {
return nil
}

func (r *Repo) Stats(ctx context.Context, opts ...GenericOption) (*RepoStats, error) {
opt := resolveOpts(opts)

args := []string{"stats", "--json", "--mode=raw-data"}
args = append(args, r.extraArgs...)
args = append(args, opt.extraArgs...)

cmd := exec.CommandContext(ctx, r.cmd, args...)
cmd.Env = append(cmd.Env, r.buildEnv()...)
cmd.Env = append(cmd.Env, opt.extraEnv...)

output, err := cmd.CombinedOutput()
if err != nil {
return nil, newCmdError(cmd, string(output), err)
}

var stats RepoStats
if err := json.Unmarshal(output, &stats); err != nil {
return nil, newCmdError(cmd, string(output), fmt.Errorf("command output is not valid JSON: %w", err))
}

return &stats, nil
}

type RetentionPolicy struct {
KeepLastN int // keep the last n snapshots.
KeepHourly int // keep the last n hourly snapshots.
Expand Down
Loading

0 comments on commit 26d4724

Please sign in to comment.