package downloader_test import ( "context" "crypto/rand" "crypto/sha256" "errors" "fmt" "net/http" "net/http/httptest" "os" "strconv" "strings" "time" . "github.com/mudler/LocalAI/pkg/downloader" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Download cancellation", func() { var filePath string // streamingRangeServer serves data one small chunk at a time with a short // pause between chunks, so a context cancellation can land mid-transfer. // It honors a `bytes=N-` Range request so a second attempt can resume. streamingRangeServer := func(data []byte) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { w.Header().Set("Accept-Ranges", "bytes") w.WriteHeader(http.StatusOK) return } start := 0 if rh := r.Header.Get("Range"); rh != "" { _, _ = fmt.Sscanf(strings.TrimPrefix(rh, "bytes="), "%d-", &start) } w.Header().Set("Content-Length", strconv.Itoa(len(data)-start)) if start > 0 { w.WriteHeader(http.StatusPartialContent) } else { w.WriteHeader(http.StatusOK) } f, _ := w.(http.Flusher) for i := start; i < len(data); i += 256 { end := i + 256 if end > len(data) { end = len(data) } if _, err := w.Write(data[i:end]); err != nil { return } if f != nil { f.Flush() } time.Sleep(20 * time.Millisecond) } })) } BeforeEach(func() { dir, err := os.Getwd() Expect(err).ToNot(HaveOccurred()) filePath = dir + "/cancel_model" }) AfterEach(func() { _ = os.Remove(filePath) _ = os.Remove(filePath + ".partial") }) It("keeps the .partial file when the context is cancelled so the download can resume", func() { data := make([]byte, 8192) _, err := rand.Read(data) Expect(err).ToNot(HaveOccurred()) server := streamingRangeServer(data) defer server.Close() ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(150 * time.Millisecond) cancel() }() err = URI(server.URL).DownloadFileWithContext(ctx, filePath, "", 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).To(HaveOccurred()) Expect(errors.Is(err, context.Canceled)).To(BeTrue()) info, statErr := os.Stat(filePath + ".partial") Expect(statErr).ToNot(HaveOccurred(), "a cancelled download must leave its .partial behind so the retry resumes instead of restarting from zero") Expect(info.Size()).To(BeNumerically(">", 0)) Expect(info.Size()).To(BeNumerically("<", int64(len(data)))) }) It("discards the .partial when the cancellation cause is ErrUserCancelled", func() { data := make([]byte, 8192) _, err := rand.Read(data) Expect(err).ToNot(HaveOccurred()) server := streamingRangeServer(data) defer server.Close() // A deliberate user abort: cancel WITH the ErrUserCancelled cause. The // half-finished download should not linger on disk. ctx, cancel := context.WithCancelCause(context.Background()) go func() { time.Sleep(150 * time.Millisecond) cancel(ErrUserCancelled) }() err = URI(server.URL).DownloadFileWithContext(ctx, filePath, "", 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).To(HaveOccurred()) Expect(errors.Is(err, context.Canceled)).To(BeTrue()) Expect(filePath + ".partial").ToNot(BeAnExistingFile(), "a deliberate user cancel must not leave a dangling .partial behind") }) It("resumes from the preserved .partial after a cancellation and completes", func() { data := make([]byte, 8192) _, err := rand.Read(data) Expect(err).ToNot(HaveOccurred()) sum := sha256.Sum256(data) sha := fmt.Sprintf("%x", sum) server := streamingRangeServer(data) defer server.Close() // First attempt: cancel mid-stream. ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(150 * time.Millisecond) cancel() }() err = URI(server.URL).DownloadFileWithContext(ctx, filePath, sha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).To(HaveOccurred()) partialInfo, statErr := os.Stat(filePath + ".partial") Expect(statErr).ToNot(HaveOccurred()) resumedFrom := partialInfo.Size() Expect(resumedFrom).To(BeNumerically(">", 0)) // Second attempt: fresh context, must resume and finish with a valid SHA. err = URI(server.URL).DownloadFileWithContext(context.Background(), filePath, sha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) final, rerr := os.ReadFile(filePath) Expect(rerr).ToNot(HaveOccurred()) Expect(final).To(Equal(data)) }) })