init repo
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"librapi/handlers/upload/templates"
|
||||
"librapi/services"
|
||||
)
|
||||
|
||||
const MaxFileSize = 200 // in MB
|
||||
|
||||
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")
|
||||
ErrUnauthorized = errors.New("unvalid authorization key")
|
||||
)
|
||||
|
||||
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]
|
||||
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",
|
||||
},
|
||||
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 Handler(s *services.SessionStore) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
getUploadFile(w, r)
|
||||
case http.MethodPost:
|
||||
postUploadFile(w, r, s)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
editor := r.FormValue(bf.Editor.Name)
|
||||
if editor == "" {
|
||||
bf.Editor.Err = ErrInvalidEditor.Error()
|
||||
}
|
||||
bf.Editor.Value = editor
|
||||
|
||||
if a := r.FormValue(bf.Authors.Name); len(a) != 0 {
|
||||
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); len(kw) != 0 {
|
||||
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, s *services.SessionStore) {
|
||||
uploadForm := templates.GetUploadForm()
|
||||
if uploadForm == nil {
|
||||
log.Error().Msg("unable to load upload form")
|
||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.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)
|
||||
}
|
||||
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
bf := extractBookForm(r)
|
||||
if err := uploadForm.Execute(buf, &bf); err != nil {
|
||||
log.Err(err).Msg("unable to generate template")
|
||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if bf.HasErrors() {
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprint(w, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
filename := bf.File.Value.GetFilename()
|
||||
log.Info().Str("filename", filename).Msg("file is uploading...")
|
||||
|
||||
dst, err := os.Create(filename)
|
||||
if err != nil {
|
||||
if err := uploadForm.Execute(buf, &BookForm{Error: "unexpected error occured while creating file"}); err != nil {
|
||||
log.Err(err).Msg("unable to generate template")
|
||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, bf.File.Value.file); err != nil {
|
||||
if err := uploadForm.Execute(buf, &BookForm{Error: "unexpected error occured while uploading file"}); err != nil {
|
||||
log.Err(err).Msg("unable to generate template")
|
||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, buf.String())
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(w, buf.String())
|
||||
}
|
||||
|
||||
func getUploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
uploadForm := templates.GetUploadForm()
|
||||
if uploadForm == nil {
|
||||
log.Error().Msg("unable to load upload form")
|
||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(w, buf); err != nil {
|
||||
log.Err(err).Msg("unable to write to response")
|
||||
http.Error(w, "unexpected error occurred", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
align-self: flex-start;
|
||||
margin: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Upload a book</h1>
|
||||
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||
<div class="main-container">
|
||||
<div class="form-item">
|
||||
<div class="form-container">
|
||||
<label>Book name:</label>
|
||||
<input type="text" name="name" value="{{.Name.Value}}" />
|
||||
</div>
|
||||
{{ if .Name.Err }}
|
||||
<div class="error">{{.Name.Err}}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-container">
|
||||
<label>Editor:</label>
|
||||
<input type="text" name="editor" value="{{.Editor.Value}}" />
|
||||
</div>
|
||||
{{ if .Editor.Err }}
|
||||
<div class="error">{{.Editor.Err}}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-container">
|
||||
<label>Authors:</label>
|
||||
<input type="text" name="authors" value="{{.Authors.Value | join}}" />
|
||||
</div>
|
||||
{{ if .Authors.Err }}
|
||||
<div class="error">{{.Authors.Err}}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-container">
|
||||
<label>Year:</label>
|
||||
<input type="number" name="year" value="{{.Year.Value | year}}" />
|
||||
</div>
|
||||
{{ if .Year.Err }}
|
||||
<div class="error">{{.Year.Err}}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-container">
|
||||
<label>Keywords:</label>
|
||||
<input type="text" name="keywords" value="{{.Keywords.Value | join}}" />
|
||||
</div>
|
||||
{{ if .Keywords.Err }}
|
||||
<div class="error">{{.Keywords.Err}}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-container">
|
||||
<input type="file" name="file" files="{{.File.Value.Header | filename}}" accept=".pdf" />
|
||||
<button id="submit" type="submit">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ if .File.Err }}
|
||||
<div class="error">{{.File.Err}}</div>
|
||||
{{ end }}
|
||||
{{ if ne .Error "" }}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{ end }}
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
var submit = document.getElementById("submit");
|
||||
submit.addEventListener("submit", function () {
|
||||
submit.disabled = true;
|
||||
});
|
||||
{ { if .IsSuccess } }
|
||||
alert("file uploaded successfully");
|
||||
{ { end } }
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"year": func(s int) string {
|
||||
if s == 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(s)
|
||||
},
|
||||
"join": func(s []string) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
} else {
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
},
|
||||
"filename": func(h *multipart.FileHeader) string {
|
||||
if h != nil {
|
||||
return h.Filename
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
//go:embed form.html.tpl
|
||||
var form string
|
||||
|
||||
var GetUploadForm = sync.OnceValue[*template.Template](func() *template.Template {
|
||||
tmpl, err := template.New("uploadForm").Funcs(funcMap).Parse(form)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("unable to parse upload form")
|
||||
return nil
|
||||
}
|
||||
return tmpl
|
||||
})
|
||||
Reference in New Issue
Block a user