新增本地持久化存储

This commit is contained in:
zyj
2025-11-22 17:48:04 +08:00
parent 62b5a3f9bc
commit ec3fc77403
6 changed files with 470 additions and 0 deletions

BIN
data/site-clone.db Normal file

Binary file not shown.

1
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/go-rod/stealth v0.4.9
github.com/mattn/go-sqlite3 v1.14.32
github.com/wailsapp/wails/v3 v3.0.0-alpha.27
go.etcd.io/bbolt v1.4.3
gopkg.in/yaml.v3 v3.0.1
)

4
go.sum
View File

@@ -137,6 +137,8 @@ github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -173,6 +175,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

171
storage/download_store.go Normal file
View File

@@ -0,0 +1,171 @@
package storage
import (
"fmt"
"time"
)
// DownloadRecord 下载记录
type DownloadRecord struct {
ID string `json:"id"` // 唯一标识
URL string `json:"url"` // 下载的 URL
SiteName string `json:"site_name"` // 站点名称
Status string `json:"status"` // 状态success, failed, processing
TotalFiles int `json:"total_files"` // 总文件数
Downloaded int `json:"downloaded"` // 已下载数
CSSCount int `json:"css_count"` // CSS 文件数
ScriptCount int `json:"script_count"` // JS 文件数
ImageCount int `json:"image_count"` // 图片文件数
VideoCount int `json:"video_count"` // 视频文件数
ErrorMsg string `json:"error_msg"` // 错误信息
StartTime time.Time `json:"start_time"` // 开始时间
EndTime time.Time `json:"end_time"` // 结束时间
Duration int64 `json:"duration"` // 耗时(秒)
}
// AddDownloadRecord 添加下载记录
func (s *Store) AddDownloadRecord(record DownloadRecord) error {
if record.ID == "" {
record.ID = fmt.Sprintf("%d", time.Now().UnixNano())
}
if record.StartTime.IsZero() {
record.StartTime = time.Now()
}
return s.Set(BucketDownload, record.ID, record)
}
// GetDownloadRecord 获取下载记录
func (s *Store) GetDownloadRecord(id string) (*DownloadRecord, error) {
var record DownloadRecord
if err := s.Get(BucketDownload, id, &record); err != nil {
return nil, err
}
return &record, nil
}
// UpdateDownloadRecord 更新下载记录
func (s *Store) UpdateDownloadRecord(record DownloadRecord) error {
if record.ID == "" {
return fmt.Errorf("记录 ID 不能为空")
}
return s.Set(BucketDownload, record.ID, record)
}
// DeleteDownloadRecord 删除下载记录
func (s *Store) DeleteDownloadRecord(id string) error {
return s.Delete(BucketDownload, id)
}
// GetAllDownloadRecords 获取所有下载记录
func (s *Store) GetAllDownloadRecords() ([]DownloadRecord, error) {
var records []DownloadRecord
if err := s.GetAll(BucketDownload, &records); err != nil {
return nil, err
}
return records, nil
}
// GetDownloadRecordsBySite 根据站点名称获取下载记录
func (s *Store) GetDownloadRecordsBySite(siteName string) ([]DownloadRecord, error) {
allRecords, err := s.GetAllDownloadRecords()
if err != nil {
return nil, err
}
var filtered []DownloadRecord
for _, record := range allRecords {
if record.SiteName == siteName {
filtered = append(filtered, record)
}
}
return filtered, nil
}
// GetDownloadRecordsByStatus 根据状态获取下载记录
func (s *Store) GetDownloadRecordsByStatus(status string) ([]DownloadRecord, error) {
allRecords, err := s.GetAllDownloadRecords()
if err != nil {
return nil, err
}
var filtered []DownloadRecord
for _, record := range allRecords {
if record.Status == status {
filtered = append(filtered, record)
}
}
return filtered, nil
}
// GetRecentDownloadRecords 获取最近的 N 条下载记录
func (s *Store) GetRecentDownloadRecords(limit int) ([]DownloadRecord, error) {
allRecords, err := s.GetAllDownloadRecords()
if err != nil {
return nil, err
}
// 按时间倒序排序
for i := 0; i < len(allRecords)-1; i++ {
for j := i + 1; j < len(allRecords); j++ {
if allRecords[i].StartTime.Before(allRecords[j].StartTime) {
allRecords[i], allRecords[j] = allRecords[j], allRecords[i]
}
}
}
if limit > 0 && limit < len(allRecords) {
return allRecords[:limit], nil
}
return allRecords, nil
}
// GetDownloadStats 获取下载统计信息
func (s *Store) GetDownloadStats() (map[string]interface{}, error) {
records, err := s.GetAllDownloadRecords()
if err != nil {
return nil, err
}
stats := map[string]interface{}{
"total": len(records),
"success": 0,
"failed": 0,
"processing": 0,
"total_files": 0,
}
for _, record := range records {
switch record.Status {
case "success":
stats["success"] = stats["success"].(int) + 1
case "failed":
stats["failed"] = stats["failed"].(int) + 1
case "processing":
stats["processing"] = stats["processing"].(int) + 1
}
stats["total_files"] = stats["total_files"].(int) + record.TotalFiles
}
return stats, nil
}
// ClearOldDownloadRecords 清理指定天数之前的下载记录
func (s *Store) ClearOldDownloadRecords(days int) (int, error) {
records, err := s.GetAllDownloadRecords()
if err != nil {
return 0, err
}
cutoffTime := time.Now().AddDate(0, 0, -days)
deleted := 0
for _, record := range records {
if record.StartTime.Before(cutoffTime) {
if err := s.DeleteDownloadRecord(record.ID); err == nil {
deleted++
}
}
}
return deleted, nil
}

