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 uint16 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) 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 }