From 825443c94207230def2fb467bfa6e665478e03f2 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 07 Jun 2026 01:42:53 +0800
Subject: [PATCH] Bump version to 2.1.29 experimental
---
privatevoice.src/internal/model/downloader.go | 131 ++++++++++++++++++++++++++++++++++++++++---
1 files changed, 120 insertions(+), 11 deletions(-)
diff --git a/privatevoice.src/internal/model/downloader.go b/privatevoice.src/internal/model/downloader.go
index 7aa9c25..f77c66d 100755
--- a/privatevoice.src/internal/model/downloader.go
+++ b/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"
@@ -27,6 +30,7 @@
if err := os.MkdirAll(modelsDir, 0755); err != nil {
return fmt.Errorf("failed to create models dir: %w", err)
}
+ cleanupStaleDownloadDirs(profile, modelsDir)
var lastErr error
for i, url := range urls {
@@ -50,6 +54,13 @@
lastErr = fmt.Errorf("no download URL configured")
}
return fmt.Errorf("download failed: %w", lastErr)
+}
+
+func cleanupStaleDownloadDirs(profile ModelProfile, modelsDir string) {
+ downloadRoot := filepath.Join(modelsDir, ".downloads", profile.ID)
+ if err := os.RemoveAll(downloadRoot); err != nil {
+ logger.Info("Failed to remove stale download dir %s: %v", downloadRoot, err)
+ }
}
func downloadAndInstallFromURL(profile ModelProfile, url, modelsDir string, attempt int, progress ProgressCallback) error {
@@ -94,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 {
@@ -125,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 {
--
Gitblit v1.9.3