package voxyimport import ( "archive/zip" "encoding/json" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "sort" "strings" "time" ) // ===== default implementation ===== // DefaultVoxyService is the production implementation of VoxyService. type DefaultVoxyService struct { SourcesAPIURL string } // NewDefaultVoxyService creates a new DefaultVoxyService with the standard API URL. func NewDefaultVoxyService() *DefaultVoxyService { return &DefaultVoxyService{ SourcesAPIURL: "http://127.0.0.1:3131/api/v1/voxy_import/", } } // ===== SearchDirs ===== func (s *DefaultVoxyService) SearchDirs(serverAddr string) ([]string, error) { var dirs []string suffix := filepath.Join(".voxy", "saves", serverAddr) err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() { clean := filepath.ToSlash(path) s := filepath.ToSlash(suffix) if strings.HasSuffix(clean, "/"+s) || clean == s { dirs = append(dirs, path) return filepath.SkipDir } } return nil }) sort.Strings(dirs) return dirs, err } // ===== FetchSources ===== func (s *DefaultVoxyService) FetchSources() ([]Source, error) { resp, err := http.Get(s.SourcesAPIURL) if err != nil { return nil, fmt.Errorf("获取下载源失败: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("获取下载源 HTTP %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } var urls []string if err := json.Unmarshal(body, &urls); err != nil { // not a plain array — try {"urls": [...]} var wrapper struct { URLs []string `json:"urls"` } if err := json.Unmarshal(body, &wrapper); err != nil { return nil, fmt.Errorf("解析下载源数据失败: %w", err) } urls = wrapper.URLs } var sources []Source if len(urls) > 0 { sources = append(sources, Source{Name: "默认源 (arinera.fun)", URL: urls[0]}) } if len(urls) > 1 { sources = append(sources, Source{Name: "CloudFlare 源 (arinera.space)", URL: urls[1]}) } return sources, nil } // ===== DownloadFile ===== func (s *DefaultVoxyService) DownloadFile(url string, destPath string, onProgress func(current, total int64)) error { resp, err := http.Get(url) if err != nil { return fmt.Errorf("http get: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("http status %d", resp.StatusCode) } out, err := os.Create(destPath) if err != nil { return fmt.Errorf("create dest file: %w", err) } defer out.Close() buf := make([]byte, 32*1024) var current int64 total := resp.ContentLength for { n, readErr := resp.Body.Read(buf) if n > 0 { if _, writeErr := out.Write(buf[:n]); writeErr != nil { return fmt.Errorf("write dest file: %w", writeErr) } current += int64(n) if onProgress != nil { onProgress(current, total) } } if readErr != nil { if readErr != io.EOF { return fmt.Errorf("read response: %w", readErr) } break } } return out.Close() } // ===== BackupDir ===== func (s *DefaultVoxyService) BackupDir(dir string) (string, error) { timestamp := time.Now().Format("2006-01-02_150405") backupName := timestamp + ".bak.zip" backupPath := filepath.Join(dir, backupName) f, err := os.Create(backupPath) if err != nil { return "", fmt.Errorf("create backup: %w", err) } defer f.Close() w := zip.NewWriter(f) defer w.Close() err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() { return nil } if path == backupPath { return nil } relPath, err := filepath.Rel(dir, path) if err != nil { return nil } relPath = filepath.ToSlash(relPath) entry, err := w.Create(relPath) if err != nil { return fmt.Errorf("create zip entry %s: %w", relPath, err) } src, err := os.Open(path) if err != nil { return fmt.Errorf("open %s: %w", path, err) } defer src.Close() if _, err := io.Copy(entry, src); err != nil { return fmt.Errorf("write %s: %w", relPath, err) } return nil }) if err != nil { return "", fmt.Errorf("backup walk: %w", err) } return backupName, nil } // ===== CleanupDir ===== func (s *DefaultVoxyService) CleanupDir(dir string, keepName string) error { keepPath := filepath.Join(dir, keepName) return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if path == dir { return nil } if path == keepPath { return nil } return os.RemoveAll(path) }) } // ===== ExtractZip ===== func (s *DefaultVoxyService) ExtractZip(zipPath, dir string) error { r, err := zip.OpenReader(zipPath) if err != nil { return fmt.Errorf("open zip: %w", err) } defer r.Close() for _, f := range r.File { if err := extractEntry(f, dir); err != nil { return err } } return nil } func extractEntry(f *zip.File, destDir string) error { targetPath := filepath.Join(destDir, f.Name) cleanDest, _ := filepath.Abs(destDir) cleanTarget, _ := filepath.Abs(targetPath) if !strings.HasPrefix(cleanTarget, cleanDest+string(os.PathSeparator)) && cleanTarget != cleanDest { return fmt.Errorf("path traversal detected: %s", f.Name) } if f.FileInfo().IsDir() { return os.MkdirAll(targetPath, os.ModePerm) } if err := os.MkdirAll(filepath.Dir(targetPath), os.ModePerm); err != nil { return fmt.Errorf("mkdir %s: %w", filepath.Dir(targetPath), err) } rc, err := f.Open() if err != nil { return fmt.Errorf("open zip entry %s: %w", f.Name, err) } defer rc.Close() out, err := os.Create(targetPath) if err != nil { return fmt.Errorf("create %s: %w", targetPath, err) } defer out.Close() if _, err := io.Copy(out, rc); err != nil { return fmt.Errorf("write %s: %w", f.Name, err) } return nil }