diff --git a/cmd/serve.go b/cmd/serve.go index 40dd400..531e719 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -4,8 +4,10 @@ import ( "fmt" "net/http" "os" + "strings" "github.com/Snider/Borg/pkg/datanode" + "github.com/Snider/Borg/pkg/tarfs" "github.com/spf13/cobra" ) @@ -17,22 +19,32 @@ var serveCmd = &cobra.Command{ Long: `Serves the contents of a packaged PWA file using a static file server.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - pwaFile := args[0] + dataFile := args[0] port, _ := cmd.Flags().GetString("port") - pwaData, err := os.ReadFile(pwaFile) + data, err := os.ReadFile(dataFile) if err != nil { - fmt.Printf("Error reading PWA file: %v\n", err) + fmt.Printf("Error reading data file: %v\n", err) return } - dn, err := datanode.FromTar(pwaData) - if err != nil { - fmt.Printf("Error creating DataNode from tarball: %v\n", err) - return + var fs http.FileSystem + if strings.HasSuffix(dataFile, ".matrix") { + fs, err = tarfs.New(data) + if err != nil { + fmt.Printf("Error creating TarFS from matrix tarball: %v\n", err) + return + } + } else { + dn, err := datanode.FromTar(data) + if err != nil { + fmt.Printf("Error creating DataNode from tarball: %v\n", err) + return + } + fs = http.FS(dn) } - http.Handle("/", http.FileServer(http.FS(dn))) + http.Handle("/", http.FileServer(fs)) fmt.Printf("Serving PWA on http://localhost:%s\n", port) err = http.ListenAndServe(":"+port, nil) diff --git a/docs/README.md b/docs/README.md index aea826a..fe82825 100644 --- a/docs/README.md +++ b/docs/README.md @@ -66,7 +66,7 @@ borg collect pwa [flags] ### `serve` -Serves the contents of a packaged DataNode file using a static file server. +Serves the contents of a packaged DataNode or Terminal Isolation Matrix file using a static file server. **Usage:** ``` @@ -78,7 +78,11 @@ borg serve [file] [flags] **Example:** ``` +# Serve a DataNode ./borg serve squoosh.dat --port 8888 + +# Serve a Terminal Isolation Matrix +./borg serve borg.matrix --port 9999 ``` ## Terminal Isolation Matrix diff --git a/examples/serve_matrix.sh b/examples/serve_matrix.sh new file mode 100755 index 0000000..2ecf613 --- /dev/null +++ b/examples/serve_matrix.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Example of using the 'borg serve' command with a .matrix file. + +# This script serves the contents of a .matrix file using a static file server. +# The main executable 'borg' is built from the project's root. +# Make sure you have built the project by running 'go build -o borg main.go' in the root directory. + +# First, create a .matrix file +./borg collect github repo https://github.com/Snider/Borg --output borg.matrix --format matrix + +# Then, serve it +./borg serve borg.matrix --port 9999 diff --git a/pkg/tarfs/tarfs.go b/pkg/tarfs/tarfs.go new file mode 100644 index 0000000..9a4b440 --- /dev/null +++ b/pkg/tarfs/tarfs.go @@ -0,0 +1,110 @@ +package tarfs + +import ( + "archive/tar" + "bytes" + "io" + "net/http" + "os" + "path" + "strings" + "time" +) + +// TarFS is a http.FileSystem that serves files from a tar archive. +type TarFS struct { + files map[string]*tar.Header + data []byte +} + +// New creates a new TarFS from a tar archive. +func New(data []byte) (*TarFS, error) { + fs := &TarFS{ + files: make(map[string]*tar.Header), + data: data, + } + + tr := tar.NewReader(bytes.NewReader(data)) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if strings.HasPrefix(hdr.Name, "rootfs/") { + fs.files[strings.TrimPrefix(hdr.Name, "rootfs/")] = hdr + } + } + + return fs, nil +} + +// Open opens a file from the tar archive. +func (fs *TarFS) Open(name string) (http.File, error) { + name = strings.TrimPrefix(name, "/") + if hdr, ok := fs.files[name]; ok { + // This is a bit inefficient, but it's the simplest way to + // get the file content without pre-indexing everything. + tr := tar.NewReader(bytes.NewReader(fs.data)) + for { + h, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if h.Name == hdr.Name { + return &tarFile{ + header: hdr, + content: tr, + modTime: hdr.ModTime, + }, nil + } + } + } + + return nil, os.ErrNotExist +} + +// tarFile is a http.File that represents a file in a tar archive. +type tarFile struct { + header *tar.Header + content io.Reader + modTime time.Time +} + +func (f *tarFile) Close() error { return nil } +func (f *tarFile) Read(p []byte) (int, error) { return f.content.Read(p) } +func (f *tarFile) Seek(offset int64, whence int) (int64, error) { + return 0, io.ErrUnexpectedEOF +} + +func (f *tarFile) Readdir(count int) ([]os.FileInfo, error) { + return nil, os.ErrInvalid +} + +func (f *tarFile) Stat() (os.FileInfo, error) { + return &tarFileInfo{ + name: path.Base(f.header.Name), + size: f.header.Size, + modTime: f.modTime, + }, nil +} + +// tarFileInfo is a os.FileInfo that represents a file in a tar archive. +type tarFileInfo struct { + name string + size int64 + modTime time.Time +} + +func (i *tarFileInfo) Name() string { return i.name } +func (i *tarFileInfo) Size() int64 { return i.size } +func (i *tarFileInfo) Mode() os.FileMode { return 0444 } +func (i *tarFileInfo) ModTime() time.Time { return i.modTime } +func (i *tarFileInfo) IsDir() bool { return false } +func (i *tarFileInfo) Sys() interface{} { return nil }