diff options
Diffstat (limited to 'serv.go')
-rw-r--r-- | serv.go | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/serv.go b/serv.go new file mode 100644 index 0000000..f7e05dc --- /dev/null +++ b/serv.go @@ -0,0 +1,180 @@ +// Server entry point +// Copyright (C) 2022 Nguyễn Gia Phong +// +// This file is part of Phylactery. +// +// Phylactery is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Phylactery is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Phylactery. If not, see <https://www.gnu.org/licenses/>. + +package main + +import ( + "archive/zip" + "embed" + "encoding/xml" + "html/template" + "io" + "log" + "net/http" + "os" + "path" + "strconv" + "strings" +) + +//go:embed static/* +var static embed.FS + +//go:embed templates/*.html +var templates embed.FS + +// Type Page represents a comic page. +type Page struct { + Index int + Name string +} + +// Type Archive represents a comic book zip archive. +type Archive struct { + Title string + Prev string + Next string +} + +// Type Directory represents a library directory in file system. +type Directory struct { + Title string + Entries []string +} + +// Function main starts Phylactery serving comics from PHYLACTERY_LIBRARY +// and listening on PHYLACTERY_ADDRESS to serve at PHYLACTERY_BASE_URL. +func main() { + http.Handle("/static/", http.FileServer(http.FS(static))) + t, err := template.New("").Funcs(template.FuncMap{ + "escape": func(name string) template.URL { + return template.URL(escape(name)) + }, + }).ParseFS(templates, "templates/*.html") + if err != nil { + log.Fatal(err) + } + lib := os.Getenv("PHYLACTERY_LIBRARY") + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + p := path.Join(lib, path.Clean(r.URL.Path)) + stat, err := os.Stat(p) + if err != nil { + http.NotFound(w, r) + return + } + r.ParseForm() + + if stat.IsDir() { + // Redirect URL without a trailing slash + // pointing to a non-directory page + if !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, r.URL.Path+"/", + http.StatusMovedPermanently) + return + } + + // Encode Atom feed + if feedFormat, isFeed := r.Form["feed"]; isFeed { + if r.URL.Path == "/" { + http.Error(w, "no feed for /", + http.StatusBadRequest) + return + } + if feedFormat[0] != "atom" { + http.Error(w, "unsupported feed format", + http.StatusUnsupportedMediaType) + return + } + w.Header().Set("Content-Type", + "application/atom+xml") + io.WriteString(w, xml.Header) + enc := xml.NewEncoder(w) + enc.Indent("", " ") + enc.Encode(synthesizeAtom(r, p, stat.ModTime())) + return + } + + // Render directory page + entries, _ := os.ReadDir(p) + var names []string + for _, entry := range entries { + if entry.IsDir() { + names = append(names, entry.Name()+"/") + } else { + names = append(names, entry.Name()) + } + } + dir := Directory{stat.Name(), names} + t.ExecuteTemplate(w, "directory.html", dir) + return + } else if strings.HasSuffix(r.URL.Path, "/") { + // Redirect URL with a trailing slash + // pointing to a non-directory page + http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], + http.StatusMovedPermanently) + return + } + + // Check if file is a valid ZIP archive + cbz, err := zip.OpenReader(p) + if err != nil { + http.Error(w, "invalid cbz", http.StatusNotAcceptable) + return + } + defer cbz.Close() + + // Respond with an image inside the CBZ + if entry, isImage := r.Form["entry"]; isImage { + i, err := strconv.Atoi(entry[0]) + if err != nil || i < 0 || i >= len(cbz.File) { + http.NotFound(w, r) + return + } + image, _ := cbz.File[i].Open() + defer image.Close() + io.Copy(w, image) + return + } + + // Render archive page + entries, _ := os.ReadDir(path.Join(p, "..")) + index := find(entries, stat.Name()) + prev := "" + if index > 0 { + prev = entries[index-1].Name() + } + next := "" + if index < len(entries)-1 { + next = entries[index+1].Name() + } + t.ExecuteTemplate(w, "archive-head.html", + Archive{stat.Name(), prev, next}) + + for i, f := range cbz.File { + if isImageFile(f) { + t.ExecuteTemplate(w, "archive-page.html", + Page{i, f.Name}) + } + } + t.ExecuteTemplate(w, "archive-foot.html", nil) + }) + + addr := os.Getenv("PHYLACTERY_ADDRESS") + log.Println("Listening on", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} |