package fetcher import ( "crypto/sha256" "fmt" "io" "net/http" "os" "path/filepath" "github.com/mholt/archiver/v3" ) // Fetcher handles downloading and extracting sources type Fetcher struct { cacheDir string } // NewFetcher creates a new fetcher func NewFetcher(cacheDir string) *Fetcher { return &Fetcher{ cacheDir: cacheDir, } } // Download downloads a file from URL and verifies checksum func (f *Fetcher) Download(url, expectedHash string) (string, error) { // Validate input parameters if url == "" { return "", fmt.Errorf("URL cannot be empty") } if expectedHash == "" { return "", fmt.Errorf("expected hash cannot be empty") } filename := filepath.Base(url) if filename == "" { return "", fmt.Errorf("invalid URL: no filename found") } cachePath := filepath.Join(f.cacheDir, filename) // Check if already downloaded and valid if f.isValidCache(cachePath, expectedHash) { return cachePath, nil } // Create cache directory if it doesn't exist if err := os.MkdirAll(f.cacheDir, 0755); err != nil { return "", fmt.Errorf("failed to create cache directory: %w", err) } // Download file resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("failed to download %s: %w", url, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to download %s: HTTP %d", url, resp.StatusCode) } // Create temporary file tmpFile, err := os.CreateTemp(f.cacheDir, "download-*") if err != nil { return "", fmt.Errorf("failed to create temp file: %w", err) } defer tmpFile.Close() // Download to temp file hasher := sha256.New() if _, err := io.Copy(tmpFile, resp.Body); err != nil { return "", fmt.Errorf("failed to download file: %w", err) } // Verify checksum calculatedHash := fmt.Sprintf("%x", hasher.Sum(nil)) if calculatedHash != expectedHash { return "", fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHash, calculatedHash) } // Move temp file to final location if err := os.Rename(tmpFile.Name(), cachePath); err != nil { return "", fmt.Errorf("failed to move downloaded file: %w", err) } return cachePath, nil } // Extract extracts an archive to destination func (f *Fetcher) Extract(archivePath, destDir string) error { // Create destination directory if err := os.MkdirAll(destDir, 0755); err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } // Extract archive using archiver.Unarchive return archiver.Unarchive(archivePath, destDir) } // isValidCache checks if cached file exists and has correct checksum func (f *Fetcher) isValidCache(path, expectedHash string) bool { file, err := os.Open(path) if err != nil { return false } defer file.Close() hasher := sha256.New() if _, err := io.Copy(hasher, file); err != nil { return false } calculatedHash := fmt.Sprintf("%x", hasher.Sum(nil)) return calculatedHash == expectedHash } // ApplyPatches applies patch files to source directory func (f *Fetcher) ApplyPatches(sourceDir string, patchFiles []string) error { for _, patchFile := range patchFiles { if err := f.applyPatch(sourceDir, patchFile); err != nil { return fmt.Errorf("failed to apply patch %s: %w", patchFile, err) } } return nil } // applyPatch applies a single patch file func (f *Fetcher) applyPatch(sourceDir, patchFile string) error { // This is a simplified patch application // In a real implementation, you'd want to use the 'patch' command // For now, we'll just copy the patch file to the source directory patchDest := filepath.Join(sourceDir, filepath.Base(patchFile)) if err := copyFile(patchFile, patchDest); err != nil { return fmt.Errorf("failed to copy patch file: %w", err) } return nil } // copyFile copies a file from src to dst func copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(dst) if err != nil { return err } defer destFile.Close() _, err = io.Copy(destFile, sourceFile) return err } // DownloadAndExtract downloads and extracts source func (f *Fetcher) DownloadAndExtract(url, expectedHash, destDir string) error { archivePath, err := f.Download(url, expectedHash) if err != nil { return err } return f.Extract(archivePath, destDir) } // DownloadMultiple downloads multiple files concurrently func (f *Fetcher) DownloadMultiple(urls []string, hashes []string) ([]string, error) { if len(urls) != len(hashes) { return nil, fmt.Errorf("urls and hashes length mismatch") } paths := make([]string, len(urls)) for i, url := range urls { path, err := f.Download(url, hashes[i]) if err != nil { return nil, err } paths[i] = path } return paths, nil } // GetCacheDir returns the cache directory func (f *Fetcher) GetCacheDir() string { return f.cacheDir } // CleanCache removes old cached files func (f *Fetcher) CleanCache() error { return os.RemoveAll(f.cacheDir) } // ListCachedFiles lists all cached files func (f *Fetcher) ListCachedFiles() ([]string, error) { files, err := os.ReadDir(f.cacheDir) if err != nil { return nil, err } var result []string for _, file := range files { if !file.IsDir() { result = append(result, file.Name()) } } return result, nil }