From 4e9f61ad2e236c0155106f052b0abf0499d8a2f3 Mon Sep 17 00:00:00 2001 From: kongfang Date: Tue, 9 Sep 2025 13:02:21 +0800 Subject: [PATCH 1/6] Use docker compose when docker-compose not installed --- Makefile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f7bf8ea..abeaaaf 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -18,19 +20,19 @@ help: ## Display this help screen @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\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 From 2feedbd36a65964fccea816bdcf4177607075c28 Mon Sep 17 00:00:00 2001 From: kongfang Date: Wed, 10 Sep 2025 13:06:21 +0800 Subject: [PATCH 2/6] add base url for service --- README.md | 22 +++++++++++------ config/config.go | 7 ++++-- internal/app/app.go | 8 +++---- internal/controller/http/opds/opds.go | 10 ++++---- internal/controller/http/opds/router.go | 20 +++++++++------- internal/controller/http/v1/router.go | 10 ++++---- internal/controller/http/web/auth.go | 24 ++++++++++--------- internal/controller/http/web/books.go | 26 +++++++++++--------- internal/controller/http/web/devices.go | 18 ++++++++------ internal/controller/http/web/router.go | 29 ++++++++++++----------- internal/controller/http/web/stats.go | 10 ++++---- internal/controller/http/webdav/router.go | 3 ++- web/templates/book.html | 4 ++-- web/templates/books.html | 8 +++---- web/templates/devices.html | 4 ++-- web/templates/layouts/master.html | 12 +++++----- web/templates/stats.html | 4 ++-- 17 files changed, 123 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index d422b72..e34d830 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ 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) @@ -15,6 +17,7 @@ What KOmpanion is NOT about: 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 @@ -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 @@ -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 @@ -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 @@ -88,6 +95,7 @@ $ make run ``` Integration tests (can be run in CI): + ```sh # DB, app + migrations, integration tests $ make compose-up-integration-test diff --git a/config/config.go b/config/config.go index 008200b..9ef7217 100644 --- a/config/config.go +++ b/config/config.go @@ -33,7 +33,8 @@ type ( // HTTP -. HTTP struct { - Port string + Port string + UrlPrefix string } // Log -. @@ -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 } diff --git a/internal/app/app.go b/internal/app/app.go index 764e546..ee76c47 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -61,10 +61,10 @@ func Run(cfg *config.Config) { // HTTP Server handler := gin.New() - 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 diff --git a/internal/controller/http/opds/opds.go b/internal/controller/http/opds/opds.go index 143802c..0d75270 100644 --- a/internal/controller/http/opds/opds.go +++ b/internal/controller/http/opds/opds.go @@ -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", }, @@ -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", }, @@ -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{ @@ -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), diff --git a/internal/controller/http/opds/router.go b/internal/controller/http/opds/router.go index 3d1d3a2..7cd7df2 100644 --- a/internal/controller/http/opds/router.go +++ b/internal/controller/http/opds/router.go @@ -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) @@ -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) } @@ -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) } diff --git a/internal/controller/http/v1/router.go b/internal/controller/http/v1/router.go index 02ea227..d8ca7a7 100644 --- a/internal/controller/http/v1/router.go +++ b/internal/controller/http/v1/router.go @@ -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) } diff --git a/internal/controller/http/web/auth.go b/internal/controller/http/web/auth.go index 14c7bb4..03ad98c 100644 --- a/internal/controller/http/web/auth.go +++ b/internal/controller/http/web/auth.go @@ -7,12 +7,14 @@ 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) @@ -20,18 +22,18 @@ func newAuthRoutes(handler *gin.RouterGroup, a auth.AuthInterface, l logger.Inte } 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) { @@ -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 } diff --git a/internal/controller/http/web/books.go b/internal/controller/http/web/books.go index e9aae9a..579696b 100644 --- a/internal/controller/http/web/books.go +++ b/internal/controller/http/web/books.go @@ -14,14 +14,16 @@ import ( ) type booksRoutes struct { - shelf library.Shelf - stats stats.ReadingStats - progress syncpkg.Progress - logger logger.Interface + urlPrefix string + shelf library.Shelf + stats stats.ReadingStats + progress syncpkg.Progress + logger logger.Interface } -func newBooksRoutes(handler *gin.RouterGroup, shelf library.Shelf, stats stats.ReadingStats, progress syncpkg.Progress, l logger.Interface) { - r := &booksRoutes{shelf: shelf, stats: stats, progress: progress, logger: l} +func newBooksRoutes(handler *gin.RouterGroup, urlPrefix string, shelf library.Shelf, stats stats.ReadingStats, progress syncpkg.Progress, l logger.Interface) { + r := &booksRoutes{urlPrefix: urlPrefix, shelf: shelf, stats: stats, progress: progress, logger: l} + handler.Group(urlPrefix) handler.GET("/", r.listBooks) handler.POST("/upload", r.uploadBook) @@ -65,7 +67,8 @@ func (r *booksRoutes) listBooks(c *gin.Context) { } c.HTML(200, "books", passStandartContext(c, gin.H{ - "books": booksWithProgress, + "urlPrefix": r.urlPrefix, + "books": booksWithProgress, "pagination": gin.H{ "currentPage": page, "perPage": perPage, @@ -107,7 +110,7 @@ func (r *booksRoutes) uploadBook(c *gin.Context) { c.JSON(500, passStandartContext(c, gin.H{"message": "internal server error"})) return } - c.Redirect(302, "/books/"+book.ID) + c.Redirect(302, r.urlPrefix+"/books/"+book.ID) } func (r *booksRoutes) downloadBook(c *gin.Context) { @@ -141,8 +144,9 @@ func (r *booksRoutes) viewBook(c *gin.Context) { } c.HTML(200, "book", passStandartContext(c, gin.H{ - "book": book, - "stats": bookStats, + "urlPrefix": r.urlPrefix, + "book": book, + "stats": bookStats, })) } @@ -166,7 +170,7 @@ func (r *booksRoutes) updateBookMetadata(c *gin.Context) { } // TODO: why not redirect? - c.HTML(200, "book", passStandartContext(c, gin.H{"book": book})) + c.HTML(200, "book", passStandartContext(c, gin.H{"urlPrefix": r.urlPrefix, "book": book})) } func (r *booksRoutes) viewBookCover(c *gin.Context) { diff --git a/internal/controller/http/web/devices.go b/internal/controller/http/web/devices.go index 35dceec..bee1ccc 100644 --- a/internal/controller/http/web/devices.go +++ b/internal/controller/http/web/devices.go @@ -7,12 +7,15 @@ import ( ) type deviceRoutes struct { - auth auth.AuthInterface - l logger.Interface + auth auth.AuthInterface + urlPrefix string + l logger.Interface } -func newDeviceRoutes(handler *gin.RouterGroup, a auth.AuthInterface, l logger.Interface) { - r := &deviceRoutes{a, l} +func newDeviceRoutes(handler *gin.RouterGroup, urlPrefix string, a auth.AuthInterface, l logger.Interface) { + r := &deviceRoutes{a, urlPrefix, l} + + handler.Group(urlPrefix) handler.GET("/", r.listDevices) handler.POST("/add", r.addDeviceAction) @@ -29,7 +32,8 @@ func (r *deviceRoutes) listDevices(c *gin.Context) { } c.HTML(200, "devices", passStandartContext(c, gin.H{ - "devices": devices, + "urlPrefix": r.urlPrefix, + "devices": devices, })) } @@ -52,7 +56,7 @@ func (r *deviceRoutes) addDeviceAction(c *gin.Context) { return } - c.Redirect(302, "/devices") + c.Redirect(302, r.urlPrefix+"/devices") } func (r *deviceRoutes) deactivateDeviceAction(c *gin.Context) { @@ -65,5 +69,5 @@ func (r *deviceRoutes) deactivateDeviceAction(c *gin.Context) { return } - c.Redirect(302, "/devices") + c.Redirect(302, r.urlPrefix+"/devices") } diff --git a/internal/controller/http/web/router.go b/internal/controller/http/web/router.go index a8df3de..b57e03d 100644 --- a/internal/controller/http/web/router.go +++ b/internal/controller/http/web/router.go @@ -24,6 +24,7 @@ import ( func NewRouter( handler *gin.Engine, + urlPrefix string, l logger.Interface, a auth.AuthInterface, p sync.Progress, @@ -42,7 +43,7 @@ func NewRouter( if err != nil { l.Error("Failed to get static files: %v", err) } - handler.StaticFS("/static", http.FS(staticFs)) + handler.StaticFS(urlPrefix+"/static", http.FS(staticFs)) config := goview.DefaultConfig config.Root = "web/templates" @@ -73,28 +74,28 @@ func NewRouter( handler.HTMLRender = gv // Home - handler.GET("/", func(c *gin.Context) { - c.Redirect(302, "/books") + handler.GET(urlPrefix+"/", func(c *gin.Context) { + c.Redirect(302, urlPrefix+"/books") }) // Login - authGroup := handler.Group("/auth") - newAuthRoutes(authGroup, a, l) + authGroup := handler.Group(urlPrefix + "/auth") + newAuthRoutes(authGroup, urlPrefix, a, l) // Product pages - bookGroup := handler.Group("/books") - bookGroup.Use(authMiddleware(a)) - newBooksRoutes(bookGroup, shelf, stats, p, l) + bookGroup := handler.Group(urlPrefix + "/books") + bookGroup.Use(authMiddleware(a, urlPrefix)) + newBooksRoutes(bookGroup, urlPrefix, shelf, stats, p, l) // Stats pages - statsGroup := handler.Group("/stats") - statsGroup.Use(authMiddleware(a)) - newStatsRoutes(statsGroup, stats, l) + statsGroup := handler.Group(urlPrefix + "/stats") + statsGroup.Use(authMiddleware(a, urlPrefix)) + newStatsRoutes(statsGroup, urlPrefix, stats, l) // Device management - deviceGroup := handler.Group("/devices") - deviceGroup.Use(authMiddleware(a)) - newDeviceRoutes(deviceGroup, a, l) + deviceGroup := handler.Group(urlPrefix + "/devices") + deviceGroup.Use(authMiddleware(a, urlPrefix)) + newDeviceRoutes(deviceGroup, urlPrefix, a, l) } func passStandartContext(c *gin.Context, data gin.H) gin.H { diff --git a/internal/controller/http/web/stats.go b/internal/controller/http/web/stats.go index 7d89167..cd834b1 100644 --- a/internal/controller/http/web/stats.go +++ b/internal/controller/http/web/stats.go @@ -117,7 +117,8 @@ func generateDailyStatsChart(stats []stats.DailyStats) ([]byte, error) { return buffer.Bytes(), nil } -func newStatsRoutes(handler *gin.RouterGroup, stats stats.ReadingStats, l logger.Interface) { +func newStatsRoutes(handler *gin.RouterGroup, urlPrefix string, stats stats.ReadingStats, l logger.Interface) { + handler.Group(urlPrefix) handler.GET("/", func(c *gin.Context) { // Get date range from query params, default to current month now := time.Now() @@ -146,9 +147,10 @@ func newStatsRoutes(handler *gin.RouterGroup, stats stats.ReadingStats, l logger } c.HTML(200, "stats", passStandartContext(c, gin.H{ - "from": from.Format("2006-01-02"), - "to": to.Format("2006-01-02"), - "stats": generalStats, + "urlPrefix": urlPrefix, + "from": from.Format("2006-01-02"), + "to": to.Format("2006-01-02"), + "stats": generalStats, })) }) diff --git a/internal/controller/http/webdav/router.go b/internal/controller/http/webdav/router.go index 5430de0..a45abda 100644 --- a/internal/controller/http/webdav/router.go +++ b/internal/controller/http/webdav/router.go @@ -11,6 +11,7 @@ import ( func NewRouter( handler *gin.Engine, + urlPrefix string, a auth.AuthInterface, l logger.Interface, rs stats.ReadingStats, @@ -19,7 +20,7 @@ func NewRouter( handler.Use(gin.Logger()) handler.Use(gin.Recovery()) - h := handler.Group("/webdav") + h := handler.Group(urlPrefix + "/webdav") h.Use(basicAuth(a)) h.Handle("PROPFIND", "/", func(c *gin.Context) { // Static response for PROPFIND diff --git a/web/templates/book.html b/web/templates/book.html index 629d06e..ba4d478 100644 --- a/web/templates/book.html +++ b/web/templates/book.html @@ -5,7 +5,7 @@
- {{.Title}} - {{.Author}} + {{.Title}} - {{.Author}}
@@ -40,7 +40,7 @@
-
diff --git a/web/templates/books.html b/web/templates/books.html index 3eb6322..5cf0b5f 100644 --- a/web/templates/books.html +++ b/web/templates/books.html @@ -2,7 +2,7 @@ {{ define "content" }}
-
+
@@ -14,13 +14,13 @@

