search for a book with a single value
This commit is contained in:
parent
1c6b758f0c
commit
42a3181797
1
go.mod
1
go.mod
@ -7,5 +7,6 @@ require github.com/rs/zerolog v1.33.0
|
|||||||
require (
|
require (
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -5,6 +5,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package home
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"librapi/services"
|
||||||
"librapi/templates"
|
"librapi/templates"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -11,11 +12,35 @@ import (
|
|||||||
|
|
||||||
const URL = "/"
|
const URL = "/"
|
||||||
|
|
||||||
func Handler() func(http.ResponseWriter, *http.Request) {
|
type SearchField struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Err string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchForm struct {
|
||||||
|
Search SearchField
|
||||||
|
Error error
|
||||||
|
Method string
|
||||||
|
Results []services.BookMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSearchForm() SearchForm {
|
||||||
|
return SearchForm{
|
||||||
|
Search: SearchField{
|
||||||
|
Name: "search",
|
||||||
|
},
|
||||||
|
Method: http.MethodPost,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(bs services.IStore) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
getHome(w, r)
|
getHome(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
postHome(w, r, bs)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
@ -26,7 +51,45 @@ func getHome(w http.ResponseWriter, _ *http.Request) {
|
|||||||
home := templates.GetHome()
|
home := templates.GetHome()
|
||||||
|
|
||||||
buf := bytes.NewBufferString("")
|
buf := bytes.NewBufferString("")
|
||||||
if err := home.Execute(buf, nil); err != nil {
|
if err := home.Execute(buf, &SearchForm{}); err != nil {
|
||||||
|
log.Err(err).Msg("unable to generate template")
|
||||||
|
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSearchForm(r *http.Request) SearchForm {
|
||||||
|
sf := NewSearchForm()
|
||||||
|
|
||||||
|
sf.Search.Value = r.FormValue(sf.Search.Name)
|
||||||
|
|
||||||
|
return sf
|
||||||
|
}
|
||||||
|
|
||||||
|
func postHome(w http.ResponseWriter, r *http.Request, bs services.IStore) {
|
||||||
|
home := templates.GetHome()
|
||||||
|
buf := bytes.NewBufferString("")
|
||||||
|
|
||||||
|
sf := extractSearchForm(r)
|
||||||
|
bms, err := bs.Search(sf.Search.Value)
|
||||||
|
if err != nil {
|
||||||
|
sf.Error = err
|
||||||
|
if err := home.Execute(buf, sf); err != nil {
|
||||||
|
log.Err(err).Msg("unable to generate template")
|
||||||
|
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sf.Results = bms
|
||||||
|
|
||||||
|
if err := home.Execute(buf, sf); err != nil {
|
||||||
log.Err(err).Msg("unable to generate template")
|
log.Err(err).Msg("unable to generate template")
|
||||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -104,6 +104,26 @@ func (bf *BookForm) IsSuccess() bool {
|
|||||||
return bf.Method == http.MethodPost && bf.Error == "" && !bf.HasErrors()
|
return bf.Method == http.MethodPost && bf.Error == "" && !bf.HasErrors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bf *BookForm) IntoMetadata() *services.BookMetadata {
|
||||||
|
bm := &services.BookMetadata{
|
||||||
|
Name: bf.Name.Value,
|
||||||
|
Editor: bf.Editor.Value,
|
||||||
|
Authors: bf.Authors.Value,
|
||||||
|
Year: uint16(bf.Year.Value),
|
||||||
|
Keywords: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc := bf.Description.Value; desc != "" {
|
||||||
|
bm.Description = &desc
|
||||||
|
}
|
||||||
|
|
||||||
|
if keywords := bf.Keywords.Value; len(keywords) > 0 {
|
||||||
|
bm.Keywords = keywords
|
||||||
|
}
|
||||||
|
|
||||||
|
return bm
|
||||||
|
}
|
||||||
|
|
||||||
func Handler(a services.IAuthenticate, s services.IStore) func(http.ResponseWriter, *http.Request) {
|
func Handler(a services.IAuthenticate, s services.IStore) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@ -203,10 +223,11 @@ func postUploadFile(w http.ResponseWriter, r *http.Request, a services.IAuthenti
|
|||||||
filename := bf.File.Value.GetFilename()
|
filename := bf.File.Value.GetFilename()
|
||||||
log.Info().Str("filename", filename).Msg("file is uploading...")
|
log.Info().Str("filename", filename).Msg("file is uploading...")
|
||||||
|
|
||||||
if err := s.Save(filename, bf.File.Value.file); err != nil {
|
if err := s.Save(bf.IntoMetadata(), bf.File.Value.file); err != nil {
|
||||||
if err := uploadForm.Execute(buf, &BookForm{Error: err.Error()}); err != nil {
|
if err := uploadForm.Execute(buf, &BookForm{Error: err.Error()}); err != nil {
|
||||||
log.Err(err).Msg("unable to generate template")
|
log.Err(err).Msg("unable to generate template")
|
||||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|||||||
2
main.go
2
main.go
@ -32,7 +32,7 @@ func main() {
|
|||||||
srv := server.NewServer(
|
srv := server.NewServer(
|
||||||
ctx,
|
ctx,
|
||||||
services.GetEnv().GetPort(),
|
services.GetEnv().GetPort(),
|
||||||
server.NewHandler(home.URL, home.Handler()),
|
server.NewHandler(home.URL, home.Handler(bs)),
|
||||||
server.NewHandler(upload.URL, upload.Handler(auth, bs)),
|
server.NewHandler(upload.URL, upload.Handler(auth, bs)),
|
||||||
server.NewHandler(login.URL, login.Handler(auth)),
|
server.NewHandler(login.URL, login.Handler(auth)),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,14 +22,89 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IStore interface {
|
type IStore interface {
|
||||||
Save(name string, content io.ReadCloser) error
|
Save(bm *BookMetadata, content io.ReadCloser) error
|
||||||
|
Search(value string) ([]BookMetadata, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ IStore = (*BookStore)(nil)
|
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 {
|
type BookStore struct {
|
||||||
processing *sync.WaitGroup
|
processing *sync.WaitGroup
|
||||||
dir string
|
dir string
|
||||||
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBookStore(dir string) *BookStore {
|
func NewBookStore(dir string) *BookStore {
|
||||||
@ -31,29 +112,71 @@ func NewBookStore(dir string) *BookStore {
|
|||||||
log.Fatal().Err(err).Msg("unable to create store dir")
|
log.Fatal().Err(err).Msg("unable to create store dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BookStore{
|
bs := &BookStore{
|
||||||
processing: &sync.WaitGroup{},
|
processing: &sync.WaitGroup{},
|
||||||
dir: dir,
|
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{} {
|
func (bs *BookStore) Done() <-chan struct{} {
|
||||||
chDone := make(chan struct{})
|
chDone := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
bs.processing.Wait()
|
bs.processing.Wait()
|
||||||
|
if err := bs.db.Close(); err != nil {
|
||||||
|
log.Err(err).Msg("unable to close the database")
|
||||||
|
}
|
||||||
chDone <- struct{}{}
|
chDone <- struct{}{}
|
||||||
log.Info().Msg("book store processing done")
|
log.Info().Msg("book store processing done")
|
||||||
}()
|
}()
|
||||||
return chDone
|
return chDone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs *BookStore) Save(name string, content io.ReadCloser) error {
|
func (bs *BookStore) Save(bm *BookMetadata, content io.ReadCloser) error {
|
||||||
bs.processing.Add(1)
|
bs.processing.Add(1)
|
||||||
defer bs.processing.Done()
|
defer bs.processing.Done()
|
||||||
|
|
||||||
defer content.Close()
|
defer content.Close()
|
||||||
|
|
||||||
dst, err := os.Create(filepath.Join(GetEnv().GetDir(), name))
|
bm.Path = filepath.Join(GetEnv().GetDir(), bm.getFormattedName())
|
||||||
|
|
||||||
|
dst, err := os.Create(bm.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg(ErrFileCreation.Error())
|
log.Err(err).Msg(ErrFileCreation.Error())
|
||||||
return ErrFileCreation
|
return ErrFileCreation
|
||||||
@ -65,5 +188,45 @@ func (bs *BookStore) Save(name string, content io.ReadCloser) error {
|
|||||||
return ErrFileCopy
|
return ErrFileCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultAPISessionExpirationDuration = 30 * time.Second
|
defaultAPISessionExpirationDuration = 5 * 60 * time.Second
|
||||||
defaultPort = 8585
|
defaultPort = 8585
|
||||||
defaultMainDir = "./store"
|
defaultMainDir = "./store"
|
||||||
)
|
)
|
||||||
|
|||||||
9
services/sql/create_books_table.sql
Normal file
9
services/sql/create_books_table.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
create table if not exists books (
|
||||||
|
name text primary key,
|
||||||
|
description text,
|
||||||
|
editor text not null,
|
||||||
|
authors jsonb not null,
|
||||||
|
year int not null,
|
||||||
|
keywords jsonb,
|
||||||
|
path text not null
|
||||||
|
)
|
||||||
2
services/sql/insert_book.sql
Normal file
2
services/sql/insert_book.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
insert into books(name, description, editor, authors, year, keywords, path)
|
||||||
|
values (?,?,?,?,?,?,?)
|
||||||
20
services/sql/search_books.sql
Normal file
20
services/sql/search_books.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
select
|
||||||
|
b.*
|
||||||
|
from
|
||||||
|
books b
|
||||||
|
where
|
||||||
|
lower(b.name) like lower(?)
|
||||||
|
or lower(b.description) like lower(?)
|
||||||
|
or lower(b.editor) like lower(?)
|
||||||
|
or lower(b.year) like lower(?)
|
||||||
|
union
|
||||||
|
select
|
||||||
|
b2.*
|
||||||
|
from books b2, json_each(b2.authors)
|
||||||
|
where lower(json_each.value) like lower(?)
|
||||||
|
union
|
||||||
|
select
|
||||||
|
b3.*
|
||||||
|
from books b3, json_each(b3.keywords)
|
||||||
|
where lower(json_each.value) like lower(?)
|
||||||
|
|
||||||
@ -1,4 +1,46 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h3>A simple API to store, search and download books.</h3>
|
<h3>A simple API to store, search and download books.</h3>
|
||||||
<p>No extra JS, CSS or fancy stuff. You click, you search, you found, you're happy.</p>
|
<div>No extra JS, CSS or fancy stuff. You click, you search, you found, you're happy.</div>
|
||||||
|
<form action="/home" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="form-item">
|
||||||
|
<div class="form-container">
|
||||||
|
<h4>Search</h4>
|
||||||
|
</div>
|
||||||
|
<div class="form-container">
|
||||||
|
<input type="text" name="search" value="{{.Search.Value}}" />
|
||||||
|
</div>
|
||||||
|
{{ if .Search.Err }}
|
||||||
|
<div class="error">{{.Search.Err}}</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<div class="form-container">
|
||||||
|
<button id="submit" type="submit">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ if ne (errStr .Error) "" }}
|
||||||
|
<div class="error">{{.Error | errStr}}</div>
|
||||||
|
{{ end }}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Editor</th>
|
||||||
|
<th>Authors</th>
|
||||||
|
<th>Year</th>
|
||||||
|
</tr>
|
||||||
|
{{range .Results}}
|
||||||
|
<tr>
|
||||||
|
<td><div style="margin: 10px;">{{.Name}}</div></td>
|
||||||
|
<td style="text-align: center;"><div style="margin: 10px;">{{.Editor}}</div></td>
|
||||||
|
<td style="text-align: center;"><div style="margin: 10px;">{{.Authors | join }}</div></td>
|
||||||
|
<td><div style="margin: 20px;">{{.Year}}</div></td>
|
||||||
|
<td><div style="margin: 20px;">{{.Path | bookUrl}}</div></td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@ -2,8 +2,10 @@ package templates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -52,6 +54,10 @@ var funcMap = template.FuncMap{
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
|
"bookUrl": func(path string) string {
|
||||||
|
_, filename := filepath.Split(path)
|
||||||
|
return fmt.Sprintf("https://books.thegux.fr/downloads/%s", filename)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var homeTmpl = sync.OnceValue[*template.Template](func() *template.Template {
|
var homeTmpl = sync.OnceValue[*template.Template](func() *template.Template {
|
||||||
@ -60,7 +66,7 @@ var homeTmpl = sync.OnceValue[*template.Template](func() *template.Template {
|
|||||||
log.Fatal().Err(err).Msg("unable to parse base tmpl")
|
log.Fatal().Err(err).Msg("unable to parse base tmpl")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := baseTmpl.New("home").Parse(home); err != nil {
|
if _, err := baseTmpl.New("home").Funcs(funcMap).Parse(home); err != nil {
|
||||||
log.Fatal().Err(err).Msg("unable to parse home tmpl")
|
log.Fatal().Err(err).Msg("unable to parse home tmpl")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user