From d7e88754d49be11be4adcad400aeb511b0468c25 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Fri, 3 Jan 2025 22:02:21 +0530 Subject: [PATCH 1/7] feat(resume downloads): add basic tests Signed-off-by: Saarthak Verma --- pkg/downloader/uri.go | 6 +++++- pkg/downloader/uri_test.go | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/downloader/uri.go b/pkg/downloader/uri.go index 9acbb621737d..89304ee3dbb5 100644 --- a/pkg/downloader/uri.go +++ b/pkg/downloader/uri.go @@ -282,8 +282,12 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat if err != nil { return fmt.Errorf("failed to create parent directory for file %q: %v", filePath, err) } - + /** Enabling partial downloads + * - Do I remove the partial file + * - + */ // save partial download to dedicated file + fmt.Printf("DELETEING PARTIAL FILE") tmpFilePath := filePath + ".partial" // remove tmp file diff --git a/pkg/downloader/uri_test.go b/pkg/downloader/uri_test.go index 3b7a80b3ee9a..861ee90e8243 100644 --- a/pkg/downloader/uri_test.go +++ b/pkg/downloader/uri_test.go @@ -38,3 +38,10 @@ var _ = Describe("Gallery API tests", func() { }) }) }) + +var _ = Describe("Download Test", func() { + Context("URI", func() { + It("Resumes partially downloaded files") + It("It accurately updates progress") + }) +}) From 9e555bb1879c1bec8167338b3760abf4f8b50092 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Sat, 4 Jan 2025 17:39:22 +0530 Subject: [PATCH 2/7] test(resume downloads): implement file download tc Signed-off-by: Saarthak Verma --- pkg/downloader/uri_test.go | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pkg/downloader/uri_test.go b/pkg/downloader/uri_test.go index 861ee90e8243..c2ac43b8844e 100644 --- a/pkg/downloader/uri_test.go +++ b/pkg/downloader/uri_test.go @@ -1,6 +1,13 @@ package downloader_test import ( + "crypto/rand" + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "os" + . "github.com/mudler/LocalAI/pkg/downloader" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -40,8 +47,32 @@ var _ = Describe("Gallery API tests", func() { }) var _ = Describe("Download Test", func() { - Context("URI", func() { - It("Resumes partially downloaded files") - It("It accurately updates progress") + Context("URI DownloadFile", func() { + It("fetches files from mock server", func() { + mockData := make([]byte, 20000) + _, err := rand.Read(mockData) + Expect(err).ToNot(HaveOccurred()) + + mockDataSha := sha256.New() + mockDataSha.Write(mockData) + + mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write(mockData) + })) + mockServer.EnableHTTP2 = true + mockServer.Start() + dir, err := os.Getwd() + filePath := dir + "/my_supercool_model" + Expect(err).NotTo(HaveOccurred()) + uri := URI(mockServer.URL) + err = uri.DownloadFile(filePath, fmt.Sprintf("%x", mockDataSha.Sum(nil)), 1, 1, func(s1, s2, s3 string, f float64) {}) + Expect(err).ToNot(HaveOccurred()) + err = os.Remove(filePath) // cleanup, also checks existance of filePath` + Expect(err).ToNot(HaveOccurred()) + }) + // It("resumes partially downloaded files") + // It("it accurately updates progress") + // It("deletes partial file if after completion hash of downloaded file doesn't match hash of the file in the server") }) }) From 44d78694052982c57593d199b2a63686052829d0 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Sat, 4 Jan 2025 23:51:48 +0530 Subject: [PATCH 3/7] test(resume downloads): add resume partial download test Signed-off-by: Saarthak Verma --- pkg/downloader/uri_test.go | 91 +++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/pkg/downloader/uri_test.go b/pkg/downloader/uri_test.go index c2ac43b8844e..b1bccb5204a2 100644 --- a/pkg/downloader/uri_test.go +++ b/pkg/downloader/uri_test.go @@ -7,6 +7,8 @@ import ( "net/http" "net/http/httptest" "os" + "regexp" + "strconv" . "github.com/mudler/LocalAI/pkg/downloader" . "github.com/onsi/ginkgo/v2" @@ -47,31 +49,88 @@ var _ = Describe("Gallery API tests", func() { }) var _ = Describe("Download Test", func() { + var mockData []byte + var mockDataSha string + var filePath string + + var getMockServer = func() *httptest.Server { + mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var respData []byte + rangeString := r.Header.Get("Range") + if rangeString != "" { + regex := regexp.MustCompile(`^bytes=(\d+)-(\d+|)$`) + matches := regex.FindStringSubmatch(rangeString) + if matches == nil { + w.WriteHeader(http.StatusBadRequest) + return + } + startPos := 0 + endPos := len(mockData) + var err error + if matches[1] != "" { + startPos, err = strconv.Atoi(matches[1]) + Expect(err).ToNot(HaveOccurred()) + } + if matches[2] != "" { + endPos, err = strconv.Atoi(matches[2]) + Expect(err).ToNot(HaveOccurred()) + endPos += 1 + } + if startPos < 0 || startPos >= len(mockData) || endPos < 0 || endPos > len(mockData) || startPos > endPos { + w.WriteHeader(http.StatusBadRequest) + return + } + respData = mockData[startPos:endPos] + } else { + respData = mockData + } + w.WriteHeader(http.StatusOK) + w.Write(respData) + })) + mockServer.EnableHTTP2 = true + mockServer.Start() + return mockServer + } + + BeforeEach(func() { + mockData = make([]byte, 20000) + _, err := rand.Read(mockData) + Expect(err).ToNot(HaveOccurred()) + _mockDataSha := sha256.New() + _, err = _mockDataSha.Write(mockData) + Expect(err).ToNot(HaveOccurred()) + mockDataSha = fmt.Sprintf("%x", _mockDataSha.Sum(nil)) + dir, err := os.Getwd() + filePath = dir + "/my_supercool_model" + Expect(err).NotTo(HaveOccurred()) + }) + Context("URI DownloadFile", func() { It("fetches files from mock server", func() { - mockData := make([]byte, 20000) - _, err := rand.Read(mockData) + mockServer := getMockServer() + defer mockServer.Close() + uri := URI(mockServer.URL) + err := uri.DownloadFile(filePath, mockDataSha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) + err = os.Remove(filePath) // cleanup, also checks existance of filePath` + Expect(err).ToNot(HaveOccurred()) + }) - mockDataSha := sha256.New() - mockDataSha.Write(mockData) - - mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write(mockData) - })) - mockServer.EnableHTTP2 = true - mockServer.Start() - dir, err := os.Getwd() - filePath := dir + "/my_supercool_model" - Expect(err).NotTo(HaveOccurred()) + It("resumes partially downloaded files", func() { + mockServer := getMockServer() + defer mockServer.Close() uri := URI(mockServer.URL) - err = uri.DownloadFile(filePath, fmt.Sprintf("%x", mockDataSha.Sum(nil)), 1, 1, func(s1, s2, s3 string, f float64) {}) + // Create a partial file + tmpFilePath := filePath + ".partial" + file, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + Expect(err).ToNot(HaveOccurred()) + _, err = file.Write(mockData[0:10000]) + Expect(err).ToNot(HaveOccurred()) + err = uri.DownloadFile(filePath, mockDataSha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) err = os.Remove(filePath) // cleanup, also checks existance of filePath` Expect(err).ToNot(HaveOccurred()) }) - // It("resumes partially downloaded files") // It("it accurately updates progress") // It("deletes partial file if after completion hash of downloaded file doesn't match hash of the file in the server") }) From a9bec0fc5fdeeb04d6b985535000129b03d6e0d4 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Sat, 4 Jan 2025 23:58:26 +0530 Subject: [PATCH 4/7] feat(resume downloads): implement resumable downloads for interrupted transfers - Adds support for resuming partially downloaded files - Uses HTTP Range header to continue from last byte position - Maintains download progress across interruptions - Preserves partial downloads with .partial extension - Validates SHA256 checksum after completion Signed-off-by: Saarthak Verma --- pkg/downloader/uri.go | 79 ++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/pkg/downloader/uri.go b/pkg/downloader/uri.go index 89304ee3dbb5..44c8625ba8bd 100644 --- a/pkg/downloader/uri.go +++ b/pkg/downloader/uri.go @@ -2,7 +2,9 @@ package downloader import ( "crypto/sha256" + "errors" "fmt" + "hash" "io" "net/http" "net/url" @@ -204,6 +206,15 @@ func removePartialFile(tmpFilePath string) error { return nil } +func calculateHashForPartialFile(file *os.File) (hash.Hash, error) { + hash := sha256.New() + _, err := io.Copy(hash, file) + if err != nil { + return nil, err + } + return hash, nil +} + func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStatus func(string, string, string, float64)) error { url := uri.ResolveURL() if uri.LooksLikeOCI() { @@ -266,8 +277,32 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat log.Info().Msgf("Downloading %q", url) - // Download file - resp, err := http.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request for %q: %v", filePath, err) + } + + /* TODO + * 1. ~~Mock downloads~~ + * 2. Check if server supports partial downloads + * 3. ~~Resume partial downloads~~ + * 4. Ensure progressWriter accurately reflects progress if a partial file is present + * 5. MAYBE: + * a. Delete file if calculatedSHA != sha + */ + + // save partial download to dedicated file + tmpFilePath := filePath + ".partial" + tmpFileInfo, err := os.Stat(tmpFilePath) + if err == nil { + startPos := tmpFileInfo.Size() + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", startPos)) + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to check file %q existence: %v", filePath, err) + } + + // Start the request + resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("failed to download file %q: %v", filePath, err) } @@ -282,33 +317,29 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat if err != nil { return fmt.Errorf("failed to create parent directory for file %q: %v", filePath, err) } - /** Enabling partial downloads - * - Do I remove the partial file - * - - */ - // save partial download to dedicated file - fmt.Printf("DELETEING PARTIAL FILE") - tmpFilePath := filePath + ".partial" - // remove tmp file - err = removePartialFile(tmpFilePath) + // Create and write file + outFile, err := os.OpenFile(tmpFilePath, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) if err != nil { - return err + return fmt.Errorf("failed to create / open file %q: %v", tmpFilePath, err) } - - // Create and write file content - outFile, err := os.Create(tmpFilePath) + defer outFile.Close() + outFileInfo, err := outFile.Stat() if err != nil { - return fmt.Errorf("failed to create file %q: %v", tmpFilePath, err) + return fmt.Errorf("failed to get file info: %v", err) + } + fileSize := outFileInfo.Size() + hash, err := calculateHashForPartialFile(outFile) + if err != nil { + return fmt.Errorf("failed to calculate hash for partial file") } - defer outFile.Close() - progress := &progressWriter{ fileName: tmpFilePath, total: resp.ContentLength, - hash: sha256.New(), + hash: hash, fileNo: fileN, totalFiles: total, + written: fileSize, downloadStatus: downloadStatus, } _, err = io.Copy(io.MultiWriter(outFile, progress), resp.Body) @@ -316,11 +347,6 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat return fmt.Errorf("failed to write file %q: %v", filePath, err) } - err = os.Rename(tmpFilePath, filePath) - if err != nil { - return fmt.Errorf("failed to rename temporary file %s -> %s: %v", tmpFilePath, filePath, err) - } - if sha != "" { // Verify SHA calculatedSHA := fmt.Sprintf("%x", progress.hash.Sum(nil)) @@ -332,6 +358,11 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat log.Debug().Msgf("SHA missing for %q. Skipping validation", filePath) } + err = os.Rename(tmpFilePath, filePath) + if err != nil { + return fmt.Errorf("failed to rename temporary file %s -> %s: %v", tmpFilePath, filePath, err) + } + log.Info().Msgf("File %q downloaded and verified", filePath) if utils.IsArchive(filePath) { basePath := filepath.Dir(filePath) From 096a550079e5bfee54cdb5323da8a6f56bb7c787 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Mon, 6 Jan 2025 01:00:30 +0530 Subject: [PATCH 5/7] fix(resume downloads): incorrect download percent on front end Signed-off-by: Saarthak Verma --- pkg/downloader/uri.go | 6 ------ pkg/downloader/uri_test.go | 1 - 2 files changed, 7 deletions(-) diff --git a/pkg/downloader/uri.go b/pkg/downloader/uri.go index 44c8625ba8bd..1d2263305986 100644 --- a/pkg/downloader/uri.go +++ b/pkg/downloader/uri.go @@ -324,11 +324,6 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat return fmt.Errorf("failed to create / open file %q: %v", tmpFilePath, err) } defer outFile.Close() - outFileInfo, err := outFile.Stat() - if err != nil { - return fmt.Errorf("failed to get file info: %v", err) - } - fileSize := outFileInfo.Size() hash, err := calculateHashForPartialFile(outFile) if err != nil { return fmt.Errorf("failed to calculate hash for partial file") @@ -339,7 +334,6 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat hash: hash, fileNo: fileN, totalFiles: total, - written: fileSize, downloadStatus: downloadStatus, } _, err = io.Copy(io.MultiWriter(outFile, progress), resp.Body) diff --git a/pkg/downloader/uri_test.go b/pkg/downloader/uri_test.go index b1bccb5204a2..bcf13a32c9c5 100644 --- a/pkg/downloader/uri_test.go +++ b/pkg/downloader/uri_test.go @@ -131,7 +131,6 @@ var _ = Describe("Download Test", func() { err = os.Remove(filePath) // cleanup, also checks existance of filePath` Expect(err).ToNot(HaveOccurred()) }) - // It("it accurately updates progress") // It("deletes partial file if after completion hash of downloaded file doesn't match hash of the file in the server") }) }) From fe8472c8635d2781c6f3dbc070fb3710caa5dc23 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Wed, 8 Jan 2025 23:07:27 +0530 Subject: [PATCH 6/7] feat(resume download): add range header check tc Signed-off-by: Saarthak Verma --- pkg/downloader/uri_test.go | 99 ++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/pkg/downloader/uri_test.go b/pkg/downloader/uri_test.go index bcf13a32c9c5..6976c9b44bf7 100644 --- a/pkg/downloader/uri_test.go +++ b/pkg/downloader/uri_test.go @@ -48,42 +48,77 @@ var _ = Describe("Gallery API tests", func() { }) }) +type RangeHeaderError struct { + msg string +} + +func (e *RangeHeaderError) Error() string { return e.msg } + var _ = Describe("Download Test", func() { var mockData []byte var mockDataSha string var filePath string - var getMockServer = func() *httptest.Server { + extractRangeHeader := func(rangeString string) (int, int, error) { + regex := regexp.MustCompile(`^bytes=(\d+)-(\d+|)$`) + matches := regex.FindStringSubmatch(rangeString) + rangeErr := RangeHeaderError{msg: "invalid / ill-formatted range"} + if matches == nil { + return -1, -1, &rangeErr + } + startPos, err := strconv.Atoi(matches[1]) + if err != nil { + return -1, -1, err + } + + endPos := -1 + if matches[2] != "" { + endPos, err = strconv.Atoi(matches[2]) + if err != nil { + return -1, -1, err + } + endPos += 1 // because range is inclusive in rangeString + } + return startPos, endPos, nil + } + + getMockServer := func(supportsRangeHeader bool) *httptest.Server { mockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "HEAD" && r.Method != "GET" { + w.WriteHeader(http.StatusNotFound) + return + } + if r.Method == "HEAD" { + if supportsRangeHeader { + w.Header().Add("Accept-Ranges", "bytes") + } + w.WriteHeader(http.StatusOK) + return + } + // GET method + startPos := 0 + endPos := len(mockData) + var err error var respData []byte rangeString := r.Header.Get("Range") if rangeString != "" { - regex := regexp.MustCompile(`^bytes=(\d+)-(\d+|)$`) - matches := regex.FindStringSubmatch(rangeString) - if matches == nil { - w.WriteHeader(http.StatusBadRequest) - return - } - startPos := 0 - endPos := len(mockData) - var err error - if matches[1] != "" { - startPos, err = strconv.Atoi(matches[1]) + startPos, endPos, err = extractRangeHeader(rangeString) + if err != nil { + if _, ok := err.(*RangeHeaderError); ok { + w.WriteHeader(http.StatusBadRequest) + return + } Expect(err).ToNot(HaveOccurred()) } - if matches[2] != "" { - endPos, err = strconv.Atoi(matches[2]) - Expect(err).ToNot(HaveOccurred()) - endPos += 1 + if endPos == -1 { + endPos = len(mockData) } if startPos < 0 || startPos >= len(mockData) || endPos < 0 || endPos > len(mockData) || startPos > endPos { w.WriteHeader(http.StatusBadRequest) return } - respData = mockData[startPos:endPos] - } else { - respData = mockData } + respData = mockData[startPos:endPos] w.WriteHeader(http.StatusOK) w.Write(respData) })) @@ -107,17 +142,15 @@ var _ = Describe("Download Test", func() { Context("URI DownloadFile", func() { It("fetches files from mock server", func() { - mockServer := getMockServer() + mockServer := getMockServer(true) defer mockServer.Close() uri := URI(mockServer.URL) err := uri.DownloadFile(filePath, mockDataSha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) - err = os.Remove(filePath) // cleanup, also checks existance of filePath` - Expect(err).ToNot(HaveOccurred()) }) It("resumes partially downloaded files", func() { - mockServer := getMockServer() + mockServer := getMockServer(true) defer mockServer.Close() uri := URI(mockServer.URL) // Create a partial file @@ -128,9 +161,25 @@ var _ = Describe("Download Test", func() { Expect(err).ToNot(HaveOccurred()) err = uri.DownloadFile(filePath, mockDataSha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) - err = os.Remove(filePath) // cleanup, also checks existance of filePath` + }) + + It("restarts download from 0 if server doesn't support Range header", func() { + mockServer := getMockServer(false) + defer mockServer.Close() + uri := URI(mockServer.URL) + // Create a partial file + tmpFilePath := filePath + ".partial" + file, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + Expect(err).ToNot(HaveOccurred()) + _, err = file.Write(mockData[0:10000]) + Expect(err).ToNot(HaveOccurred()) + err = uri.DownloadFile(filePath, mockDataSha, 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) }) - // It("deletes partial file if after completion hash of downloaded file doesn't match hash of the file in the server") + }) + + AfterEach(func() { + os.Remove(filePath) // cleanup, also checks existance of filePath` + os.Remove(filePath + ".partial") }) }) From 7a46b416e51b9080db2d059ee37da65ea48fe0f7 Mon Sep 17 00:00:00 2001 From: Saarthak Verma Date: Wed, 8 Jan 2025 23:09:07 +0530 Subject: [PATCH 7/7] feat(resume download): implement range header check Signed-off-by: Saarthak Verma --- pkg/downloader/uri.go | 44 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/pkg/downloader/uri.go b/pkg/downloader/uri.go index 1d2263305986..2e0363c836dc 100644 --- a/pkg/downloader/uri.go +++ b/pkg/downloader/uri.go @@ -215,6 +215,16 @@ func calculateHashForPartialFile(file *os.File) (hash.Hash, error) { return hash, nil } +func (uri URI) checkSeverSupportsRangeHeader() (bool, error) { + url := uri.ResolveURL() + resp, err := http.Head(url) + if err != nil { + return false, err + } + defer resp.Body.Close() + return resp.Header.Get("Accept-Ranges") == "bytes", nil +} + func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStatus func(string, string, string, float64)) error { url := uri.ResolveURL() if uri.LooksLikeOCI() { @@ -282,21 +292,23 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat return fmt.Errorf("failed to create request for %q: %v", filePath, err) } - /* TODO - * 1. ~~Mock downloads~~ - * 2. Check if server supports partial downloads - * 3. ~~Resume partial downloads~~ - * 4. Ensure progressWriter accurately reflects progress if a partial file is present - * 5. MAYBE: - * a. Delete file if calculatedSHA != sha - */ - // save partial download to dedicated file tmpFilePath := filePath + ".partial" tmpFileInfo, err := os.Stat(tmpFilePath) if err == nil { - startPos := tmpFileInfo.Size() - req.Header.Set("Range", fmt.Sprintf("bytes=%d-", startPos)) + support, err := uri.checkSeverSupportsRangeHeader() + if err != nil { + return fmt.Errorf("failed to check if uri server supports range header: %v", err) + } + if support { + startPos := tmpFileInfo.Size() + req.Header.Set("Range", fmt.Sprintf("bytes=%d-", startPos)) + } else { + err := removePartialFile(tmpFilePath) + if err != nil { + return err + } + } } else if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to check file %q existence: %v", filePath, err) } @@ -341,6 +353,11 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat return fmt.Errorf("failed to write file %q: %v", filePath, err) } + err = os.Rename(tmpFilePath, filePath) + if err != nil { + return fmt.Errorf("failed to rename temporary file %s -> %s: %v", tmpFilePath, filePath, err) + } + if sha != "" { // Verify SHA calculatedSHA := fmt.Sprintf("%x", progress.hash.Sum(nil)) @@ -352,11 +369,6 @@ func (uri URI) DownloadFile(filePath, sha string, fileN, total int, downloadStat log.Debug().Msgf("SHA missing for %q. Skipping validation", filePath) } - err = os.Rename(tmpFilePath, filePath) - if err != nil { - return fmt.Errorf("failed to rename temporary file %s -> %s: %v", tmpFilePath, filePath, err) - } - log.Info().Msgf("File %q downloaded and verified", filePath) if utils.IsArchive(filePath) { basePath := filepath.Dir(filePath)