2021-05-06 06:48:02 +00:00
|
|
|
//package service provides a http handler that reads the path in the request.url and returns
|
|
|
|
// an xml document that follows the OPDS 1.1 standard
|
|
|
|
// https://specs.opds.io/opds-1.1.html
|
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/xml"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"mime"
|
|
|
|
"net/http"
|
2021-06-06 03:36:53 +00:00
|
|
|
"net/url"
|
2021-05-06 06:48:02 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/dubyte/dir2opds/opds"
|
|
|
|
"golang.org/x/tools/blog/atom"
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
_ = mime.AddExtensionType(".mobi", "application/x-mobipocket-ebook")
|
|
|
|
_ = mime.AddExtensionType(".epub", "application/epub+zip")
|
|
|
|
_ = mime.AddExtensionType(".cbz", "application/x-cbz")
|
|
|
|
_ = mime.AddExtensionType(".cbr", "application/x-cbr")
|
|
|
|
_ = mime.AddExtensionType(".fb2", "text/fb2+xml")
|
|
|
|
}
|
|
|
|
|
2021-06-10 06:18:53 +00:00
|
|
|
const (
|
|
|
|
pathTypeFile = iota
|
|
|
|
pathTypeDirOfDirs
|
|
|
|
pathTypeDirOfFiles
|
|
|
|
)
|
|
|
|
|
2021-05-06 06:48:02 +00:00
|
|
|
type OPDS struct {
|
|
|
|
DirRoot string
|
|
|
|
Author string
|
|
|
|
AuthorEmail string
|
|
|
|
AuthorURI string
|
|
|
|
}
|
|
|
|
|
2021-06-10 05:00:52 +00:00
|
|
|
var TimeNow = timeNowFunc()
|
2021-05-06 06:48:02 +00:00
|
|
|
|
2021-06-10 06:18:53 +00:00
|
|
|
// Handler serve the content of a book file or
|
|
|
|
// returns an Acquisition Feed when the entries are documents or
|
|
|
|
// returns an Navegation Feed when the entries are other folders
|
2021-05-06 06:48:02 +00:00
|
|
|
func (s OPDS) Handler(w http.ResponseWriter, req *http.Request) error {
|
2021-06-06 03:36:53 +00:00
|
|
|
fPath := filepath.Join(s.DirRoot, req.URL.Path)
|
2021-05-06 06:48:02 +00:00
|
|
|
|
|
|
|
log.Printf("fPath:'%s'", fPath)
|
|
|
|
|
2021-06-10 06:18:53 +00:00
|
|
|
if getPathType(fPath) == pathTypeFile {
|
2021-05-06 06:48:02 +00:00
|
|
|
http.ServeFile(w, req, fPath)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-06-10 06:18:53 +00:00
|
|
|
navFeed := s.makeFeed(fPath, req)
|
|
|
|
|
|
|
|
var content []byte
|
|
|
|
var err error
|
|
|
|
if getPathType(fPath) == pathTypeDirOfFiles {
|
|
|
|
acFeed := &opds.AcquisitionFeed{Feed: &navFeed, Dc: "http://purl.org/dc/terms/", Opds: "http://opds-spec.org/2010/catalog"}
|
|
|
|
content, err = xml.MarshalIndent(acFeed, " ", " ")
|
2021-06-10 06:40:08 +00:00
|
|
|
w.Header().Add("Content-Type", "application/atom+xml;profile=opds-catalog;kind=acquisition")
|
2021-06-10 06:18:53 +00:00
|
|
|
} else {
|
|
|
|
content, err = xml.MarshalIndent(navFeed, " ", " ")
|
2021-06-10 06:40:08 +00:00
|
|
|
w.Header().Add("Content-Type", "application/atom+xml;profile=opds-catalog;kind=navigation")
|
2021-06-10 06:18:53 +00:00
|
|
|
}
|
2021-05-06 06:48:02 +00:00
|
|
|
if err != nil {
|
2021-06-10 06:18:53 +00:00
|
|
|
log.Printf("error while serving '%s': %s", fPath, err)
|
2021-05-06 06:48:02 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
content = append([]byte(xml.Header), content...)
|
2021-06-10 05:00:52 +00:00
|
|
|
http.ServeContent(w, req, "feed.xml", TimeNow(), bytes.NewReader(content))
|
2021-05-06 06:48:02 +00:00
|
|
|
|
2021-06-10 06:18:53 +00:00
|
|
|
return nil
|
2021-05-06 06:48:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const navigationType = "application/atom+xml;profile=opds-catalog;kind=navigation"
|
|
|
|
|
|
|
|
func (s OPDS) makeFeed(dirpath string, req *http.Request) atom.Feed {
|
|
|
|
feedBuilder := opds.FeedBuilder.
|
|
|
|
ID(req.URL.Path).
|
|
|
|
Title("Catalog in " + req.URL.Path).
|
|
|
|
Author(opds.AuthorBuilder.Name(s.Author).Email(s.AuthorEmail).URI(s.AuthorURI).Build()).
|
2021-06-10 05:00:52 +00:00
|
|
|
Updated(TimeNow()).
|
2021-05-06 06:48:02 +00:00
|
|
|
AddLink(opds.LinkBuilder.Rel("start").Href("/").Type(navigationType).Build())
|
|
|
|
|
|
|
|
fis, _ := ioutil.ReadDir(dirpath)
|
|
|
|
for _, fi := range fis {
|
2021-06-06 03:36:53 +00:00
|
|
|
pathType := getPathType(filepath.Join(dirpath, fi.Name()))
|
2021-05-06 06:48:02 +00:00
|
|
|
feedBuilder = feedBuilder.
|
|
|
|
AddEntry(opds.EntryBuilder.
|
|
|
|
ID(req.URL.Path + fi.Name()).
|
|
|
|
Title(fi.Name()).
|
2021-06-10 05:00:52 +00:00
|
|
|
Updated(TimeNow()).
|
|
|
|
Published(TimeNow()).
|
2021-05-06 06:48:02 +00:00
|
|
|
AddLink(opds.LinkBuilder.
|
|
|
|
Rel(getRel(fi.Name(), pathType)).
|
|
|
|
Title(fi.Name()).
|
2021-06-10 06:18:53 +00:00
|
|
|
Href(filepath.Join(req.URL.RequestURI(), url.PathEscape(fi.Name()))).
|
2021-05-06 06:48:02 +00:00
|
|
|
Type(getType(fi.Name(), pathType)).
|
|
|
|
Build()).
|
|
|
|
Build())
|
|
|
|
}
|
|
|
|
return feedBuilder.Build()
|
|
|
|
}
|
|
|
|
|
|
|
|
func getRel(name string, pathType int) string {
|
|
|
|
if pathType == pathTypeDirOfFiles || pathType == pathTypeDirOfDirs {
|
|
|
|
return "subsection"
|
|
|
|
}
|
|
|
|
|
|
|
|
ext := filepath.Ext(name)
|
|
|
|
if ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" {
|
|
|
|
return "http://opds-spec.org/image/thumbnail"
|
|
|
|
}
|
|
|
|
|
|
|
|
// mobi, epub, etc
|
|
|
|
return "http://opds-spec.org/acquisition"
|
|
|
|
}
|
|
|
|
|
|
|
|
func getType(name string, pathType int) string {
|
2021-06-10 06:40:08 +00:00
|
|
|
switch pathType {
|
|
|
|
case pathTypeFile:
|
2021-05-06 06:48:02 +00:00
|
|
|
return mime.TypeByExtension(filepath.Ext(name))
|
2021-06-10 06:40:08 +00:00
|
|
|
case pathTypeDirOfFiles:
|
|
|
|
return "application/atom+xml;profile=opds-catalog;kind=acquisition"
|
|
|
|
case pathTypeDirOfDirs:
|
|
|
|
return "application/atom+xml;profile=opds-catalog;kind=navigation"
|
|
|
|
default:
|
|
|
|
return mime.TypeByExtension("xml")
|
2021-05-06 06:48:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getPathType(dirpath string) int {
|
|
|
|
fi, _ := os.Stat(dirpath)
|
|
|
|
if isFile(fi) {
|
|
|
|
return pathTypeFile
|
|
|
|
}
|
|
|
|
|
|
|
|
fis, _ := ioutil.ReadDir(dirpath)
|
|
|
|
for _, fi := range fis {
|
|
|
|
if isFile(fi) {
|
|
|
|
return pathTypeDirOfFiles
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Directory of directories
|
|
|
|
return pathTypeDirOfDirs
|
|
|
|
}
|
|
|
|
|
|
|
|
func isFile(fi os.FileInfo) bool {
|
|
|
|
return !fi.IsDir()
|
|
|
|
}
|
|
|
|
|
2021-06-10 05:00:52 +00:00
|
|
|
func timeNowFunc() func() time.Time {
|
|
|
|
t := time.Now()
|
|
|
|
return func() time.Time { return t }
|
2021-05-06 06:48:02 +00:00
|
|
|
}
|