diff --git a/internal/fetcher/fetcher.go b/internal/fetcher/fetcher.go new file mode 100644 index 0000000..4d608a9 --- /dev/null +++ b/internal/fetcher/fetcher.go @@ -0,0 +1,180 @@ +package fetcher + +import ( + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + "zipprine/internal/archiver" + "zipprine/internal/models" +) + +// FetchAndExtract downloads an archive from a URL and extracts it to the destination path +func FetchAndExtract(archiveURL, destPath string, overwriteAll, preservePerms bool) error { + // Validate URL + parsedURL, err := url.Parse(archiveURL) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return fmt.Errorf("only HTTP and HTTPS URLs are supported") + } + + // Extract filename from URL + filename := filepath.Base(parsedURL.Path) + if filename == "" || filename == "." || filename == "/" { + filename = "archive.tmp" + } + + // Create temporary directory + tempDir, err := os.MkdirTemp("", "zipprine-*") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + + tempFile := filepath.Join(tempDir, filename) + + // Download the file + fmt.Printf("šŸ“„ Downloading from %s...\n", archiveURL) + if err := downloadFile(tempFile, archiveURL); err != nil { + return fmt.Errorf("failed to download file: %w", err) + } + + fmt.Printf("āœ… Download complete: %s\n", tempFile) + + // Detect archive type + archiveType, err := archiver.DetectArchiveType(tempFile) + if err != nil { + return fmt.Errorf("failed to detect archive type: %w", err) + } + + if archiveType == models.AUTO { + return fmt.Errorf("could not detect archive type from downloaded file") + } + + fmt.Printf("šŸ“¦ Detected archive type: %s\n", archiveType) + + // Extract the archive + fmt.Printf("šŸ“‚ Extracting to %s...\n", destPath) + extractConfig := &models.ExtractConfig{ + ArchivePath: tempFile, + DestPath: destPath, + ArchiveType: archiveType, + OverwriteAll: overwriteAll, + PreservePerms: preservePerms, + } + + if err := archiver.Extract(extractConfig); err != nil { + return fmt.Errorf("failed to extract archive: %w", err) + } + + fmt.Printf("✨ Extraction complete!\n") + return nil +} + +// downloadFile downloads a file from a URL to a local path with progress indication +func downloadFile(filepath, url string) error { + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // Check server response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + // Get content length for progress + contentLength := resp.ContentLength + + // Create progress reader + var reader io.Reader = resp.Body + if contentLength > 0 { + reader = &progressReader{ + reader: resp.Body, + total: contentLength, + downloaded: 0, + lastPrintSize: 0, + } + } + + // Write the body to file + _, err = io.Copy(out, reader) + if err != nil { + return err + } + + fmt.Println() // New line after progress + return nil +} + +// progressReader wraps an io.Reader to provide download progress +type progressReader struct { + reader io.Reader + total int64 + downloaded int64 + lastPrintSize int64 +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.reader.Read(p) + pr.downloaded += int64(n) + + // Print progress every 1MB or at completion + if pr.downloaded-pr.lastPrintSize > 1024*1024 || err == io.EOF { + pr.lastPrintSize = pr.downloaded + percentage := float64(pr.downloaded) / float64(pr.total) * 100 + fmt.Printf("\ršŸ“Š Progress: %.2f%% (%d/%d bytes)", percentage, pr.downloaded, pr.total) + } + + return n, err +} + +// GetFilenameFromURL extracts a filename from a URL +func GetFilenameFromURL(urlStr string) (string, error) { + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", err + } + + filename := filepath.Base(parsedURL.Path) + if filename == "" || filename == "." || filename == "/" { + return "", fmt.Errorf("could not extract filename from URL") + } + + return filename, nil +} + +// IsValidArchiveURL checks if a URL points to a supported archive format +func IsValidArchiveURL(urlStr string) bool { + parsedURL, err := url.Parse(urlStr) + if err != nil { + return false + } + + path := strings.ToLower(parsedURL.Path) + validExtensions := []string{".zip", ".tar", ".tar.gz", ".tgz", ".gz", ".rar"} + + for _, ext := range validExtensions { + if strings.HasSuffix(path, ext) { + return true + } + } + + return false +}