diff --git a/forms/forms.go b/forms/forms.go new file mode 100644 index 0000000..b9e73fe --- /dev/null +++ b/forms/forms.go @@ -0,0 +1,247 @@ +package forms + +import ( + "errors" + "librapi/services" + "mime/multipart" + "net/http" + "strconv" + "strings" + + "github.com/rs/zerolog/log" +) + +const MaxFileSize = 200 // in MB + +var ( + ErrInvalidUsername = errors.New("username must not be empty") + ErrInvalidPassword = errors.New("password must not be empty") + ErrInvalidCredentials = errors.New("bad credentials") + + ErrInvalidName = errors.New("book name must not be empty") + ErrInvalidEditor = errors.New("book editor must not be empty") + ErrInvalidYear = errors.New("invalid year, unable to parse") + ErrInvalidAuthors = errors.New("must at least contains one author") + ErrFileMaxSizeReached = errors.New("max file size reached, must be <= 200MB") + ErrFileOpen = errors.New("unable to open file from form") +) + +type StrList = []string + +type FormFieldType interface { + int | string | StrList | UploadFile +} + +type FormField[T FormFieldType] struct { + Name string + Value T + Err string +} + +type UploadFile struct { + File multipart.File + Header *multipart.FileHeader +} + +func (uf *UploadFile) GetFilename() string { + return uf.Header.Filename +} + +func (uf *UploadFile) CheckSize() error { + if uf.Header.Size > (MaxFileSize << 20) { + return ErrFileMaxSizeReached + } + return nil +} + +type UploadForm struct { + Name FormField[string] + Description FormField[string] + Editor FormField[string] + Authors FormField[StrList] + Year FormField[int] + Keywords FormField[StrList] + File FormField[UploadFile] + Error string + Method string +} + +func UploadFormFromRequest(r *http.Request) UploadForm { + uf := NewUploadForm() + + name := r.FormValue(uf.Name.Name) + if name == "" { + uf.Name.Err = ErrInvalidName.Error() + } + uf.Name.Value = name + + uf.Description.Value = r.FormValue(uf.Description.Name) + + editor := r.FormValue(uf.Editor.Name) + if editor == "" { + uf.Editor.Err = ErrInvalidEditor.Error() + } + uf.Editor.Value = editor + + if a := r.FormValue(uf.Authors.Name); a != "" { + uf.Authors.Value = strings.Split(a, ",") + } else { + uf.Authors.Err = ErrInvalidAuthors.Error() + } + + year, errParse := strconv.Atoi(r.FormValue(uf.Year.Name)) + if errParse != nil { + log.Err(errParse).Msg("unable to parse date") + uf.Year.Err = ErrInvalidYear.Error() + } + uf.Year.Value = year + + if kw := r.FormValue(uf.Keywords.Name); kw != "" { + uf.Keywords.Value = strings.Split(kw, ",") + } + + file, fileh, err := r.FormFile(uf.File.Name) + if err != nil { + log.Err(err).Msg("unable to get file from form") + uf.File.Err = ErrFileOpen.Error() + return uf + } + + uf.File.Value = UploadFile{ + File: file, + Header: fileh, + } + + if err := uf.File.Value.CheckSize(); err != nil { + uf.File.Err = err.Error() + } + + return uf +} + +func NewUploadForm() UploadForm { + return UploadForm{ + Name: FormField[string]{ + Name: "name", + }, + Description: FormField[string]{ + Name: "description", + }, + Editor: FormField[string]{ + Name: "editor", + }, + Authors: FormField[StrList]{ + Name: "authors", + }, + Year: FormField[int]{ + Name: "year", + }, + Keywords: FormField[StrList]{ + Name: "keywords", + }, + File: FormField[UploadFile]{ + Name: "file", + }, + Method: http.MethodPost, + } +} + +func (uf *UploadForm) HasErrors() bool { + return uf.Name.Err != "" || + uf.Authors.Err != "" || + uf.Editor.Err != "" || + uf.Year.Err != "" || + uf.Keywords.Err != "" || + uf.File.Err != "" +} + +func (uf *UploadForm) IsSuccess() bool { + return uf.Method == http.MethodPost && uf.Error == "" && !uf.HasErrors() +} + +func (uf *UploadForm) IntoMetadata() *services.BookMetadata { + bm := &services.BookMetadata{ + Name: uf.Name.Value, + Editor: uf.Editor.Value, + Authors: uf.Authors.Value, + Year: uint16(uf.Year.Value), + Keywords: nil, + } + + if desc := uf.Description.Value; desc != "" { + bm.Description = &desc + } + + if keywords := uf.Keywords.Value; len(keywords) > 0 { + bm.Keywords = keywords + } + + return bm +} + +type LoginForm struct { + Username FormField[string] + Password FormField[string] + Error error + Method string +} + +func NewLoginForm() LoginForm { + return LoginForm{ + Username: FormField[string]{ + Name: "username", + }, + Password: FormField[string]{ + Name: "password", + }, + Method: http.MethodPost, + } +} + +func LoginFormFromRequest(r *http.Request) LoginForm { + lf := NewLoginForm() + + username := r.FormValue(lf.Username.Name) + if username == "" { + lf.Username.Err = ErrInvalidUsername.Error() + } + lf.Username.Value = username + + password := r.FormValue(lf.Password.Name) + if password == "" { + lf.Password.Err = ErrInvalidPassword.Error() + } + lf.Password.Value = password + + return lf +} + +func (lf *LoginForm) HasErrors() bool { + return lf.Username.Err != "" || lf.Password.Err != "" +} + +func (lf *LoginForm) IsSuccess() bool { + return lf.Method == http.MethodPost && lf.Error != nil && !lf.HasErrors() +} + +type SearchForm struct { + Search FormField[string] + Error error + Method string + Results []services.BookMetadata +} + +func SearchFormFromRequest(r *http.Request) SearchForm { + sf := NewSearchForm() + sf.Search.Value = r.FormValue(sf.Search.Name) + return sf +} + +func NewSearchForm() SearchForm { + return SearchForm{ + Search: FormField[string]{ + Name: "search", + }, + Method: http.MethodPost, + } +} diff --git a/handlers/download/handler.go b/handlers/download/handler.go index 0709cb0..ddeb69b 100644 --- a/handlers/download/handler.go +++ b/handlers/download/handler.go @@ -4,6 +4,7 @@ import ( "fmt" "librapi/services" "net/http" + "os" "path/filepath" "github.com/rs/zerolog/log" @@ -27,7 +28,7 @@ func getDownload(w http.ResponseWriter, r *http.Request, bs services.IStore) { downloadFiles, ok := queryParams["file"] if !ok { log.Error().Msg("file query param does not exist") - http.Error(w, "file does not exists", http.StatusBadRequest) + http.Error(w, "file query param does not exist", http.StatusBadRequest) return } @@ -40,6 +41,15 @@ func getDownload(w http.ResponseWriter, r *http.Request, bs services.IStore) { filename := downloadFiles[0] filePath := filepath.Join(bs.GetStoreDir(), filename) + if _, err := os.Stat(filePath); err != nil { + if os.IsNotExist(err) { + http.Error(w, "file does not exist", http.StatusInternalServerError) + return + } + http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + return + } + http.ServeFile(w, r, filePath) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) diff --git a/handlers/home/handler.go b/handlers/home/handler.go index b7d922d..0f0a920 100644 --- a/handlers/home/handler.go +++ b/handlers/home/handler.go @@ -1,39 +1,18 @@ package home import ( - "bytes" "fmt" - "librapi/services" - "librapi/templates" "net/http" "github.com/rs/zerolog/log" + + "librapi/forms" + "librapi/services" + "librapi/templates" ) const URL = "/" -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) { switch r.Method { @@ -48,52 +27,38 @@ func Handler(bs services.IStore) func(http.ResponseWriter, *http.Request) { } func getHome(w http.ResponseWriter, _ *http.Request) { - home := templates.GetHome() - - buf := bytes.NewBufferString("") - if err := home.Execute(buf, &SearchForm{}); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteHomeTmpl(&forms.SearchForm{}, w) + if err != nil { + log.Err(err).Msg("unable to generate home template") return } - fmt.Fprint(w, buf) -} - -func extractSearchForm(r *http.Request) SearchForm { - sf := NewSearchForm() - - sf.Search.Value = r.FormValue(sf.Search.Name) - - return sf + fmt.Fprint(w, tmpl) } func postHome(w http.ResponseWriter, r *http.Request, bs services.IStore) { - home := templates.GetHome() - buf := bytes.NewBufferString("") - - sf := extractSearchForm(r) + sf := forms.SearchFormFromRequest(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) + tmpl, err := templates.ExecuteHomeTmpl(&sf, w) + if err != nil { + log.Err(err).Msg("unable to generate home template") return } w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) return } sf.Results = bms - if err := home.Execute(buf, sf); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteHomeTmpl(&sf, w) + if err != nil { + log.Err(err).Msg("unable to generate home template") return } - fmt.Fprint(w, buf) + fmt.Fprint(w, tmpl) } diff --git a/handlers/login/handler.go b/handlers/login/handler.go index b3d9aba..7e0966d 100644 --- a/handlers/login/handler.go +++ b/handlers/login/handler.go @@ -1,58 +1,19 @@ package login import ( - "bytes" "errors" "fmt" "net/http" "github.com/rs/zerolog/log" + "librapi/forms" "librapi/services" "librapi/templates" ) const URL = "/login" -var ( - ErrInvalidUsername = errors.New("username must not be empty") - ErrInvalidPassword = errors.New("password must not be empty") - ErrInvalidCredentials = errors.New("bad credentials") -) - -type LoginField struct { - Name string - Value string - Err string -} - -type LoginForm struct { - Username LoginField - Password LoginField - Error error - Method string -} - -func NewLoginForm() LoginForm { - return LoginForm{ - Username: LoginField{ - Name: "username", - }, - Password: LoginField{ - Name: "password", - }, - Method: http.MethodPost, - } -} - -func (lf *LoginForm) HasErrors() bool { - return lf.Username.Err != "" || lf.Password.Err != "" -} - -func (lf *LoginForm) IsSuccess() bool { - return lf.Method == http.MethodPost && lf.Error != nil && !lf.HasErrors() -} - func Handler(a services.IAuthenticate) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -66,49 +27,27 @@ func Handler(a services.IAuthenticate) func(http.ResponseWriter, *http.Request) } } -func extractLoginForm(r *http.Request) LoginForm { - lf := NewLoginForm() - - username := r.FormValue(lf.Username.Name) - if username == "" { - lf.Username.Err = ErrInvalidUsername.Error() - } - lf.Username.Value = username - - password := r.FormValue(lf.Password.Name) - if password == "" { - lf.Password.Err = ErrInvalidPassword.Error() - } - lf.Password.Value = password - - return lf -} - func postLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) { - loginForm := templates.GetLoginForm() - loginSuccess := templates.GetLoginSuccess() - if a.IsLogged(r) { - buf := bytes.NewBufferString("") - if err := loginSuccess.Execute(buf, nil); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteLoginSuccessTmpl(w) + if err != nil { + log.Err(err).Msg("unable to generate login success template") return } - fmt.Fprint(w, buf) + fmt.Fprint(w, tmpl) return } - lf := extractLoginForm(r) + lf := forms.LoginFormFromRequest(r) if lf.HasErrors() { - buf := bytes.NewBufferString("") - if err := loginForm.Execute(buf, &lf); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteLoginFormTmpl(&lf, w) + if err != nil { + log.Err(err).Msg("unable to generate login form template") + return } w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) return } @@ -117,17 +56,16 @@ func postLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) if errors.Is(err, services.ErrUnauthorized) { log.Warn().Str("username", lf.Username.Value).Msg("bad credentials") - lf.Error = ErrInvalidCredentials + lf.Error = forms.ErrInvalidCredentials - buf := bytes.NewBufferString("") - if err := loginForm.Execute(buf, &lf); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteLoginFormTmpl(&lf, w) + if err != nil { + log.Err(err).Msg("unable to generate login form template") return } w.WriteHeader(http.StatusUnauthorized) - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) return } @@ -138,39 +76,32 @@ func postLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) cookie := session.GenerateCookie(a.IsSecure()) http.SetCookie(w, cookie) - buf := bytes.NewBufferString("") - if err := loginSuccess.Execute(buf, nil); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteLoginSuccessTmpl(w) + if err != nil { + log.Err(err).Msg("unable to generate login success template") return } - fmt.Fprint(w, buf) + fmt.Fprint(w, tmpl) } func getLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate) { - loginForm := templates.GetLoginForm() - if a.IsLogged(r) { - loginSuccess := templates.GetLoginSuccess() - - buf := bytes.NewBufferString("") - if err := loginSuccess.Execute(buf, nil); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteLoginSuccessTmpl(w) + if err != nil { + log.Err(err).Msg("unable to generate login success template") return } - fmt.Fprint(w, buf) + fmt.Fprint(w, tmpl) return } - buf := bytes.NewBufferString("") - if err := loginForm.Execute(buf, &LoginForm{}); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteLoginFormTmpl(&forms.LoginForm{}, w) + if err != nil { + log.Err(err).Msg("unable to generate login form template") return } - fmt.Fprint(w, buf) + fmt.Fprint(w, tmpl) } diff --git a/handlers/upload/handler.go b/handlers/upload/handler.go index 4322d6d..4b7d77f 100644 --- a/handlers/upload/handler.go +++ b/handlers/upload/handler.go @@ -1,129 +1,20 @@ package upload import ( - "bytes" - "errors" "fmt" - "mime/multipart" "net/http" - "strconv" - "strings" "github.com/rs/zerolog/log" + "librapi/forms" "librapi/services" "librapi/templates" ) const ( - URL = "/upload" - MaxFileSize = 200 // in MB + URL = "/upload" ) -var ( - ErrInvalidName = errors.New("book name must not be empty") - ErrInvalidEditor = errors.New("book editor must not be empty") - ErrInvalidYear = errors.New("invalid year, unable to parse") - ErrInvalidAuthors = errors.New("must at least contains one author") - ErrFileMaxSizeReached = errors.New("max file size reached, must be <= 200MB") - ErrFileOpen = errors.New("unable to open file from form") -) - -type StrList = []string - -type BookFile struct { - file multipart.File - Header *multipart.FileHeader -} - -func (bf *BookFile) GetFilename() string { - return bf.Header.Filename -} - -func (bf *BookFile) CheckSize() error { - if bf.Header.Size > (MaxFileSize << 20) { - return ErrFileMaxSizeReached - } - return nil -} - -type BookFieldType interface { - int | string | StrList | BookFile -} - -type BookField[T BookFieldType] struct { - Name string - Value T - Err string -} - -type BookForm struct { - Name BookField[string] - Description BookField[string] - Editor BookField[string] - Authors BookField[StrList] - Year BookField[int] - Keywords BookField[StrList] - File BookField[BookFile] - Error string - Method string -} - -func NewBookForm() BookForm { - return BookForm{ - Name: BookField[string]{ - Name: "name", - }, - Description: BookField[string]{ - Name: "description", - }, - Editor: BookField[string]{ - Name: "editor", - }, - Authors: BookField[StrList]{ - Name: "authors", - }, - Year: BookField[int]{ - Name: "year", - }, - Keywords: BookField[StrList]{ - Name: "keywords", - }, - File: BookField[BookFile]{ - Name: "file", - }, - Method: http.MethodPost, - } -} - -func (bf *BookForm) HasErrors() bool { - return bf.Name.Err != "" || bf.Authors.Err != "" || bf.Editor.Err != "" || bf.Year.Err != "" || bf.Keywords.Err != "" || bf.File.Err != "" -} - -func (bf *BookForm) IsSuccess() bool { - 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) { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -137,123 +28,62 @@ func Handler(a services.IAuthenticate, s services.IStore) func(http.ResponseWrit } } -func extractBookForm(r *http.Request) BookForm { - bf := NewBookForm() - - name := r.FormValue(bf.Name.Name) - if name == "" { - bf.Name.Err = ErrInvalidName.Error() - } - bf.Name.Value = name - - bf.Description.Value = r.FormValue(bf.Description.Name) - - editor := r.FormValue(bf.Editor.Name) - if editor == "" { - bf.Editor.Err = ErrInvalidEditor.Error() - } - bf.Editor.Value = editor - - if a := r.FormValue(bf.Authors.Name); a != "" { - bf.Authors.Value = strings.Split(a, ",") - } else { - bf.Authors.Err = ErrInvalidAuthors.Error() - } - - year, errParse := strconv.Atoi(r.FormValue(bf.Year.Name)) - if errParse != nil { - log.Err(errParse).Msg("unable to parse date") - bf.Year.Err = ErrInvalidYear.Error() - } - bf.Year.Value = year - - if kw := r.FormValue(bf.Keywords.Name); kw != "" { - bf.Keywords.Value = strings.Split(kw, ",") - } - - file, fileh, err := r.FormFile(bf.File.Name) - if err != nil { - log.Err(err).Msg("unable to get file from form") - bf.File.Err = ErrFileOpen.Error() - return bf - } - - bf.File.Value = BookFile{ - file: file, - Header: fileh, - } - - if err := bf.File.Value.CheckSize(); err != nil { - bf.File.Err = err.Error() - } - - return bf -} - func postUploadFile(w http.ResponseWriter, r *http.Request, a services.IAuthenticate, s services.IStore) { - uploadForm := templates.GetUploadForm() - if !a.IsLogged(r) { - buf := bytes.NewBufferString("") - if err := uploadForm.Execute(buf, &BookForm{Error: services.ErrUnauthorized.Error()}); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteUploadFormTmpl(&forms.UploadForm{Error: services.ErrUnauthorized.Error()}, w) + if err != nil { + log.Err(err).Msg("unable to generate upload template") return } w.WriteHeader(http.StatusUnauthorized) - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) return } - bf := extractBookForm(r) - buf := bytes.NewBufferString("") - if err := uploadForm.Execute(buf, &bf); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + uf := forms.UploadFormFromRequest(r) + tmpl, err := templates.ExecuteUploadFormTmpl(&uf, w) + if err != nil { + log.Err(err).Msg("unable to generate upload template") return } - if bf.HasErrors() { + if uf.HasErrors() { w.WriteHeader(http.StatusBadRequest) - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) return } - filename := bf.File.Value.GetFilename() + filename := uf.File.Value.GetFilename() log.Info().Str("filename", filename).Msg("file is uploading...") - if err := s.Save(bf.IntoMetadata(), bf.File.Value.file); err != nil { - if err := uploadForm.Execute(buf, &BookForm{Error: err.Error()}); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + if err := s.Save(uf.IntoMetadata(), uf.File.Value.File); err != nil { + tmpl, err := templates.ExecuteUploadFormTmpl(&uf, w) + if err != nil { + log.Err(err).Msg("unable to generate upload template") return } w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) return } - buf.Reset() - if err := uploadForm.Execute(buf, &BookForm{Method: http.MethodPost}); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err = templates.ExecuteUploadFormTmpl(&forms.UploadForm{Method: http.MethodPost}, w) + if err != nil { + log.Err(err).Msg("unable to generate upload template") return } - fmt.Fprint(w, buf.String()) + fmt.Fprint(w, tmpl) } func getUploadFile(w http.ResponseWriter, _ *http.Request) { - uploadForm := templates.GetUploadForm() - - buf := bytes.NewBufferString("") - if err := uploadForm.Execute(buf, &BookForm{}); err != nil { - log.Err(err).Msg("unable to generate template") - http.Error(w, "unexpected error occurred", http.StatusInternalServerError) + tmpl, err := templates.ExecuteUploadFormTmpl(&forms.UploadForm{}, w) + if err != nil { + log.Err(err).Msg("unable to generate upload template") return } - fmt.Fprint(w, buf) + fmt.Fprint(w, tmpl) } diff --git a/services/book_store.go b/services/book_store.go index e0e1688..690707f 100644 --- a/services/book_store.go +++ b/services/book_store.go @@ -29,14 +29,41 @@ type IStore interface { 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 +const ( + sqlSearchBooks = ` + 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(?) +` + sqlInsertBook = "insert into books(name, description, editor, authors, year, keywords, path) values (?,?,?,?,?,?,?)" + sqlCreateBookTable = ` + 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 + ) + ` +) type BookMetadata struct { Name string @@ -45,7 +72,7 @@ type BookMetadata struct { Authors []string Year uint16 Keywords []string - Path string + path string } func (bm *BookMetadata) getAuthors() (string, error) { @@ -98,10 +125,14 @@ func (bm *BookMetadata) intoStmtValues() ([]any, error) { authors, bm.Year, keywords, - bm.Path, + bm.path, }, nil } +func (bm *BookMetadata) GetPath() string { + return bm.path +} + type BookStore struct { processing *sync.WaitGroup dir string @@ -128,7 +159,7 @@ func (bs *BookStore) init(dir string) { log.Fatal().Err(err).Msg("unable initialize sqlite3 database") } - if _, err := db.Exec(sqlCreateTable); err != nil { + if _, err := db.Exec(sqlCreateBookTable); err != nil { log.Fatal().Err(err).Msg("unable to create books table") } @@ -179,9 +210,9 @@ func (bs *BookStore) Save(bm *BookMetadata, content io.ReadCloser) error { defer content.Close() - bm.Path = filepath.Join(bs.dir, bm.getFormattedName()) + bm.path = filepath.Join(bs.dir, bm.getFormattedName()) - dst, err := os.Create(bm.Path) + dst, err := os.Create(bm.path) if err != nil { log.Err(err).Msg(ErrFileCreation.Error()) return ErrFileCreation @@ -220,13 +251,14 @@ func (bs *BookStore) Search(value string) ([]BookMetadata, error) { 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 { + 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 { + log.Err(err).Msg("unable to unmarshal authors into slice") return nil, err } bm.Authors = authorsSlice diff --git a/services/sql/create_books_table.sql b/services/sql/create_books_table.sql deleted file mode 100644 index ec0c1ba..0000000 --- a/services/sql/create_books_table.sql +++ /dev/null @@ -1,9 +0,0 @@ -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 -) \ No newline at end of file diff --git a/services/sql/insert_book.sql b/services/sql/insert_book.sql deleted file mode 100644 index 685ec31..0000000 --- a/services/sql/insert_book.sql +++ /dev/null @@ -1,2 +0,0 @@ -insert into books(name, description, editor, authors, year, keywords, path) -values (?,?,?,?,?,?,?) \ No newline at end of file diff --git a/services/sql/search_books.sql b/services/sql/search_books.sql deleted file mode 100644 index abdaccc..0000000 --- a/services/sql/search_books.sql +++ /dev/null @@ -1,20 +0,0 @@ -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(?) - diff --git a/templates/base.html.tpl b/templates/base.html.tpl index a5dd393..e26e346 100644 --- a/templates/base.html.tpl +++ b/templates/base.html.tpl @@ -28,6 +28,12 @@ .error { color: red; } + + .col-item { + margin: 10px; + word-wrap: break-word; + width: 200px; + } @@ -54,7 +60,7 @@ \ No newline at end of file diff --git a/templates/error.html.tpl b/templates/error.html.tpl new file mode 100644 index 0000000..dbd0e47 --- /dev/null +++ b/templates/error.html.tpl @@ -0,0 +1,4 @@ +{{ define "content" }} +

