Skip to content

Commit

Permalink
feat: basic forget support in backend and UI
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Dec 1, 2023
1 parent 7e93e08 commit d22d9d1
Show file tree
Hide file tree
Showing 37 changed files with 849 additions and 430 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ jobs:
node-version: "20"

- name: Install Deps
run: ./hack/install-deps.sh
run: ./scripts/install-deps.sh

- name: Build
run: ./hack/build.sh
run: ./scripts/build.sh

- name: Test
run: PATH=$(pwd):$PATH go test ./...
11 changes: 5 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,10 @@ jobs:
node-version: "20"

- name: Install Deps
run: ./hack/install-deps.sh
run: ./scripts/install-deps.sh

- name: Build
run: ./hack/build.sh

- name: Rename Files
run: |
mv resticui resticui-linux-amd64
run: ./scripts/build-all.sh

- uses: "marvinpinto/action-automatic-releases@latest"
with:
Expand All @@ -41,3 +37,6 @@ jobs:
files: |
LICENSE
resticui-linux-amd64
resticui-linux-arm64
resticui-darwin-amd64
resticui-darwin-arm64
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
resticui
resticui-*
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ ResticUI is accessible from a web browser. By default it binds to `0.0.0.0:9898`
* `RESTICUI_CONFIG_PATH` - the path to the config file. Defaults to `$HOME/.config/resticui/config.json` or if `$XDG_CONFIG_HOME` is set, `$XDG_CONFIG_HOME/resticui/config.json`.
* `RESTICUI_DATA_DIR` - the path to the data directory. Defaults to `$HOME/.local/share/resticui` or if `$XDG_DATA_HOME` is set, `$XDG_DATA_HOME/resticui`.
* `RESTICUI_RESTIC_BIN_PATH` - the path to the restic binary. Defaults managed version of restic which will be downloaded and installed in the data directory.
* `XDG_CACHE_HOME` -- the path to the cache directory. This is propagated to restic.

## Screenshots

Expand Down
238 changes: 179 additions & 59 deletions gen/go/v1/config.pb.go

Large diffs are not rendered by default.

82 changes: 51 additions & 31 deletions gen/go/v1/operations.pb.go

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

2 changes: 1 addition & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func loggingFunc(l *zap.Logger) logging.Logger {
case logging.LevelDebug:
logger.Debug(msg)
case logging.LevelInfo:
logger.Info(msg)
logger.Debug(msg)
case logging.LevelWarn:
logger.Warn(msg)
case logging.LevelError:
Expand Down
16 changes: 10 additions & 6 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,22 +194,26 @@ func (s *Server) GetOperationEvents(_ *emptypb.Empty, stream v1.ResticUI_GetOper
}

func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest) (*v1.OperationList, error) {
collector := indexutil.CollectAll()
idCollector := indexutil.CollectAll()

if req.LastN != 0 {
collector = indexutil.CollectLastN(int(req.LastN))
idCollector = indexutil.CollectLastN(int(req.LastN))
}

var err error
var ops []*v1.Operation
opCollector := func(op *v1.Operation) error {
ops = append(ops, op)
return nil
}
if req.RepoId != "" && req.PlanId != "" {
return nil, errors.New("cannot specify both repoId and planId")
} else if req.PlanId != "" {
ops, err = s.oplog.GetByPlan(req.PlanId, collector)
err = s.oplog.ForEachByPlan(req.PlanId, idCollector, opCollector)
} else if req.RepoId != "" {
ops, err = s.oplog.GetByRepo(req.RepoId, collector)
err = s.oplog.ForEachByRepo(req.RepoId, idCollector, opCollector)
} else if req.SnapshotId != "" {
ops, err = s.oplog.GetBySnapshotId(req.SnapshotId, collector)
err = s.oplog.ForEachBySnapshotId(req.SnapshotId, idCollector, opCollector)
} else if len(req.Ids) > 0 {
ops = make([]*v1.Operation, 0, len(req.Ids))
for i, id := range req.Ids {
Expand All @@ -220,7 +224,7 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest
ops = append(ops, op)
}
} else {
ops, err = s.oplog.GetAll()
err = s.oplog.ForAll(opCollector)
}
if err != nil {
return nil, fmt.Errorf("failed to get operations: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion internal/config/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func BindAddress() string {
}
return val
}
return ":9898"
return "127.0.0.1:9898"
}

func ResticBinPath() string {
Expand Down
7 changes: 3 additions & 4 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func ValidateConfig(c *v1.Config) error {
err = multierror.Append(err, fmt.Errorf("plan %s: %w", plan.GetId(), e))
}
}
}
}

return err
}
Expand Down Expand Up @@ -78,14 +78,13 @@ func validatePlan(plan *v1.Plan, repos map[string]*v1.Repo) error {
}

if plan.Repo == "" {
err = multierror.Append(err,fmt.Errorf("repo is required"))
err = multierror.Append(err, fmt.Errorf("repo is required"))
}

