Compare commits
3 Commits
5865fe3c13
...
75ed394f8d
Author | SHA1 | Date | |
---|---|---|---|
75ed394f8d | |||
803c187a00 | |||
da1baeb4cd |
15
.drone.yml
15
.drone.yml
@ -4,21 +4,10 @@ name: default
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Unit Tests
|
# Unit Tests
|
||||||
- name: unit test
|
- name: tests
|
||||||
image: golang
|
image: golang
|
||||||
commands:
|
commands:
|
||||||
- make tests_unit
|
- make tests
|
||||||
|
|
||||||
# Integration Tests (Every Month)
|
|
||||||
- name: integration test
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- make tests_integration
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- cron
|
|
||||||
cron:
|
|
||||||
- integration-test
|
|
||||||
|
|
||||||
# Fetch tags
|
# Fetch tags
|
||||||
- name: fetch tags
|
- name: fetch tags
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ TODO.md
|
|||||||
data/
|
data/
|
||||||
build/
|
build/
|
||||||
.direnv/
|
.direnv/
|
||||||
|
cover.html
|
||||||
|
9
Makefile
9
Makefile
@ -42,8 +42,7 @@ dev: build_tailwind
|
|||||||
clean:
|
clean:
|
||||||
rm -rf ./build
|
rm -rf ./build
|
||||||
|
|
||||||
tests_integration:
|
tests:
|
||||||
go test -v -tags=integration -coverpkg=./... ./metadata
|
SET_TEST=set_val go test -coverpkg=./... ./... -coverprofile=./cover.out
|
||||||
|
go tool cover -html=./cover.out -o ./cover.html
|
||||||
tests_unit:
|
rm ./cover.out
|
||||||
SET_TEST=set_val go test -v -coverpkg=./... ./...
|
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/itchyny/gojq"
|
"github.com/itchyny/gojq"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"reichard.io/antholume/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adminAction string
|
type adminAction string
|
||||||
@ -63,7 +64,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
var rAdminAction requestAdminAction
|
var rAdminAction requestAdminAction
|
||||||
if err := c.ShouldBind(&rAdminAction); err != nil {
|
if err := c.ShouldBind(&rAdminAction); err != nil {
|
||||||
log.Error("Invalid Form Bind: ", err)
|
log.Error("Invalid Form Bind: ", err)
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
// 2. Select all / deselect?
|
// 2. Select all / deselect?
|
||||||
case adminCacheTables:
|
case adminCacheTables:
|
||||||
go api.db.CacheTempTables()
|
go api.db.CacheTempTables()
|
||||||
|
// TODO - Message
|
||||||
case adminRestore:
|
case adminRestore:
|
||||||
api.processRestoreFile(rAdminAction, c)
|
api.processRestoreFile(rAdminAction, c)
|
||||||
return
|
return
|
||||||
@ -83,7 +85,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to vacuum DB: ", err)
|
log.Error("Unable to vacuum DB: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +128,7 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
var rAdminLogs requestAdminLogs
|
var rAdminLogs requestAdminLogs
|
||||||
if err := c.ShouldBindQuery(&rAdminLogs); err != nil {
|
if err := c.ShouldBindQuery(&rAdminLogs); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid URI parameters.")
|
appErrorPage(c, http.StatusNotFound, "Invalid URI parameters")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)
|
rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)
|
||||||
@ -136,14 +138,14 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
parsed, err := gojq.Parse(rAdminLogs.Filter)
|
parsed, err := gojq.Parse(rAdminLogs.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to parse JQ filter")
|
log.Error("Unable to parse JQ filter")
|
||||||
appErrorPage(c, http.StatusNotFound, "Unable to parse JQ filter.")
|
appErrorPage(c, http.StatusNotFound, "Unable to parse JQ filter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jqFilter, err = gojq.Compile(parsed)
|
jqFilter, err = gojq.Compile(parsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to compile JQ filter")
|
log.Error("Unable to compile JQ filter")
|
||||||
appErrorPage(c, http.StatusNotFound, "Unable to compile JQ filter.")
|
appErrorPage(c, http.StatusNotFound, "Unable to compile JQ filter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,7 +154,7 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
|
|||||||
logPath := filepath.Join(api.cfg.ConfigPath, "logs/antholume.log")
|
logPath := filepath.Join(api.cfg.ConfigPath, "logs/antholume.log")
|
||||||
logFile, err := os.Open(logPath)
|
logFile, err := os.Open(logPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
appErrorPage(c, http.StatusBadRequest, "Missing AnthoLume log file.")
|
appErrorPage(c, http.StatusBadRequest, "Missing AnthoLume log file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer logFile.Close()
|
defer logFile.Close()
|
||||||
@ -229,7 +231,7 @@ func (api *API) appGetAdminImport(c *gin.Context) {
|
|||||||
var rImportFolder requestAdminImport
|
var rImportFolder requestAdminImport
|
||||||
if err := c.ShouldBindQuery(&rImportFolder); err != nil {
|
if err := c.ShouldBindQuery(&rImportFolder); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid directory.")
|
appErrorPage(c, http.StatusNotFound, "Invalid directory")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +246,7 @@ func (api *API) appGetAdminImport(c *gin.Context) {
|
|||||||
dPath, err := filepath.Abs(api.cfg.DataPath)
|
dPath, err := filepath.Abs(api.cfg.DataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Absolute filepath error: ", rImportFolder.Directory)
|
log.Error("Absolute filepath error: ", rImportFolder.Directory)
|
||||||
appErrorPage(c, http.StatusNotFound, "Unable to get data directory absolute path.")
|
appErrorPage(c, http.StatusNotFound, "Unable to get data directory absolute path")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +256,7 @@ func (api *API) appGetAdminImport(c *gin.Context) {
|
|||||||
entries, err := os.ReadDir(rImportFolder.Directory)
|
entries, err := os.ReadDir(rImportFolder.Directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Invalid directory: ", rImportFolder.Directory)
|
log.Error("Invalid directory: ", rImportFolder.Directory)
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid directory.")
|
appErrorPage(c, http.StatusNotFound, "Invalid directory")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,13 +281,46 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
|||||||
var rAdminImport requestAdminImport
|
var rAdminImport requestAdminImport
|
||||||
if err := c.ShouldBind(&rAdminImport); err != nil {
|
if err := c.ShouldBind(&rAdminImport); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid directory.")
|
appErrorPage(c, http.StatusNotFound, "Invalid directory")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO - Store results for approval?
|
||||||
|
|
||||||
fmt.Println(rAdminImport)
|
// Walk import directory & copy or import files
|
||||||
|
importDirectory := filepath.Clean(rAdminImport.Directory)
|
||||||
|
_ = filepath.WalkDir(importDirectory, func(currentPath string, f fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if f.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metadata
|
||||||
|
fileMeta, err := metadata.GetMetadata(currentPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("metadata error: %v\n", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only needed if copying
|
||||||
|
newName := deriveBaseFileName(fileMeta)
|
||||||
|
|
||||||
|
// Open File on Disk
|
||||||
|
// file, err := os.Open(currentPath)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer file.Close()
|
||||||
|
|
||||||
|
// TODO - BasePath in DB
|
||||||
|
// TODO - Copy / Import
|
||||||
|
|
||||||
|
fmt.Printf("New File Metadata: %s\n", newName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
templateVars["CurrentPath"] = filepath.Clean(rAdminImport.Directory)
|
templateVars["CurrentPath"] = filepath.Clean(rAdminImport.Directory)
|
||||||
|
|
||||||
@ -297,14 +332,14 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
uploadedFile, err := rAdminAction.RestoreFile.Open()
|
uploadedFile, err := rAdminAction.RestoreFile.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error: ", err)
|
log.Error("File Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to open file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("MIME Error")
|
log.Error("MIME Error")
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileExtension := fileMime.Extension()
|
fileExtension := fileMime.Extension()
|
||||||
@ -312,7 +347,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
// Validate Extension
|
// Validate Extension
|
||||||
if !slices.Contains([]string{".zip"}, fileExtension) {
|
if !slices.Contains([]string{".zip"}, fileExtension) {
|
||||||
log.Error("Invalid FileType: ", fileExtension)
|
log.Error("Invalid FileType: ", fileExtension)
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid filetype")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,7 +355,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
tempFile, err := os.CreateTemp("", "restore")
|
tempFile, err := os.CreateTemp("", "restore")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Temp File Create Error: ", err)
|
log.Warn("Temp File Create Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to create temp file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to create temp file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer os.Remove(tempFile.Name())
|
defer os.Remove(tempFile.Name())
|
||||||
@ -330,7 +365,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
err = c.SaveUploadedFile(rAdminAction.RestoreFile, tempFile.Name())
|
err = c.SaveUploadedFile(rAdminAction.RestoreFile, tempFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error: ", err)
|
log.Error("File Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to save file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,7 +373,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
fileInfo, err := tempFile.Stat()
|
fileInfo, err := tempFile.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error: ", err)
|
log.Error("File Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to read file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to read file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,7 +381,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
zipReader, err := zip.NewReader(tempFile, fileInfo.Size())
|
zipReader, err := zip.NewReader(tempFile, fileInfo.Size())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("ZIP Error: ", err)
|
log.Error("ZIP Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to read zip.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to read zip")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +415,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
backupFile, err := os.Create(backupFilePath)
|
backupFile, err := os.Create(backupFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to create backup file: ", err)
|
log.Error("Unable to create backup file: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to create backup file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to create backup file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer backupFile.Close()
|
defer backupFile.Close()
|
||||||
@ -389,7 +424,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
_, err = api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
_, err = api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to vacuum DB: ", err)
|
log.Error("Unable to vacuum DB: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,7 +433,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
err = api.createBackup(w, []string{"covers", "documents"})
|
err = api.createBackup(w, []string{"covers", "documents"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to save backup file: ", err)
|
log.Error("Unable to save backup file: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save backup file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to save backup file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,26 +441,26 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
err = api.removeData()
|
err = api.removeData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to delete data: ", err)
|
log.Error("Unable to delete data: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to delete data.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to delete data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore Data
|
// Restore Data
|
||||||
err = api.restoreData(zipReader)
|
err = api.restoreData(zipReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to restore data.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to restore data")
|
||||||
log.Panic("Unable to restore data: ", err)
|
log.Panic("Unable to restore data: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinit DB
|
// Reinit DB
|
||||||
if err := api.db.Reload(); err != nil {
|
if err := api.db.Reload(); err != nil {
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to reload DB.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to reload DB")
|
||||||
log.Panicf("Unable to reload DB: %v", err)
|
log.Panicf("Unable to reload DB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotate Auth Hashes
|
// Rotate Auth Hashes
|
||||||
if err := api.rotateAllAuthHashes(); err != nil {
|
if err := api.rotateAllAuthHashes(); err != nil {
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to rotate hashes.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to rotate hashes")
|
||||||
log.Panicf("Unable to rotate auth hashes: %v", err)
|
log.Panicf("Unable to rotate auth hashes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,6 +468,7 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
|||||||
c.Redirect(http.StatusFound, "/login")
|
c.Redirect(http.StatusFound, "/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore all data
|
||||||
func (api *API) restoreData(zipReader *zip.Reader) error {
|
func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||||
// Ensure Directories
|
// Ensure Directories
|
||||||
api.cfg.EnsureDirectories()
|
api.cfg.EnsureDirectories()
|
||||||
@ -463,6 +499,7 @@ func (api *API) restoreData(zipReader *zip.Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove all data
|
||||||
func (api *API) removeData() error {
|
func (api *API) removeData() error {
|
||||||
allPaths := []string{
|
allPaths := []string{
|
||||||
"covers",
|
"covers",
|
||||||
@ -485,6 +522,7 @@ func (api *API) removeData() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backup all data
|
||||||
func (api *API) createBackup(w io.Writer, directories []string) error {
|
func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||||
ar := zip.NewWriter(w)
|
ar := zip.NewWriter(w)
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ func (api *API) appGetDocument(c *gin.Context) {
|
|||||||
var rDocID requestDocumentID
|
var rDocID requestDocumentID
|
||||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid document.")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,7 +361,7 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
|
|||||||
var rDoc requestDocumentID
|
var rDoc requestDocumentID
|
||||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid document.")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,7 +417,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
|||||||
var rDocUpload requestDocumentUpload
|
var rDocUpload requestDocumentUpload
|
||||||
if err := c.ShouldBind(&rDocUpload); err != nil {
|
if err := c.ShouldBind(&rDocUpload); err != nil {
|
||||||
log.Error("Invalid Form Bind")
|
log.Error("Invalid Form Bind")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,153 +426,92 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Type & Derive Extension on MIME
|
|
||||||
uploadedFile, err := rDocUpload.DocumentFile.Open()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("File Error: ", err)
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("MIME Error")
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileExtension := fileMime.Extension()
|
|
||||||
|
|
||||||
// Validate Extension
|
|
||||||
if !slices.Contains([]string{".epub"}, fileExtension) {
|
|
||||||
log.Error("Invalid FileType: ", fileExtension)
|
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Temp File
|
// Create Temp File
|
||||||
tempFile, err := os.CreateTemp("", "book")
|
tempFile, err := os.CreateTemp("", "book")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Temp File Create Error: ", err)
|
log.Warn("Temp File Create Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to create temp file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to create temp file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer os.Remove(tempFile.Name())
|
defer os.Remove(tempFile.Name())
|
||||||
defer tempFile.Close()
|
defer tempFile.Close()
|
||||||
|
|
||||||
// Save Temp
|
// Save Temp File
|
||||||
err = c.SaveUploadedFile(rDocUpload.DocumentFile, tempFile.Name())
|
err = c.SaveUploadedFile(rDocUpload.DocumentFile, tempFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error: ", err)
|
log.Error("File Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to save file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Metadata
|
// Get Metadata
|
||||||
metadataInfo, err := metadata.GetMetadata(tempFile.Name())
|
metadataInfo, err := metadata.GetMetadata(tempFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("GetMetadata Error: ", err)
|
log.Errorf("unable to acquire metadata: %v", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to acquire file metadata.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to acquire metadata")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate Partial MD5 ID
|
// Check Already Exists
|
||||||
partialMD5, err := utils.CalculatePartialMD5(tempFile.Name())
|
_, err = api.db.Queries.GetDocument(api.db.Ctx, *metadataInfo.PartialMD5)
|
||||||
if err != nil {
|
|
||||||
log.Warn("Partial MD5 Error: ", err)
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to calculate partial MD5.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Exists
|
|
||||||
_, err = api.db.Queries.GetDocument(api.db.Ctx, partialMD5)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
|
log.Warnf("document already exists: %s", *metadataInfo.PartialMD5)
|
||||||
return
|
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", *metadataInfo.PartialMD5))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate Actual MD5
|
|
||||||
fileHash, err := getFileMD5(tempFile.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("MD5 Hash Failure: ", err)
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to calculate MD5.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Word Count
|
|
||||||
wordCount, err := metadata.GetWordCount(tempFile.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Word Count Failure: ", err)
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to calculate word count.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive Filename
|
|
||||||
var fileName string
|
|
||||||
if *metadataInfo.Author != "" {
|
|
||||||
fileName = fileName + *metadataInfo.Author
|
|
||||||
} else {
|
|
||||||
fileName = fileName + "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
if *metadataInfo.Title != "" {
|
|
||||||
fileName = fileName + " - " + *metadataInfo.Title
|
|
||||||
} else {
|
|
||||||
fileName = fileName + " - Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove Slashes
|
|
||||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
|
||||||
|
|
||||||
// Derive & Sanitize File Name
|
// Derive & Sanitize File Name
|
||||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, partialMD5, fileExtension))
|
fileName := deriveBaseFileName(metadataInfo)
|
||||||
|
|
||||||
// Generate Storage Path & Open File
|
|
||||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||||
|
|
||||||
|
// Open Destination File
|
||||||
destFile, err := os.Create(safePath)
|
destFile, err := os.Create(safePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Dest File Error: ", err)
|
log.Errorf("unable to open destination file: %v", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to open destination file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer destFile.Close()
|
defer destFile.Close()
|
||||||
|
|
||||||
// Copy File
|
// Copy File
|
||||||
if _, err = io.Copy(destFile, tempFile); err != nil {
|
if _, err = io.Copy(destFile, tempFile); err != nil {
|
||||||
log.Error("Copy Temp File Error: ", err)
|
log.Errorf("unable to save file: %v", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to save file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: partialMD5,
|
ID: *metadataInfo.PartialMD5,
|
||||||
Title: metadataInfo.Title,
|
Title: metadataInfo.Title,
|
||||||
Author: metadataInfo.Author,
|
Author: metadataInfo.Author,
|
||||||
Description: metadataInfo.Description,
|
Description: metadataInfo.Description,
|
||||||
Words: &wordCount,
|
Md5: metadataInfo.MD5,
|
||||||
Md5: fileHash,
|
Words: metadataInfo.WordCount,
|
||||||
Filepath: &fileName,
|
Filepath: &fileName,
|
||||||
|
|
||||||
|
// TODO (BasePath):
|
||||||
|
// - Should be current config directory
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("UpsertDocument DB Error: ", err)
|
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
|
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", *metadataInfo.PartialMD5))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) appEditDocument(c *gin.Context) {
|
func (api *API) appEditDocument(c *gin.Context) {
|
||||||
var rDocID requestDocumentID
|
var rDocID requestDocumentID
|
||||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid document.")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var rDocEdit requestDocumentEdit
|
var rDocEdit requestDocumentEdit
|
||||||
if err := c.ShouldBind(&rDocEdit); err != nil {
|
if err := c.ShouldBind(&rDocEdit); err != nil {
|
||||||
log.Error("Invalid Form Bind")
|
log.Error("Invalid Form Bind")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,7 +525,7 @@ func (api *API) appEditDocument(c *gin.Context) {
|
|||||||
rDocEdit.CoverGBID == nil &&
|
rDocEdit.CoverGBID == nil &&
|
||||||
rDocEdit.CoverFile == nil {
|
rDocEdit.CoverFile == nil {
|
||||||
log.Error("Missing Form Values")
|
log.Error("Missing Form Values")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,14 +539,14 @@ func (api *API) appEditDocument(c *gin.Context) {
|
|||||||
uploadedFile, err := rDocEdit.CoverFile.Open()
|
uploadedFile, err := rDocEdit.CoverFile.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error")
|
log.Error("File Error")
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to open file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to open file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("MIME Error")
|
log.Error("MIME Error")
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileExtension := fileMime.Extension()
|
fileExtension := fileMime.Extension()
|
||||||
@ -615,7 +554,7 @@ func (api *API) appEditDocument(c *gin.Context) {
|
|||||||
// Validate Extension
|
// Validate Extension
|
||||||
if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
|
if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
|
||||||
log.Error("Invalid FileType: ", fileExtension)
|
log.Error("Invalid FileType: ", fileExtension)
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid filetype.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid filetype")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -627,7 +566,7 @@ func (api *API) appEditDocument(c *gin.Context) {
|
|||||||
err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
|
err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error: ", err)
|
log.Error("File Error: ", err)
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
appErrorPage(c, http.StatusInternalServerError, "Unable to save file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,7 +602,7 @@ func (api *API) appDeleteDocument(c *gin.Context) {
|
|||||||
var rDocID requestDocumentID
|
var rDocID requestDocumentID
|
||||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid document.")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
changed, err := api.db.Queries.DeleteDocument(api.db.Ctx, rDocID.DocumentID)
|
changed, err := api.db.Queries.DeleteDocument(api.db.Ctx, rDocID.DocumentID)
|
||||||
@ -674,7 +613,7 @@ func (api *API) appDeleteDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if changed == 0 {
|
if changed == 0 {
|
||||||
log.Error("DeleteDocument DB Error")
|
log.Error("DeleteDocument DB Error")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid document.")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,14 +624,14 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
|
|||||||
var rDocID requestDocumentID
|
var rDocID requestDocumentID
|
||||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||||
log.Error("Invalid URI Bind")
|
log.Error("Invalid URI Bind")
|
||||||
appErrorPage(c, http.StatusNotFound, "Invalid document.")
|
appErrorPage(c, http.StatusNotFound, "Invalid document")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var rDocIdentify requestDocumentIdentify
|
var rDocIdentify requestDocumentIdentify
|
||||||
if err := c.ShouldBind(&rDocIdentify); err != nil {
|
if err := c.ShouldBind(&rDocIdentify); err != nil {
|
||||||
log.Error("Invalid Form Bind")
|
log.Error("Invalid Form Bind")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,7 +649,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
|
|||||||
// Validate Values
|
// Validate Values
|
||||||
if rDocIdentify.ISBN == nil && rDocIdentify.Title == nil && rDocIdentify.Author == nil {
|
if rDocIdentify.ISBN == nil && rDocIdentify.Title == nil && rDocIdentify.Author == nil {
|
||||||
log.Error("Invalid Form")
|
log.Error("Invalid Form")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,7 +657,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
|
|||||||
templateVars, auth := api.getBaseTemplateVars("document", c)
|
templateVars, auth := api.getBaseTemplateVars("document", c)
|
||||||
|
|
||||||
// Get Metadata
|
// Get Metadata
|
||||||
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
|
metadataResults, err := metadata.SearchMetadata(metadata.SOURCE_GBOOK, metadata.MetadataInfo{
|
||||||
Title: rDocIdentify.Title,
|
Title: rDocIdentify.Title,
|
||||||
Author: rDocIdentify.Author,
|
Author: rDocIdentify.Author,
|
||||||
ISBN10: rDocIdentify.ISBN,
|
ISBN10: rDocIdentify.ISBN,
|
||||||
@ -767,7 +706,7 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
var rDocAdd requestDocumentAdd
|
var rDocAdd requestDocumentAdd
|
||||||
if err := c.ShouldBind(&rDocAdd); err != nil {
|
if err := c.ShouldBind(&rDocAdd); err != nil {
|
||||||
log.Error("Invalid Form Bind")
|
log.Error("Invalid Form Bind")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -845,7 +784,7 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
fileName = strings.ReplaceAll(fileName, "/", "")
|
||||||
|
|
||||||
// Derive & Sanitize File Name
|
// Derive & Sanitize File Name
|
||||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, partialMD5, fileExtension))
|
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *partialMD5, fileExtension))
|
||||||
|
|
||||||
// Open Source File
|
// Open Source File
|
||||||
sourceFile, err := os.Open(tempFilePath)
|
sourceFile, err := os.Open(tempFilePath)
|
||||||
@ -901,12 +840,12 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: partialMD5,
|
ID: *partialMD5,
|
||||||
Title: rDocAdd.Title,
|
Title: rDocAdd.Title,
|
||||||
Author: rDocAdd.Author,
|
Author: rDocAdd.Author,
|
||||||
Md5: fileHash,
|
Md5: fileHash,
|
||||||
Filepath: &fileName,
|
Filepath: &fileName,
|
||||||
Words: &wordCount,
|
Words: wordCount,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("UpsertDocument DB Error: ", err)
|
log.Error("UpsertDocument DB Error: ", err)
|
||||||
sendDownloadMessage("Unable to save to database", gin.H{"Error": true})
|
sendDownloadMessage("Unable to save to database", gin.H{"Error": true})
|
||||||
@ -917,7 +856,7 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
|||||||
sendDownloadMessage("Download Success", gin.H{
|
sendDownloadMessage("Download Success", gin.H{
|
||||||
"Progress": 100,
|
"Progress": 100,
|
||||||
"ButtonText": "Go to Book",
|
"ButtonText": "Go to Book",
|
||||||
"ButtonHref": fmt.Sprintf("./documents/%s", partialMD5),
|
"ButtonHref": fmt.Sprintf("./documents/%s", *partialMD5),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -925,14 +864,14 @@ func (api *API) appEditSettings(c *gin.Context) {
|
|||||||
var rUserSettings requestSettingsEdit
|
var rUserSettings requestSettingsEdit
|
||||||
if err := c.ShouldBind(&rUserSettings); err != nil {
|
if err := c.ShouldBind(&rUserSettings); err != nil {
|
||||||
log.Error("Invalid Form Bind")
|
log.Error("Invalid Form Bind")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Something Exists
|
// Validate Something Exists
|
||||||
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
|
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
|
||||||
log.Error("Missing Form Values")
|
log.Error("Missing Form Values")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1023,7 +962,7 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats
|
|||||||
} else {
|
} else {
|
||||||
if _, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
if _, err := qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Words: &wordCount,
|
Words: wordCount,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("UpsertDocument DB Error: ", err)
|
log.Error("UpsertDocument DB Error: ", err)
|
||||||
return err
|
return err
|
||||||
|
@ -95,7 +95,7 @@ func (api *API) createGetCoverHandler(errorFunc func(*gin.Context, int, string))
|
|||||||
var coverFile string = "UNKNOWN"
|
var coverFile string = "UNKNOWN"
|
||||||
|
|
||||||
// Identify Documents & Save Covers
|
// Identify Documents & Save Covers
|
||||||
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
|
metadataResults, err := metadata.SearchMetadata(metadata.SOURCE_GBOOK, metadata.MetadataInfo{
|
||||||
Title: document.Title,
|
Title: document.Title,
|
||||||
Author: document.Author,
|
Author: document.Author,
|
||||||
})
|
})
|
||||||
|
@ -10,13 +10,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gabriel-vasile/mimetype"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
"reichard.io/antholume/metadata"
|
"reichard.io/antholume/metadata"
|
||||||
)
|
)
|
||||||
@ -456,21 +453,11 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open Form File
|
||||||
fileData, err := c.FormFile("file")
|
fileData, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Error:", err)
|
log.Error("File Error:", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "File Error")
|
apiErrorPage(c, http.StatusBadRequest, "File error")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Type & Derive Extension on MIME
|
|
||||||
uploadedFile, err := fileData.Open()
|
|
||||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
|
||||||
fileExtension := fileMime.Extension()
|
|
||||||
|
|
||||||
if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
|
|
||||||
log.Error("Invalid FileType:", fileExtension)
|
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Filetype")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,25 +469,29 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open File
|
||||||
|
uploadedFile, err := fileData.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to open file")
|
||||||
|
apiErrorPage(c, http.StatusBadRequest, "Unable to open file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Support
|
||||||
|
docType, err := metadata.GetDocumentTypeReader(uploadedFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unsupported file")
|
||||||
|
apiErrorPage(c, http.StatusBadRequest, "Unsupported file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Derive Filename
|
// Derive Filename
|
||||||
var fileName string
|
fileName := deriveBaseFileName(&metadata.MetadataInfo{
|
||||||
if document.Author != nil {
|
Type: *docType,
|
||||||
fileName = fileName + *document.Author
|
PartialMD5: &document.ID,
|
||||||
} else {
|
Title: document.Title,
|
||||||
fileName = fileName + "Unknown"
|
Author: document.Author,
|
||||||
}
|
})
|
||||||
|
|
||||||
if document.Title != nil {
|
|
||||||
fileName = fileName + " - " + *document.Title
|
|
||||||
} else {
|
|
||||||
fileName = fileName + " - Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove Slashes
|
|
||||||
fileName = strings.ReplaceAll(fileName, "/", "")
|
|
||||||
|
|
||||||
// Derive & Sanitize File Name
|
|
||||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, document.ID, fileExtension))
|
|
||||||
|
|
||||||
// Generate Storage Path
|
// Generate Storage Path
|
||||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||||
@ -516,28 +507,20 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get MD5 Hash
|
// Acquire Metadata
|
||||||
fileHash, err := getFileMD5(safePath)
|
metadataInfo, err := metadata.GetMetadata(safePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Hash Failure:", err)
|
log.Errorf("Unable to acquire metadata: %v", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "File Error")
|
apiErrorPage(c, http.StatusBadRequest, "Unable to acquire metadata")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Word Count
|
|
||||||
wordCount, err := metadata.GetWordCount(safePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Word Count Failure:", err)
|
|
||||||
apiErrorPage(c, http.StatusBadRequest, "File Error")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert Document
|
// Upsert Document
|
||||||
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
if _, err = api.db.Queries.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||||
ID: document.ID,
|
ID: document.ID,
|
||||||
Md5: fileHash,
|
Md5: metadataInfo.MD5,
|
||||||
|
Words: metadataInfo.WordCount,
|
||||||
Filepath: &fileName,
|
Filepath: &fileName,
|
||||||
Words: &wordCount,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error("UpsertDocument DB Error:", err)
|
log.Error("UpsertDocument DB Error:", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Document Error")
|
apiErrorPage(c, http.StatusBadRequest, "Document Error")
|
||||||
|
22
api/utils.go
22
api/utils.go
@ -4,10 +4,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
"reichard.io/antholume/graph"
|
"reichard.io/antholume/graph"
|
||||||
|
"reichard.io/antholume/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UTCOffset struct {
|
type UTCOffset struct {
|
||||||
@ -144,3 +147,22 @@ func fields(value interface{}) (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
||||||
|
// Derive New FileName
|
||||||
|
var newFileName string
|
||||||
|
if *metadataInfo.Author != "" {
|
||||||
|
newFileName = newFileName + *metadataInfo.Author
|
||||||
|
} else {
|
||||||
|
newFileName = newFileName + "Unknown"
|
||||||
|
}
|
||||||
|
if *metadataInfo.Title != "" {
|
||||||
|
newFileName = newFileName + " - " + *metadataInfo.Title
|
||||||
|
} else {
|
||||||
|
newFileName = newFileName + " - Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Slashes
|
||||||
|
fileName := strings.ReplaceAll(newFileName, "/", "")
|
||||||
|
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
|
||||||
|
}
|
||||||
|
@ -1,12 +1,35 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
func TestNiceSeconds(t *testing.T) {
|
func TestNiceSeconds(t *testing.T) {
|
||||||
want := "22d 7h 39m 31s"
|
wantOne := "22d 7h 39m 31s"
|
||||||
nice := niceSeconds(1928371)
|
wantNA := "N/A"
|
||||||
|
|
||||||
if nice != want {
|
niceOne := niceSeconds(1928371)
|
||||||
t.Fatalf(`Expected: %v, Got: %v`, want, nice)
|
niceNA := niceSeconds(0)
|
||||||
}
|
|
||||||
|
assert.Equal(t, wantOne, niceOne, "should be nice seconds")
|
||||||
|
assert.Equal(t, wantNA, niceNA, "should be nice NA")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiceNumbers(t *testing.T) {
|
||||||
|
wantMillions := "198M"
|
||||||
|
wantThousands := "19.8k"
|
||||||
|
wantThousandsTwo := "1.98k"
|
||||||
|
wantZero := "0"
|
||||||
|
|
||||||
|
niceMillions := niceNumbers(198236461)
|
||||||
|
niceThousands := niceNumbers(19823)
|
||||||
|
niceThousandsTwo := niceNumbers(1984)
|
||||||
|
niceZero := niceNumbers(0)
|
||||||
|
|
||||||
|
assert.Equal(t, wantMillions, niceMillions, "should be nice millions")
|
||||||
|
assert.Equal(t, wantThousands, niceThousands, "should be nice thousands")
|
||||||
|
assert.Equal(t, wantThousandsTwo, niceThousandsTwo, "should be nice thousands")
|
||||||
|
assert.Equal(t, wantZero, niceZero, "should be nice zero")
|
||||||
}
|
}
|
||||||
|
2
assets/lib/epub.min.js
vendored
2
assets/lib/epub.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
@ -97,16 +97,18 @@ class EBookReader {
|
|||||||
flow: "paginated",
|
flow: "paginated",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
allowScriptedContent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup Reader
|
// Setup Reader
|
||||||
this.book.ready.then(this.setupReader.bind(this));
|
this.book.ready.then(this.setupReader.bind(this));
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
|
this.initCSP();
|
||||||
this.initDevice();
|
this.initDevice();
|
||||||
this.initWakeLock();
|
this.initWakeLock();
|
||||||
this.initThemes();
|
this.initThemes();
|
||||||
this.initRenditionListeners();
|
this.initViewerListeners();
|
||||||
this.initDocumentListeners();
|
this.initDocumentListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +281,36 @@ class EBookReader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EpubJS will set iframe sandbox when settings "allowScriptedContent: false".
|
||||||
|
* However, Safari completely blocks us from attaching listeners to the iframe
|
||||||
|
* document. So instead we just inject a restrictive CSP rule.
|
||||||
|
*
|
||||||
|
* This effectively blocks all script content within the iframe while still
|
||||||
|
* allowing us to attach listeners to the iframe document.
|
||||||
|
**/
|
||||||
|
initCSP() {
|
||||||
|
// Derive CSP Host
|
||||||
|
var protocol = document.location.protocol;
|
||||||
|
var host = document.location.host;
|
||||||
|
var cspURL = `${protocol}//${host}`;
|
||||||
|
|
||||||
|
// Add CSP Policy
|
||||||
|
this.book.spine.hooks.content.register((output, section) => {
|
||||||
|
let cspWrapper = document.createElement("div");
|
||||||
|
cspWrapper.innerHTML = `
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="require-trusted-types-for 'script';
|
||||||
|
style-src 'self' blob: 'unsafe-inline' ${cspURL};
|
||||||
|
object-src 'none';
|
||||||
|
script-src 'none';"
|
||||||
|
>`;
|
||||||
|
let cspMeta = cspWrapper.children[0];
|
||||||
|
output.head.append(cspMeta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set theme & meta theme color
|
* Set theme & meta theme color
|
||||||
**/
|
**/
|
||||||
@ -371,9 +403,9 @@ class EBookReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendition hooks
|
* Viewer Listeners
|
||||||
**/
|
**/
|
||||||
initRenditionListeners() {
|
initViewerListeners() {
|
||||||
/**
|
/**
|
||||||
* Initiate the debounce when the given function returns true.
|
* Initiate the debounce when the given function returns true.
|
||||||
* Don't run it again until the timeout lapses.
|
* Don't run it again until the timeout lapses.
|
||||||
@ -401,56 +433,17 @@ class EBookReader {
|
|||||||
let bottomBar = document.querySelector("#bottom-bar");
|
let bottomBar = document.querySelector("#bottom-bar");
|
||||||
|
|
||||||
// Local Functions
|
// Local Functions
|
||||||
let getCFIFromXPath = this.getCFIFromXPath.bind(this);
|
|
||||||
let setPosition = this.setPosition.bind(this);
|
|
||||||
let nextPage = this.nextPage.bind(this);
|
let nextPage = this.nextPage.bind(this);
|
||||||
let prevPage = this.prevPage.bind(this);
|
let prevPage = this.prevPage.bind(this);
|
||||||
let saveSettings = this.saveSettings.bind(this);
|
|
||||||
|
|
||||||
// Local Vars
|
|
||||||
let readerSettings = this.readerSettings;
|
|
||||||
let bookState = this.bookState;
|
|
||||||
|
|
||||||
this.rendition.hooks.render.register(function (doc, data) {
|
|
||||||
let renderDoc = doc.document;
|
|
||||||
|
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
// ---------------- Wake Lock Hack ---------------- //
|
// ----------------- Swipe Helpers ---------------- //
|
||||||
// ------------------------------------------------ //
|
|
||||||
let wakeLockListener = function () {
|
|
||||||
doc.window.parent.document.dispatchEvent(new CustomEvent("wakelock"));
|
|
||||||
};
|
|
||||||
renderDoc.addEventListener("click", wakeLockListener);
|
|
||||||
renderDoc.addEventListener("gesturechange", wakeLockListener);
|
|
||||||
renderDoc.addEventListener("touchstart", wakeLockListener);
|
|
||||||
|
|
||||||
// ------------------------------------------------ //
|
|
||||||
// --------------- Swipe Pagination --------------- //
|
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
let touchStartX,
|
let touchStartX,
|
||||||
touchStartY,
|
touchStartY,
|
||||||
touchEndX,
|
touchEndX,
|
||||||
touchEndY = undefined;
|
touchEndY = undefined;
|
||||||
|
|
||||||
renderDoc.addEventListener(
|
|
||||||
"touchstart",
|
|
||||||
function (event) {
|
|
||||||
touchStartX = event.changedTouches[0].screenX;
|
|
||||||
touchStartY = event.changedTouches[0].screenY;
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
renderDoc.addEventListener(
|
|
||||||
"touchend",
|
|
||||||
function (event) {
|
|
||||||
touchEndX = event.changedTouches[0].screenX;
|
|
||||||
touchEndY = event.changedTouches[0].screenY;
|
|
||||||
handleGesture(event);
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleGesture(event) {
|
function handleGesture(event) {
|
||||||
let drasticity = 75;
|
let drasticity = 75;
|
||||||
|
|
||||||
@ -476,8 +469,32 @@ class EBookReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSwipeDown() {
|
||||||
|
if (bottomBar.classList.contains("bottom-0"))
|
||||||
|
bottomBar.classList.remove("bottom-0");
|
||||||
|
else topBar.classList.add("top-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSwipeUp() {
|
||||||
|
if (topBar.classList.contains("top-0")) topBar.classList.remove("top-0");
|
||||||
|
else bottomBar.classList.add("bottom-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rendition.hooks.render.register(function (doc, data) {
|
||||||
|
let renderDoc = doc.document;
|
||||||
|
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
// --------------- Bottom & Top Bar --------------- //
|
// ---------------- Wake Lock Hack ---------------- //
|
||||||
|
// ------------------------------------------------ //
|
||||||
|
let wakeLockListener = function () {
|
||||||
|
renderDoc.dispatchEvent(new CustomEvent("wakelock"));
|
||||||
|
};
|
||||||
|
renderDoc.addEventListener("click", wakeLockListener);
|
||||||
|
renderDoc.addEventListener("gesturechange", wakeLockListener);
|
||||||
|
renderDoc.addEventListener("touchstart", wakeLockListener);
|
||||||
|
|
||||||
|
// ------------------------------------------------ //
|
||||||
|
// --------------- Bars & Page Turn --------------- //
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
renderDoc.addEventListener(
|
renderDoc.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
@ -529,45 +546,25 @@ class EBookReader {
|
|||||||
}, 400),
|
}, 400),
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleSwipeDown() {
|
|
||||||
if (bottomBar.classList.contains("bottom-0"))
|
|
||||||
bottomBar.classList.remove("bottom-0");
|
|
||||||
else topBar.classList.add("top-0");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSwipeUp() {
|
|
||||||
if (topBar.classList.contains("top-0"))
|
|
||||||
topBar.classList.remove("top-0");
|
|
||||||
else bottomBar.classList.add("bottom-0");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
// -------------- Keyboard Shortcuts -------------- //
|
// ------------------- Gestures ------------------- //
|
||||||
// ------------------------------------------------ //
|
// ------------------------------------------------ //
|
||||||
|
|
||||||
renderDoc.addEventListener(
|
renderDoc.addEventListener(
|
||||||
"keyup",
|
"touchstart",
|
||||||
function (e) {
|
function (event) {
|
||||||
// Left Key (Previous Page)
|
touchStartX = event.changedTouches[0].screenX;
|
||||||
if ((e.keyCode || e.which) == 37) {
|
touchStartY = event.changedTouches[0].screenY;
|
||||||
prevPage();
|
},
|
||||||
}
|
false,
|
||||||
|
|
||||||
// Right Key (Next Page)
|
|
||||||
if ((e.keyCode || e.which) == 39) {
|
|
||||||
nextPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// "t" Key (Theme Cycle)
|
|
||||||
if ((e.keyCode || e.which) == 84) {
|
|
||||||
let currentThemeIdx = THEMES.indexOf(
|
|
||||||
readerSettings.theme.colorScheme,
|
|
||||||
);
|
);
|
||||||
let colorScheme =
|
|
||||||
THEMES.length == currentThemeIdx + 1
|
renderDoc.addEventListener(
|
||||||
? THEMES[0]
|
"touchend",
|
||||||
: THEMES[currentThemeIdx + 1];
|
function (event) {
|
||||||
setTheme({ colorScheme });
|
touchEndX = event.changedTouches[0].screenX;
|
||||||
}
|
touchEndY = event.changedTouches[0].screenY;
|
||||||
|
handleGesture(event);
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@ -584,7 +581,9 @@ class EBookReader {
|
|||||||
let nextPage = this.nextPage.bind(this);
|
let nextPage = this.nextPage.bind(this);
|
||||||
let prevPage = this.prevPage.bind(this);
|
let prevPage = this.prevPage.bind(this);
|
||||||
|
|
||||||
// Keyboard Shortcuts
|
// ------------------------------------------------ //
|
||||||
|
// -------------- Keyboard Shortcuts -------------- //
|
||||||
|
// ------------------------------------------------ //
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"keyup",
|
"keyup",
|
||||||
function (e) {
|
function (e) {
|
||||||
|
@ -118,6 +118,7 @@ func (c *Config) EnsureDirectories() {
|
|||||||
docDir := filepath.Join(c.DataPath, "documents")
|
docDir := filepath.Join(c.DataPath, "documents")
|
||||||
coversDir := filepath.Join(c.DataPath, "covers")
|
coversDir := filepath.Join(c.DataPath, "covers")
|
||||||
backupDir := filepath.Join(c.DataPath, "backups")
|
backupDir := filepath.Join(c.DataPath, "backups")
|
||||||
|
|
||||||
os.Mkdir(docDir, 0755)
|
os.Mkdir(docDir, 0755)
|
||||||
os.Mkdir(coversDir, 0755)
|
os.Mkdir(coversDir, 0755)
|
||||||
os.Mkdir(backupDir, 0755)
|
os.Mkdir(backupDir, 0755)
|
||||||
|
@ -1,35 +1,37 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
func TestLoadConfig(t *testing.T) {
|
func TestLoadConfig(t *testing.T) {
|
||||||
conf := Load()
|
conf := Load()
|
||||||
want := "sqlite"
|
assert.Equal(t, "sqlite", conf.DBType)
|
||||||
if conf.DBType != want {
|
|
||||||
t.Fatalf(`Load().DBType = %q, want match for %#q, nil`, conf.DBType, want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetEnvDefault(t *testing.T) {
|
func TestGetEnvDefault(t *testing.T) {
|
||||||
want := "def_val"
|
desiredValue := "def_val"
|
||||||
envDefault := getEnv("DEFAULT_TEST", want)
|
envDefault := getEnv("DEFAULT_TEST", desiredValue)
|
||||||
if envDefault != want {
|
|
||||||
t.Fatalf(`getEnv("DEFAULT_TEST", "def_val") = %q, want match for %#q, nil`, envDefault, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetEnvSet(t *testing.T) {
|
assert.Equal(t, desiredValue, envDefault)
|
||||||
envDefault := getEnv("SET_TEST", "not_this")
|
|
||||||
want := "set_val"
|
|
||||||
if envDefault != want {
|
|
||||||
t.Fatalf(`getEnv("SET_TEST", "not_this") = %q, want match for %#q, nil`, envDefault, want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrimLowerString(t *testing.T) {
|
func TestTrimLowerString(t *testing.T) {
|
||||||
want := "trimtest"
|
desiredValue := "trimtest"
|
||||||
output := trimLowerString(" trimTest ")
|
outputValue := trimLowerString(" trimTest ")
|
||||||
if output != want {
|
|
||||||
t.Fatalf(`trimLowerString(" trimTest ") = %q, want match for %#q, nil`, output, want)
|
assert.Equal(t, desiredValue, outputValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrettyCaller(t *testing.T) {
|
||||||
|
p, _, _, _ := runtime.Caller(0)
|
||||||
|
result := runtime.CallersFrames([]uintptr{p})
|
||||||
|
f, _ := result.Next()
|
||||||
|
functionName, fileName := prettyCaller(&f)
|
||||||
|
|
||||||
|
assert.Equal(t, "TestPrettyCaller", functionName, "should have current function name")
|
||||||
|
assert.Equal(t, "config/config_test.go@30", fileName, "should have current file path and line number")
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"reichard.io/antholume/config"
|
"reichard.io/antholume/config"
|
||||||
"reichard.io/antholume/utils"
|
"reichard.io/antholume/utils"
|
||||||
)
|
)
|
||||||
@ -28,9 +29,7 @@ func TestNewMgr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbm := NewMgr(&cfg)
|
dbm := NewMgr(&cfg)
|
||||||
if dbm == nil {
|
assert.NotNil(t, dbm, "should not be nil dbm")
|
||||||
t.Fatalf(`Expected: *DBManager, Got: nil`)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Database", func(t *testing.T) {
|
t.Run("Database", func(t *testing.T) {
|
||||||
dt := databaseTest{t, dbm}
|
dt := databaseTest{t, dbm}
|
||||||
@ -46,9 +45,7 @@ func (dt *databaseTest) TestUser() {
|
|||||||
dt.Run("User", func(t *testing.T) {
|
dt.Run("User", func(t *testing.T) {
|
||||||
// Generate Auth Hash
|
// Generate Auth Hash
|
||||||
rawAuthHash, err := utils.GenerateToken(64)
|
rawAuthHash, err := utils.GenerateToken(64)
|
||||||
if err != nil {
|
assert.Nil(t, err, "should be nil err")
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, nil, err, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||||
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
|
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
|
||||||
@ -57,14 +54,13 @@ func (dt *databaseTest) TestUser() {
|
|||||||
AuthHash: &authHash,
|
AuthHash: &authHash,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil || changed != 1 {
|
assert.Nil(t, err, "should be nil err")
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, changed, err)
|
assert.Equal(t, int64(1), changed)
|
||||||
}
|
|
||||||
|
|
||||||
user, err := dt.dbm.Queries.GetUser(dt.dbm.Ctx, userID)
|
user, err := dt.dbm.Queries.GetUser(dt.dbm.Ctx, userID)
|
||||||
if err != nil || *user.Pass != userPass {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, userPass, *user.Pass, err)
|
assert.Nil(t, err, "should be nil err")
|
||||||
}
|
assert.Equal(t, userPass, *user.Pass)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
go.mod
4
go.mod
@ -27,6 +27,7 @@ require (
|
|||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
@ -39,6 +40,7 @@ require (
|
|||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.2.2 // indirect
|
github.com/gorilla/sessions v1.2.2 // indirect
|
||||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||||
|
github.com/jarcoal/httpmock v1.3.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
@ -47,9 +49,11 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sethvargo/go-retry v0.2.4 // indirect
|
github.com/sethvargo/go-retry v0.2.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -127,6 +127,8 @@ github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
|||||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
|
||||||
|
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
110
metadata/_test_files/gbooks_id_response.json
Normal file
110
metadata/_test_files/gbooks_id_response.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"kind": "books#volume",
|
||||||
|
"id": "ZxwpakTv_MIC",
|
||||||
|
"etag": "mhqr3GsebaQ",
|
||||||
|
"selfLink": "https://www.googleapis.com/books/v1/volumes/ZxwpakTv_MIC",
|
||||||
|
"volumeInfo": {
|
||||||
|
"title": "Alice in Wonderland",
|
||||||
|
"authors": [
|
||||||
|
"Lewis Carroll"
|
||||||
|
],
|
||||||
|
"publisher": "The Floating Press",
|
||||||
|
"publishedDate": "2009-01-01",
|
||||||
|
"description": "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing.",
|
||||||
|
"industryIdentifiers": [
|
||||||
|
{
|
||||||
|
"type": "ISBN_10",
|
||||||
|
"identifier": "1877527815"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ISBN_13",
|
||||||
|
"identifier": "9781877527814"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readingModes": {
|
||||||
|
"text": true,
|
||||||
|
"image": false
|
||||||
|
},
|
||||||
|
"pageCount": 104,
|
||||||
|
"printedPageCount": 112,
|
||||||
|
"printType": "BOOK",
|
||||||
|
"categories": [
|
||||||
|
"Fiction / Classics",
|
||||||
|
"Juvenile Fiction / General"
|
||||||
|
],
|
||||||
|
"averageRating": 5,
|
||||||
|
"ratingsCount": 1,
|
||||||
|
"maturityRating": "NOT_MATURE",
|
||||||
|
"allowAnonLogging": true,
|
||||||
|
"contentVersion": "0.2.3.0.preview.2",
|
||||||
|
"panelizationSummary": {
|
||||||
|
"containsEpubBubbles": false,
|
||||||
|
"containsImageBubbles": false
|
||||||
|
},
|
||||||
|
"imageLinks": {
|
||||||
|
"smallThumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE71e5b-TeAKTiPSvXNUPeUi8rItzur2xSzwH8QU3qjKH0A2opmoq1o5I9RqJFt1BtcCCqILhnYRcB2aFLJmEvom11gx3Qn3PNN1iBLj2H5y2JHjM8wIwGT7iWFQmEn0Od7s6sOdk&source=gbs_api",
|
||||||
|
"thumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE70QORt9J_DmKJgfyf9UEjQkdDMZ0qAu0GP315a1Q4CRS3snEjKnJJO2fYFdxjMwsSpmHoXDFPZbsy4gw-kMvF7lL8LtwxGbJGlfETHw_jbQBKBlKTrneK4XFvvV-EXNrZRgylxj&source=gbs_api",
|
||||||
|
"small": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=2&edge=curl&imgtk=AFLRE70r1pAUt6VhuEEW8vXFhu8LvKln3yj0mdlaWPO4ZQuODLFQnH0fTebKMMX4ANR5i4PtC0oaI48XkwF-EdzlEM1WmUcR5383N4kRMXcta_i9nmb2y38dnh3hObwQW5VoAxbc9psn&source=gbs_api",
|
||||||
|
"medium": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=3&edge=curl&imgtk=AFLRE7019EVuXvhzbhmtbz1QFh-ajB6kTKRHGhqijFf8big_GPRMMdpCdKlklFbkCfXvy8F64t5NKlThUHb3tFP-51bbDXkrVErFbCqKGzGnDSSm8cewqT8HiYDNHqn0hXYnuYvN4vYf&source=gbs_api",
|
||||||
|
"large": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=4&edge=curl&imgtk=AFLRE72I15XZqp_8c8BAj4EskxkdC6nQz8F0Fs6VJhkykwIqfjzwuM34tUSQa3UnMGbx-UYjZjSLmCNFlePS8aR7yy-0UP9BRnYD-h5Qbesnnt_xdOb3u7Wdiobi6VbciNCBwUwbCyeH&source=gbs_api",
|
||||||
|
"extraLarge": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=6&edge=curl&imgtk=AFLRE70rC6ktY6U0K_hqG1HxPl_9hMjpKb10p9DryVIwQgUjoJfWQOjpNA3EQ-5yk167yYDlO27gylqNAdJBYWu7ZHr3GuqkjTDpXjDvzBBppVyWaVNxKwhOz3gfJ-gzM6cC4kLHP26R&source=gbs_api"
|
||||||
|
},
|
||||||
|
"language": "en",
|
||||||
|
"previewLink": "http://books.google.com/books?id=ZxwpakTv_MIC&hl=&source=gbs_api",
|
||||||
|
"infoLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&source=gbs_api",
|
||||||
|
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC"
|
||||||
|
},
|
||||||
|
"layerInfo": {
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"layerId": "geo",
|
||||||
|
"volumeAnnotationsVersion": "2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"saleInfo": {
|
||||||
|
"country": "US",
|
||||||
|
"saleability": "FOR_SALE",
|
||||||
|
"isEbook": true,
|
||||||
|
"listPrice": {
|
||||||
|
"amount": 3.99,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"retailPrice": {
|
||||||
|
"amount": 3.99,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"buyLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&rdid=book-ZxwpakTv_MIC&rdot=1&source=gbs_api",
|
||||||
|
"offers": [
|
||||||
|
{
|
||||||
|
"finskyOfferType": 1,
|
||||||
|
"listPrice": {
|
||||||
|
"amountInMicros": 3990000,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"retailPrice": {
|
||||||
|
"amountInMicros": 3990000,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"giftable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"accessInfo": {
|
||||||
|
"country": "US",
|
||||||
|
"viewability": "PARTIAL",
|
||||||
|
"embeddable": true,
|
||||||
|
"publicDomain": false,
|
||||||
|
"textToSpeechPermission": "ALLOWED",
|
||||||
|
"epub": {
|
||||||
|
"isAvailable": true,
|
||||||
|
"acsTokenLink": "http://books.google.com/books/download/Alice_in_Wonderland-sample-epub.acsm?id=ZxwpakTv_MIC&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||||
|
},
|
||||||
|
"pdf": {
|
||||||
|
"isAvailable": false
|
||||||
|
},
|
||||||
|
"webReaderLink": "http://play.google.com/books/reader?id=ZxwpakTv_MIC&hl=&source=gbs_api",
|
||||||
|
"accessViewStatus": "SAMPLE",
|
||||||
|
"quoteSharingAllowed": false
|
||||||
|
}
|
||||||
|
}
|
105
metadata/_test_files/gbooks_query_response.json
Normal file
105
metadata/_test_files/gbooks_query_response.json
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"kind": "books#volumes",
|
||||||
|
"totalItems": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": "books#volume",
|
||||||
|
"id": "ZxwpakTv_MIC",
|
||||||
|
"etag": "F2eR9VV6VwQ",
|
||||||
|
"selfLink": "https://www.googleapis.com/books/v1/volumes/ZxwpakTv_MIC",
|
||||||
|
"volumeInfo": {
|
||||||
|
"title": "Alice in Wonderland",
|
||||||
|
"authors": [
|
||||||
|
"Lewis Carroll"
|
||||||
|
],
|
||||||
|
"publisher": "The Floating Press",
|
||||||
|
"publishedDate": "2009-01-01",
|
||||||
|
"description": "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing.",
|
||||||
|
"industryIdentifiers": [
|
||||||
|
{
|
||||||
|
"type": "ISBN_13",
|
||||||
|
"identifier": "9781877527814"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ISBN_10",
|
||||||
|
"identifier": "1877527815"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"readingModes": {
|
||||||
|
"text": true,
|
||||||
|
"image": false
|
||||||
|
},
|
||||||
|
"pageCount": 104,
|
||||||
|
"printType": "BOOK",
|
||||||
|
"categories": [
|
||||||
|
"Fiction"
|
||||||
|
],
|
||||||
|
"averageRating": 5,
|
||||||
|
"ratingsCount": 1,
|
||||||
|
"maturityRating": "NOT_MATURE",
|
||||||
|
"allowAnonLogging": true,
|
||||||
|
"contentVersion": "0.2.3.0.preview.2",
|
||||||
|
"panelizationSummary": {
|
||||||
|
"containsEpubBubbles": false,
|
||||||
|
"containsImageBubbles": false
|
||||||
|
},
|
||||||
|
"imageLinks": {
|
||||||
|
"smallThumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api",
|
||||||
|
"thumbnail": "http://books.google.com/books/content?id=ZxwpakTv_MIC&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api"
|
||||||
|
},
|
||||||
|
"language": "en",
|
||||||
|
"previewLink": "http://books.google.com/books?id=ZxwpakTv_MIC&printsec=frontcover&dq=isbn:1877527815&hl=&cd=1&source=gbs_api",
|
||||||
|
"infoLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&source=gbs_api",
|
||||||
|
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC"
|
||||||
|
},
|
||||||
|
"saleInfo": {
|
||||||
|
"country": "US",
|
||||||
|
"saleability": "FOR_SALE",
|
||||||
|
"isEbook": true,
|
||||||
|
"listPrice": {
|
||||||
|
"amount": 3.99,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"retailPrice": {
|
||||||
|
"amount": 3.99,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"buyLink": "https://play.google.com/store/books/details?id=ZxwpakTv_MIC&rdid=book-ZxwpakTv_MIC&rdot=1&source=gbs_api",
|
||||||
|
"offers": [
|
||||||
|
{
|
||||||
|
"finskyOfferType": 1,
|
||||||
|
"listPrice": {
|
||||||
|
"amountInMicros": 3990000,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"retailPrice": {
|
||||||
|
"amountInMicros": 3990000,
|
||||||
|
"currencyCode": "USD"
|
||||||
|
},
|
||||||
|
"giftable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"accessInfo": {
|
||||||
|
"country": "US",
|
||||||
|
"viewability": "PARTIAL",
|
||||||
|
"embeddable": true,
|
||||||
|
"publicDomain": false,
|
||||||
|
"textToSpeechPermission": "ALLOWED",
|
||||||
|
"epub": {
|
||||||
|
"isAvailable": true,
|
||||||
|
"acsTokenLink": "http://books.google.com/books/download/Alice_in_Wonderland-sample-epub.acsm?id=ZxwpakTv_MIC&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||||
|
},
|
||||||
|
"pdf": {
|
||||||
|
"isAvailable": false
|
||||||
|
},
|
||||||
|
"webReaderLink": "http://play.google.com/books/reader?id=ZxwpakTv_MIC&hl=&source=gbs_api",
|
||||||
|
"accessViewStatus": "SAMPLE",
|
||||||
|
"quoteSharingAllowed": false
|
||||||
|
},
|
||||||
|
"searchInfo": {
|
||||||
|
"textSnippet": "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -16,6 +16,7 @@ func getEPUBMetadata(filepath string) (*MetadataInfo, error) {
|
|||||||
rf := rc.Rootfiles[0]
|
rf := rc.Rootfiles[0]
|
||||||
|
|
||||||
parsedMetadata := &MetadataInfo{
|
parsedMetadata := &MetadataInfo{
|
||||||
|
Type: TYPE_EPUB,
|
||||||
Title: &rf.Title,
|
Title: &rf.Title,
|
||||||
Author: &rf.Creator,
|
Author: &rf.Creator,
|
||||||
Description: &rf.Description,
|
Description: &rf.Description,
|
||||||
|
130
metadata/gbooks_test.go
Normal file
130
metadata/gbooks_test.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jarcoal/httpmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// const GBOOKS_QUERY_URL string = "https://www.googleapis.com/books/v1/volumes?q=%s"
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
//go:embed _test_files/gbooks_id_response.json
|
||||||
|
var idResp string
|
||||||
|
|
||||||
|
//go:embed _test_files/gbooks_query_response.json
|
||||||
|
var queryResp string
|
||||||
|
|
||||||
|
type details struct {
|
||||||
|
URLs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook API Helper
|
||||||
|
func hookAPI() *details {
|
||||||
|
// Start HTTPMock
|
||||||
|
httpmock.Activate()
|
||||||
|
|
||||||
|
// Create details struct
|
||||||
|
d := &details{
|
||||||
|
URLs: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Hook
|
||||||
|
matchRE := regexp.MustCompile(`^https://www\.googleapis\.com/books/v1/volumes.*`)
|
||||||
|
httpmock.RegisterRegexpResponder("GET", matchRE, func(req *http.Request) (*http.Response, error) {
|
||||||
|
// Append URL
|
||||||
|
d.URLs = append(d.URLs, req.URL.String())
|
||||||
|
|
||||||
|
// Get Raw Response
|
||||||
|
var rawResp string
|
||||||
|
if req.URL.Query().Get("q") != "" {
|
||||||
|
rawResp = queryResp
|
||||||
|
} else {
|
||||||
|
rawResp = idResp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to JSON Response
|
||||||
|
var responseData map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(rawResp), &responseData)
|
||||||
|
|
||||||
|
// Return Response
|
||||||
|
return httpmock.NewJsonResponse(200, responseData)
|
||||||
|
})
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGBooksGBIDMetadata(t *testing.T) {
|
||||||
|
hookDetails := hookAPI()
|
||||||
|
defer httpmock.DeactivateAndReset()
|
||||||
|
|
||||||
|
GBID := "ZxwpakTv_MIC"
|
||||||
|
expectedURL := fmt.Sprintf(GBOOKS_GBID_INFO_URL, GBID)
|
||||||
|
metadataResp, err := getGBooksMetadata(MetadataInfo{ID: &GBID})
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should not have error")
|
||||||
|
assert.Contains(t, hookDetails.URLs, expectedURL, "should have intercepted URL")
|
||||||
|
assert.Equal(t, 1, len(metadataResp), "should have single result")
|
||||||
|
|
||||||
|
mResult := metadataResp[0]
|
||||||
|
validateResult(t, &mResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGBooksISBNQuery(t *testing.T) {
|
||||||
|
hookDetails := hookAPI()
|
||||||
|
defer httpmock.DeactivateAndReset()
|
||||||
|
|
||||||
|
ISBN10 := "1877527815"
|
||||||
|
expectedURL := fmt.Sprintf(GBOOKS_QUERY_URL, "isbn:"+ISBN10)
|
||||||
|
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||||
|
ISBN10: &ISBN10,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should not have error")
|
||||||
|
assert.Contains(t, hookDetails.URLs, expectedURL, "should have intercepted URL")
|
||||||
|
assert.Equal(t, 1, len(metadataResp), "should have single result")
|
||||||
|
|
||||||
|
mResult := metadataResp[0]
|
||||||
|
validateResult(t, &mResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGBooksTitleQuery(t *testing.T) {
|
||||||
|
hookDetails := hookAPI()
|
||||||
|
defer httpmock.DeactivateAndReset()
|
||||||
|
|
||||||
|
title := "Alice in Wonderland 1877527815"
|
||||||
|
expectedURL := fmt.Sprintf(GBOOKS_QUERY_URL, url.QueryEscape(strings.TrimSpace(title)))
|
||||||
|
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
||||||
|
Title: &title,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should not have error")
|
||||||
|
assert.Contains(t, hookDetails.URLs, expectedURL, "should have intercepted URL")
|
||||||
|
assert.NotEqual(t, 0, len(metadataResp), "should not have no results")
|
||||||
|
|
||||||
|
mResult := metadataResp[0]
|
||||||
|
validateResult(t, &mResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateResult(t *testing.T, m *MetadataInfo) {
|
||||||
|
expectedTitle := "Alice in Wonderland"
|
||||||
|
expectedAuthor := "Lewis Carroll"
|
||||||
|
expectedDesc := "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing."
|
||||||
|
expectedISBN10 := "1877527815"
|
||||||
|
expectedISBN13 := "9781877527814"
|
||||||
|
|
||||||
|
assert.Equal(t, expectedTitle, *m.Title, "should have title")
|
||||||
|
assert.Equal(t, expectedAuthor, *m.Author, "should have author")
|
||||||
|
assert.Equal(t, expectedDesc, *m.Description, "should have description")
|
||||||
|
assert.Equal(t, expectedISBN10, *m.ISBN10, "should have ISBN10")
|
||||||
|
assert.Equal(t, expectedISBN13, *m.ISBN13, "should have ISBN10")
|
||||||
|
}
|
@ -1,76 +0,0 @@
|
|||||||
//go:build integration
|
|
||||||
|
|
||||||
package metadata
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGBooksGBIDMetadata(t *testing.T) {
|
|
||||||
GBID := "ZxwpakTv_MIC"
|
|
||||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
|
||||||
ID: &GBID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(metadataResp) != 1 {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, len(metadataResp), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mResult := metadataResp[0]
|
|
||||||
validateResult(&mResult, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGBooksISBNQuery(t *testing.T) {
|
|
||||||
ISBN10 := "1877527815"
|
|
||||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
|
||||||
ISBN10: &ISBN10,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(metadataResp) != 1 {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, len(metadataResp), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mResult := metadataResp[0]
|
|
||||||
validateResult(&mResult, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGBooksTitleQuery(t *testing.T) {
|
|
||||||
title := "Alice in Wonderland 1877527815"
|
|
||||||
metadataResp, err := getGBooksMetadata(MetadataInfo{
|
|
||||||
Title: &title,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(metadataResp) == 0 {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, "> 0", len(metadataResp), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mResult := metadataResp[0]
|
|
||||||
validateResult(&mResult, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateResult(m *MetadataInfo, t *testing.T) {
|
|
||||||
expect := "Lewis Carroll"
|
|
||||||
if *m.Author != expect {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.Author)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect = "Alice in Wonderland"
|
|
||||||
if *m.Title != expect {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect = "Alice in Wonderland (also known as Alice's Adventures in Wonderland), from 1865, is the peculiar and imaginative tale of a girl who falls down a rabbit-hole into a bizarre world of eccentric and unusual creatures. Lewis Carroll's prominent example of the genre of \"literary nonsense\" has endured in popularity with its clever way of playing with logic and a narrative structure that has influence generations of fiction writing."
|
|
||||||
if *m.Description != expect {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect = "1877527815"
|
|
||||||
if *m.ISBN10 != expect {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.ISBN10)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect = "9781877527814"
|
|
||||||
if *m.ISBN13 != expect {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v`, expect, *m.ISBN13)
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,27 +3,47 @@ package metadata
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"reichard.io/antholume/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MetadataHandler func(string) (*MetadataInfo, error)
|
||||||
|
|
||||||
|
type DocumentType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TYPE_EPUB DocumentType = ".epub"
|
||||||
|
)
|
||||||
|
|
||||||
|
var extensionHandlerMap = map[DocumentType]MetadataHandler{
|
||||||
|
TYPE_EPUB: getEPUBMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
type Source int
|
type Source int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GBOOK Source = iota
|
SOURCE_GBOOK Source = iota
|
||||||
OLIB
|
SOURCE_OLIB
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetadataInfo struct {
|
type MetadataInfo struct {
|
||||||
ID *string
|
ID *string
|
||||||
|
MD5 *string
|
||||||
|
PartialMD5 *string
|
||||||
|
WordCount *int64
|
||||||
|
|
||||||
Title *string
|
Title *string
|
||||||
Author *string
|
Author *string
|
||||||
Description *string
|
Description *string
|
||||||
ISBN10 *string
|
ISBN10 *string
|
||||||
ISBN13 *string
|
ISBN13 *string
|
||||||
|
Type DocumentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Downloads the Google Books cover file and saves it to the provided directory.
|
||||||
func CacheCover(gbid string, coverDir string, documentID string, overwrite bool) (*string, error) {
|
func CacheCover(gbid string, coverDir string, documentID string, overwrite bool) (*string, error) {
|
||||||
// Get Filepath
|
// Get Filepath
|
||||||
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
|
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
|
||||||
@ -39,11 +59,12 @@ func CacheCover(gbid string, coverDir string, documentID string, overwrite bool)
|
|||||||
return &coverFile, nil
|
return &coverFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Searches source for metadata based on the provided information.
|
||||||
func SearchMetadata(s Source, metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
func SearchMetadata(s Source, metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
||||||
switch s {
|
switch s {
|
||||||
case GBOOK:
|
case SOURCE_GBOOK:
|
||||||
return getGBooksMetadata(metadataSearch)
|
return getGBooksMetadata(metadataSearch)
|
||||||
case OLIB:
|
case SOURCE_OLIB:
|
||||||
return nil, errors.New("Not implemented")
|
return nil, errors.New("Not implemented")
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("Not implemented")
|
return nil, errors.New("Not implemented")
|
||||||
@ -51,32 +72,112 @@ func SearchMetadata(s Source, metadataSearch MetadataInfo) ([]MetadataInfo, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWordCount(filepath string) (int64, error) {
|
// Returns the word count of the provided filepath. An error will be returned
|
||||||
fileMime, err := mimetype.DetectFile(filepath)
|
// if the file is not supported.
|
||||||
if err != nil {
|
func GetWordCount(filepath string) (*int64, error) {
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
|
|
||||||
totalWords, err := countEPUBWords(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return totalWords, nil
|
|
||||||
} else {
|
|
||||||
return 0, errors.New("Invalid Extension")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetMetadata(filepath string) (*MetadataInfo, error) {
|
|
||||||
fileMime, err := mimetype.DetectFile(filepath)
|
fileMime, err := mimetype.DetectFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
|
if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
|
||||||
return getEPUBMetadata(filepath)
|
totalWords, err := countEPUBWords(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &totalWords, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Invalid Extension")
|
return nil, fmt.Errorf("Invalid extension")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns embedded metadata of the provided file. An error will be returned if
|
||||||
|
// the file is not supported.
|
||||||
|
func GetMetadata(filepath string) (*MetadataInfo, error) {
|
||||||
|
// Detect Extension Type
|
||||||
|
fileMime, err := mimetype.DetectFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Extension Type Metadata Handler
|
||||||
|
fileExtension := fileMime.Extension()
|
||||||
|
handler, ok := extensionHandlerMap[DocumentType(fileExtension)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid extension %s", fileExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire Metadata
|
||||||
|
metadataInfo, err := handler(filepath)
|
||||||
|
|
||||||
|
// Calculate MD5 & Partial MD5
|
||||||
|
partialMD5, err := utils.CalculatePartialMD5(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to calculate partial MD5")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Actual MD5
|
||||||
|
MD5, err := utils.CalculateMD5(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to calculate MD5")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Word Count
|
||||||
|
wordCount, err := GetWordCount(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to calculate word count")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataInfo.WordCount = wordCount
|
||||||
|
metadataInfo.PartialMD5 = partialMD5
|
||||||
|
metadataInfo.MD5 = MD5
|
||||||
|
|
||||||
|
return metadataInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the extension of the provided filepath (e.g. ".epub"). An error
|
||||||
|
// will be returned if the file is not supported.
|
||||||
|
func GetDocumentType(filepath string) (*DocumentType, error) {
|
||||||
|
// Detect Extension Type
|
||||||
|
fileMime, err := mimetype.DetectFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect
|
||||||
|
fileExtension := fileMime.Extension()
|
||||||
|
docType, ok := ParseDocumentType(fileExtension)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("filetype not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &docType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the extension of the provided file reader (e.g. ".epub"). An error
|
||||||
|
// will be returned if the file is not supported.
|
||||||
|
func GetDocumentTypeReader(r io.Reader) (*DocumentType, error) {
|
||||||
|
// Detect Extension Type
|
||||||
|
fileMime, err := mimetype.DetectReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect
|
||||||
|
fileExtension := fileMime.Extension()
|
||||||
|
docType, ok := ParseDocumentType(fileExtension)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("filetype not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &docType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a filetype string, attempt to resolve a DocumentType
|
||||||
|
func ParseDocumentType(input string) (DocumentType, bool) {
|
||||||
|
validTypes := map[string]DocumentType{
|
||||||
|
string(TYPE_EPUB): TYPE_EPUB,
|
||||||
|
}
|
||||||
|
found, ok := validTypes[input]
|
||||||
|
return found, ok
|
||||||
|
}
|
||||||
|
@ -1,36 +1,46 @@
|
|||||||
package metadata
|
package metadata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetWordCount(t *testing.T) {
|
func TestGetWordCount(t *testing.T) {
|
||||||
var want int64 = 30080
|
var desiredCount int64 = 30080
|
||||||
wordCount, err := countEPUBWords("../_test_files/alice.epub")
|
actualCount, err := countEPUBWords("../_test_files/alice.epub")
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should have no error")
|
||||||
|
assert.Equal(t, desiredCount, actualCount, "should be correct word count")
|
||||||
|
|
||||||
if wordCount != want {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, wordCount, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMetadata(t *testing.T) {
|
func TestGetMetadata(t *testing.T) {
|
||||||
metadataInfo, err := getEPUBMetadata("../_test_files/alice.epub")
|
desiredTitle := "Alice's Adventures in Wonderland / Illustrated by Arthur Rackham. With a Proem by Austin Dobson"
|
||||||
if err != nil {
|
desiredAuthor := "Lewis Carroll"
|
||||||
t.Fatalf(`Expected: *MetadataInfo, Got: nil, Error: %v`, err)
|
desiredDescription := ""
|
||||||
}
|
|
||||||
|
|
||||||
want := "Alice's Adventures in Wonderland / Illustrated by Arthur Rackham. With a Proem by Austin Dobson"
|
metadataInfo, err := GetMetadata("../_test_files/alice.epub")
|
||||||
if *metadataInfo.Title != want {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Title, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
want = "Lewis Carroll"
|
assert.Nil(t, err, "should have no error")
|
||||||
if *metadataInfo.Author != want {
|
assert.Equal(t, desiredTitle, *metadataInfo.Title, "should be correct title")
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Author, err)
|
assert.Equal(t, desiredAuthor, *metadataInfo.Author, "should be correct author")
|
||||||
}
|
assert.Equal(t, desiredDescription, *metadataInfo.Description, "should be correct author")
|
||||||
|
assert.Equal(t, TYPE_EPUB, metadataInfo.Type, "should be correct type")
|
||||||
want = ""
|
}
|
||||||
if *metadataInfo.Description != want {
|
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, *metadataInfo.Description, err)
|
func TestGetExtension(t *testing.T) {
|
||||||
}
|
docType, err := GetDocumentType("../_test_files/alice.epub")
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should have no error")
|
||||||
|
assert.Equal(t, TYPE_EPUB, *docType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExtensionReader(t *testing.T) {
|
||||||
|
file, _ := os.Open("../_test_files/alice.epub")
|
||||||
|
docType, err := GetDocumentTypeReader(file)
|
||||||
|
|
||||||
|
assert.Nil(t, err, "should have no error")
|
||||||
|
assert.Equal(t, TYPE_EPUB, *docType)
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
// Feed root element for acquisition or navigation feed
|
// Feed root element for acquisition or navigation feed
|
||||||
type Feed struct {
|
type Feed struct {
|
||||||
|
ID string `xml:"id,omitempty"`
|
||||||
XMLName xml.Name `xml:"feed"`
|
XMLName xml.Name `xml:"feed"`
|
||||||
ID string `xml:"id,omitempty",`
|
|
||||||
Title string `xml:"title,omitempty"`
|
Title string `xml:"title,omitempty"`
|
||||||
Updated time.Time `xml:"updated,omitempty"`
|
Updated time.Time `xml:"updated,omitempty"`
|
||||||
Entries []Entry `xml:"entry,omitempty"`
|
Entries []Entry `xml:"entry,omitempty"`
|
||||||
|
75
search/anna.go
Normal file
75
search/anna.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseAnnasArchiveDownloadURL(body io.ReadCloser) (string, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, _ := goquery.NewDocumentFromReader(body)
|
||||||
|
|
||||||
|
// Return Download URL
|
||||||
|
downloadURL, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
||||||
|
if exists == false {
|
||||||
|
return "", fmt.Errorf("Download URL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possible Funky URL
|
||||||
|
downloadURL = strings.ReplaceAll(downloadURL, "\\", "/")
|
||||||
|
|
||||||
|
return downloadURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Results
|
||||||
|
var allEntries []SearchItem
|
||||||
|
doc.Find("form > div.w-full > div.w-full > div > div.justify-center").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
// Parse Details
|
||||||
|
details := rawBook.Find("div:nth-child(2) > div:nth-child(1)").Text()
|
||||||
|
detailsSplit := strings.Split(details, ", ")
|
||||||
|
|
||||||
|
// Invalid Details
|
||||||
|
if len(detailsSplit) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
language := detailsSplit[0]
|
||||||
|
fileType := detailsSplit[1]
|
||||||
|
fileSize := detailsSplit[2]
|
||||||
|
|
||||||
|
// Get Title & Author
|
||||||
|
title := rawBook.Find("h3").Text()
|
||||||
|
author := rawBook.Find("div:nth-child(2) > div:nth-child(4)").Text()
|
||||||
|
|
||||||
|
// Parse MD5
|
||||||
|
itemHref, _ := rawBook.Find("a").Attr("href")
|
||||||
|
hrefArray := strings.Split(itemHref, "/")
|
||||||
|
id := hrefArray[len(hrefArray)-1]
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Language: language,
|
||||||
|
FileType: fileType,
|
||||||
|
FileSize: fileSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return Results
|
||||||
|
return allEntries, nil
|
||||||
|
}
|
42
search/goodreads.go
Normal file
42
search/goodreads.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GoodReadsMostRead(c Cadence) ([]SearchItem, error) {
|
||||||
|
body, err := getPage("https://www.goodreads.com/book/most_read?category=all&country=US&duration=" + string(c))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseGoodReads(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGoodReads(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Results
|
||||||
|
var allEntries []SearchItem
|
||||||
|
|
||||||
|
doc.Find("[itemtype=\"http://schema.org/Book\"]").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
title := rawBook.Find(".bookTitle span").Text()
|
||||||
|
author := rawBook.Find(".authorName span").Text()
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return Results
|
||||||
|
return allEntries, nil
|
||||||
|
}
|
123
search/libgen.go
Normal file
123
search/libgen.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseLibGenFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Results
|
||||||
|
var allEntries []SearchItem
|
||||||
|
doc.Find("table.catalog tbody > tr").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
|
||||||
|
// Parse File Details
|
||||||
|
fileItem := rawBook.Find("td:nth-child(5)")
|
||||||
|
fileDesc := fileItem.Text()
|
||||||
|
fileDescSplit := strings.Split(fileDesc, "/")
|
||||||
|
fileType := strings.ToLower(strings.TrimSpace(fileDescSplit[0]))
|
||||||
|
fileSize := strings.TrimSpace(fileDescSplit[1])
|
||||||
|
|
||||||
|
// Parse Upload Date
|
||||||
|
uploadedRaw, _ := fileItem.Attr("title")
|
||||||
|
uploadedDateRaw := strings.Split(uploadedRaw, "Uploaded at ")[1]
|
||||||
|
uploadDate, _ := time.Parse("2006-01-02 15:04:05", uploadedDateRaw)
|
||||||
|
|
||||||
|
// Parse MD5
|
||||||
|
editHref, _ := rawBook.Find("td:nth-child(7) a").Attr("href")
|
||||||
|
hrefArray := strings.Split(editHref, "/")
|
||||||
|
id := hrefArray[len(hrefArray)-1]
|
||||||
|
|
||||||
|
// Parse Other Details
|
||||||
|
title := rawBook.Find("td:nth-child(3) p a").Text()
|
||||||
|
author := rawBook.Find(".catalog_authors li a").Text()
|
||||||
|
language := rawBook.Find("td:nth-child(4)").Text()
|
||||||
|
series := rawBook.Find("td:nth-child(2)").Text()
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Series: series,
|
||||||
|
Language: language,
|
||||||
|
FileType: fileType,
|
||||||
|
FileSize: fileSize,
|
||||||
|
UploadDate: uploadDate.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return Results
|
||||||
|
return allEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLibGenNonFiction(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Results
|
||||||
|
var allEntries []SearchItem
|
||||||
|
doc.Find("table.c tbody > tr:nth-child(n + 2)").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
|
||||||
|
// Parse Type & Size
|
||||||
|
fileSize := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(8)").Text()))
|
||||||
|
fileType := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(9)").Text()))
|
||||||
|
|
||||||
|
// Parse MD5
|
||||||
|
titleRaw := rawBook.Find("td:nth-child(3) [id]")
|
||||||
|
editHref, _ := titleRaw.Attr("href")
|
||||||
|
hrefArray := strings.Split(editHref, "?md5=")
|
||||||
|
id := hrefArray[1]
|
||||||
|
|
||||||
|
// Parse Other Details
|
||||||
|
title := titleRaw.Text()
|
||||||
|
author := rawBook.Find("td:nth-child(2)").Text()
|
||||||
|
language := rawBook.Find("td:nth-child(7)").Text()
|
||||||
|
series := rawBook.Find("td:nth-child(3) [href*='column=series']").Text()
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Series: series,
|
||||||
|
Language: language,
|
||||||
|
FileType: fileType,
|
||||||
|
FileSize: fileSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return Results
|
||||||
|
return allEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLibGenDownloadURL(body io.ReadCloser) (string, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, _ := goquery.NewDocumentFromReader(body)
|
||||||
|
|
||||||
|
// Return Download URL
|
||||||
|
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
|
||||||
|
downloadURL, exists := doc.Find("#download h2 a").Attr("href")
|
||||||
|
if exists == false {
|
||||||
|
return "", fmt.Errorf("Download URL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadURL, nil
|
||||||
|
}
|
225
search/search.go
225
search/search.go
@ -2,16 +2,13 @@ package search
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -102,14 +99,14 @@ func SaveBook(id string, source Source) (string, error) {
|
|||||||
bookURL, err := def.parseDownloadFunc(body)
|
bookURL, err := def.parseDownloadFunc(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Parse Download URL Error: ", err)
|
log.Error("Parse Download URL Error: ", err)
|
||||||
return "", errors.New("Download Failure")
|
return "", fmt.Errorf("Download Failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create File
|
// Create File
|
||||||
tempFile, err := os.CreateTemp("", "book")
|
tempFile, err := os.CreateTemp("", "book")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("File Create Error: ", err)
|
log.Error("File Create Error: ", err)
|
||||||
return "", errors.New("File Failure")
|
return "", fmt.Errorf("File Failure")
|
||||||
}
|
}
|
||||||
defer tempFile.Close()
|
defer tempFile.Close()
|
||||||
|
|
||||||
@ -119,7 +116,7 @@ func SaveBook(id string, source Source) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(tempFile.Name())
|
os.Remove(tempFile.Name())
|
||||||
log.Error("Book URL API Failure: ", err)
|
log.Error("Book URL API Failure: ", err)
|
||||||
return "", errors.New("API Failure")
|
return "", fmt.Errorf("API Failure")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
@ -129,20 +126,12 @@ func SaveBook(id string, source Source) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(tempFile.Name())
|
os.Remove(tempFile.Name())
|
||||||
log.Error("File Copy Error: ", err)
|
log.Error("File Copy Error: ", err)
|
||||||
return "", errors.New("File Failure")
|
return "", fmt.Errorf("File Failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
return tempFile.Name(), nil
|
return tempFile.Name(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GoodReadsMostRead(c Cadence) ([]SearchItem, error) {
|
|
||||||
body, err := getPage("https://www.goodreads.com/book/most_read?category=all&country=US&duration=" + string(c))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return parseGoodReads(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBookURL(id string, bookType BookType) (string, error) {
|
func GetBookURL(id string, bookType BookType) (string, error) {
|
||||||
// Derive Info URL
|
// Derive Info URL
|
||||||
var infoURL string
|
var infoURL string
|
||||||
@ -180,212 +169,6 @@ func getPage(page string) (io.ReadCloser, error) {
|
|||||||
return resp.Body, err
|
return resp.Body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLibGenFiction(body io.ReadCloser) ([]SearchItem, error) {
|
|
||||||
// Parse
|
|
||||||
defer body.Close()
|
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize Results
|
|
||||||
var allEntries []SearchItem
|
|
||||||
doc.Find("table.catalog tbody > tr").Each(func(ix int, rawBook *goquery.Selection) {
|
|
||||||
|
|
||||||
// Parse File Details
|
|
||||||
fileItem := rawBook.Find("td:nth-child(5)")
|
|
||||||
fileDesc := fileItem.Text()
|
|
||||||
fileDescSplit := strings.Split(fileDesc, "/")
|
|
||||||
fileType := strings.ToLower(strings.TrimSpace(fileDescSplit[0]))
|
|
||||||
fileSize := strings.TrimSpace(fileDescSplit[1])
|
|
||||||
|
|
||||||
// Parse Upload Date
|
|
||||||
uploadedRaw, _ := fileItem.Attr("title")
|
|
||||||
uploadedDateRaw := strings.Split(uploadedRaw, "Uploaded at ")[1]
|
|
||||||
uploadDate, _ := time.Parse("2006-01-02 15:04:05", uploadedDateRaw)
|
|
||||||
|
|
||||||
// Parse MD5
|
|
||||||
editHref, _ := rawBook.Find("td:nth-child(7) a").Attr("href")
|
|
||||||
hrefArray := strings.Split(editHref, "/")
|
|
||||||
id := hrefArray[len(hrefArray)-1]
|
|
||||||
|
|
||||||
// Parse Other Details
|
|
||||||
title := rawBook.Find("td:nth-child(3) p a").Text()
|
|
||||||
author := rawBook.Find(".catalog_authors li a").Text()
|
|
||||||
language := rawBook.Find("td:nth-child(4)").Text()
|
|
||||||
series := rawBook.Find("td:nth-child(2)").Text()
|
|
||||||
|
|
||||||
item := SearchItem{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
Author: author,
|
|
||||||
Series: series,
|
|
||||||
Language: language,
|
|
||||||
FileType: fileType,
|
|
||||||
FileSize: fileSize,
|
|
||||||
UploadDate: uploadDate.Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
allEntries = append(allEntries, item)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return Results
|
|
||||||
return allEntries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLibGenNonFiction(body io.ReadCloser) ([]SearchItem, error) {
|
|
||||||
// Parse
|
|
||||||
defer body.Close()
|
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize Results
|
|
||||||
var allEntries []SearchItem
|
|
||||||
doc.Find("table.c tbody > tr:nth-child(n + 2)").Each(func(ix int, rawBook *goquery.Selection) {
|
|
||||||
|
|
||||||
// Parse Type & Size
|
|
||||||
fileSize := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(8)").Text()))
|
|
||||||
fileType := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(9)").Text()))
|
|
||||||
|
|
||||||
// Parse MD5
|
|
||||||
titleRaw := rawBook.Find("td:nth-child(3) [id]")
|
|
||||||
editHref, _ := titleRaw.Attr("href")
|
|
||||||
hrefArray := strings.Split(editHref, "?md5=")
|
|
||||||
id := hrefArray[1]
|
|
||||||
|
|
||||||
// Parse Other Details
|
|
||||||
title := titleRaw.Text()
|
|
||||||
author := rawBook.Find("td:nth-child(2)").Text()
|
|
||||||
language := rawBook.Find("td:nth-child(7)").Text()
|
|
||||||
series := rawBook.Find("td:nth-child(3) [href*='column=series']").Text()
|
|
||||||
|
|
||||||
item := SearchItem{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
Author: author,
|
|
||||||
Series: series,
|
|
||||||
Language: language,
|
|
||||||
FileType: fileType,
|
|
||||||
FileSize: fileSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
allEntries = append(allEntries, item)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return Results
|
|
||||||
return allEntries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLibGenDownloadURL(body io.ReadCloser) (string, error) {
|
|
||||||
// Parse
|
|
||||||
defer body.Close()
|
|
||||||
doc, _ := goquery.NewDocumentFromReader(body)
|
|
||||||
|
|
||||||
// Return Download URL
|
|
||||||
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
|
|
||||||
downloadURL, exists := doc.Find("#download h2 a").Attr("href")
|
|
||||||
if exists == false {
|
|
||||||
return "", errors.New("Download URL not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return downloadURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGoodReads(body io.ReadCloser) ([]SearchItem, error) {
|
|
||||||
// Parse
|
|
||||||
defer body.Close()
|
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize Results
|
|
||||||
var allEntries []SearchItem
|
|
||||||
|
|
||||||
doc.Find("[itemtype=\"http://schema.org/Book\"]").Each(func(ix int, rawBook *goquery.Selection) {
|
|
||||||
title := rawBook.Find(".bookTitle span").Text()
|
|
||||||
author := rawBook.Find(".authorName span").Text()
|
|
||||||
|
|
||||||
item := SearchItem{
|
|
||||||
Title: title,
|
|
||||||
Author: author,
|
|
||||||
}
|
|
||||||
|
|
||||||
allEntries = append(allEntries, item)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return Results
|
|
||||||
return allEntries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAnnasArchiveDownloadURL(body io.ReadCloser) (string, error) {
|
|
||||||
// Parse
|
|
||||||
defer body.Close()
|
|
||||||
doc, _ := goquery.NewDocumentFromReader(body)
|
|
||||||
|
|
||||||
// Return Download URL
|
|
||||||
downloadURL, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
|
||||||
if exists == false {
|
|
||||||
return "", errors.New("Download URL not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possible Funky URL
|
|
||||||
downloadURL = strings.ReplaceAll(downloadURL, "\\", "/")
|
|
||||||
|
|
||||||
return downloadURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
|
||||||
// Parse
|
|
||||||
defer body.Close()
|
|
||||||
doc, err := goquery.NewDocumentFromReader(body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize Results
|
|
||||||
var allEntries []SearchItem
|
|
||||||
doc.Find("form > div.w-full > div.w-full > div > div.justify-center").Each(func(ix int, rawBook *goquery.Selection) {
|
|
||||||
// Parse Details
|
|
||||||
details := rawBook.Find("div:nth-child(2) > div:nth-child(1)").Text()
|
|
||||||
detailsSplit := strings.Split(details, ", ")
|
|
||||||
|
|
||||||
// Invalid Details
|
|
||||||
if len(detailsSplit) < 3 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
language := detailsSplit[0]
|
|
||||||
fileType := detailsSplit[1]
|
|
||||||
fileSize := detailsSplit[2]
|
|
||||||
|
|
||||||
// Get Title & Author
|
|
||||||
title := rawBook.Find("h3").Text()
|
|
||||||
author := rawBook.Find("div:nth-child(2) > div:nth-child(4)").Text()
|
|
||||||
|
|
||||||
// Parse MD5
|
|
||||||
itemHref, _ := rawBook.Find("a").Attr("href")
|
|
||||||
hrefArray := strings.Split(itemHref, "/")
|
|
||||||
id := hrefArray[len(hrefArray)-1]
|
|
||||||
|
|
||||||
item := SearchItem{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
Author: author,
|
|
||||||
Language: language,
|
|
||||||
FileType: fileType,
|
|
||||||
FileSize: fileSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
allEntries = append(allEntries, item)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return Results
|
|
||||||
return allEntries, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadBook(bookURL string) (*http.Response, error) {
|
func downloadBook(bookURL string) (*http.Response, error) {
|
||||||
// Allow Insecure
|
// Allow Insecure
|
||||||
client := &http.Client{Transport: &http.Transport{
|
client := &http.Client{Transport: &http.Transport{
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col-reverse text-black dark:text-white"
|
<!-- Required for iOS "Hover" Events (onclick) -->
|
||||||
|
<div onclick
|
||||||
|
class="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
|
||||||
style="font-family: monospace">
|
style="font-family: monospace">
|
||||||
{{ range $log := .Data }}
|
{{ range $log := .Data }}
|
||||||
<span class="whitespace-nowrap hover:whitespace-pre">{{ $log }}</span>
|
<span class="whitespace-nowrap hover:whitespace-pre">{{ $log }}</span>
|
||||||
|
@ -10,10 +10,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Reimplemented KOReader Partial MD5 Calculation
|
// Reimplemented KOReader Partial MD5 Calculation
|
||||||
func CalculatePartialMD5(filePath string) (string, error) {
|
func CalculatePartialMD5(filePath string) (*string, error) {
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
@ -41,7 +41,8 @@ func CalculatePartialMD5(filePath string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allBytes := buf.Bytes()
|
allBytes := buf.Bytes()
|
||||||
return fmt.Sprintf("%x", md5.Sum(allBytes)), nil
|
fileHash := fmt.Sprintf("%x", md5.Sum(allBytes))
|
||||||
|
return &fileHash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a token of n size
|
// Creates a token of n size
|
||||||
@ -53,3 +54,23 @@ func GenerateToken(n int) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate MD5 of a file
|
||||||
|
func CalculateMD5(filePath string) (*string, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := md5.New()
|
||||||
|
_, err = io.Copy(hash, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileHash := fmt.Sprintf("%x", hash.Sum(nil))
|
||||||
|
|
||||||
|
return &fileHash, nil
|
||||||
|
}
|
||||||
|
@ -1,12 +1,26 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestCalculatePartialMD5(t *testing.T) {
|
func TestCalculatePartialMD5(t *testing.T) {
|
||||||
partialMD5, err := CalculatePartialMD5("../_test_files/alice.epub")
|
assert := assert.New(t)
|
||||||
|
|
||||||
want := "386d1cb51fe4a72e5c9fdad5e059bad9"
|
desiredPartialMD5 := "386d1cb51fe4a72e5c9fdad5e059bad9"
|
||||||
if partialMD5 != want {
|
calculatedPartialMD5, err := CalculatePartialMD5("../_test_files/alice.epub")
|
||||||
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, partialMD5, err)
|
|
||||||
}
|
assert.Nil(err, "error should be nil")
|
||||||
|
assert.Equal(desiredPartialMD5, *calculatedPartialMD5, "should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateMD5(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
desiredMD5 := "0f36c66155de34b281c4791654d0b1ce"
|
||||||
|
calculatedMD5, err := CalculateMD5("../_test_files/alice.epub")
|
||||||
|
|
||||||
|
assert.Nil(err, "error should be nil")
|
||||||
|
assert.Equal(desiredMD5, *calculatedMD5, "should be equal")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user