diff --git a/data/site-clone.db b/data/site-clone.db new file mode 100644 index 0000000..d4a7cbb Binary files /dev/null and b/data/site-clone.db differ diff --git a/go.mod b/go.mod index c4562bf..e744bfd 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 7329556..058e543 100644 --- a/go.sum +++ b/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= diff --git a/storage/download_store.go b/storage/download_store.go new file mode 100644 index 0000000..23886dd --- /dev/null +++ b/storage/download_store.go @@ -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 +} diff --git a/storage/site_store.go b/storage/site_store.go new file mode 100644 index 0000000..ba93a19 --- /dev/null +++ b/storage/site_store.go @@ -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) +} diff --git a/storage/store.go b/storage/store.go new file mode 100644 index 0000000..0f939bc --- /dev/null +++ b/storage/store.go @@ -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) + }) +}