Updated with latest and title top level
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
This commit is contained in:
parent
b55b29e661
commit
7c163203b3
25
.github/workflows/go.yml
vendored
25
.github/workflows/go.yml
vendored
@ -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 ./...
|
|
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@ -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 }}
|
|
||||||
|
|
@ -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:'
|
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -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"]
|
61
Makefile
Normal file
61
Makefile
Normal file
@ -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
|
1
go.mod
1
go.mod
@ -5,6 +5,7 @@ go 1.12
|
|||||||
require (
|
require (
|
||||||
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939
|
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
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
|
github.com/stretchr/testify v1.7.0
|
||||||
golang.org/x/tools v0.0.0-20170217234718-8e779ee0a450
|
golang.org/x/tools v0.0.0-20170217234718-8e779ee0a450
|
||||||
)
|
)
|
||||||
|
2
go.sum
2
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
@ -6,6 +6,8 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
@ -13,10 +15,11 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dubyte/dir2opds/opds"
|
"github.com/dubyte/dir2opds/opds"
|
||||||
"golang.org/x/tools/blog/atom"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -25,6 +28,7 @@ func init() {
|
|||||||
_ = mime.AddExtensionType(".cbz", "application/x-cbz")
|
_ = mime.AddExtensionType(".cbz", "application/x-cbz")
|
||||||
_ = mime.AddExtensionType(".cbr", "application/x-cbr")
|
_ = mime.AddExtensionType(".cbr", "application/x-cbr")
|
||||||
_ = mime.AddExtensionType(".fb2", "text/fb2+xml")
|
_ = mime.AddExtensionType(".fb2", "text/fb2+xml")
|
||||||
|
_ = mime.AddExtensionType(".pdf", "application/pdf")
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -33,6 +37,15 @@ const (
|
|||||||
pathTypeDirOfFiles
|
pathTypeDirOfFiles
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var files = []BookFile{}
|
||||||
|
|
||||||
|
type BookFile struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Author string
|
||||||
|
FileInfo fs.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
type OPDS struct {
|
type OPDS struct {
|
||||||
DirRoot string
|
DirRoot string
|
||||||
Author string
|
Author string
|
||||||
@ -42,23 +55,100 @@ type OPDS struct {
|
|||||||
|
|
||||||
var TimeNow = timeNowFunc()
|
var TimeNow = timeNowFunc()
|
||||||
|
|
||||||
|
const navigationType = "application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||||
|
|
||||||
// Handler serve the content of a book file or
|
// Handler serve the content of a book file or
|
||||||
// returns an Acquisition Feed when the entries are documents or
|
// returns an Acquisition Feed when the entries are documents or
|
||||||
// returns an Navegation Feed when the entries are other folders
|
// returns an Navegation Feed when the entries are other folders
|
||||||
func (s OPDS) Handler(w http.ResponseWriter, req *http.Request) error {
|
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)
|
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)
|
http.ServeFile(w, req, fPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
navFeed := s.makeFeed(fPath, req)
|
navFeed := feedBuilder.Build()
|
||||||
|
|
||||||
var content []byte
|
var content []byte
|
||||||
var err error
|
|
||||||
if getPathType(fPath) == pathTypeDirOfFiles {
|
if getPathType(fPath) == pathTypeDirOfFiles {
|
||||||
acFeed := &opds.AcquisitionFeed{Feed: &navFeed, Dc: "http://purl.org/dc/terms/", Opds: "http://opds-spec.org/2010/catalog"}
|
acFeed := &opds.AcquisitionFeed{Feed: &navFeed, Dc: "http://purl.org/dc/terms/", Opds: "http://opds-spec.org/2010/catalog"}
|
||||||
content, err = xml.MarshalIndent(acFeed, " ", " ")
|
content, err = xml.MarshalIndent(acFeed, " ", " ")
|
||||||
@ -78,36 +168,6 @@ func (s OPDS) Handler(w http.ResponseWriter, req *http.Request) error {
|
|||||||
return nil
|
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 {
|
func getRel(name string, pathType int) string {
|
||||||
if pathType == pathTypeDirOfFiles || pathType == pathTypeDirOfDirs {
|
if pathType == pathTypeDirOfFiles || pathType == pathTypeDirOfDirs {
|
||||||
return "subsection"
|
return "subsection"
|
||||||
@ -159,3 +219,21 @@ func timeNowFunc() func() time.Time {
|
|||||||
t := time.Now()
|
t := time.Now()
|
||||||
return func() time.Time { return t }
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user