// 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 . 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)) }