package config import ( "encoding/xml" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "sort" "strings" "time" ) type WebDAVClient struct { URL string Username string Password string } // BackupInfo 备份文件信息 type BackupInfo struct { Name string `json:"name"` Size int64 `json:"size"` ModTime string `json:"mod_time"` } func NewWebDAVClient(url, user, pass string) *WebDAVClient { if !strings.HasSuffix(url, "/") { url += "/" } return &WebDAVClient{URL: url, Username: user, Password: pass} } func (w *WebDAVClient) Upload(localPath, remoteName string) error { file, err := os.Open(localPath) if err != nil { return err } defer file.Close() req, err := http.NewRequest("PUT", w.URL+remoteName, file) if err != nil { return err } req.SetBasicAuth(w.Username, w.Password) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("upload failed: %s", resp.Status) } return nil } // Download 从 WebDAV 下载文件 func (w *WebDAVClient) Download(remoteName, localPath string) error { req, err := http.NewRequest("GET", w.URL+remoteName, nil) if err != nil { return err } req.SetBasicAuth(w.Username, w.Password) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("download failed: %s", resp.Status) } out, err := os.Create(localPath) if err != nil { return err } defer out.Close() _, err = io.Copy(out, resp.Body) return err } // Delete 删除 WebDAV 上的文件 func (w *WebDAVClient) Delete(remoteName string) error { req, err := http.NewRequest("DELETE", w.URL+remoteName, nil) if err != nil { return err } req.SetBasicAuth(w.Username, w.Password) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("delete failed: %s", resp.Status) } return nil } // WebDAV PROPFIND XML 结构 type multiStatus struct { Responses []davResponse `xml:"response"` } type davResponse struct { Href string `xml:"href"` PropStat []propStat `xml:"propstat"` } type propStat struct { Prop davProp `xml:"prop"` Status string `xml:"status"` } type davProp struct { ContentLength int64 `xml:"getcontentlength"` LastModified string `xml:"getlastmodified"` } // List 列出云端备份,返回详细信息 func (w *WebDAVClient) List() ([]BackupInfo, error) { req, err := http.NewRequest("PROPFIND", w.URL, nil) if err != nil { return nil, err } req.Header.Set("Depth", "1") req.SetBasicAuth(w.Username, w.Password) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) // 尝试 XML 解析获取详细信息 var ms multiStatus re := regexp.MustCompile(`tonav_backup_[0-9_]+\.db`) if xml.Unmarshal(body, &ms) == nil && len(ms.Responses) > 0 { var list []BackupInfo seen := make(map[string]bool) for _, r := range ms.Responses { matches := re.FindAllString(r.Href, -1) for _, name := range matches { if seen[name] { continue } seen[name] = true info := BackupInfo{Name: name} if len(r.PropStat) > 0 { info.Size = r.PropStat[0].Prop.ContentLength info.ModTime = r.PropStat[0].Prop.LastModified } // 从文件名解析时间 if info.ModTime == "" { info.ModTime = parseTimeFromName(name) } list = append(list, info) } } // 按名称倒序(最新的在前) sort.Slice(list, func(i, j int) bool { return list[i].Name > list[j].Name }) return list, nil } // 回退:正则匹配 matches := re.FindAllString(string(body), -1) unique := make(map[string]bool) var list []BackupInfo for _, m := range matches { if !unique[m] { unique[m] = true list = append(list, BackupInfo{ Name: m, ModTime: parseTimeFromName(m), }) } } sort.Slice(list, func(i, j int) bool { return list[i].Name > list[j].Name }) return list, nil } // parseTimeFromName 从备份文件名解析时间 func parseTimeFromName(name string) string { re := regexp.MustCompile(`(\d{8})_(\d{6})`) m := re.FindStringSubmatch(name) if len(m) == 3 { t, err := time.Parse("20060102_150405", m[1]+"_"+m[2]) if err == nil { return t.Format("2006-01-02 15:04:05") } } return "" } // CreateBackup 创建本地备份,存放到 backups/ 目录 func CreateBackup(dbPath string) (string, error) { // 确保 backups 目录存在 backupDir := "backups" if err := os.MkdirAll(backupDir, 0755); err != nil { return "", fmt.Errorf("创建备份目录失败: %v", err) } timestamp := time.Now().Format("20060102_150405") backupName := fmt.Sprintf("tonav_backup_%s.db", timestamp) backupPath := filepath.Join(backupDir, backupName) input, err := os.ReadFile(dbPath) if err != nil { return "", fmt.Errorf("读取数据库失败: %v", err) } err = os.WriteFile(backupPath, input, 0644) if err != nil { return "", fmt.Errorf("写入备份文件失败: %v", err) } return backupPath, nil } // CleanOldBackups 清理本地旧备份,保留最近 keep 份 func CleanOldBackups(keep int) { backupDir := "backups" entries, err := os.ReadDir(backupDir) if err != nil { return } re := regexp.MustCompile(`^tonav_backup_\d{8}_\d{6}\.db$`) var backups []string for _, e := range entries { if !e.IsDir() && re.MatchString(e.Name()) { backups = append(backups, e.Name()) } } // 按名称排序(时间戳命名,字母序即时间序) sort.Strings(backups) // 删除多余的旧备份 if len(backups) > keep { for _, name := range backups[:len(backups)-keep] { os.Remove(filepath.Join(backupDir, name)) } } }