// 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" "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 escape ensures that question marks in path // are not recognized as URL parameters. func escape(name string) template.URL { return template.URL(strings.Replace(name, "?", "%3f", -1)) } // Function find searches for the directory entry of given name. func find(entries []os.DirEntry, name string) int { for i, entry := range entries { if entry.Name() == name { return i } } return -1 } // Function main starts Phylactery serving comics from PHYLACTERY_LIBRARY // and listening on PHYLACTERY_ADDRESS. func main() { http.Handle("/static/", http.FileServer(http.FS(static))) t, err := template.New("").Funcs(template.FuncMap{ "escape": escape, }).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 } 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 } // 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 r.ParseForm() 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 { image, _ := cbz.File[i].Open() defer image.Close() buf := make([]byte, 512) n, _ := image.Read(buf) mime := http.DetectContentType(buf[:n]) if strings.HasPrefix(mime, "image/") { 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)) }