From 2ad637e1d744fd2de01f0b9913ddf0ade50ee553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 24 Jul 2026 19:06:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/route/backup.go | 2 ++ internal/service/backup.go | 23 +++++++++++++++++++++++ web/src/views/backup/ListView.vue | 16 +++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/route/backup.go b/internal/route/backup.go index abf056dd..703ce5b8 100644 --- a/internal/route/backup.go +++ b/internal/route/backup.go @@ -20,6 +20,8 @@ func BackupRoutes(i do.Injector) (Endpoints, error) { {Method: http.MethodPost, Path: "/api/backup/{type}", Handler: backup.Create, Summary: "创建备份", Tags: []string{"备份"}, Request: request.BackupCreate{}}, {Method: http.MethodPost, Path: "/api/backup/{type}/upload", Handler: backup.Upload, Summary: "上传备份", Tags: []string{"备份"}}, + {Method: http.MethodGet, Path: "/api/backup/{type}/download", Handler: backup.Download, Summary: "下载备份", Tags: []string{"备份"}, + Request: request.BackupFile{}}, {Method: http.MethodDelete, Path: "/api/backup/{type}/delete", Handler: backup.Delete, Summary: "删除备份", Tags: []string{"备份"}, Request: request.BackupFile{}}, {Method: http.MethodPost, Path: "/api/backup/{type}/restore", Handler: backup.Restore, Summary: "恢复备份", Tags: []string{"备份"}, diff --git a/internal/service/backup.go b/internal/service/backup.go index 6437461c..6cc1eb04 100644 --- a/internal/service/backup.go +++ b/internal/service/backup.go @@ -152,6 +152,29 @@ func (s *BackupService) Delete(w http.ResponseWriter, r *http.Request) { Success(w, nil) } +func (s *BackupService) Download(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.BackupFile](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + file := filepath.Join(s.backupRepo.GetDefaultPath(biz.BackupType(req.Type)), req.File) + info, err := os.Stat(file) + if err != nil { + Error(w, http.StatusNotFound, "%v", err) + return + } + if info.IsDir() { + Error(w, http.StatusForbidden, s.t.Get("can't download a directory")) + return + } + + render := chix.NewRender(w, r) + defer render.Release() + render.Download(file, info.Name()) +} + func (s *BackupService) Restore(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.BackupRestore](r) if err != nil { diff --git a/web/src/views/backup/ListView.vue b/web/src/views/backup/ListView.vue index 49ae235d..2b090ded 100644 --- a/web/src/views/backup/ListView.vue +++ b/web/src/views/backup/ListView.vue @@ -65,10 +65,24 @@ const columns: any = [ { title: $gettext('Actions'), key: 'actions', - width: 260, + width: 320, hideInExcel: true, render(row: any) { return h(NFlex, { size: 'small', align: 'center' }, () => [ + h( + NButton, + { + size: 'small', + type: 'primary', + secondary: true, + onClick: () => { + window.open( + `/api/backup/${type.value}/download?file=${encodeURIComponent(row.name)}`, + ) + }, + }, + { default: () => $gettext('Download') }, + ), h( NButton, {