package main import ( "archive/zip" "fmt" "io" "net/http" "os" "path/filepath" "strings" tea "github.com/charmbracelet/bubbletea" ) func executeAction(versionDir string, action Action, index int, backupDir string) tea.Cmd { total := index + 1 prefix := fmt.Sprintf("[%d]", total) switch action.Type { case "add": return executeAdd(versionDir, action, index, backupDir, prefix) case "delete": return func() tea.Msg { absPath := filepath.Join(versionDir, action.Path) if err := backupPath(versionDir, action.Path, backupDir); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("backup failed: %w", err)} } if err := os.RemoveAll(absPath); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("delete failed: %w", err)} } return actionCompleteMsg{index: index} } case "copy": return func() tea.Msg { dst := filepath.Join(versionDir, action.NewPath) if _, err := os.Stat(dst); err == nil { if err := backupPath(versionDir, action.NewPath, backupDir); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("backup failed: %w", err)} } } src := filepath.Join(versionDir, action.Path) if err := copyPath(src, dst); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("copy failed: %w", err)} } return actionCompleteMsg{index: index} } case "move": return func() tea.Msg { src := filepath.Join(versionDir, action.Path) dst := filepath.Join(versionDir, action.NewPath) if _, err := os.Stat(dst); err == nil { if err := backupPath(versionDir, action.NewPath, backupDir); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("backup failed: %w", err)} } } if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("mkdir failed: %w", err)} } if err := os.Rename(src, dst); err != nil { if err := copyPath(src, dst); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("move failed: %w", err)} } os.RemoveAll(src) } return actionCompleteMsg{index: index} } default: return func() tea.Msg { return actionErrorMsg{index: index, err: fmt.Errorf("unknown action type: %s", action.Type)} } } } func executeAdd(versionDir string, action Action, index int, backupDir, prefix string) tea.Cmd { return func() tea.Msg { absPath := filepath.Join(versionDir, action.Path) if err := os.MkdirAll(filepath.Dir(absPath), 0o755); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("mkdir failed: %w", err)} } if _, err := os.Stat(absPath); err == nil { if err := backupPath(versionDir, action.Path, backupDir); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("backup failed: %w", err)} } } err := downloadFile(action.URL, absPath) if err != nil { if len(action.Mirrors) > 0 { return mirrorChoiceMsg{index: index, mirrors: action.Mirrors, action: action} } return actionErrorMsg{index: index, err: fmt.Errorf("download failed: %w", err)} } if action.Unzip { destDir := filepath.Dir(absPath) if err := unzipFile(absPath, destDir); err != nil { return actionErrorMsg{index: index, err: fmt.Errorf("unzip failed: %w", err)} } os.Remove(absPath) } return actionCompleteMsg{index: index} } } func waitForProgress(ch chan float64, index int) tea.Cmd { return func() tea.Msg { p, ok := <-ch if !ok { return nil } return actionProgressMsg{percent: p} } } func downloadFile(url, destPath string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %d", resp.StatusCode) } tmpPath := destPath + ".tmp" f, err := os.Create(tmpPath) if err != nil { return err } _, err = io.Copy(f, resp.Body) f.Close() if err != nil { os.Remove(tmpPath) return err } return os.Rename(tmpPath, destPath) } func unzipFile(zipPath, destDir string) error { r, err := zip.OpenReader(zipPath) if err != nil { return err } defer r.Close() for _, f := range r.File { target := filepath.Join(destDir, f.Name) if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)+string(os.PathSeparator)) { continue } if f.FileInfo().IsDir() { os.MkdirAll(target, 0o755) continue } if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err } rc, err := f.Open() if err != nil { return err } outFile, err := os.Create(target) if err != nil { rc.Close() return err } _, err = io.Copy(outFile, rc) outFile.Close() rc.Close() if err != nil { return err } } return nil } func backupPath(versionDir, relativePath, backupDir string) error { src := filepath.Join(versionDir, relativePath) if _, err := os.Stat(src); os.IsNotExist(err) { return nil } dst := filepath.Join(versionDir, backupDir, relativePath) if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } info, err := os.Stat(src) if err != nil { return err } if info.IsDir() { return copyDir(src, dst) } return copyFile(src, dst) } func copyPath(src, dst string) error { info, err := os.Stat(src) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } if info.IsDir() { return copyDir(src, dst) } return copyFile(src, dst) } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err } func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } target := filepath.Join(dst, rel) if info.IsDir() { return os.MkdirAll(target, 0o755) } return copyFile(path, target) }) }