265 lines
5.8 KiB
Go
265 lines
5.8 KiB
Go
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
|
|
}
|