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 +++++++++ templates/gititemrefs.qtpl | 2 + templates/gititemrefs.qtpl.go | 90 +++++++++++++++++------------ templates/gititemsummary.qtpl | 1 + templates/gititemsummary.qtpl.go | 105 ++++++++++++++++++---------------- 9 files changed, 304 insertions(+), 85 deletions(-) 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 { diff --git a/templates/gititemrefs.qtpl b/templates/gititemrefs.qtpl index ff1561b..6244082 100644 --- a/templates/gititemrefs.qtpl +++ b/templates/gititemrefs.qtpl @@ -21,6 +21,7 @@ type GitItemRefsPage struct {
@@ -41,6 +42,7 @@ type GitItemRefsPage struct {
diff --git a/templates/gititemrefs.qtpl.go b/templates/gititemrefs.qtpl.go index b00736e..da9bfe7 100644 --- a/templates/gititemrefs.qtpl.go +++ b/templates/gititemrefs.qtpl.go @@ -90,113 +90,131 @@ func (g *GitItemRefsPage) StreamGitContent(qw422016 *qt422016.Writer, name, ref //line gititemrefs.qtpl:24 qw422016.E().S(name) //line gititemrefs.qtpl:24 - qw422016.N().S(`/tree/`) + qw422016.N().S(`/archive/`) //line gititemrefs.qtpl:24 qw422016.E().S(t.Name().Short()) //line gititemrefs.qtpl:24 - qw422016.N().S(`/">tree + qw422016.N().S(`.tar.gz">tar.gz tree + log
`) -//line gititemrefs.qtpl:29 +//line gititemrefs.qtpl:30 } -//line gititemrefs.qtpl:29 +//line gititemrefs.qtpl:30 qw422016.N().S(` `) -//line gititemrefs.qtpl:31 +//line gititemrefs.qtpl:32 } else { -//line gititemrefs.qtpl:31 +//line gititemrefs.qtpl:32 qw422016.N().S(`

No tags

`) -//line gititemrefs.qtpl:33 +//line gititemrefs.qtpl:34 } -//line gititemrefs.qtpl:33 +//line gititemrefs.qtpl:34 qw422016.N().S(`
`) -//line gititemrefs.qtpl:37 +//line gititemrefs.qtpl:38 for _, b := range g.Branches { -//line gititemrefs.qtpl:37 +//line gititemrefs.qtpl:38 qw422016.N().S(`
`) -//line gititemrefs.qtpl:40 +//line gititemrefs.qtpl:41 qw422016.E().S(b.Name().Short()) -//line gititemrefs.qtpl:40 +//line gititemrefs.qtpl:41 qw422016.N().S(`
`) -//line gititemrefs.qtpl:49 +//line gititemrefs.qtpl:51 } -//line gititemrefs.qtpl:49 +//line gititemrefs.qtpl:51 qw422016.N().S(`
`) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 } -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 func (g *GitItemRefsPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) { -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 qw422016 := qt422016.AcquireWriter(qq422016) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 g.StreamGitContent(qw422016, name, ref) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 qt422016.ReleaseWriter(qw422016) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 } -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 func (g *GitItemRefsPage) GitContent(name, ref string) string { -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 qb422016 := qt422016.AcquireByteBuffer() -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 g.WriteGitContent(qb422016, name, ref) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 qs422016 := string(qb422016.B) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 qt422016.ReleaseByteBuffer(qb422016) -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 return qs422016 -//line gititemrefs.qtpl:53 +//line gititemrefs.qtpl:55 } diff --git a/templates/gititemsummary.qtpl b/templates/gititemsummary.qtpl index e3d3a08..06a785a 100644 --- a/templates/gititemsummary.qtpl +++ b/templates/gititemsummary.qtpl @@ -23,6 +23,7 @@ type GitItemSummaryPage struct {
diff --git a/templates/gititemsummary.qtpl.go b/templates/gititemsummary.qtpl.go index ad025f7..4e48efd 100644 --- a/templates/gititemsummary.qtpl.go +++ b/templates/gititemsummary.qtpl.go @@ -94,83 +94,92 @@ func (g *GitItemSummaryPage) StreamGitContent(qw422016 *qt422016.Writer, name, r //line gititemsummary.qtpl:26 qw422016.E().S(name) //line gititemsummary.qtpl:26 - qw422016.N().S(`/tree/`) + qw422016.N().S(`/archive/`) //line gititemsummary.qtpl:26 qw422016.E().S(t.Name().Short()) //line gititemsummary.qtpl:26 - qw422016.N().S(`/">tree + qw422016.N().S(`.tar.gz">tar.gz tree + log
`) -//line gititemsummary.qtpl:31 +//line gititemsummary.qtpl:32 } -//line gititemsummary.qtpl:31 +//line gititemsummary.qtpl:32 qw422016.N().S(` `) -//line gititemsummary.qtpl:33 +//line gititemsummary.qtpl:34 } else { -//line gititemsummary.qtpl:33 +//line gititemsummary.qtpl:34 qw422016.N().S(`

No tags

`) -//line gititemsummary.qtpl:35 +//line gititemsummary.qtpl:36 } -//line gititemsummary.qtpl:35 +//line gititemsummary.qtpl:36 qw422016.N().S(`
`) -//line gititemsummary.qtpl:39 +//line gititemsummary.qtpl:40 for _, b := range g.Branches { -//line gititemsummary.qtpl:39 +//line gititemsummary.qtpl:40 qw422016.N().S(`
`) -//line gititemsummary.qtpl:42 +//line gititemsummary.qtpl:43 qw422016.E().S(b.Name().Short()) -//line gititemsummary.qtpl:42 +//line gititemsummary.qtpl:43 qw422016.N().S(`
`) -//line gititemsummary.qtpl:51 +//line gititemsummary.qtpl:52 } -//line gititemsummary.qtpl:51 +//line gititemsummary.qtpl:52 qw422016.N().S(`
@@ -178,70 +187,70 @@ func (g *GitItemSummaryPage) StreamGitContent(qw422016 *qt422016.Writer, name, r
`) -//line gititemsummary.qtpl:57 +//line gititemsummary.qtpl:58 for _, c := range g.Commits { -//line gititemsummary.qtpl:57 +//line gititemsummary.qtpl:58 qw422016.N().S(`
`) -//line gititemsummary.qtpl:60 +//line gititemsummary.qtpl:61 qw422016.E().S(TimeFormat(c.Committer.When)) -//line gititemsummary.qtpl:60 +//line gititemsummary.qtpl:61 qw422016.N().S(`
`)
-//line gititemsummary.qtpl:63
+//line gititemsummary.qtpl:64
 		qw422016.E().S(c.Message)
-//line gititemsummary.qtpl:63
+//line gititemsummary.qtpl:64
 		qw422016.N().S(`
`) -//line gititemsummary.qtpl:66 +//line gititemsummary.qtpl:67 qw422016.E().S(c.Committer.Name) -//line gititemsummary.qtpl:66 +//line gititemsummary.qtpl:67 qw422016.N().S(` <`) -//line gititemsummary.qtpl:66 +//line gititemsummary.qtpl:67 qw422016.E().S(c.Committer.Email) -//line gititemsummary.qtpl:66 +//line gititemsummary.qtpl:67 qw422016.N().S(`>
`) -//line gititemsummary.qtpl:69 +//line gititemsummary.qtpl:70 } -//line gititemsummary.qtpl:69 +//line gititemsummary.qtpl:70 qw422016.N().S(`
`) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 } -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 func (g *GitItemSummaryPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) { -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 qw422016 := qt422016.AcquireWriter(qq422016) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 g.StreamGitContent(qw422016, name, ref) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 qt422016.ReleaseWriter(qw422016) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 } -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 func (g *GitItemSummaryPage) GitContent(name, ref string) string { -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 qb422016 := qt422016.AcquireByteBuffer() -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 g.WriteGitContent(qb422016, name, ref) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 qs422016 := string(qb422016.B) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 qt422016.ReleaseByteBuffer(qb422016) -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 return qs422016 -//line gititemsummary.qtpl:72 +//line gititemsummary.qtpl:73 } -- cgit v1.2.3