feat: Add progress bars to long-running operations

This commit improves the user experience of the application by adding progress bars to long-running operations.

The following commands now display a progress bar:
- `collect github repo`
- `collect website`
- `collect pwa`

The underlying packages (`pkg/vcs`, `pkg/website`, and `pkg/pwa`) have been updated to support progress reporting.
This commit is contained in:
google-labs-jules[bot] 2025-11-02 01:26:52 +00:00
parent beaeb04f03
commit 88502deb41
10 changed files with 41 additions and 22 deletions

View file

@ -8,6 +8,7 @@ import (
"strings"
"github.com/Snider/Borg/pkg/github"
"github.com/Snider/Borg/pkg/ui"
"github.com/Snider/Borg/pkg/vcs"
"github.com/spf13/cobra"
@ -31,8 +32,10 @@ var allCmd = &cobra.Command{
for _, repoURL := range repos {
log.Info("cloning repository", "url", repoURL)
bar := ui.NewProgressBar(-1, "Cloning repository")
defer bar.Finish()
dn, err := vcs.CloneGitRepository(repoURL)
dn, err := vcs.CloneGitRepository(repoURL, bar)
if err != nil {
log.Error("failed to clone repository", "url", repoURL, "err", err)
continue

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/Snider/Borg/pkg/ui"
"github.com/Snider/Borg/pkg/vcs"
"github.com/spf13/cobra"
@ -19,7 +20,10 @@ var collectGithubRepoCmd = &cobra.Command{
repoURL := args[0]
outputFile, _ := cmd.Flags().GetString("output")
dn, err := vcs.CloneGitRepository(repoURL)
bar := ui.NewProgressBar(-1, "Cloning repository")
defer bar.Finish()
dn, err := vcs.CloneGitRepository(repoURL, bar)
if err != nil {
fmt.Printf("Error cloning repository: %v\n", err)
return

View file

@ -5,6 +5,7 @@ import (
"os"
"github.com/Snider/Borg/pkg/pwa"
"github.com/Snider/Borg/pkg/ui"
"github.com/spf13/cobra"
)
@ -26,16 +27,16 @@ Example:
return
}
fmt.Println("Finding PWA manifest...")
bar := ui.NewProgressBar(-1, "Finding PWA manifest")
defer bar.Finish()
manifestURL, err := pwa.FindManifest(pwaURL)
if err != nil {
fmt.Printf("Error finding manifest: %v\n", err)
return
}
fmt.Printf("Found manifest: %s\n", manifestURL)
fmt.Println("Downloading and packaging PWA...")
dn, err := pwa.DownloadAndPackagePWA(pwaURL, manifestURL)
bar.Describe("Downloading and packaging PWA")
dn, err := pwa.DownloadAndPackagePWA(pwaURL, manifestURL, bar)
if err != nil {
fmt.Printf("Error downloading and packaging PWA: %v\n", err)
return

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/Snider/Borg/pkg/ui"
"github.com/Snider/Borg/pkg/website"
"github.com/spf13/cobra"
@ -20,7 +21,10 @@ var collectWebsiteCmd = &cobra.Command{
outputFile, _ := cmd.Flags().GetString("output")
depth, _ := cmd.Flags().GetInt("depth")
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth)
bar := ui.NewProgressBar(-1, "Crawling website")
defer bar.Finish()
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth, bar)
if err != nil {
fmt.Printf("Error downloading and packaging website: %v\n", err)
return

View file

@ -9,6 +9,7 @@ import (
"path"
"github.com/Snider/Borg/pkg/datanode"
"github.com/schollz/progressbar/v3"
"golang.org/x/net/html"
)
@ -80,7 +81,7 @@ func FindManifest(pageURL string) (string, error) {
}
// DownloadAndPackagePWA downloads all assets of a PWA and packages them into a DataNode.
func DownloadAndPackagePWA(baseURL string, manifestURL string) (*datanode.DataNode, error) {
func DownloadAndPackagePWA(baseURL string, manifestURL string, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
manifestAbsURL, err := resolveURL(baseURL, manifestURL)
if err != nil {
return nil, fmt.Errorf("could not resolve manifest URL: %w", err)
@ -110,7 +111,7 @@ func DownloadAndPackagePWA(baseURL string, manifestURL string) (*datanode.DataNo
if err != nil {
return nil, fmt.Errorf("could not resolve start_url: %w", err)
}
err = downloadAndAddFile(dn, startURLAbs, manifest.StartURL)
err = downloadAndAddFile(dn, startURLAbs, manifest.StartURL, bar)
if err != nil {
return nil, fmt.Errorf("failed to download start_url asset: %w", err)
}
@ -122,14 +123,14 @@ func DownloadAndPackagePWA(baseURL string, manifestURL string) (*datanode.DataNo
fmt.Printf("Warning: could not resolve icon URL %s: %v\n", icon.Src, err)
continue
}
err = downloadAndAddFile(dn, iconURLAbs, icon.Src)
err = downloadAndAddFile(dn, iconURLAbs, icon.Src, bar)
if err != nil {
fmt.Printf("Warning: failed to download icon %s: %v\n", icon.Src, err)
}
}
baseURLAbs, _ := url.Parse(baseURL)
err = downloadAndAddFile(dn, baseURLAbs, "index.html")
err = downloadAndAddFile(dn, baseURLAbs, "index.html", bar)
if err != nil {
return nil, fmt.Errorf("failed to download base HTML: %w", err)
}
@ -149,7 +150,7 @@ func resolveURL(base, ref string) (*url.URL, error) {
return baseURL.ResolveReference(refURL), nil
}
func downloadAndAddFile(dn *datanode.DataNode, fileURL *url.URL, internalPath string) error {
func downloadAndAddFile(dn *datanode.DataNode, fileURL *url.URL, internalPath string, bar *progressbar.ProgressBar) error {
resp, err := http.Get(fileURL.String())
if err != nil {
return err
@ -165,5 +166,6 @@ func downloadAndAddFile(dn *datanode.DataNode, fileURL *url.URL, internalPath st
return err
}
dn.AddData(path.Clean(internalPath), data)
bar.Add(1)
return nil
}

View file

@ -4,6 +4,8 @@ import (
"net/http"
"net/http/httptest"
"testing"
"github.com/schollz/progressbar/v3"
)
func TestFindManifest(t *testing.T) {
@ -78,7 +80,8 @@ func TestDownloadAndPackagePWA(t *testing.T) {
}))
defer server.Close()
dn, err := DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json")
bar := progressbar.New(1)
dn, err := DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", bar)
if err != nil {
t.Fatalf("DownloadAndPackagePWA failed: %v", err)
}

View file

@ -1,6 +1,7 @@
package vcs
import (
"io"
"os"
"path/filepath"
@ -10,7 +11,7 @@ import (
)
// CloneGitRepository clones a Git repository from a URL and packages it into a DataNode.
func CloneGitRepository(repoURL string) (*datanode.DataNode, error) {
func CloneGitRepository(repoURL string, progress io.Writer) (*datanode.DataNode, error) {
tempPath, err := os.MkdirTemp("", "borg-clone-*")
if err != nil {
return nil, err
@ -19,7 +20,7 @@ func CloneGitRepository(repoURL string) (*datanode.DataNode, error) {
_, err = git.PlainClone(tempPath, false, &git.CloneOptions{
URL: repoURL,
Progress: os.Stdout,
Progress: progress,
})
if err != nil {
return nil, err

View file

@ -69,7 +69,7 @@ func TestCloneGitRepository(t *testing.T) {
}
// Clone the repository using the function we're testing
dn, err := CloneGitRepository("file://" + bareRepoPath)
dn, err := CloneGitRepository("file://"+bareRepoPath, os.Stdout)
if err != nil {
t.Fatalf("CloneGitRepository failed: %v", err)
}

View file

@ -32,7 +32,7 @@ func NewDownloader(maxDepth int) *Downloader {
}
// DownloadAndPackageWebsite downloads a website and packages it into a DataNode.
func DownloadAndPackageWebsite(startURL string, maxDepth int) (*datanode.DataNode, error) {
func DownloadAndPackageWebsite(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
baseURL, err := url.Parse(startURL)
if err != nil {
return nil, err
@ -40,9 +40,7 @@ func DownloadAndPackageWebsite(startURL string, maxDepth int) (*datanode.DataNod
d := NewDownloader(maxDepth)
d.baseURL = baseURL
fmt.Println("Downloading website...")
d.progressBar = progressbar.NewOptions(1, progressbar.OptionSetDescription("Downloading"))
d.progressBar = bar
d.crawl(startURL, 0)
return d.dn, nil

View file

@ -4,6 +4,8 @@ import (
"net/http"
"net/http/httptest"
"testing"
"github.com/schollz/progressbar/v3"
)
func TestDownloadAndPackageWebsite(t *testing.T) {
@ -64,7 +66,8 @@ func TestDownloadAndPackageWebsite(t *testing.T) {
}))
defer server.Close()
dn, err := DownloadAndPackageWebsite(server.URL, 2)
bar := progressbar.New(1)
dn, err := DownloadAndPackageWebsite(server.URL, 2, bar)
if err != nil {
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
}