Add installable PWA shell with safe updates and offline status

This commit is contained in:
eezd committed 2026-10-19 13:22:56 +08:00
1 parent 64915a7b94
commit 1562519f08
18 files changed
+310 -5

No files matched your search

+4 -1
View File
@@ -124,6 +124,9 @@ func (s *Server) Handler(static fs.FS) http.Handler {
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
if strings.HasPrefix(r.URL.Path, "/api/") {
h.Set("Cache-Control", "no-store")
}
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "same-origin")
h.Set("X-Frame-Options", "DENY")
@@ -394,7 +397,7 @@ func (s *Server) serveRaw(w http.ResponseWriter, r *http.Request, rel string) {
if ct := mime.TypeByExtension(path.Ext(info.Name())); ct != "" {
h.Set("Content-Type", ct)
}
h.Set("Cache-Control", "private, no-cache")
h.Set("Cache-Control", "no-store")
http.ServeContent(w, r, info.Name(), info.ModTime(), f)
}
+36
View File
@@ -1,6 +1,7 @@
package api
import (
"compress/gzip"
"encoding/json"
"io"
"net/http"
@@ -139,6 +140,41 @@ func TestOpenAPICoversRoutes(t *testing.T) {
}
}
func TestAPIResponsesAreNotCached(t *testing.T) {
f := newFixture(t)
f.write("笔记.md", "# 正文")
// 登录状态与目录曾在 Firefox 离线时命中 HTTP 缓存,误进入无法加载的工作区。
for _, path := range []string{"/api/v1/auth/state", "/api/v1/tree", "/api/v1/files/笔记.md", "/api/v1/raw/笔记.md"} {
res := f.do(f.admin, "GET", path, "", nil)
if res.StatusCode != http.StatusOK || res.Header.Get("Cache-Control") != "no-store" {
t.Errorf("%s:状态 %d,缓存策略 %q", path, res.StatusCode, res.Header.Get("Cache-Control"))
}
}
}
func TestCompressedManifestContentType(t *testing.T) {
body := strings.Repeat(" ", 300) + `{"name":"RawMark","display":"standalone"}`
handler := newStaticHandler(fstest.MapFS{"manifest.webmanifest": {Data: []byte(body)}})
req := httptest.NewRequest("GET", "/manifest.webmanifest", nil)
req.Header.Set("Accept-Encoding", "gzip")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
res := recorder.Result()
defer res.Body.Close()
if res.Header.Get("Content-Type") != "application/manifest+json" || res.Header.Get("Content-Encoding") != "gzip" {
t.Fatalf("清单响应头不正确:%v", res.Header)
}
zr, err := gzip.NewReader(res.Body)
if err != nil {
t.Fatal(err)
}
defer zr.Close()
var manifest map[string]string
if err := json.NewDecoder(zr).Decode(&manifest); err != nil || manifest["name"] != "RawMark" {
t.Fatalf("压缩清单无法解析:%v", err)
}
}
func TestShareAccess(t *testing.T) {
f := newFixture(t)
f.write("目录/笔记.md", "# 标题\n\n![](笔记.assets/图.png)\n")
+4 -1
View File
@@ -68,7 +68,10 @@ func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if share {
hd.Set("X-Robots-Tag", "noindex")
}
if ct := mime.TypeByExtension(path.Ext(name)); ct != "" {
if path.Ext(name) == ".webmanifest" {
// Go 的 MIME 表未必包含此扩展名,不能让 ServeContent 对压缩后的数据嗅探类型。
hd.Set("Content-Type", "application/manifest+json")
} else if ct := mime.TypeByExtension(path.Ext(name)); ct != "" {
hd.Set("Content-Type", ct)
}
hd.Add("Vary", "Accept-Encoding")