diff --git a/cmd/all.go b/cmd/all.go index f411e1a..ac0a85e 100644 --- a/cmd/all.go +++ b/cmd/all.go @@ -11,7 +11,7 @@ import ( "github.com/Snider/Borg/pkg/compress" "github.com/Snider/Borg/pkg/datanode" "github.com/Snider/Borg/pkg/github" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" "github.com/Snider/Borg/pkg/ui" "github.com/Snider/Borg/pkg/vcs" "github.com/spf13/cobra" @@ -31,6 +31,10 @@ func NewAllCmd() *cobra.Command { format, _ := cmd.Flags().GetString("format") compression, _ := cmd.Flags().GetString("compression") + if format != "datanode" && format != "tim" { + return fmt.Errorf("invalid format: %s (must be 'datanode' or 'tim')", format) + } + owner, err := parseGithubOwner(url) if err != nil { return err @@ -98,12 +102,12 @@ func NewAllCmd() *cobra.Command { } var data []byte - if format == "matrix" { - matrix, err := matrix.FromDataNode(allDataNodes) + if format == "tim" { + t, err := tim.FromDataNode(allDataNodes) if err != nil { return fmt.Errorf("error creating matrix: %w", err) } - data, err = matrix.ToTar() + data, err = t.ToTar() if err != nil { return fmt.Errorf("error serializing matrix: %w", err) } @@ -119,6 +123,13 @@ func NewAllCmd() *cobra.Command { return fmt.Errorf("error compressing data: %w", err) } + if outputFile == "" { + outputFile = "all." + format + if compression != "none" { + outputFile += "." + compression + } + } + err = os.WriteFile(outputFile, compressedData, 0644) if err != nil { return fmt.Errorf("error writing DataNode to file: %w", err) @@ -129,8 +140,8 @@ func NewAllCmd() *cobra.Command { return nil }, } - allCmd.PersistentFlags().String("output", "all.dat", "Output file for the DataNode") - allCmd.PersistentFlags().String("format", "datanode", "Output format (datanode or matrix)") + allCmd.PersistentFlags().String("output", "", "Output file for the DataNode") + allCmd.PersistentFlags().String("format", "datanode", "Output format (datanode or tim)") allCmd.PersistentFlags().String("compression", "none", "Compression format (none, gz, or xz)") return allCmd } diff --git a/cmd/collect_github_repo.go b/cmd/collect_github_repo.go index 9fb5f84..02dfdb3 100644 --- a/cmd/collect_github_repo.go +++ b/cmd/collect_github_repo.go @@ -6,7 +6,7 @@ import ( "os" "github.com/Snider/Borg/pkg/compress" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" "github.com/Snider/Borg/pkg/ui" "github.com/Snider/Borg/pkg/vcs" @@ -35,8 +35,8 @@ func NewCollectGithubRepoCmd() *cobra.Command { format, _ := cmd.Flags().GetString("format") compression, _ := cmd.Flags().GetString("compression") - if format != "datanode" && format != "matrix" { - return fmt.Errorf("invalid format: %s (must be 'datanode' or 'matrix')", format) + if format != "datanode" && format != "tim" { + return fmt.Errorf("invalid format: %s (must be 'datanode' or 'tim')", format) } if compression != "none" && compression != "gz" && compression != "xz" { return fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression) @@ -58,12 +58,12 @@ func NewCollectGithubRepoCmd() *cobra.Command { } var data []byte - if format == "matrix" { - matrix, err := matrix.FromDataNode(dn) + if format == "tim" { + t, err := tim.FromDataNode(dn) if err != nil { return fmt.Errorf("error creating matrix: %w", err) } - data, err = matrix.ToTar() + data, err = t.ToTar() if err != nil { return fmt.Errorf("error serializing matrix: %w", err) } @@ -96,7 +96,7 @@ func NewCollectGithubRepoCmd() *cobra.Command { }, } cmd.Flags().String("output", "", "Output file for the DataNode") - cmd.Flags().String("format", "datanode", "Output format (datanode or matrix)") + cmd.Flags().String("format", "datanode", "Output format (datanode or tim)") cmd.Flags().String("compression", "none", "Compression format (none, gz, or xz)") return cmd } diff --git a/cmd/collect_pwa.go b/cmd/collect_pwa.go index 649516a..ee2fc6a 100644 --- a/cmd/collect_pwa.go +++ b/cmd/collect_pwa.go @@ -5,7 +5,7 @@ import ( "os" "github.com/Snider/Borg/pkg/compress" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" "github.com/Snider/Borg/pkg/pwa" "github.com/Snider/Borg/pkg/ui" @@ -45,7 +45,7 @@ Example: } c.Flags().String("uri", "", "The URI of the PWA to collect") c.Flags().String("output", "", "Output file for the DataNode") - c.Flags().String("format", "datanode", "Output format (datanode or matrix)") + c.Flags().String("format", "datanode", "Output format (datanode or tim)") c.Flags().String("compression", "none", "Compression format (none, gz, or xz)") return c } @@ -57,8 +57,8 @@ func CollectPWA(client pwa.PWAClient, pwaURL string, outputFile string, format s if pwaURL == "" { return "", fmt.Errorf("uri is required") } - if format != "datanode" && format != "matrix" { - return "", fmt.Errorf("invalid format: %s (must be 'datanode' or 'matrix')", format) + if format != "datanode" && format != "tim" { + return "", fmt.Errorf("invalid format: %s (must be 'datanode' or 'tim')", format) } if compression != "none" && compression != "gz" && compression != "xz" { return "", fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression) @@ -78,12 +78,12 @@ func CollectPWA(client pwa.PWAClient, pwaURL string, outputFile string, format s } var data []byte - if format == "matrix" { - matrix, err := matrix.FromDataNode(dn) + if format == "tim" { + t, err := tim.FromDataNode(dn) if err != nil { return "", fmt.Errorf("error creating matrix: %w", err) } - data, err = matrix.ToTar() + data, err = t.ToTar() if err != nil { return "", fmt.Errorf("error serializing matrix: %w", err) } diff --git a/cmd/collect_website.go b/cmd/collect_website.go index d6bf56e..75f5cd2 100644 --- a/cmd/collect_website.go +++ b/cmd/collect_website.go @@ -6,7 +6,7 @@ import ( "github.com/schollz/progressbar/v3" "github.com/Snider/Borg/pkg/compress" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" "github.com/Snider/Borg/pkg/ui" "github.com/Snider/Borg/pkg/website" @@ -37,6 +37,10 @@ func NewCollectWebsiteCmd() *cobra.Command { format, _ := cmd.Flags().GetString("format") compression, _ := cmd.Flags().GetString("compression") + if format != "datanode" && format != "tim" { + return fmt.Errorf("invalid format: %s (must be 'datanode' or 'tim')", format) + } + prompter := ui.NewNonInteractivePrompter(ui.GetWebsiteQuote) prompter.Start() defer prompter.Stop() @@ -51,12 +55,12 @@ func NewCollectWebsiteCmd() *cobra.Command { } var data []byte - if format == "matrix" { - matrix, err := matrix.FromDataNode(dn) + if format == "tim" { + t, err := tim.FromDataNode(dn) if err != nil { return fmt.Errorf("error creating matrix: %w", err) } - data, err = matrix.ToTar() + data, err = t.ToTar() if err != nil { return fmt.Errorf("error serializing matrix: %w", err) } @@ -90,7 +94,7 @@ func NewCollectWebsiteCmd() *cobra.Command { } collectWebsiteCmd.PersistentFlags().String("output", "", "Output file for the DataNode") collectWebsiteCmd.PersistentFlags().Int("depth", 2, "Recursion depth for downloading") - collectWebsiteCmd.PersistentFlags().String("format", "datanode", "Output format (datanode or matrix)") + collectWebsiteCmd.PersistentFlags().String("format", "datanode", "Output format (datanode or tim)") collectWebsiteCmd.PersistentFlags().String("compression", "none", "Compression format (none, gz, or xz)") return collectWebsiteCmd } diff --git a/cmd/compile.go b/cmd/compile.go index 1b01edd..f9f2b3e 100644 --- a/cmd/compile.go +++ b/cmd/compile.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" "github.com/spf13/cobra" ) @@ -17,14 +17,14 @@ var compileCmd = NewCompileCmd() func NewCompileCmd() *cobra.Command { compileCmd := &cobra.Command{ Use: "compile", - Short: "Compile a Borgfile into a Terminal Isolation Matrix.", + Short: "Compile a Borgfile into a TIM.", RunE: func(cmd *cobra.Command, args []string) error { content, err := os.ReadFile(borgfile) if err != nil { return err } - m, err := matrix.New() + m, err := tim.New() if err != nil { return err } @@ -61,7 +61,7 @@ func NewCompileCmd() *cobra.Command { }, } compileCmd.Flags().StringVarP(&borgfile, "file", "f", "Borgfile", "Path to the Borgfile.") - compileCmd.Flags().StringVarP(&output, "output", "o", "a.matrix", "Path to the output matrix file.") + compileCmd.Flags().StringVarP(&output, "output", "o", "a.tim", "Path to the output TIM file.") return compileCmd } diff --git a/cmd/run.go b/cmd/run.go index d57d1c9..e1069e6 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" "github.com/spf13/cobra" ) @@ -9,11 +9,11 @@ var runCmd = NewRunCmd() func NewRunCmd() *cobra.Command { return &cobra.Command{ - Use: "run [matrix file]", - Short: "Run a Terminal Isolation Matrix.", + Use: "run [tim file]", + Short: "Run a TIM.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return matrix.Run(args[0]) + return tim.Run(args[0]) }, } } diff --git a/cmd/run_test.go b/cmd/run_test.go index 051e561..dfe58cd 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -7,16 +7,16 @@ import ( "path/filepath" "testing" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" ) func TestRunCmd_Good(t *testing.T) { - // Create a dummy matrix file. - matrixPath := createDummyMatrix(t) + // Create a dummy tim file. + timPath := createDummyTIM(t) - // Mock the exec.Command function in the matrix package. - origExecCommand := matrix.ExecCommand - matrix.ExecCommand = func(command string, args ...string) *exec.Cmd { + // Mock the exec.Command function in the tim package. + origExecCommand := tim.ExecCommand + tim.ExecCommand = func(command string, args ...string) *exec.Cmd { cs := []string{"-test.run=TestHelperProcess", "--", command} cs = append(cs, args...) cmd := exec.Command(os.Args[0], cs...) @@ -24,13 +24,13 @@ func TestRunCmd_Good(t *testing.T) { return cmd } t.Cleanup(func() { - matrix.ExecCommand = origExecCommand + tim.ExecCommand = origExecCommand }) // Run the run command. rootCmd := NewRootCmd() rootCmd.AddCommand(GetRunCmd()) - _, err := executeCommand(rootCmd, "run", matrixPath) + _, err := executeCommand(rootCmd, "run", timPath) if err != nil { t.Fatalf("run command failed: %v", err) } @@ -41,7 +41,7 @@ func TestRunCmd_Bad(t *testing.T) { // Run the run command with a non-existent file. rootCmd := NewRootCmd() rootCmd.AddCommand(GetRunCmd()) - _, err := executeCommand(rootCmd, "run", "/non/existent/file.matrix") + _, err := executeCommand(rootCmd, "run", "/non/existent/file.tim") if err == nil { t.Fatal("run command should have failed but did not") } @@ -49,37 +49,37 @@ func TestRunCmd_Bad(t *testing.T) { } func TestRunCmd_Ugly(t *testing.T) { - t.Run("Invalid matrix file", func(t *testing.T) { - // Create an invalid (non-tar) matrix file. + t.Run("Invalid tim file", func(t *testing.T) { + // Create an invalid (non-tar) tim file. tempDir := t.TempDir() - matrixPath := filepath.Join(tempDir, "invalid.matrix") - err := os.WriteFile(matrixPath, []byte("this is not a tar file"), 0644) + timPath := filepath.Join(tempDir, "invalid.tim") + err := os.WriteFile(timPath, []byte("this is not a tar file"), 0644) if err != nil { - t.Fatalf("failed to create invalid matrix file: %v", err) + t.Fatalf("failed to create invalid tim file: %v", err) } // Run the run command. rootCmd := NewRootCmd() rootCmd.AddCommand(GetRunCmd()) - _, err = executeCommand(rootCmd, "run", matrixPath) + _, err = executeCommand(rootCmd, "run", timPath) if err == nil { t.Fatal("run command should have failed but did not") } }) } -// createDummyMatrix creates a valid, empty matrix file for testing. -func createDummyMatrix(t *testing.T) string { +// createDummyTIM creates a valid, empty tim file for testing. +func createDummyTIM(t *testing.T) string { t.Helper() tempDir := t.TempDir() - matrixPath := filepath.Join(tempDir, "test.matrix") - matrixFile, err := os.Create(matrixPath) + timPath := filepath.Join(tempDir, "test.tim") + timFile, err := os.Create(timPath) if err != nil { - t.Fatalf("failed to create dummy matrix file: %v", err) + t.Fatalf("failed to create dummy tim file: %v", err) } - defer matrixFile.Close() + defer timFile.Close() - tw := tar.NewWriter(matrixFile) + tw := tar.NewWriter(timFile) // Add a dummy config.json. This is not a valid config, but it's enough to test the run command. configContent := []byte(`{}`) @@ -108,5 +108,5 @@ func createDummyMatrix(t *testing.T) string { if err := tw.Close(); err != nil { t.Fatalf("failed to close tar writer: %v", err) } - return matrixPath + return timPath } diff --git a/examples/create_matrix_programmatically/main.go b/examples/create_matrix_programmatically/main.go index 64e2fe3..5b8ad2a 100644 --- a/examples/create_matrix_programmatically/main.go +++ b/examples/create_matrix_programmatically/main.go @@ -5,31 +5,31 @@ import ( "os" "github.com/Snider/Borg/pkg/datanode" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" ) func main() { // Create a new DataNode to hold the root filesystem. dn := datanode.New() - dn.AddData("hello.txt", []byte("Hello from within the matrix!")) + dn.AddData("hello.txt", []byte("Hello from within the TIM!")) // Create a new TerminalIsolationMatrix from the DataNode. - m, err := matrix.FromDataNode(dn) + m, err := tim.FromDataNode(dn) if err != nil { - log.Fatalf("Failed to create matrix: %v", err) + log.Fatalf("Failed to create TIM: %v", err) } - // Serialize the matrix to a tarball. + // Serialize the TIM to a tarball. tarball, err := m.ToTar() if err != nil { - log.Fatalf("Failed to serialize matrix to tar: %v", err) + log.Fatalf("Failed to serialize TIM to tar: %v", err) } // Write the tarball to a file. - err = os.WriteFile("programmatic.matrix", tarball, 0644) + err = os.WriteFile("programmatic.tim", tarball, 0644) if err != nil { - log.Fatalf("Failed to write matrix file: %v", err) + log.Fatalf("Failed to write TIM file: %v", err) } - log.Println("Successfully created programmatic.matrix") + log.Println("Successfully created programmatic.tim") } diff --git a/examples/run_matrix_programmatically/main.go b/examples/run_matrix_programmatically/main.go index 2204c6e..5ad2430 100644 --- a/examples/run_matrix_programmatically/main.go +++ b/examples/run_matrix_programmatically/main.go @@ -3,16 +3,16 @@ package main import ( "log" - "github.com/Snider/Borg/pkg/matrix" + "github.com/Snider/Borg/pkg/tim" ) func main() { - log.Println("Executing matrix with Borg...") + log.Println("Executing TIM with Borg...") - // Execute the matrix using the Borg package. - if err := matrix.Run("programmatic.matrix"); err != nil { - log.Fatalf("Failed to run matrix: %v", err) + // Execute the TIM using the Borg package. + if err := tim.Run("programmatic.tim"); err != nil { + log.Fatalf("Failed to run TIM: %v", err) } - log.Println("Matrix execution finished.") + log.Println("TIM execution finished.") } diff --git a/pkg/compress/compress.go b/pkg/compress/compress.go index 07e4d28..a104c61 100644 --- a/pkg/compress/compress.go +++ b/pkg/compress/compress.go @@ -8,7 +8,17 @@ import ( "github.com/ulikunitz/xz" ) -// Compress compresses data using the specified format. +// Compress compresses a byte slice using the specified format. +// Supported formats are "gz" and "xz". If an unsupported format is provided, +// the original data is returned unmodified. +// +// Example: +// +// compressedData, err := compress.Compress([]byte("hello world"), "gz") +// if err != nil { +// // handle error +// } +// // compressedData now holds the gzipped version of "hello world" func Compress(data []byte, format string) ([]byte, error) { var buf bytes.Buffer var writer io.WriteCloser @@ -39,7 +49,17 @@ func Compress(data []byte, format string) ([]byte, error) { return buf.Bytes(), nil } -// Decompress decompresses data, detecting the format automatically. +// Decompress decompresses a byte slice, automatically detecting the compression +// format (gz or xz) by inspecting the header magic bytes. If the data is not +// compressed in a recognized format, it is returned unmodified. +// +// Example: +// +// decompressedData, err := compress.Decompress(compressedData) +// if err != nil { +// // handle error +// } +// // decompressedData now holds the original uncompressed data func Decompress(data []byte) ([]byte, error) { // Check for gzip header if len(data) > 2 && data[0] == 0x1f && data[1] == 0x8b { diff --git a/pkg/datanode/datanode.go b/pkg/datanode/datanode.go index a793f02..46bc3c3 100644 --- a/pkg/datanode/datanode.go +++ b/pkg/datanode/datanode.go @@ -12,17 +12,37 @@ import ( "time" ) -// DataNode is an in-memory filesystem that is compatible with fs.FS. +// DataNode represents an in-memory filesystem, compatible with the standard +// library's io/fs.FS interface. It stores files and their contents in memory, +// making it useful for manipulating collections of files, such as those from +// a tar archive or a Git repository, without writing them to disk. type DataNode struct { files map[string]*dataFile } -// New creates a new, empty DataNode. +// New creates and returns a new, empty DataNode. This is the starting point +// for building an in-memory filesystem. +// +// Example: +// +// dn := datanode.New() func New() *DataNode { return &DataNode{files: make(map[string]*dataFile)} } -// FromTar creates a new DataNode from a tarball. +// FromTar creates a new DataNode by reading a tar archive. The tarball's +// contents are unpacked into the in-memory filesystem. +// +// Example: +// +// tarData, err := os.ReadFile("my-archive.tar") +// if err != nil { +// // handle error +// } +// dn, err := datanode.FromTar(tarData) +// if err != nil { +// // handle error +// } func FromTar(tarball []byte) (*DataNode, error) { dn := New() tarReader := tar.NewReader(bytes.NewReader(tarball)) @@ -48,7 +68,20 @@ func FromTar(tarball []byte) (*DataNode, error) { return dn, nil } -// ToTar serializes the DataNode to a tarball. +// ToTar serializes the DataNode into a tar archive. This is useful for +// saving the in-memory filesystem to disk or for transmitting it over a +// network. +// +// Example: +// +// tarData, err := dn.ToTar() +// if err != nil { +// // handle error +// } +// err = os.WriteFile("my-archive.tar", tarData, 0644) +// if err != nil { +// // handle error +// } func (d *DataNode) ToTar() ([]byte, error) { buf := new(bytes.Buffer) tw := tar.NewWriter(buf) @@ -75,7 +108,14 @@ func (d *DataNode) ToTar() ([]byte, error) { return buf.Bytes(), nil } -// AddData adds a file to the DataNode. +// AddData adds a file to the DataNode with the given name and content. If the +// file already exists, it will be overwritten. Directory paths are created +// implicitly and do not need to be added separately. +// +// Example: +// +// dn.AddData("my-file.txt", []byte("hello world")) +// dn.AddData("my-dir/my-other-file.txt", []byte("hello again")) func (d *DataNode) AddData(name string, content []byte) { name = strings.TrimPrefix(name, "/") if name == "" { @@ -93,7 +133,21 @@ func (d *DataNode) AddData(name string, content []byte) { } } -// Open opens a file from the DataNode. +// Open opens a file from the DataNode for reading. It returns an fs.File, +// which can be used with standard library functions that operate on files. +// This method is part of the fs.FS interface implementation. +// +// Example: +// +// file, err := dn.Open("my-file.txt") +// if err != nil { +// // handle error +// } +// defer file.Close() +// content, err := io.ReadAll(file) +// if err != nil { +// // handle error +// } func (d *DataNode) Open(name string) (fs.File, error) { name = strings.TrimPrefix(name, "/") if file, ok := d.files[name]; ok { @@ -112,7 +166,18 @@ func (d *DataNode) Open(name string) (fs.File, error) { return nil, fs.ErrNotExist } -// ReadDir reads and returns all directory entries for the named directory. +// ReadDir reads the named directory and returns a list of directory entries. +// This method is part of the fs.ReadDirFS interface implementation. +// +// Example: +// +// entries, err := dn.ReadDir("my-dir") +// if err != nil { +// // handle error +// } +// for _, entry := range entries { +// fmt.Println(entry.Name()) +// } func (d *DataNode) ReadDir(name string) ([]fs.DirEntry, error) { name = strings.TrimPrefix(name, "/") if name == "." { @@ -165,7 +230,16 @@ func (d *DataNode) ReadDir(name string) ([]fs.DirEntry, error) { return entries, nil } -// Stat returns the FileInfo structure describing file. +// Stat returns the fs.FileInfo structure describing the named file or directory. +// This method is part of the fs.StatFS interface implementation. +// +// Example: +// +// info, err := dn.Stat("my-file.txt") +// if err != nil { +// // handle error +// } +// fmt.Println(info.Size()) func (d *DataNode) Stat(name string) (fs.FileInfo, error) { name = strings.TrimPrefix(name, "/") if file, ok := d.files[name]; ok { @@ -185,12 +259,31 @@ func (d *DataNode) Stat(name string) (fs.FileInfo, error) { return nil, fs.ErrNotExist } -// ExistsOptions allows customizing the Exists check. +// ExistsOptions provides options for customizing the behavior of the Exists +// method. type ExistsOptions struct { + // WantType specifies the desired file type (e.g., fs.ModeDir for a + // directory). If the file exists but is not of the desired type, Exists + // will return false. WantType fs.FileMode } -// Exists returns true if the file or directory exists. +// Exists checks if a file or directory at the given path exists in the DataNode. +// It can optionally check if the file is of a specific type (e.g., a directory). +// +// Example: +// +// // Check if a file exists +// exists, err := dn.Exists("my-file.txt") +// if err != nil { +// // handle error +// } +// +// // Check if a directory exists +// exists, err = dn.Exists("my-dir", datanode.ExistsOptions{WantType: fs.ModeDir}) +// if err != nil { +// // handle error +// } func (d *DataNode) Exists(name string, opts ...ExistsOptions) (bool, error) { info, err := d.Stat(name) if err != nil { @@ -210,14 +303,30 @@ func (d *DataNode) Exists(name string, opts ...ExistsOptions) (bool, error) { return true, nil } -// WalkOptions allows customizing the Walk behavior. +// WalkOptions provides options for customizing the behavior of the Walk method. type WalkOptions struct { - MaxDepth int - Filter func(path string, d fs.DirEntry) bool + // MaxDepth limits the depth of the walk. A value of 0 means no limit. + MaxDepth int + // Filter is a function that can be used to skip files or directories. If + // the function returns false for an entry, that entry is skipped. If the + // entry is a directory, the entire subdirectory is skipped. + Filter func(path string, d fs.DirEntry) bool + // SkipErrors causes the walk to continue when an error is encountered. SkipErrors bool } -// Walk recursively descends the file tree rooted at root, calling fn for each file or directory. +// Walk walks the in-memory file tree rooted at root, calling fn for each file or +// directory in the tree, including root. The walk is depth-first. +// +// Example: +// +// err := dn.Walk(".", func(path string, d fs.DirEntry, err error) error { +// if err != nil { +// return err +// } +// fmt.Println(path) +// return nil +// }) func (d *DataNode) Walk(root string, fn fs.WalkDirFunc, opts ...WalkOptions) error { var maxDepth int var filter func(string, fs.DirEntry) bool @@ -270,7 +379,15 @@ func (d *DataNode) Walk(root string, fn fs.WalkDirFunc, opts ...WalkOptions) err }) } -// CopyFile copies a file from the DataNode to the local filesystem. +// CopyFile copies a file from the DataNode to a specified path on the local +// filesystem. +// +// Example: +// +// err := dn.CopyFile("my-file.txt", "/tmp/my-file.txt", 0644) +// if err != nil { +// // handle error +// } func (d *DataNode) CopyFile(sourcePath string, target string, perm os.FileMode) error { sourceFile, err := d.Open(sourcePath) if err != nil { diff --git a/pkg/github/github.go b/pkg/github/github.go index 2e2e832..e6a27e7 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -1,3 +1,4 @@ +// Package github provides a client for interacting with the GitHub API. package github import ( @@ -11,23 +12,40 @@ import ( "golang.org/x/oauth2" ) +// Repo represents a GitHub repository, containing the information needed to +// clone it. type Repo struct { + // CloneURL is the URL used to clone the repository. CloneURL string `json:"clone_url"` } -// GithubClient is an interface for interacting with the Github API. +// GithubClient defines the interface for interacting with the GitHub API. This +// allows for mocking the client in tests. type GithubClient interface { + // GetPublicRepos retrieves a list of all public repository clone URLs for a + // given user or organization. GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) } -// NewGithubClient creates a new GithubClient. +// NewGithubClient creates and returns a new GithubClient. +// +// Example: +// +// client := github.NewGithubClient() +// repos, err := client.GetPublicRepos(context.Background(), "my-org") +// if err != nil { +// // handle error +// } func NewGithubClient() GithubClient { return &githubClient{} } type githubClient struct{} -// NewAuthenticatedClient creates a new authenticated http client. +// NewAuthenticatedClient creates a new http.Client that authenticates with the +// GitHub API using a token from the GITHUB_TOKEN environment variable. If the +// variable is not set, it returns the default http.Client. This variable can +// be overridden in tests to provide a mock client. var NewAuthenticatedClient = func(ctx context.Context) *http.Client { token := os.Getenv("GITHUB_TOKEN") if token == "" { diff --git a/pkg/github/release.go b/pkg/github/release.go index 4aaa1ef..3a400f8 100644 --- a/pkg/github/release.go +++ b/pkg/github/release.go @@ -12,20 +12,31 @@ import ( ) var ( - // NewClient is a variable that holds the function to create a new GitHub client. - // This allows for mocking in tests. + // NewClient is a function that creates a new GitHub client. It is a + // variable to allow for mocking in tests. NewClient = func(httpClient *http.Client) *github.Client { return github.NewClient(httpClient) } - // NewRequest is a variable that holds the function to create a new HTTP request. + // NewRequest is a function that creates a new HTTP request. It is a + // variable to allow for mocking in tests. NewRequest = func(method, url string, body io.Reader) (*http.Request, error) { return http.NewRequest(method, url, body) } - // DefaultClient is the default http client + // DefaultClient is the default http client used for making requests. It is + // a variable to allow for mocking in tests. DefaultClient = &http.Client{} ) -// GetLatestRelease gets the latest release for a repository. +// GetLatestRelease fetches the latest release metadata for a given GitHub +// repository. +// +// Example: +// +// release, err := github.GetLatestRelease("my-org", "my-repo") +// if err != nil { +// // handle error +// } +// fmt.Println(release.GetTagName()) func GetLatestRelease(owner, repo string) (*github.RepositoryRelease, error) { client := NewClient(nil) release, _, err := client.Repositories.GetLatestRelease(context.Background(), owner, repo) @@ -35,7 +46,20 @@ func GetLatestRelease(owner, repo string) (*github.RepositoryRelease, error) { return release, nil } -// DownloadReleaseAsset downloads a release asset. +// DownloadReleaseAsset downloads the content of a release asset. +// +// Example: +// +// // Assuming 'release' is a *github.RepositoryRelease +// for _, asset := range release.Assets { +// if asset.GetName() == "my-asset.zip" { +// data, err := github.DownloadReleaseAsset(asset) +// if err != nil { +// // handle error +// } +// // do something with data +// } +// } func DownloadReleaseAsset(asset *github.ReleaseAsset) ([]byte, error) { req, err := NewRequest("GET", asset.GetBrowserDownloadURL(), nil) if err != nil { @@ -61,7 +85,16 @@ func DownloadReleaseAsset(asset *github.ReleaseAsset) ([]byte, error) { return buf.Bytes(), nil } -// ParseRepoFromURL parses the owner and repository from a GitHub URL. +// ParseRepoFromURL extracts the owner and repository name from a variety of +// GitHub URL formats, including HTTPS, Git, and SCP-style URLs. +// +// Example: +// +// owner, repo, err := github.ParseRepoFromURL("https://github.com/my-org/my-repo.git") +// if err != nil { +// // handle error +// } +// fmt.Println(owner, repo) // "my-org", "my-repo" func ParseRepoFromURL(u string) (owner, repo string, err error) { u = strings.TrimSuffix(u, ".git") diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 0dfc2d2..4ef7789 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,3 +1,4 @@ +// Package logger provides a simple configurable logger for the application. package logger import ( @@ -5,6 +6,19 @@ import ( "os" ) +// New creates a new slog.Logger. If verbose is true, the logger will be +// configured to show debug messages. Otherwise, it will only show info +// level and above. +// +// Example: +// +// // Create a standard logger +// log := logger.New(false) +// log.Info("This is an info message") +// +// // Create a verbose logger +// verboseLog := logger.New(true) +// verboseLog.Debug("This is a debug message") func New(verbose bool) *slog.Logger { level := slog.LevelInfo if verbose { diff --git a/pkg/mocks/http.go b/pkg/mocks/http.go index f47fe67..3c70e3c 100644 --- a/pkg/mocks/http.go +++ b/pkg/mocks/http.go @@ -1,3 +1,4 @@ +// Package mocks provides mock implementations of interfaces for testing purposes. package mocks import ( @@ -7,20 +8,28 @@ import ( "sync" ) -// MockRoundTripper is a mock implementation of http.RoundTripper. +// MockRoundTripper is a mock implementation of the http.RoundTripper interface, +// used for mocking HTTP clients in tests. It allows setting predefined responses +// for specific URLs. type MockRoundTripper struct { mu sync.RWMutex responses map[string]*http.Response } -// SetResponses sets the mock responses in a thread-safe way. +// SetResponses sets the mock responses for the MockRoundTripper in a thread-safe +// manner. The responses map keys are URLs and values are the http.Response +// objects to be returned for those URLs. func (m *MockRoundTripper) SetResponses(responses map[string]*http.Response) { m.mu.Lock() defer m.mu.Unlock() m.responses = responses } -// RoundTrip implements the http.RoundTripper interface. +// RoundTrip is the implementation of the http.RoundTripper interface. It looks +// up the request URL in the mock responses map and returns the corresponding +// response. If no response is found, it returns a 404 Not Found response. +// It performs a deep copy of the response to prevent race conditions on the +// response body. func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { url := req.URL.String() m.mu.RLock() @@ -64,7 +73,21 @@ func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) }, nil } -// NewMockClient creates a new http.Client with a MockRoundTripper. +// NewMockClient creates a new http.Client that uses the MockRoundTripper. This +// is a convenience function for creating a mock HTTP client for tests. The +// responses map is defensively copied to prevent race conditions. +// +// Example: +// +// mockResponses := map[string]*http.Response{ +// "https://example.com": { +// StatusCode: http.StatusOK, +// Body: io.NopCloser(bytes.NewBufferString("Hello")), +// }, +// } +// client := mocks.NewMockClient(mockResponses) +// resp, err := client.Get("https://example.com") +// // ... func NewMockClient(responses map[string]*http.Response) *http.Client { responsesCopy := make(map[string]*http.Response) if responses != nil { diff --git a/pkg/mocks/mock_vcs.go b/pkg/mocks/mock_vcs.go index 6c0890d..a393cf0 100644 --- a/pkg/mocks/mock_vcs.go +++ b/pkg/mocks/mock_vcs.go @@ -7,13 +7,26 @@ import ( "github.com/Snider/Borg/pkg/vcs" ) -// MockGitCloner is a mock implementation of the GitCloner interface. +// MockGitCloner is a mock implementation of the vcs.GitCloner interface, used +// for testing code that clones Git repositories. It allows setting a predefined +// DataNode and an error to be returned. type MockGitCloner struct { - DN *datanode.DataNode + // DN is the DataNode to be returned by CloneGitRepository. + DN *datanode.DataNode + // Err is the error to be returned by CloneGitRepository. Err error } -// NewMockGitCloner creates a new MockGitCloner. +// NewMockGitCloner creates a new MockGitCloner with the given DataNode and +// error. This is a convenience function for creating a mock Git cloner for +// tests. +// +// Example: +// +// mockDN := datanode.New() +// mockDN.AddData("file.txt", []byte("hello")) +// mockCloner := mocks.NewMockGitCloner(mockDN, nil) +// // use mockCloner in tests func NewMockGitCloner(dn *datanode.DataNode, err error) vcs.GitCloner { return &MockGitCloner{ DN: dn, @@ -21,7 +34,8 @@ func NewMockGitCloner(dn *datanode.DataNode, err error) vcs.GitCloner { } } -// CloneGitRepository mocks the cloning of a Git repository. +// CloneGitRepository is the mock implementation of the vcs.GitCloner interface. +// It returns the pre-configured DataNode and error. func (m *MockGitCloner) CloneGitRepository(repoURL string, progress io.Writer) (*datanode.DataNode, error) { return m.DN, m.Err } diff --git a/pkg/pwa/pwa.go b/pkg/pwa/pwa.go index 3272035..beda6cf 100644 --- a/pkg/pwa/pwa.go +++ b/pkg/pwa/pwa.go @@ -1,3 +1,5 @@ +// Package pwa provides functionality for discovering and downloading Progressive +// Web Application (PWA) assets. package pwa import ( @@ -14,13 +16,26 @@ import ( "golang.org/x/net/html" ) -// PWAClient is an interface for interacting with PWAs. +// PWAClient defines the interface for interacting with Progressive Web Apps. +// This allows for mocking the client in tests. type PWAClient interface { + // FindManifest discovers the web app manifest URL for a given PWA URL. FindManifest(pwaURL string) (string, error) + // DownloadAndPackagePWA downloads all the assets of a PWA, including the + // manifest, start URL, and icons, and packages them into a DataNode. DownloadAndPackagePWA(pwaURL, manifestURL string, bar *progressbar.ProgressBar) (*datanode.DataNode, error) } -// NewPWAClient creates a new PWAClient. +// NewPWAClient creates and returns a new PWAClient that uses a default +// http.Client. +// +// Example: +// +// client := pwa.NewPWAClient() +// manifestURL, err := client.FindManifest("https://example.com") +// if err != nil { +// // handle error +// } func NewPWAClient() PWAClient { return &pwaClient{client: http.DefaultClient} } @@ -29,7 +44,8 @@ type pwaClient struct { client *http.Client } -// FindManifest finds the manifest for a PWA. +// FindManifest discovers the web app manifest URL for a given PWA URL. It does +// this by fetching the PWA's HTML and looking for a tag. func (p *pwaClient) FindManifest(pwaURL string) (string, error) { resp, err := p.client.Get(pwaURL) if err != nil { @@ -83,7 +99,9 @@ func (p *pwaClient) FindManifest(pwaURL string) (string, error) { return resolvedURL.String(), nil } -// DownloadAndPackagePWA downloads and packages a PWA into a DataNode. +// DownloadAndPackagePWA downloads all the assets of a PWA, including the +// manifest, start URL, and icons, and packages them into a DataNode. It +// downloads the assets concurrently for performance. func (p *pwaClient) DownloadAndPackagePWA(pwaURL, manifestURL string, bar *progressbar.ProgressBar) (*datanode.DataNode, error) { dn := datanode.New() var wg sync.WaitGroup @@ -206,14 +224,28 @@ func (p *pwaClient) resolveURL(base, ref string) (*url.URL, error) { return baseURL.ResolveReference(refURL), nil } -// MockPWAClient is a mock implementation of the PWAClient interface. +// MockPWAClient is a mock implementation of the PWAClient interface, used for +// testing. It allows setting a predefined manifest URL, DataNode, and error to +// be returned by its methods. type MockPWAClient struct { + // ManifestURL is the manifest URL to be returned by FindManifest. ManifestURL string - DN *datanode.DataNode - Err error + // DN is the DataNode to be returned by DownloadAndPackagePWA. + DN *datanode.DataNode + // Err is the error to be returned by the mock methods. + Err error } -// NewMockPWAClient creates a new MockPWAClient. +// NewMockPWAClient creates a new MockPWAClient with the given manifest URL, +// DataNode, and error. This is a convenience function for creating a mock PWA +// client for tests. +// +// Example: +// +// mockDN := datanode.New() +// mockDN.AddData("manifest.json", []byte("{}")) +// mockClient := pwa.NewMockPWAClient("https://example.com/manifest.json", mockDN, nil) +// // use mockClient in tests func NewMockPWAClient(manifestURL string, dn *datanode.DataNode, err error) PWAClient { return &MockPWAClient{ ManifestURL: manifestURL, @@ -222,12 +254,14 @@ func NewMockPWAClient(manifestURL string, dn *datanode.DataNode, err error) PWAC } } -// FindManifest mocks the finding of a PWA manifest. +// FindManifest is the mock implementation of the PWAClient interface. It +// returns the pre-configured manifest URL and error. func (m *MockPWAClient) FindManifest(pwaURL string) (string, error) { return m.ManifestURL, m.Err } -// DownloadAndPackagePWA mocks the downloading and packaging of a PWA. +// DownloadAndPackagePWA is the mock implementation of the PWAClient interface. +// It returns the pre-configured DataNode and error. func (m *MockPWAClient) DownloadAndPackagePWA(pwaURL, manifestURL string, bar *progressbar.ProgressBar) (*datanode.DataNode, error) { return m.DN, m.Err } diff --git a/pkg/tarfs/tarfs.go b/pkg/tarfs/tarfs.go index 6abbee4..143ebb0 100644 --- a/pkg/tarfs/tarfs.go +++ b/pkg/tarfs/tarfs.go @@ -1,3 +1,6 @@ +// Package tarfs provides an http.FileSystem implementation that serves files +// from a tar archive. This is particularly useful for serving the contents of a +// .tim file's rootfs directly without unpacking it to disk. package tarfs import ( @@ -11,12 +14,28 @@ import ( "time" ) -// TarFS is a http.FileSystem that serves files from a tar archive. +// TarFS is an implementation of http.FileSystem that serves files from an +// in-memory tar archive. It specifically looks for files within a "rootfs/" +// directory in the archive, which is the convention used by .tim files. type TarFS struct { files map[string]*tarFile } -// New creates a new TarFS from a tar archive. +// New creates a new TarFS from a byte slice containing a tar archive. It +// parses the archive and stores the files from the "rootfs/" directory in +// memory. +// +// Example: +// +// tarData, err := os.ReadFile("my-archive.tar") +// if err != nil { +// // handle error +// } +// fs, err := tarfs.New(tarData) +// if err != nil { +// // handle error +// } +// http.Handle("/", http.FileServer(fs)) func New(data []byte) (*TarFS, error) { fs := &TarFS{ files: make(map[string]*tarFile), @@ -48,7 +67,8 @@ func New(data []byte) (*TarFS, error) { return fs, nil } -// Open opens a file from the tar archive. +// Open opens a file from the tar archive for reading. This is the implementation +// of the http.FileSystem interface. func (fs *TarFS) Open(name string) (http.File, error) { name = strings.TrimPrefix(name, "/") if file, ok := fs.files[name]; ok { diff --git a/pkg/matrix/config.go b/pkg/tim/config.go similarity index 89% rename from pkg/matrix/config.go rename to pkg/tim/config.go index 6ed7aa7..a491fc9 100644 --- a/pkg/matrix/config.go +++ b/pkg/tim/config.go @@ -1,10 +1,11 @@ -package matrix +package tim import ( "encoding/json" ) -// This is the default runc spec, generated by `runc spec`. +// DefaultConfigJSON is the default runc spec in JSON format, generated by +// `runc spec`. This is used as the base configuration for a new TIM. const DefaultConfigJSON = `{ "ociVersion": "1.2.1", "process": { @@ -179,7 +180,8 @@ const DefaultConfigJSON = `{ } }` -// defaultConfig returns the default runc spec. +// defaultConfigVar is a function variable that unmarshals the default runc spec +// from DefaultConfigJSON. It is a variable to allow for mocking in tests. var defaultConfigVar = func() (map[string]interface{}, error) { var spec map[string]interface{} err := json.Unmarshal([]byte(DefaultConfigJSON), &spec) diff --git a/pkg/matrix/run.go b/pkg/tim/run.go similarity index 62% rename from pkg/matrix/run.go rename to pkg/tim/run.go index 1d1d1f8..776a5ad 100644 --- a/pkg/matrix/run.go +++ b/pkg/tim/run.go @@ -1,4 +1,4 @@ -package matrix +package tim import ( "archive/tar" @@ -8,20 +8,25 @@ import ( "path/filepath" ) -// ExecCommand is a wrapper around exec.Command that can be overridden for testing. +// ExecCommand is a function variable that creates a new exec.Cmd. It is a +// variable to allow for mocking in tests. var ExecCommand = exec.Command -// Run executes a Terminal Isolation Matrix from a given path. -func Run(matrixPath string) error { - // Create a temporary directory to unpack the matrix file. +// Run unpacks and executes a TIM from a given tarball path +// using runc. It unpacks the bundle into a temporary directory, then executes it +// with "runc run". +// +// Note: This function requires "runc" to be installed and in the system's PATH. +func Run(timPath string) error { + // Create a temporary directory to unpack the TIM file. tempDir, err := os.MkdirTemp("", "borg-run-*") if err != nil { return err } defer os.RemoveAll(tempDir) - // Unpack the matrix file. - file, err := os.Open(matrixPath) + // Unpack the TIM file. + file, err := os.Open(timPath) if err != nil { return err } @@ -55,7 +60,7 @@ func Run(matrixPath string) error { } } - // Run the matrix. + // Run the TIM. runc := ExecCommand("runc", "run", "borg-container") runc.Dir = tempDir runc.Stdout = os.Stdout diff --git a/pkg/matrix/run_test.go b/pkg/tim/run_test.go similarity index 98% rename from pkg/matrix/run_test.go rename to pkg/tim/run_test.go index c76a03c..49feedb 100644 --- a/pkg/matrix/run_test.go +++ b/pkg/tim/run_test.go @@ -1,4 +1,4 @@ -package matrix +package tim import ( "fmt" diff --git a/pkg/matrix/matrix.go b/pkg/tim/tim.go similarity index 56% rename from pkg/matrix/matrix.go rename to pkg/tim/tim.go index a890974..531b2d2 100644 --- a/pkg/matrix/matrix.go +++ b/pkg/tim/tim.go @@ -1,4 +1,7 @@ -package matrix +// Package tim provides types and functions for creating and manipulating +// Terminal Isolation Matrix (.tim) files, which are runc-compatible container +// bundles. +package tim import ( "archive/tar" @@ -11,18 +14,32 @@ import ( ) var ( + // ErrDataNodeRequired is returned when a DataNode is required but not provided. ErrDataNodeRequired = errors.New("datanode is required") - ErrConfigIsNil = errors.New("config is nil") + // ErrConfigIsNil is returned when the config is nil. + ErrConfigIsNil = errors.New("config is nil") ) -// TerminalIsolationMatrix represents a runc bundle. -type TerminalIsolationMatrix struct { +// TIM represents a runc-compatible container bundle. It consists of a runc +// configuration file (config.json) and a root filesystem (rootfs). +type TIM struct { + // Config is the JSON representation of the runc configuration. Config []byte + // RootFS is an in-memory filesystem representing the container's root. RootFS *datanode.DataNode } -// New creates a new, empty TerminalIsolationMatrix. -func New() (*TerminalIsolationMatrix, error) { +// New creates a new, empty TIM with a default runc +// configuration. +// +// Example: +// +// m, err := tim.New() +// if err != nil { +// // handle error +// } +// m.RootFS.AddData("hello.txt", []byte("hello world")) +func New() (*TIM, error) { // Use the default runc spec as a starting point. // This can be customized later. spec, err := defaultConfigVar() @@ -35,14 +52,24 @@ func New() (*TerminalIsolationMatrix, error) { return nil, err } - return &TerminalIsolationMatrix{ + return &TIM{ Config: specBytes, RootFS: datanode.New(), }, nil } -// FromDataNode creates a new TerminalIsolationMatrix from a DataNode. -func FromDataNode(dn *datanode.DataNode) (*TerminalIsolationMatrix, error) { +// FromDataNode creates a new TIM using the provided DataNode +// as the root filesystem. It uses a default runc configuration. +// +// Example: +// +// dn := datanode.New() +// dn.AddData("my-file.txt", []byte("hello")) +// m, err := tim.FromDataNode(dn) +// if err != nil { +// // handle error +// } +func FromDataNode(dn *datanode.DataNode) (*TIM, error) { if dn == nil { return nil, ErrDataNodeRequired } @@ -54,8 +81,22 @@ func FromDataNode(dn *datanode.DataNode) (*TerminalIsolationMatrix, error) { return m, nil } -// ToTar serializes the TerminalIsolationMatrix to a tarball. -func (m *TerminalIsolationMatrix) ToTar() ([]byte, error) { +// ToTar serializes the TIM into a tar archive. The resulting +// tarball will contain a config.json file and a rootfs directory, making it +// compatible with runc. +// +// Example: +// +// // Assuming 'm' is a *tim.TIM +// tarData, err := m.ToTar() +// if err != nil { +// // handle error +// } +// err = os.WriteFile("my-bundle.tar", tarData, 0644) +// if err != nil { +// // handle error +// } +func (m *TIM) ToTar() ([]byte, error) { if m.Config == nil { return nil, ErrConfigIsNil } diff --git a/pkg/matrix/matrix_more_test.go b/pkg/tim/tim_more_test.go similarity index 98% rename from pkg/matrix/matrix_more_test.go rename to pkg/tim/tim_more_test.go index a8100ff..e23661e 100644 --- a/pkg/matrix/matrix_more_test.go +++ b/pkg/tim/tim_more_test.go @@ -1,4 +1,4 @@ -package matrix +package tim import ( "errors" diff --git a/pkg/matrix/matrix_test.go b/pkg/tim/tim_test.go similarity index 99% rename from pkg/matrix/matrix_test.go rename to pkg/tim/tim_test.go index bd4c917..1221579 100644 --- a/pkg/matrix/matrix_test.go +++ b/pkg/tim/tim_test.go @@ -1,4 +1,4 @@ -package matrix +package tim import ( "archive/tar" diff --git a/pkg/ui/data.go b/pkg/ui/data.go index 4c6937e..e6fa12d 100644 --- a/pkg/ui/data.go +++ b/pkg/ui/data.go @@ -2,5 +2,8 @@ package ui import "embed" +// QuotesJSON is an embedded filesystem containing the quotes.json file. +// This allows the quotes to be bundled directly into the application binary. +// //go:embed quotes.json var QuotesJSON embed.FS diff --git a/pkg/ui/non_interactive_prompter.go b/pkg/ui/non_interactive_prompter.go index 8144a72..e818de9 100644 --- a/pkg/ui/non_interactive_prompter.go +++ b/pkg/ui/non_interactive_prompter.go @@ -1,3 +1,5 @@ +// Package ui provides components for creating command-line user interfaces, +// including progress bars and non-interactive prompters. package ui import ( @@ -10,6 +12,10 @@ import ( "github.com/mattn/go-isatty" ) +// NonInteractivePrompter is used to display thematic quotes during long-running +// operations in non-interactive sessions (e.g., in a CI/CD pipeline). It can be +// started and stopped, and it will periodically print a quote from the provided +// quote function. type NonInteractivePrompter struct { stopChan chan struct{} quoteFunc func() (string, error) @@ -18,6 +24,15 @@ type NonInteractivePrompter struct { stopOnce sync.Once } +// NewNonInteractivePrompter creates a new NonInteractivePrompter with the given +// quote function. +// +// Example: +// +// prompter := ui.NewNonInteractivePrompter(ui.GetVCSQuote) +// prompter.Start() +// // ... long-running operation ... +// prompter.Stop() func NewNonInteractivePrompter(quoteFunc func() (string, error)) *NonInteractivePrompter { return &NonInteractivePrompter{ stopChan: make(chan struct{}), @@ -25,6 +40,8 @@ func NewNonInteractivePrompter(quoteFunc func() (string, error)) *NonInteractive } } +// Start begins the prompter, which will periodically print quotes to the console +// in non-interactive sessions. It is safe to call Start multiple times. func (p *NonInteractivePrompter) Start() { p.mu.Lock() if p.started { @@ -59,6 +76,7 @@ func (p *NonInteractivePrompter) Start() { }() } +// Stop halts the prompter. It is safe to call Stop multiple times. func (p *NonInteractivePrompter) Stop() { if p.IsInteractive() { return @@ -68,6 +86,8 @@ func (p *NonInteractivePrompter) Stop() { }) } +// IsInteractive checks if the current session is interactive (i.e., running in +// a terminal). func (p *NonInteractivePrompter) IsInteractive() bool { return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) } diff --git a/pkg/ui/progress_writer.go b/pkg/ui/progress_writer.go index b46b51b..8061a00 100644 --- a/pkg/ui/progress_writer.go +++ b/pkg/ui/progress_writer.go @@ -3,14 +3,21 @@ package ui import "github.com/schollz/progressbar/v3" +// progressWriter is an io.Writer that updates a progress bar's description +// with the written data. This is useful for showing the progress of operations +// that produce streaming text output, like git cloning. type progressWriter struct { bar *progressbar.ProgressBar } +// NewProgressWriter creates a new progressWriter that wraps the given +// progress bar. func NewProgressWriter(bar *progressbar.ProgressBar) *progressWriter { return &progressWriter{bar: bar} } +// Write implements the io.Writer interface. It updates the progress bar's +// description with the contents of the byte slice. func (pw *progressWriter) Write(p []byte) (n int, err error) { if pw == nil || pw.bar == nil { return len(p), nil diff --git a/pkg/ui/progressbar.go b/pkg/ui/progressbar.go index 8f143e1..15a8a7d 100644 --- a/pkg/ui/progressbar.go +++ b/pkg/ui/progressbar.go @@ -4,7 +4,16 @@ import ( "github.com/schollz/progressbar/v3" ) -// NewProgressBar creates a new progress bar with the specified total and description. +// NewProgressBar creates and returns a new progress bar with a standard +// set of options suitable for the application. +// +// Example: +// +// bar := ui.NewProgressBar(100, "Downloading files") +// for i := 0; i < 100; i++ { +// bar.Add(1) +// time.Sleep(10 * time.Millisecond) +// } func NewProgressBar(total int, description string) *progressbar.ProgressBar { return progressbar.NewOptions(total, progressbar.OptionSetDescription(description), diff --git a/pkg/ui/quote.go b/pkg/ui/quote.go index 166cf91..540777b 100644 --- a/pkg/ui/quote.go +++ b/pkg/ui/quote.go @@ -20,6 +20,7 @@ func init() { rand.Seed(time.Now().UnixNano()) } +// Quotes represents the structure of the quotes.json file. type Quotes struct { InitWorkAssimilate []string `json:"init_work_assimilate"` EncryptionServiceMessages []string `json:"encryption_service_messages"` @@ -61,6 +62,8 @@ func getQuotes() (*Quotes, error) { return cachedQuotes, quotesErr } +// GetRandomQuote selects and returns a random quote from all available quote +// categories. func GetRandomQuote() (string, error) { quotes, err := getQuotes() if err != nil { @@ -82,6 +85,7 @@ func GetRandomQuote() (string, error) { return allQuotes[rand.Intn(len(allQuotes))], nil } +// PrintQuote retrieves a random quote and prints it to the console in green. func PrintQuote() { quote, err := GetRandomQuote() if err != nil { @@ -92,6 +96,7 @@ func PrintQuote() { c.Println(quote) } +// GetVCSQuote returns a random quote specifically from the VCS processing category. func GetVCSQuote() (string, error) { quotes, err := getQuotes() if err != nil { @@ -103,6 +108,7 @@ func GetVCSQuote() (string, error) { return quotes.VCSProcessing[rand.Intn(len(quotes.VCSProcessing))], nil } +// GetPWAQuote returns a random quote specifically from the PWA processing category. func GetPWAQuote() (string, error) { quotes, err := getQuotes() if err != nil { @@ -114,6 +120,8 @@ func GetPWAQuote() (string, error) { return quotes.PWAProcessing[rand.Intn(len(quotes.PWAProcessing))], nil } +// GetWebsiteQuote returns a random quote specifically from the "code related long" +// category, which is used for website processing. func GetWebsiteQuote() (string, error) { quotes, err := getQuotes() if err != nil { diff --git a/pkg/vcs/git.go b/pkg/vcs/git.go index 92e20aa..0c0aa44 100644 --- a/pkg/vcs/git.go +++ b/pkg/vcs/git.go @@ -1,3 +1,5 @@ +// Package vcs provides functionality for interacting with version control +// systems, such as Git. package vcs import ( @@ -10,19 +12,33 @@ import ( "github.com/go-git/go-git/v5" ) -// GitCloner is an interface for cloning Git repositories. +// GitCloner defines the interface for cloning Git repositories. This allows for +// mocking the cloner in tests. type GitCloner interface { + // CloneGitRepository clones a Git repository from a URL and packages its + // contents into a DataNode. CloneGitRepository(repoURL string, progress io.Writer) (*datanode.DataNode, error) } -// NewGitCloner creates a new GitCloner. +// NewGitCloner creates and returns a new GitCloner. +// +// Example: +// +// cloner := vcs.NewGitCloner() +// dn, err := cloner.CloneGitRepository("https://github.com/example/repo.git", nil) +// if err != nil { +// // handle error +// } func NewGitCloner() GitCloner { return &gitCloner{} } type gitCloner struct{} -// CloneGitRepository clones a Git repository from a URL and packages it into a DataNode. +// CloneGitRepository clones a Git repository from a URL into a temporary +// directory, then packages the contents of the repository into a DataNode. +// The .git directory is excluded from the DataNode. If the repository is empty, +// an empty DataNode is returned. func (g *gitCloner) CloneGitRepository(repoURL string, progress io.Writer) (*datanode.DataNode, error) { tempPath, err := os.MkdirTemp("", "borg-clone-*") if err != nil { diff --git a/pkg/website/website.go b/pkg/website/website.go index b2bd517..2324b82 100644 --- a/pkg/website/website.go +++ b/pkg/website/website.go @@ -1,3 +1,5 @@ +// Package website provides functionality for recursively downloading a website's +// assets and packaging them into a DataNode. package website import ( @@ -13,9 +15,13 @@ import ( "golang.org/x/net/html" ) +// DownloadAndPackageWebsite is a function variable that can be overridden for +// testing. It defaults to the internal downloadAndPackageWebsite function. var DownloadAndPackageWebsite = downloadAndPackageWebsite -// Downloader is a recursive website downloader. +// Downloader is a recursive website downloader that stores the downloaded files +// in a DataNode. It keeps track of visited URLs to avoid infinite loops and +// respects a maximum crawl depth. type Downloader struct { baseURL *url.URL dn *datanode.DataNode @@ -26,12 +32,19 @@ type Downloader struct { errors []error } -// NewDownloader creates a new Downloader. +// NewDownloader creates a new Downloader with a default http.Client. +// +// Example: +// +// downloader := website.NewDownloader(2) +// // The downloader can be further configured before starting the crawl. func NewDownloader(maxDepth int) *Downloader { return NewDownloaderWithClient(maxDepth, http.DefaultClient) } // NewDownloaderWithClient creates a new Downloader with a custom http.Client. +// This is useful for testing or for configuring a client with specific +// transport settings. func NewDownloaderWithClient(maxDepth int, client *http.Client) *Downloader { return &Downloader{ dn: datanode.New(),