diff --git a/go.mod b/go.mod index d7c6781..de03416 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/MrMelon54/exit-reload v0.0.1 + github.com/dustin/go-humanize v1.0.1 github.com/golang-migrate/migrate/v4 v4.17.0 github.com/julienschmidt/httprouter v1.3.0 github.com/thanhpk/randstr v1.0.6 diff --git a/go.sum b/go.sum index bceb037..ac4637a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/MrMelon54/exit-reload v0.0.1 h1:sxHa59tNEQMcikwuX2+93lw6Vi1+R7oCRF8a0 github.com/MrMelon54/exit-reload v0.0.1/go.mod h1:PLiSfmUzwdpTTQP3BBfUPhkqPwaIZjx0DuXBnM76Bug= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/routes/dirlist.go b/routes/dirlist.go new file mode 100644 index 0000000..99113c6 --- /dev/null +++ b/routes/dirlist.go @@ -0,0 +1,126 @@ +package routes + +import ( + _ "embed" + "github.com/dustin/go-humanize" + "github.com/julienschmidt/httprouter" + "html/template" + "io" + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" +) + +//go:embed dirlist.go.html +var dirListHtml string + +var dirListTemplate = template.Must(template.New("dirlist").Parse(dirListHtml)) + +func (r *routeCtx) handleFiles(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) { + if containsDotDot(req.URL.Path) { + http.Error(rw, "invalid URL path", http.StatusBadRequest) + return + } + if strings.HasSuffix(req.URL.Path, "/") { + r.handleDirList(rw, req) + return + } + open, err := os.Open(filepath.Join(r.basePath, req.URL.Path)) + if err != nil { + http.Error(rw, "404 Not Found", http.StatusNotFound) + return + } + stat, err := open.Stat() + if err != nil { + http.Error(rw, "500 Internal Server Error: Failed to stat file", http.StatusInternalServerError) + return + } + http.ServeContent(rw, req, open.Name(), stat.ModTime(), open) +} + +type fileInfo struct { + Name string + URL string + Size string + ModTime string +} + +func (r *routeCtx) handleDirList(rw http.ResponseWriter, req *http.Request) { + openDir, err := os.ReadDir(filepath.Join(r.basePath, req.URL.Path)) + if err != nil { + http.Error(rw, "404 Not Found", http.StatusNotFound) + return + } + fileInfos := make([]*fileInfo, len(openDir)) + for i := range openDir { + info, err := openDir[i].Info() + if err != nil { + http.Error(rw, "500 Internal Server Error: Failed to stat file", http.StatusInternalServerError) + return + } + url := path.Join(req.URL.Path, info.Name()) + name := path.Base(url) + size := "" + if info.IsDir() { + url += "/" + name += "/" + } else { + size = humanize.IBytes(uint64(info.Size())) + } + fileInfos[i] = &fileInfo{ + Name: name, + URL: url, + Size: size, + ModTime: info.ModTime().Format("2006-01-02 15:04:05 -0700"), + } + } + err = dirListTemplate.Execute(rw, map[string]any{ + "Name": r.name, + "Path": req.URL.Path, + "Files": fileInfos, + }) + if err != nil { + log.Println("[GoMVN] Index template error: ", err) + } +} + +func (r *routeCtx) handlePut(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) { + p, err := r.pathUtils.ParsePath(req) + if err != nil { + http.Error(rw, "404 Not Found", http.StatusNotFound) + return + } + p = filepath.Join(r.basePath, p) + err = os.MkdirAll(filepath.Dir(p), os.ModePerm) + if err != nil { + http.Error(rw, "500 Failed to create directory", http.StatusInternalServerError) + return + } + create, err := os.Create(p) + if err != nil { + http.Error(rw, "500 Failed to open file", http.StatusInternalServerError) + return + } + _, err = io.Copy(create, req.Body) + if err != nil { + http.Error(rw, "500 Failed to write file", http.StatusInternalServerError) + return + } +} + +func containsDotDot(v string) bool { + if !strings.Contains(v, "..") { + return false + } + for _, ent := range strings.FieldsFunc(v, isSlashRune) { + if ent == ".." { + return true + } + } + return false +} + +func isSlashRune(r rune) bool { return r == '/' || r == '\\' } diff --git a/routes/dirlist.go.html b/routes/dirlist.go.html new file mode 100644 index 0000000..86d18b5 --- /dev/null +++ b/routes/dirlist.go.html @@ -0,0 +1,41 @@ + + + + {{ .Name }} - {{ .Path }} + + + +

{{.Name}}

+ + + + + + + {{range .Files}} + + + + + + {{end}} +
NameLast ModifiedSize
{{ .Name }}{{ .ModTime }}{{ .Size }}
+ + diff --git a/routes/index.go.html b/routes/index.go.html deleted file mode 100644 index 0ae7e19..0000000 --- a/routes/index.go.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - GoMVN - {{.Name}} - - - -

{{.Name}}

-{{range $k, $v := .Repositories}} - -{{end}} - - diff --git a/routes/router.go b/routes/router.go index e2ef56e..05fead7 100644 --- a/routes/router.go +++ b/routes/router.go @@ -7,13 +7,8 @@ import ( "github.com/1f349/gomvn/paths" "github.com/julienschmidt/httprouter" "github.com/thanhpk/randstr" - "html/template" - "io" - "log" "net/http" - "os" "path" - "path/filepath" ) type routeCtx struct { @@ -63,9 +58,9 @@ func Router(db *database.Queries, name, basePath string, repository []string) ht rWeb := httprouter.New() rWeb.PUT("/*filepath", base.repoAuth(base.handlePut)) - rWeb.GET("/", base.handleIndex) + rWeb.GET("/", base.handleFiles) for _, repo := range repository { - rWeb.ServeFiles(path.Join("/", repo, "*filepath"), http.FS(os.DirFS(filepath.Join(basePath, repo)))) + rWeb.GET(path.Join("/", repo, "*filepath"), base.handleFiles) } mux := http.NewServeMux() @@ -85,47 +80,3 @@ func Router(db *database.Queries, name, basePath string, repository []string) ht return mux } - -//go:embed index.go.html -var indexHtml string - -var indexTemplate = template.Must(template.New("index").Parse(indexHtml)) - -func (r *routeCtx) handleIndex(rw http.ResponseWriter, _ *http.Request, _ httprouter.Params) { - repositories, err := paths.GetRepositories(r.basePath, r.repository) - if err != nil { - http.Error(rw, "500 Internal Server Error", http.StatusInternalServerError) - return - } - err = indexTemplate.Execute(rw, map[string]any{ - "Name": r.name, - "Repositories": repositories, - }) - if err != nil { - log.Println("[GoMVN] Index template error: ", err) - } -} - -func (r *routeCtx) handlePut(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { - p, err := r.pathUtils.ParsePath(req) - if err != nil { - http.Error(rw, "404 Not Found", http.StatusNotFound) - return - } - p = filepath.Join(r.basePath, p) - err = os.MkdirAll(filepath.Dir(p), os.ModePerm) - if err != nil { - http.Error(rw, "500 Failed to create directory", http.StatusInternalServerError) - return - } - create, err := os.Create(p) - if err != nil { - http.Error(rw, "500 Failed to open file", http.StatusInternalServerError) - return - } - _, err = io.Copy(create, req.Body) - if err != nil { - http.Error(rw, "500 Failed to write file", http.StatusInternalServerError) - return - } -}