flounder

A simple gemini site builder
Log | Files | Refs | README | LICENSE

commit 98c0e38c17e0935914797a5e6763829d3f48ad1f
parent 18a042de0538b53ca8cf949b0b575d1d597bf92a
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 27 Dec 2020 18:40:04 -0800

Messy commit, some refactoring

Diffstat:
Mgemfeed.go | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
Mgemini.go | 37++++++++++++++++++++++++++++++++++++-
Mhttp.go | 66+++++++++++++++++++++++++-----------------------------------------
Mmain.go | 42++++++++++++++++++++++--------------------
Atemplates/folder.gmi | 8++++++++
Dtemplates/folder.html | 23-----------------------
6 files changed, 142 insertions(+), 86 deletions(-)

diff --git a/gemfeed.go b/gemfeed.go @@ -30,7 +30,57 @@ type FeedEntry struct { Feed *Gemfeed } -// TODO definitely cache this function -- it reads EVERY gemini file on flounder. +// Non-standard extension +// Requires yyyy-mm-dd formatted files +func generateFeedFromFolder(folder string) []*FeedEntry { + user := getCreator(folder) + feed := Gemfeed{ + Title: user + "'s Gemfeed", + Creator: user, + // URL? + } + var feedEntries []*FeedEntry + err := filepath.Walk(folder, func(thepath string, info os.FileInfo, err error) error { + base := path.Base(thepath) + if len(base) >= 10 { + entry := FeedEntry{} + date, err := time.Parse("2006-01-02", base[:10]) + if err != nil { + return nil + } + entry.Date = date + entry.DateString = base[:10] + entry.Feed = &feed + f, err := os.Open(thepath) + if err != nil { + return nil + } + defer f.Close() + scanner := bufio.NewScanner(f) + i := 0 + for scanner.Scan() { + if i > 5 { // To be more efficient, only scan the top 5 lines + break + } + line := scanner.Text() + if strings.HasPrefix(line, "#") { + entry.Title = strings.Trim(line, "# \t") + break + } + i += 1 + } + // get title from first header + } + return nil + }) + if err != nil { + return nil + } + return feedEntries +} + +// TODO definitely cache this function +// TODO include generateFeedFromFolder for "gemfeed" folders func getAllGemfeedEntries() ([]*FeedEntry, []*Gemfeed, error) { maxUserItems := 25 maxItems := 50 diff --git a/gemini.go b/gemini.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "crypto/tls" "crypto/x509/pkix" gmi "git.sr.ht/~adnano/go-gemini" + "io/ioutil" "log" "path" "path/filepath" @@ -12,6 +14,34 @@ import ( "time" ) +var gt *template.Template + +func generateGemfeedPage(user string) string { + return "" +} + +func generateFolderPage(fullpath string) string { + files, _ := ioutil.ReadDir(fullpath) + var renderedFiles = []File{} + for _, file := range files { + // Very awkward + res := fileFromPath(path.Join(fullpath, file.Name())) + renderedFiles = append(renderedFiles, res) + } + var buff bytes.Buffer + data := struct { + Host string + Folder string + Files []File + }{c.Host, getLocalPath(fullpath), renderedFiles} + err := gt.ExecuteTemplate(&buff, "folder.gmi", data) + if err != nil { + log.Println(err) + return "" + } + return buff.String() +} + func gmiIndex(w *gmi.ResponseWriter, r *gmi.Request) { log.Println("Index request") t, err := template.ParseFiles("templates/index.gmi") @@ -54,13 +84,18 @@ func gmiPage(w *gmi.ResponseWriter, r *gmi.Request) { func runGeminiServer() { log.Println("Starting gemini server") + var err error + gt, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.gmi")) + if err != nil { + log.Fatal(err) + } var server gmi.Server server.ReadTimeout = 1 * time.Minute server.WriteTimeout = 2 * time.Minute hostname := strings.SplitN(c.Host, ":", 2)[0] // is this necc? - err := server.Certificates.Load(c.GeminiCertStore) + err = server.Certificates.Load(c.GeminiCertStore) if err != nil { } server.CreateCertificate = func(h string) (tls.Certificate, error) { diff --git a/http.go b/http.go @@ -256,7 +256,7 @@ func mySiteHandler(w http.ResponseWriter, r *http.Request) { data := struct { Host string PageTitle string - Files []*File + Files []File AuthUser AuthUser CurrentDate string }{c.Host, c.SiteTitle, files, user, currentDate} @@ -530,13 +530,13 @@ func userFile(w http.ResponseWriter, r *http.Request) { userName := filepath.Clean(strings.Split(r.Host, ".")[0]) // Clean probably unnecessary p := filepath.Clean(r.URL.Path) var isDir bool - fileName := path.Join(c.FilesDirectory, userName, p) // TODO rename filepath - stat, err := os.Stat(fileName) + fullPath := path.Join(c.FilesDirectory, userName, p) // TODO rename filepath + stat, _ := os.Stat(fullPath) if stat != nil { isDir = stat.IsDir() } - if p == "/" || isDir { - fileName = path.Join(fileName, "index.gmi") + if strings.HasSuffix(p, "index.gmi") { + http.Redirect(w, r, path.Dir(p), http.StatusMovedPermanently) } if strings.HasPrefix(p, "/"+HIDDEN_FOLDER) { @@ -548,47 +548,32 @@ func userFile(w http.ResponseWriter, r *http.Request) { return } - _, err = os.Stat(fileName) - if os.IsNotExist(err) { - if p == "/" || isDir { - fileName := path.Join(c.FilesDirectory, userName, p) - favicon := getFavicon(userName) - files, _ := ioutil.ReadDir(fileName) - renderedFiles := []File{} - for _, file := range files { - n := file.Name() - newFile := File{ - Name: path.Join(p, n), // SHOULD be safe - UpdatedTime: file.ModTime(), - Host: c.Host, - Creator: getCreator(fileName), - } - renderedFiles = append(renderedFiles, newFile) + var geminiContent string + if p == "/" || isDir { + _, err := os.Stat(path.Join(fullPath, "index.gmi")) + if os.IsNotExist(err) { + if p == "/gemlog" { + // geminiContent = generateGemfeedPage(fullPath) + geminiContent = generateFolderPage(fullPath) + } else { + geminiContent = generateFolderPage(fullPath) } - hostname := strings.Split(r.Host, ":")[0] - URI := hostname + r.URL.String() - data := struct { - Folder string - Files []File - Favicon string - PageTitle string - URI string - }{p, renderedFiles, favicon, userName + p, URI} - // TODO check if gemlog - t.ExecuteTemplate(w, "folder.html", data) - return } else { - renderDefaultError(w, http.StatusNotFound) - return + fullPath = path.Join(fullPath, "index.gmi") } } - // Dumb content negotiation _, raw := r.URL.Query()["raw"] acceptsGemini := strings.Contains(r.Header.Get("Accept"), "text/gemini") - if !raw && !acceptsGemini && isGemini(fileName) { - file, _ := os.Open(fileName) - htmlString := textToHTML(gmi.ParseText(file)) + if !raw && !acceptsGemini && (isGemini(fullPath) || geminiContent != "") { + var htmlString string + if geminiContent == "" { + file, _ := os.Open(fullPath) + htmlString = textToHTML(gmi.ParseText(file)) + defer file.Close() + } else { + htmlString = textToHTML(gmi.ParseText(strings.NewReader(geminiContent))) + } favicon := getFavicon(userName) hostname := strings.Split(r.Host, ":")[0] URI := hostname + r.URL.String() @@ -599,9 +584,8 @@ func userFile(w http.ResponseWriter, r *http.Request) { URI string }{template.HTML(htmlString), favicon, userName + p, URI} t.ExecuteTemplate(w, "user_page.html", data) - file.Close() } else { - http.ServeFile(w, r, fileName) + http.ServeFile(w, r, fullPath) } } diff --git a/main.go b/main.go @@ -30,10 +30,26 @@ type File struct { // also folders UpdatedTime time.Time TimeAgo string IsText bool - Children []*File + Children []File Host string } +func fileFromPath(fullPath string) File { + info, _ := os.Stat(fullPath) + creatorFolder := getCreator(fullPath) + isText := strings.HasPrefix(mime.TypeByExtension(path.Ext(fullPath)), "text") // Not perfect + updatedTime := info.ModTime() + return File{ + Name: getLocalPath(fullPath), + Creator: path.Base(creatorFolder), + UpdatedTime: updatedTime, + IsText: isText, + TimeAgo: timeago(&updatedTime), + Host: c.Host, + } + +} + type User struct { Username string Email string @@ -119,14 +135,8 @@ func getIndexFiles(admin bool) ([]*File, error) { // cache this function } // make this do what it should if !info.IsDir() { - creatorFolder := getCreator(thepath) - updatedTime := info.ModTime() - result = append(result, &File{ - Name: getLocalPath(thepath), - Creator: path.Base(creatorFolder), - UpdatedTime: updatedTime, - TimeAgo: timeago(&updatedTime), - }) + res := fileFromPath(thepath) + result = append(result, &res) } return nil }) @@ -142,23 +152,15 @@ func getIndexFiles(admin bool) ([]*File, error) { // cache this function return result, nil } // todo clean up paths -func getMyFilesRecursive(p string, creator string) ([]*File, error) { - result := []*File{} +func getMyFilesRecursive(p string, creator string) ([]File, error) { + result := []File{} files, err := ioutil.ReadDir(p) if err != nil { return nil, err } for _, file := range files { - isText := strings.HasPrefix(mime.TypeByExtension(path.Ext(file.Name())), "text") fullPath := path.Join(p, file.Name()) - localPath := getLocalPath(fullPath) - f := &File{ - Name: localPath, - Creator: creator, - UpdatedTime: file.ModTime(), - IsText: isText, - Host: c.Host, - } + f := fileFromPath(fullPath) if file.IsDir() { f.Children, err = getMyFilesRecursive(path.Join(p, file.Name()), creator) } diff --git a/templates/folder.gmi b/templates/folder.gmi @@ -0,0 +1,8 @@ +{{$host := .Host }} +# {{ .Folder }} + +{{ range .Files }} +=> //{{.Creator}}.{{$host}}/{{.Name}} {{.Name}} +{{ end }} + +=> //{{$host}} Home diff --git a/templates/folder.html b/templates/folder.html @@ -1,23 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8" /> - <title>{{.PageTitle }}</title> - <meta name="viewport" content="width=device-width" /> - <link rel="stylesheet" type="text/css" href="/style.css" /> - <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>{{.Favicon}}</text></svg>"> - </head> - <body> -<main> -<h1>{{.PageTitle}}</h1> -{{range .Files}} -<p> -<a href="//{{.Creator}}.{{.Host}}/{{.Name}}">{{.Name}}</a> -</p> -{{end}} -<br> -<a href="/">home</a> -<br> -</main> -</body> -</html>