From a75a5c2297df4eb89235a54efd38d9539b7c15e5 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 5 May 2024 04:11:23 -0700 Subject: [PATCH] feat: add download link to create a zip archive of restored files --- backrest.go | 1 + internal/api/downloadhandler.go | 84 ++++++++++++++++++++++ internal/orchestrator/tasks/taskrestore.go | 7 +- webui/src/components/OperationRow.tsx | 4 ++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 internal/api/downloadhandler.go diff --git a/backrest.go b/backrest.go index 773b5e8..2bed367 100644 --- a/backrest.go +++ b/backrest.go @@ -108,6 +108,7 @@ func main() { backrestHandlerPath, backrestHandler := v1connect.NewBackrestHandler(apiBackrestHandler) mux.Handle(backrestHandlerPath, auth.RequireAuthentication(backrestHandler, authenticator)) mux.Handle("/", webui.Handler()) + mux.Handle("/download/", http.StripPrefix("/download", api.NewDownloadHandler(oplog))) // Serve the HTTP gateway server := &http.Server{ diff --git a/internal/api/downloadhandler.go b/internal/api/downloadhandler.go new file mode 100644 index 0000000..b28860e --- /dev/null +++ b/internal/api/downloadhandler.go @@ -0,0 +1,84 @@ +package api + +import ( + "archive/zip" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/oplog" + "go.uber.org/zap" +) + +func NewDownloadHandler(oplog *oplog.OpLog) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path[1:] + sep := strings.Index(p, "/") + if sep == -1 { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + restoreID := p[:sep] + filePath := p[sep+1:] + opID, err := strconv.ParseInt(restoreID, 16, 64) + if err != nil { + http.Error(w, "invalid restore ID: "+err.Error(), http.StatusBadRequest) + return + } + op, err := oplog.Get(int64(opID)) + if err != nil { + http.Error(w, "restore not found", http.StatusNotFound) + return + } + restoreOp, ok := op.Op.(*v1.Operation_OperationRestore) + if !ok { + http.Error(w, "restore not found", http.StatusNotFound) + return + } + targetPath := restoreOp.OperationRestore.GetTarget() + if targetPath == "" { + http.Error(w, "restore target not found", http.StatusNotFound) + return + } + fullPath := filepath.Join(targetPath, filePath) + + w.Header().Set("Content-Disposition", "attachment; filename=archive.zip") + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Transfer-Encoding", "binary") + + z := zip.NewWriter(w) + zap.L().Info("creating zip archive", zap.String("path", fullPath)) + if err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + file, err := os.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + zap.L().Warn("error opening file", zap.String("path", path), zap.Error(err)) + return nil + } + defer file.Close() + f, err := z.Create(path[len(fullPath)+1:]) + if err != nil { + return fmt.Errorf("add file to zip archive: %w", err) + } + io.Copy(f, file) + return nil + }); err != nil { + zap.S().Errorf("error creating zip archive: %v", err) + http.Error(w, "error creating zip archive", http.StatusInternalServerError) + } + if err := z.Close(); err != nil { + zap.S().Errorf("error closing zip archive: %v", err) + http.Error(w, "error closing zip archive", http.StatusInternalServerError) + } + }) +} diff --git a/internal/orchestrator/tasks/taskrestore.go b/internal/orchestrator/tasks/taskrestore.go index 7021cd0..9be2c58 100644 --- a/internal/orchestrator/tasks/taskrestore.go +++ b/internal/orchestrator/tasks/taskrestore.go @@ -24,7 +24,12 @@ func NewOneoffRestoreTask(repoID, planID string, flowID int64, at time.Time, sna RunAt: at, ProtoOp: &v1.Operation{ SnapshotId: snapshotID, - Op: &v1.Operation_OperationRestore{}, + Op: &v1.Operation_OperationRestore{ + OperationRestore: &v1.OperationRestore{ + Path: path, + Target: target, + }, + }, }, }, Do: func(ctx context.Context, st ScheduledTask, taskRunner TaskRunner) error { diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index b845c53..9e42996 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -194,6 +194,10 @@ export const OperationRow = ({ {details.percentage !== undefined ? ( ) : null} + {operation.status == OperationStatus.STATUS_SUCCESS ? (<> +
+ + ) : null} ); } else if (operation.op.case === "operationRunHook") {