Ariver
2026-06-03 e1c362699c9a545b9db99130424f5f32d20723c4
privatevoice.src/internal/model/downloader.go
@@ -1,12 +1,15 @@
package model
import (
   "errors"
   "fmt"
   "io"
   "net/http"
   "os"
   "os/exec"
   "path/filepath"
   "strconv"
   "strings"
   "time"
   "voicesnap/internal/logger"
@@ -102,27 +105,84 @@
}
func downloadFile(url, destPath string, progress ProgressCallback) error {
   resp, err := http.Get(url)
   const maxAttempts = 6
   var lastErr error
   var lastPercent = -1.0
   for attempt := 1; attempt <= maxAttempts; attempt++ {
      downloaded := existingFileSize(destPath)
      err := downloadFileAttempt(url, destPath, downloaded, &lastPercent, progress)
      if err == nil {
         return nil
      }
      if isNonRetryableDownloadError(err) {
         return err
      }
      lastErr = err
      logger.Info("Download attempt %d/%d failed: %v", attempt, maxAttempts, err)
      if attempt < maxAttempts {
         time.Sleep(time.Duration(attempt) * time.Second)
      }
   }
   return fmt.Errorf("download failed after %d attempts: %w", maxAttempts, lastErr)
}
func downloadFileAttempt(url, destPath string, resumeFrom int64, lastPercent *float64, progress ProgressCallback) error {
   req, err := http.NewRequest(http.MethodGet, url, nil)
   if err != nil {
      return nonRetryableDownloadError{err: err}
   }
   if resumeFrom > 0 {
      req.Header.Set("Range", fmt.Sprintf("bytes=%d-", resumeFrom))
   }
   resp, err := http.DefaultClient.Do(req)
   if err != nil {
      return err
   }
   defer resp.Body.Close()
   if resp.StatusCode != http.StatusOK {
      return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
   appendMode := resumeFrom > 0 && resp.StatusCode == http.StatusPartialContent
   if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
      err := fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
      if resp.StatusCode < http.StatusInternalServerError {
         return nonRetryableDownloadError{err: err}
      }
      return err
   }
   if resumeFrom > 0 && resp.StatusCode == http.StatusOK {
      logger.Info("Download server ignored Range request; restarting download")
      resumeFrom = 0
      appendMode = false
   }
   totalBytes := resp.ContentLength
   out, err := os.Create(destPath)
   if appendMode {
      if total := parseContentRangeTotal(resp.Header.Get("Content-Range")); total > 0 {
         totalBytes = total
      } else if resp.ContentLength > 0 {
         totalBytes = resumeFrom + resp.ContentLength
      }
   }
   flag := os.O_CREATE | os.O_WRONLY
   if appendMode {
      flag |= os.O_APPEND
   } else {
      flag |= os.O_TRUNC
   }
   out, err := os.OpenFile(destPath, flag, 0644)
   if err != nil {
      return err
   }
   defer out.Close()
   downloaded := resumeFrom
   if !appendMode {
      downloaded = 0
   }
   buf := make([]byte, 32*1024)
   var downloaded int64
   lastPercent := -1.0
   for {
      n, readErr := resp.Body.Read(buf)
      if n > 0 {
@@ -133,21 +193,62 @@
         if totalBytes > 0 && progress != nil {
            pct := float64(downloaded) / float64(totalBytes) * 100
            if pct-lastPercent >= 0.5 {
               lastPercent = pct
            if pct-*lastPercent >= 0.5 {
               *lastPercent = pct
               progress(pct, downloaded, totalBytes)
            }
         }
      }
      if readErr != nil {
         if readErr == io.EOF {
            break
            if totalBytes > 0 && downloaded < totalBytes {
               return fmt.Errorf("short download: got %d of %d bytes", downloaded, totalBytes)
            }
            return nil
         }
         return readErr
      }
   }
}
   return nil
func existingFileSize(path string) int64 {
   info, err := os.Stat(path)
   if err != nil || info.IsDir() {
      return 0
   }
   return info.Size()
}
func parseContentRangeTotal(value string) int64 {
   if value == "" {
      return 0
   }
   slash := strings.LastIndex(value, "/")
   if slash < 0 || slash == len(value)-1 {
      return 0
   }
   total, err := strconv.ParseInt(value[slash+1:], 10, 64)
   if err != nil {
      return 0
   }
   return total
}
type nonRetryableDownloadError struct {
   err error
}
func (e nonRetryableDownloadError) Error() string {
   return e.err.Error()
}
func (e nonRetryableDownloadError) Unwrap() error {
   return e.err
}
func isNonRetryableDownloadError(err error) bool {
   var target nonRetryableDownloadError
   return errors.As(err, &target)
}
func extractArchive(archivePath, destDir string) error {