diff --git a/docs/rebuild-spec/02-features.md b/docs/rebuild-spec/02-features.md index e0e8de1..3f2ef39 100644 --- a/docs/rebuild-spec/02-features.md +++ b/docs/rebuild-spec/02-features.md @@ -190,7 +190,7 @@ - [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁 - [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区 - [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR(Logo/Favicon/OG 上传未迁) -- [ ] SQLite 一键备份与下载 +- [x] SQLite 一键备份与下载 — SSR `/admin/settings`(仅 sqlite;写入 data 目录后下载) --- diff --git a/docs/rebuild-spec/06-pages-ux.md b/docs/rebuild-spec/06-pages-ux.md index f56f506..f256ac2 100644 --- a/docs/rebuild-spec/06-pages-ux.md +++ b/docs/rebuild-spec/06-pages-ux.md @@ -195,7 +195,7 @@ | Users | 搜索;禁言;认证;设等级;调积分;授徽章 | | Badges | 定义自动/限定徽章 | | Media | 分类浏览;批量删 | -| Settings | Tab:论坛限制、侧栏组件、伪静态、邮件、OIDC+客户端、Gitea、存储、品牌、敏感词、备份 | +| Settings | Tab:论坛限制、侧栏组件、伪静态、邮件、OIDC+客户端、Gitea、存储、品牌、敏感词、备份 — SSR `/admin/settings` 已迁品牌/限流/侧栏/敏感词/邮件/SQLite 备份(OIDC/Gitea/存储/伪静态未迁) | --- diff --git a/routers/setup.go b/routers/setup.go index a0dda64..84448ec 100644 --- a/routers/setup.go +++ b/routers/setup.go @@ -109,6 +109,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) { Report: reportSvc, Captcha: captchaSvc, Notify: notifySvc, + Backup: backupSvc, }, authMW) r.GET("/media/thumb/*filepath", h.ServeImageThumb) diff --git a/routers/web/admin.go b/routers/web/admin.go index 0ef353d..d1c0e8c 100644 --- a/routers/web/admin.go +++ b/routers/web/admin.go @@ -3,6 +3,7 @@ package web import ( "fmt" "net/http" + "path/filepath" "strconv" "strings" "time" @@ -434,6 +435,7 @@ type adminSettingsData struct { MailReady bool TestTo string AsideWidgets []adminAsideWidgetRow + CanBackup bool } // AdminSettingsGet 设置页 @@ -468,6 +470,7 @@ func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) { Mail: mail, MailReady: d.Settings.MailReady(), AsideWidgets: asideRows, + CanBackup: models.DialectorName() == "sqlite", } data.Error = errMsg ctx.HTML(http.StatusOK, "admin/settings", data) @@ -645,6 +648,37 @@ func (d Deps) AdminSettingsAsideWidgetsPost(c *gin.Context) { ctx.Redirect("/admin/settings") } +// AdminSettingsBackupPost 一键导出 SQLite 备份并跳转下载 +func (d Deps) AdminSettingsBackupPost(c *gin.Context) { + ctx := d.ctx(c) + if !ctx.CheckCSRF() { + d.renderAdminSettings(ctx, "无效请求,请重试") + return + } + if d.Backup == nil { + d.renderAdminSettings(ctx, "备份服务未就绪") + return + } + path, err := d.Backup.ExportSQLite() + if err != nil { + d.renderAdminSettings(ctx, err.Error()) + return + } + name := filepath.Base(path) + ctx.Redirect("/admin/settings/backup/download/" + name) +} + +// AdminSettingsBackupDownload 下载 data 目录下的备份文件 +func (d Deps) AdminSettingsBackupDownload(c *gin.Context) { + name := c.Param("name") + if !strings.HasPrefix(name, "jiang13_backup_") || !strings.HasSuffix(name, ".db") { + c.String(http.StatusBadRequest, "无效的备份文件名") + return + } + path := filepath.Join(d.DataDir, name) + c.FileAttachment(path, name) +} + // moveAsideWidget 解析 move 值「up:id」或「down:id」并交换相邻项 func moveAsideWidget(widgets []services.AsideWidget, move string) []services.AsideWidget { parts := strings.SplitN(move, ":", 2) diff --git a/routers/web/chrome.go b/routers/web/chrome.go index 373597e..f37d86e 100644 --- a/routers/web/chrome.go +++ b/routers/web/chrome.go @@ -30,6 +30,7 @@ type Deps struct { Report *services.ReportService Captcha *services.CaptchaService Notify *services.NotifyService + Backup *services.BackupService } // SitePageLink 导航/页脚站点单页链接 diff --git a/routers/web/home.go b/routers/web/home.go index 2868271..49ccd72 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -76,6 +76,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) { admin.POST("/settings/mail", deps.AdminSettingsMailPost) admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost) admin.POST("/settings/aside-widgets", deps.AdminSettingsAsideWidgetsPost) + admin.POST("/settings/backup", deps.AdminSettingsBackupPost) + admin.GET("/settings/backup/download/:name", deps.AdminSettingsBackupDownload) admin.GET("/friend-links", deps.AdminFriendLinksGet) admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost) admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost) diff --git a/templates/admin/settings.tmpl b/templates/admin/settings.tmpl index e338625..e761bf2 100644 --- a/templates/admin/settings.tmpl +++ b/templates/admin/settings.tmpl @@ -83,6 +83,17 @@ + +

SQLite 备份

+

将当前数据库文件复制到 data 目录并下载。备份含用户哈希、私信等全库数据,请妥善保管。仅支持 SQLite;其它引擎请使用库方工具。

+ {{if .CanBackup}} +
+ + +
+ {{else}} +

当前不是 SQLite,一键文件备份不可用。

+ {{end}} {{template "base/footer" .}} {{end}}