aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Hurst <ark@mansus.space>2023-01-15 02:16:35 +0100
committerJulian Hurst <ark@mansus.space>2023-01-15 02:16:35 +0100
commit09a34dda989df0f74bbfcd11ac63254d61b92a18 (patch)
treea62769481ceea1cd235b60c930a50e7a7c5a49e0
parent7d502441a6bd210aff8a8625ee87ea5a1fbbd7ee (diff)
downloaddocspace-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.go2
-rw-r--r--imgs.go186
-rw-r--r--main.go90
-rw-r--r--static/style.scss (renamed from static/style.css)49
-rw-r--r--templates/base.html1
-rw-r--r--templates/imgs.html6
-rw-r--r--templates/imgs_page.html17
-rw-r--r--templates/imgs_stub.html1
-rw-r--r--templates/login.html4
-rw-r--r--templates/nav_logged.html3
-rw-r--r--templates/user.html2
11 files changed, 315 insertions, 46 deletions
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.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}}