feat: improve daemon resilience, streaming, and usenet downloads
- 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)
This commit is contained in:
parent
e332c0a6e4
commit
197e33956a
24 changed files with 2310 additions and 84 deletions
123
internal/upgrade/extract.go
Normal file
123
internal/upgrade/extract.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue