diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e7a0d..cddeca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.0] - 2021-06-10 +### Changed +- time is calculated only once +- updated the mimetype returned for the navigation and acquisition xmls + ## [0.0.11] - 2021-06-05 ### Changed - return to filepath as the best way to handle paths for different platforms. diff --git a/internal/service/service.go b/internal/service/service.go index 54c2c9b..b77d60f 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -27,6 +27,12 @@ func init() { _ = mime.AddExtensionType(".fb2", "text/fb2+xml") } +const ( + pathTypeFile = iota + pathTypeDirOfDirs + pathTypeDirOfFiles +) + type OPDS struct { DirRoot string Author string @@ -34,42 +40,42 @@ type OPDS struct { AuthorURI string } -var TimeNowFunc = timeNow +var TimeNow = timeNowFunc() +// 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 func (s OPDS) Handler(w http.ResponseWriter, req *http.Request) error { fPath := filepath.Join(s.DirRoot, req.URL.Path) log.Printf("fPath:'%s'", fPath) - fi, err := os.Stat(fPath) - if err != nil { - return err - } - - if isFile(fi) { + if getPathType(fPath) == pathTypeFile { http.ServeFile(w, req, fPath) return nil } - content, err := s.getContent(req, fPath) + 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, " ", " ") + w.Header().Add("Content-Type", "application/atom+xml;profile=opds-catalog;kind=acquisition") + } else { + content, err = xml.MarshalIndent(navFeed, " ", " ") + w.Header().Add("Content-Type", "application/atom+xml;profile=opds-catalog;kind=navigation") + } if err != nil { + log.Printf("error while serving '%s': %s", fPath, err) return err } content = append([]byte(xml.Header), content...) - http.ServeContent(w, req, "feed.xml", TimeNowFunc(), bytes.NewReader(content)) - return nil -} + http.ServeContent(w, req, "feed.xml", TimeNow(), bytes.NewReader(content)) -func (s OPDS) getContent(req *http.Request, dirpath string) (result []byte, err error) { - feed := s.makeFeed(dirpath, req) - if getPathType(dirpath) == pathTypeDirOfFiles { - acFeed := &opds.AcquisitionFeed{&feed, "http://purl.org/dc/terms/", "http://opds-spec.org/2010/catalog"} - result, err = xml.MarshalIndent(acFeed, " ", " ") - } else { - result, err = xml.MarshalIndent(feed, " ", " ") - } - return + return nil } const navigationType = "application/atom+xml;profile=opds-catalog;kind=navigation" @@ -79,7 +85,7 @@ func (s OPDS) makeFeed(dirpath string, req *http.Request) atom.Feed { ID(req.URL.Path). Title("Catalog in " + req.URL.Path). Author(opds.AuthorBuilder.Name(s.Author).Email(s.AuthorEmail).URI(s.AuthorURI).Build()). - Updated(TimeNowFunc()). + Updated(TimeNow()). AddLink(opds.LinkBuilder.Rel("start").Href("/").Type(navigationType).Build()) fis, _ := ioutil.ReadDir(dirpath) @@ -89,12 +95,12 @@ func (s OPDS) makeFeed(dirpath string, req *http.Request) atom.Feed { AddEntry(opds.EntryBuilder. ID(req.URL.Path + fi.Name()). Title(fi.Name()). - Updated(TimeNowFunc()). - Published(TimeNowFunc()). + Updated(TimeNow()). + Published(TimeNow()). AddLink(opds.LinkBuilder. Rel(getRel(fi.Name(), pathType)). Title(fi.Name()). - Href(getHref(req, fi.Name())). + Href(filepath.Join(req.URL.RequestURI(), url.PathEscape(fi.Name()))). Type(getType(fi.Name(), pathType)). Build()). Build()) @@ -117,22 +123,18 @@ func getRel(name string, pathType int) string { } func getType(name string, pathType int) string { - if pathType == pathTypeFile { + switch pathType { + case pathTypeFile: return mime.TypeByExtension(filepath.Ext(name)) + 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") } - return "application/atom+xml;profile=opds-catalog;kind=acquisition" } -func getHref(req *http.Request, name string) string { - return filepath.Join(req.URL.RequestURI(), url.PathEscape(name)) -} - -const ( - pathTypeFile = iota - pathTypeDirOfDirs - pathTypeDirOfFiles -) - func getPathType(dirpath string) int { fi, _ := os.Stat(dirpath) if isFile(fi) { @@ -153,6 +155,7 @@ func isFile(fi os.FileInfo) bool { return !fi.IsDir() } -func timeNow() time.Time { - return time.Now() +func timeNowFunc() func() time.Time { + t := time.Now() + return func() time.Time { return t } } diff --git a/internal/service/service_test.go b/internal/service/service_test.go index 33d321b..434b8e1 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -14,9 +14,9 @@ import ( func TestHandler(t *testing.T) { // pre-setup - nowFn := service.TimeNowFunc + nowFn := service.TimeNow defer func() { - service.TimeNowFunc = nowFn + service.TimeNow = nowFn }() tests := map[string]struct { @@ -24,8 +24,8 @@ func TestHandler(t *testing.T) { want string WantedContentType string }{ - "feed (dir of folders )": {input: "/", want: feed, WantedContentType: "application/xml"}, - "acquisitionFeed(dir of files)": {input: "/mybook", want: acquisitionFeed, WantedContentType: "application/xml"}, + "feed (dir of dirs )": {input: "/", want: feed, WantedContentType: "application/atom+xml;profile=opds-catalog;kind=navigation"}, + "acquisitionFeed(dir of files)": {input: "/mybook", want: acquisitionFeed, WantedContentType: "application/atom+xml;profile=opds-catalog;kind=acquisition"}, "servingAFile": {input: "/mybook/mybook.txt", want: "Fixture", WantedContentType: "text/plain; charset=utf-8"}, "serving file with spaces": {input: "/mybook/mybook%20copy.txt", want: "Fixture", WantedContentType: "text/plain; charset=utf-8"}, } @@ -36,7 +36,7 @@ func TestHandler(t *testing.T) { s := service.OPDS{"testdata", "", "", ""} w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, tc.input, nil) - service.TimeNowFunc = func() time.Time { + service.TimeNow = func() time.Time { return time.Date(2020, 05, 25, 00, 00, 00, 0, time.UTC) }