librapi/services/book_store.go
2025-01-07 09:29:21 +01:00

233 lines
4.8 KiB
Go

package services
import (
"database/sql"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
)
var (
ErrFileCreation = errors.New("unexpected error occurred while creating book file")
ErrFileCopy = errors.New("unexpected error occurred while copying book file")
)
type IStore interface {
Save(bm *BookMetadata, content io.ReadCloser) error
Search(value string) ([]BookMetadata, error)
}
var _ IStore = (*BookStore)(nil)
//go:embed sql/create_books_table.sql
var sqlCreateTable string
//go:embed sql/insert_book.sql
var sqlInsertBook string
//go:embed sql/search_books.sql
var sqlSearchBooks string
type BookMetadata struct {
Name string
Description *string
Editor string
Authors []string
Year uint16
Keywords []string
Path string
}
func (bm *BookMetadata) getAuthors() (string, error) {
authors, err := json.Marshal(bm.Authors)
if err != nil {
log.Err(err).Msg("unable to marshal authors")
return "", err
}
return string(authors), nil
}
func (bm *BookMetadata) getKeywords() (*string, error) {
if bm.Keywords == nil {
return nil, nil
}
keywords, err := json.Marshal(bm.Keywords)
if err != nil {
log.Err(err).Msg("unable to marshal keywords")
return nil, err
}
strKeywords := string(keywords)
return &strKeywords, nil
}
func (bm *BookMetadata) getFormattedName() string {
name := strings.ReplaceAll(strings.ToLower(bm.Name), " ", "-")
editor := strings.ReplaceAll(strings.ToLower(bm.Editor), " ", "-")
author := strings.ReplaceAll(strings.ToLower(bm.Authors[0]), " ", "-")
return fmt.Sprintf("%s_%s_%s_%d.pdf", name, editor, author, bm.Year)
}
func (bm *BookMetadata) intoStmtValues() ([]any, error) {
authors, err := bm.getAuthors()
if err != nil {
return nil, err
}
keywords, err := bm.getKeywords()
if err != nil {
log.Err(err).Msg("unable to marshal authors")
return nil, err
}
return []any{
bm.Name,
bm.Description,
bm.Editor,
authors,
bm.Year,
keywords,
bm.Path,
}, nil
}
type BookStore struct {
processing *sync.WaitGroup
dir string
db *sql.DB
}
func NewBookStore(dir string) *BookStore {
if err := os.MkdirAll(dir, 0755); err != nil { //nolint
log.Fatal().Err(err).Msg("unable to create store dir")
}
bs := &BookStore{
processing: &sync.WaitGroup{},
dir: dir,
}
bs.init(dir)
return bs
}
func (bs *BookStore) init(dir string) {
db, err := sql.Open("sqlite3", filepath.Join(dir, "librapi.db"))
if err != nil {
log.Fatal().Err(err).Msg("unable initialize sqlite3 database")
}
if _, err := db.Exec(sqlCreateTable); err != nil {
log.Fatal().Err(err).Msg("unable to create books table")
}
bs.db = db
}
func (bs *BookStore) store(bm *BookMetadata) error {
stmt, err := bs.db.Prepare(sqlInsertBook)
if err != nil {
log.Err(err).Msg("unable to parse insert book sql")
return err
}
defer stmt.Close()
values, err := bm.intoStmtValues()
if err != nil {
return err
}
if _, err := stmt.Exec(values...); err != nil {
log.Err(err).Msg("unable to insert new book")
return err
}
return nil
}
func (bs *BookStore) Done() <-chan struct{} {
chDone := make(chan struct{})
go func() {
bs.processing.Wait()
if err := bs.db.Close(); err != nil {
log.Err(err).Msg("unable to close the database")
}
chDone <- struct{}{}
log.Info().Msg("book store processing done")
}()
return chDone
}
func (bs *BookStore) Save(bm *BookMetadata, content io.ReadCloser) error {
bs.processing.Add(1)
defer bs.processing.Done()
defer content.Close()
bm.Path = filepath.Join(GetEnv().GetDir(), bm.getFormattedName())
dst, err := os.Create(bm.Path)
if err != nil {
log.Err(err).Msg(ErrFileCreation.Error())
return ErrFileCreation
}
defer dst.Close()
if _, err := io.Copy(dst, content); err != nil {
log.Err(err).Msg(ErrFileCopy.Error())
return ErrFileCopy
}
return bs.store(bm)
}
func (bs *BookStore) Search(value string) ([]BookMetadata, error) {
stmt, err := bs.db.Prepare(sqlSearchBooks)
if err != nil {
log.Err(err).Msg("unable to parse search book sql")
return nil, err
}
defer stmt.Close()
values := []any{}
for i := 0; i <= 5; i++ {
values = append(values, fmt.Sprintf("%%%s%%", value))
}
rows, err := stmt.Query(values...)
if err != nil {
log.Err(err).Msg("unable to query books")
return nil, err
}
bms := []BookMetadata{}
for rows.Next() {
var bm BookMetadata
var authors string
var keyword *string
if err := rows.Scan(&bm.Name, &bm.Description, &bm.Editor, &authors, &bm.Year, &keyword, &bm.Path); err != nil {
log.Err(err).Msg("unable to scan row")
return nil, err
}
var authorsSlice []string
if err := json.Unmarshal([]byte(authors), &authorsSlice); err != nil {
return nil, err
}
bm.Authors = authorsSlice
bms = append(bms, bm)
}
return bms, nil
}