238 lines
4.9 KiB
Go
238 lines
4.9 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)
|
|
GetStoreDir() string
|
|
}
|
|
|
|
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) GetStoreDir() string {
|
|
return bs.dir
|
|
}
|
|
|
|
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(bs.dir, 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
|
|
}
|