diff options
| author | Julian Hurst <ark@mansus.space> | 2023-01-15 02:16:35 +0100 |
|---|---|---|
| committer | Julian Hurst <ark@mansus.space> | 2023-01-15 02:16:35 +0100 |
| commit | 09a34dda989df0f74bbfcd11ac63254d61b92a18 (patch) | |
| tree | a62769481ceea1cd235b60c930a50e7a7c5a49e0 | |
| parent | 7d502441a6bd210aff8a8625ee87ea5a1fbbd7ee (diff) | |
| download | docspace-09a34dda989df0f74bbfcd11ac63254d61b92a18.tar.gz | |
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.
| -rw-r--r-- | db.go | 2 | ||||
| -rw-r--r-- | imgs.go | 186 | ||||
| -rw-r--r-- | main.go | 90 | ||||
| -rw-r--r-- | static/style.scss (renamed from static/style.css) | 49 | ||||
| -rw-r--r-- | templates/base.html | 1 | ||||
| -rw-r--r-- | templates/imgs.html | 6 | ||||
| -rw-r--r-- | templates/imgs_page.html | 17 | ||||
| -rw-r--r-- | templates/imgs_stub.html | 1 | ||||
| -rw-r--r-- | templates/login.html | 4 | ||||
| -rw-r--r-- | templates/nav_logged.html | 3 | ||||
| -rw-r--r-- | templates/user.html | 2 |
11 files changed, 315 insertions, 46 deletions
@@ -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 @@ -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 +//} @@ -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.scss index 42ae410..ca1fdfb 100644 --- a/static/style.css +++ b/static/style.scss @@ -16,33 +16,53 @@ nav { /*padding-left: 8px;*/ } -ul.nav { +%navlist { display: inline-flex; margin: 0; padding: 0; width: 100%; + li { + @extend %navitem; + } } -ul.nav li { +ul.nav { + @extend %navlist; +} + +ul.navlogged { + @extend %navlist; +} + +%navitem { display: inline-block; - padding: 5px; - /*padding-right: 5px; + /*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:hover { - background: grey; +ul.nav li { + @extend %navitem; } -ul.nav li a { - display: block; - width: 100%; - height: 100%; +ul.navlogged li { + @extend %navitem; + width: calc(100% - 33%); } div.docs { @@ -71,3 +91,12 @@ form.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 @@ <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="data:;base64,iVBORw0KGgo="> <link rel="stylesheet" type="text/css" href="/static/style.css"> <script src="/static/htmx.min.js"></script> <title>{{block "title" .}}{{end}} - docspace</title> 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"}} + <h1>Images</h1> + {{template "imgspage" .}} + <span>{{.NbFiles}} images</span> +{{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}} + <div hx-get="/imgs?page={{add $.Page 1}}&fragment" + hx-trigger="revealed" + hx-swap="afterend"> + <span>{{$img.Name}}</span><br/> + <a href="{{$img.Link}}"><img src="{{$img.Link}}" /></a><br/> + {{else}} + <div> + <span>{{$img.Name}}</span><br/> + <a href="{{$img.Link}}"><img src="{{$img.Link}}" /></a><br/> + {{end}} + <hr/> + </div> +{{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 @@ <p class="error">{{.Error}}</p> {{end}} <form action="/login" method="POST"> - <input type="text" name="user" id="user" placeholder="Nom d'utilisateur"><br/><br/> - <input type="password" name="pass" id="pass" placeholder="Mot de passe"><br/><br/> + <input required type="text" name="user" id="user" placeholder="Nom d'utilisateur"><br/><br/> + <input required type="password" name="pass" id="pass" placeholder="Mot de passe"><br/><br/> <input type="submit" value="Connexion"> </form> {{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"}} <nav> - <ul class="nav"> + <ul class="navlogged"> <!--<li><a href="/admin">Admin</a></li>--> <li><a href="/">Accueil</a></li> + <li><a href="/imgs">Images</a></li> <li><a href="/logout">Se déconnecter</a></li> </ul> </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 @@ <a href="{{.Link}}">{{.Name}}</a> </td> <td> - {{.ModTime}} + {{formatmodtime .ModTime}} </td> <td> {{.Size}} |
