diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..860cd15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +borg +*.cube diff --git a/cmd/all.go b/cmd/all.go new file mode 100644 index 0000000..dcb3dac --- /dev/null +++ b/cmd/all.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + "os" + + "borg-data-collector/pkg/borg" + "borg-data-collector/pkg/github" + "borg-data-collector/pkg/trix" + + "github.com/spf13/cobra" +) + +// allCmd represents the all command +var allCmd = &cobra.Command{ + Use: "all [user/org]", + Short: "Collect all public repositories from a user or organization", + Long: `Collect all public repositories from a user or organization and store them in a Trix cube.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(borg.GetRandomAssimilationMessage()) + + repos, err := github.GetPublicRepos(args[0]) + if err != nil { + fmt.Println(err) + return + } + + outputFile, _ := cmd.Flags().GetString("output") + + cube, err := trix.NewCube(outputFile) + if err != nil { + fmt.Println(err) + return + } + defer cube.Close() + + for _, repoURL := range repos { + fmt.Printf("Cloning %s...\n", repoURL) + + tempPath, err := os.MkdirTemp("", "borg-clone-*") + if err != nil { + fmt.Println(err) + return + } + defer os.RemoveAll(tempPath) + + err = addRepoToCube(repoURL, cube, tempPath) + if err != nil { + fmt.Printf("Error cloning %s: %s\n", repoURL, err) + continue + } + } + + fmt.Println(borg.GetRandomCodeLongMessage()) + }, +} + +func init() { + collectCmd.AddCommand(allCmd) +} diff --git a/cmd/cat.go b/cmd/cat.go new file mode 100644 index 0000000..9efce67 --- /dev/null +++ b/cmd/cat.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + "io" + "os" + + "borg-data-collector/pkg/trix" + + "github.com/spf13/cobra" +) + +// catCmd represents the cat command +var catCmd = &cobra.Command{ + Use: "cat [cube-file] [file-to-extract]", + Short: "Extract a file from a Trix cube", + Long: `Extract a file from a Trix cube and print its content to standard output.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + cubeFile := args[0] + fileToExtract := args[1] + + reader, file, err := trix.Extract(cubeFile) + if err != nil { + fmt.Println(err) + return + } + defer file.Close() + + for { + hdr, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + fmt.Println(err) + return + } + + if hdr.Name == fileToExtract { + if _, err := io.Copy(os.Stdout, reader); err != nil { + fmt.Println(err) + return + } + return + } + } + }, +} + +func init() { + rootCmd.AddCommand(catCmd) +} diff --git a/cmd/collect.go b/cmd/collect.go new file mode 100644 index 0000000..33fcbe9 --- /dev/null +++ b/cmd/collect.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "fmt" + + "borg-data-collector/pkg/trix" + + "github.com/spf13/cobra" +) + +// collectCmd represents the collect command +var collectCmd = &cobra.Command{ + Use: "collect [repository-url]", + Short: "Collect a single repository", + Long: `Collect a single repository and store it in a Trix cube.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + fmt.Println("Please provide a repository URL") + return + } + repoURL := args[0] + clonePath, _ := cmd.Flags().GetString("path") + outputFile, _ := cmd.Flags().GetString("output") + + cube, err := trix.NewCube(outputFile) + if err != nil { + fmt.Println(err) + return + } + defer cube.Close() + + err = addRepoToCube(repoURL, cube, clonePath) + if err != nil { + fmt.Println(err) + return + } + }, +} + +func init() { + rootCmd.AddCommand(collectCmd) + collectCmd.PersistentFlags().String("path", "/tmp/borg-clone", "Path to clone the repository") + collectCmd.PersistentFlags().String("output", "borg.cube", "Output file for the Trix cube") +} diff --git a/cmd/helpers.go b/cmd/helpers.go new file mode 100644 index 0000000..f318023 --- /dev/null +++ b/cmd/helpers.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "os" + "path/filepath" + + "borg-data-collector/pkg/trix" + + "github.com/go-git/go-git/v5" +) + +func addRepoToCube(repoURL string, cube *trix.Cube, clonePath string) error { + _, err := git.PlainClone(clonePath, false, &git.CloneOptions{ + URL: repoURL, + Progress: os.Stdout, + }) + + if err != nil { + return err + } + + err = filepath.Walk(clonePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + content, err := os.ReadFile(path) + if err != nil { + return err + } + relPath, err := filepath.Rel(clonePath, path) + if err != nil { + return err + } + cube.AddFile(relPath, content) + } + return nil + }) + + return err +} diff --git a/cmd/ingest.go b/cmd/ingest.go new file mode 100644 index 0000000..23a8f32 --- /dev/null +++ b/cmd/ingest.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + "os" + + "borg-data-collector/pkg/borg" + "borg-data-collector/pkg/trix" + + "github.com/spf13/cobra" +) + +// ingestCmd represents the ingest command +var ingestCmd = &cobra.Command{ + Use: "ingest [cube-file] [file-to-add]", + Short: "Add a file to a Trix cube", + Long: `Add a file to a Trix cube. If the cube file does not exist, it will be created.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + cubeFile := args[0] + fileToAdd := args[1] + + var cube *trix.Cube + var err error + + if _, err := os.Stat(cubeFile); os.IsNotExist(err) { + cube, err = trix.NewCube(cubeFile) + } else { + cube, err = trix.AppendToCube(cubeFile) + } + + if err != nil { + fmt.Println(err) + return + } + defer cube.Close() + + content, err := os.ReadFile(fileToAdd) + if err != nil { + fmt.Println(err) + return + } + + err = cube.AddFile(fileToAdd, content) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(borg.GetRandomCodeShortMessage()) + }, +} + +func init() { + rootCmd.AddCommand(ingestCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..d409cbe --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "borg-data-collector", + Short: "A tool for collecting and managing data.", + Long: `Borg Data Collector is a command-line tool for cloning Git repositories, +packaging their contents into a single file, and managing the data within.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.borg-data-collector.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d98196b --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module borg-data-collector + +go 1.24.3 + +require github.com/spf13/cobra v1.10.1 + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.3 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ae7851b --- /dev/null +++ b/go.sum @@ -0,0 +1,77 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= +github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/github/github.go b/pkg/github/github.go new file mode 100644 index 0000000..f28553b --- /dev/null +++ b/pkg/github/github.go @@ -0,0 +1,43 @@ +package github + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type Repo struct { + CloneURL string `json:"clone_url"` +} + +func GetPublicRepos(userOrOrg string) ([]string, error) { + resp, err := http.Get(fmt.Sprintf("https://api.github.com/users/%s/repos", userOrOrg)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // Try organization endpoint + resp, err = http.Get(fmt.Sprintf("https://api.github.com/orgs/%s/repos", userOrOrg)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch repos: %s", resp.Status) + } + } + + var repos []Repo + if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil { + return nil, err + } + + var cloneURLs []string + for _, repo := range repos { + cloneURLs = append(cloneURLs, repo.CloneURL) + } + + return cloneURLs, nil +} diff --git a/pkg/trix/trix.go b/pkg/trix/trix.go new file mode 100644 index 0000000..79f4fd2 --- /dev/null +++ b/pkg/trix/trix.go @@ -0,0 +1,63 @@ +package trix + +import ( + "archive/tar" + "os" +) + +type Cube struct { + writer *tar.Writer + file *os.File +} + +func NewCube(path string) (*Cube, error) { + file, err := os.Create(path) + if err != nil { + return nil, err + } + return &Cube{ + writer: tar.NewWriter(file), + file: file, + }, nil +} + +func (c *Cube) AddFile(path string, content []byte) error { + hdr := &tar.Header{ + Name: path, + Mode: 0600, + Size: int64(len(content)), + } + if err := c.writer.WriteHeader(hdr); err != nil { + return err + } + if _, err := c.writer.Write(content); err != nil { + return err + } + return nil +} + +func (c *Cube) Close() error { + if err := c.writer.Close(); err != nil { + return err + } + return c.file.Close() +} + +func Extract(path string) (*tar.Reader, *os.File, error) { + file, err := os.Open(path) + if err != nil { + return nil, nil, err + } + return tar.NewReader(file), file, nil +} + +func AppendToCube(path string) (*Cube, error) { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil, err + } + return &Cube{ + writer: tar.NewWriter(file), + file: file, + }, nil +}