Skip to content

Commit

Permalink
feat: show snapshots in sidenav
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Nov 18, 2023
1 parent 0cdfd11 commit 1a9a5b6
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 83 deletions.
19 changes: 10 additions & 9 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import (
type Server struct {
*v1.UnimplementedResticUIServer
orchestrator *orchestrator.Orchestrator
oplog *oplog.OpLog
oplog *oplog.OpLog
}

var _ v1.ResticUIServer = &Server{}

func NewServer(orchestrator *orchestrator.Orchestrator, oplog *oplog.OpLog) *Server {
s := &Server{
orchestrator: orchestrator,
oplog: oplog,
oplog: oplog,
}

return s
Expand All @@ -52,7 +52,7 @@ func (s *Server) SetConfig(ctx context.Context, c *v1.Config) (*v1.Config, error
return nil, errors.New("config modno mismatch, reload and try again")
}
c.Modno += 1

if err := config.Default.Update(c); err != nil {
return nil, fmt.Errorf("failed to update config: %w", err)
}
Expand Down Expand Up @@ -80,7 +80,7 @@ func (s *Server) AddRepo(ctx context.Context, repo *v1.Repo) (*v1.Config, error)
}

r := restic.NewRepo(repo)
// use background context such that the init op can try to complete even if the connection is closed.
// use background context such that the init op can try to complete even if the connection is closed.
if err := r.Init(context.Background()); err != nil {
return nil, fmt.Errorf("failed to init repo: %w", err)
}
Expand Down Expand Up @@ -142,7 +142,7 @@ func (s *Server) ListSnapshotFiles(ctx context.Context, query *v1.ListSnapshotFi
}

return &v1.ListSnapshotFilesResponse{
Path: query.Path,
Path: query.Path,
Entries: entries,
}, nil
}
Expand All @@ -164,7 +164,7 @@ func (s *Server) GetOperationEvents(_ *emptypb.Empty, stream v1.ResticUI_GetOper
}

event := &v1.OperationEvent{
Type: eventTypeMapped,
Type: eventTypeMapped,
Operation: op,
}

Expand All @@ -191,14 +191,16 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest
filter = oplog.FilterLastN(req.LastN)
}

var err error
var err error
var ops []*v1.Operation
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, filter)
} else if req.RepoId != "" {
ops, err = s.oplog.GetByRepo(req.RepoId, filter)
} else {
ops, err = s.oplog.GetAll(filter)
}
if err != nil {
return nil, fmt.Errorf("failed to get operations: %w", err)
Expand All @@ -209,7 +211,7 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest
}, nil
}

func (s* Server) Backup(ctx context.Context, req *types.StringValue) (*emptypb.Empty, error) {
func (s *Server) Backup(ctx context.Context, req *types.StringValue) (*emptypb.Empty, error) {
plan, err := s.orchestrator.GetPlan(req.Value)
if err != nil {
return nil, fmt.Errorf("failed to get plan %q: %w", req.Value, err)
Expand All @@ -233,4 +235,3 @@ func (s *Server) PathAutocomplete(ctx context.Context, path *types.StringValue)

return &types.StringList{Values: paths}, nil
}

18 changes: 18 additions & 0 deletions internal/database/oplog/oplog.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,24 @@ func (o *OpLog) GetByPlan(planId string, filter Filter) ([]*v1.Operation, error)
return ops, nil
}

func (o *OpLog) GetAll(filter Filter) ([]*v1.Operation, error) {
var ops []*v1.Operation
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)
}
return nil
}); err != nil {
return nil, err
}
return ops, nil
}

