[refactor] remove app route builder
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
a69f20d5a9
commit
74abc65340
83
api/api.go
83
api/api.go
@ -83,51 +83,52 @@ func (api *API) registerWebAppRoutes() {
|
||||
api.Router.HTMLRender = *api.generateTemplates()
|
||||
|
||||
// Static Assets (Required @ Root)
|
||||
api.Router.GET("/manifest.json", api.webManifest)
|
||||
api.Router.GET("/favicon.ico", api.faviconIcon)
|
||||
api.Router.GET("/sw.js", api.serviceWorker)
|
||||
api.Router.GET("/manifest.json", api.appWebManifest)
|
||||
api.Router.GET("/favicon.ico", api.appFaviconIcon)
|
||||
api.Router.GET("/sw.js", api.appServiceWorker)
|
||||
|
||||
// Local / Offline Static Pages (No Template, No Auth)
|
||||
api.Router.GET("/local", api.localDocuments)
|
||||
api.Router.GET("/local", api.appLocalDocuments)
|
||||
|
||||
// Reader (Reader Page, Document Progress, Devices)
|
||||
api.Router.GET("/reader", api.documentReader)
|
||||
api.Router.GET("/reader/devices", api.authWebAppMiddleware, api.getDevices)
|
||||
api.Router.GET("/reader/progress/:document", api.authWebAppMiddleware, api.getDocumentProgress)
|
||||
api.Router.GET("/reader", api.appDocumentReader)
|
||||
api.Router.GET("/reader/devices", api.authWebAppMiddleware, api.appGetDevices)
|
||||
api.Router.GET("/reader/progress/:document", api.authWebAppMiddleware, api.appGetDocumentProgress)
|
||||
|
||||
// Web App
|
||||
api.Router.GET("/", api.authWebAppMiddleware, api.createAppResourcesRoute("home"))
|
||||
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
|
||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
||||
api.Router.GET("/", api.authWebAppMiddleware, api.appGetHome)
|
||||
api.Router.GET("/activity", api.authWebAppMiddleware, api.appGetActivity)
|
||||
api.Router.GET("/progress", api.authWebAppMiddleware, api.appGetProgress)
|
||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.appGetDocuments)
|
||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.appGetDocument)
|
||||
api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover)
|
||||
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocument)
|
||||
api.Router.GET("/login", api.createAppResourcesRoute("login"))
|
||||
api.Router.GET("/logout", api.authWebAppMiddleware, api.authLogout)
|
||||
api.Router.GET("/register", api.createAppResourcesRoute("login", gin.H{"Register": true}))
|
||||
api.Router.GET("/settings", api.authWebAppMiddleware, api.createAppResourcesRoute("settings"))
|
||||
api.Router.POST("/login", api.authFormLogin)
|
||||
api.Router.POST("/register", api.authFormRegister)
|
||||
api.Router.GET("/login", api.appGetLogin)
|
||||
api.Router.GET("/logout", api.authWebAppMiddleware, api.appAuthLogout)
|
||||
api.Router.GET("/register", api.appGetRegister)
|
||||
api.Router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings)
|
||||
api.Router.POST("/login", api.appAuthFormLogin)
|
||||
api.Router.POST("/register", api.appAuthFormRegister)
|
||||
|
||||
// Demo Mode Enabled Configuration
|
||||
if api.Config.DemoMode {
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.demoModeAppError)
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.appDemoModeError)
|
||||
} else {
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.uploadNewDocument)
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.deleteDocument)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.editDocument)
|
||||
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.identifyDocument)
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.editSettings)
|
||||
api.Router.POST("/documents", api.authWebAppMiddleware, api.appUploadNewDocument)
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.appDeleteDocument)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.appEditDocument)
|
||||
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.appIdentifyDocument)
|
||||
api.Router.POST("/settings", api.authWebAppMiddleware, api.appEditSettings)
|
||||
}
|
||||
|
||||
// Search Enabled Configuration
|
||||
if api.Config.SearchEnabled {
|
||||
api.Router.GET("/search", api.authWebAppMiddleware, api.createAppResourcesRoute("search"))
|
||||
api.Router.POST("/search", api.authWebAppMiddleware, api.saveNewDocument)
|
||||
api.Router.GET("/search", api.authWebAppMiddleware, api.appGetSearch)
|
||||
api.Router.POST("/search", api.authWebAppMiddleware, api.appSaveNewDocument)
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,22 +137,22 @@ func (api *API) registerKOAPIRoutes(apiGroup *gin.RouterGroup) {
|
||||
|
||||
// KO Sync Routes (WebApp Uses - Progress & Activity)
|
||||
koGroup.GET("/documents/:document/file", api.authKOMiddleware, api.downloadDocument)
|
||||
koGroup.GET("/syncs/progress/:document", api.authKOMiddleware, api.getProgress)
|
||||
koGroup.GET("/users/auth", api.authKOMiddleware, api.authorizeUser)
|
||||
koGroup.POST("/activity", api.authKOMiddleware, api.addActivities)
|
||||
koGroup.POST("/syncs/activity", api.authKOMiddleware, api.checkActivitySync)
|
||||
koGroup.POST("/users/create", api.createUser)
|
||||
koGroup.PUT("/syncs/progress", api.authKOMiddleware, api.setProgress)
|
||||
koGroup.GET("/syncs/progress/:document", api.authKOMiddleware, api.koGetProgress)
|
||||
koGroup.GET("/users/auth", api.authKOMiddleware, api.koAuthorizeUser)
|
||||
koGroup.POST("/activity", api.authKOMiddleware, api.koAddActivities)
|
||||
koGroup.POST("/syncs/activity", api.authKOMiddleware, api.koCheckActivitySync)
|
||||
koGroup.POST("/users/create", api.koCreateUser)
|
||||
koGroup.PUT("/syncs/progress", api.authKOMiddleware, api.koSetProgress)
|
||||
|
||||
// Demo Mode Enabled Configuration
|
||||
if api.Config.DemoMode {
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.demoModeJSONError)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.demoModeJSONError)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.demoModeJSONError)
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.koDemoModeJSONError)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.koDemoModeJSONError)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.koDemoModeJSONError)
|
||||
} else {
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.addDocuments)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.checkDocumentsSync)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.uploadExistingDocument)
|
||||
koGroup.POST("/documents", api.authKOMiddleware, api.koAddDocuments)
|
||||
koGroup.POST("/syncs/documents", api.authKOMiddleware, api.koCheckDocumentsSync)
|
||||
koGroup.PUT("/documents/:document/file", api.authKOMiddleware, api.koUploadExistingDocument)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,50 +70,30 @@ type requestDocumentAdd struct {
|
||||
Source search.Source `form:"source"`
|
||||
}
|
||||
|
||||
func (api *API) webManifest(c *gin.Context) {
|
||||
func (api *API) appWebManifest(c *gin.Context) {
|
||||
c.Header("Content-Type", "application/manifest+json")
|
||||
c.FileFromFS("assets/manifest.json", http.FS(api.Assets))
|
||||
}
|
||||
|
||||
func (api *API) serviceWorker(c *gin.Context) {
|
||||
func (api *API) appServiceWorker(c *gin.Context) {
|
||||
c.FileFromFS("assets/sw.js", http.FS(api.Assets))
|
||||
}
|
||||
|
||||
func (api *API) faviconIcon(c *gin.Context) {
|
||||
func (api *API) appFaviconIcon(c *gin.Context) {
|
||||
c.FileFromFS("assets/icons/favicon.ico", http.FS(api.Assets))
|
||||
}
|
||||
|
||||
func (api *API) localDocuments(c *gin.Context) {
|
||||
func (api *API) appLocalDocuments(c *gin.Context) {
|
||||
c.FileFromFS("assets/local/index.htm", http.FS(api.Assets))
|
||||
}
|
||||
|
||||
func (api *API) documentReader(c *gin.Context) {
|
||||
func (api *API) appDocumentReader(c *gin.Context) {
|
||||
c.FileFromFS("assets/reader/index.htm", http.FS(api.Assets))
|
||||
}
|
||||
|
||||
func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any) func(*gin.Context) {
|
||||
// Merge Optional Template Data
|
||||
var templateVarsBase = gin.H{}
|
||||
if len(args) > 0 {
|
||||
templateVarsBase = args[0]
|
||||
}
|
||||
templateVarsBase["RouteName"] = routeName
|
||||
templateVarsBase["SearchEnabled"] = api.Config.SearchEnabled
|
||||
|
||||
return func(c *gin.Context) {
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
}
|
||||
|
||||
// Copy Base & Update
|
||||
templateVars := gin.H{}
|
||||
for k, v := range templateVarsBase {
|
||||
templateVars[k] = v
|
||||
}
|
||||
templateVars["User"] = userID
|
||||
|
||||
if routeName == "documents" {
|
||||
func (api *API) appGetDocuments(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("documents", c)
|
||||
userID := templateVars["User"].(string)
|
||||
qParams := bindQueryParams(c, 9)
|
||||
|
||||
var query *string
|
||||
@ -129,20 +109,20 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
Limit: *qParams.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDocumentsWithStats DB Error:", err)
|
||||
log.Error("[appGetDocuments] GetDocumentsWithStats DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
length, err := api.DB.Queries.GetDocumentsSize(api.DB.Ctx, query)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDocumentsSize DB Error:", err)
|
||||
log.Error("[appGetDocuments] GetDocumentsSize DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsSize DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = api.getDocumentsWordCount(documents); err != nil {
|
||||
log.Error("[createAppResourcesRoute] Unable to Get Word Counts: ", err)
|
||||
log.Error("[appGetDocuments] Unable to Get Word Counts: ", err)
|
||||
}
|
||||
|
||||
totalPages := int64(math.Ceil(float64(length) / float64(*qParams.Limit)))
|
||||
@ -159,10 +139,17 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
|
||||
templateVars["PageLimit"] = *qParams.Limit
|
||||
templateVars["Data"] = documents
|
||||
} else if routeName == "document" {
|
||||
|
||||
c.HTML(http.StatusOK, "page/documents", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetDocument(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("document", c)
|
||||
userID := templateVars["User"].(string)
|
||||
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[createAppResourcesRoute] Invalid URI Bind")
|
||||
log.Error("[appGetDocument] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
@ -172,14 +159,48 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
DocumentID: rDocID.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDocumentWithStats DB Error:", err)
|
||||
log.Error("[appGetDocument] GetDocumentWithStats DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = document
|
||||
templateVars["TotalTimeLeftSeconds"] = int64((100.0 - document.Percentage) * float64(document.SecondsPerPercent))
|
||||
} else if routeName == "activity" {
|
||||
|
||||
c.HTML(http.StatusOK, "page/document", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetProgress(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("progress", c)
|
||||
userID := templateVars["User"].(string)
|
||||
qParams := bindQueryParams(c, 15)
|
||||
|
||||
progressFilter := database.GetProgressParams{
|
||||
UserID: userID,
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
}
|
||||
|
||||
if qParams.Document != nil {
|
||||
progressFilter.DocFilter = true
|
||||
progressFilter.DocumentID = *qParams.Document
|
||||
}
|
||||
|
||||
progress, err := api.DB.Queries.GetProgress(api.DB.Ctx, progressFilter)
|
||||
if err != nil {
|
||||
log.Error("[appGetProgress] GetProgress DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = progress
|
||||
|
||||
c.HTML(http.StatusOK, "page/progress", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetActivity(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("activity", c)
|
||||
userID := templateVars["User"].(string)
|
||||
qParams := bindQueryParams(c, 15)
|
||||
|
||||
activityFilter := database.GetActivityParams{
|
||||
@ -195,52 +216,70 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
|
||||
activity, err := api.DB.Queries.GetActivity(api.DB.Ctx, activityFilter)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetActivity DB Error:", err)
|
||||
log.Error("[appGetActivity] GetActivity DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = activity
|
||||
} else if routeName == "home" {
|
||||
|
||||
c.HTML(http.StatusOK, "page/activity", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetHome(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("home", c)
|
||||
userID := templateVars["User"].(string)
|
||||
|
||||
start := time.Now()
|
||||
read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, userID)
|
||||
graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, userID)
|
||||
log.Info("GetDailyReadStats Performance: ", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, userID)
|
||||
databaseInfo, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, userID)
|
||||
log.Info("GetDatabaseInfo Performance: ", time.Since(start))
|
||||
|
||||
streaks, _ := api.DB.Queries.GetUserStreaks(api.DB.Ctx, userID)
|
||||
wpm_leaderboard, _ := api.DB.Queries.GetWPMLeaderboard(api.DB.Ctx)
|
||||
WPMLeaderboard, _ := api.DB.Queries.GetWPMLeaderboard(api.DB.Ctx)
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"Streaks": streaks,
|
||||
"GraphData": read_graph_data,
|
||||
"DatabaseInfo": database_info,
|
||||
"WPMLeaderboard": wpm_leaderboard,
|
||||
"GraphData": graphData,
|
||||
"DatabaseInfo": databaseInfo,
|
||||
"WPMLeaderboard": WPMLeaderboard,
|
||||
}
|
||||
} else if routeName == "settings" {
|
||||
|
||||
c.HTML(http.StatusOK, "page/home", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetSettings(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("settings", c)
|
||||
userID := templateVars["User"].(string)
|
||||
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, userID)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetUser DB Error:", err)
|
||||
log.Error("[appGetSettings] GetUser DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] GetDevices DB Error:", err)
|
||||
log.Error("[appGetSettings] GetDevices DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"Settings": gin.H{
|
||||
"TimeOffset": *user.TimeOffset,
|
||||
},
|
||||
"Devices": devices,
|
||||
}
|
||||
} else if routeName == "search" {
|
||||
|
||||
c.HTML(http.StatusOK, "page/settings", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetSearch(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("search", c)
|
||||
|
||||
var sParams searchParams
|
||||
c.BindQuery(&sParams)
|
||||
|
||||
@ -258,121 +297,45 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
} else if sParams.Query != nil || sParams.Source != nil {
|
||||
templateVars["SearchErrorMessage"] = "Invalid Query"
|
||||
}
|
||||
} else if routeName == "login" {
|
||||
|
||||
c.HTML(http.StatusOK, "page/search", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appGetLogin(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("login", c)
|
||||
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
||||
}
|
||||
c.HTML(http.StatusOK, "page/"+routeName, templateVars)
|
||||
}
|
||||
c.HTML(http.StatusOK, "page/login", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) getDocumentCover(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[getDocumentCover] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid cover.")
|
||||
func (api *API) appGetRegister(c *gin.Context) {
|
||||
if !api.Config.RegistrationEnabled {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[getDocumentCover] GetDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
||||
return
|
||||
templateVars := api.getBaseTemplateVars("login", c)
|
||||
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
||||
templateVars["Register"] = true
|
||||
c.HTML(http.StatusOK, "page/login", templateVars)
|
||||
}
|
||||
|
||||
// Handle Identified Document
|
||||
if document.Coverfile != nil {
|
||||
if *document.Coverfile == "UNKNOWN" {
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.Assets))
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Path
|
||||
safePath := filepath.Join(api.Config.DataPath, "covers", *document.Coverfile)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(safePath)
|
||||
if err != nil {
|
||||
log.Error("[getDocumentCover] File Should But Doesn't Exist:", err)
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.Assets))
|
||||
return
|
||||
}
|
||||
|
||||
c.File(safePath)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Attempt Metadata ---
|
||||
|
||||
var coverDir string = filepath.Join(api.Config.DataPath, "covers")
|
||||
var coverFile string = "UNKNOWN"
|
||||
|
||||
// Identify Documents & Save Covers
|
||||
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
|
||||
Title: document.Title,
|
||||
Author: document.Author,
|
||||
})
|
||||
|
||||
if err == nil && len(metadataResults) > 0 && metadataResults[0].ID != nil {
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
// Save Cover
|
||||
fileName, err := metadata.CacheCover(*firstResult.ID, coverDir, document.ID, false)
|
||||
if err == nil {
|
||||
coverFile = *fileName
|
||||
}
|
||||
|
||||
// Store First Metadata Result
|
||||
if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
|
||||
DocumentID: document.ID,
|
||||
Title: firstResult.Title,
|
||||
Author: firstResult.Author,
|
||||
Description: firstResult.Description,
|
||||
Gbid: firstResult.ID,
|
||||
Olid: nil,
|
||||
Isbn10: firstResult.ISBN10,
|
||||
Isbn13: firstResult.ISBN13,
|
||||
}); err != nil {
|
||||
log.Error("[getDocumentCover] AddMetadata DB Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Coverfile: &coverFile,
|
||||
}); err != nil {
|
||||
log.Warn("[getDocumentCover] UpsertDocument DB Error:", err)
|
||||
}
|
||||
|
||||
// Return Unknown Cover
|
||||
if coverFile == "UNKNOWN" {
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.Assets))
|
||||
return
|
||||
}
|
||||
|
||||
coverFilePath := filepath.Join(coverDir, coverFile)
|
||||
c.File(coverFilePath)
|
||||
}
|
||||
|
||||
func (api *API) getDocumentProgress(c *gin.Context) {
|
||||
func (api *API) appGetDocumentProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[getDocumentProgress] Invalid URI Bind")
|
||||
log.Error("[appGetDocumentProgress] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
|
||||
progress, err := api.DB.Queries.GetProgress(api.DB.Ctx, database.GetProgressParams{
|
||||
progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{
|
||||
DocumentID: rDoc.DocumentID,
|
||||
UserID: rUser.(string),
|
||||
})
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Error("[getDocumentProgress] UpsertDocument DB Error:", err)
|
||||
log.Error("[appGetDocumentProgress] UpsertDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
@ -382,7 +345,7 @@ func (api *API) getDocumentProgress(c *gin.Context) {
|
||||
DocumentID: rDoc.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[getDocumentProgress] GetDocumentWithStats DB Error:", err)
|
||||
log.Error("[appGetDocumentProgress] GetDocumentWithStats DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
@ -397,13 +360,13 @@ func (api *API) getDocumentProgress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) getDevices(c *gin.Context) {
|
||||
func (api *API) appGetDevices(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string))
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Error("[getDevices] GetDevices DB Error:", err)
|
||||
log.Error("[appGetDevices] GetDevices DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
@ -411,10 +374,10 @@ func (api *API) getDevices(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, devices)
|
||||
}
|
||||
|
||||
func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
func (api *API) appUploadNewDocument(c *gin.Context) {
|
||||
var rDocUpload requestDocumentUpload
|
||||
if err := c.ShouldBind(&rDocUpload); err != nil {
|
||||
log.Error("[uploadNewDocument] Invalid Form Bind")
|
||||
log.Error("[appUploadNewDocument] Invalid Form Bind")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
@ -427,14 +390,14 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := rDocUpload.DocumentFile.Open()
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] File Error: ", err)
|
||||
log.Error("[appUploadNewDocument] File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
||||
return
|
||||
}
|
||||
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] MIME Error")
|
||||
log.Error("[appUploadNewDocument] MIME Error")
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
||||
return
|
||||
}
|
||||
@ -442,7 +405,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
|
||||
// Validate Extension
|
||||
if !slices.Contains([]string{".epub"}, fileExtension) {
|
||||
log.Error("[uploadNewDocument] Invalid FileType: ", fileExtension)
|
||||
log.Error("[appUploadNewDocument] Invalid FileType: ", fileExtension)
|
||||
errorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
||||
return
|
||||
}
|
||||
@ -450,7 +413,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Create Temp File
|
||||
tempFile, err := os.CreateTemp("", "book")
|
||||
if err != nil {
|
||||
log.Warn("[uploadNewDocument] Temp File Create Error: ", err)
|
||||
log.Warn("[appUploadNewDocument] Temp File Create Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to create temp file.")
|
||||
return
|
||||
}
|
||||
@ -460,7 +423,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Save Temp
|
||||
err = c.SaveUploadedFile(rDocUpload.DocumentFile, tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] File Error: ", err)
|
||||
log.Error("[appUploadNewDocument] File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
@ -468,7 +431,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Get Metadata
|
||||
metadataInfo, err := metadata.GetMetadata(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Warn("[uploadNewDocument] GetMetadata Error: ", err)
|
||||
log.Warn("[appUploadNewDocument] GetMetadata Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to acquire file metadata.")
|
||||
return
|
||||
}
|
||||
@ -476,7 +439,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Calculate Partial MD5 ID
|
||||
partialMD5, err := utils.CalculatePartialMD5(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Warn("[uploadNewDocument] Partial MD5 Error: ", err)
|
||||
log.Warn("[appUploadNewDocument] Partial MD5 Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate partial MD5.")
|
||||
return
|
||||
}
|
||||
@ -491,7 +454,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Calculate Actual MD5
|
||||
fileHash, err := getFileMD5(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] MD5 Hash Failure:", err)
|
||||
log.Error("[appUploadNewDocument] MD5 Hash Failure:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate MD5.")
|
||||
return
|
||||
}
|
||||
@ -499,7 +462,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
// Get Word Count
|
||||
wordCount, err := metadata.GetWordCount(tempFile.Name())
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] Word Count Failure:", err)
|
||||
log.Error("[appUploadNewDocument] Word Count Failure:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate word count.")
|
||||
return
|
||||
}
|
||||
@ -528,7 +491,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadNewDocument] Dest File Error:", err)
|
||||
log.Error("[appUploadNewDocument] Dest File Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
@ -536,7 +499,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
|
||||
// Copy File
|
||||
if _, err = io.Copy(destFile, tempFile); err != nil {
|
||||
log.Error("[uploadNewDocument] Copy Temp File Error:", err)
|
||||
log.Error("[appUploadNewDocument] Copy Temp File Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
@ -551,7 +514,7 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
Md5: fileHash,
|
||||
Filepath: &fileName,
|
||||
}); err != nil {
|
||||
log.Error("[uploadNewDocument] UpsertDocument DB Error:", err)
|
||||
log.Error("[appUploadNewDocument] UpsertDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
@ -559,17 +522,17 @@ func (api *API) uploadNewDocument(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
|
||||
}
|
||||
|
||||
func (api *API) editDocument(c *gin.Context) {
|
||||
func (api *API) appEditDocument(c *gin.Context) {
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[createAppResourcesRoute] Invalid URI Bind")
|
||||
log.Error("[appEditDocument] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
|
||||
var rDocEdit requestDocumentEdit
|
||||
if err := c.ShouldBind(&rDocEdit); err != nil {
|
||||
log.Error("[createAppResourcesRoute] Invalid Form Bind")
|
||||
log.Error("[appEditDocument] Invalid Form Bind")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
@ -583,7 +546,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
rDocEdit.RemoveCover == nil &&
|
||||
rDocEdit.CoverGBID == nil &&
|
||||
rDocEdit.CoverFile == nil {
|
||||
log.Error("[createAppResourcesRoute] Missing Form Values")
|
||||
log.Error("[appEditDocument] Missing Form Values")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
@ -597,14 +560,14 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := rDocEdit.CoverFile.Open()
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] File Error")
|
||||
log.Error("[appEditDocument] File Error")
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
||||
return
|
||||
}
|
||||
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] MIME Error")
|
||||
log.Error("[appEditDocument] MIME Error")
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
||||
return
|
||||
}
|
||||
@ -612,7 +575,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
|
||||
// Validate Extension
|
||||
if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
|
||||
log.Error("[uploadDocumentFile] Invalid FileType: ", fileExtension)
|
||||
log.Error("[appEditDocument] Invalid FileType: ", fileExtension)
|
||||
errorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
||||
return
|
||||
}
|
||||
@ -624,7 +587,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
// Save
|
||||
err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] File Error: ", err)
|
||||
log.Error("[appEditDocument] File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
return
|
||||
}
|
||||
@ -648,7 +611,7 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
Isbn13: api.sanitizeInput(rDocEdit.ISBN13),
|
||||
Coverfile: coverFileName,
|
||||
}); err != nil {
|
||||
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
|
||||
log.Error("[appEditDocument] UpsertDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
@ -657,21 +620,21 @@ func (api *API) editDocument(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) deleteDocument(c *gin.Context) {
|
||||
func (api *API) appDeleteDocument(c *gin.Context) {
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[deleteDocument] Invalid URI Bind")
|
||||
log.Error("[appDeleteDocument] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
changed, err := api.DB.Queries.DeleteDocument(api.DB.Ctx, rDocID.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[deleteDocument] DeleteDocument DB Error")
|
||||
log.Error("[appDeleteDocument] DeleteDocument DB Error")
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("DeleteDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
if changed == 0 {
|
||||
log.Error("[deleteDocument] DeleteDocument DB Error")
|
||||
log.Error("[appDeleteDocument] DeleteDocument DB Error")
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
@ -679,19 +642,17 @@ func (api *API) deleteDocument(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "../")
|
||||
}
|
||||
|
||||
func (api *API) identifyDocument(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
func (api *API) appIdentifyDocument(c *gin.Context) {
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[identifyDocument] Invalid URI Bind")
|
||||
log.Error("[appIdentifyDocument] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid document.")
|
||||
return
|
||||
}
|
||||
|
||||
var rDocIdentify requestDocumentIdentify
|
||||
if err := c.ShouldBind(&rDocIdentify); err != nil {
|
||||
log.Error("[identifyDocument] Invalid Form Bind")
|
||||
log.Error("[appIdentifyDocument] Invalid Form Bind")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
@ -709,15 +670,14 @@ func (api *API) identifyDocument(c *gin.Context) {
|
||||
|
||||
// Validate Values
|
||||
if rDocIdentify.ISBN == nil && rDocIdentify.Title == nil && rDocIdentify.Author == nil {
|
||||
log.Error("[identifyDocument] Invalid Form")
|
||||
log.Error("[appIdentifyDocument] Invalid Form")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
// Template Variables
|
||||
templateVars := gin.H{
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
}
|
||||
// Get Template Variables
|
||||
templateVars := api.getBaseTemplateVars("document", c)
|
||||
userID := templateVars["User"].(string)
|
||||
|
||||
// Get Metadata
|
||||
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
|
||||
@ -740,21 +700,21 @@ func (api *API) identifyDocument(c *gin.Context) {
|
||||
Isbn10: firstResult.ISBN10,
|
||||
Isbn13: firstResult.ISBN13,
|
||||
}); err != nil {
|
||||
log.Error("[identifyDocument] AddMetadata DB Error:", err)
|
||||
log.Error("[appIdentifyDocument] AddMetadata DB Error:", err)
|
||||
}
|
||||
|
||||
templateVars["Metadata"] = firstResult
|
||||
} else {
|
||||
log.Warn("[identifyDocument] Metadata Error")
|
||||
log.Warn("[appIdentifyDocument] Metadata Error")
|
||||
templateVars["MetadataError"] = "No Metadata Found"
|
||||
}
|
||||
|
||||
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
|
||||
UserID: rUser.(string),
|
||||
UserID: userID,
|
||||
DocumentID: rDocID.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[identifyDocument] GetDocumentWithStats DB Error:", err)
|
||||
log.Error("[appIdentifyDocument] GetDocumentWithStats DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentWithStats DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
@ -765,24 +725,16 @@ func (api *API) identifyDocument(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "page/document", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) saveNewDocument(c *gin.Context) {
|
||||
func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
var rDocAdd requestDocumentAdd
|
||||
if err := c.ShouldBind(&rDocAdd); err != nil {
|
||||
log.Error("[saveNewDocument] Invalid Form Bind")
|
||||
log.Error("[appSaveNewDocument] Invalid Form Bind")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
// Render Initial Template
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
}
|
||||
templateVars := gin.H{
|
||||
"RouteName": "search",
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
"User": userID,
|
||||
}
|
||||
templateVars := api.getBaseTemplateVars("search", c)
|
||||
c.HTML(http.StatusOK, "page/search", templateVars)
|
||||
|
||||
// Create Streamer
|
||||
@ -812,7 +764,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Save Book
|
||||
tempFilePath, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source)
|
||||
if err != nil {
|
||||
log.Warn("[saveNewDocument] Temp File Error: ", err)
|
||||
log.Warn("[appSaveNewDocument] Temp File Error: ", err)
|
||||
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -823,7 +775,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Calculate Partial MD5 ID
|
||||
partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
|
||||
if err != nil {
|
||||
log.Warn("[saveNewDocument] Partial MD5 Error: ", err)
|
||||
log.Warn("[appSaveNewDocument] Partial MD5 Error: ", err)
|
||||
sendDownloadMessage("Unable to calculate partial MD5", gin.H{"Error": true})
|
||||
}
|
||||
|
||||
@ -857,7 +809,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Open Source File
|
||||
sourceFile, err := os.Open(tempFilePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Source File Error:", err)
|
||||
log.Error("[appSaveNewDocument] Source File Error:", err)
|
||||
sendDownloadMessage("Unable to open file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -868,7 +820,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Dest File Error:", err)
|
||||
log.Error("[appSaveNewDocument] Dest File Error:", err)
|
||||
sendDownloadMessage("Unable to create file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -876,7 +828,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
|
||||
// Copy File
|
||||
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
||||
log.Error("[saveNewDocument] Copy Temp File Error:", err)
|
||||
log.Error("[appSaveNewDocument] Copy Temp File Error:", err)
|
||||
sendDownloadMessage("Unable to save file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -887,7 +839,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Hash Failure:", err)
|
||||
log.Error("[appSaveNewDocument] Hash Failure:", err)
|
||||
sendDownloadMessage("Unable to calculate MD5", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -898,7 +850,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Get Word Count
|
||||
wordCount, err := metadata.GetWordCount(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Word Count Failure:", err)
|
||||
log.Error("[appSaveNewDocument] Word Count Failure:", err)
|
||||
sendDownloadMessage("Unable to calculate word count", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -915,7 +867,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
Filepath: &fileName,
|
||||
Words: &wordCount,
|
||||
}); err != nil {
|
||||
log.Error("[saveNewDocument] UpsertDocument DB Error:", err)
|
||||
log.Error("[appSaveNewDocument] UpsertDocument DB Error:", err)
|
||||
sendDownloadMessage("Unable to save to database", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
@ -928,34 +880,32 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) editSettings(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
func (api *API) appEditSettings(c *gin.Context) {
|
||||
var rUserSettings requestSettingsEdit
|
||||
if err := c.ShouldBind(&rUserSettings); err != nil {
|
||||
log.Error("[editSettings] Invalid Form Bind")
|
||||
log.Error("[appEditSettings] Invalid Form Bind")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Something Exists
|
||||
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
|
||||
log.Error("[editSettings] Missing Form Values")
|
||||
log.Error("[appEditSettings] Missing Form Values")
|
||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
||||
return
|
||||
}
|
||||
|
||||
templateVars := gin.H{
|
||||
"User": rUser,
|
||||
}
|
||||
templateVars := api.getBaseTemplateVars("settings", c)
|
||||
userID := templateVars["User"].(string)
|
||||
|
||||
newUserSettings := database.UpdateUserParams{
|
||||
UserID: rUser.(string),
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
// Set New Password
|
||||
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
|
||||
authorized := api.authorizeCredentials(rUser.(string), password)
|
||||
authorized := api.authorizeCredentials(userID, password)
|
||||
if authorized == true {
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
@ -979,38 +929,39 @@ func (api *API) editSettings(c *gin.Context) {
|
||||
// Update User
|
||||
_, err := api.DB.Queries.UpdateUser(api.DB.Ctx, newUserSettings)
|
||||
if err != nil {
|
||||
log.Error("[editSettings] UpdateUser DB Error:", err)
|
||||
log.Error("[appEditSettings] UpdateUser DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Get User
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, rUser.(string))
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, userID)
|
||||
if err != nil {
|
||||
log.Error("[editSettings] GetUser DB Error:", err)
|
||||
log.Error("[appEditSettings] GetUser DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Get Devices
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string))
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID)
|
||||
if err != nil {
|
||||
log.Error("[editSettings] GetDevices DB Error:", err)
|
||||
log.Error("[appEditSettings] GetDevices DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"Settings": gin.H{
|
||||
"TimeOffset": *user.TimeOffset,
|
||||
},
|
||||
"Devices": devices,
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "page/settings", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) appDemoModeError(c *gin.Context) {
|
||||
errorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
|
||||
}
|
||||
|
||||
func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStatsRow) error {
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
@ -1050,6 +1001,23 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) gin.H {
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"User": userID,
|
||||
"RouteName": routeName,
|
||||
"Config": gin.H{
|
||||
"Version": api.Config.Version,
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {
|
||||
var qParams queryParams
|
||||
c.BindQuery(&qParams)
|
||||
|
63
api/auth.go
63
api/auth.go
@ -115,35 +115,31 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) authFormLogin(c *gin.Context) {
|
||||
func (api *API) appAuthFormLogin(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("login", c)
|
||||
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusUnauthorized, "page/login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// MD5 - KOSync Compatiblity
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
||||
c.HTML(http.StatusUnauthorized, "page/login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// Set Session
|
||||
session := sessions.Default(c)
|
||||
if err := setSession(session, username); err != nil {
|
||||
c.HTML(http.StatusUnauthorized, "page/login", gin.H{
|
||||
"RegistrationEnabled": api.Config.RegistrationEnabled,
|
||||
"Error": "Unknown Error",
|
||||
})
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
@ -151,30 +147,29 @@ func (api *API) authFormLogin(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (api *API) authFormRegister(c *gin.Context) {
|
||||
func (api *API) appAuthFormRegister(c *gin.Context) {
|
||||
if !api.Config.RegistrationEnabled {
|
||||
errorPage(c, http.StatusUnauthorized, "Nice try. Registration is disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
templateVars := api.getBaseTemplateVars("login", c)
|
||||
templateVars["Register"] = true
|
||||
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusBadRequest, "page/login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
templateVars["Error"] = "Invalid User or Password"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "page/login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
@ -185,19 +180,17 @@ func (api *API) authFormRegister(c *gin.Context) {
|
||||
|
||||
// SQL Error
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "page/login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
log.Error("[appAuthFormRegister] CreateUser DB Error:", err)
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
// User Already Exists
|
||||
if rows == 0 {
|
||||
c.HTML(http.StatusBadRequest, "page/login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
log.Warn("[appAuthFormRegister] User Already Exists:", username)
|
||||
templateVars["Error"] = "Registration Disabled or User Already Exists"
|
||||
c.HTML(http.StatusBadRequest, "page/login", templateVars)
|
||||
return
|
||||
}
|
||||
|
||||
@ -212,21 +205,13 @@ func (api *API) authFormRegister(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (api *API) authLogout(c *gin.Context) {
|
||||
func (api *API) appAuthLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
func (api *API) demoModeAppError(c *gin.Context) {
|
||||
errorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
|
||||
}
|
||||
|
||||
func (api *API) demoModeJSONError(c *gin.Context) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Not Allowed in Demo Mode"})
|
||||
}
|
||||
|
||||
func getSession(session sessions.Session) (user string, ok bool) {
|
||||
// Check Session
|
||||
authorizedUser := session.Get("authorizedUser")
|
||||
|
140
api/common.go
Normal file
140
api/common.go
Normal file
@ -0,0 +1,140 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/metadata"
|
||||
)
|
||||
|
||||
func (api *API) downloadDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[downloadDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get Document
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[downloadDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
if document.Filepath == nil {
|
||||
log.Error("[downloadDocument] Document Doesn't Have File:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exist"})
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(api.Config.DataPath, "documents", *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("[downloadDocument] File Doesn't Exist:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exists"})
|
||||
return
|
||||
}
|
||||
|
||||
// Force Download (Security)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(*document.Filepath)))
|
||||
c.File(filePath)
|
||||
}
|
||||
|
||||
func (api *API) getDocumentCover(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[getDocumentCover] Invalid URI Bind")
|
||||
errorPage(c, http.StatusNotFound, "Invalid cover.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[getDocumentCover] GetDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Identified Document
|
||||
if document.Coverfile != nil {
|
||||
if *document.Coverfile == "UNKNOWN" {
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.Assets))
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Path
|
||||
safePath := filepath.Join(api.Config.DataPath, "covers", *document.Coverfile)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(safePath)
|
||||
if err != nil {
|
||||
log.Error("[getDocumentCover] File Should But Doesn't Exist:", err)
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.Assets))
|
||||
return
|
||||
}
|
||||
|
||||
c.File(safePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt Metadata
|
||||
var coverDir string = filepath.Join(api.Config.DataPath, "covers")
|
||||
var coverFile string = "UNKNOWN"
|
||||
|
||||
// Identify Documents & Save Covers
|
||||
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
|
||||
Title: document.Title,
|
||||
Author: document.Author,
|
||||
})
|
||||
|
||||
if err == nil && len(metadataResults) > 0 && metadataResults[0].ID != nil {
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
// Save Cover
|
||||
fileName, err := metadata.CacheCover(*firstResult.ID, coverDir, document.ID, false)
|
||||
if err == nil {
|
||||
coverFile = *fileName
|
||||
}
|
||||
|
||||
// Store First Metadata Result
|
||||
if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
|
||||
DocumentID: document.ID,
|
||||
Title: firstResult.Title,
|
||||
Author: firstResult.Author,
|
||||
Description: firstResult.Description,
|
||||
Gbid: firstResult.ID,
|
||||
Olid: nil,
|
||||
Isbn10: firstResult.ISBN10,
|
||||
Isbn13: firstResult.ISBN13,
|
||||
}); err != nil {
|
||||
log.Error("[getDocumentCover] AddMetadata DB Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Coverfile: &coverFile,
|
||||
}); err != nil {
|
||||
log.Warn("[getDocumentCover] UpsertDocument DB Error:", err)
|
||||
}
|
||||
|
||||
// Return Unknown Cover
|
||||
if coverFile == "UNKNOWN" {
|
||||
c.FileFromFS("assets/images/no-cover.jpg", http.FS(api.Assets))
|
||||
return
|
||||
}
|
||||
|
||||
coverFilePath := filepath.Join(coverDir, coverFile)
|
||||
c.File(coverFilePath)
|
||||
}
|
146
api/ko-routes.go
146
api/ko-routes.go
@ -75,13 +75,13 @@ type requestDocumentID struct {
|
||||
DocumentID string `uri:"document" binding:"required"`
|
||||
}
|
||||
|
||||
func (api *API) authorizeUser(c *gin.Context) {
|
||||
func (api *API) koAuthorizeUser(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"authorized": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) createUser(c *gin.Context) {
|
||||
func (api *API) koCreateUser(c *gin.Context) {
|
||||
if !api.Config.RegistrationEnabled {
|
||||
c.AbortWithStatus(http.StatusConflict)
|
||||
return
|
||||
@ -89,20 +89,20 @@ func (api *API) createUser(c *gin.Context) {
|
||||
|
||||
var rUser requestUser
|
||||
if err := c.ShouldBindJSON(&rUser); err != nil {
|
||||
log.Error("[createUser] Invalid JSON Bind")
|
||||
log.Error("[koCreateUser] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
if rUser.Username == "" || rUser.Password == "" {
|
||||
log.Error("[createUser] Invalid User - Empty Username or Password")
|
||||
log.Error("[koCreateUser] Invalid User - Empty Username or Password")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(rUser.Password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
log.Error("[createUser] Argon2 Hash Failure:", err)
|
||||
log.Error("[koCreateUser] Argon2 Hash Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
@ -112,7 +112,7 @@ func (api *API) createUser(c *gin.Context) {
|
||||
Pass: &hashedPassword,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[createUser] CreateUser DB Error:", err)
|
||||
log.Error("[koCreateUser] CreateUser DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
@ -128,12 +128,12 @@ func (api *API) createUser(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) setProgress(c *gin.Context) {
|
||||
func (api *API) koSetProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rPosition requestPosition
|
||||
if err := c.ShouldBindJSON(&rPosition); err != nil {
|
||||
log.Error("[setProgress] Invalid JSON Bind")
|
||||
log.Error("[koSetProgress] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Progress Data"})
|
||||
return
|
||||
}
|
||||
@ -145,14 +145,14 @@ func (api *API) setProgress(c *gin.Context) {
|
||||
DeviceName: rPosition.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Error("[setProgress] UpsertDevice DB Error:", err)
|
||||
log.Error("[koSetProgress] UpsertDevice DB Error:", err)
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: rPosition.DocumentID,
|
||||
}); err != nil {
|
||||
log.Error("[setProgress] UpsertDocument DB Error:", err)
|
||||
log.Error("[koSetProgress] UpsertDocument DB Error:", err)
|
||||
}
|
||||
|
||||
// Create or Replace Progress
|
||||
@ -164,17 +164,17 @@ func (api *API) setProgress(c *gin.Context) {
|
||||
Progress: rPosition.Progress,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[setProgress] UpdateProgress DB Error:", err)
|
||||
log.Error("[koSetProgress] UpdateProgress DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update Statistic
|
||||
log.Info("[setProgress] UpdateDocumentUserStatistic Running...")
|
||||
log.Info("[koSetProgress] UpdateDocumentUserStatistic Running...")
|
||||
if err := api.DB.UpdateDocumentUserStatistic(rPosition.DocumentID, rUser.(string)); err != nil {
|
||||
log.Error("[setProgress] UpdateDocumentUserStatistic Error:", err)
|
||||
log.Error("[koSetProgress] UpdateDocumentUserStatistic Error:", err)
|
||||
}
|
||||
log.Info("[setProgress] UpdateDocumentUserStatistic Complete")
|
||||
log.Info("[koSetProgress] UpdateDocumentUserStatistic Complete")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"document": progress.DocumentID,
|
||||
@ -182,17 +182,17 @@ func (api *API) setProgress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) getProgress(c *gin.Context) {
|
||||
func (api *API) koGetProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[getProgress] Invalid URI Bind")
|
||||
log.Error("[koGetProgress] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
progress, err := api.DB.Queries.GetProgress(api.DB.Ctx, database.GetProgressParams{
|
||||
progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{
|
||||
DocumentID: rDocID.DocumentID,
|
||||
UserID: rUser.(string),
|
||||
})
|
||||
@ -202,7 +202,7 @@ func (api *API) getProgress(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error("[getProgress] GetProgress DB Error:", err)
|
||||
log.Error("[koGetProgress] GetDocumentProgress DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
@ -216,12 +216,12 @@ func (api *API) getProgress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) addActivities(c *gin.Context) {
|
||||
func (api *API) koAddActivities(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rActivity requestActivity
|
||||
if err := c.ShouldBindJSON(&rActivity); err != nil {
|
||||
log.Error("[addActivity] Invalid JSON Bind")
|
||||
log.Error("[koAddActivities] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
||||
return
|
||||
}
|
||||
@ -229,7 +229,7 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("[addActivities] Transaction Begin DB Error:", err)
|
||||
log.Error("[koAddActivities] Transaction Begin DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
@ -250,7 +250,7 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
if _, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: doc,
|
||||
}); err != nil {
|
||||
log.Error("[addActivities] UpsertDocument DB Error:", err)
|
||||
log.Error("[koAddActivities] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
@ -263,7 +263,7 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
DeviceName: rActivity.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Error("[addActivities] UpsertDevice DB Error:", err)
|
||||
log.Error("[koAddActivities] UpsertDevice DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
return
|
||||
}
|
||||
@ -279,7 +279,7 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
StartPercentage: float64(item.Page) / float64(item.Pages),
|
||||
EndPercentage: float64(item.Page+1) / float64(item.Pages),
|
||||
}); err != nil {
|
||||
log.Error("[addActivities] AddActivity DB Error:", err)
|
||||
log.Error("[koAddActivities] AddActivity DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
||||
return
|
||||
}
|
||||
@ -287,18 +287,18 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
|
||||
// Commit Transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("[addActivities] Transaction Commit DB Error:", err)
|
||||
log.Error("[koAddActivities] Transaction Commit DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update Statistic
|
||||
for _, doc := range allDocuments {
|
||||
log.Info("[addActivities] UpdateDocumentUserStatistic Running...")
|
||||
log.Info("[koAddActivities] UpdateDocumentUserStatistic Running...")
|
||||
if err := api.DB.UpdateDocumentUserStatistic(doc, rUser.(string)); err != nil {
|
||||
log.Error("[addActivities] UpdateDocumentUserStatistic Error:", err)
|
||||
log.Error("[koAddActivities] UpdateDocumentUserStatistic Error:", err)
|
||||
}
|
||||
log.Info("[addActivities] UpdateDocumentUserStatistic Complete")
|
||||
log.Info("[koAddActivities] UpdateDocumentUserStatistic Complete")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@ -306,12 +306,12 @@ func (api *API) addActivities(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) checkActivitySync(c *gin.Context) {
|
||||
func (api *API) koCheckActivitySync(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rCheckActivity requestCheckActivitySync
|
||||
if err := c.ShouldBindJSON(&rCheckActivity); err != nil {
|
||||
log.Error("[checkActivitySync] Invalid JSON Bind")
|
||||
log.Error("[koCheckActivitySync] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
@ -323,7 +323,7 @@ func (api *API) checkActivitySync(c *gin.Context) {
|
||||
DeviceName: rCheckActivity.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
log.Error("[checkActivitySync] UpsertDevice DB Error", err)
|
||||
log.Error("[koCheckActivitySync] UpsertDevice DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
return
|
||||
}
|
||||
@ -336,7 +336,7 @@ func (api *API) checkActivitySync(c *gin.Context) {
|
||||
if err == sql.ErrNoRows {
|
||||
lastActivity = time.UnixMilli(0).Format(time.RFC3339)
|
||||
} else if err != nil {
|
||||
log.Error("[checkActivitySync] GetLastActivity DB Error:", err)
|
||||
log.Error("[koCheckActivitySync] GetLastActivity DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
@ -344,7 +344,7 @@ func (api *API) checkActivitySync(c *gin.Context) {
|
||||
// Parse Time
|
||||
parsedTime, err := time.Parse(time.RFC3339, lastActivity)
|
||||
if err != nil {
|
||||
log.Error("[checkActivitySync] Time Parse Error:", err)
|
||||
log.Error("[koCheckActivitySync] Time Parse Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
@ -354,10 +354,10 @@ func (api *API) checkActivitySync(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) addDocuments(c *gin.Context) {
|
||||
func (api *API) koAddDocuments(c *gin.Context) {
|
||||
var rNewDocs requestDocument
|
||||
if err := c.ShouldBindJSON(&rNewDocs); err != nil {
|
||||
log.Error("[addDocuments] Invalid JSON Bind")
|
||||
log.Error("[koAddDocuments] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document(s)"})
|
||||
return
|
||||
}
|
||||
@ -365,7 +365,7 @@ func (api *API) addDocuments(c *gin.Context) {
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] Transaction Begin DB Error:", err)
|
||||
log.Error("[koAddDocuments] Transaction Begin DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
@ -386,7 +386,7 @@ func (api *API) addDocuments(c *gin.Context) {
|
||||
Description: api.sanitizeInput(doc.Description),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] UpsertDocument DB Error:", err)
|
||||
log.Error("[koAddDocuments] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
@ -394,7 +394,7 @@ func (api *API) addDocuments(c *gin.Context) {
|
||||
|
||||
// Commit Transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("[addDocuments] Transaction Commit DB Error:", err)
|
||||
log.Error("[koAddDocuments] Transaction Commit DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
@ -404,12 +404,12 @@ func (api *API) addDocuments(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rCheckDocs requestCheckDocumentSync
|
||||
if err := c.ShouldBindJSON(&rCheckDocs); err != nil {
|
||||
log.Error("[checkDocumentsSync] Invalid JSON Bind")
|
||||
log.Error("[koCheckDocumentsSync] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
@ -422,7 +422,7 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] UpsertDevice DB Error", err)
|
||||
log.Error("[koCheckDocumentsSync] UpsertDevice DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
return
|
||||
}
|
||||
@ -433,7 +433,7 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
// Get Missing Documents
|
||||
missingDocs, err = api.DB.Queries.GetMissingDocuments(api.DB.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] GetMissingDocuments DB Error", err)
|
||||
log.Error("[koCheckDocumentsSync] GetMissingDocuments DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
@ -441,7 +441,7 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
// Get Deleted Documents
|
||||
deletedDocIDs, err = api.DB.Queries.GetDeletedDocuments(api.DB.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] GetDeletedDocuments DB Error", err)
|
||||
log.Error("[koCheckDocumentsSync] GetDeletedDocuments DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
@ -449,14 +449,14 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
// Get Wanted Documents
|
||||
jsonHaves, err := json.Marshal(rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] JSON Marshal Error", err)
|
||||
log.Error("[koCheckDocumentsSync] JSON Marshal Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
wantedDocs, err := api.DB.Queries.GetWantedDocuments(api.DB.Ctx, string(jsonHaves))
|
||||
if err != nil {
|
||||
log.Error("[checkDocumentsSync] GetWantedDocuments DB Error", err)
|
||||
log.Error("[koCheckDocumentsSync] GetWantedDocuments DB Error", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
@ -497,17 +497,17 @@ func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, rCheckDocSync)
|
||||
}
|
||||
|
||||
func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[uploadExistingDocument] Invalid URI Bind")
|
||||
log.Error("[koUploadExistingDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] File Error:", err)
|
||||
log.Error("[koUploadExistingDocument] File Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -518,7 +518,7 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
|
||||
log.Error("[uploadExistingDocument] Invalid FileType:", fileExtension)
|
||||
log.Error("[koUploadExistingDocument] Invalid FileType:", fileExtension)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
return
|
||||
}
|
||||
@ -526,7 +526,7 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] GetDocument DB Error:", err)
|
||||
log.Error("[koUploadExistingDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
@ -559,7 +559,7 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
if os.IsNotExist(err) {
|
||||
err = c.SaveUploadedFile(fileData, safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] Save Failure:", err)
|
||||
log.Error("[koUploadExistingDocument] Save Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -568,7 +568,7 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] Hash Failure:", err)
|
||||
log.Error("[koUploadExistingDocument] Hash Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -576,7 +576,7 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
// Get Word Count
|
||||
wordCount, err := metadata.GetWordCount(safePath)
|
||||
if err != nil {
|
||||
log.Error("[uploadExistingDocument] Word Count Failure:", err)
|
||||
log.Error("[koUploadExistingDocument] Word Count Failure:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
@ -588,7 +588,7 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
Filepath: &fileName,
|
||||
Words: &wordCount,
|
||||
}); err != nil {
|
||||
log.Error("[uploadExistingDocument] UpsertDocument DB Error:", err)
|
||||
log.Error("[koUploadExistingDocument] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Error"})
|
||||
return
|
||||
}
|
||||
@ -598,42 +598,8 @@ func (api *API) uploadExistingDocument(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) downloadDocument(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
log.Error("[downloadDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get Document
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[downloadDocument] GetDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
if document.Filepath == nil {
|
||||
log.Error("[downloadDocument] Document Doesn't Have File:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exist"})
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(api.Config.DataPath, "documents", *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("[downloadDocument] File Doesn't Exist:", rDoc.DocumentID)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exists"})
|
||||
return
|
||||
}
|
||||
|
||||
// Force Download (Security)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(*document.Filepath)))
|
||||
c.File(filePath)
|
||||
func (api *API) koDemoModeJSONError(c *gin.Context) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Not Allowed in Demo Mode"})
|
||||
}
|
||||
|
||||
func (api *API) sanitizeInput(val any) *string {
|
||||
|
@ -1272,3 +1272,14 @@ class EBookReader {
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", initReader);
|
||||
|
||||
// WIP
|
||||
async function getTOC() {
|
||||
let toc = currentReader.book.navigation.toc;
|
||||
|
||||
// Alternatively:
|
||||
// let nav = await currentReader.book.loaded.navigation;
|
||||
// let toc = nav.toc;
|
||||
|
||||
currentReader.rendition.display(nav.toc[10].href);
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -146,6 +146,20 @@ ORDER BY devices.last_synced DESC;
|
||||
SELECT * FROM documents
|
||||
WHERE id = $document_id LIMIT 1;
|
||||
|
||||
-- name: GetDocumentProgress :one
|
||||
SELECT
|
||||
document_progress.*,
|
||||
devices.device_name
|
||||
FROM document_progress
|
||||
JOIN devices ON document_progress.device_id = devices.id
|
||||
WHERE
|
||||
document_progress.user_id = $user_id
|
||||
AND document_progress.document_id = $document_id
|
||||
ORDER BY
|
||||
document_progress.created_at
|
||||
DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetDocumentWithStats :one
|
||||
SELECT
|
||||
docs.id,
|
||||
@ -258,19 +272,30 @@ WHERE
|
||||
AND documents.deleted = false
|
||||
AND documents.id NOT IN (sqlc.slice('document_ids'));
|
||||
|
||||
-- name: GetProgress :one
|
||||
-- name: GetProgress :many
|
||||
SELECT
|
||||
document_progress.*,
|
||||
devices.device_name
|
||||
FROM document_progress
|
||||
JOIN devices ON document_progress.device_id = devices.id
|
||||
documents.title,
|
||||
documents.author,
|
||||
devices.device_name,
|
||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||
progress.document_id,
|
||||
progress.user_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', progress.created_at, users.time_offset) AS TEXT) AS created_at
|
||||
FROM document_progress AS progress
|
||||
LEFT JOIN users ON progress.user_id = users.id
|
||||
LEFT JOIN devices ON progress.device_id = devices.id
|
||||
LEFT JOIN documents ON progress.document_id = documents.id
|
||||
WHERE
|
||||
document_progress.user_id = $user_id
|
||||
AND document_progress.document_id = $document_id
|
||||
ORDER BY
|
||||
document_progress.created_at
|
||||
DESC
|
||||
LIMIT 1;
|
||||
progress.user_id = $user_id
|
||||
AND (
|
||||
(
|
||||
CAST($doc_filter AS BOOLEAN) = TRUE
|
||||
AND document_id = $document_id
|
||||
) OR $doc_filter = FALSE
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $limit
|
||||
OFFSET $offset;
|
||||
|
||||
-- name: GetUser :one
|
||||
SELECT * FROM users
|
||||
|
@ -478,6 +478,51 @@ func (q *Queries) GetDocument(ctx context.Context, documentID string) (Document,
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDocumentProgress = `-- name: GetDocumentProgress :one
|
||||
SELECT
|
||||
document_progress.user_id, document_progress.document_id, document_progress.device_id, document_progress.percentage, document_progress.progress, document_progress.created_at,
|
||||
devices.device_name
|
||||
FROM document_progress
|
||||
JOIN devices ON document_progress.device_id = devices.id
|
||||
WHERE
|
||||
document_progress.user_id = ?1
|
||||
AND document_progress.document_id = ?2
|
||||
ORDER BY
|
||||
document_progress.created_at
|
||||
DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetDocumentProgressParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
}
|
||||
|
||||
type GetDocumentProgressRow struct {
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Progress string `json:"progress"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DeviceName string `json:"device_name"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetDocumentProgress(ctx context.Context, arg GetDocumentProgressParams) (GetDocumentProgressRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDocumentProgress, arg.UserID, arg.DocumentID)
|
||||
var i GetDocumentProgressRow
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.DocumentID,
|
||||
&i.DeviceID,
|
||||
&i.Percentage,
|
||||
&i.Progress,
|
||||
&i.CreatedAt,
|
||||
&i.DeviceName,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getDocumentWithStats = `-- name: GetDocumentWithStats :one
|
||||
SELECT
|
||||
docs.id,
|
||||
@ -827,49 +872,85 @@ func (q *Queries) GetMissingDocuments(ctx context.Context, documentIds []string)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getProgress = `-- name: GetProgress :one
|
||||
const getProgress = `-- name: GetProgress :many
|
||||
SELECT
|
||||
document_progress.user_id, document_progress.document_id, document_progress.device_id, document_progress.percentage, document_progress.progress, document_progress.created_at,
|
||||
devices.device_name
|
||||
FROM document_progress
|
||||
JOIN devices ON document_progress.device_id = devices.id
|
||||
documents.title,
|
||||
documents.author,
|
||||
devices.device_name,
|
||||
ROUND(CAST(progress.percentage AS REAL) * 100, 2) AS percentage,
|
||||
progress.document_id,
|
||||
progress.user_id,
|
||||
CAST(STRFTIME('%Y-%m-%d %H:%M:%S', progress.created_at, users.time_offset) AS TEXT) AS created_at
|
||||
FROM document_progress AS progress
|
||||
LEFT JOIN users ON progress.user_id = users.id
|
||||
LEFT JOIN devices ON progress.device_id = devices.id
|
||||
LEFT JOIN documents ON progress.document_id = documents.id
|
||||
WHERE
|
||||
document_progress.user_id = ?1
|
||||
AND document_progress.document_id = ?2
|
||||
ORDER BY
|
||||
document_progress.created_at
|
||||
DESC
|
||||
LIMIT 1
|
||||
progress.user_id = ?1
|
||||
AND (
|
||||
(
|
||||
CAST(?2 AS BOOLEAN) = TRUE
|
||||
AND document_id = ?3
|
||||
) OR ?2 = FALSE
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?5
|
||||
OFFSET ?4
|
||||
`
|
||||
|
||||
type GetProgressParams struct {
|
||||
UserID string `json:"user_id"`
|
||||
DocFilter bool `json:"doc_filter"`
|
||||
DocumentID string `json:"document_id"`
|
||||
Offset int64 `json:"offset"`
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetProgressRow struct {
|
||||
UserID string `json:"user_id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Progress string `json:"progress"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
DeviceName string `json:"device_name"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
DocumentID string `json:"document_id"`
|
||||
UserID string `json:"user_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) (GetProgressRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getProgress, arg.UserID, arg.DocumentID)
|
||||
var i GetProgressRow
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.DocumentID,
|
||||
&i.DeviceID,
|
||||
&i.Percentage,
|
||||
&i.Progress,
|
||||
&i.CreatedAt,
|
||||
&i.DeviceName,
|
||||
func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) ([]GetProgressRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getProgress,
|
||||
arg.UserID,
|
||||
arg.DocFilter,
|
||||
arg.DocumentID,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
return i, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetProgressRow
|
||||
for rows.Next() {
|
||||
var i GetProgressRow
|
||||
if err := rows.Scan(
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.DeviceName,
|
||||
&i.Percentage,
|
||||
&i.DocumentID,
|
||||
&i.UserID,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getUser = `-- name: GetUser :one
|
||||
|
@ -152,11 +152,11 @@
|
||||
<span class="mx-4 text-sm font-normal">Documents</span>
|
||||
</a>
|
||||
<a
|
||||
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100"
|
||||
href="/local"
|
||||
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "progress"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
|
||||
href="/progress"
|
||||
>
|
||||
<span class="text-left">{{ template "svg/documents" (dict "Size" 20) }}</span>
|
||||
<span class="mx-4 text-sm font-normal">Local</span>
|
||||
<span class="text-left">{{ template "svg/activity" (dict "Size" 20) }}</span>
|
||||
<span class="mx-4 text-sm font-normal">Progress</span>
|
||||
</a>
|
||||
<a
|
||||
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "activity"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
|
||||
@ -165,7 +165,7 @@
|
||||
<span class="text-left">{{ template "svg/activity" (dict "Size" 20) }}</span>
|
||||
<span class="mx-4 text-sm font-normal">Activity</span>
|
||||
</a>
|
||||
{{ if .SearchEnabled }}
|
||||
{{ if .Config.SearchEnabled }}
|
||||
<a
|
||||
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "search"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
|
||||
href="/search"
|
||||
@ -223,7 +223,7 @@
|
||||
class="transition duration-200 z-20 absolute right-4 top-16 pt-4"
|
||||
>
|
||||
<div
|
||||
class="w-56 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
|
||||
class="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div
|
||||
class="py-1"
|
||||
@ -240,6 +240,15 @@
|
||||
<span>Settings</span>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/local"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="flex flex-col">
|
||||
<span>Offline</span>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="/logout"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
|
@ -129,7 +129,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="w-full">
|
||||
<a href="./progress" class="w-full">
|
||||
<div
|
||||
class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"
|
||||
>
|
||||
@ -142,7 +142,7 @@
|
||||
<p class="text-sm text-gray-400">Progress Records</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"
|
||||
|
@ -122,7 +122,7 @@
|
||||
</button>
|
||||
</form>
|
||||
<div class="pt-12 pb-12 text-center">
|
||||
{{ if .RegistrationEnabled }} {{ if .Register }}
|
||||
{{ if .Config.RegistrationEnabled }} {{ if .Register }}
|
||||
<p>
|
||||
Trying to login?
|
||||
<a href="./login" class="font-semibold underline">
|
||||
|
49
templates/pages/progress.html
Normal file
49
templates/pages/progress.html
Normal file
@ -0,0 +1,49 @@
|
||||
{{template "base" .}} {{define "title"}}Progress{{end}} {{define "header"}}
|
||||
<a href="./progress">Progress</a>
|
||||
{{end}} {{define "content"}}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead class="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
Document
|
||||
</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
Device
|
||||
</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
Percent
|
||||
</th>
|
||||
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
|
||||
Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-black dark:text-white">
|
||||
{{ if not .Data }}
|
||||
<tr>
|
||||
<td class="text-center p-3" colspan="4">No Results</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{range $progress := .Data }}
|
||||
<tr>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<a href="./documents/{{ $progress.DocumentID }}">{{ $progress.Author }} - {{ $progress.Title }}</p></a>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $progress.DeviceName }}</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $progress.Percentage }}%</p>
|
||||
</td>
|
||||
<td class="p-3 border-b border-gray-200">
|
||||
<p>{{ $progress.CreatedAt }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
@ -89,14 +89,7 @@
|
||||
>
|
||||
{{ range $item := GetUTCOffsets }}
|
||||
<option
|
||||
{{
|
||||
if
|
||||
(eq
|
||||
$item.Value
|
||||
$.Data.Settings.TimeOffset)
|
||||
}}selected{{
|
||||
end
|
||||
}}
|
||||
{{ if (eq $item.Value $.Data.TimeOffset) }}selected{{ end }}
|
||||
value="{{ $item.Value }}"
|
||||
>
|
||||
{{ $item.Name }}
|
||||
|
Loading…
Reference in New Issue
Block a user