- Add daemon state persistence and stale resume file cleanup - Add TriggerPoll for WebSocket resume actions - Improve stream server with graceful shutdown and connection tracking - Add desktop notifications for download completion - Add media file organization with Movies/TV Shows detection - Improve usenet downloader with progress tracking and resume support - Add self-update package with GitHub release verification - Downgrade tablewriter to v0.0.5 (v1.x API breaking change)
123 lines
2.4 KiB
Go
123 lines
2.4 KiB
Go
package upgrade
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// extractBinary extracts the unarr binary from the release archive into destDir.
|
|
// Returns the path to the extracted binary.
|
|
func extractBinary(archivePath, destDir string) (string, error) {
|
|
if runtime.GOOS == "windows" {
|
|
return extractZip(archivePath, destDir)
|
|
}
|
|
return extractTarGz(archivePath, destDir)
|
|
}
|
|
|
|
func extractTarGz(archivePath, destDir string) (string, error) {
|
|
f, err := os.Open(archivePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
|
|
gz, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return "", fmt.Errorf("gzip: %w", err)
|
|
}
|
|
defer gz.Close()
|
|
|
|
tr := tar.NewReader(gz)
|
|
target := binaryName
|
|
if runtime.GOOS == "windows" {
|
|
target += ".exe"
|
|
}
|
|
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("tar: %w", err)
|
|
}
|
|
|
|
name := filepath.Base(hdr.Name)
|
|
if name != target {
|
|
continue
|
|
}
|
|
|
|
// Validate: must be a regular file
|
|
if hdr.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
|
|
dst := filepath.Join(destDir, target)
|
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := io.Copy(out, io.LimitReader(tr, 200<<20)); err != nil { // 200MB limit
|
|
out.Close()
|
|
return "", fmt.Errorf("extract: %w", err)
|
|
}
|
|
out.Close()
|
|
return dst, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("binary %q not found in archive", target)
|
|
}
|
|
|
|
func extractZip(archivePath, destDir string) (string, error) {
|
|
r, err := zip.OpenReader(archivePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("zip: %w", err)
|
|
}
|
|
defer r.Close()
|
|
|
|
target := binaryName + ".exe"
|
|
|
|
for _, f := range r.File {
|
|
name := filepath.Base(f.Name)
|
|
|
|
// Guard against path traversal
|
|
if strings.Contains(f.Name, "..") {
|
|
continue
|
|
}
|
|
|
|
if name != target {
|
|
continue
|
|
}
|
|
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
dst := filepath.Join(destDir, target)
|
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
|
if err != nil {
|
|
rc.Close()
|
|
return "", err
|
|
}
|
|
|
|
if _, err := io.Copy(out, io.LimitReader(rc, 200<<20)); err != nil { // 200MB limit
|
|
out.Close()
|
|
rc.Close()
|
|
return "", fmt.Errorf("extract: %w", err)
|
|
}
|
|
out.Close()
|
|
rc.Close()
|
|
return dst, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("binary %q not found in archive", target)
|
|
}
|