[add] tests, [add] refactor epub feat

This commit is contained in:
Evan Reichard 2023-10-23 20:18:16 -04:00
parent bf6ac96376
commit b5d5e4bd64
17 changed files with 429 additions and 353 deletions

View File

@ -25,3 +25,10 @@ docker_build_release_latest:
-t gitea.va.reichard.io/evan/bookmanager:latest \ -t gitea.va.reichard.io/evan/bookmanager:latest \
-t gitea.va.reichard.io/evan/bookmanager:`git describe --tags` \ -t gitea.va.reichard.io/evan/bookmanager:`git describe --tags` \
--push . --push .
tests_integration:
go test -v -tags=integration -coverpkg=./... ./metadata
tests_unit:
SET_TEST=set_val go test -v -coverpkg=./... ./...

View File

@ -13,8 +13,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"reichard.io/bbank/config" "reichard.io/bbank/config"
"reichard.io/bbank/database" "reichard.io/bbank/database"
"reichard.io/bbank/graph"
"reichard.io/bbank/utils"
) )
type API struct { type API struct {
@ -75,9 +73,9 @@ func (api *API) registerWebAppRoutes() {
// Define Templates & Helper Functions // Define Templates & Helper Functions
render := multitemplate.NewRenderer() render := multitemplate.NewRenderer()
helperFuncs := template.FuncMap{ helperFuncs := template.FuncMap{
"GetSVGGraphData": graph.GetSVGGraphData, "GetSVGGraphData": getSVGGraphData,
"GetUTCOffsets": utils.GetUTCOffsets, "GetUTCOffsets": getUTCOffsets,
"NiceSeconds": utils.NiceSeconds, "NiceSeconds": niceSeconds,
} }
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html") render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")

View File

@ -19,7 +19,6 @@ import (
"reichard.io/bbank/database" "reichard.io/bbank/database"
"reichard.io/bbank/metadata" "reichard.io/bbank/metadata"
"reichard.io/bbank/search" "reichard.io/bbank/search"
"reichard.io/bbank/utils"
) )
type queryParams struct { type queryParams struct {
@ -587,7 +586,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
} }
// Calculate Partial MD5 ID // Calculate Partial MD5 ID
partialMD5, err := utils.CalculatePartialMD5(tempFilePath) partialMD5, err := calculatePartialMD5(tempFilePath)
if err != nil { if err != nil {
log.Warn("[saveNewDocument] Partial MD5 Error: ", err) log.Warn("[saveNewDocument] Partial MD5 Error: ", err)
c.AbortWithStatus(http.StatusBadRequest) c.AbortWithStatus(http.StatusBadRequest)

View File

@ -1,4 +1,4 @@
package utils package api
import ( import (
"bytes" "bytes"
@ -7,6 +7,9 @@ import (
"io" "io"
"math" "math"
"os" "os"
"reichard.io/bbank/database"
"reichard.io/bbank/graph"
) )
type UTCOffset struct { type UTCOffset struct {
@ -55,11 +58,11 @@ var UTC_OFFSETS = []UTCOffset{
{Value: "+14 hours", Name: "UTC+14:00"}, {Value: "+14 hours", Name: "UTC+14:00"},
} }
func GetUTCOffsets() []UTCOffset { func getUTCOffsets() []UTCOffset {
return UTC_OFFSETS return UTC_OFFSETS
} }
func NiceSeconds(input int64) (result string) { func niceSeconds(input int64) (result string) {
if input == 0 { if input == 0 {
return "N/A" return "N/A"
} }
@ -88,7 +91,7 @@ func NiceSeconds(input int64) (result string) {
} }
// 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 "", err
@ -121,3 +124,13 @@ func CalculatePartialMD5(filePath string) (string, error) {
allBytes := buf.Bytes() allBytes := buf.Bytes()
return fmt.Sprintf("%x", md5.Sum(allBytes)), nil return fmt.Sprintf("%x", md5.Sum(allBytes)), nil
} }
// Convert Database Array -> Int64 Array
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
var intData []int64
for _, item := range inputData {
intData = append(intData, item.MinutesRead)
}
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
}

View File

@ -13,7 +13,6 @@ type Config struct {
// DB Configuration // DB Configuration
DBType string DBType string
DBName string DBName string
DBPassword string
// Data Paths // Data Paths
ConfigPath string ConfigPath string
@ -30,7 +29,6 @@ func Load() *Config {
Version: "0.0.2", Version: "0.0.2",
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")), DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
DBName: trimLowerString(getEnv("DATABASE_NAME", "book_manager")), DBName: trimLowerString(getEnv("DATABASE_NAME", "book_manager")),
DBPassword: getEnv("DATABASE_PASSWORD", ""),
ConfigPath: getEnv("CONFIG_PATH", "/config"), ConfigPath: getEnv("CONFIG_PATH", "/config"),
DataPath: getEnv("DATA_PATH", "/data"), DataPath: getEnv("DATA_PATH", "/data"),
ListenPort: getEnv("LISTEN_PORT", "8585"), ListenPort: getEnv("LISTEN_PORT", "8585"),

35
config/config_test.go Normal file
View File

@ -0,0 +1,35 @@
package config
import "testing"
func TestLoadConfig(t *testing.T) {
conf := Load()
want := "sqlite"
if conf.DBType != want {
t.Fatalf(`Load().DBType = %q, want match for %#q, nil`, conf.DBType, want)
}
}
func TestGetEnvDefault(t *testing.T) {
want := "def_val"
envDefault := getEnv("DEFAULT_TEST", want)
if envDefault != want {
t.Fatalf(`getEnv("DEFAULT_TEST", "def_val") = %q, want match for %#q, nil`, envDefault, want)
}
}
func TestGetEnvSet(t *testing.T) {
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) {
want := "trimtest"
output := trimLowerString(" trimTest ")
if output != want {
t.Fatalf(`trimLowerString(" trimTest ") = %q, want match for %#q, nil`, output, want)
}
}

View File

@ -6,14 +6,9 @@ import (
_ "embed" _ "embed"
"fmt" "fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
_ "modernc.org/sqlite"
"path" "path"
"reichard.io/bbank/config" "reichard.io/bbank/config"
// CGO SQLite
// sqlite "github.com/mattn/go-sqlite3"
// GO SQLite
_ "modernc.org/sqlite"
) )
type DBManager struct { type DBManager struct {
@ -35,9 +30,12 @@ func NewMgr(c *config.Config) *DBManager {
} }
// Create Database // Create Database
if c.DBType == "sqlite" { if c.DBType == "sqlite" || c.DBType == "memory" {
// GO SQLite var dbLocation string = ":memory:"
dbLocation := path.Join(c.ConfigPath, fmt.Sprintf("%s.db", c.DBName)) if c.DBType == "sqlite" {
dbLocation = path.Join(c.ConfigPath, fmt.Sprintf("%s.db", c.DBName))
}
var err error var err error
dbm.DB, err = sql.Open("sqlite", dbLocation) dbm.DB, err = sql.Open("sqlite", dbLocation)
if err != nil { if err != nil {
@ -49,19 +47,6 @@ func NewMgr(c *config.Config) *DBManager {
if _, err := dbm.DB.Exec(ddl, nil); err != nil { if _, err := dbm.DB.Exec(ddl, nil); err != nil {
log.Info("Exec Error:", err) log.Info("Exec Error:", err)
} }
// CGO SQLite
// sql.Register("sqlite3_custom", &sqlite.SQLiteDriver{
// ConnectHook: connectHookSQLite,
// })
// dbLocation := path.Join(c.ConfigPath, fmt.Sprintf("%s.db", c.DBName))
// var err error
// dbm.DB, err = sql.Open("sqlite3_custom", dbLocation)
// if err != nil {
// log.Fatal(err)
// }
} else { } else {
log.Fatal("Unsupported Database") log.Fatal("Unsupported Database")
} }
@ -77,15 +62,3 @@ func (dbm *DBManager) CacheTempTables() error {
} }
return nil return nil
} }
/*
// CGO SQLite
func connectHookSQLite(conn *sqlite.SQLiteConn) error {
// Create Tables
log.Debug("Creating Schema")
if _, err := conn.Exec(ddl, nil); err != nil {
log.Warn("Create Schema Failure: ", err)
}
return nil
}
*/

213
database/manager_test.go Normal file
View File

@ -0,0 +1,213 @@
package database
import (
"testing"
"time"
"reichard.io/bbank/config"
)
type databaseTest struct {
*testing.T
dbm *DBManager
}
var userID string = "testUser"
var userPass string = "testPass"
var deviceID string = "testDevice"
var deviceName string = "testDeviceName"
var documentID string = "testDocument"
var documentTitle string = "testTitle"
var documentAuthor string = "testAuthor"
func TestNewMgr(t *testing.T) {
cfg := config.Config{
DBType: "memory",
}
dbm := NewMgr(&cfg)
if dbm == nil {
t.Fatalf(`Expected: *DBManager, Got: nil`)
}
t.Run("Database", func(t *testing.T) {
dt := databaseTest{t, dbm}
dt.TestUser()
dt.TestDocument()
dt.TestDevice()
dt.TestActivity()
dt.TestDailyReadStats()
})
}
func (dt *databaseTest) TestUser() {
dt.Run("User", func(t *testing.T) {
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
ID: userID,
Pass: &userPass,
})
if err != nil || changed != 1 {
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, 1, changed, err)
}
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)
}
})
}
func (dt *databaseTest) TestDocument() {
dt.Run("Document", func(t *testing.T) {
doc, err := dt.dbm.Queries.UpsertDocument(dt.dbm.Ctx, UpsertDocumentParams{
ID: documentID,
Title: &documentTitle,
Author: &documentAuthor,
})
if err != nil {
t.Fatalf(`Expected: Document, Got: %v, Error: %v`, doc, err)
}
if doc.ID != documentID {
t.Fatalf(`Expected: %v, Got: %v`, documentID, doc.ID)
}
if *doc.Title != documentTitle {
t.Fatalf(`Expected: %v, Got: %v`, documentTitle, *doc.Title)
}
if *doc.Author != documentAuthor {
t.Fatalf(`Expected: %v, Got: %v`, documentAuthor, *doc.Author)
}
})
}
func (dt *databaseTest) TestDevice() {
dt.Run("Device", func(t *testing.T) {
device, err := dt.dbm.Queries.UpsertDevice(dt.dbm.Ctx, UpsertDeviceParams{
ID: deviceID,
UserID: userID,
DeviceName: deviceName,
})
if err != nil {
t.Fatalf(`Expected: Device, Got: %v, Error: %v`, device, err)
}
if device.ID != deviceID {
t.Fatalf(`Expected: %v, Got: %v`, deviceID, device.ID)
}
if device.UserID != userID {
t.Fatalf(`Expected: %v, Got: %v`, userID, device.UserID)
}
if device.DeviceName != deviceName {
t.Fatalf(`Expected: %v, Got: %v`, deviceName, device.DeviceName)
}
})
}
func (dt *databaseTest) TestActivity() {
dt.Run("Progress", func(t *testing.T) {
// 10 Activities, 10 Days
end := time.Now()
start := end.AddDate(0, 0, -9)
var counter int64 = 0
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
counter += 1
// Add Item
activity, err := dt.dbm.Queries.AddActivity(dt.dbm.Ctx, AddActivityParams{
DocumentID: documentID,
DeviceID: deviceID,
UserID: userID,
StartTime: d.UTC().Format(time.RFC3339),
Duration: 60,
Page: counter,
Pages: 100,
})
// Validate No Error
if err != nil {
t.Fatalf(`expected: rawactivity, got: %v, error: %v`, activity, err)
}
// Validate Auto Increment Working
if activity.ID != counter {
t.Fatalf(`Expected: %v, Got: %v`, counter, activity.ID)
}
}
// Initiate Cache
if err := dt.dbm.CacheTempTables(); err != nil {
t.Fatalf(`Error: %v`, err)
}
// Validate Exists
existsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
UserID: userID,
Offset: 0,
Limit: 50,
})
if err != nil {
t.Fatalf(`Expected: []GetActivityRow, Got: %v, Error: %v`, existsRows, err)
}
if len(existsRows) != 10 {
t.Fatalf(`Expected: %v, Got: %v`, 10, len(existsRows))
}
// Validate Doesn't Exist
doesntExistsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
UserID: userID,
DocumentID: "unknownDoc",
DocFilter: true,
Offset: 0,
Limit: 50,
})
if err != nil {
t.Fatalf(`Expected: []GetActivityRow, Got: %v, Error: %v`, doesntExistsRows, err)
}
if len(doesntExistsRows) != 0 {
t.Fatalf(`Expected: %v, Got: %v`, 0, len(doesntExistsRows))
}
})
}
func (dt *databaseTest) TestDailyReadStats() {
dt.Run("DailyReadStats", func(t *testing.T) {
readStats, err := dt.dbm.Queries.GetDailyReadStats(dt.dbm.Ctx, userID)
if err != nil {
t.Fatalf(`Expected: []GetDailyReadStatsRow, Got: %v, Error: %v`, readStats, err)
}
// Validate 30 Days Stats
if len(readStats) != 30 {
t.Fatalf(`Expected: %v, Got: %v`, 30, len(readStats))
}
// Validate 1 Minute / Day - Last 10 Days
for i := 0; i < 10; i++ {
stat := readStats[i]
if stat.MinutesRead != 1 {
t.Fatalf(`Day: %v, Expected: %v, Got: %v`, stat.Date, 1, stat.MinutesRead)
}
}
// Validate 0 Minute / Day - Remaining 20 Days
for i := 10; i < 30; i++ {
stat := readStats[i]
if stat.MinutesRead != 0 {
t.Fatalf(`Day: %v, Expected: %v, Got: %v`, stat.Date, 0, stat.MinutesRead)
}
}
})
}

