diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | atom.go | 129 | ||||
-rw-r--r-- | misc.go | 52 | ||||
-rw-r--r-- | serv.go (renamed from main.go) | 61 | ||||
-rw-r--r-- | templates/directory.html | 1 |
5 files changed, 216 insertions, 31 deletions
diff --git a/README.md b/README.md index c98f657..118af79 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Phylactery is a web server rendering comic books directly from ZIP archives. ## Installation - go build -o $prefix/bin/phylactery main.go + go build -o $prefix/bin/phylactery *.go ## Usage @@ -14,7 +14,7 @@ to the listening address and run `phylactery`. ## Hacking - PHYLACTERY_LIBRARY=/path/to/library PHYLACTERY_ADDRESS=:42069 go run main.go + PHYLACTERY_LIBRARY=/path/to/library PHYLACTERY_ADDRESS=:42069 go run *.go ## Contributing diff --git a/atom.go b/atom.go new file mode 100644 index 0000000..d905348 --- /dev/null +++ b/atom.go @@ -0,0 +1,129 @@ +// Atom feed synthesizer +// 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" + "encoding/xml" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +// Type Img represents an XHTML img tag. +type Img struct { + Src string `xml:"src,attr"` + Alt string `xml:"alt,attr"` +} + +// Type Div represents an XHTML div tag. +type Div struct { + NS string `xml:"xmlns,attr"` + Img []Img `xml:"img"` +} + +// Type AtomLink represents an atom:link element. +type AtomLink struct { + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` +} + +// Type AtomContent represents an atom:content element. +type AtomContent struct { + Type string `xml:"type,attr"` + Div Div `xml:"div"` +} + +// Type AtomEntry represents an Atom Entry Document. +type AtomEntry struct { + Title string `xml:"title"` + ID string `xml:"id"` + Link []AtomLink `xml:"link"` + Updated time.Time `xml:"updated"` + Content AtomContent `xml:"content"` +} + +// Type AtomFeed represents an Atom Feed Document. +type AtomFeed struct { + XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` + Title string `xml:"title"` + ID string `xml:"id"` + Link []AtomLink `xml:"link"` + Updated time.Time `xml:"updated"` + Author string `xml:"author>name"` + Entry []AtomEntry `xml:"entry"` +} + +// Function synthesizeAtom generates an Atom feed for given directory. +func synthesizeAtom(r *http.Request, dir string, updated time.Time) AtomFeed { + baseURL := "http://" + r.Host + if r.TLS != nil { + baseURL = "https" + baseURL[4:] + } + var entries []AtomEntry + filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + name := strings.TrimPrefix(p, dir)[1:] + u := escape(baseURL + path.Join(r.URL.Path, name)) + var images []Img + cbz, err := zip.OpenReader(p) + if err != nil { + return nil + } + defer cbz.Close() + for i, f := range cbz.File { + if isImageFile(f) { + continue + } + images = append(images, + Img{u + "?entry=" + strconv.Itoa(i), f.Name}) + } + + info, _ := d.Info() + entries = append(entries, AtomEntry{ + Title: name, + ID: name, + Link: []AtomLink{{"alternate", u}}, + Updated: info.ModTime(), + Content: AtomContent{ + "xhtml", + Div{"http://www.w3.org/1999/xhtml", images}, + }, + }) + return nil + }) + + id := escape(baseURL + r.URL.Path) + return AtomFeed{ + Title: r.URL.Path[1 : len(r.URL.Path)-1], + ID: id, + Link: []AtomLink{ + {"alternate", id}, + {"self", id + "?feed=atom"}, + }, + Updated: updated, + Entry: entries, + } +} diff --git a/misc.go b/misc.go new file mode 100644 index 0000000..700256c --- /dev/null +++ b/misc.go @@ -0,0 +1,52 @@ +// Miscellaneous utilities +// 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" + "net/http" + "os" + "strings" +) + +// Function escape ensures that question marks in path +// are not recognized as URL parameters. +func escape(name string) string { + return strings.ReplaceAll(name, "?", "%3f") +} + +// 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 isImageFile detects if given file is an image. +func isImageFile(file *zip.File) bool { + image, _ := file.Open() + defer image.Close() + buf := make([]byte, 512) + n, _ := image.Read(buf) + mime := http.DetectContentType(buf[:n]) + return strings.HasPrefix(mime, "image/") +} diff --git a/main.go b/serv.go index dfc01a5..f7e05dc 100644 --- a/main.go +++ b/serv.go @@ -21,6 +21,7 @@ package main import ( "archive/zip" "embed" + "encoding/xml" "html/template" "io" "log" @@ -45,9 +46,9 @@ type Page struct { // Type Archive represents a comic book zip archive. type Archive struct { - Title string - Prev string - Next string + Title string + Prev string + Next string } // Type Directory represents a library directory in file system. @@ -56,28 +57,14 @@ type Directory struct { 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. +// 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": escape, + "escape": func(name string) template.URL { + return template.URL(escape(name)) + }, }).ParseFS(templates, "templates/*.html") if err != nil { log.Fatal(err) @@ -90,6 +77,7 @@ func main() { http.NotFound(w, r) return } + r.ParseForm() if stat.IsDir() { // Redirect URL without a trailing slash @@ -100,6 +88,27 @@ func main() { 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 @@ -130,7 +139,6 @@ func main() { 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) { @@ -155,15 +163,10 @@ func main() { next = entries[index+1].Name() } t.ExecuteTemplate(w, "archive-head.html", - Archive{ stat.Name(), prev, next }) + 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/") { + if isImageFile(f) { t.ExecuteTemplate(w, "archive-page.html", Page{i, f.Name}) } diff --git a/templates/directory.html b/templates/directory.html index 570e526..15ee01c 100644 --- a/templates/directory.html +++ b/templates/directory.html @@ -1,5 +1,6 @@ {{template "base-head.html" -}} <link rel=stylesheet href=/static/directory.css> +<link rel='alternate' type='application/atom+xml' href='?feed=atom'> <title>{{.Title}}</title> <body> <main> |