From e1664fcbc4685906d3dabc66bf947a17bce7efc0 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Giovanini" Date: Sat, 22 Jun 2024 16:30:47 +0200 Subject: feat: Add archive capability --- pkg/ext/mime.go | 11 ++++- pkg/git/git.go | 118 +++++++++++++++++++++++++++++++++++++++++++++ pkg/handler/git/handler.go | 36 ++++++++++++++ pkg/handler/router.go | 1 + pkg/service/git.go | 25 ++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) (limited to 'pkg') diff --git a/pkg/ext/mime.go b/pkg/ext/mime.go index 6da66e3..c42d4de 100644 --- a/pkg/ext/mime.go +++ b/pkg/ext/mime.go @@ -5,7 +5,8 @@ import "net/http" type ContentType = string const ( - TextHTML ContentType = "text/html" + TextHTML ContentType = "text/html" + ApplicationGZip ContentType = "application/gzip" ) func Html(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { @@ -14,9 +15,17 @@ func Html(next func(w http.ResponseWriter, r *http.Request)) func(w http.Respons } } +func SetFileName(w http.ResponseWriter, name string) { + h := "inline; filename=\"" + name + "\"" + w.Header().Add("Content-Disposition", h) +} + func SetHTML(w http.ResponseWriter) { SetMIME(w, TextHTML) +} +func SetGZip(w http.ResponseWriter) { + SetMIME(w, ApplicationGZip) } func SetMIME(w http.ResponseWriter, mime ContentType) { diff --git a/pkg/git/git.go b/pkg/git/git.go index b725cd8..591fafb 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -1,9 +1,13 @@ package git import ( + "archive/tar" "errors" "fmt" "io" + "io/fs" + "path" + "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" @@ -26,6 +30,13 @@ type ( // this is setRef when ref is setRef setRef bool } + infoWrapper struct { + name string + size int64 + mode fs.FileMode + modTime time.Time + isDir bool + } ) func OpenRepository(dir string) (*GitRepository, error) { @@ -213,3 +224,110 @@ func (g *GitRepository) FileContent(path string) (string, error) { return "Binary file", nil } } + +func (g *GitRepository) WriteTar(w io.Writer, prefix string) error { + tw := tar.NewWriter(w) + defer tw.Close() + + tree, err := g.Tree("") + if err != nil { + return err + } + + walker := object.NewTreeWalker(tree, true, nil) + defer walker.Close() + + name, entry, err := walker.Next() + for ; err == nil; name, entry, err = walker.Next() { + info, err := newInfoWrapper(name, prefix, &entry, tree) + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + + err = tw.WriteHeader(header) + if err != nil { + return err + } + + if !info.IsDir() { + c, err := g.FileContent(name) + if err != nil { + return err + } + + _, err = tw.Write([]byte(c)) + if err != nil { + return err + } + } + } + + return nil +} + +func newInfoWrapper( + filename string, + prefix string, + entry *object.TreeEntry, + tree *object.Tree, +) (*infoWrapper, error) { + var ( + size int64 + mode fs.FileMode + isDir bool + ) + + if entry.Mode.IsFile() { + file, err := tree.TreeEntryFile(entry) + if err != nil { + return nil, err + } + mode = fs.FileMode(file.Mode) + + size, err = tree.Size(filename) + if err != nil { + return nil, err + } + } else { + isDir = true + mode = fs.ModeDir | fs.ModePerm + } + + fullname := path.Join(prefix, filename) + return &infoWrapper{ + name: fullname, + size: size, + mode: mode, + modTime: time.Unix(0, 0), + isDir: isDir, + }, nil +} + +func (i *infoWrapper) Name() string { + return i.name +} + +func (i *infoWrapper) Size() int64 { + return i.size +} + +func (i *infoWrapper) Mode() fs.FileMode { + return i.mode +} + +func (i *infoWrapper) ModTime() time.Time { + return i.modTime +} + +func (i *infoWrapper) IsDir() bool { + return i.isDir +} + +func (i *infoWrapper) Sys() any { + return nil +} diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go index 25505ba..aed9917 100644 --- a/pkg/handler/git/handler.go +++ b/pkg/handler/git/handler.go @@ -2,10 +2,13 @@ package git import ( "bytes" + "fmt" "io" + "log/slog" "net/http" "os" "path/filepath" + "strings" "git.gabrielgio.me/cerrado/pkg/ext" "git.gabrielgio.me/cerrado/pkg/service" @@ -36,6 +39,7 @@ type ( GetAbout(name string) (string, error) ListTags(name string) ([]*plumbing.Reference, error) ListBranches(name string) ([]*plumbing.Reference, error) + WriteTarGZip(w io.Writer, name, ref, filename string) error } configurationRepository interface { @@ -84,6 +88,38 @@ func (g *GitHandler) List(w http.ResponseWriter, _ *http.Request) error { return nil } +func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error { + ext.SetGZip(w) + name := r.PathValue("name") + refs := r.PathValue("refs") + ref := strings.TrimSuffix(refs, ".tar.gz") + + // TODO: remove it once we can support more than gzip + if !strings.HasSuffix(refs, ".tar.gz") { + ext.NotFound(w) + return nil + } + + filenameWithExt := fmt.Sprintf("%s-%s.tar.gz", name, ref) + ext.SetFileName(w, filenameWithExt) + filename := fmt.Sprintf("%s-%s", name, ref) + + // writing to a buffer so we can run all the process before writing error + var buf bytes.Buffer + err := g.gitService.WriteTarGZip(&buf, name, ref, filename) + if err != nil { + return err + } + + // since that has write to w it cannot return a error. + _, err = io.Copy(w, &buf) + if err != nil { + slog.Error("Error copying buffer", "error", err) + } + + return nil +} + func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") diff --git a/pkg/handler/router.go b/pkg/handler/router.go index c8f8984..2293ab6 100644 --- a/pkg/handler/router.go +++ b/pkg/handler/router.go @@ -41,6 +41,7 @@ func MountHandler( mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree) mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob) mux.HandleFunc("/{name}/log/{ref}/", gitHandler.Log) + mux.HandleFunc("/{name}/archive/{refs...}", gitHandler.Archive) mux.HandleFunc("/config", configHandler) mux.HandleFunc("/about", aboutHandler.About) mux.HandleFunc("/", gitHandler.List) diff --git a/pkg/service/git.go b/pkg/service/git.go index 6bb6e9e..cbee90a 100644 --- a/pkg/service/git.go +++ b/pkg/service/git.go @@ -1,7 +1,9 @@ package service import ( + "compress/gzip" "errors" + "io" "log/slog" "git.gabrielgio.me/cerrado/pkg/config" @@ -92,6 +94,29 @@ func (g *GitService) ListCommits(name, ref string, count int) ([]*object.Commit, return repo.Commits(count) } +func (g *GitService) WriteTarGZip(w io.Writer, name, ref string, filename string) error { + r := g.configRepo.GetByName(name) + if r == nil { + return RepositoryNotFoundErr + } + + repo, err := git.OpenRepository(r.Path) + if err != nil { + return err + } + + err = repo.SetRef(ref) + if err != nil { + return err + } + + gw := gzip.NewWriter(w) + defer gw.Close() + + return repo.WriteTar(gw, filename) + +} + func (g *GitService) GetTree(name, ref, path string) (*object.Tree, error) { r := g.configRepo.GetByName(name) if r == nil { -- cgit v1.2.3