Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
build-args: |
KOMPANION_VERSION=${{ steps.get_tag.outputs.TAG }}
push: true
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ WORKDIR /app
ARG KOMPANION_VERSION=local
ENV KOMPANION_VERSION=$KOMPANION_VERSION

RUN GOOS=linux GOARCH=amd64 \
ARG TARGETARCH
RUN GOOS=linux GOARCH=${TARGETARCH:-amd64} \
go build -ldflags "-X main.Version=$KOMPANION_VERSION" -tags migrate -o /bin/app ./cmd/app


# Step 3: Final
FROM golang:1.22.5-alpine
ENV GIN_MODE=release
Expand Down
10 changes: 6 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ endif
LOCAL_BIN:=$(CURDIR)/bin
PATH:=$(LOCAL_BIN):$(PATH)

COMPOSE_CMD := $(shell command -v docker-compose >/dev/null 2>&1 && echo "docker-compose" || echo "docker compose")

# HELP =================================================================================================================
# This will output the help for each task
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
Expand All @@ -18,19 +20,19 @@ help: ## Display this help screen
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

compose-up: ### Run docker-compose
docker-compose up --build -d postgres && docker-compose logs -f
$(COMPOSE_CMD) up --build -d postgres && $(COMPOSE_CMD) logs -f
.PHONY: compose-up

compose-all: ### Run all containers
UID=$(id -u) GID=$(id -g) docker-compose up --build
UID=$(id -u) GID=$(id -g) $(COMPOSE_CMD) up --build
.PHONY: compose-all

compose-up-integration-test: ### Run docker-compose with integration test
docker-compose -f docker-compose.yml -f docker-compose-integration.yml up --build --abort-on-container-exit --exit-code-from integration
$(COMPOSE_CMD) -f docker-compose.yml -f docker-compose-integration.yml up --build --abort-on-container-exit --exit-code-from integration
.PHONY: compose-up-integration-test

compose-down: ### Down docker-compose
docker-compose down --remove-orphans
$(COMPOSE_CMD) down --remove-orphans
.PHONY: compose-down

run: ### swag run
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

KOmpanion is a minimalistic library web application, that tightly coupled to KOReader features.
Main features are:

- upload and view your bookshelf
- OPDS to download books
- KOReader sync progress API
- KOReader book stats via WebDAV

What KOmpanion is NOT about:

- web interface for book reading (just install KOReader)
- converter between formats (I don't want to do another calibre)

## Why KOReader for all?

KOReader is the best available reader on the market (personal opinion).
Features, that can buy you in:

- sync progress between tablet, phone and ebook
- extensive stats for book reading: total time, time per page, estimates

Expand All @@ -40,6 +43,7 @@ Features, that can buy you in:
- `KOMPANION_AUTH_PASSWORD` - required for setup
- `KOMPANION_AUTH_STORAGE` - postgres or memory (default: postgres)
- `KOMPANION_HTTP_PORT` - port for service (default: 8080)
- `KOMPANION_URL_PREFIX` - base url for service (default: "/")
- `KOMPANION_LOG_LEVEL` - debug, info, error (default: info)
- `KOMPANION_PG_POOL_MAX` - integer number for pooling connections (default: 2)
- `KOMPANION_PG_URL` - postgresql link
Expand All @@ -53,6 +57,7 @@ Features, that can buy you in:
### Web interface

First of all, you need to add your devices:

1. Go to service
2. Login
3. Click devices
Expand All @@ -63,23 +68,25 @@ First of all, you need to add your devices:
### KOReader

Go to following plugins:

1. Cloud storage
1. Add new WebDAV: URL - `https://your-kompanion.org/webdav/`, username - device name, password - password
1. Add new WebDAV: URL - `https://your-kompanion.org/webdav/`, username - device name, password - password
2. Statistics - Settings - Cloud sync
1. It's OKAY to have empty list, just press on **Long press to choose current folder**.
1. It's OKAY to have empty list, just press on **Long press to choose current folder**.
3. Open book - tools - Progress sync
1. Custom sync server: `https://your-kompanion.org/`
1. Login: username - device name, password - password
1. Custom sync server: `https://your-kompanion.org/`
2. Login: username - device name, password - password
4. To setup OPDS catalog:
1. Toolbar -> Search -> OPDS Catalog
2. Hit plus
3. Catalog URL: `https://your-kompanion.org/opds/`, username - device name, password - password
1. Toolbar -> Search -> OPDS Catalog
2. Hit plus
3. Catalog URL: `https://your-kompanion.org/opds/`, username - device name, password - password

## Development

Project was started with [go-clean-template](https://github.com/evrone/go-clean-template), but then heavily modified.

Local development:

```sh
# Postgres
$ make compose-up
Expand All @@ -88,6 +95,7 @@ $ make run
```

Integration tests (can be run in CI):

```sh
# DB, app + migrations, integration tests
$ make compose-up-integration-test
Expand Down
7 changes: 5 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ type (

// HTTP -.
HTTP struct {
Port string
Port string
UrlPrefix string
}

// Log -.
Expand Down Expand Up @@ -117,9 +118,11 @@ func readHTTPConfig() (HTTP, error) {
if port == "" {
port = "8080"
}
url_prefix := readPrefixedEnv("URL_PREFIX")

return HTTP{
Port: port,
Port: port,
UrlPrefix: strings.TrimSuffix(url_prefix, "/"),
}, nil
}

Expand Down
8 changes: 4 additions & 4 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ func Run(cfg *config.Config) {

// HTTP Server
handler := gin.New()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify implementation, crate RouterGroup with prefix here, and then forwared it to the NewRouter without changes to the code

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router := gin.New()
handler := r.Group(cfg.Urlprefix)

web.NewRouter(handler, l, authService, progress, shelf, rs, cfg.Version)
v1.NewRouter(handler, l, authService, progress, shelf)
opds.NewRouter(handler, l, authService, progress, shelf)
webdav.NewRouter(handler, authService, l, rs)
web.NewRouter(handler, cfg.UrlPrefix, l, authService, progress, shelf, rs, cfg.Version)
v1.NewRouter(handler, cfg.UrlPrefix, l, authService, progress, shelf)
opds.NewRouter(handler, cfg.UrlPrefix, l, authService, progress, shelf)
webdav.NewRouter(handler, cfg.UrlPrefix, authService, l, rs)
httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port))

// Waiting signal
Expand Down
10 changes: 5 additions & 5 deletions internal/controller/http/opds/opds.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ type Summary struct {
Text string `xml:",chardata"`
}

func BuildFeed(id, title, href string, entries []Entry, additionalLinks []Link) *Feed {
func BuildFeed(id, title, href string, entries []Entry, additionalLinks []Link, urlPrefix string) *Feed {
finalLinks := []Link{
{
Href: "/opds/",
Href: urlPrefix + "/opds/",
Type: DirMime,
Rel: "start",
},
Expand All @@ -67,7 +67,7 @@ func BuildFeed(id, title, href string, entries []Entry, additionalLinks []Link)
Rel: "self",
},
{
Href: "/opds/search/{searchTerms}/",
Href: urlPrefix + "/opds/search/{searchTerms}/",
Type: "application/atom+xml",
Rel: "search",
},
Expand All @@ -83,7 +83,7 @@ func BuildFeed(id, title, href string, entries []Entry, additionalLinks []Link)
}
}

func translateBooksToEntries(books []entity.Book) []Entry {
func translateBooksToEntries(books []entity.Book, urlPrefix string) []Entry {
entries := make([]Entry, 0, len(books))
for _, book := range books {
entries = append(entries, Entry{
Expand All @@ -95,7 +95,7 @@ func translateBooksToEntries(books []entity.Book) []Entry {
},
Link: []Link{
{
Href: fmt.Sprintf("/opds/book/%s/download", book.ID),
Href: fmt.Sprintf(urlPrefix+"/opds/book/%s/download", book.ID),
Type: book.MimeType(),
Rel: FileRel,
// Mtime: book.UpdatedAt.Format(AtomTime),
Expand Down
20 changes: 11 additions & 9 deletions internal/controller/http/opds/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ import (
)

type OPDSRouter struct {
books library.Shelf
logger logger.Interface
urlPrefix string
books library.Shelf
logger logger.Interface
}

func NewRouter(
handler *gin.Engine,
urlPrefix string,
l logger.Interface,
a auth.AuthInterface,
p sync.Progress,
shelf library.Shelf) {
sh := &OPDSRouter{shelf, l}
sh := &OPDSRouter{urlPrefix, shelf, l}

h := handler.Group("/opds")
h := handler.Group(urlPrefix + "/opds")
h.Use(basicAuth(a))
{
h.GET("/", sh.listShelves)
Expand All @@ -43,14 +45,14 @@ func (r *OPDSRouter) listShelves(c *gin.Context) {
Title: "By Newest",
Link: []Link{
{
Href: "/opds/newest/",
Href: r.urlPrefix + "/opds/newest/",
Type: "application/atom+xml;type=feed;profile=opds-catalog",
},
},
},
}
links := []Link{}
feed := BuildFeed("urn:kompanion:main", "KOmpanion library", "/opds", shelves, links)
feed := BuildFeed("urn:kompanion:main", "KOmpanion library", r.urlPrefix+"/opds", shelves, links, r.urlPrefix)
c.XML(http.StatusOK, feed)
}

Expand All @@ -66,10 +68,10 @@ func (r *OPDSRouter) listNewest(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"message": "Internal server error", "code": 1001})
return
}
baseUrl := "/opds/newest/"
entries := translateBooksToEntries(books.Books)
baseUrl := r.urlPrefix + "/opds/newest/"
entries := translateBooksToEntries(books.Books, r.urlPrefix)
navLinks := formNavLinks(baseUrl, books)
feed := BuildFeed("urn:kompanion:newest", "KOmpanion library", baseUrl, entries, navLinks)
feed := BuildFeed("urn:kompanion:newest", "KOmpanion library", baseUrl, entries, navLinks, r.urlPrefix)
c.XML(http.StatusOK, feed)
}

Expand Down
10 changes: 5 additions & 5 deletions internal/controller/http/v1/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import (
)

// NewRouter -.
func NewRouter(handler *gin.Engine, l logger.Interface, a auth.AuthInterface, p sync.Progress, shelf library.Shelf) {
func NewRouter(handler *gin.Engine, urlPrefix string, l logger.Interface, a auth.AuthInterface, p sync.Progress, shelf library.Shelf) {
// Options
handler.Use(gin.Logger())
handler.Use(gin.Recovery())

// K8s probe
handler.GET("/healthcheck", func(c *gin.Context) { c.Status(http.StatusOK) })
handler.GET(urlPrefix+"/healthcheck", func(c *gin.Context) { c.Status(http.StatusOK) })

// Prometheus metrics
handler.GET("/metrics", gin.WrapH(promhttp.Handler()))
handler.GET(urlPrefix+"/metrics", gin.WrapH(promhttp.Handler()))

// Routers
newUserRoutes(handler.Group("/"), a, l)
newUserRoutes(handler.Group(urlPrefix+"/"), a, l)

syncRoutes := handler.Group("/syncs")
syncRoutes := handler.Group(urlPrefix + "/syncs")
syncRoutes.Use(authDeviceMiddleware(a, l))
newSyncRoutes(syncRoutes, p, l)
}
24 changes: 13 additions & 11 deletions internal/controller/http/web/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,33 @@ import (
)

type authRoutes struct {
auth auth.AuthInterface
l logger.Interface
auth auth.AuthInterface
urlPrefix string
l logger.Interface
}

func newAuthRoutes(handler *gin.RouterGroup, a auth.AuthInterface, l logger.Interface) {
r := &authRoutes{a, l}
func newAuthRoutes(handler *gin.RouterGroup, urlPrefix string, a auth.AuthInterface, l logger.Interface) {
r := &authRoutes{a, urlPrefix, l}
handler.Group(urlPrefix)

handler.GET("/login", r.loginForm)
handler.POST("/login", r.loginAction)
handler.GET("/logout", r.logoutAction)
}

func (r *authRoutes) loginForm(c *gin.Context) {
c.HTML(200, "login", passStandartContext(c, gin.H{}))
c.HTML(200, "login", passStandartContext(c, gin.H{"urlPrefix": r.urlPrefix}))
}

func (r *authRoutes) logoutAction(c *gin.Context) {
sessionKey, err := c.Cookie("session")
if err != nil {
c.Redirect(302, "/auth/login")
c.Redirect(302, r.urlPrefix+"/auth/login")
return
}
r.auth.Logout(c.Request.Context(), sessionKey)
c.SetCookie("session", "", 0, "/", "", false, true)
c.Redirect(302, "/auth/login")
c.Redirect(302, r.urlPrefix+"/auth/login")
}

func (r *authRoutes) loginAction(c *gin.Context) {
Expand All @@ -49,20 +51,20 @@ func (r *authRoutes) loginAction(c *gin.Context) {
return
}
c.SetCookie("session", sessionKey, 0, "/", "", false, true)
c.Redirect(302, "/books")
c.Redirect(302, r.urlPrefix+"/books")
}

func authMiddleware(a auth.AuthInterface) gin.HandlerFunc {
func authMiddleware(a auth.AuthInterface, urlPrefix string) gin.HandlerFunc {
return func(c *gin.Context) {
sessionKey, err := c.Cookie("session")
if err != nil {
c.Redirect(302, "/auth/login")
c.Redirect(302, urlPrefix+"/auth/login")
c.Abort()
return
}

if !a.IsAuthenticated(c.Request.Context(), sessionKey) {
c.Redirect(302, "/auth/login")
c.Redirect(302, urlPrefix+"/auth/login")
c.Abort()
return
}
Expand Down
Loading