xplshn
·
2025-08-13
fetch.go
Go
1package main
2
3import (
4 "context"
5 "encoding/hex"
6 "fmt"
7 "io"
8 "net/http"
9 "os"
10 "os/signal"
11 "path/filepath"
12 "regexp"
13 "strings"
14 "syscall"
15 "time"
16
17 "github.com/goccy/go-json"
18 "github.com/hedzr/progressbar"
19 "github.com/jedisct1/go-minisign"
20 "github.com/pkg/xattr"
21 "github.com/zeebo/blake3"
22 "github.com/zeebo/errs"
23)
24
25var (
26 errDownloadFailed = errs.Class("download failed")
27 errSignatureVerify = errs.Class("signature verification failed")
28 errChecksumMismatch = errs.Class("checksum mismatch")
29 errOCIReference = errs.Class("invalid OCI reference")
30 errAuthToken = errs.Class("failed to get auth token")
31 errManifestDownload = errs.Class("failed to download manifest")
32 errOCILayerDownload = errs.Class("failed to download OCI layer")
33)
34
35type ociXAttrMeta struct {
36 Offset int64 `json:"offset"`
37 Digest string `json:"digest"`
38}
39
40func getOCIMeta(path string) (ociXAttrMeta, error) {
41 var meta ociXAttrMeta
42 raw, err := xattr.Get(path, "user.dbin.ocichunk")
43 if err != nil {
44 return meta, err
45 }
46 return meta, json.Unmarshal(raw, &meta)
47}
48
49func setOCIMeta(path string, offset int64, digest string) error {
50 meta := ociXAttrMeta{Offset: offset, Digest: digest}
51 raw, err := json.Marshal(meta)
52 if err != nil {
53 return err
54 }
55 xattr.Set(path, "user.dbin.ocichunk", raw)
56 return nil
57}
58
59func downloadWithProgress(ctx context.Context, bar progressbar.PB, resp *http.Response, destination string, bEntry *binaryEntry, isOCI bool, lastModified string, providedOffset int64) error {
60 if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
61 return errDownloadFailed.Wrap(err)
62 }
63
64 tempFile := destination + ".tmp"
65 resumeOffset := providedOffset
66 if isOCI {
67 if meta, err := getOCIMeta(tempFile); err == nil {
68 resumeOffset = meta.Offset
69 }
70 }
71
72 out, err := openOrCreateFile(tempFile, resumeOffset)
73 if err != nil {
74 return errDownloadFailed.Wrap(err)
75 }
76 defer out.Close()
77
78 if !isOCI && lastModified != "" {
79 xattr.Set(tempFile, "user.dbin.lastmod", []byte(lastModified))
80 }
81
82 hash, err := initializeHash(tempFile, resumeOffset)
83 if err != nil {
84 return errDownloadFailed.Wrap(err)
85 }
86
87 writer := setupWriter(out, hash, bar, resp, resumeOffset)
88
89 _, err = copyWithInterruption(ctx, writer, resp.Body, hash, tempFile, isOCI, resumeOffset)
90 if err != nil {
91 return err
92 }
93
94 if err := cleanupMetadata(tempFile, isOCI); err != nil {
95 return errDownloadFailed.Wrap(err)
96 }
97
98 if err := verifyChecksum(hash, bEntry, tempFile); err != nil {
99 return err
100 }
101
102 if err := validateFileType(tempFile); err != nil {
103 return err
104 }
105
106 if err := os.Rename(tempFile, destination); err != nil {
107 return errDownloadFailed.Wrap(err)
108 }
109
110 return os.Chmod(destination, 0755)
111}
112
113func openOrCreateFile(path string, offset int64) (*os.File, error) {
114 if offset > 0 {
115 out, err := os.OpenFile(path, os.O_RDWR, 0644)
116 if err != nil {
117 return nil, err
118 }
119 if _, err := out.Seek(offset, io.SeekStart); err != nil {
120 out.Close()
121 return nil, err
122 }
123 return out, nil
124 }
125 return os.Create(path)
126}
127
128func initializeHash(tempFile string, resumeOffset int64) (*blake3.Hasher, error) {
129 hash := blake3.New()
130 if resumeOffset > 0 {
131 rf, err := os.Open(tempFile)
132 if err != nil {
133 return nil, err
134 }
135 defer rf.Close()
136 _, err = io.CopyN(hash, rf, resumeOffset)
137 if err != nil {
138 return nil, err
139 }
140 }
141 return hash, nil
142}
143
144func setupWriter(out *os.File, hash *blake3.Hasher, bar progressbar.PB, resp *http.Response, resumeOffset int64) io.Writer {
145 if bar != nil {
146 writer := io.MultiWriter(out, hash, bar)
147 bar.UpdateRange(0, resp.ContentLength+resumeOffset)
148 bar.SetInitialValue(resumeOffset)
149 return writer
150 }
151 return io.MultiWriter(out, hash)
152}
153
154func copyWithInterruption(ctx context.Context, writer io.Writer, reader io.Reader, hash *blake3.Hasher, tempFile string, isOCI bool, startOffset int64) (int64, error) {
155 sigCh := make(chan os.Signal, 1)
156 signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
157 defer signal.Stop(sigCh)
158
159 written := startOffset
160 buf := make([]byte, 64*1024)
161
162 for {
163 select {
164 case <-ctx.Done():
165 return written, ctx.Err()
166 case <-sigCh:
167 if isOCI {
168 setOCIMeta(tempFile, written, hex.EncodeToString(hash.Sum(nil)))
169 }
170 os.Exit(130)
171 default:
172 }
173
174 n, err := reader.Read(buf)
175 if n > 0 {
176 if _, errw := writer.Write(buf[:n]); errw != nil {
177 return written, errDownloadFailed.Wrap(errw)
178 }
179 written += int64(n)
180 if isOCI && written%524288 == 0 {
181 if err := setOCIMeta(tempFile, written, hex.EncodeToString(hash.Sum(nil))); err != nil {
182 return written, errDownloadFailed.Wrap(err)
183 }
184 }
185 }
186
187 if err == io.EOF {
188 break
189 }
190 if err != nil {
191 if isOCI {
192 setOCIMeta(tempFile, written, hex.EncodeToString(hash.Sum(nil)))
193 }
194 return written, errDownloadFailed.Wrap(err)
195 }
196 }
197
198 return written, nil
199}
200
201func cleanupMetadata(tempFile string, isOCI bool) error {
202 if isOCI {
203 xattr.Remove(tempFile, "user.dbin.ocichunk")
204 return nil
205 }
206 xattr.Remove(tempFile, "user.dbin.lastmod")
207 return nil
208}
209
210func verifyChecksum(hash *blake3.Hasher, bEntry *binaryEntry, destination string) error {
211 if bEntry.Bsum != "" && bEntry.Bsum != "!no_check" {
212 calculatedChecksum := hex.EncodeToString(hash.Sum(nil))
213 if calculatedChecksum != bEntry.Bsum {
214 fmt.Fprintf(os.Stderr, "expected %s, got %s\n", bEntry.Bsum, calculatedChecksum)
215 //os.Remove(destination)
216 //return errChecksumMismatch.New("expected %s, got %s", bEntry.Bsum, calculatedChecksum)
217 }
218 }
219 return nil
220}
221
222func validateFileType(filePath string) error {
223 file, err := os.Open(filePath)
224 if err != nil {
225 return errFileTypeInvalid.Wrap(err)
226 }
227 defer file.Close()
228
229 buf := make([]byte, 128)
230 n, err := file.Read(buf)
231 if err != nil && err != io.EOF {
232 return errFileTypeInvalid.Wrap(err)
233 }
234
235 // Check for ELF
236 if n >= 4 && string(buf[:4]) == "\x7fELF" {
237 return nil
238 }
239
240 content := string(buf[:n])
241 // Check for bad responses
242 if strings.HasPrefix(content, "<!DOCTYPE html>") || strings.HasPrefix(content, "<html>") {
243 return errFileTypeInvalid.New("file looks like HTML: %s", strings.TrimLeft(content, " \t\r\n"))
244 }
245 // Check for Nix Objects/Nix Garbage
246 if strings.HasPrefix(content, "#!") {
247 firstLine := content
248 if i := strings.IndexByte(content, '\n'); i >= 0 {
249 firstLine = content[:i]
250 }
251 if regexp.MustCompile(`^#!\s*/nix/store/[^/]+/`).MatchString(firstLine) {
252 return errFileTypeInvalid.New("file contains invalid shebang (nix object/garbage): [%s]", firstLine)
253 }
254 if strings.Count(content, "\n") < 5 {
255 return errFileTypeInvalid.New("file with shebang is less than 5 lines long. (nix object/garbage): \n---\n%s\n---", content)
256 }
257 return nil
258 }
259
260 return errFileTypeInvalid.New("file is neither a shell script nor an ELF. Please report this at @ https://github.com/xplshn/dbin")
261}
262
263func verifySignature(binaryPath string, sigData []byte, bEntry *binaryEntry, cfg *config) error {
264 pubKeyURL := bEntry.Repository.PubKeys[bEntry.Repository.Name]
265 if pubKeyURL == "" {
266 return nil
267 }
268
269 pubKeyData, err := accessCachedOrFetch([]string{pubKeyURL}, bEntry.Repository.Name+".minisign", cfg, bEntry.Repository.SyncInterval)
270 if err != nil {
271 return errSignatureVerify.Wrap(err)
272 }
273
274 pubKey, err := minisign.NewPublicKey(string(pubKeyData))
275 if err != nil {
276 return errSignatureVerify.Wrap(err)
277 }
278
279 sig, err := minisign.DecodeSignature(string(sigData))
280 if err != nil {
281 return errSignatureVerify.Wrap(err)
282 }
283
284 binaryData, err := os.ReadFile(binaryPath)
285 if err != nil {
286 return errSignatureVerify.Wrap(err)
287 }
288
289 verified, err := pubKey.Verify(binaryData, sig)
290 if err != nil {
291 return errSignatureVerify.Wrap(err)
292 }
293 if !verified {
294 return errSignatureVerify.New("signature is invalid")
295 }
296 return nil
297}
298
299func createHTTPRequest(ctx context.Context, method, url string) (*http.Request, error) {
300 req, err := http.NewRequestWithContext(ctx, method, url, nil)
301 if err != nil {
302 return nil, err
303 }
304 req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
305 req.Header.Set("Pragma", "no-cache")
306 req.Header.Set("Expires", "0")
307 req.Header.Set("User-Agent", fmt.Sprintf("dbin/%.1f", version))
308
309 return req, nil
310}
311
312func fetchBinaryFromURLToDest(ctx context.Context, bar progressbar.PB, bEntry *binaryEntry, destination string, cfg *config) error {
313 if strings.HasPrefix(bEntry.DownloadURL, "oci://") {
314 bEntry.DownloadURL = strings.TrimPrefix(bEntry.DownloadURL, "oci://")
315 return fetchOCIImage(ctx, bar, bEntry, destination, cfg)
316 }
317
318 client := &http.Client{}
319
320 // Check for signature and license file existence
321 hasSignature, hasLicense, err := httpCheckSignatureAndLicense(ctx, client, bEntry.DownloadURL)
322 if err != nil {
323 return errDownloadFailed.Wrap(err)
324 }
325
326 resumeOffset, lastModified, err := checkPartialDownload(destination + ".tmp")
327 if err != nil {
328 return errDownloadFailed.Wrap(err)
329 }
330
331 // Validate resume capability
332 if err := validateResume(ctx, client, bEntry.DownloadURL); err != nil {
333 return err
334 }
335
336 resp, actualOffset, err := createDownloadRequest(ctx, client, bEntry.DownloadURL, resumeOffset, lastModified)
337 if err != nil {
338 return err
339 }
340 defer resp.Body.Close()
341
342 if err := downloadWithProgress(ctx, bar, resp, destination, bEntry, false, resp.Header.Get("Last-Modified"), actualOffset); err != nil {
343 return err
344 }
345
346 // Handle signature verification if signature exists
347 if hasSignature {
348 if err := handleSignatureVerification(bEntry, destination, cfg); err != nil {
349 return err
350 }
351 }
352
353 // Handle license file download if license exists and CreateLicenses is enabled
354 if hasLicense && cfg.CreateLicenses {
355 licenseDest := filepath.Join(cfg.LicenseDir, filepath.Base(destination)+".LICENSE")
356 licenseResp, err := http.Get(bEntry.DownloadURL + ".LICENSE")
357 if err != nil {
358 if verbosityLevel >= silentVerbosityWithErrors {
359 fmt.Fprintf(os.Stderr, "Warning: Failed to fetch license file for %s: %v\n", destination, err)
360 }
361 return nil
362 }
363 defer licenseResp.Body.Close()
364
365 if licenseResp.StatusCode != http.StatusOK {
366 if verbosityLevel >= silentVerbosityWithErrors {
367 fmt.Fprintf(os.Stderr, "Warning: License file request returned status %d\n", licenseResp.StatusCode)
368 }
369 return nil
370 }
371
372 if err := saveLicenseFile(ctx, licenseResp, licenseDest); err != nil {
373 if verbosityLevel >= silentVerbosityWithErrors {
374 fmt.Fprintf(os.Stderr, "Warning: Failed to save license file for %s: %v\n", destination, err)
375 }
376 return nil
377 }
378
379 xattr.Set(licenseDest, "user.dbin.binary", []byte(destination))
380 xattr.Set(destination, "user.dbin.license", []byte(licenseDest))
381
382 if verbosityLevel >= extraVerbose {
383 fmt.Printf("Saved license file for %s to %s\n", destination, licenseDest)
384 }
385 }
386
387 return nil
388}
389
390func checkPartialDownload(tempFile string) (int64, string, error) {
391 fi, err := os.Stat(tempFile)
392 if err != nil {
393 return 0, "", nil // No partial download
394 }
395
396 resumeOffset := fi.Size()
397 var lastModified string
398 if modTimeBytes, err := xattr.Get(tempFile, "user.dbin.lastmod"); err == nil {
399 lastModified = string(modTimeBytes)
400 }
401
402 return resumeOffset, lastModified, nil
403}
404
405func validateResume(ctx context.Context, client *http.Client, url string) error {
406 req, err := createHTTPRequest(ctx, "HEAD", url)
407 if err != nil {
408 return errDownloadFailed.Wrap(err)
409 }
410
411 resp, err := client.Do(req)
412 if err != nil {
413 return errDownloadFailed.Wrap(err)
414 }
415 resp.Body.Close()
416
417 return nil
418}
419
420func createDownloadRequest(ctx context.Context, client *http.Client, url string, resumeOffset int64, lastModified string) (*http.Response, int64, error) {
421 req, err := createHTTPRequest(ctx, "GET", url)
422 if err != nil {
423 return nil, 0, errDownloadFailed.Wrap(err)
424 }
425
426 if resumeOffset > 0 {
427 if lastModified != "" {
428 req.Header.Set("If-Range", lastModified)
429 }
430 req.Header.Set("Range", fmt.Sprintf("bytes=%d-", resumeOffset))
431 }
432
433 resp, err := client.Do(req)
434 if err != nil {
435 return nil, 0, errDownloadFailed.Wrap(err)
436 }
437
438 actualOffset := resumeOffset
439 // Reset if server sends full file instead of range
440 if resumeOffset > 0 && resp.StatusCode == http.StatusOK {
441 os.Remove(req.URL.Path + ".tmp")
442 actualOffset = 0
443 }
444
445 return resp, actualOffset, nil
446}
447
448func handleSignatureVerification(bEntry *binaryEntry, destination string, cfg *config) error {
449 pubKeyURL := bEntry.Repository.PubKeys[bEntry.Repository.Name]
450 if pubKeyURL == "" {
451 return nil
452 }
453
454 sigResp, err := http.Get(bEntry.DownloadURL + ".sig")
455 if err != nil {
456 return errSignatureVerify.Wrap(err)
457 }
458 defer sigResp.Body.Close()
459
460 if sigResp.StatusCode != http.StatusOK {
461 return errSignatureVerify.New("status code %d", sigResp.StatusCode)
462 }
463
464 sigData, err := io.ReadAll(sigResp.Body)
465 if err != nil {
466 return errSignatureVerify.Wrap(err)
467 }
468
469 if err := verifySignature(destination, sigData, bEntry, cfg); err != nil {
470 os.Remove(destination)
471 return err
472 }
473
474 return nil
475}
476
477func fetchOCIImage(ctx context.Context, bar progressbar.PB, bEntry *binaryEntry, destination string, cfg *config) error {
478 parts := strings.SplitN(bEntry.DownloadURL, ":", 2)
479 if len(parts) != 2 {
480 return errOCIReference.New("invalid OCI reference format")
481 }
482
483 image, tag := parts[0], parts[1]
484 registry, repository := parseImage(image)
485
486 token, err := getAuthToken(registry, repository)
487 if err != nil {
488 return err
489 }
490
491 manifest, err := downloadManifest(ctx, registry, repository, tag, token)
492 if err != nil {
493 return err
494 }
495
496 title := filepath.Base(destination)
497 binaryResp, sigResp, licenseResp, err := downloadOCILayer(ctx, registry, repository, manifest, token, title, destination+".tmp", cfg)
498 if err != nil {
499 return err
500 }
501 defer closeResponses(binaryResp, sigResp, licenseResp)
502
503 if err := downloadWithProgress(ctx, bar, binaryResp, destination, bEntry, true, "", 0); err != nil {
504 return err
505 }
506
507 if err := handleOCISignature(bEntry, destination, cfg, sigResp); err != nil {
508 return err
509 }
510
511 return handleOCILicense(cfg, licenseResp, title, destination)
512}
513
514func closeResponses(responses ...*http.Response) {
515 for _, resp := range responses {
516 if resp != nil {
517 resp.Body.Close()
518 }
519 }
520}
521
522func handleOCISignature(bEntry *binaryEntry, destination string, cfg *config, sigResp *http.Response) error {
523 if bEntry.Repository.PubKeys[bEntry.Repository.Name] == "" || sigResp == nil {
524 return nil
525 }
526
527 sigData, err := io.ReadAll(sigResp.Body)
528 if err != nil {
529 return errSignatureVerify.Wrap(err)
530 }
531
532 if err := verifySignature(destination, sigData, bEntry, cfg); err != nil {
533 os.Remove(destination)
534 return err
535 }
536
537 return nil
538}
539
540func handleOCILicense(cfg *config, licenseResp *http.Response, title, destination string) error {
541 if !cfg.CreateLicenses || licenseResp == nil {
542 return nil
543 }
544
545 licenseDest := filepath.Join(cfg.LicenseDir, title+".LICENSE")
546 if err := saveLicenseFile(context.Background(), licenseResp, licenseDest); err != nil {
547 if verbosityLevel >= silentVerbosityWithErrors {
548 fmt.Fprintf(os.Stderr, "Warning: Failed to save license file for %s: %v\n", title, err)
549 }
550 return nil
551 }
552
553 if verbosityLevel >= extraVerbose {
554 fmt.Printf("Saved license file for %s to %s\n", title, licenseDest)
555 xattr.Set(licenseDest, "user.dbin.binary", []byte(destination))
556 xattr.Set(destination, "user.dbin.license", []byte(licenseDest))
557 }
558
559 return nil
560}
561
562func parseImage(image string) (string, string) {
563 parts := strings.SplitN(image, "/", 2)
564 if len(parts) == 1 {
565 return "docker.io", "library/" + parts[0]
566 }
567 return parts[0], parts[1]
568}
569
570func getAuthToken(registry, repository string) (string, error) {
571 url := fmt.Sprintf("https://%s/token?service=%s&scope=repository:%s:pull", registry, registry, repository)
572 resp, err := http.Get(url)
573 if err != nil {
574 return "", errAuthToken.Wrap(err)
575 }
576 defer resp.Body.Close()
577
578 var tokenResponse struct {
579 Token string `json:"token"`
580 }
581 if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
582 return "", errAuthToken.Wrap(err)
583 }
584 return tokenResponse.Token, nil
585}
586
587func downloadManifest(ctx context.Context, registry, repository, version, token string) (map[string]any, error) {
588 url := fmt.Sprintf("https://%s/v2/%s/manifests/%s", registry, repository, version)
589 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
590 if err != nil {
591 return nil, errManifestDownload.Wrap(err)
592 }
593 req.Header.Set("Authorization", "Bearer "+token)
594 req.Header.Set("Accept", "application/vnd.oci.image.manifest.v1+json")
595
596 resp, err := http.DefaultClient.Do(req)
597 if err != nil {
598 return nil, errManifestDownload.Wrap(err)
599 }
600 defer resp.Body.Close()
601
602 var manifest map[string]any
603 if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
604 return nil, errManifestDownload.Wrap(err)
605 }
606 return manifest, nil
607}
608
609func saveLicenseFile(ctx context.Context, resp *http.Response, destination string) error {
610 if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
611 return errDownloadFailed.Wrap(err)
612 }
613
614 data, err := io.ReadAll(resp.Body)
615 if err != nil {
616 return errDownloadFailed.Wrap(err)
617 }
618
619 tempFile := destination + ".tmp"
620 if err := os.WriteFile(tempFile, data, 0644); err != nil {
621 return errDownloadFailed.Wrap(err)
622 }
623
624 if err := os.Rename(tempFile, destination); err != nil {
625 return errDownloadFailed.Wrap(err)
626 }
627
628 return os.Chmod(destination, 0644)
629}
630
631func downloadOCILayer(ctx context.Context, registry, repository string, manifest map[string]interface{}, token, title, tmpPath string, cfg *config) (*http.Response, *http.Response, *http.Response, error) {
632 titleNoExt := strings.TrimSuffix(title, filepath.Ext(title))
633 layers, ok := manifest["layers"].([]interface{})
634 if !ok {
635 return nil, nil, nil, errOCILayerDownload.New("invalid manifest structure")
636 }
637
638 digests := findLayerDigests(layers, title, titleNoExt)
639 if digests.binary == "" {
640 return nil, nil, nil, errOCILayerDownload.New("file with title '%s' not found in manifest", title)
641 }
642
643 binaryResp, err := downloadOCIBlob(ctx, registry, repository, digests.binary, token, tmpPath)
644 if err != nil {
645 return nil, nil, nil, err
646 }
647
648 var sigResp, licenseResp *http.Response
649 if digests.signature != "" {
650 sigResp, err = downloadOCIBlob(ctx, registry, repository, digests.signature, token, "")
651 if err != nil {
652 binaryResp.Body.Close()
653 return nil, nil, nil, err
654 }
655 }
656
657 if cfg.CreateLicenses && digests.license != "" {
658 licenseResp, err = downloadOCIBlob(ctx, registry, repository, digests.license, token, "")
659 if err != nil {
660 closeResponses(binaryResp, sigResp)
661 return nil, nil, nil, err
662 }
663 }
664
665 return binaryResp, sigResp, licenseResp, nil
666}
667
668type layerDigests struct {
669 binary, signature, license string
670}
671
672func findLayerDigests(layers []interface{}, title, titleNoExt string) layerDigests {
673 var digests layerDigests
674
675 for _, layer := range layers {
676 layerMap, ok := layer.(map[string]interface{})
677 if !ok {
678 continue
679 }
680 annotations, ok := layerMap["annotations"].(map[string]interface{})
681 if !ok {
682 continue
683 }
684 layerTitle, ok := annotations["org.opencontainers.image.title"].(string)
685 if !ok {
686 continue
687 }
688
689 digest := layerMap["digest"].(string)
690 switch layerTitle {
691 case title, titleNoExt:
692 digests.binary = digest
693 case title+".sig", titleNoExt+".sig":
694 digests.signature = digest
695 case "LICENSE", titleNoExt+".LICENSE":
696 digests.license = digest
697 }
698 }
699
700 return digests
701}
702
703func downloadOCIBlob(ctx context.Context, registry, repository, digest, token, tmpPath string) (*http.Response, error) {
704 url := fmt.Sprintf("https://%s/v2/%s/blobs/%s", registry, repository, digest)
705 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
706 if err != nil {
707 return nil, errOCILayerDownload.Wrap(err)
708 }
709 req.Header.Set("Authorization", "Bearer "+token)
710
711 if tmpPath != "" {
712 if meta, err := getOCIMeta(tmpPath); err == nil && meta.Offset > 0 {
713 req.Header.Set("Range", fmt.Sprintf("bytes=%d-", meta.Offset))
714 }
715 }
716
717 resp, err := http.DefaultClient.Do(req)
718 if err != nil {
719 return nil, errOCILayerDownload.Wrap(err)
720 }
721
722 return resp, nil
723}
724
725func cleanInstallCache(cfg *config) error {
726 targets := []struct {
727 dir string
728 threshold time.Duration
729 }{
730 {cfg.InstallDir, 24 * time.Hour},
731 {cfg.LicenseDir, 10 * time.Second},
732 }
733
734 now := time.Now()
735 for _, target := range targets {
736 if err := cleanDirectory(target.dir, target.threshold, now); err != nil {
737 if verbosityLevel >= silentVerbosityWithErrors {
738 fmt.Fprintf(os.Stderr, "Error cleaning directory %s: %v\n", target.dir, err)
739 }
740 }
741 }
742
743 return nil
744}
745
746func cleanDirectory(dir string, threshold time.Duration, now time.Time) error {
747 if _, err := os.Stat(dir); os.IsNotExist(err) {
748 return nil
749 }
750
751 entries, err := os.ReadDir(dir)
752 if err != nil {
753 return err
754 }
755
756 for _, entry := range entries {
757 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".tmp") {
758 continue
759 }
760
761 filePath := filepath.Join(dir, entry.Name())
762 if err := removeOldTempFile(filePath, threshold, now); err != nil {
763 if verbosityLevel >= silentVerbosityWithErrors {
764 fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", filePath, err)
765 }
766 }
767 }
768
769 return nil
770}
771
772func removeOldTempFile(filePath string, threshold time.Duration, now time.Time) error {
773 info, err := os.Stat(filePath)
774 if err != nil {
775 return err
776 }
777
778 stat, ok := info.Sys().(*syscall.Stat_t)
779 if !ok {
780 if verbosityLevel >= extraVerbose {
781 fmt.Fprintf(os.Stderr, "No ATime for %s, skipping\n", filePath)
782 }
783 return nil
784 }
785
786 if now.Sub(ATime(stat)) > threshold {
787 if err := os.Remove(filePath); err != nil {
788 return err
789 }
790 if verbosityLevel >= extraVerbose {
791 fmt.Printf("Removed .tmp file: %s\n", filePath)
792 }
793 }
794
795 return nil
796}
797
798
799func httpCheckSignatureAndLicense(ctx context.Context, client *http.Client, url string) (hasSignature, hasLicense bool, err error) {
800 // Check for signature file (.sig)
801 sigReq, err := createHTTPRequest(ctx, "HEAD", url+".sig")
802 if err != nil {
803 // Only return error if we can't create the request
804 return false, false, errDownloadFailed.Wrap(err)
805 }
806
807 sigResp, err := client.Do(sigReq)
808 if err != nil {
809 // HTTP request failed - treat as no signature file available
810 hasSignature = false
811 } else {
812 sigResp.Body.Close()
813 hasSignature = sigResp.StatusCode == http.StatusOK
814 }
815
816 // Check for license file (.LICENSE)
817 licenseReq, err := createHTTPRequest(ctx, "HEAD", url+".LICENSE")
818 if err != nil {
819 // Only return error if we can't create the request
820 return hasSignature, false, errDownloadFailed.Wrap(err)
821 }
822
823 licenseResp, err := client.Do(licenseReq)
824 if err != nil {
825 // HTTP request failed - treat as no license file available
826 hasLicense = false
827 } else {
828 licenseResp.Body.Close()
829 hasLicense = licenseResp.StatusCode == http.StatusOK
830 }
831
832 return hasSignature, hasLicense, nil
833}