From 7c163203b38116ca346f62b5fb01e49bfc065cc2 Mon Sep 17 00:00:00 2001 From: Marcus Noble Date: Fri, 12 Nov 2021 17:39:43 +0000 Subject: [PATCH] Updated with latest and title top level Signed-off-by: Marcus Noble --- .github/workflows/go.yml | 25 ------ .github/workflows/release.yml | 36 --------- .goreleaser.yml | 63 --------------- Dockerfile | 13 +++ Makefile | 61 ++++++++++++++ go.mod | 1 + go.sum | 2 + internal/service/service.go | 148 ++++++++++++++++++++++++++-------- 8 files changed, 190 insertions(+), 159 deletions(-) delete mode 100644 .github/workflows/go.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .goreleaser.yml create mode 100644 Dockerfile create mode 100644 Makefile diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index a4021c0..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Go - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 - - - name: Build - run: go build -v ./... - - - name: Test - run: go test -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1dce362..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: goreleaser - -on: - push: - tags: - - '*' - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 - - - name: Test - run: go test -v ./... - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 0abeb34..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,63 +0,0 @@ -project_name: dir2opds -env: - - GO111MODULE=on - - GOPROXY=https://proxy.golang.org -before: - hooks: - - go mod download -builds: - - env: - - CGO_ENABLED=0 - flags: - - -buildmode - - exe - goos: - - darwin - - linux - - windows - - freebsd - - netbsd - - openbsd - - dragonfly - goarch: - - amd64 - - 386 - - arm - - arm64 - goarm: - - 7 - - 6 - ignore: - - goos: darwin - goarch: 386 - -archives: - - - id: "dir2opds" - builds: ['dir2opds'] - format: tar.gz - format_overrides: - - goos: windows - format: zip - replacements: - amd64: 64bit - 386: 32bit - arm: ARM - arm64: ARM64 - darwin: macOS - linux: Linux - windows: Windows - openbsd: OpenBSD - netbsd: NetBSD - freebsd: FreeBSD - dragonfly: DragonFlyBSD -checksum: - name_template: 'checksums.txt' -snapshot: - name_template: "{{ .Tag }}-next" -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..de6d464 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.16-alpine AS builder +RUN apk update && apk add --no-cache git && apk add -U --no-cache ca-certificates +WORKDIR /app/ +ADD go.mod go.sum ./ +RUN go mod download +ADD . . +RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o dir2opds main.go + +FROM scratch +WORKDIR /app/ +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app/dir2opds /app/dir2opds +ENTRYPOINT ["/app/dir2opds"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3bf32fc --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +.DEFAULT_GOAL := default + +IMAGE ?= docker.cluster.fun/averagemarcus/dir2opds:latest + +.PHONY: test # Run all tests, linting and format checks +test: lint check-format run-tests + +.PHONY: lint # Perform lint checks against code +lint: + @go vet && golint -set_exit_status ./... + +.PHONY: check-format # Checks code formatting and returns a non-zero exit code if formatting errors found +check-format: + @gofmt -e -l . + +.PHONY: format # Performs automatic format fixes on all code +format: + @gofmt -s -w . + +.PHONY: run-tests # Runs all tests +run-tests: + @go test + +.PHONY: fetch-deps # Fetch all project dependencies +fetch-deps:s + @go mod tidy + +.PHONY: build # Build the project +build: lint check-format fetch-deps + @go build -o PROJECT_NAME main.go + +.PHONY: docker-build # Build the docker image +docker-build: + @docker build -t $(IMAGE) . + +.PHONY: docker-publish # Push the docker image to the remote registry +docker-publish: + @docker push $(IMAGE) + +.PHONY: run # Run the application +run: + @go run main.go + +.PHONY: ci # Perform CI specific tasks to perform on a pull request +ci: + @echo "⚠️ 'ci' unimplemented" + +.PHONY: release # Release the latest version of the application +release: + @kubectl --namespace dir2opds set image deployment dir2opds web=docker.cluster.fun/averagemarcus/dir2opds:$(SHA) + +.PHONY: help # Show this list of commands +help: + @echo "dir2opds" + @echo "Usage: make [target]" + @echo "" + @echo "target description" | expand -t20 + @echo "-----------------------------------" + @grep '^.PHONY: .* #' Makefile | sed 's/\.PHONY: \(.*\) # \(.*\)/\1 \2/' | expand -t20 + +default: test build diff --git a/go.mod b/go.mod index 9b3ea43..5bb7c1b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/sebojanko/epub v0.0.0-20190901091113-b71e439ac337 github.com/stretchr/testify v1.7.0 golang.org/x/tools v0.0.0-20170217234718-8e779ee0a450 ) diff --git a/go.sum b/go.sum index 1ed6fec..6bf2e3f 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sebojanko/epub v0.0.0-20190901091113-b71e439ac337 h1:EcpYTVnuaho+mvtct5HLev0j8r8mpg5PbSI9RxD+1qI= +github.com/sebojanko/epub v0.0.0-20190901091113-b71e439ac337/go.mod h1:/F41nd9ouioh76JzQyYGQbZzzCRxvGLpWu2TnNhP3X0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/service/service.go b/internal/service/service.go index b77d60f..7e1aef8 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -6,6 +6,8 @@ package service import ( "bytes" "encoding/xml" + "fmt" + "io/fs" "io/ioutil" "log" "mime" @@ -13,10 +15,11 @@ import ( "net/url" "os" "path/filepath" + "sort" + "strings" "time" "github.com/dubyte/dir2opds/opds" - "golang.org/x/tools/blog/atom" ) func init() { @@ -25,6 +28,7 @@ func init() { _ = mime.AddExtensionType(".cbz", "application/x-cbz") _ = mime.AddExtensionType(".cbr", "application/x-cbr") _ = mime.AddExtensionType(".fb2", "text/fb2+xml") + _ = mime.AddExtensionType(".pdf", "application/pdf") } const ( @@ -33,6 +37,15 @@ const ( pathTypeDirOfFiles ) +var files = []BookFile{} + +type BookFile struct { + Name string + Path string + Author string + FileInfo fs.FileInfo +} + type OPDS struct { DirRoot string Author string @@ -42,23 +55,100 @@ type OPDS struct { var TimeNow = timeNowFunc() +const navigationType = "application/atom+xml;profile=opds-catalog;kind=navigation" + // 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) + var err error + urlPath, err := url.PathUnescape(req.URL.Path) + if err != nil { + log.Printf("error while serving '%s': %s", req.URL.Path, err) + return err + } + fPath := filepath.Join(s.DirRoot, urlPath) + log.Printf("urlPath:'%s'", urlPath) log.Printf("fPath:'%s'", fPath) - if getPathType(fPath) == pathTypeFile { + feedBuilder := opds.FeedBuilder. + ID(urlPath). + Title(strings.Title(strings.TrimPrefix(urlPath, "/"))). + Author(opds.AuthorBuilder.Name(s.Author).Email(s.AuthorEmail).URI(s.AuthorURI).Build()). + Updated(TimeNow()). + AddLink(opds.LinkBuilder.Rel("start").Href("/").Type(navigationType).Build()) + + if urlPath == "/" { + files = []BookFile{} + filepath.WalkDir(s.DirRoot, func(path string, de fs.DirEntry, err error) error { + if !de.IsDir() { + file, err := de.Info() + if err != nil { + fmt.Println(err) + return nil + } + + files = append(files, BookFile{ + Name: file.Name(), + Path: path, + FileInfo: file, + }) + } + return nil + }) + + feedBuilder = feedBuilder. + AddEntry(opds.EntryBuilder. + ID("/latest"). + Title("Latest"). + Updated(TimeNow()). + Published(TimeNow()). + AddLink(opds.LinkBuilder.Rel(getRel("latest", pathTypeDirOfDirs)).Title("Latest").Href(filepath.Join("/", url.PathEscape("latest"))).Type(getType("Latest", pathTypeDirOfDirs)).Build()). + Build()). + AddEntry(opds.EntryBuilder. + ID("/titles"). + Title("By Title"). + Updated(TimeNow()). + Published(TimeNow()). + AddLink(opds.LinkBuilder.Rel(getRel("titles", pathTypeDirOfDirs)).Title("By Title").Href(filepath.Join("/", url.PathEscape("titles"))).Type(getType("By Title", pathTypeDirOfDirs)).Build()). + Build()) + } else if urlPath == "/latest" { + fPath = strings.TrimSuffix(fPath, "/latest") + for _, f := range sortByLatest(files) { + fi := f.FileInfo + pathType := getPathType(f.Path) + feedBuilder = feedBuilder. + AddEntry(opds.EntryBuilder. + ID(urlPath + fi.Name()). + Title(fi.Name()). + Updated(TimeNow()). + Published(TimeNow()). + AddLink(opds.LinkBuilder.Rel(getRel(f.Path, pathType)).Title(fi.Name()).Href(filepath.Join("/", url.PathEscape(strings.TrimPrefix(f.Path, s.DirRoot)))).Type(getType(f.Path, pathType)).Build()). + Build()) + } + } else if urlPath == "/titles" { + fPath = strings.TrimSuffix(fPath, "/titles") + for _, f := range sortByTitle(files) { + fi := f.FileInfo + pathType := getPathType(f.Path) + feedBuilder = feedBuilder. + AddEntry(opds.EntryBuilder. + ID(urlPath + fi.Name()). + Title(fi.Name()). + Updated(TimeNow()). + Published(TimeNow()). + AddLink(opds.LinkBuilder.Rel(getRel(f.Path, pathType)).Title(fi.Name()).Href(filepath.Join("/", url.PathEscape(strings.TrimPrefix(f.Path, s.DirRoot)))).Type(getType(f.Path, pathType)).Build()). + Build()) + } + } else if getPathType(fPath) == pathTypeFile { http.ServeFile(w, req, fPath) return nil } - navFeed := s.makeFeed(fPath, req) + navFeed := feedBuilder.Build() 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, " ", " ") @@ -78,36 +168,6 @@ func (s OPDS) Handler(w http.ResponseWriter, req *http.Request) error { return nil } -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()). - Updated(TimeNow()). - AddLink(opds.LinkBuilder.Rel("start").Href("/").Type(navigationType).Build()) - - fis, _ := ioutil.ReadDir(dirpath) - for _, fi := range fis { - pathType := getPathType(filepath.Join(dirpath, fi.Name())) - feedBuilder = feedBuilder. - AddEntry(opds.EntryBuilder. - ID(req.URL.Path + fi.Name()). - Title(fi.Name()). - Updated(TimeNow()). - Published(TimeNow()). - AddLink(opds.LinkBuilder. - Rel(getRel(fi.Name(), pathType)). - Title(fi.Name()). - Href(filepath.Join(req.URL.RequestURI(), url.PathEscape(fi.Name()))). - Type(getType(fi.Name(), pathType)). - Build()). - Build()) - } - return feedBuilder.Build() -} - func getRel(name string, pathType int) string { if pathType == pathTypeDirOfFiles || pathType == pathTypeDirOfDirs { return "subsection" @@ -159,3 +219,21 @@ func timeNowFunc() func() time.Time { t := time.Now() return func() time.Time { return t } } + +func sortByLatest(files []BookFile) []BookFile { + sortedFiles := files + sort.Slice(sortedFiles, func(i, j int) bool { + return sortedFiles[i].FileInfo.ModTime().After(sortedFiles[j].FileInfo.ModTime()) + }) + + return sortedFiles +} + +func sortByTitle(files []BookFile) []BookFile { + sortedFiles := files + sort.Slice(sortedFiles, func(i, j int) bool { + return strings.Compare(sortedFiles[i].Name, sortedFiles[j].Name) < 0 + }) + + return sortedFiles +}