diff --git a/go.mod b/go.mod index cf5f13e..324b661 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect +require ( + github.com/Nigel2392/mux v1.2.4 // indirect + github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect +) diff --git a/go.sum b/go.sum index 94f8abf..432cd1c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Nigel2392/goldcrest v1.0.4 h1:Xx+QLht6QjJ3Gg9uksgc6Ye1XjbtzQ1208ClZwoVWsg= github.com/Nigel2392/goldcrest v1.0.4/go.mod h1:UpnPrYJqZY/b7TkoVKdoNNPKTlQtld+fsrZEA98c1c0= +github.com/Nigel2392/mux v1.2.4 h1:nS/Yeo3DQhQLFcLIuObhqCg9Ay1XD6EYJYiAro0AFn0= +github.com/Nigel2392/mux v1.2.4/go.mod h1:Hrj90gq33BCcFy8167rU03oBr2YIQMRvNkMf9JnRcAU= github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk= github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/main.go b/main.go index 093b207..42618ca 100644 --- a/main.go +++ b/main.go @@ -329,7 +329,7 @@ func main() { ) var server = &http.Server{ Addr: addr, - Handler: qg, + Handler: qg.HttpHandler(), } if qg.Config.TLSKey != "" && qg.Config.TLSCert != "" { diff --git a/quickgo/_templates/file.tmpl b/quickgo/_templates/file.tmpl index e240679..853b022 100644 --- a/quickgo/_templates/file.tmpl +++ b/quickgo/_templates/file.tmpl @@ -3,6 +3,19 @@ {{define "content"}}
{{ template "parent_url" . }} -
{{.Content}}
+ {{ if gt (len .ObjectList) 0 }} +
+ {{ range $obj := .ObjectList }} + + {{ end }} +
+ {{ else }} +
+

You don't have any projects

+
+ {{ end }}
{{end}} diff --git a/quickgo/_templates/index.tmpl b/quickgo/_templates/index.tmpl index 3aede6c..1143530 100644 --- a/quickgo/_templates/index.tmpl +++ b/quickgo/_templates/index.tmpl @@ -2,7 +2,11 @@ {{define "content"}}
-

Index: {{.Dir.GetName}}

-
{{.Content}}
+

View all projects:

+
{{end}} diff --git a/quickgo/hooks.go b/quickgo/hooks.go index 427714b..71eee30 100644 --- a/quickgo/hooks.go +++ b/quickgo/hooks.go @@ -14,7 +14,7 @@ type ( AppServeHook func(*App, http.ResponseWriter, *http.Request) (written bool, err error) // quickgo.funcs.listProjects - AppListProjectsHook func(*App, []string) ([]string, error) + AppListProjectsHook func(*App, []*config.Project) ([]*config.Project, error) // quickgo.project.loaded // quickgo.project.example diff --git a/quickgo/hooks_test.go b/quickgo/hooks_test.go index 56c82ee..b40fe13 100644 --- a/quickgo/hooks_test.go +++ b/quickgo/hooks_test.go @@ -71,38 +71,6 @@ func TestAppServeHook(t *testing.T) { } -func TestAppListProjectsHook(t *testing.T) { - var hookName = "quickgo.test.TestAppListProjectsHook" - var ( - err error - a = &quickgo.App{} - p = make([]string, 0) - ) - p = append(p, "project1") - p = append(p, "project2") - - goldcrest.Register( - hookName, 0, - func(a *quickgo.App, projects []string) ([]string, error) { - projects = append(projects, "project3") - return projects, nil - }, - ) - for _, hook := range goldcrest.Get[quickgo.AppListProjectsHook](hookName) { - if p, err = hook(a, p); err != nil { - t.Fatal(err) - } - } - - if len(p) != 3 { - t.Fatalf("expected 3, got %d", len(p)) - } - - if p[2] != "project3" { - t.Fatalf("expected %q, got %q", "project3", p[2]) - } -} - func TestProjectHook(t *testing.T) { var hookName = "quickgo.test.TestProjectHook" diff --git a/quickgo/quickgo.go b/quickgo/quickgo.go index 3acc902..246d1df 100644 --- a/quickgo/quickgo.go +++ b/quickgo/quickgo.go @@ -516,9 +516,9 @@ func (a *App) CopyFileContent(proj *config.Project, file *os.File, f *quickfs.FS ) } -func (a *App) ListProjects() ([]string, error) { +func (a *App) ListProjectObjects() ([]*config.Project, error) { var ( - projects = make([]string, 0) + projects = make([]*config.Project, 0) dirPath = getProjectFilePath("", true) ) @@ -534,11 +534,12 @@ func (a *App) ListProjects() ([]string, error) { var path = filepath.Join(dirPath, d.Name()) var configName = filepath.Join(path, config.PROJECT_CONFIG_NAME) - if _, err = os.Stat(configName); err != nil { - continue + proj, err := config.LoadYaml[config.Project](configName) + if err != nil && !os.IsNotExist(err) { + return nil, errors.Wrapf(err, "failed to load project config %s", configName) } - projects = append(projects, d.Name()) + projects = append(projects, proj) } for _, hook := range goldcrest.Get[AppListProjectsHook](HookQuickGoListProjects) { @@ -554,6 +555,18 @@ func (a *App) ListProjects() ([]string, error) { return projects, nil } +func (a *App) ListProjects() ([]string, error) { + var p, err = a.ListProjectObjects() + if err != nil { + return nil, err + } + var names = make([]string, 0, len(p)) + for _, proj := range p { + names = append(names, proj.Name) + } + return names, nil +} + func (a *App) WriteFile(data []byte, path string) error { path = filepath.Join(executableDir, config.QUICKGO_DIR, path) return os.WriteFile(path, data, os.ModePerm) @@ -564,82 +577,54 @@ func (a *App) ReadFile(path string) ([]byte, error) { return os.ReadFile(path) } -func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (a *App) HttpHandler() http.Handler { + var mux = http.NewServeMux() + mux.Handle("/", &LogHandler{ + Handler: http.HandlerFunc(a.serveIndex), + Where: "index", + Level: logger.InfoLevel, + }) + mux.Handle("/projects/", &LogHandler{ + Handler: http.StripPrefix( + "/projects/", + http.HandlerFunc(a.serveProjects), + ), + Where: "root", + Level: logger.InfoLevel, + }) + mux.Handle("/static/", &LogHandler{ + Handler: http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))), + Where: "static files", + Level: logger.DebugLevel, + }) + mux.Handle("/favicon.ico", &LogHandler{ + Handler: http.HandlerFunc(a.serveFavicon), + Where: "favicon", + Level: logger.DebugLevel, + }) + return a.middleware(mux) +} - go func() { - if err := recover(); err != nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - } - }() +func (a *App) serveIndex(w http.ResponseWriter, r *http.Request) { - var pathParts = strings.Split(strings.Trim(r.URL.Path, "/"), "/") - var primary = strings.ToLower(pathParts[0]) - - for _, hook := range goldcrest.Get[AppServeHook](HookQuickGoServer) { - if served, err := hook(a, w, r); err != nil { - logger.Errorf("Failed to serve: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } else if served { - logger.Debugf("'%s' was served and hijacked by a hook", r.URL.Path) - return - } + var projects, err = a.ListProjectObjects() + if err != nil { + logger.Errorf("Failed to list projects: %v", err) + http.Error(w, "Failed to list projects", http.StatusInternalServerError) + return } - switch { - case primary == "projects": - - logServe("projects", r) - a.serveProjects(w, r, pathParts[1:]) - - case primary == "favicon.ico" && len(pathParts) == 1: - - // Write out the favicon file. - // No need to log the happy path here, quite boring. - var f, err = staticFS.Open("quickgo.png") - if err != nil { - logger.Errorf("Failed to open 'quickgo.png': %v", err) - http.Error(w, "Failed to open 'quickgo.png'", http.StatusInternalServerError) - return - } - defer f.Close() - - w.Header().Set("Content-Type", "image/x-icon") - if _, err = io.Copy(w, f); err != nil { - logger.Errorf("Failed to read 'quickgo.png': %v", err) - http.Error(w, "Failed to read 'quickgo.png'", http.StatusInternalServerError) - } - - case primary == "static": - - // Serve static files. - logServe("static file", r) - // Serve from embedded file system. - var handler = http.FileServer(http.FS(staticFS)) - handler = http.StripPrefix("/static/", handler) - handler.ServeHTTP(w, r) - - default: - - logger.Debugf("Invalid request path '%s'", r.URL.Path) - http.Error(w, "Invalid path", http.StatusBadRequest) + var ctx = &ProjectTemplateContext{ + ObjectList: projects, } -} - -func logServe(where string, r *http.Request) { - logger.Debugf("Serving %s to %s on path '%s'", where, r.RemoteAddr, r.URL.Path) -} -type ProjectTemplateContext struct { - Project *config.Project - File *quickfs.FSFile - Dir *quickfs.FSDirectory - Parent *quickfs.FSDirectory - ObjectList []quickfs.FileLike - Content string + if err = a.executeServeTemplate(w, "index.tmpl", ctx); err != nil { + logger.Errorf("Failed to render index: %v", err) + http.Error(w, "Failed to render index", http.StatusInternalServerError) + } } -func (a *App) serveProjects(w http.ResponseWriter, r *http.Request, pathParts []string) { +func (a *App) serveProjects(w http.ResponseWriter, r *http.Request) { var ( proj *config.Project parent *quickfs.FSDirectory @@ -647,11 +632,7 @@ func (a *App) serveProjects(w http.ResponseWriter, r *http.Request, pathParts [] err error ) - if len(pathParts) == 0 { - logger.Errorf("Invalid request path '%s'", r.URL.Path) - http.Error(w, "Invalid path", http.StatusBadRequest) - return - } + var pathParts = strings.Split(strings.Trim(r.URL.Path, "/"), "/") proj, closeFiles, err := a.ReadProjectConfig(pathParts[0]) if err != nil { @@ -723,6 +704,77 @@ func (a *App) serveProjects(w http.ResponseWriter, r *http.Request, pathParts [] } } +func (a *App) serveFavicon(w http.ResponseWriter, r *http.Request) { + // Write out the favicon file. + // No need to log the happy path here, quite boring. + var f, err = staticFS.Open("quickgo.png") + if err != nil { + logger.Errorf("Failed to open 'quickgo.png': %v", err) + http.Error(w, "Failed to open 'quickgo.png'", http.StatusInternalServerError) + return + } + defer f.Close() + + w.Header().Set("Content-Type", "image/x-icon") + if _, err = io.Copy(w, f); err != nil { + logger.Errorf("Failed to read 'quickgo.png': %v", err) + http.Error(w, "Failed to read 'quickgo.png'", http.StatusInternalServerError) + } +} + +func (a *App) middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + }() + + for _, hook := range goldcrest.Get[AppServeHook](HookQuickGoServer) { + if served, err := hook(a, w, r); err != nil { + logger.Errorf("Failed to serve: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } else if served { + logger.Debugf("'%s' was served and hijacked by a hook", r.URL.Path) + return + } + } + + next.ServeHTTP(w, r) + }) +} + +type LogHandler struct { + Handler http.Handler + Level logger.LogLevel + Where string +} + +func (h *LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var s = fmt.Sprintf("Serving %s to %s on path '%s'", h.Where, r.RemoteAddr, r.URL.Path) + switch h.Level { + case logger.DebugLevel: + logger.Debug(s) + case logger.InfoLevel: + logger.Info(s) + case logger.WarnLevel: + logger.Warn(s) + case logger.ErrorLevel: + logger.Error(s) + } + h.Handler.ServeHTTP(w, r) +} + +type ProjectTemplateContext struct { + Project *config.Project + File *quickfs.FSFile + Dir *quickfs.FSDirectory + Parent *quickfs.FSDirectory + ObjectList any + Content string +} + func (a *App) executeServeTemplate(w http.ResponseWriter, name string, context *ProjectTemplateContext) (err error) { var tpl = html_template.New("base") @@ -734,6 +786,12 @@ func (a *App) executeServeTemplate(w http.ResponseWriter, name string, context * fl.GetPath(), )) }, + "ProjectURL": func(project *config.Project) string { + return filepath.ToSlash(path.Join( + "/projects", + project.Name, + )) + }, "FileSize": func(size int64) string { f_size := float64(size) if f_size < 1024 { diff --git a/quickgo/render.go b/quickgo/render.go index 0292730..2b600ce 100644 --- a/quickgo/render.go +++ b/quickgo/render.go @@ -40,14 +40,14 @@ func PrintLogo() { Craft(CMD_Blue, "$$ \033[31m/\033[34m $$ |$$\\ $$\\ $$\\ $$$$$$$\\ $$ | $$\\ "+Craft(CMD_Cyan, " $$ / \\__| $$$$$$\\ ####\n")) + Craft(CMD_Blue, "$$ \033[31m|\033[34m $$ |$$ | $$ |$$ |$$ \033[31m_____|\033[34m$$ | $$ \033[31m|\033[34m "+Craft(CMD_Cyan, " $$ |$$$$\\ $$ __$$\\\n")) + Craft(CMD_Blue, "$$ \033[31m|\033[34m $$ |$$ | $$ |$$ |$$ \033[31m/\033[34m $$$$$$ \033[31m/\033[34m "+Craft(CMD_Cyan, " $$ |\\_$$ |$$ / $$ | ######\n")) + - Craft(CMD_Purple, "$$ $$\\$$ |$$ | $$ |$$ |$$ | $$ _$$< "+Craft(CMD_Cyan, " $$ | $$ |$$ | $$ |\n")) + + Craft(CMD_Purple, "$$ $$\\$$ |$$ | $$ |$$ |$$ \033[31m|\033[35m $$ _$$< "+Craft(CMD_Cyan, " $$ | $$ |$$ | $$ |\n")) + Craft(CMD_Purple, "\\$$$$$$ / \\$$$$$$ |$$ |\\$$$$$$$\\ $$ | \\$$\\ "+Craft(CMD_Cyan, " \\$$$$$$ |\\$$$$$$ | #####\n")) + Craft(CMD_Red, " \\___"+CMD_Reset+Craft(CMD_Purple, "$$$")+Craft(CMD_Red, "\\ \\______/ \\__| \\_______|\\__| \\__| ")+Craft(CMD_Cyan, " \\______/ \\______/\n")) + - Craft(CMD_Red, " \\___| "+Craft(CMD_Cyan, " \n")) + Craft(CMD_Red, " \\___|") fmt.Println(str) fmt.Println(Craft(CMD_Red, "\nCreated by: ") + Craft(CMD_Purple, "Nigel van Keulen")) - var info, ok = debug.ReadBuildInfo() - if ok && info.Main.Version != "" { + + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" { fmt.Printf(Craft(CMD_Cyan, "Version: %s\n"), info.Main.Version) }