Error

+
Unexpected error occurred, try again or contact the support.
+{{ end }} \ No newline at end of file diff --git a/templates/home.html.tpl b/templates/home.html.tpl index 6ea9458..69fcded 100644 --- a/templates/home.html.tpl +++ b/templates/home.html.tpl @@ -24,6 +24,7 @@ {{ if ne (errStr .Error) "" }}
{{.Error | errStr}}
{{ end }} +{{ if .Results }} @@ -34,15 +35,31 @@ {{range .Results}} - - - - - - + + + + + + {{ end }}
Name
{{.Name}}
{{.Description | noDesc}}
{{.Editor}}
{{.Authors | join }}
{{.Year}}
Download
+
{{.Name}}
+
+
{{.Description | noDesc}}
+
+
{{.Editor}}
+
+
{{.Authors | join }}
+
+
{{.Year}}
+
+
+ Download +
+
+
{{len .Results}} results found
+{{ end }} {{ end }} \ No newline at end of file diff --git a/templates/templates.go b/templates/templates.go index af36584..3a380cf 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -1,32 +1,46 @@ package templates import ( + "bytes" _ "embed" "fmt" "html/template" "mime/multipart" + "net/http" "path/filepath" "strconv" "strings" - "sync" "github.com/rs/zerolog/log" + + "librapi/handlers/download" ) -//go:embed base.html.tpl -var base string +var ( + //go:embed base.html.tpl + base string -//go:embed login/login_form.html.tpl -var loginForm string + //go:embed login/login_form.html.tpl + loginForm string -//go:embed login/login_success.html.tpl -var loginSuccess string + //go:embed login/login_success.html.tpl + loginSuccess string -//go:embed upload_form.html.tpl -var uploadForm string + //go:embed upload_form.html.tpl + uploadForm string -//go:embed home.html.tpl -var home string + //go:embed home.html.tpl + home string + + //go:embed error.html.tpl + errorBase string + + loginFormTmpl = loadLoginFormTmpl() + loginSuccessTmpl = loadLoginSuccessTmpl() + errTmpl = loadErrorTmpl() + homeTmpl = loadHomeTmpl() + uploadFormTmpl = loadUploadFormTmpl() +) var funcMap = template.FuncMap{ "year": func(s int) string { @@ -56,7 +70,7 @@ var funcMap = template.FuncMap{ }, "bookUrl": func(path string) string { _, filename := filepath.Split(path) - return fmt.Sprintf("/download?file=%s", filename) + return fmt.Sprintf("%s?file=%s", download.URL, filename) }, "noDesc": func(desc *string) string { if desc == nil { @@ -66,46 +80,20 @@ var funcMap = template.FuncMap{ }, } -var homeTmpl = sync.OnceValue[*template.Template](func() *template.Template { +func loadErrorTmpl() *template.Template { baseTmpl, err := template.New("base").Parse(base) if err != nil { log.Fatal().Err(err).Msg("unable to parse base tmpl") } - if _, err := baseTmpl.New("home").Funcs(funcMap).Parse(home); err != nil { - log.Fatal().Err(err).Msg("unable to parse home tmpl") + if _, err := baseTmpl.New("error").Funcs(funcMap).Parse(errorBase); err != nil { + log.Fatal().Err(err).Msg("unable to parse error base tmpl") } return baseTmpl -}) +} -var uploadFormTmpl = sync.OnceValue[*template.Template](func() *template.Template { - baseTmpl, err := template.New("base").Parse(base) - if err != nil { - log.Fatal().Err(err).Msg("unable to parse base tmpl") - } - - if _, err := baseTmpl.New("uploadForm").Funcs(funcMap).Parse(uploadForm); err != nil { - log.Fatal().Err(err).Msg("unable to parse upload tmpl") - } - - return baseTmpl -}) - -var loginFormTmpl = sync.OnceValue[*template.Template](func() *template.Template { - baseTmpl, err := template.New("base").Parse(base) - if err != nil { - log.Fatal().Err(err).Msg("unable to parse base tmpl") - } - - if _, err := baseTmpl.New("loginForm").Funcs(funcMap).Parse(loginForm); err != nil { - log.Fatal().Err(err).Msg("unable to parse login tmpl") - } - - return baseTmpl -}) - -var loginSuccessTmpl = sync.OnceValue[*template.Template](func() *template.Template { +func loadLoginSuccessTmpl() *template.Template { baseTmpl, err := template.New("base").Parse(base) if err != nil { log.Fatal().Err(err).Msg("unable to parse base tmpl") @@ -116,20 +104,73 @@ var loginSuccessTmpl = sync.OnceValue[*template.Template](func() *template.Templ } return baseTmpl -}) +} + +func loadLoginFormTmpl() *template.Template { + baseTmpl, err := template.New("base").Parse(base) + if err != nil { + log.Fatal().Err(err).Msg("unable to parse base tmpl") + } + + if _, err := baseTmpl.New("loginForm").Funcs(funcMap).Parse(loginForm); err != nil { + log.Fatal().Err(err).Msg("unable to parse login form tmpl") + } + + return baseTmpl +} + +func loadHomeTmpl() *template.Template { + baseTmpl, err := template.New("base").Parse(base) + if err != nil { + log.Fatal().Err(err).Msg("unable to parse base tmpl") + } + + if _, err := baseTmpl.New("home").Funcs(funcMap).Parse(home); err != nil { + log.Fatal().Err(err).Msg("unable to parse home tmpl") + } + + return baseTmpl +} + +func loadUploadFormTmpl() *template.Template { + baseTmpl, err := template.New("base").Parse(base) + if err != nil { + log.Fatal().Err(err).Msg("unable to parse base tmpl") + } + + if _, err := baseTmpl.New("uploadform").Funcs(funcMap).Parse(uploadForm); err != nil { + log.Fatal().Err(err).Msg("unable to parse upload form tmpl") + } + + return baseTmpl +} func GetHome() *template.Template { - return homeTmpl() + return homeTmpl } -func GetLoginForm() *template.Template { - return loginFormTmpl() +func executeTmpl(tmpl *template.Template, form any, w http.ResponseWriter) (string, error) { + buf := bytes.NewBufferString("") + if err := tmpl.Execute(buf, form); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, errTmpl) + return "", err + } + return buf.String(), nil } -func GetLoginSuccess() *template.Template { - return loginSuccessTmpl() +func ExecuteLoginSuccessTmpl(w http.ResponseWriter) (string, error) { + return executeTmpl(loginSuccessTmpl, nil, w) } -func GetUploadForm() *template.Template { - return uploadFormTmpl() +func ExecuteLoginFormTmpl(form any, w http.ResponseWriter) (string, error) { + return executeTmpl(loginFormTmpl, form, w) +} + +func ExecuteHomeTmpl(form any, w http.ResponseWriter) (string, error) { + return executeTmpl(homeTmpl, form, w) +} + +func ExecuteUploadFormTmpl(form any, w http.ResponseWriter) (string, error) { + return executeTmpl(uploadFormTmpl, form, w) }