[add] opds catalog, [add] migrate to non-cgo sqlite driver

This commit is contained in:
2023-10-05 19:56:19 -04:00
parent 4e1ee0022a
commit eb7d711022
10 changed files with 384 additions and 55 deletions

View File

@@ -66,6 +66,7 @@ func NewApi(db *database.DBManager, c *config.Config) *API {
// Register API Routes
apiGroup := api.Router.Group("/api")
api.registerKOAPIRoutes(apiGroup)
api.registerOPDSRoutes(apiGroup)
return api
}
@@ -112,18 +113,27 @@ func (api *API) registerKOAPIRoutes(apiGroup *gin.RouterGroup) {
koGroup := apiGroup.Group("/ko")
koGroup.POST("/users/create", api.createUser)
koGroup.GET("/users/auth", api.authAPIMiddleware, api.authorizeUser)
koGroup.GET("/users/auth", api.authKOMiddleware, api.authorizeUser)
koGroup.PUT("/syncs/progress", api.authAPIMiddleware, api.setProgress)
koGroup.GET("/syncs/progress/:document", api.authAPIMiddleware, api.getProgress)
koGroup.PUT("/syncs/progress", api.authKOMiddleware, api.setProgress)
koGroup.GET("/syncs/progress/:document", api.authKOMiddleware, api.getProgress)
koGroup.POST("/documents", api.authAPIMiddleware, api.addDocuments)
koGroup.POST("/syncs/documents", api.authAPIMiddleware, api.checkDocumentsSync)
koGroup.PUT("/documents/:document/file", api.authAPIMiddleware, api.uploadDocumentFile)
koGroup.GET("/documents/:document/file", api.authAPIMiddleware, api.downloadDocumentFile)
koGroup.POST("/documents", api.authKOMiddleware, api.addDocuments)
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.checkDocumentsSync)
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.uploadDocumentFile)
koGroup.GET("/documents/:document/file", api.authKOMiddleware, api.downloadDocumentFile)
koGroup.POST("/activity", api.authAPIMiddleware, api.addActivities)
koGroup.POST("/syncs/activity", api.authAPIMiddleware, api.checkActivitySync)
koGroup.POST("/activity", api.authKOMiddleware, api.addActivities)
koGroup.POST("/syncs/activity", api.authKOMiddleware, api.checkActivitySync)
}
func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
opdsGroup := apiGroup.Group("/opds")
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsDocuments)
opdsGroup.GET("/search.xml", api.authOPDSMiddleware, api.opdsSearchDescription)
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocumentFile)
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
}
func generateToken(n int) ([]byte, error) {

View File

@@ -15,11 +15,16 @@ import (
)
// KOSync API Auth Headers
type authHeader struct {
type authKOHeader struct {
AuthUser string `header:"x-auth-user"`
AuthKey string `header:"x-auth-key"`
}
// OPDS Auth Headers
type authOPDSHeader struct {
Authorization string `header:"authorization"`
}
func (api *API) authorizeCredentials(username string, password string) (authorized bool) {
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
if err != nil {
@@ -33,7 +38,7 @@ func (api *API) authorizeCredentials(username string, password string) (authoriz
return true
}
func (api *API) authAPIMiddleware(c *gin.Context) {
func (api *API) authKOMiddleware(c *gin.Context) {
session := sessions.Default(c)
// Check Session First
@@ -46,7 +51,7 @@ func (api *API) authAPIMiddleware(c *gin.Context) {
// Session Failed -> Check Headers (Allowed on API for KOSync Compatibility)
var rHeader authHeader
var rHeader authKOHeader
if err := c.ShouldBindHeader(&rHeader); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Incorrect Headers"})
return
@@ -71,6 +76,29 @@ func (api *API) authAPIMiddleware(c *gin.Context) {
c.Next()
}
func (api *API) authOPDSMiddleware(c *gin.Context) {
c.Header("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
user, rawPassword, hasAuth := c.Request.BasicAuth()
// Validate Auth Fields
if hasAuth != true || user == "" || rawPassword == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
return
}
// Validate Auth
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
if authorized := api.authorizeCredentials(user, password); authorized != true {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
c.Set("AuthorizedUser", user)
c.Header("Cache-Control", "private")
c.Next()
}
func (api *API) authWebAppMiddleware(c *gin.Context) {
session := sessions.Default(c)

113
api/opds-routes.go Normal file
View File

@@ -0,0 +1,113 @@
package api
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"reichard.io/bbank/database"
"reichard.io/bbank/opds"
)
var mimeMapping map[string]string = map[string]string{
"epub": "application/epub+zip",
"azw": "application/vnd.amazon.mobi8-ebook",
"mobi": "application/x-mobipocket-ebook",
"pdf": "application/pdf",
"zip": "application/zip",
"txt": "text/plain",
"rtf": "application/rtf",
"htm": "text/html",
"html": "text/html",
"doc": "application/msword",
"lit": "application/x-ms-reader",
}
func (api *API) opdsDocuments(c *gin.Context) {
var userID string
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
userID = rUser.(string)
}
// Potential URL Parameters
qParams := bindQueryParams(c)
// Get Documents
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
UserID: userID,
Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit,
})
if err != nil {
log.Error("[opdsDocuments] GetDocumentsWithStats DB Error:", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
// Build OPDS Entries
var allEntries []opds.Entry
for _, doc := range documents {
// Require File
if doc.Filepath != nil {
splitFilepath := strings.Split(*doc.Filepath, ".")
fileType := splitFilepath[len(splitFilepath)-1]
item := opds.Entry{
Title: fmt.Sprintf("[%3d%%] %s", int(doc.Percentage.(float64)), *doc.Title),
Author: []opds.Author{
{
Name: *doc.Author,
},
},
Content: &opds.Content{
Content: *doc.Description,
ContentType: "text",
},
Links: []opds.Link{
{
Rel: "http://opds-spec.org/acquisition",
Href: fmt.Sprintf("./documents/%s/file", doc.ID),
TypeLink: mimeMapping[fileType],
},
{
Rel: "http://opds-spec.org/image",
Href: fmt.Sprintf("./documents/%s/cover", doc.ID),
TypeLink: "image/jpeg",
},
},
}
allEntries = append(allEntries, item)
}
}
// Build & Return XML
searchFeed := &opds.Feed{
Title: "All Documents",
Updated: time.Now().UTC(),
// TODO
// Links: []opds.Link{
// {
// Title: "Search Book Manager",
// Rel: "search",
// TypeLink: "application/opensearchdescription+xml",
// Href: "search.xml",
// },
// },
Entries: allEntries,
}
c.XML(http.StatusOK, searchFeed)
}
func (api *API) opdsSearchDescription(c *gin.Context) {
rawXML := `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Search Book Manager</ShortName>
<Description>Search Book Manager</Description>
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="./search?query={searchTerms}"/>
</OpenSearchDescription>`
c.Data(http.StatusOK, "application/xml", []byte(rawXML))
}