Compare commits
2 commits
main
...
docs-and-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3acd7a94b1 | ||
|
|
38fafbf639 |
31 changed files with 582 additions and 150 deletions
23
cmd/all.go
23
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <link rel="manifest"> 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package matrix
|
||||
package tim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package matrix
|
||||
package tim
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package matrix
|
||||
package tim
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue