From 09a34dda989df0f74bbfcd11ac63254d61b92a18 Mon Sep 17 00:00:00 2001 From: Julian Hurst Date: Sun, 15 Jan 2023 02:16:35 +0100 Subject: Add images support (inf scroll via htmx of images taken from user docs) This commit also includes a migration from css to scss, some improvements to the redirection workflow when no session is found and refactors concerning flash cookies. --- db.go | 2 +- imgs.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 90 ++++++++++++++-------- static/style.css | 73 ------------------ static/style.scss | 102 +++++++++++++++++++++++++ templates/base.html | 1 + templates/imgs.html | 6 ++ templates/imgs_page.html | 17 +++++ templates/imgs_stub.html | 1 + templates/login.html | 4 +- templates/nav_logged.html | 3 +- templates/user.html | 2 +- 12 files changed, 378 insertions(+), 109 deletions(-) create mode 100644 imgs.go delete mode 100644 static/style.css create mode 100644 static/style.scss create mode 100644 templates/imgs.html create mode 100644 templates/imgs_page.html create mode 100644 templates/imgs_stub.html diff --git a/db.go b/db.go index fb17b14..8eedd7e 100644 --- a/db.go +++ b/db.go @@ -52,7 +52,7 @@ func GetUser(db *sql.DB, user User) (User, error) { } defer rows.Close() if !rows.Next() { - return user, errors.New("No user found for given username") + return user, errors.New("No user found for the given username") } var id int var u, email, pass string diff --git a/imgs.go b/imgs.go new file mode 100644 index 0000000..ee67083 --- /dev/null +++ b/imgs.go @@ -0,0 +1,186 @@ +package main + +import ( + "fmt" + "os" + "mime" + "net/http" + "strconv" + "path" + "path/filepath" + "sort" +) + +const pageSize = 5 + +func imgs(w http.ResponseWriter, r *http.Request) { + u, err := checkSession(w, r) + if u != nil && err == nil { + fragment := r.URL.Query().Has("fragment") + pageQuery := r.URL.Query().Get("page") + if pageQuery == "" { + pageQuery = "0" + } + page, err := strconv.Atoi(pageQuery) + if err != nil { + sendError(w, r, err.Error(), http.StatusInternalServerError) + return + } + userDocPath := filepath.Join(baseDocDir, u.User) + err = os.Mkdir(userDocPath, 0750) + if err != nil && !os.IsExist(err) { + sendError(w, r, err.Error(), http.StatusInternalServerError) + return + } + //files, err := os.ReadDir(userDocPath) + //if err != nil { + // sendError(w, r, err.Error(), http.StatusInternalServerError) + // return + //} + //sort.Slice(files, func(i, j int) bool { + // info1, err := files[i].Info() + // if err != nil { + // return false + // } + // info2, err := files[j].Info() + // if err != nil { + // return false + // } + // return info1.ModTime().After(info2.ModTime()) + //}) + start := page * pageSize + //imgs := extractImgs(files, start) + imgs, err := extractImgsGlob(userDocPath) + if err != nil { + sendError(w, r, err.Error(), http.StatusInternalServerError) + return + } + sort.Slice(imgs, func(i, j int) bool { + return imgs[i].ModTime.After(imgs[j].ModTime) + }) + end := start + pageSize + if end > len(imgs) { + end = len(imgs) + } + flasherr := consumeFlash(w, r, "error") + data := struct { + Imgs []Doc + Start int + End int + NbFiles int + Page int + Error string + }{ + imgs[start:end], + start + 1, + end, + len(imgs), + page, + flasherr, + } + if fragment { + //serveSimple(w, r, data, "templates/imgs_stub.html") + serveSimple(w, r, data, "templates/imgs_page.html", "templates/imgs_stub.html") + } else { + serveTemplate(w, r, data, "templates/imgs_page.html", "templates/imgs.html") + } + return + } else if err != nil { + sendError(w, r, err.Error(), http.StatusInternalServerError) + return + } + sendFlash(w, r, "redirect", "/imgs") + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +func extractImgsGlob(userDocPath string) ([]Doc, error) { + var typs []string = []string { + "image/jpeg", + "image/png", + "image/gif", + "image/avif", + "image/bmp", + "image/vnd.microsoft.icon", + "image/svg+xml", + "image/tiff", + "image/webp", + } + var exts []string + for _, typ := range typs { + e, err := mime.ExtensionsByType(typ) + if err != nil { + return nil, err + } + exts = append(exts, e...) + } + var imgs []Doc + for _, ext := range exts { + glob := fmt.Sprintf("*%s", ext) + p := filepath.Join(userDocPath, glob) + matches, err := filepath.Glob(p) + if err != nil { + return nil, err + } + for _, match := range matches { + info, err := os.Stat(match) + if err != nil { + return nil, err + } + imgs = append(imgs, Doc { + info.Name(), + humanize(info.Size()), + info.ModTime(), + path.Join(userDocPath, info.Name()), + }) + } + } + return imgs, nil +} + +//func extractImgsPaged(files []fs.DirEntry, start int) []Doc { +// var typs []string = []string { +// "image/jpeg", +// "image/png", +// "image/gif", +// "image/avif", +// "image/bmp", +// "image/vnd.microsoft.icon", +// "image/svg+xml", +// "image/tiff", +// "image/webp", +// } +// var exts []string +// for _, typ := range typs { +// e, err := mime.ExtensionsByType(typ) +// if err != nil { +// sendError(w, r, err.Error(), http.StatusInternalServerError) +// return +// } +// exts = append(exts, e...) +// } +// var imgs []Doc +// var i int = 0 +// for _, file := range files { +// if i >= start + pageSize { +// break +// } +// for _, ext := range exts { +// if strings.HasSuffix(file.Name(), ext) && i >= start { +// info, err := file.Info() +// if err != nil { +// sendError(w, r, err.Error(), http.StatusInternalServerError) +// return +// } +// imgs = append(imgs, Doc { +// file.Name(), +// humanize(info.Size()), +// info.ModTime(), +// path.Join(baseDocDir, u.User, file.Name()), +// }) +// i++ +// break +// } +// } +// } +// return imgs +//} diff --git a/main.go b/main.go index 4ece388..42377a3 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,6 @@ import ( "path" "path/filepath" "sort" - "errors" "net/http" "html/template" "flag" @@ -16,6 +15,7 @@ import ( "encoding/json" "encoding/base64" "sync" + "time" "archive/zip" "github.com/satori/go.uuid" @@ -31,7 +31,7 @@ var sessionIds sync.Map type Doc struct { Name string Size string - ModTime string + ModTime time.Time Link string } @@ -40,16 +40,36 @@ type UserSession struct { SessionId string } -func serveTemplate(w http.ResponseWriter, r *http.Request, view string, data interface{}) { +var fmap template.FuncMap = template.FuncMap { + "add": func(i, j int) int { + return i + j + }, + "formatmodtime": func(i time.Time) string { + return i.Format("2006-01-02") + }, +} + +func serveTemplate(w http.ResponseWriter, r *http.Request, data interface{}, view ...string) { var nav string = "templates/nav.html" if u, err := checkSession(w, r); u != nil && err == nil { nav = "templates/nav_logged.html" } - t, err := template.New("base.html").Funcs(template.FuncMap { - "add": func(i, j int) int { - return i + j - }, - }).ParseFiles("templates/base.html", nav, view) + views := []string {"templates/base.html", nav} + views = append(views, view...) + t, err := template.New("base.html").Funcs(fmap).ParseFiles(views...) + if err != nil { + log.Fatal(err) + } + if err := t.Execute(w, data); err != nil { + log.Fatal(err) + } +} + +func serveSimple(w http.ResponseWriter, r *http.Request, data interface{}, view string, xviews ...string) { + views := []string {view} + views = append(views, xviews...) + fp := filepath.Base(views[len(views)-1]) + t, err := template.New(fp).Funcs(fmap).ParseFiles(views...) if err != nil { log.Fatal(err) } @@ -150,11 +170,11 @@ func index(w http.ResponseWriter, r *http.Request) { docs = append(docs, Doc { file.Name(), humanize(info.Size()), - info.ModTime().Format("2006-01-02"), + info.ModTime(), path.Join(baseDocDir, u.User, file.Name()), }) } - flasherr := consumeFlashError(w, r) + flasherr := consumeFlash(w, r, "error") data := struct { Docs []Doc Error string @@ -162,19 +182,20 @@ func index(w http.ResponseWriter, r *http.Request) { docs, flasherr, } - serveTemplate(w, r, "templates/user.html", data) + serveTemplate(w, r, data, "templates/user.html") return } else if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } + sendFlash(w, r, "redirect", "/") http.Redirect(w, r, "/login", http.StatusSeeOther) } func admin(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil && u.IsAdmin { - serveTemplate(w, r, "templates/admin.html", nil) + serveTemplate(w, r, nil, "templates/admin.html") } else if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } else { @@ -190,11 +211,11 @@ func adminUsers(w http.ResponseWriter, r *http.Request) { sendError(w, r, err.Error(), http.StatusInternalServerError) return } - serveTemplate(w, r, "templates/admin/users.html", struct { + serveTemplate(w, r, struct { Users []User }{ users, - }) + }, "templates/admin/users.html") } else if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } else { @@ -205,24 +226,26 @@ func adminUsers(w http.ResponseWriter, r *http.Request) { func createuser(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - err := consumeFlashError(w, r) + err := consumeFlash(w, r, "error") data := struct { Error string }{ err, } - serveTemplate(w, r, "templates/createuser.html", data) + serveTemplate(w, r, data, "templates/createuser.html") case http.MethodPost: u := r.FormValue("user") email := r.FormValue("email") pass := r.FormValue("pass") cpass := r.FormValue("cpass") if len(pass) < 10 { - sendFlashError(w, r, "/createuser", errors.New("Le mot de passe doit avoir une longeur supérieure ou égale à 10 caractères.")) + sendFlash(w, r, "error", "Le mot de passe doit avoir une longeur supérieure ou égale à 10 caractères.") + http.Redirect(w, r, "/createuser", http.StatusSeeOther) return } if pass != cpass { - sendFlashError(w, r, "/createuser", errors.New("Le mot de passe et la confirmation du mot de passe ne sont pas les mêmes.")) + sendFlash(w, r, "error", "Le mot de passe et la confirmation du mot de passe ne sont pas les mêmes.") + http.Redirect(w, r, "/createuser", http.StatusSeeOther) return } user := User{-1, u, email, pass, false} @@ -237,8 +260,8 @@ func createuser(w http.ResponseWriter, r *http.Request) { } } -func consumeFlashError(w http.ResponseWriter, r *http.Request) string { - cookie, err := r.Cookie("flasherror") +func consumeFlash(w http.ResponseWriter, r *http.Request, name string) string { + cookie, err := r.Cookie(name) if err != nil { if err == http.ErrNoCookie { return "" @@ -247,7 +270,7 @@ func consumeFlashError(w http.ResponseWriter, r *http.Request) string { } } http.SetCookie(w, &http.Cookie{ - Name: "flasherror", + Name: name, Value: "", MaxAge: -1, }) @@ -258,10 +281,10 @@ func consumeFlashError(w http.ResponseWriter, r *http.Request) string { return string(s) } -func sendFlashError(w http.ResponseWriter, r *http.Request, url string, err error) { - str := base64.StdEncoding.EncodeToString([]byte(err.Error())) +func sendFlash(w http.ResponseWriter, r *http.Request, name, s string) { + str := base64.StdEncoding.EncodeToString([]byte(s)) cookie := http.Cookie { - Name: "flasherror", + Name: name, Value: str, MaxAge: 0, // Only https on qutebrowser @@ -270,7 +293,6 @@ func sendFlashError(w http.ResponseWriter, r *http.Request, url string, err erro SameSite: http.SameSiteStrictMode, } http.SetCookie(w, &cookie) - http.Redirect(w, r, url, http.StatusSeeOther) } func logout(w http.ResponseWriter, r *http.Request) { @@ -294,19 +316,20 @@ func logout(w http.ResponseWriter, r *http.Request) { func login(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - err := consumeFlashError(w, r) + err := consumeFlash(w, r, "error") data := struct { Error string }{ err, } - serveTemplate(w, r, "templates/login.html", data) + serveTemplate(w, r, data, "templates/login.html") case http.MethodPost: u := r.FormValue("user") pass := r.FormValue("pass") user, err := CheckUserPass(db, User{-1, u, "", pass, false}) if err != nil { - sendFlashError(w, r, "/login", err) + sendFlash(w, r, "error", err.Error()) + http.Redirect(w, r, "/login", http.StatusSeeOther) return } user.Pass = "" @@ -318,7 +341,8 @@ func login(w http.ResponseWriter, r *http.Request) { } jsonData, err := json.Marshal(us) if err != nil { - sendFlashError(w, r, "/login", err) + sendFlash(w, r, "error", err.Error()) + http.Redirect(w, r, "/login", http.StatusSeeOther) return } bStr := base64.StdEncoding.EncodeToString(jsonData) @@ -332,7 +356,9 @@ func login(w http.ResponseWriter, r *http.Request) { SameSite: http.SameSiteStrictMode, } http.SetCookie(w, &cookie) - http.Redirect(w, r, "/", http.StatusSeeOther) + redirectflash := consumeFlash(w, r, "redirect") + log.Printf("read redirect: %s\n", redirectflash) + http.Redirect(w, r, redirectflash, http.StatusSeeOther) default: sendInvalidMethod(w, r) } @@ -363,7 +389,8 @@ func download(w http.ResponseWriter, r *http.Request) { r.ParseForm() selection := r.Form["selection"] if len(selection) == 0 { - sendFlashError(w, r, "/", errors.New("Aucun fichier sélectionné")) + sendFlash(w, r, "error", "Aucun fichier sélectionné") + http.Redirect(w, r, "/", http.StatusSeeOther) return } contentDisposition := fmt.Sprintf("attachment; filename=\"Documents.zip\"") @@ -477,6 +504,7 @@ func main() { http.HandleFunc("/logout", logout) http.HandleFunc("/upload", upload) http.HandleFunc("/download", download) + http.HandleFunc("/imgs", imgs) http.HandleFunc("/admin", admin) http.HandleFunc("/admin/users", adminUsers) log.Printf("Serving http://localhost:%d\n", *p) diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 42ae410..0000000 --- a/static/style.css +++ /dev/null @@ -1,73 +0,0 @@ -body { - margin: 0; -} - -.error { - color: red; -} - -div.content { - padding: 5px; - margin: 8px; -} - -nav { - border-bottom: 1px solid black; - /*padding-left: 8px;*/ -} - -ul.nav { - display: inline-flex; - margin: 0; - padding: 0; - width: 100%; -} - -ul.nav li { - display: inline-block; - padding: 5px; - /*padding-right: 5px; - padding-left: 5px; - width: 150px;*/ - width: 50%; - text-align: center; - background: lightgrey; - border-right: 1px solid black; -} - -ul.nav li:hover { - background: grey; -} - -ul.nav li a { - display: block; - width: 100%; - height: 100%; -} - -div.docs { - overflow: scroll; -} - -table { - border-collapse: collapse; - /*table-layout: fixed;*/ -} - -td, th { - border: 1px solid black; - padding: 10px; -} - -td.filename { - overflow: scroll; - white-space: nowrap; -} - -form.inline { - display: inline; -} - -form.inlineblk { - display: inline-block; -} diff --git a/static/style.scss b/static/style.scss new file mode 100644 index 0000000..ca1fdfb --- /dev/null +++ b/static/style.scss @@ -0,0 +1,102 @@ +body { + margin: 0; +} + +.error { + color: red; +} + +div.content { + padding: 5px; + margin: 8px; +} + +nav { + border-bottom: 1px solid black; + /*padding-left: 8px;*/ +} + +%navlist { + display: inline-flex; + margin: 0; + padding: 0; + width: 100%; + li { + @extend %navitem; + } +} + +ul.nav { + @extend %navlist; +} + +ul.navlogged { + @extend %navlist; +} + +%navitem { + display: inline-block; + /*padding: 5px; + padding-right: 5px; + padding-left: 5px; + width: 150px;*/ + width: 50%; + height: 100%; + text-align: center; + background: lightgrey; + border-right: 1px solid black; + &:hover { + background: grey; + } + a { + display: block; + width: 100%; + height: 100%; + padding: 5px 0px 5px 0px; + } +} + +ul.nav li { + @extend %navitem; +} + +ul.navlogged li { + @extend %navitem; + width: calc(100% - 33%); +} + +div.docs { + overflow: scroll; +} + +table { + border-collapse: collapse; + /*table-layout: fixed;*/ +} + +td, th { + border: 1px solid black; + padding: 10px; +} + +td.filename { + overflow: scroll; + white-space: nowrap; +} + +form.inline { + display: inline; +} + +form.inlineblk { + display: inline-block; +} + +img { + width: 100%; + max-width: 600px; +} + +hr { + border-top: 1px dashed black; +} diff --git a/templates/base.html b/templates/base.html index c2ca497..653570d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,7 @@ + {{block "title" .}}{{end}} - docspace diff --git a/templates/imgs.html b/templates/imgs.html new file mode 100644 index 0000000..9b584a8 --- /dev/null +++ b/templates/imgs.html @@ -0,0 +1,6 @@ +{{define "title"}}Acceuil{{end}} +{{define "content"}} +

Images

+ {{template "imgspage" .}} + {{.NbFiles}} images +{{end}} diff --git a/templates/imgs_page.html b/templates/imgs_page.html new file mode 100644 index 0000000..665daf3 --- /dev/null +++ b/templates/imgs_page.html @@ -0,0 +1,17 @@ +{{define "imgspage"}} +{{range $i, $img := .Imgs}} + {{if eq (add $i $.Start) $.End}} +
+ {{$img.Name}}
+
+ {{else}} +
+ {{$img.Name}}
+
+ {{end}} +
+
+{{end}} +{{end}} diff --git a/templates/imgs_stub.html b/templates/imgs_stub.html new file mode 100644 index 0000000..a1c542b --- /dev/null +++ b/templates/imgs_stub.html @@ -0,0 +1 @@ +{{template "imgspage" .}} diff --git a/templates/login.html b/templates/login.html index e1b4c44..5e717c3 100644 --- a/templates/login.html +++ b/templates/login.html @@ -5,8 +5,8 @@

{{.Error}}

{{end}}
-

-

+

+

{{end}} diff --git a/templates/nav_logged.html b/templates/nav_logged.html index a62585f..50e9a36 100644 --- a/templates/nav_logged.html +++ b/templates/nav_logged.html @@ -1,8 +1,9 @@ {{define "nav"}} diff --git a/templates/user.html b/templates/user.html index f116286..cd0d98c 100644 --- a/templates/user.html +++ b/templates/user.html @@ -27,7 +27,7 @@ {{.Name}} - {{.ModTime}} + {{formatmodtime .ModTime}} {{.Size}} -- cgit v1.2.3