package installer import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" ) // ExtractArchiveWithOutput extracts an archive to the specified destination with output logging func (i *Installer) ExtractArchiveWithOutput(cr *CommandRunner, src, dest string) error { cr.outputChan <- fmt.Sprintf("Extracting: %s", src) var err error ext := strings.ToLower(filepath.Ext(src)) switch { case ext == ".zip": err = i.unzipWithOutput(cr, src, dest) case ext == ".7z": err = i.extract7zWithOutput(cr, src, dest) case strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tar.xz"): err = i.extractTarWithOutput(cr, src, dest) default: err = fmt.Errorf("unsupported archive format: %s", ext) } if err != nil { cr.outputChan <- fmt.Sprintf("Error: Extraction failed: %v", err) return err } cr.outputChan <- fmt.Sprintf("Extracted to: %s", dest) return nil } // ExtractArchiveContentsWithOutput extracts archive contents only (skips top-level directory) // Downloads to temp dir, extracts, then moves contents to destination func (i *Installer) ExtractArchiveContentsWithOutput(cr *CommandRunner, url, dest string) error { // Create temp directory tempDir, err := os.MkdirTemp("", "mpv-extract") if err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } defer os.RemoveAll(tempDir) cr.outputChan <- fmt.Sprintf("Downloading to temp directory: %s", tempDir) // Download archive to temp dir archiveName := filepath.Base(url) archivePath := filepath.Join(tempDir, archiveName) if err := i.DownloadFileWithProgressToChannel(cr, url, archivePath); err != nil { return err } cr.outputChan <- "Extracting archive to temp directory..." if err := i.ExtractArchiveWithOutput(cr, archivePath, tempDir); err != nil { return err } // Find extracted directory (should be single top-level dir) entries, err := os.ReadDir(tempDir) if err != nil { return err } var extractedDir string var hasTopLevelDir bool for _, entry := range entries { if entry.IsDir() && entry.Name() != archiveName && !strings.HasPrefix(entry.Name(), archiveName) { if extractedDir == "" { extractedDir = filepath.Join(tempDir, entry.Name()) hasTopLevelDir = true } } } var sourceDir string if hasTopLevelDir { sourceDir = extractedDir cr.outputChan <- fmt.Sprintf("Moving contents from %s to %s", filepath.Base(extractedDir), dest) } else { sourceDir = tempDir cr.outputChan <- fmt.Sprintf("Moving files from temp to %s", dest) } // Ensure destination exists if err := i.EnsureDirWithOutput(cr, dest); err != nil { return err } // Move all contents from source to destination var fileCount int err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } relPath, err := filepath.Rel(sourceDir, path) if err != nil { return err } if relPath == "." { return nil } destPath := filepath.Join(dest, relPath) if info.IsDir() { return os.MkdirAll(destPath, info.Mode()) } fileCount++ return i.copyFileQuiet(path, destPath, info.Mode()) }) if err != nil { return fmt.Errorf("failed to move files: %w", err) } cr.outputChan <- fmt.Sprintf("Moved %d files successfully", fileCount) return nil } // SetSevenZipPath sets a custom path to the 7zip executable func (i *Installer) SetSevenZipPath(path string) { i.sevenZipPath = path } // unzipWithOutput extracts a ZIP archive with output logging func (i *Installer) unzipWithOutput(cr *CommandRunner, src, dest string) error { if runtime.GOOS == "windows" { ext := strings.ToLower(filepath.Ext(src)) if ext == ".zip" { psCmd := fmt.Sprintf("Expand-Archive -Path '%s' -DestinationPath '%s' -Force", src, dest) cmd := cr.RunCommand("powershell", "-Command", psCmd) return cmd } sevenZip := i.sevenZipPath if sevenZip == "" { sevenZip = Find7zip() } if sevenZip == "" { return fmt.Errorf(constants.Err7zipNotFoundDetailed) } cmd := cr.RunCommand(sevenZip, "x", src, fmt.Sprintf("-o%s", dest), "-y") return cmd } cmd := cr.RunCommand("unzip", "-o", "-q", src, "-d", dest) return cmd } // extract7zWithOutput extracts a 7z archive with output logging func (i *Installer) extract7zWithOutput(cr *CommandRunner, src, dest string) error { sevenZip := i.sevenZipPath if sevenZip == "" { sevenZip = Find7zip() } if sevenZip == "" { return fmt.Errorf(constants.Err7zipNotFoundDetailed) } cmd := cr.RunCommand(sevenZip, "x", src, fmt.Sprintf("-o%s", dest), "-y") return cmd } // extractTarWithOutput extracts a tar archive (optionally gzipped) with output logging func (i *Installer) extractTarWithOutput(cr *CommandRunner, src, dest string) error { if strings.HasSuffix(src, ".tar.gz") { cmd := cr.RunCommand("tar", "xzf", src, "-C", dest) return cmd } cmd := cr.RunCommand("tar", "xf", src, "-C", dest) return cmd } // extractZip extracts a ZIP archive (no output logging) func (i *Installer) extractZip(src, dest string) error { if runtime.GOOS == "windows" { sevenZip := i.sevenZipPath if sevenZip == "" { sevenZip = Find7zip() } if sevenZip == "" { log.Error("7zip not found. Cannot extract ZIP on Windows") return fmt.Errorf("7zip not found. Cannot extract ZIP on Windows") } cmd := exec.Command(sevenZip, "x", src, fmt.Sprintf("-o%s", dest), "-y") if err := cmd.Run(); err != nil { log.Error(fmt.Sprintf("7-ZIP extraction failed: %s", err.Error())) return err } return nil } cmd := exec.Command("unzip", "-q", src, "-d", dest) if err := cmd.Run(); err != nil { log.Error(fmt.Sprintf("unzip extraction failed: %s", err.Error())) return err } return nil } // ExtractArchive extracts an archive to the specified destination (no output logging) func (i *Installer) ExtractArchive(src, dest string) error { ext := strings.ToLower(filepath.Ext(src)) switch ext { case ".zip": return i.extractZip(src, dest) case ".7z", ".7zip": return i.extract7z(src, dest) case ".tar": return i.extractTar(src, dest) case ".gz": if strings.HasSuffix(src, ".tar.gz") { return i.extractTarGz(src, dest) } return i.extractGz(src, dest) case ".xz": return i.extractXz(src, dest) case ".dmg": return fmt.Errorf("DMG files must be opened manually") case ".exe": return fmt.Errorf("EXE files must be executed manually") default: return fmt.Errorf("unsupported archive format: %s", ext) } } // extract7z extracts a 7z archive (no output logging) func (i *Installer) extract7z(src, dest string) error { sevenZip := Find7zip() if sevenZip == "" { return fmt.Errorf(constants.Err7zipNotFoundDetailed) } cmd := exec.Command(sevenZip, "x", src, fmt.Sprintf("-o%s", dest), "-y") return cmd.Run() } // extractTar extracts a tar archive (no output logging) func (i *Installer) extractTar(src, dest string) error { cmd := exec.Command("tar", "xf", src, "-C", dest) return cmd.Run() } // extractTarGz extracts a tar.gz archive (no output logging) func (i *Installer) extractTarGz(src, dest string) error { cmd := exec.Command("tar", "xzf", src, "-C", dest) return cmd.Run() } // extractGz extracts a gz archive (no Output logging) func (i *Installer) extractGz(src, dest string) error { cmd := exec.Command("gzip", "-d", "-c", src) output, err := cmd.Output() if err != nil { log.Error(fmt.Sprintf("gzip extraction failed for %s: %s", src, err.Error())) return fmt.Errorf("gzip extraction failed: %w", err) } destPath := filepath.Join(dest, strings.TrimSuffix(filepath.Base(src), ".gz")) if err := os.MkdirAll(dest, 0755); err != nil { log.Error(fmt.Sprintf("Failed to create directory %s: %s", dest, err.Error())) return err } if err := os.WriteFile(destPath, output, 0755); err != nil { log.Error(fmt.Sprintf("Failed to write file %s: %s", destPath, err.Error())) return err } return nil } // extractXz extracts an xz archive (no output logging) func (i *Installer) extractXz(src, dest string) error { cmd := exec.Command("xz", "-d", "-c", src) output, err := cmd.Output() if err != nil { log.Error(fmt.Sprintf("xz extraction failed for %s: %s", src, err.Error())) return fmt.Errorf("xz extraction failed: %w", err) } destPath := filepath.Join(dest, strings.TrimSuffix(filepath.Base(src), ".xz")) if err := os.MkdirAll(dest, 0755); err != nil { log.Error(fmt.Sprintf("Failed to create directory %s: %s", dest, err.Error())) return err } if err := os.WriteFile(destPath, output, 0755); err != nil { log.Error(fmt.Sprintf("Failed to write file %s: %s", destPath, err.Error())) return err } return nil }