diff --git a/api/api.go b/api/api.go index 78a773f..a5d4819 100644 --- a/api/api.go +++ b/api/api.go @@ -25,7 +25,7 @@ type API struct { func NewApi(db *database.DBManager, c *config.Config) *API { api := &API{ - HTMLPolicy: bluemonday.StripTagsPolicy(), + HTMLPolicy: bluemonday.StrictPolicy(), Router: gin.Default(), Config: c, DB: db, @@ -97,6 +97,8 @@ func (api *API) registerWebAppRoutes() { 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.DELETE("/documents/:document", api.authWebAppMiddleware, api.deleteDocument) + api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.createAppResourcesRoute("document-edit")) api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocumentFile) api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover) diff --git a/api/app-routes.go b/api/app-routes.go index 3670c97..21890b4 100644 --- a/api/app-routes.go +++ b/api/app-routes.go @@ -2,9 +2,11 @@ package api import ( "fmt" + "mime/multipart" "net/http" "os" "path/filepath" + "strings" "time" "github.com/gin-gonic/gin" @@ -13,6 +15,13 @@ import ( "reichard.io/bbank/metadata" ) +type requestDocumentEdit struct { + Title *string `form:"title"` + Author *string `form:"author"` + Description *string `form:"description"` + Cover *multipart.FileHeader `form:"cover"` +} + func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) { variables := gin.H{"RouteName": template} if len(args) > 0 { @@ -84,6 +93,46 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any } templateVars["Data"] = document + } else if routeName == "document-edit" { + var rDocID requestDocumentID + if err := c.ShouldBindUri(&rDocID); err != nil { + log.Error("[createAppResourcesRoute] Invalid URI Bind") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + var rDocEdit requestDocumentEdit + if err := c.ShouldBind(&rDocEdit); err != nil { + log.Error("[createAppResourcesRoute] Invalid Form Bind") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + if rDocEdit.Author == nil && rDocEdit.Title == nil && rDocEdit.Description == nil && rDocEdit.Cover == nil { + log.Error("[createAppResourcesRoute] Missing Form Values") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + // TODO - Handle Cover + if rDocEdit.Cover != nil { + + } + + // Update Document + if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{ + ID: rDocID.DocumentID, + Title: api.sanitizeInput(rDocEdit.Title), + Author: api.sanitizeInput(rDocEdit.Author), + Description: api.sanitizeInput(rDocEdit.Description), + }); err != nil { + log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + c.Redirect(http.StatusFound, "./") + return } else if routeName == "activity" { activityFilter := database.GetActivityParams{ UserID: rUser.(string), @@ -195,25 +244,27 @@ func (api *API) getDocumentCover(c *gin.Context) { var coverFilePath string // Identify Documents & Save Covers - bookMetadata := metadata.MetadataInfo{ + metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{ Title: document.Title, Author: document.Author, - } - err = metadata.GetMetadata(&bookMetadata) - if err == nil && bookMetadata.GBID != nil { + }) + + if err == nil && len(metadataResults) > 0 && metadataResults[0].GBID != nil { + firstResult := metadataResults[0] + // Derive & Sanitize File Name - fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *bookMetadata.GBID)) + fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *firstResult.GBID)) // Generate Storage Path coverFilePath = filepath.Join(api.Config.DataPath, "covers", fileName) - err := metadata.SaveCover(*bookMetadata.GBID, coverFilePath) + err := metadata.SaveCover(*firstResult.GBID, coverFilePath) if err == nil { - coverID = *bookMetadata.GBID - log.Info("Title:", *bookMetadata.Title) - log.Info("Author:", *bookMetadata.Author) - log.Info("Description:", *bookMetadata.Description) - log.Info("IDs:", bookMetadata.ISBN) + coverID = *firstResult.GBID + log.Info("Title:", *firstResult.Title) + log.Info("Author:", *firstResult.Author) + log.Info("Description:", *firstResult.Description) + log.Info("IDs:", firstResult.ISBN) } } @@ -241,3 +292,60 @@ func (api *API) getDocumentCover(c *gin.Context) { c.File(coverFilePath) } + +// DELETE /api/documents/:document +func (api *API) deleteDocument(c *gin.Context) { + var rDocID requestDocumentID + if err := c.ShouldBindUri(&rDocID); err != nil { + log.Error("[deleteDocument] Invalid URI Bind") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + // TODO +} + +// POST /api/documents/:document/identify +func (api *API) identifyDocument(c *gin.Context) { + var rDocID requestDocumentID + if err := c.ShouldBindUri(&rDocID); err != nil { + log.Error("[identifyDocument] Invalid URI Bind") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + isbn := strings.TrimSpace(c.PostForm("ISBN")) + if isbn == "" { + log.Error("[identifyDocument] Invalid Form") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"}) + return + } + + metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{ + ISBN: []*string{&isbn}, + }) + if err != nil || len(metadataResults) == 0 { + log.Error("[identifyDocument] Metadata Error") + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"}) + return + } + + // TODO + firstResult := metadataResults[0] + + if firstResult.Title != nil { + log.Info("Title:", *firstResult.Title) + } + if firstResult.Author != nil { + log.Info("Author:", *firstResult.Author) + } + if firstResult.Description != nil { + log.Info("Description:", *firstResult.Description) + } + + for _, val := range firstResult.ISBN { + log.Info("ISBN:", *val) + } + + c.Redirect(http.StatusFound, "/") +} diff --git a/api/ko-routes.go b/api/ko-routes.go index df16738..de29212 100644 --- a/api/ko-routes.go +++ b/api/ko-routes.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "fmt" + "html" "io" "net/http" "os" @@ -609,12 +610,12 @@ func (api *API) sanitizeInput(val any) *string { switch v := val.(type) { case *string: if v != nil { - newString := api.HTMLPolicy.Sanitize(string(*v)) + newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(*v))) return &newString } case string: if v != "" { - newString := api.HTMLPolicy.Sanitize(string(v)) + newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(v))) return &newString } } diff --git a/metadata/metadata.go b/metadata/metadata.go index e6f8b68..2251e38 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -46,48 +46,56 @@ const GBOOKS_QUERY_URL string = "https://www.googleapis.com/books/v1/volumes?q=% const GBOOKS_GBID_INFO_URL string = "https://www.googleapis.com/books/v1/volumes/%s" const GBOOKS_GBID_COVER_URL string = "https://books.google.com/books/content/images/frontcover/%s?fife=w480-h690" -func GetMetadata(data *MetadataInfo) error { - var queryResult *gBooksQueryItem - if data.GBID != nil { +func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) { + var queryResults []gBooksQueryItem + if metadataSearch.GBID != nil { // Use GBID - resp, err := performGBIDRequest(*data.GBID) + resp, err := performGBIDRequest(*metadataSearch.GBID) if err != nil { - return err + return nil, err } - queryResult = resp - } else if len(data.ISBN) > 0 { - searchQuery := "isbn:" + *data.ISBN[0] + queryResults = []gBooksQueryItem{*resp} + } else if len(metadataSearch.ISBN) > 0 { + searchQuery := "isbn:" + *metadataSearch.ISBN[0] resp, err := performSearchRequest(searchQuery) if err != nil { - return err + return nil, err } - queryResult = &resp.Items[0] - } else if data.Title != nil && data.Author != nil { - searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *data.Title, *data.Author)) + queryResults = resp.Items + } else if metadataSearch.Title != nil && metadataSearch.Author != nil { + searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *metadataSearch.Title, *metadataSearch.Author)) resp, err := performSearchRequest(searchQuery) if err != nil { - return err + return nil, err } - queryResult = &resp.Items[0] + queryResults = resp.Items } else { - return errors.New("Invalid Data") + return nil, errors.New("Invalid Data") } - // Merge Data - data.GBID = &queryResult.ID - data.Description = &queryResult.Info.Description - data.Title = &queryResult.Info.Title - if len(queryResult.Info.Authors) > 0 { - data.Author = &queryResult.Info.Authors[0] - } - for _, item := range queryResult.Info.Identifiers { - if item.Type == "ISBN_10" || item.Type == "ISBN_13" { - data.ISBN = append(data.ISBN, &item.Identifier) + // Normalize Data + allMetadata := []MetadataInfo{} + for _, item := range queryResults { + itemResult := MetadataInfo{ + GBID: &item.ID, + Title: &item.Info.Title, + Description: &item.Info.Description, } + if len(item.Info.Authors) > 0 { + itemResult.Author = &item.Info.Authors[0] + } + for _, item := range item.Info.Identifiers { + if item.Type == "ISBN_10" || item.Type == "ISBN_13" { + itemResult.ISBN = append(itemResult.ISBN, &item.Identifier) + } + + } + + allMetadata = append(allMetadata, itemResult) } - return nil + return allMetadata, nil } func SaveCover(id string, safePath string) error { diff --git a/templates/document-edit.html b/templates/document-edit.html new file mode 100644 index 0000000..68d9027 --- /dev/null +++ b/templates/document-edit.html @@ -0,0 +1,166 @@ +{{template "base.html" .}} + +{{define "title"}}Documents{{end}} + +{{define "header"}} +Documents +{{end}} + +{{define "content"}} +
Title
+ ++ {{ or .Data.Title "N/A" }} +
+Author
++ {{ or .Data.Author "N/A" }} +
+Progress
++ {{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%) +
+Time Read
++ {{ .Data.TotalTimeMinutes }} Minutes +
+Description
++ {{ or .Data.Description "N/A" }} +
+Title
- -- {{ or .Data.Title "N/A" }} -
+Title
++ {{ or .Data.Title "N/A" }} +
Author
-- {{ or .Data.Author "N/A" }} -
+Author
++ {{ or .Data.Author "N/A" }} +
Progress
-+
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
Minutes Read
-+
Time Read
+{{ .Data.TotalTimeMinutes }} Minutes
Description
-- {{ or .Data.Description "N/A" }} -
+ +Description
+{{ or .Data.Description "N/A" }}
+