if _, ok := repos[plan.Repo]; !ok {
err = multierror.Append(err, fmt.Errorf("repo %q not found", plan.Repo))
}


if _, e := cronexpr.Parse(plan.Cron); e != nil {
err = multierror.Append(err, fmt.Errorf("invalid cron %q: %w", plan.Cron, e))
}
Expand Down Expand Up @@ -118,4 +117,4 @@ func validateRetention(policy *v1.RetentionPolicy) error {
}
}
return err
}
}
61 changes: 24 additions & 37 deletions internal/oplog/oplog.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,14 @@ const (
EventTypeOpUpdated = EventType(iota)
)

const (
schemaVersion int64 = 1
)
var ErrNotExist = errors.New("operation does not exist")

var (
SystemBucket = []byte("oplog.system") // system stores metadata
OpLogBucket = []byte("oplog.log") // oplog stores the operations themselves
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
indexBuckets = [][]byte{RepoIndexBucket, PlanIndexBucket, SnapshotIndexBucket}
)

// OpLog represents a log of operations performed.
Expand Down Expand Up @@ -194,7 +191,7 @@ func (o *OpLog) notifyHelper(eventType EventType, op *v1.Operation) {
func (o *OpLog) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, error) {
bytes := b.Get(serializationutil.Itob(id))
if bytes == nil {
return nil, fmt.Errorf("operation with ID %d does not exist", id)
return nil, ErrNotExist
}

var op v1.Operation
Expand Down Expand Up @@ -304,71 +301,61 @@ func (o *OpLog) Get(id int64) (*v1.Operation, error) {
return op, nil
}

func (o *OpLog) GetByRepo(repoId string, collector indexutil.Collector) ([]*v1.Operation, error) {
var err error
var ops []*v1.Operation
o.db.View(func(tx *bolt.Tx) error {
func (o *OpLog) ForEachByRepo(repoId string, collector indexutil.Collector, do func(op *v1.Operation) error) error {
return o.db.View(func(tx *bolt.Tx) error {
ids := collector(indexutil.IndexSearchByteValue(tx.Bucket(RepoIndexBucket), []byte(repoId)))
ops, err = o.getOpsByIds(tx, ids)
return nil
return o.forOpsByIds(tx, ids, do)
})
return ops, err
}

func (o *OpLog) GetByPlan(planId string, collector indexutil.Collector) ([]*v1.Operation, error) {
var err error
var ops []*v1.Operation
o.db.View(func(tx *bolt.Tx) error {
func (o *OpLog) ForEachByPlan(planId string, collector indexutil.Collector, do func(op *v1.Operation) error) error {
return o.db.View(func(tx *bolt.Tx) error {
ids := collector(indexutil.IndexSearchByteValue(tx.Bucket(PlanIndexBucket), []byte(planId)))
ops, err = o.getOpsByIds(tx, ids)
return nil
return o.forOpsByIds(tx, ids, do)
})
return ops, err
}

func (o *OpLog) GetBySnapshotId(snapshotId string, collector indexutil.Collector) ([]*v1.Operation, error) {
func (o *OpLog) ForEachBySnapshotId(snapshotId string, collector indexutil.Collector, do func(op *v1.Operation) error) error {
if err := restic.ValidateSnapshotId(snapshotId); err != nil {
return nil, err
return nil
}
var err error
var ops []*v1.Operation
o.db.View(func(tx *bolt.Tx) error {
return o.db.View(func(tx *bolt.Tx) error {
ids := collector(indexutil.IndexSearchByteValue(tx.Bucket(SnapshotIndexBucket), []byte(snapshotId)))
ops, err = o.getOpsByIds(tx, ids)
return nil
return o.forOpsByIds(tx, ids, do)
})
return ops, err
}

func (o *OpLog) getOpsByIds(tx *bolt.Tx, ids []int64) ([]*v1.Operation, error) {
func (o *OpLog) forOpsByIds(tx *bolt.Tx, ids []int64, do func(*v1.Operation) error) error {
b := tx.Bucket(OpLogBucket)
ops := make([]*v1.Operation, 0, len(ids))
for _, id := range ids {
op, err := o.getOperationHelper(b, id)
if err != nil {
return nil, err
return err
}
if err := do(op); err != nil {
return err
}
ops = append(ops, op)
}
return ops, nil
return nil
}

func (o *OpLog) GetAll() ([]*v1.Operation, error) {
var ops []*v1.Operation
func (o *OpLog) ForAll(do func(op *v1.Operation) error) error {
if err := o.db.View(func(tx *bolt.Tx) error {
c := tx.Bucket(OpLogBucket).Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
op := &v1.Operation{}
if err := proto.Unmarshal(v, op); err != nil {
return fmt.Errorf("error unmarshalling operation: %w", err)
}
ops = append(ops, op)
if err := do(op); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
return nil
}
return ops, nil
return nil
}

func (o *OpLog) Subscribe(callback *func(EventType, *v1.Operation)) {
Expand Down
Loading

0 comments on commit d22d9d1

Please sign in to comment.