func (o *OpLog) Subscribe(callback *func(EventType, *v1.Operation)) {
o.subscribersMu.Lock()
defer o.subscribersMu.Unlock()
Expand Down
2 changes: 1 addition & 1 deletion webui/.proxyrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"/api": {
"target": "http://localhost:9090",
"target": "http://localhost:9898",
"secure": false
}
}
78 changes: 19 additions & 59 deletions webui/src/components/OperationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import {
import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb";
import { EOperation } from "../state/oplog";
import { SnapshotBrowser } from "./SnapshotBrowser";
import {
formatBytes,
formatDuration,
formatTime,
normalizeSnapshotId,
} from "../lib/formatting";

export const OperationList = ({
operations,
Expand Down Expand Up @@ -47,24 +53,10 @@ export const OperationList = ({
return Object.values(groups);
};

// snapshotKey is a heuristic that tries to find a snapshot ID to group the operation by,
// if one can not be found the operation ID is the key.
const snapshotKey = (op: EOperation) => {
if (
op.operationBackup &&
op.operationBackup.lastStatus &&
op.operationBackup.lastStatus.summary
) {
return normalizeSnapshotId(
op.operationBackup.lastStatus.summary.snapshotId!
);
} else if (op.operationIndexSnapshot) {
return normalizeSnapshotId(op.operationIndexSnapshot.snapshot!.id!);
}
return op.id!;
};

const groupedItems = groupBy(operations, snapshotKey);
// groups items by snapshotID if one can be identified, otherwise by operation ID.
const groupedItems = groupBy(operations, (op: EOperation) => {
return getSnapshotId(op) || op.id!;
});
groupedItems.sort((a, b) => {
return b[0].parsedTime - a[0].parsedTime;
});
Expand Down Expand Up @@ -334,46 +326,14 @@ const BackupOperationStatus = ({
}
};

const formatBytes = (bytes?: number | string) => {
if (!bytes) {
return 0;
}
if (typeof bytes === "string") {
bytes = parseInt(bytes);
}

const units = ["B", "KB", "MB", "GB", "TB", "PB"];
let unit = 0;
while (bytes > 1024) {
bytes /= 1024;
unit++;
}
return `${Math.round(bytes * 100) / 100} ${units[unit]}`;
};

const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
const formatTime = (time: number | string) => {
if (typeof time === "string") {
time = parseInt(time);
}
const d = new Date();
d.setTime(time - timezoneOffsetMs);
const isoStr = d.toISOString();
return `${isoStr.substring(0, 10)} ${d.getUTCHours()}h${d.getUTCMinutes()}m`;
};

const formatDuration = (ms: number) => {
const seconds = Math.floor(ms / 100) / 10;
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours === 0 && minutes === 0) {
return `${seconds % 60}s`;
} else if (hours === 0) {
return `${minutes}m${seconds % 60}s`;
const getSnapshotId = (op: EOperation): string | null => {
if (op.operationBackup) {
const ob = op.operationBackup;
if (ob.lastStatus && ob.lastStatus.summary) {
return normalizeSnapshotId(ob.lastStatus.summary.snapshotId!);
}
} else if (op.operationIndexSnapshot) {
return normalizeSnapshotId(op.operationIndexSnapshot.snapshot!.id!);
}
return `${hours}h${minutes % 60}m${seconds % 60}s`;
};

const normalizeSnapshotId = (id: string) => {
return id.substring(0, 8);
return null;
};
43 changes: 43 additions & 0 deletions webui/src/lib/formatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const formatBytes = (bytes?: number | string) => {
if (!bytes) {
return 0;
}
if (typeof bytes === "string") {
bytes = parseInt(bytes);
}

const units = ["B", "KB", "MB", "GB", "TB", "PB"];
let unit = 0;
while (bytes > 1024) {
bytes /= 1024;
unit++;
}
return `${Math.round(bytes * 100) / 100} ${units[unit]}`;
};

const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
export const formatTime = (time: number | string) => {
if (typeof time === "string") {
time = parseInt(time);
}
const d = new Date();
d.setTime(time - timezoneOffsetMs);
const isoStr = d.toISOString();
return `${isoStr.substring(0, 10)} ${d.getUTCHours()}h${d.getUTCMinutes()}m`;
};

export const formatDuration = (ms: number) => {
const seconds = Math.floor(ms / 100) / 10;
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours === 0 && minutes === 0) {
return `${seconds % 60}s`;
} else if (hours === 0) {
return `${minutes}m${seconds % 60}s`;
}
return `${hours}h${minutes % 60}m${seconds % 60}s`;
};

export const normalizeSnapshotId = (id: string) => {
return id.substring(0, 8);
};
8 changes: 4 additions & 4 deletions webui/src/state/oplog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const getOperations = async ({
planId,
repoId,
lastN,
}: GetOperationsRequest): Promise<Operation[]> => {
}: GetOperationsRequest): Promise<EOperation[]> => {
const opList = await ResticUI.GetOperations(
{
planId,
Expand All @@ -55,7 +55,7 @@ export const getOperations = async ({
pathPrefix: "/api",
}
);
return opList.operations || [];
return (opList.operations || []).map(toEop);
};

export const subscribeToOperations = (
Expand Down Expand Up @@ -85,7 +85,7 @@ export const buildOperationListListener = (
let operations: EOperation[] = [];

(async () => {
let opsFromServer = (await getOperations(req)).map(toEop);
let opsFromServer = await getOperations(req);
operations = opsFromServer.filter(
(o) => !operations.find((op) => op.parsedId === o.parsedId)
);
Expand Down Expand Up @@ -123,7 +123,7 @@ export const buildOperationListListener = (
};
};

const toEop = (op: Operation): EOperation => {
export const toEop = (op: Operation): EOperation => {
const time =
op.operationIndexSnapshot?.snapshot?.unixTimeMs || op.unixTimeStartMs;

Expand Down
Loading

0 comments on commit 1a9a5b6

Please sign in to comment.