Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-27 05:45:56 |
Commit | 5aa454cedeed0b7d4c4e48142cdac4d3793fdd7b |
Parent | cfc2b79df71292d2b6c6463bcf1bf9e484b395b7 |
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)