feat: Admin 设置页 SQLite 一键备份与下载
复用 BackupService,表单导出后安全下载备份文件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -190,7 +190,7 @@
|
|||||||
- [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁
|
- [x] 基础限流(post/comment/register/login/window)— SSR;完整 Limits 字数等未迁
|
||||||
- [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区
|
- [x] SMTP 配置与测试信 — SSR `/admin/settings` 邮件区
|
||||||
- [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR(Logo/Favicon/OG 上传未迁)
|
- [x] 站点品牌文案:名称、标语、简介、keywords、Logo 字标、ICP — SSR(Logo/Favicon/OG 上传未迁)
|
||||||
- [ ] SQLite 一键备份与下载
|
- [x] SQLite 一键备份与下载 — SSR `/admin/settings`(仅 sqlite;写入 data 目录后下载)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@
|
|||||||
| Users | 搜索;禁言;认证;设等级;调积分;授徽章 |
|
| Users | 搜索;禁言;认证;设等级;调积分;授徽章 |
|
||||||
| Badges | 定义自动/限定徽章 |
|
| Badges | 定义自动/限定徽章 |
|
||||||
| Media | 分类浏览;批量删 |
|
| Media | 分类浏览;批量删 |
|
||||||
| Settings | Tab:论坛限制、侧栏组件、伪静态、邮件、OIDC+客户端、Gitea、存储、品牌、敏感词、备份 |
|
| Settings | Tab:论坛限制、侧栏组件、伪静态、邮件、OIDC+客户端、Gitea、存储、品牌、敏感词、备份 — SSR `/admin/settings` 已迁品牌/限流/侧栏/敏感词/邮件/SQLite 备份(OIDC/Gitea/存储/伪静态未迁) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
Report: reportSvc,
|
Report: reportSvc,
|
||||||
Captcha: captchaSvc,
|
Captcha: captchaSvc,
|
||||||
Notify: notifySvc,
|
Notify: notifySvc,
|
||||||
|
Backup: backupSvc,
|
||||||
}, authMW)
|
}, authMW)
|
||||||
|
|
||||||
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package web
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -434,6 +435,7 @@ type adminSettingsData struct {
|
|||||||
MailReady bool
|
MailReady bool
|
||||||
TestTo string
|
TestTo string
|
||||||
AsideWidgets []adminAsideWidgetRow
|
AsideWidgets []adminAsideWidgetRow
|
||||||
|
CanBackup bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminSettingsGet 设置页
|
// AdminSettingsGet 设置页
|
||||||
@@ -468,6 +470,7 @@ func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) {
|
|||||||
Mail: mail,
|
Mail: mail,
|
||||||
MailReady: d.Settings.MailReady(),
|
MailReady: d.Settings.MailReady(),
|
||||||
AsideWidgets: asideRows,
|
AsideWidgets: asideRows,
|
||||||
|
CanBackup: models.DialectorName() == "sqlite",
|
||||||
}
|
}
|
||||||
data.Error = errMsg
|
data.Error = errMsg
|
||||||
ctx.HTML(http.StatusOK, "admin/settings", data)
|
ctx.HTML(http.StatusOK, "admin/settings", data)
|
||||||
@@ -645,6 +648,37 @@ func (d Deps) AdminSettingsAsideWidgetsPost(c *gin.Context) {
|
|||||||
ctx.Redirect("/admin/settings")
|
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」并交换相邻项
|
// moveAsideWidget 解析 move 值「up:id」或「down:id」并交换相邻项
|
||||||
func moveAsideWidget(widgets []services.AsideWidget, move string) []services.AsideWidget {
|
func moveAsideWidget(widgets []services.AsideWidget, move string) []services.AsideWidget {
|
||||||
parts := strings.SplitN(move, ":", 2)
|
parts := strings.SplitN(move, ":", 2)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type Deps struct {
|
|||||||
Report *services.ReportService
|
Report *services.ReportService
|
||||||
Captcha *services.CaptchaService
|
Captcha *services.CaptchaService
|
||||||
Notify *services.NotifyService
|
Notify *services.NotifyService
|
||||||
|
Backup *services.BackupService
|
||||||
}
|
}
|
||||||
|
|
||||||
// SitePageLink 导航/页脚站点单页链接
|
// SitePageLink 导航/页脚站点单页链接
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
|
|||||||
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
|
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
|
||||||
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
|
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
|
||||||
admin.POST("/settings/aside-widgets", deps.AdminSettingsAsideWidgetsPost)
|
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.GET("/friend-links", deps.AdminFriendLinksGet)
|
||||||
admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost)
|
admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost)
|
||||||
admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost)
|
admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost)
|
||||||
|
|||||||
@@ -83,6 +83,17 @@
|
|||||||
<label>测试收件邮箱 <input name="test_to" type="email" required placeholder="you@example.com"/></label>
|
<label>测试收件邮箱 <input name="test_to" type="email" required placeholder="you@example.com"/></label>
|
||||||
<button type="submit" class="j13-btn-secondary">发送测试信</button>
|
<button type="submit" class="j13-btn-secondary">发送测试信</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h2>SQLite 备份</h2>
|
||||||
|
<p class="j13-muted">将当前数据库文件复制到 data 目录并下载。备份含用户哈希、私信等全库数据,请妥善保管。仅支持 SQLite;其它引擎请使用库方工具。</p>
|
||||||
|
{{if .CanBackup}}
|
||||||
|
<form method="post" action="/admin/settings/backup" class="j13-form j13-admin-form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{.CSRF}}"/>
|
||||||
|
<button type="submit">立即备份并下载</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<p class="j13-muted">当前不是 SQLite,一键文件备份不可用。</p>
|
||||||
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user