107
storage/site_store.go Normal file
View File

@@ -0,0 +1,107 @@
package storage
import (
"fmt"
"go-site-clone/types"
"time"
)
// SiteRecord 站点数据记录(带时间戳)
type SiteRecord struct {
types.NginxSiteConfig
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AddSite 添加站点配置
func (s *Store) AddSite(site types.NginxSiteConfig) error {
record := SiteRecord{
NginxSiteConfig: site,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return s.Set(BucketSites, site.Name, record)
}
// GetSite 获取站点配置
func (s *Store) GetSite(siteName string) (*types.NginxSiteConfig, error) {
var record SiteRecord
if err := s.Get(BucketSites, siteName, &record); err != nil {
return nil, err
}
return &record.NginxSiteConfig, nil
}
// UpdateSite 更新站点配置
func (s *Store) UpdateSite(site types.NginxSiteConfig) error {
var record SiteRecord
// 尝试获取现有记录以保留创建时间
if err := s.Get(BucketSites, site.Name, &record); err == nil {
record.NginxSiteConfig = site
record.UpdatedAt = time.Now()
} else {
// 如果不存在,创建新记录
record = SiteRecord{
NginxSiteConfig: site,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
return s.Set(BucketSites, site.Name, record)
}
// DeleteSite 删除站点配置
func (s *Store) DeleteSite(siteName string) error {
return s.Delete(BucketSites, siteName)
}
// GetAllSites 获取所有站点配置
func (s *Store) GetAllSites() ([]types.NginxSiteConfig, error) {
var records []SiteRecord
if err := s.GetAll(BucketSites, &records); err != nil {
return nil, err
}
sites := make([]types.NginxSiteConfig, len(records))
for i, record := range records {
sites[i] = record.NginxSiteConfig
}
return sites, nil
}
// SiteExists 检查站点是否存在
func (s *Store) SiteExists(siteName string) bool {
return s.Exists(BucketSites, siteName)
}
// GetSitesByStatus 根据启用状态获取站点
func (s *Store) GetSitesByStatus(enabled bool) ([]types.NginxSiteConfig, error) {
allSites, err := s.GetAllSites()
if err != nil {
return nil, err
}
var filtered []types.NginxSiteConfig
for _, site := range allSites {
if site.Enabled == enabled {
filtered = append(filtered, site)
}
}
return filtered, nil
} // UpdateSiteStatus 更新站点启用状态
func (s *Store) UpdateSiteStatus(siteName string, enabled bool) error {
site, err := s.GetSite(siteName)
if err != nil {
return fmt.Errorf("站点不存在: %v", err)
}
site.Enabled = enabled
return s.UpdateSite(*site)
}
// GetSitesCount 获取站点总数
func (s *Store) GetSitesCount() int {
return s.Count(BucketSites)
}

187
storage/store.go Normal file
View File

@@ -0,0 +1,187 @@
package storage
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"time"
bolt "go.etcd.io/bbolt"
)
// Bucket 名称常量
var (
BucketSites = []byte("sites")
BucketDownload = []byte("download_records")
BucketLogs = []byte("access_logs")
BucketSettings = []byte("settings")
)
// Store BBolt 存储服务
type Store struct {
db *bolt.DB
}
// NewStore 创建新的存储服务
func NewStore(dbPath string) (*Store, error) {
// 确保数据库目录存在
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("创建数据库目录失败: %v", err)
}
// 打开数据库
db, err := bolt.Open(dbPath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("打开数据库失败: %v", err)
}
// 创建必要的 buckets
err = db.Update(func(tx *bolt.Tx) error {
buckets := [][]byte{BucketSites, BucketDownload, BucketLogs, BucketSettings}
for _, bucket := range buckets {
if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
return fmt.Errorf("创建 bucket %s 失败: %v", string(bucket), err)
}
}
return nil
})
if err != nil {
db.Close()
return nil, err
}
log.Printf("数据库初始化成功: %s", dbPath)
return &Store{db: db}, nil
}
// Close 关闭数据库连接
func (s *Store) Close() error {
if s.db != nil {
return s.db.Close()
}
return nil
}
// Set 存储键值对到指定 bucket
func (s *Store) Set(bucket []byte, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("序列化数据失败: %v", err)
}
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return fmt.Errorf("bucket %s 不存在", string(bucket))
}
return b.Put([]byte(key), data)
})
}
// Get 从指定 bucket 获取值
func (s *Store) Get(bucket []byte, key string, dest interface{}) error {
return s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return fmt.Errorf("bucket %s 不存在", string(bucket))
}
data := b.Get([]byte(key))
if data == nil {
return fmt.Errorf("键 %s 不存在", key)
}
return json.Unmarshal(data, dest)
})
}
// Delete 删除指定键
func (s *Store) Delete(bucket []byte, key string) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return fmt.Errorf("bucket %s 不存在", string(bucket))
}
return b.Delete([]byte(key))
})
}
// GetAll 获取指定 bucket 的所有数据
func (s *Store) GetAll(bucket []byte, destSlice interface{}) error {
var items []json.RawMessage
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return fmt.Errorf("bucket %s 不存在", string(bucket))
}
return b.ForEach(func(k, v []byte) error {
items = append(items, json.RawMessage(v))
return nil
})
})
if err != nil {
return err
}
// 将 items 转换为目标切片类型
data, err := json.Marshal(items)
if err != nil {
return err
}
return json.Unmarshal(data, destSlice)
}
// Exists 检查键是否存在
func (s *Store) Exists(bucket []byte, key string) bool {
exists := false
s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return nil
}
exists = b.Get([]byte(key)) != nil
return nil
})
return exists
}
// Count 统计指定 bucket 的键数量
func (s *Store) Count(bucket []byte) int {
count := 0
s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return nil
}
b.ForEach(func(k, v []byte) error {
count++
return nil
})
return nil
})
return count
}
// Backup 备份数据库到指定路径
func (s *Store) Backup(backupPath string) error {
// 确保备份目录存在
dir := filepath.Dir(backupPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建备份目录失败: %v", err)
}
return s.db.View(func(tx *bolt.Tx) error {
return tx.CopyFile(backupPath, 0600)
})
}