- + {{.Title}}

diff --git a/web/templates/devices.html b/web/templates/devices.html index e3c6c83..7fbf427 100644 --- a/web/templates/devices.html +++ b/web/templates/devices.html @@ -12,7 +12,7 @@

Device Management

Add New Device

- + @@ -34,7 +34,7 @@

Registered Devices

{{.Name}} - + @@ -45,7 +45,7 @@

Reading Statistics

Daily Reading Progress

- Daily Reading Progress
From b6364026f9bf0880b5ae8233fccea2a1d389b3bb Mon Sep 17 00:00:00 2001 From: kongfang Date: Wed, 10 Sep 2025 15:21:12 +0800 Subject: [PATCH 3/6] support build arm64 docker image --- .github/workflows/publish.yml | 1 + Dockerfile | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 36680c1..bf88687 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/Dockerfile b/Dockerfile index 94813f7..415f1bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 From 93b2dc9fc51e4eb1ae79d3cb00f6943b229e641b Mon Sep 17 00:00:00 2001 From: kongfang Date: Wed, 10 Sep 2025 15:22:41 +0800 Subject: [PATCH 4/6] require book file when click upload button --- web/templates/books.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/books.html b/web/templates/books.html index 5cf0b5f..7b97a6d 100644 --- a/web/templates/books.html +++ b/web/templates/books.html @@ -4,7 +4,7 @@
- +
From bbda479a9f6f1f8ce3a51f7a7553a99f9b072301 Mon Sep 17 00:00:00 2001 From: kongfang Date: Thu, 11 Sep 2025 16:14:08 +0800 Subject: [PATCH 5/6] set global urlprefix in app.go --- internal/app/app.go | 13 +++++++------ internal/controller/http/opds/router.go | 7 +++---- internal/controller/http/v1/router.go | 10 +++++----- internal/controller/http/web/auth.go | 1 - internal/controller/http/web/books.go | 1 - internal/controller/http/web/devices.go | 14 ++++++++------ internal/controller/http/web/router.go | 21 ++++++++++++--------- internal/controller/http/web/stats.go | 1 - internal/controller/http/webdav/router.go | 5 ++--- 9 files changed, 37 insertions(+), 36 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index ee76c47..b10f747 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -60,12 +60,13 @@ func Run(cfg *config.Config) { rs := stats.NewKOReaderPGStats(pg) // HTTP Server - handler := gin.New() - 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)) + 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(router, httpserver.Port(cfg.HTTP.Port)) // Waiting signal interrupt := make(chan os.Signal, 1) diff --git a/internal/controller/http/opds/router.go b/internal/controller/http/opds/router.go index 7cd7df2..0a49990 100644 --- a/internal/controller/http/opds/router.go +++ b/internal/controller/http/opds/router.go @@ -19,15 +19,14 @@ type OPDSRouter struct { } func NewRouter( - handler *gin.Engine, - urlPrefix string, + handler *gin.RouterGroup, l logger.Interface, a auth.AuthInterface, p sync.Progress, shelf library.Shelf) { - sh := &OPDSRouter{urlPrefix, shelf, l} + sh := &OPDSRouter{handler.BasePath(), shelf, l} - h := handler.Group(urlPrefix + "/opds") + h := handler.Group("/opds") h.Use(basicAuth(a)) { h.GET("/", sh.listShelves) diff --git a/internal/controller/http/v1/router.go b/internal/controller/http/v1/router.go index d8ca7a7..7668a72 100644 --- a/internal/controller/http/v1/router.go +++ b/internal/controller/http/v1/router.go @@ -14,21 +14,21 @@ import ( ) // NewRouter -. -func NewRouter(handler *gin.Engine, urlPrefix string, 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()) // K8s probe - handler.GET(urlPrefix+"/healthcheck", func(c *gin.Context) { c.Status(http.StatusOK) }) + handler.GET("/healthcheck", func(c *gin.Context) { c.Status(http.StatusOK) }) // Prometheus metrics - handler.GET(urlPrefix+"/metrics", gin.WrapH(promhttp.Handler())) + handler.GET("/metrics", gin.WrapH(promhttp.Handler())) // Routers - newUserRoutes(handler.Group(urlPrefix+"/"), a, l) + newUserRoutes(handler.Group("/"), a, l) - syncRoutes := handler.Group(urlPrefix + "/syncs") + syncRoutes := handler.Group("/syncs") syncRoutes.Use(authDeviceMiddleware(a, l)) newSyncRoutes(syncRoutes, p, l) } diff --git a/internal/controller/http/web/auth.go b/internal/controller/http/web/auth.go index 03ad98c..a881e1e 100644 --- a/internal/controller/http/web/auth.go +++ b/internal/controller/http/web/auth.go @@ -14,7 +14,6 @@ type authRoutes struct { 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) diff --git a/internal/controller/http/web/books.go b/internal/controller/http/web/books.go index 579696b..9a68a28 100644 --- a/internal/controller/http/web/books.go +++ b/internal/controller/http/web/books.go @@ -23,7 +23,6 @@ type booksRoutes struct { func newBooksRoutes(handler *gin.RouterGroup, urlPrefix string, shelf library.Shelf, stats stats.ReadingStats, progress syncpkg.Progress, l logger.Interface) { r := &booksRoutes{urlPrefix: urlPrefix, shelf: shelf, stats: stats, progress: progress, logger: l} - handler.Group(urlPrefix) handler.GET("/", r.listBooks) handler.POST("/upload", r.uploadBook) diff --git a/internal/controller/http/web/devices.go b/internal/controller/http/web/devices.go index bee1ccc..3e5f9ed 100644 --- a/internal/controller/http/web/devices.go +++ b/internal/controller/http/web/devices.go @@ -15,8 +15,6 @@ type deviceRoutes struct { func newDeviceRoutes(handler *gin.RouterGroup, urlPrefix string, a auth.AuthInterface, l logger.Interface) { r := &deviceRoutes{a, urlPrefix, l} - handler.Group(urlPrefix) - handler.GET("/", r.listDevices) handler.POST("/add", r.addDeviceAction) handler.POST("/deactivate/:device_name", r.deactivateDeviceAction) @@ -26,7 +24,8 @@ func (r *deviceRoutes) listDevices(c *gin.Context) { devices, err := r.auth.ListDevices(c.Request.Context()) if err != nil { c.HTML(500, "devices", passStandartContext(c, gin.H{ - "error": "Failed to load devices", + "urlPrefix": r.urlPrefix, + "error": "Failed to load devices", })) return } @@ -43,7 +42,8 @@ func (r *deviceRoutes) addDeviceAction(c *gin.Context) { if deviceName == "" || password == "" { c.HTML(400, "devices", passStandartContext(c, gin.H{ - "error": "Device name and password are required", + "error": "Device name and password are required", + "urlPrefix": r.urlPrefix, })) return } @@ -51,7 +51,8 @@ func (r *deviceRoutes) addDeviceAction(c *gin.Context) { err := r.auth.AddUserDevice(c.Request.Context(), deviceName, password) if err != nil { c.HTML(400, "devices", passStandartContext(c, gin.H{ - "error": err.Error(), + "error": err.Error(), + "urlPrefix": r.urlPrefix, })) return } @@ -64,7 +65,8 @@ func (r *deviceRoutes) deactivateDeviceAction(c *gin.Context) { err := r.auth.DeactivateUserDevice(c.Request.Context(), deviceName) if err != nil { c.HTML(400, "devices", passStandartContext(c, gin.H{ - "error": err.Error(), + "urlPrefix": r.urlPrefix, + "error": err.Error(), })) return } diff --git a/internal/controller/http/web/router.go b/internal/controller/http/web/router.go index b57e03d..92975cc 100644 --- a/internal/controller/http/web/router.go +++ b/internal/controller/http/web/router.go @@ -23,8 +23,8 @@ import ( ) func NewRouter( - handler *gin.Engine, - urlPrefix string, + handler *gin.RouterGroup, + router *gin.Engine, l logger.Interface, a auth.AuthInterface, p sync.Progress, @@ -38,12 +38,15 @@ func NewRouter( handler.Use(func(c *gin.Context) { c.Set("startTime", time.Now()) }) + + // hold origin prefix as urlPrefix + urlPrefix := handler.BasePath() // static files staticFs, err := fs.Sub(kompanion.WebAssets, "web/static") if err != nil { l.Error("Failed to get static files: %v", err) } - handler.StaticFS(urlPrefix+"/static", http.FS(staticFs)) + handler.StaticFS("/static", http.FS(staticFs)) config := goview.DefaultConfig config.Root = "web/templates" @@ -71,29 +74,29 @@ func NewRouter( } gv := ginview.New(config) gv.SetFileHandler(embeddedFH) - handler.HTMLRender = gv + router.HTMLRender = gv // Home - handler.GET(urlPrefix+"/", func(c *gin.Context) { + handler.GET("/", func(c *gin.Context) { c.Redirect(302, urlPrefix+"/books") }) // Login - authGroup := handler.Group(urlPrefix + "/auth") + authGroup := handler.Group("/auth") newAuthRoutes(authGroup, urlPrefix, a, l) // Product pages - bookGroup := handler.Group(urlPrefix + "/books") + bookGroup := handler.Group("/books") bookGroup.Use(authMiddleware(a, urlPrefix)) newBooksRoutes(bookGroup, urlPrefix, shelf, stats, p, l) // Stats pages - statsGroup := handler.Group(urlPrefix + "/stats") + statsGroup := handler.Group("/stats") statsGroup.Use(authMiddleware(a, urlPrefix)) newStatsRoutes(statsGroup, urlPrefix, stats, l) // Device management - deviceGroup := handler.Group(urlPrefix + "/devices") + deviceGroup := handler.Group("/devices") deviceGroup.Use(authMiddleware(a, urlPrefix)) newDeviceRoutes(deviceGroup, urlPrefix, a, l) } diff --git a/internal/controller/http/web/stats.go b/internal/controller/http/web/stats.go index cd834b1..69f736a 100644 --- a/internal/controller/http/web/stats.go +++ b/internal/controller/http/web/stats.go @@ -118,7 +118,6 @@ func generateDailyStatsChart(stats []stats.DailyStats) ([]byte, error) { } func newStatsRoutes(handler *gin.RouterGroup, urlPrefix string, stats stats.ReadingStats, l logger.Interface) { - handler.Group(urlPrefix) handler.GET("/", func(c *gin.Context) { // Get date range from query params, default to current month now := time.Now() diff --git a/internal/controller/http/webdav/router.go b/internal/controller/http/webdav/router.go index a45abda..acf1d1e 100644 --- a/internal/controller/http/webdav/router.go +++ b/internal/controller/http/webdav/router.go @@ -10,8 +10,7 @@ import ( ) func NewRouter( - handler *gin.Engine, - urlPrefix string, + handler *gin.RouterGroup, a auth.AuthInterface, l logger.Interface, rs stats.ReadingStats, @@ -20,7 +19,7 @@ func NewRouter( handler.Use(gin.Logger()) handler.Use(gin.Recovery()) - h := handler.Group(urlPrefix + "/webdav") + h := handler.Group("/webdav") h.Use(basicAuth(a)) h.Handle("PROPFIND", "/", func(c *gin.Context) { // Static response for PROPFIND From 7c7f2347448bbd9a7814b7bbe36d181369a4b870 Mon Sep 17 00:00:00 2001 From: kongfang Date: Thu, 11 Sep 2025 18:05:04 +0800 Subject: [PATCH 6/6] bugfix: BasePath() return / would make mistake, trim it --- internal/controller/http/opds/router.go | 4 +++- internal/controller/http/web/router.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/controller/http/opds/router.go b/internal/controller/http/opds/router.go index 0a49990..70c8f2e 100644 --- a/internal/controller/http/opds/router.go +++ b/internal/controller/http/opds/router.go @@ -3,6 +3,7 @@ package opds import ( "net/http" "strconv" + "strings" "time" "github.com/gin-gonic/gin" @@ -24,7 +25,8 @@ func NewRouter( a auth.AuthInterface, p sync.Progress, shelf library.Shelf) { - sh := &OPDSRouter{handler.BasePath(), shelf, l} + urlPrefix := strings.TrimSuffix(handler.BasePath(), "/") + sh := &OPDSRouter{urlPrefix, shelf, l} h := handler.Group("/opds") h.Use(basicAuth(a)) diff --git a/internal/controller/http/web/router.go b/internal/controller/http/web/router.go index 92975cc..90bd4a6 100644 --- a/internal/controller/http/web/router.go +++ b/internal/controller/http/web/router.go @@ -40,7 +40,7 @@ func NewRouter( }) // hold origin prefix as urlPrefix - urlPrefix := handler.BasePath() + urlPrefix := strings.TrimSuffix(handler.BasePath(), "/") // static files staticFs, err := fs.Sub(kompanion.WebAssets, "web/static") if err != nil {