Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-11-20 10:18:08
Commit7dd44593815e8cb2019e0757ac1900a3da470755
Parent1c90d8bf4e04b11a27a31e77762870e7bbde7122

Implement repository and directory downloading

Diffstat

M res/repo/file.html | 2 +-
M res/repo/header.html | 1 +
M res/repo/tree.html | 10 ++++++----
M src/main.go | 1 +
M src/repo/download.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
M src/repo/log.go | 2 +-

6 files changed, 78 insertions, 13 deletions

diff --git a/res/repo/file.html b/res/repo/file.html
index 271e647..14290c3 100644
--- a/res/repo/file.html
+++ b/res/repo/file.html
@@ -3,7 +3,7 @@
 <body>
 	<header>
 		{{template "repo/header" .}}<hr>
-		{{.File}} ({{.Size}}) {{.Mode}}
+		{{.File}} ({{.Size}}) {{.Mode}} <a href="/{{.Name}}/download/{{.File}}">download</a>
 	</header><hr>
 	<main>
 		<table>
diff --git a/res/repo/header.html b/res/repo/header.html
index 8391023..9f7e1df 100644
--- a/res/repo/header.html
+++ b/res/repo/header.html
@@ -24,6 +24,7 @@
 			{{if .Licence}}
 				| <a href="{{.Licence}}">LICENCE</a>
 			{{end}}
+			| <a href="/{{.Name}}/download">Download</a>
 			{{if .Editable}}
 				| <a href="/{{.Name}}/edit">Edit</a>
 			{{end}}
diff --git a/res/repo/tree.html b/res/repo/tree.html
index c3a1a6e..5880f9c 100644
--- a/res/repo/tree.html
+++ b/res/repo/tree.html
@@ -20,10 +20,12 @@
 							<td><a href="/{{$.Name}}/{{.Path}}">{{.Name}}</a></td>
 							<td align="right" {{if .B}}style="padding-right: calc(2ch + 0.4em);"{{end}}>{{.Size}}</td>
 							<td>
-								<a href="/{{$.Name}}/log/{{.RawPath}}">log</a>
-								{{if .IsFile}}
-									blame
-									<a href="/{{$.Name}}/raw/{{.RawPath}}">raw</a>
+								{{if .RawPath}}
+									<a href="/{{$.Name}}/log/{{.RawPath}}">log</a>
+									{{if .IsFile}}
+										blame
+										<a href="/{{$.Name}}/raw/{{.RawPath}}">raw</a>
+									{{end}}
 									<a href="/{{$.Name}}/download/{{.RawPath}}">download</a>
 								{{end}}
 							</td>
