mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 15:06:36 +08:00
数据导出
This commit is contained in:
@@ -19,12 +19,18 @@ limitations under the License.
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -200,3 +206,159 @@ func GetSystemStatus(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(res))
|
||||
}
|
||||
|
||||
// DatabaseInfoResponse 数据库信息响应结构体
|
||||
type DatabaseInfoResponse struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// getSQLiteInfo 返回 SQLite 数据库信息
|
||||
func getSQLiteInfo(ctx context.Context) DatabaseInfoResponse {
|
||||
info := DatabaseInfoResponse{
|
||||
Type: "sqlite",
|
||||
Name: config.Config.Database.SQLitePath,
|
||||
Version: "SQLite",
|
||||
}
|
||||
if info.Name == "" {
|
||||
info.Name = "./data/wavelet.db"
|
||||
}
|
||||
gormDB := db.DB(ctx)
|
||||
if gormDB == nil {
|
||||
return info
|
||||
}
|
||||
var ver string
|
||||
if err := gormDB.Raw("SELECT sqlite_version()").Scan(&ver).Error; err == nil && ver != "" {
|
||||
info.Version = "SQLite " + ver
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// getPostgresInfo 返回 PostgreSQL 数据库信息
|
||||
func getPostgresInfo(ctx context.Context) DatabaseInfoResponse {
|
||||
info := DatabaseInfoResponse{
|
||||
Type: "postgres",
|
||||
Name: config.Config.Database.Database,
|
||||
Version: "PostgreSQL",
|
||||
}
|
||||
gormDB := db.DB(ctx)
|
||||
if gormDB == nil {
|
||||
return info
|
||||
}
|
||||
var ver string
|
||||
if err := gormDB.Raw("SELECT version()").Scan(&ver).Error; err == nil && ver != "" {
|
||||
info.Version = ver
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
// GetDatabaseInfo 获取当前数据库类型及版本信息
|
||||
// @Summary 获取数据库信息
|
||||
// @Description 返回当前使用的数据库类型(sqlite/postgres)、名称/路径及版本字符串,需要管理员权限
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {object} util.ResponseAny{data=status.DatabaseInfoResponse} "获取成功"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Router /api/v1/admin/db-info [get]
|
||||
func GetDatabaseInfo(c *gin.Context) {
|
||||
var info DatabaseInfoResponse
|
||||
if !config.Config.Database.Enabled {
|
||||
info = getSQLiteInfo(c.Request.Context())
|
||||
} else {
|
||||
info = getPostgresInfo(c.Request.Context())
|
||||
}
|
||||
c.JSON(http.StatusOK, util.OK(info))
|
||||
}
|
||||
|
||||
// ExportDatabase 导出数据库
|
||||
// @Summary 导出数据库
|
||||
// @Description SQLite 时直接下载 .db 文件;PostgreSQL 时执行 pg_dump 并流式下载 .sql 文件,需要管理员权限
|
||||
// @Tags admin
|
||||
// @Produce application/octet-stream
|
||||
// @Security SessionCookie
|
||||
// @Success 200 {file} binary "数据库文件"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "导出失败"
|
||||
// @Router /api/v1/admin/db-export [get]
|
||||
func ExportDatabase(c *gin.Context) {
|
||||
if !config.Config.Database.Enabled {
|
||||
exportSQLite(c)
|
||||
} else {
|
||||
exportPostgres(c)
|
||||
}
|
||||
}
|
||||
|
||||
// exportSQLite 以 HTTP 附件方式下载 SQLite .db 文件
|
||||
func exportSQLite(c *gin.Context) {
|
||||
path := config.Config.Database.SQLitePath
|
||||
if path == "" {
|
||||
path = "./data/wavelet.db"
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("无法打开数据库文件: "+err.Error()))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := f.Close(); closeErr != nil {
|
||||
_ = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("无法读取数据库文件信息: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Disposition", `attachment; filename="wavelet.db"`)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
||||
c.Status(http.StatusOK)
|
||||
http.ServeContent(c.Writer, c.Request, "wavelet.db", fi.ModTime(), f)
|
||||
}
|
||||
|
||||
// exportPostgres 执行 pg_dump 并将输出流式传输给客户端
|
||||
func exportPostgres(c *gin.Context) {
|
||||
dbCfg := config.Config.Database
|
||||
|
||||
// 检查 pg_dump 是否可用
|
||||
pgDumpPath, err := exec.LookPath("pg_dump")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err("pg_dump 不可用,请确保服务器已安装 PostgreSQL 客户端工具"))
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-password",
|
||||
"-h", dbCfg.Host,
|
||||
"-p", fmt.Sprintf("%d", dbCfg.Port),
|
||||
"-U", dbCfg.Username,
|
||||
dbCfg.Database,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(c.Request.Context(), pgDumpPath, args...)
|
||||
if dbCfg.Password != "" {
|
||||
cmd.Env = append(os.Environ(), "PGPASSWORD="+dbCfg.Password)
|
||||
} else {
|
||||
cmd.Env = os.Environ()
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("wavelet_%s.sql", time.Now().Format("20060102_150405"))
|
||||
c.Header("Content-Disposition", `attachment; filename="`+fileName+`"`)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Status(http.StatusOK)
|
||||
|
||||
cmd.Stdout = c.Writer
|
||||
cmd.Stderr = nil // 忽略 stderr 以避免污染输出流
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
// 响应头已发出,无法再写 JSON 错误,记录到服务器日志
|
||||
log.Printf("[db-export] pg_dump failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user