新增本地持久化存储
This commit is contained in:
BIN
data/site-clone.db
Normal file
BIN
data/site-clone.db
Normal file
Binary file not shown.
1
go.mod
1
go.mod
@@ -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
4
go.sum
@@ -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
171
storage/download_store.go
Normal 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
107
storage/site_store.go
Normal 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
187
storage/store.go
Normal 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user