diff --git a/src/main.go b/src/main.go
index 00cedf2..fd36b89 100644
--- a/src/main.go
+++ b/src/main.go
@@ -101,6 +101,7 @@ func main() {
 	h.Path("/{repo}/tree/{path:.*}").Methods("GET").HandlerFunc(repo.HandleTree)
 	h.Path("/{repo}/file/{path:.*}").Methods("GET").HandlerFunc(repo.HandleFile)
 	h.Path("/{repo}/raw/{path:.*}").Methods("GET").HandlerFunc(repo.HandleRaw)
+	h.Path("/{repo}/download").Methods("GET").HandlerFunc(repo.HandleDownload)
 	h.Path("/{repo}/download/{path:.*}").Methods("GET").HandlerFunc(repo.HandleDownload)
 	h.Path("/{repo}/refs").Methods("GET").HandlerFunc(repo.HandleRefs)
 	h.Path("/{repo}/edit").Methods("GET", "POST").HandlerFunc(repo.HandleEdit)
diff --git a/src/repo/download.go b/src/repo/download.go
index db538be..51d5548 100644
--- a/src/repo/download.go
+++ b/src/repo/download.go
@@ -1,14 +1,17 @@
 package repo
 
 import (
+	"archive/zip"
 	"errors"
 	"fmt"
 	"io"
 	"log"
 	"net/http"
 	"path/filepath"
+	"strings"
 
 	"github.com/Jamozed/Goit/src/goit"
+	"github.com/Jamozed/Goit/src/util"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/object"
@@ -18,7 +21,7 @@ import (
 func HandleDownload(w http.ResponseWriter, r *http.Request) {
 	auth, user, err := goit.Auth(w, r, true)
 	if err != nil {
-		log.Println("[repo/raw]", err.Error())
+		log.Println("[repo/download]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -36,7 +39,7 @@ func HandleDownload(w http.ResponseWriter, r *http.Request) {
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true))
 	if err != nil {
-		log.Println("[/repo/file]", err.Error())
+		log.Println("[/repo/download]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
@@ -46,30 +49,88 @@ func HandleDownload(w http.ResponseWriter, r *http.Request) {
 		goit.HttpError(w, http.StatusNotFound)
 		return
 	} else if err != nil {
-		log.Println("[/repo/file]", err.Error())
+		log.Println("[/repo/download]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
 
 	commit, err := gr.CommitObject(ref.Hash())
 	if err != nil {
-		log.Println("[/repo/file]", err.Error())
+		log.Println("[/repo/download]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
 
 	file, err := commit.File(path)
 	if errors.Is(err, object.ErrFileNotFound) {
-		goit.HttpError(w, http.StatusNotFound)
+		/* Possibly a directory, search file tree for prefix */
+		var files []string
+		var zSize uint64
+
+		iter, err := commit.Files()
+		if err != nil {
+			log.Println("[/repo/download]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+
+		iter.ForEach(func(f *object.File) error {
+			if path == "" || strings.HasPrefix(f.Name, path+"/") {
+				files = append(files, f.Name)
+				zSize += uint64(f.Size)
+			}
+
+			return nil
+		})
+
+		if len(files) == 0 {
+			goit.HttpError(w, http.StatusNotFound)
+			return
+		}
+
+		/* Build and write ZIP of directory */
+		w.Header().Set(
+			"Content-Disposition", "attachment; filename="+util.If(path == "", repo.Name, filepath.Base(path))+".zip",
+		)
+		// w.Header().Set("Content-Length", fmt.Sprint(zSize))
+
+		z := zip.NewWriter(w)
+		for _, f := range files {
+			zh := zip.FileHeader{Name: f, Method: zip.Store}
+
+			zf, err := z.CreateHeader(&zh)
+			if err != nil {
+				log.Println("[/repo/download]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+			}
+
+			if file, err := commit.File(f); err != nil {
+				log.Println("[/repo/download]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			} else if rc, err := file.Blob.Reader(); err != nil {
+				log.Println("[/repo/download]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			} else {
+				if _, err := io.Copy(zf, rc); err != nil {
+					log.Println("[/repo/download]", err.Error())
+				}
+
+				rc.Close()
+			}
+		}
+
+		z.Close()
 		return
 	} else if err != nil {
-		log.Println("[/repo/file]", err.Error())
+		log.Println("[/repo/download]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	}
 
 	if rc, err := file.Blob.Reader(); err != nil {
-		log.Println("[/repo/file]", err.Error())
+		log.Println("[/repo/download]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
 	} else {
diff --git a/src/repo/log.go b/src/repo/log.go
index 1a7b47d..73192b3 100644
--- a/src/repo/log.go
+++ b/src/repo/log.go
@@ -92,7 +92,7 @@ func HandleLog(w http.ResponseWriter, r *http.Request) {
 			log.Println("[/repo/log]", err.Error())
 		} else if path != "" {
 			for _, s := range stats {
-				if strings.HasPrefix(s.Name, path) {
+				if s.Name == path || strings.HasPrefix(s.Name, path+"/") {
 					files += 1
 					additions += s.Addition
 					deletions += s.Deletion