Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e703c8792 | ||
|
|
367420961e |
No files matched your search
@@ -721,6 +721,7 @@ Pour mettre à jour l'une d'elles :
|
||||
| `tags.go` | stockage des tags de couleur |
|
||||
| `presets.go`, `presets.json` | préréglages de tags (`/api/presets`), liste intégrée |
|
||||
| `api.go` | routes HTTP `/api/*` |
|
||||
| `static.go` | fichiers de l'interface embarqués : ETag par fichier, revérifiés à chaque chargement (`no-cache`, 304 s'ils n'ont pas changé) pour toujours prendre une mise à jour ; polices en cache 30 jours |
|
||||
| `web/` | interface (HTML, CSS, JavaScript simple, sans étape de build), embarquée dans le binaire ; les traductions sont dans `web/app.js` (`I18N`), et dans `web/login.html` pour la page de connexion |
|
||||
|
||||
## Remarque
|
||||
|
||||
@@ -663,6 +663,7 @@ To update one of them:
|
||||
| `tags.go` | color tag storage |
|
||||
| `presets.go`, `presets.json` | color tag presets (`/api/presets`), built-in list |
|
||||
| `api.go` | `/api/*` HTTP routes |
|
||||
| `static.go` | embedded UI files: ETag per file, revalidated on each load (`no-cache`, 304 when unchanged) so an update is always picked up; fonts cached 30 days |
|
||||
| `web/` | UI (HTML, CSS, plain JavaScript, no build step), embedded in the binary; translations live in `web/app.js` (`I18N`), and in `web/login.html` for the login page |
|
||||
|
||||
## Note
|
||||
|
||||
@@ -230,7 +230,7 @@ func main() {
|
||||
go api.files.Run(ctx)
|
||||
api.selfLog = selfLog
|
||||
api.Routes(mux)
|
||||
mux.Handle("GET /", http.FileServer(http.FS(static)))
|
||||
mux.Handle("GET /", staticFiles(static))
|
||||
|
||||
app := readOnly(mux)
|
||||
handler, err := newAuth(cfg.auth, app)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// staticFiles serves the embedded UI with an ETag per file (a hash of its
|
||||
// content, computed at startup). Pages, scripts and styles are revalidated on
|
||||
// every load ("no-cache"): an unchanged file costs a 304 without body, and the
|
||||
// files of a new version are always taken after an update. Fonts are kept for
|
||||
// 30 days.
|
||||
func staticFiles(static fs.FS) http.Handler {
|
||||
etags := map[string]string{}
|
||||
_ = fs.WalkDir(static, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
b, err := fs.ReadFile(static, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
etags["/"+path] = `"` + hex.EncodeToString(sum[:8]) + `"`
|
||||
return nil
|
||||
})
|
||||
etags["/"] = etags["/index.html"]
|
||||
files := http.FileServer(http.FS(static))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if tag, ok := etags[r.URL.Path]; ok {
|
||||
// http.FileServer answers If-None-Match with 304 from this header.
|
||||
w.Header().Set("ETag", tag)
|
||||
if strings.HasPrefix(r.URL.Path, "/fonts/") {
|
||||
w.Header().Set("Cache-Control", "public, max-age=2592000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
}
|
||||
}
|
||||
files.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestStaticFilesCache(t *testing.T) {
|
||||
h := staticFiles(fstest.MapFS{
|
||||
"index.html": {Data: []byte("<p>v1</p>")},
|
||||
"app.js": {Data: []byte("v1")},
|
||||
"fonts/mono.woff2": {Data: []byte("font")},
|
||||
})
|
||||
get := func(path, etag string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
if etag != "" {
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
rec := get("/app.js", "")
|
||||
tag := rec.Header().Get("ETag")
|
||||
if rec.Code != http.StatusOK || tag == "" || rec.Header().Get("Cache-Control") != "no-cache" {
|
||||
t.Fatalf("app.js: %d, ETag %q, Cache-Control %q", rec.Code, tag, rec.Header().Get("Cache-Control"))
|
||||
}
|
||||
if rec := get("/app.js", tag); rec.Code != http.StatusNotModified || rec.Body.Len() != 0 {
|
||||
t.Fatalf("revalidation: %d with %d bytes, want 304 without body", rec.Code, rec.Body.Len())
|
||||
}
|
||||
if rec := get("/app.js", `"old"`); rec.Code != http.StatusOK || rec.Body.String() != "v1" {
|
||||
t.Fatalf("stale ETag: %d %q", rec.Code, rec.Body)
|
||||
}
|
||||
if rec := get("/", ""); rec.Code != http.StatusOK || rec.Header().Get("ETag") == "" || rec.Header().Get("Cache-Control") != "no-cache" {
|
||||
t.Fatalf("index: %d %v", rec.Code, rec.Header())
|
||||
}
|
||||
if rec := get("/fonts/mono.woff2", ""); rec.Header().Get("Cache-Control") != "public, max-age=2592000" {
|
||||
t.Fatalf("font Cache-Control %q", rec.Header().Get("Cache-Control"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user