Goit

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

AuthorJakob Wakeling <[email protected]>
Date2023-07-27 05:45:56
Commit5aa454cedeed0b7d4c4e48142cdac4d3793fdd7b
Parentcfc2b79df71292d2b6c6463bcf1bf9e484b395b7

Implement file viewing

Diffstat

M go.mod | 1 +
M go.sum | 2 ++
M main.go | 1 +
A res/repo/file.html | 22 ++++++++++++++++++++++
M res/repo/tree.html | 2 +-
M res/res.go | 3 +++
M res/style.css | 5 +++++
M src/http.go | 1 +
A src/repo/file.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M src/repo/tree.go | 10 +++++++---
M src/util/util.go | 18 ++++++++++++++++++

11 files changed, 174 insertions, 4 deletions

diff --git a/go.mod b/go.mod
index 9bd7e89..cc85f1e 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/gorilla/mux v1.8.0
 	github.com/mattn/go-sqlite3 v1.14.17
 	golang.org/x/crypto v0.9.0
+	golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
 )
 
 require (
diff --git a/go.sum b/go.sum
index 6a99376..faeb509 100644
--- a/go.sum
+++ b/go.sum
@@ -77,6 +77,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
 golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
+golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
diff --git a/main.go b/main.go
index be5cb26..9aea17f 100644
--- a/main.go
+++ b/main.go
@@ -43,6 +43,7 @@ func main() {
 	h.Path("/{repo}/log/{path:.*}").Methods("GET").HandlerFunc(repo.HandleLog)
 	h.Path("/{repo}/tree").Methods("GET").HandlerFunc(repo.HandleTree)
 	h.Path("/{repo}/tree/{path:.*}").Methods("GET").HandlerFunc(repo.HandleTree)
+	h.Path("/{repo}/file/{path:.*}").Methods("GET").HandlerFunc(repo.HandleFile)
 	h.Path("/{repo}/refs").Methods("GET").HandlerFunc(goit.HandleRepoRefs)
 	h.Path("/{repo}/info/refs").Methods("GET").HandlerFunc(goit.HandleInfoRefs)
 	h.Path("/{repo}/git-upload-pack").Methods("POST").HandlerFunc(goit.HandleUploadPack)
diff --git a/res/repo/file.html b/res/repo/file.html
new file mode 100644
index 0000000..e97c399
--- /dev/null
+++ b/res/repo/file.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<head lang="en-GB">{{template "base/head" .}}</head>
+<body>
+	<header>
+		{{template "repo/header" .}}<hr>
+		{{.File}} ({{.Size}}) {{.Mode}}
+	</header><hr>
+	<main>
+		<table>
+			{{if .Lines}}
+				{{range $i, $l := .Lines}}
+					<tr id="{{$i}}">
+						<td class="lnum" style="text-align: right;"><a href="#{{$i}}">{{$i}}</a></td>
+						<td class="line">{{$l}}</td>
+					</tr>
+				{{end}}
+			{{else}}
+				<tr><td>Binary file</td></tr>
+			{{end}}
+		</table>
+	</main>
+</body>
diff --git a/res/repo/tree.html b/res/repo/tree.html
index 9e2a1ff..331fbb4 100644
--- a/res/repo/tree.html
+++ b/res/repo/tree.html
@@ -17,7 +17,7 @@
 					{{range .Files}}
 						<tr>
 							<td>{{.Mode}}</td>
-							<td><a href="/{{$.Name}}/tree/{{.Path}}">{{.Name}}</a></td>
+							<td><a href="/{{$.Name}}/{{.Path}}">{{.Name}}</a></td>
 							<td align="right" {{if .B}}style="padding-right: calc(2ch + 0.4em);"{{end}}>{{.Size}}</td>
 							<td>log blame raw download</td>
 						</tr>
diff --git a/res/res.go b/res/res.go
index 94c8dfc..3e6405c 100644
--- a/res/res.go
+++ b/res/res.go
@@ -50,6 +50,9 @@ var RepoLog string
 //go:embed repo/tree.html
 var RepoTree string
 
+//go:embed repo/file.html
+var RepoFile string
+
 //go:embed repo/refs.html
 var RepoRefs string
 
diff --git a/res/style.css b/res/style.css
index 6e14231..2b6c58a 100644
--- a/res/style.css
+++ b/res/style.css
@@ -11,6 +11,11 @@ table td:empty::after { content: "\00a0"; }
 
 main table tr:hover td { background-color: #222222; }
 
+table td.lnum { padding: 0; }
+table td.lnum a { color: inherit; display: block; padding: 0 0.4rem 0 0.8rem; }
+table td.lnum a:hover { text-decoration: none; }
+table td.line { tab-size: 4; white-space: pre; }
+
 form table input { border: 2px solid #333333; border-radius: 3px; background-color: #111111; padding: 2px; }
 form table input[type="text"] { color: #888888; width: 24em; }
 form table input[type="submit"] { color: #FF7E00; width: 6em; }
diff --git a/src/http.go b/src/http.go
index 76722f5..5846028 100644
--- a/src/http.go
+++ b/src/http.go
@@ -34,6 +34,7 @@ func init() {
 
 	template.Must(Tmpl.New("repo/log").Parse(res.RepoLog))
 	template.Must(Tmpl.New("repo/tree").Parse(res.RepoTree))
+	template.Must(Tmpl.New("repo/file").Parse(res.RepoFile))
 	template.Must(Tmpl.New("repo/refs").Parse(res.RepoRefs))
 }
 
diff --git a/src/repo/file.go b/src/repo/file.go
new file mode 100644
index 0000000..e19dadd
--- /dev/null
+++ b/src/repo/file.go
@@ -0,0 +1,113 @@
+package repo
+
+import (
+	"errors"
+	"io"
+	"log"
+	"net/http"
+	"strings"
+
+	goit "github.com/Jamozed/Goit/src"
+	"github.com/Jamozed/Goit/src/util"
+	"github.com/dustin/go-humanize"
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/gorilla/mux"
+)
+
+func HandleFile(w http.ResponseWriter, r *http.Request) {
+	_, uid := goit.AuthCookie(w, r, true)
+
+	treepath := mux.Vars(r)["path"]
+	// if treepath == "" {
+	// 	goit.HttpError(w, http.StatusNotFound)
+	// 	return
+	// }
+
+	repo, err := goit.GetRepoByName(mux.Vars(r)["repo"])
+	if err != nil {
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else if repo == nil || (repo.IsPrivate && repo.OwnerId != uid) {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	}
+
+	data := struct {
+		Title, Name, Description, Url string
+		Readme, Licence               string
+		Mode, File, Size              string
+		Lines                         []string
+	}{
+		Title: repo.Name + " - File", Name: repo.Name, Description: repo.Description,
+		Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+	}
+
+	gr, err := git.PlainOpen(goit.RepoPath(repo.Name))
+	if err != nil {
+		log.Println("[/repo/file]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	ref, err := gr.Head()
+	if errors.Is(err, plumbing.ErrReferenceNotFound) {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	} else if err != nil {
+		log.Println("[/repo/file]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	commit, err := gr.CommitObject(ref.Hash())
+	if err != nil {
+		log.Println("[/repo/file]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	file, err := commit.File(treepath)
+	if errors.Is(err, object.ErrFileNotFound) {
+		goit.HttpError(w, http.StatusNotFound)
+		return
+	} else if err != nil {
+		log.Println("[/repo/file]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	}
+
+	data.Mode = util.ModeString(uint32(file.Mode))
+	data.File = file.Name
+	data.Size = humanize.IBytes(uint64(file.Size))
+
+	if rc, err := file.Blob.Reader(); err != nil {
+		log.Println("[/repo/file]", err.Error())
+		goit.HttpError(w, http.StatusInternalServerError)
+		return
+	} else {
+		buf := make([]byte, util.Min(file.Size, 512))
+
+		if _, err := rc.Read(buf); err != nil {
+			log.Println("[/repo/file]", err.Error())
+			goit.HttpError(w, http.StatusInternalServerError)
+			return
+		}
+
+		if strings.HasPrefix(http.DetectContentType(buf), "text") {
+			buf2 := make([]byte, util.Min(file.Size-int64(len(buf)), (10*1024*1024)-int64(len(buf))))
+			if _, err := rc.Read(buf2); err != nil && !errors.Is(err, io.EOF) {
+				log.Println("[/repo/file]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			}
+
+			data.Lines = strings.Split(string(append(buf, buf2...)), "\n")
+		}
+	}
+
+	if err := goit.Tmpl.ExecuteTemplate(w, "repo/file", data); err != nil {
+		log.Println("[/repo/file]", err.Error())
+	}
+}
diff --git a/src/repo/tree.go b/src/repo/tree.go
index 5a91496..36dba07 100644
--- a/src/repo/tree.go
+++ b/src/repo/tree.go
@@ -72,7 +72,9 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 		}
 
 		if treepath != "" {
-			data.Files = append(data.Files, row{Mode: "d---------", Name: "..", Path: path.Dir(treepath)})
+			data.Files = append(data.Files, row{
+				Mode: "d---------", Name: "..", Path: path.Join("tree", path.Dir(treepath)),
+			})
 
 			tree, err = tree.Tree(treepath)
 			if errors.Is(err, object.ErrDirectoryNotFound) {
@@ -94,7 +96,7 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 		})
 
 		for _, v := range tree.Entries {
-			size := ""
+			var fpath, size string
 
 			if v.Mode&0o40000 == 0 {
 				file, err := tree.File(v.Name)
@@ -104,6 +106,7 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 					return
 				}
 
+				fpath = path.Join("file", treepath, v.Name)
 				size = humanize.IBytes(uint64(file.Size))
 			} else {
 				var dirSize uint64
@@ -124,11 +127,12 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 					return
 				}
 
+				fpath = path.Join("tree", treepath, v.Name)
 				size = humanize.IBytes(dirSize)
 			}
 
 			data.Files = append(data.Files, row{
-				Mode: util.ModeString(uint32(v.Mode)), Name: v.Name, Path: path.Join(treepath, v.Name), Size: size,
+				Mode: util.ModeString(uint32(v.Mode)), Name: v.Name, Path: fpath, Size: size,
 				B: util.If(strings.HasSuffix(size, " B"), true, false),
 			})
 		}
diff --git a/src/util/util.go b/src/util/util.go
index ca55333..aa48e40 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -10,6 +10,8 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
+
+	"golang.org/x/exp/constraints"
 )
 
 const ModeNotRegular = os.ModeSymlink | os.ModeDevice | os.ModeNamedPipe | os.ModeSocket | os.ModeCharDevice |
@@ -33,6 +35,22 @@ func SliceContains[T comparable](s []T, e T) bool {
 	return false
 }
 
+func Min[T constraints.Ordered](a, b T) T {
+	if a < b {
+		return a
+	}
+
+	return b
+}
+
+func Max[T constraints.Ordered](a, b T) T {
+	if a > b {
+		return a
+	}
+
+	return b
+}
+
 /* Return the named cookie or nil if not found or invalid. */
 func Cookie(r *http.Request, name string) *http.Cookie {
 	c, err := r.Cookie(name)