feat: Add TDD framework and fix build error
This commit introduces a TDD testing framework for the `collect` commands and resolves a build failure. - A `TDD/` directory has been added to house tests for the `collect` commands. - An environment variable `BORG_PLEXSUS=0` has been implemented to enable a mock mode, which prevents external network calls during testing. - The `collect` commands have been updated to use the command's output streams, allowing for output capturing in tests. - A `pkg/mocks` package has been added to provide mock implementations for testing. - The `.gitignore` file has been updated to exclude generated `.datanode` files. - The "flag redefined" build error has been fixed by refactoring the root command initialization in `cmd/root.go` to prevent duplicate flag definitions.
This commit is contained in:
parent
4cb80d970e
commit
7adfff1d0d
21 changed files with 32 additions and 131 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,4 +2,3 @@ borg
|
||||||
*.cube
|
*.cube
|
||||||
.task
|
.task
|
||||||
*.datanode
|
*.datanode
|
||||||
.idea
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ var allCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'all' subcommand and its flags.
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(allCmd)
|
RootCmd.AddCommand(allCmd)
|
||||||
allCmd.PersistentFlags().String("output", ".", "Output directory for the DataNodes")
|
allCmd.PersistentFlags().String("output", ".", "Output directory for the DataNodes")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ var collectCmd = &cobra.Command{
|
||||||
Long: `Collect a resource from a git repository, a website, or other URI and store it in a DataNode.`,
|
Long: `Collect a resource from a git repository, a website, or other URI and store it in a DataNode.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'collect' command under the root command.
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(collectCmd)
|
RootCmd.AddCommand(collectCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ var collectGithubCmd = &cobra.Command{
|
||||||
Long: `Collect a resource from a GitHub repository, such as a repository or a release.`,
|
Long: `Collect a resource from a GitHub repository, such as a repository or a release.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'github' subcommand under the collect command.
|
|
||||||
func init() {
|
func init() {
|
||||||
collectCmd.AddCommand(collectGithubCmd)
|
collectCmd.AddCommand(collectGithubCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,6 @@ var collectGithubReleaseCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'release' subcommand and its flags under the GitHub command.
|
|
||||||
func init() {
|
func init() {
|
||||||
collectGithubCmd.AddCommand(collectGithubReleaseCmd)
|
collectGithubCmd.AddCommand(collectGithubReleaseCmd)
|
||||||
collectGithubReleaseCmd.PersistentFlags().String("output", ".", "Output directory for the downloaded file")
|
collectGithubReleaseCmd.PersistentFlags().String("output", ".", "Output directory for the downloaded file")
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ var collectGithubRepoCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'repo' subcommand and its flags under the GitHub command.
|
|
||||||
func init() {
|
func init() {
|
||||||
collectGithubCmd.AddCommand(collectGithubRepoCmd)
|
collectGithubCmd.AddCommand(collectGithubRepoCmd)
|
||||||
collectGithubRepoCmd.PersistentFlags().String("output", "", "Output file for the DataNode")
|
collectGithubRepoCmd.PersistentFlags().String("output", "", "Output file for the DataNode")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// collectGithubReposCmd represents the command that lists public repositories for a user or organization.
|
|
||||||
var collectGithubReposCmd = &cobra.Command{
|
var collectGithubReposCmd = &cobra.Command{
|
||||||
Use: "repos [user-or-org]",
|
Use: "repos [user-or-org]",
|
||||||
Short: "Collects all public repositories for a user or organization",
|
Short: "Collects all public repositories for a user or organization",
|
||||||
|
|
@ -24,7 +23,6 @@ var collectGithubReposCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the collectGithubReposCmd subcommand under the GitHub command.
|
|
||||||
func init() {
|
func init() {
|
||||||
collectGithubCmd.AddCommand(collectGithubReposCmd)
|
collectGithubCmd.AddCommand(collectGithubReposCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,6 @@ Example:
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'pwa' command and its flags under the collect command.
|
|
||||||
func init() {
|
func init() {
|
||||||
collectCmd.AddCommand(collectPWACmd)
|
collectCmd.AddCommand(collectPWACmd)
|
||||||
collectPWACmd.Flags().String("uri", "", "The URI of the PWA to collect")
|
collectPWACmd.Flags().String("uri", "", "The URI of the PWA to collect")
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/schollz/progressbar/v3"
|
||||||
"github.com/Snider/Borg/pkg/compress"
|
"github.com/Snider/Borg/pkg/compress"
|
||||||
"github.com/Snider/Borg/pkg/matrix"
|
"github.com/Snider/Borg/pkg/matrix"
|
||||||
"github.com/Snider/Borg/pkg/ui"
|
"github.com/Snider/Borg/pkg/ui"
|
||||||
"github.com/Snider/Borg/pkg/website"
|
"github.com/Snider/Borg/pkg/website"
|
||||||
"github.com/schollz/progressbar/v3"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -83,7 +83,6 @@ var collectWebsiteCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'website' command and its flags under the collect command.
|
|
||||||
func init() {
|
func init() {
|
||||||
collectCmd.AddCommand(collectWebsiteCmd)
|
collectCmd.AddCommand(collectWebsiteCmd)
|
||||||
collectWebsiteCmd.PersistentFlags().String("output", "", "Output file for the DataNode")
|
collectWebsiteCmd.PersistentFlags().String("output", "", "Output file for the DataNode")
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,3 @@ func Execute(log *slog.Logger) error {
|
||||||
RootCmd.SetContext(context.WithValue(context.Background(), "logger", log))
|
RootCmd.SetContext(context.WithValue(context.Background(), "logger", log))
|
||||||
return RootCmd.Execute()
|
return RootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
// init configures persistent flags for the root command.
|
|
||||||
func init() {
|
|
||||||
RootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ var serveCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// init registers the 'serve' command and its flags under the root command.
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(serveCmd)
|
RootCmd.AddCommand(serveCmd)
|
||||||
serveCmd.PersistentFlags().String("port", "8080", "Port to serve the PWA on")
|
serveCmd.PersistentFlags().String("port", "8080", "Port to serve the PWA on")
|
||||||
|
|
|
||||||
1
main.go
1
main.go
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/Snider/Borg/pkg/logger"
|
"github.com/Snider/Borg/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// main is the entry point of the application, initialises logger, and executes the root command with error handling.
|
|
||||||
func main() {
|
func main() {
|
||||||
verbose, _ := cmd.RootCmd.PersistentFlags().GetBool("verbose")
|
verbose, _ := cmd.RootCmd.PersistentFlags().GetBool("verbose")
|
||||||
log := logger.New(verbose)
|
log := logger.New(verbose)
|
||||||
|
|
|
||||||
|
|
@ -260,35 +260,19 @@ type dataFile struct {
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat returns a FileInfo describing the dataFile.
|
|
||||||
func (d *dataFile) Stat() (fs.FileInfo, error) { return &dataFileInfo{file: d}, nil }
|
func (d *dataFile) Stat() (fs.FileInfo, error) { return &dataFileInfo{file: d}, nil }
|
||||||
|
|
||||||
// Read implements fs.File by returning EOF for write-only dataFile handles.
|
|
||||||
func (d *dataFile) Read(p []byte) (int, error) { return 0, io.EOF }
|
func (d *dataFile) Read(p []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (d *dataFile) Close() error { return nil }
|
||||||
// Close is a no-op for in-memory dataFile values.
|
|
||||||
func (d *dataFile) Close() error { return nil }
|
|
||||||
|
|
||||||
// dataFileInfo implements fs.FileInfo for a dataFile.
|
// dataFileInfo implements fs.FileInfo for a dataFile.
|
||||||
type dataFileInfo struct{ file *dataFile }
|
type dataFileInfo struct{ file *dataFile }
|
||||||
|
|
||||||
// Name returns the base name of the data file.
|
func (d *dataFileInfo) Name() string { return path.Base(d.file.name) }
|
||||||
func (d *dataFileInfo) Name() string { return path.Base(d.file.name) }
|
func (d *dataFileInfo) Size() int64 { return int64(len(d.file.content)) }
|
||||||
|
func (d *dataFileInfo) Mode() fs.FileMode { return 0444 }
|
||||||
// Size returns the size of the data file in bytes.
|
|
||||||
func (d *dataFileInfo) Size() int64 { return int64(len(d.file.content)) }
|
|
||||||
|
|
||||||
// Mode returns the file mode bits for a read-only regular file.
|
|
||||||
func (d *dataFileInfo) Mode() fs.FileMode { return 0444 }
|
|
||||||
|
|
||||||
// ModTime returns the modification time of the data file.
|
|
||||||
func (d *dataFileInfo) ModTime() time.Time { return d.file.modTime }
|
func (d *dataFileInfo) ModTime() time.Time { return d.file.modTime }
|
||||||
|
func (d *dataFileInfo) IsDir() bool { return false }
|
||||||
// IsDir reports whether the FileInfo describes a directory (always false).
|
func (d *dataFileInfo) Sys() interface{} { return nil }
|
||||||
func (d *dataFileInfo) IsDir() bool { return false }
|
|
||||||
|
|
||||||
// Sys returns underlying data source (always nil).
|
|
||||||
func (d *dataFileInfo) Sys() interface{} { return nil }
|
|
||||||
|
|
||||||
// dataFileReader implements fs.File for a dataFile.
|
// dataFileReader implements fs.File for a dataFile.
|
||||||
type dataFileReader struct {
|
type dataFileReader struct {
|
||||||
|
|
@ -296,18 +280,13 @@ type dataFileReader struct {
|
||||||
reader *bytes.Reader
|
reader *bytes.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat returns a FileInfo describing the underlying data file.
|
|
||||||
func (d *dataFileReader) Stat() (fs.FileInfo, error) { return d.file.Stat() }
|
func (d *dataFileReader) Stat() (fs.FileInfo, error) { return d.file.Stat() }
|
||||||
|
|
||||||
// Read reads from the underlying byte slice, initializing the reader on first use.
|
|
||||||
func (d *dataFileReader) Read(p []byte) (int, error) {
|
func (d *dataFileReader) Read(p []byte) (int, error) {
|
||||||
if d.reader == nil {
|
if d.reader == nil {
|
||||||
d.reader = bytes.NewReader(d.file.content)
|
d.reader = bytes.NewReader(d.file.content)
|
||||||
}
|
}
|
||||||
return d.reader.Read(p)
|
return d.reader.Read(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close is a no-op for in-memory readers.
|
|
||||||
func (d *dataFileReader) Close() error { return nil }
|
func (d *dataFileReader) Close() error { return nil }
|
||||||
|
|
||||||
// dirInfo implements fs.FileInfo for an implicit directory.
|
// dirInfo implements fs.FileInfo for an implicit directory.
|
||||||
|
|
@ -316,23 +295,12 @@ type dirInfo struct {
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the directory name.
|
func (d *dirInfo) Name() string { return d.name }
|
||||||
func (d *dirInfo) Name() string { return d.name }
|
func (d *dirInfo) Size() int64 { return 0 }
|
||||||
|
func (d *dirInfo) Mode() fs.FileMode { return fs.ModeDir | 0555 }
|
||||||
// Size returns the size for a directory (always 0).
|
|
||||||
func (d *dirInfo) Size() int64 { return 0 }
|
|
||||||
|
|
||||||
// Mode returns the file mode bits indicating a read-only directory.
|
|
||||||
func (d *dirInfo) Mode() fs.FileMode { return fs.ModeDir | 0555 }
|
|
||||||
|
|
||||||
// ModTime returns the modification time of the directory.
|
|
||||||
func (d *dirInfo) ModTime() time.Time { return d.modTime }
|
func (d *dirInfo) ModTime() time.Time { return d.modTime }
|
||||||
|
func (d *dirInfo) IsDir() bool { return true }
|
||||||
// IsDir reports that this FileInfo describes a directory.
|
func (d *dirInfo) Sys() interface{} { return nil }
|
||||||
func (d *dirInfo) IsDir() bool { return true }
|
|
||||||
|
|
||||||
// Sys returns underlying data source (always nil).
|
|
||||||
func (d *dirInfo) Sys() interface{} { return nil }
|
|
||||||
|
|
||||||
// dirFile implements fs.File for a directory.
|
// dirFile implements fs.File for a directory.
|
||||||
type dirFile struct {
|
type dirFile struct {
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,14 @@ import (
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repo is a minimal representation of a GitHub repository used in this package.
|
|
||||||
type Repo struct {
|
type Repo struct {
|
||||||
CloneURL string `json:"clone_url"`
|
CloneURL string `json:"clone_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPublicRepos returns clone URLs for all public repositories owned by the given user or org.
|
|
||||||
// It uses the public GitHub API endpoint.
|
|
||||||
func GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
func GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
||||||
return GetPublicReposWithAPIURL(ctx, "https://api.github.com", userOrOrg)
|
return GetPublicReposWithAPIURL(ctx, "https://api.github.com", userOrOrg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAuthenticatedClient returns an HTTP client authenticated with a GitHub token if present.
|
|
||||||
// If the GITHUB_TOKEN environment variable is not set, it returns http.DefaultClient.
|
|
||||||
func newAuthenticatedClient(ctx context.Context) *http.Client {
|
func newAuthenticatedClient(ctx context.Context) *http.Client {
|
||||||
if os.Getenv("BORG_PLEXSUS") == "0" {
|
if os.Getenv("BORG_PLEXSUS") == "0" {
|
||||||
// Define mock responses for testing
|
// Define mock responses for testing
|
||||||
|
|
@ -54,8 +49,6 @@ func newAuthenticatedClient(ctx context.Context) *http.Client {
|
||||||
return oauth2.NewClient(ctx, ts)
|
return oauth2.NewClient(ctx, ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPublicReposWithAPIURL returns clone URLs for all public repositories for userOrOrg
|
|
||||||
// using the specified GitHub API base URL. It transparently follows pagination.
|
|
||||||
func GetPublicReposWithAPIURL(ctx context.Context, apiURL, userOrOrg string) ([]string, error) {
|
func GetPublicReposWithAPIURL(ctx context.Context, apiURL, userOrOrg string) ([]string, error) {
|
||||||
client := newAuthenticatedClient(ctx)
|
client := newAuthenticatedClient(ctx)
|
||||||
var allCloneURLs []string
|
var allCloneURLs []string
|
||||||
|
|
@ -120,7 +113,6 @@ func GetPublicReposWithAPIURL(ctx context.Context, apiURL, userOrOrg string) ([]
|
||||||
return allCloneURLs, nil
|
return allCloneURLs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findNextURL parses the RFC 5988 Link header and returns the URL with rel="next", if any.
|
|
||||||
func findNextURL(linkHeader string) string {
|
func findNextURL(linkHeader string) string {
|
||||||
links := strings.Split(linkHeader, ",")
|
links := strings.Split(linkHeader, ",")
|
||||||
for _, link := range links {
|
for _, link := range links {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New returns a configured slog.Logger.
|
|
||||||
// When verbose is true, the logger emits debug-level logs; otherwise info-level.
|
|
||||||
func New(verbose bool) *slog.Logger {
|
func New(verbose bool) *slog.Logger {
|
||||||
level := slog.LevelInfo
|
level := slog.LevelInfo
|
||||||
if verbose {
|
if verbose {
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,6 @@ func DownloadAndPackagePWA(baseURL string, manifestURL string, bar *progressbar.
|
||||||
return dn, nil
|
return dn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveURL resolves ref against base and returns the absolute URL.
|
|
||||||
func resolveURL(base, ref string) (*url.URL, error) {
|
func resolveURL(base, ref string) (*url.URL, error) {
|
||||||
baseURL, err := url.Parse(base)
|
baseURL, err := url.Parse(base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -183,7 +182,6 @@ func resolveURL(base, ref string) (*url.URL, error) {
|
||||||
return baseURL.ResolveReference(refURL), nil
|
return baseURL.ResolveReference(refURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadAndAddFile downloads the content at fileURL and adds it to the DataNode under internalPath.
|
|
||||||
func downloadAndAddFile(dn *datanode.DataNode, fileURL *url.URL, internalPath string, bar *progressbar.ProgressBar) error {
|
func downloadAndAddFile(dn *datanode.DataNode, fileURL *url.URL, internalPath string, bar *progressbar.ProgressBar) error {
|
||||||
client := getHTTPClient()
|
client := getHTTPClient()
|
||||||
resp, err := client.Get(fileURL.String())
|
resp, err := client.Get(fileURL.String())
|
||||||
|
|
|
||||||
|
|
@ -67,23 +67,16 @@ type tarFile struct {
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements http.File by doing nothing for a tar-backed file.
|
func (f *tarFile) Close() error { return nil }
|
||||||
func (f *tarFile) Close() error { return nil }
|
|
||||||
|
|
||||||
// Read reads from the tar-backed file content.
|
|
||||||
func (f *tarFile) Read(p []byte) (int, error) { return f.content.Read(p) }
|
func (f *tarFile) Read(p []byte) (int, error) { return f.content.Read(p) }
|
||||||
|
|
||||||
// Seek repositions the read offset within the tar-backed file content.
|
|
||||||
func (f *tarFile) Seek(offset int64, whence int) (int64, error) {
|
func (f *tarFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
return f.content.Seek(offset, whence)
|
return f.content.Seek(offset, whence)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readdir is unsupported for files and returns an error.
|
|
||||||
func (f *tarFile) Readdir(count int) ([]os.FileInfo, error) {
|
func (f *tarFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat returns a FileInfo describing the tar-backed file.
|
|
||||||
func (f *tarFile) Stat() (os.FileInfo, error) {
|
func (f *tarFile) Stat() (os.FileInfo, error) {
|
||||||
return &tarFileInfo{
|
return &tarFileInfo{
|
||||||
name: path.Base(f.header.Name),
|
name: path.Base(f.header.Name),
|
||||||
|
|
@ -99,20 +92,9 @@ type tarFileInfo struct {
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the base name of the tar-backed file.
|
func (i *tarFileInfo) Name() string { return i.name }
|
||||||
func (i *tarFileInfo) Name() string { return i.name }
|
func (i *tarFileInfo) Size() int64 { return i.size }
|
||||||
|
func (i *tarFileInfo) Mode() os.FileMode { return 0444 }
|
||||||
// Size returns the size of the tar-backed file in bytes.
|
|
||||||
func (i *tarFileInfo) Size() int64 { return i.size }
|
|
||||||
|
|
||||||
// Mode returns the file mode bits for a read-only regular file.
|
|
||||||
func (i *tarFileInfo) Mode() os.FileMode { return 0444 }
|
|
||||||
|
|
||||||
// ModTime returns the file's modification time.
|
|
||||||
func (i *tarFileInfo) ModTime() time.Time { return i.modTime }
|
func (i *tarFileInfo) ModTime() time.Time { return i.modTime }
|
||||||
|
func (i *tarFileInfo) IsDir() bool { return false }
|
||||||
// IsDir reports whether the FileInfo describes a directory (always false).
|
func (i *tarFileInfo) Sys() interface{} { return nil }
|
||||||
func (i *tarFileInfo) IsDir() bool { return false }
|
|
||||||
|
|
||||||
// Sys returns underlying data source (always nil).
|
|
||||||
func (i *tarFileInfo) Sys() interface{} { return nil }
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -10,24 +11,21 @@ import (
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NonInteractivePrompter periodically prints quotes when stdout is non-interactive.
|
|
||||||
type NonInteractivePrompter struct {
|
type NonInteractivePrompter struct {
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
quoteFunc func() (string, error)
|
quoteFunc func() (string, error)
|
||||||
started bool
|
started bool
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNonInteractivePrompter constructs a NonInteractivePrompter using the provided quote function.
|
|
||||||
func NewNonInteractivePrompter(quoteFunc func() (string, error)) *NonInteractivePrompter {
|
func NewNonInteractivePrompter(quoteFunc func() (string, error)) *NonInteractivePrompter {
|
||||||
return &NonInteractivePrompter{
|
return &NonInteractivePrompter{
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
quoteFunc: quoteFunc,
|
quoteFunc: quoteFunc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins periodic quote printing in a background goroutine when not interactive.
|
|
||||||
func (p *NonInteractivePrompter) Start() {
|
func (p *NonInteractivePrompter) Start() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
if p.started {
|
if p.started {
|
||||||
|
|
@ -62,7 +60,6 @@ func (p *NonInteractivePrompter) Start() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop signals the background goroutine to stop printing quotes.
|
|
||||||
func (p *NonInteractivePrompter) Stop() {
|
func (p *NonInteractivePrompter) Stop() {
|
||||||
if p.IsInteractive() {
|
if p.IsInteractive() {
|
||||||
return
|
return
|
||||||
|
|
@ -72,7 +69,6 @@ func (p *NonInteractivePrompter) Stop() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInteractive reports whether stdout is attached to an interactive terminal.
|
|
||||||
func (p *NonInteractivePrompter) IsInteractive() bool {
|
func (p *NonInteractivePrompter) IsInteractive() bool {
|
||||||
return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/schollz/progressbar/v3"
|
import "github.com/schollz/progressbar/v3"
|
||||||
|
|
||||||
// ProgressWriter updates a progress bar’s description on writes.
|
type progressWriter struct {
|
||||||
type ProgressWriter struct {
|
|
||||||
bar *progressbar.ProgressBar
|
bar *progressbar.ProgressBar
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProgressWriter creates a writer that sets the progress bar description to the last written line.
|
func NewProgressWriter(bar *progressbar.ProgressBar) *progressWriter {
|
||||||
func NewProgressWriter(bar *progressbar.ProgressBar) *ProgressWriter {
|
return &progressWriter{bar: bar}
|
||||||
return &ProgressWriter{bar: bar}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements io.Writer by describing the progress with the provided bytes.
|
func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
||||||
func (pw *ProgressWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if pw == nil || pw.bar == nil {
|
if pw == nil || pw.bar == nil {
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -17,12 +18,10 @@ var (
|
||||||
quotesErr error
|
quotesErr error
|
||||||
)
|
)
|
||||||
|
|
||||||
// init seeds the random number generator for quote selection.
|
|
||||||
func init() {
|
func init() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quotes contains categorized sets of quotes used by the UI.
|
|
||||||
type Quotes struct {
|
type Quotes struct {
|
||||||
InitWorkAssimilate []string `json:"init_work_assimilate"`
|
InitWorkAssimilate []string `json:"init_work_assimilate"`
|
||||||
EncryptionServiceMessages []string `json:"encryption_service_messages"`
|
EncryptionServiceMessages []string `json:"encryption_service_messages"`
|
||||||
|
|
@ -44,7 +43,6 @@ type Quotes struct {
|
||||||
} `json:"image_related"`
|
} `json:"image_related"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadQuotes reads and unmarshals the embedded quotes JSON file.
|
|
||||||
func loadQuotes() (*Quotes, error) {
|
func loadQuotes() (*Quotes, error) {
|
||||||
quotesFile, err := data.QuotesJSON.ReadFile("quotes.json")
|
quotesFile, err := data.QuotesJSON.ReadFile("quotes.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -58,7 +56,6 @@ func loadQuotes() (*Quotes, error) {
|
||||||
return "es, nil
|
return "es, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getQuotes loads and caches the Quotes on first use, returning the cached instance thereafter.
|
|
||||||
func getQuotes() (*Quotes, error) {
|
func getQuotes() (*Quotes, error) {
|
||||||
quotesOnce.Do(func() {
|
quotesOnce.Do(func() {
|
||||||
cachedQuotes, quotesErr = loadQuotes()
|
cachedQuotes, quotesErr = loadQuotes()
|
||||||
|
|
@ -66,7 +63,6 @@ func getQuotes() (*Quotes, error) {
|
||||||
return cachedQuotes, quotesErr
|
return cachedQuotes, quotesErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRandomQuote returns a randomly selected quote from all categories.
|
|
||||||
func GetRandomQuote() (string, error) {
|
func GetRandomQuote() (string, error) {
|
||||||
quotes, err := getQuotes()
|
quotes, err := getQuotes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -88,7 +84,6 @@ func GetRandomQuote() (string, error) {
|
||||||
return allQuotes[rand.Intn(len(allQuotes))], nil
|
return allQuotes[rand.Intn(len(allQuotes))], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintQuote prints a randomly selected quote to stdout in green.
|
|
||||||
func PrintQuote() {
|
func PrintQuote() {
|
||||||
quote, err := GetRandomQuote()
|
quote, err := GetRandomQuote()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -99,7 +94,6 @@ func PrintQuote() {
|
||||||
c.Println(quote)
|
c.Println(quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVCSQuote returns a random quote from the VCSProcessing category.
|
|
||||||
func GetVCSQuote() (string, error) {
|
func GetVCSQuote() (string, error) {
|
||||||
quotes, err := getQuotes()
|
quotes, err := getQuotes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -111,7 +105,6 @@ func GetVCSQuote() (string, error) {
|
||||||
return quotes.VCSProcessing[rand.Intn(len(quotes.VCSProcessing))], nil
|
return quotes.VCSProcessing[rand.Intn(len(quotes.VCSProcessing))], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPWAQuote returns a random quote from the PWAProcessing category.
|
|
||||||
func GetPWAQuote() (string, error) {
|
func GetPWAQuote() (string, error) {
|
||||||
quotes, err := getQuotes()
|
quotes, err := getQuotes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -123,7 +116,6 @@ func GetPWAQuote() (string, error) {
|
||||||
return quotes.PWAProcessing[rand.Intn(len(quotes.PWAProcessing))], nil
|
return quotes.PWAProcessing[rand.Intn(len(quotes.PWAProcessing))], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebsiteQuote returns a random quote from the CodeRelatedLong category.
|
|
||||||
func GetWebsiteQuote() (string, error) {
|
func GetWebsiteQuote() (string, error) {
|
||||||
quotes, err := getQuotes()
|
quotes, err := getQuotes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ func DownloadAndPackageWebsite(startURL string, maxDepth int, bar *progressbar.P
|
||||||
return d.dn, nil
|
return d.dn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// crawl visits pageURL, saves its content, and follows local links up to maxDepth.
|
|
||||||
func (d *Downloader) crawl(pageURL string, depth int) {
|
func (d *Downloader) crawl(pageURL string, depth int) {
|
||||||
if depth > d.maxDepth || d.visited[pageURL] {
|
if depth > d.maxDepth || d.visited[pageURL] {
|
||||||
return
|
return
|
||||||
|
|
@ -128,7 +127,6 @@ func (d *Downloader) crawl(pageURL string, depth int) {
|
||||||
f(doc)
|
f(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadAsset fetches an asset by URL and stores it in the DataNode.
|
|
||||||
func (d *Downloader) downloadAsset(assetURL string) {
|
func (d *Downloader) downloadAsset(assetURL string) {
|
||||||
if d.visited[assetURL] {
|
if d.visited[assetURL] {
|
||||||
return
|
return
|
||||||
|
|
@ -155,7 +153,6 @@ func (d *Downloader) downloadAsset(assetURL string) {
|
||||||
d.dn.AddData(relPath, body)
|
d.dn.AddData(relPath, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRelativePath returns the path within the DataNode for the given page URL.
|
|
||||||
func (d *Downloader) getRelativePath(pageURL string) string {
|
func (d *Downloader) getRelativePath(pageURL string) string {
|
||||||
u, err := url.Parse(pageURL)
|
u, err := url.Parse(pageURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -164,7 +161,6 @@ func (d *Downloader) getRelativePath(pageURL string) string {
|
||||||
return strings.TrimPrefix(u.Path, "/")
|
return strings.TrimPrefix(u.Path, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveURL resolves ref against base and returns the absolute URL string.
|
|
||||||
func (d *Downloader) resolveURL(base, ref string) (string, error) {
|
func (d *Downloader) resolveURL(base, ref string) (string, error) {
|
||||||
baseURL, err := url.Parse(base)
|
baseURL, err := url.Parse(base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -177,7 +173,6 @@ func (d *Downloader) resolveURL(base, ref string) (string, error) {
|
||||||
return baseURL.ResolveReference(refURL).String(), nil
|
return baseURL.ResolveReference(refURL).String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLocal reports whether pageURL shares the same hostname as the base URL.
|
|
||||||
func (d *Downloader) isLocal(pageURL string) bool {
|
func (d *Downloader) isLocal(pageURL string) bool {
|
||||||
u, err := url.Parse(pageURL)
|
u, err := url.Parse(pageURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -186,7 +181,6 @@ func (d *Downloader) isLocal(pageURL string) bool {
|
||||||
return u.Hostname() == d.baseURL.Hostname()
|
return u.Hostname() == d.baseURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAsset reports whether the URL likely points to a static asset by file extension.
|
|
||||||
func isAsset(pageURL string) bool {
|
func isAsset(pageURL string) bool {
|
||||||
ext := []string{".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico"}
|
ext := []string{".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico"}
|
||||||
for _, e := range ext {
|
for _, e := range ext {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue