Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,13 @@ func Run(cfg *config.Config) {
rs := stats.NewKOReaderPGStats(pg)

// HTTP Server
handler := gin.New()
web.NewRouter(handler, l, authService, progress, shelf, rs, cfg.Version)
router := gin.New()
handler := router.Group(cfg.UrlPrefix)
web.NewRouter(handler, router, 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)
httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port))
httpServer := httpserver.New(router, httpserver.Port(cfg.HTTP.Port))

// Waiting signal
interrupt := make(chan os.Signal, 1)
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
21 changes: 12 additions & 9 deletions internal/controller/http/opds/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package opds
import (
"net/http"
"strconv"
"strings"
"time"

"github.com/gin-gonic/gin"
Expand All @@ -13,17 +14,19 @@ import (
)

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

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

h := handler.Group("/opds")
h.Use(basicAuth(a))
Expand All @@ -43,14 +46,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 +69,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
2 changes: 1 addition & 1 deletion internal/controller/http/v1/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

// NewRouter -.
func NewRouter(handler *gin.Engine, l logger.Interface, a auth.AuthInterface, p sync.Progress, shelf library.Shelf) {
func NewRouter(handler *gin.RouterGroup, l logger.Interface, a auth.AuthInterface, p sync.Progress, shelf library.Shelf) {
// Options
handler.Use(gin.Logger())
handler.Use(gin.Recovery())
Expand Down
23 changes: 12 additions & 11 deletions internal/controller/http/web/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,32 @@ 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.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 +50,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