3
go.mod
View File

@ -3,6 +3,7 @@ module reichard.io/bbank
go 1.19 go 1.19
require ( require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
github.com/gabriel-vasile/mimetype v1.4.2 github.com/gabriel-vasile/mimetype v1.4.2
github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271 github.com/gin-contrib/multitemplate v0.0.0-20230212012517-45920c92c271
@ -10,6 +11,7 @@ require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/microcosm-cc/bluemonday v1.0.25 github.com/microcosm-cc/bluemonday v1.0.25
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/net v0.15.0 golang.org/x/net v0.15.0
@ -17,7 +19,6 @@ require (
) )
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/bytedance/sonic v1.10.0 // indirect github.com/bytedance/sonic v1.10.0 // indirect

3
go.sum
View File

@ -146,6 +146,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115 h1:OEAIMYp5l9kJ2kT9UPL5QSUriKIIDhnLmpJTy69sltA=
github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115/go.mod h1:AIVbkIe1G7fpFHiKOdxZnU5p9tFPYNTQyH3H5IrRkGw=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
@ -225,7 +227,6 @@ golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

View File

@ -3,8 +3,6 @@ package graph
import ( import (
"fmt" "fmt"
"math" "math"
"reichard.io/bbank/database"
) )
type SVGGraphPoint struct { type SVGGraphPoint struct {
@ -28,12 +26,12 @@ type SVGBezierOpposedLine struct {
Angle int Angle int
} }
func GetSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) SVGGraphData { func GetSVGGraphData(inputData []int64, svgWidth int, svgHeight int) SVGGraphData {
// Derive Height // Derive Height
var maxHeight int = 0 var maxHeight int = 0
for _, item := range inputData { for _, item := range inputData {
if int(item.MinutesRead) > maxHeight { if int(item) > maxHeight {
maxHeight = int(item.MinutesRead) maxHeight = int(item)
} }
} }
@ -55,7 +53,7 @@ func GetSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, sv
var maxBY int = 0 var maxBY int = 0
var minBX int = 0 var minBX int = 0
for idx, item := range inputData { for idx, item := range inputData {
itemSize := int(float32(item.MinutesRead) * sizeRatio) itemSize := int(float32(item) * sizeRatio)
itemY := svgHeight - itemSize itemY := svgHeight - itemSize
lineX := (idx + 1) * blockOffset lineX := (idx + 1) * blockOffset
barPoints = append(barPoints, SVGGraphPoint{ barPoints = append(barPoints, SVGGraphPoint{

33
graph/graph_test.go Normal file
View File

@ -0,0 +1,33 @@
package graph
import (
"testing"
)
func TestGetSVGGraphData(t *testing.T) {
inputPoints := []int64{10, 90, 50, 5, 10, 5, 70, 60, 50, 90}
svgData := GetSVGGraphData(inputPoints, 500, 100)
expect := "M 50,95 C63,95 80,50 100,50 C120,50 128,73 150,73 C172,73 180,98 200,98 C220,98 230,95 250,95 C270,95 279,98 300,98 C321,98 330,62 350,62 C370,62 380,67 400,67 C420,67 430,73 450,73 C470,73 489,50 500,50"
if svgData.BezierPath != expect {
t.Fatalf(`Expected: %v, Got: %v`, expect, svgData.BezierPath)
}
expect = "L 500,98 L 50,98 Z"
if svgData.BezierFill != expect {
t.Fatalf(`Expected: %v, Got: %v`, expect, svgData.BezierFill)
}
if svgData.Width != 500 {
t.Fatalf(`Expected: %v, Got: %v`, 500, svgData.Width)
}
if svgData.Height != 100 {
t.Fatalf(`Expected: %v, Got: %v`, 100, svgData.Height)
}
if svgData.Offset != 50 {
t.Fatalf(`Expected: %v, Got: %v`, 50, svgData.Offset)
}
}

Binary file not shown.

View File

@ -1,301 +1,35 @@
/*
Package epub provides basic support for reading EPUB archives.
Adapted from: https://github.com/taylorskalyo/goreader
*/
package metadata package metadata
import ( import (
"archive/zip"
"bytes"
"encoding/xml"
"errors"
"io" "io"
"os"
"path"
"strings" "strings"
"github.com/taylorskalyo/goreader/epub"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
const containerPath = "META-INF/container.xml" func countEPUBWords(filepath string) (int64, error) {
rc, err := epub.OpenReader(filepath)
var (
// ErrNoRootfile occurs when there are no rootfile entries found in
// container.xml.
ErrNoRootfile = errors.New("epub: no rootfile found in container")
// ErrBadRootfile occurs when container.xml references a rootfile that does
// not exist in the zip.
ErrBadRootfile = errors.New("epub: container references non-existent rootfile")
// ErrNoItemref occurrs when a content.opf contains a spine without any
// itemref entries.
ErrNoItemref = errors.New("epub: no itemrefs found in spine")
// ErrBadItemref occurs when an itemref entry in content.opf references an
// item that does not exist in the manifest.
ErrBadItemref = errors.New("epub: itemref references non-existent item")
// ErrBadManifest occurs when a manifest in content.opf references an item
// that does not exist in the zip.
ErrBadManifest = errors.New("epub: manifest references non-existent item")
)
// Reader represents a readable epub file.
type Reader struct {
Container
files map[string]*zip.File
}
// ReadCloser represents a readable epub file that can be closed.
type ReadCloser struct {
Reader
f *os.File
}
// Rootfile contains the location of a content.opf package file.
type Rootfile struct {
FullPath string `xml:"full-path,attr"`
Package
}
// Container serves as a directory of Rootfiles.
type Container struct {
Rootfiles []*Rootfile `xml:"rootfiles>rootfile"`
}
// Package represents an epub content.opf file.
type Package struct {
Metadata
Manifest
Spine
}
// Metadata contains publishing information about the epub.
type Metadata struct {
Title string `xml:"metadata>title"`
Language string `xml:"metadata>language"`
Identifier string `xml:"metadata>idenifier"`
Creator string `xml:"metadata>creator"`
Contributor string `xml:"metadata>contributor"`
Publisher string `xml:"metadata>publisher"`
Subject string `xml:"metadata>subject"`
Description string `xml:"metadata>description"`
Event []struct {
Name string `xml:"event,attr"`
Date string `xml:",innerxml"`
} `xml:"metadata>date"`
Type string `xml:"metadata>type"`
Format string `xml:"metadata>format"`
Source string `xml:"metadata>source"`
Relation string `xml:"metadata>relation"`
Coverage string `xml:"metadata>coverage"`
Rights string `xml:"metadata>rights"`
}
// Manifest lists every file that is part of the epub.
type Manifest struct {
Items []Item `xml:"manifest>item"`
}
// Item represents a file stored in the epub.
type Item struct {
ID string `xml:"id,attr"`
HREF string `xml:"href,attr"`
MediaType string `xml:"media-type,attr"`
f *zip.File
}
// Spine defines the reading order of the epub documents.
type Spine struct {
Itemrefs []Itemref `xml:"spine>itemref"`
}
// Itemref points to an Item.
type Itemref struct {
IDREF string `xml:"idref,attr"`
*Item
}
// OpenEPUBReader will open the epub file specified by name and return a
// ReadCloser.
func OpenEPUBReader(name string) (*ReadCloser, error) {
f, err := os.Open(name)
if err != nil { if err != nil {
return nil, err return 0, err
} }
rf := rc.Rootfiles[0]
rc := new(ReadCloser)
rc.f = f
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
z, err := zip.NewReader(f, fi.Size())
if err != nil {
return nil, err
}
if err = rc.init(z); err != nil {
return nil, err
}
return rc, nil
}
// NewReader returns a new Reader reading from ra, which is assumed to have the
// given size in bytes.
func NewReader(ra io.ReaderAt, size int64) (*Reader, error) {
z, err := zip.NewReader(ra, size)
if err != nil {
return nil, err
}
r := new(Reader)
if err = r.init(z); err != nil {
return nil, err
}
return r, nil
}
func (r *Reader) init(z *zip.Reader) error {
// Create a file lookup table
r.files = make(map[string]*zip.File)
for _, f := range z.File {
r.files[f.Name] = f
}
err := r.setContainer()
if err != nil {
return err
}
err = r.setPackages()
if err != nil {
return err
}
err = r.setItems()
if err != nil {
return err
}
return nil
}
// setContainer unmarshals the epub's container.xml file.
func (r *Reader) setContainer() error {
f, err := r.files[containerPath].Open()
if err != nil {
return err
}
var b bytes.Buffer
_, err = io.Copy(&b, f)
if err != nil {
return err
}
err = xml.Unmarshal(b.Bytes(), &r.Container)
if err != nil {
return err
}
if len(r.Container.Rootfiles) < 1 {
return ErrNoRootfile
}
return nil
}
// setPackages unmarshal's each of the epub's content.opf files.
func (r *Reader) setPackages() error {
for _, rf := range r.Container.Rootfiles {
if r.files[rf.FullPath] == nil {
return ErrBadRootfile
}
f, err := r.files[rf.FullPath].Open()
if err != nil {
return err
}
var b bytes.Buffer
_, err = io.Copy(&b, f)
if err != nil {
return err
}
err = xml.Unmarshal(b.Bytes(), &rf.Package)
if err != nil {
return err
}
}
return nil
}
// setItems associates Itemrefs with their respective Item and Items with
// their zip.File.
func (r *Reader) setItems() error {
itemrefCount := 0
for _, rf := range r.Container.Rootfiles {
itemMap := make(map[string]*Item)
for i := range rf.Manifest.Items {
item := &rf.Manifest.Items[i]
itemMap[item.ID] = item
abs := path.Join(path.Dir(rf.FullPath), item.HREF)
item.f = r.files[abs]
}
for i := range rf.Spine.Itemrefs {
itemref := &rf.Spine.Itemrefs[i]
itemref.Item = itemMap[itemref.IDREF]
if itemref.Item == nil {
return ErrBadItemref
}
}
itemrefCount += len(rf.Spine.Itemrefs)
}
if itemrefCount < 1 {
return ErrNoItemref
}
return nil
}
// Open returns a ReadCloser that provides access to the Items's contents.
// Multiple items may be read concurrently.
func (item *Item) Open() (r io.ReadCloser, err error) {
if item.f == nil {
return nil, ErrBadManifest
}
return item.f.Open()
}
// Close closes the epub file, rendering it unusable for I/O.
func (rc *ReadCloser) Close() {
rc.f.Close()
}
// Hehe
func (rf *Rootfile) CountWords() int64 {
var completeCount int64 var completeCount int64
for _, item := range rf.Spine.Itemrefs { for _, item := range rf.Spine.Itemrefs {
f, _ := item.Open() f, _ := item.Open()
tokenizer := html.NewTokenizer(f) tokenizer := html.NewTokenizer(f)
completeCount = completeCount + countWords(*tokenizer) newCount, err := countTokenizerWords(*tokenizer)
if err != nil {
return 0, err
}
completeCount = completeCount + newCount
} }
return completeCount return completeCount, nil
} }
func countWords(tokenizer html.Tokenizer) int64 { func countTokenizerWords(tokenizer html.Tokenizer) (int64, error) {
var err error var err error
var totalWords int64 var totalWords int64
for { for {
@ -308,23 +42,9 @@ func countWords(tokenizer html.Tokenizer) int64 {
err = tokenizer.Err() err = tokenizer.Err()
} }
if err == io.EOF { if err == io.EOF {
return totalWords return totalWords, nil
} else if err != nil { } else if err != nil {
return 0 return 0, err
} }
} }
} }
/*
func main() {
rc, err := OpenEPUBReader("test.epub")
if err != nil {
log.Fatal(err)
}
rf := rc.Rootfiles[0]
totalWords := rf.CountWords()
log.Info("WOAH WORDS:", totalWords)
}
*/

View File

@ -0,0 +1,76 @@
//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"
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)
}
}

View File

@ -58,13 +58,10 @@ func GetWordCount(filepath string) (int64, error) {
} }
if fileExtension := fileMime.Extension(); fileExtension == ".epub" { if fileExtension := fileMime.Extension(); fileExtension == ".epub" {
rc, err := OpenEPUBReader(filepath) totalWords, err := countEPUBWords(filepath)
if err != nil { if err != nil {
return 0, err return 0, err
} }
rf := rc.Rootfiles[0]
totalWords := rf.CountWords()
return totalWords, nil return totalWords, nil
} else { } else {
return 0, errors.New("Invalid Extension") return 0, errors.New("Invalid Extension")

14
metadata/metadata_test.go Normal file
View File

@ -0,0 +1,14 @@
package metadata
import (
"testing"
)
func TestGetWordCount(t *testing.T) {
var want int64 = 30477
wordCount, err := countEPUBWords("./_test_files/alice.epub")
if wordCount != want {
t.Fatalf(`Expected: %v, Got: %v, Error: %v`, want, wordCount, err)
}
}