278 lines
5.6 KiB
Go
278 lines
5.6 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"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 resource file")
|
|
ErrFileCopy = errors.New("unexpected error occurred while copying resource file")
|
|
)
|
|
|
|
type IStore interface {
|
|
Create(r *Resource) error
|
|
Search(value string) ([]Resource, error)
|
|
GetDir() string
|
|
}
|
|
|
|
var _ IStore = (*Store)(nil)
|
|
|
|
const (
|
|
sqlSearchResources = `
|
|
select
|
|
r.*
|
|
from
|
|
resources r
|
|
where
|
|
lower(r.name) like lower(?)
|
|
or lower(r.description) like lower(?)
|
|
or lower(r.editor) like lower(?)
|
|
or lower(r.year) like lower(?)
|
|
union
|
|
select
|
|
r2.*
|
|
from resources r2, json_each(r2.authors)
|
|
where lower(json_each.value) like lower(?)
|
|
union
|
|
select
|
|
r3.*
|
|
from resources r3, json_each(r3.keywords)
|
|
where lower(json_each.value) like lower(?)
|
|
`
|
|
sqlInsertResource = "insert into resources(name, description, editor, authors, year, keywords, path) values (?,?,?,?,?,?,?)"
|
|
sqlCreateResourceTable = `
|
|
create table if not exists resources (
|
|
name text primary key,
|
|
description text,
|
|
editor text not null,
|
|
authors jsonb not null,
|
|
year int not null,
|
|
keywords jsonb,
|
|
path text not null
|
|
)
|
|
`
|
|
)
|
|
|
|
func sanitizeFilename(name string) string {
|
|
return strings.ReplaceAll(
|
|
strings.ReplaceAll(
|
|
strings.ToLower(name), " ", "-",
|
|
),
|
|
"'", "",
|
|
)
|
|
}
|
|
|
|
type Resource struct {
|
|
Name string
|
|
Description *string
|
|
Editor string
|
|
Authors []string
|
|
Year int
|
|
Keywords []string
|
|
Content *bytes.Buffer
|
|
Path string
|
|
}
|
|
|
|
func (r *Resource) getAuthors() (string, error) {
|
|
authors, err := json.Marshal(r.Authors)
|
|
if err != nil {
|
|
log.Err(err).Msg("unable to marshal authors")
|
|
return "", err
|
|
}
|
|
return string(authors), nil
|
|
}
|
|
|
|
func (r *Resource) getKeywords() (*string, error) {
|
|
if r.Keywords == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
keywords, err := json.Marshal(r.Keywords)
|
|
if err != nil {
|
|
log.Err(err).Msg("unable to marshal keywords")
|
|
return nil, err
|
|
}
|
|
strKeywords := string(keywords)
|
|
return &strKeywords, nil
|
|
}
|
|
|
|
func (r *Resource) getFormattedName() string {
|
|
name := sanitizeFilename(r.Name)
|
|
editor := sanitizeFilename(r.Editor)
|
|
author := sanitizeFilename((r.Authors[0]))
|
|
|
|
return fmt.Sprintf("%s_%s_%s_%d.pdf", name, editor, author, r.Year)
|
|
}
|
|
|
|
func (r *Resource) intoStmtValues() ([]any, error) {
|
|
authors, err := r.getAuthors()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keywords, err := r.getKeywords()
|
|
if err != nil {
|
|
log.Err(err).Msg("unable to marshal authors")
|
|
return nil, err
|
|
}
|
|
|
|
return []any{
|
|
r.Name,
|
|
r.Description,
|
|
r.Editor,
|
|
authors,
|
|
r.Year,
|
|
keywords,
|
|
r.Path,
|
|
}, nil
|
|
}
|
|
|
|
type Store struct {
|
|
processing *sync.WaitGroup
|
|
dir string
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewStore(dir string) *Store {
|
|
if err := os.MkdirAll(dir, 0755); err != nil { //nolint
|
|
log.Fatal().Err(err).Msg("unable to create store dir")
|
|
}
|
|
|
|
s := &Store{
|
|
processing: &sync.WaitGroup{},
|
|
dir: dir,
|
|
}
|
|
|
|
s.init(dir)
|
|
|
|
log.Info().Str("dir", s.dir).Msg("store initialized")
|
|
return s
|
|
}
|
|
|
|
func (s *Store) 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(sqlCreateResourceTable); err != nil {
|
|
log.Fatal().Err(err).Msg("unable to create resources table")
|
|
}
|
|
|
|
s.db = db
|
|
}
|
|
|
|
func (s *Store) createRessource(r *Resource) error {
|
|
stmt, err := s.db.Prepare(sqlInsertResource)
|
|
if err != nil {
|
|
log.Err(err).Msg("unable to parse insert resource sql")
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
values, err := r.intoStmtValues()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := stmt.Exec(values...); err != nil {
|
|
log.Err(err).Msg("unable to insert new resource")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Done() <-chan struct{} {
|
|
chDone := make(chan struct{})
|
|
go func() {
|
|
s.processing.Wait()
|
|
if err := s.db.Close(); err != nil {
|
|
log.Err(err).Msg("unable to close the database")
|
|
}
|
|
chDone <- struct{}{}
|
|
log.Info().Msg("resource store processing done")
|
|
}()
|
|
return chDone
|
|
}
|
|
|
|
func (s *Store) GetDir() string {
|
|
return s.dir
|
|
}
|
|
|
|
func (s *Store) Create(r *Resource) error {
|
|
s.processing.Add(1)
|
|
defer s.processing.Done()
|
|
|
|
path := filepath.Join(s.dir, r.getFormattedName())
|
|
r.Path = path
|
|
|
|
dst, err := os.Create(path)
|
|
if err != nil {
|
|
log.Err(err).Msg(ErrFileCreation.Error())
|
|
return ErrFileCreation
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err := io.Copy(dst, r.Content); err != nil {
|
|
log.Err(err).Msg(ErrFileCopy.Error())
|
|
return ErrFileCopy
|
|
}
|
|
|
|
return s.createRessource(r)
|
|
}
|
|
|
|
func (s *Store) Search(value string) ([]Resource, error) {
|
|
stmt, err := s.db.Prepare(sqlSearchResources)
|
|
if err != nil {
|
|
log.Err(err).Msg("unable to parse search resource 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 resources")
|
|
return nil, err
|
|
}
|
|
|
|
resources := []Resource{}
|
|
for rows.Next() {
|
|
var resource Resource
|
|
var authors string
|
|
var keyword *string
|
|
if err := rows.Scan(&resource.Name, &resource.Description, &resource.Editor, &authors, &resource.Year, &keyword, &resource.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 {
|
|
log.Err(err).Msg("unable to unmarshal authors into slice")
|
|
return nil, err
|
|
}
|
|
resource.Authors = authorsSlice
|
|
resources = append(resources, resource)
|
|
}
|
|
|
|
return resources, nil
|
|
}
|