mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(shares): say when "the latest version" was (BEA-31) (#76)
A /s/<token> page promises the reader it always shows the latest version, then never says when latest was — so a stranger can't tell whether a living wiki page is from today or last March. Print FileInfo.Time, already in hand at the point handleShared renders, as a muted line above the content: human date, precise RFC3339 in the title attribute. Zero time prints nothing rather than a 1970 date. Markdown shells only. The .html/.htm branch stays a raw io.Copy and binaries stay untouched — the new test asserts byte equality for both. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6da3c7957e
commit
ecc8328512
@@ -370,7 +370,7 @@ func (s *Server) handleShared(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, sharedMarkdownShell, html.EscapeString(path.Base(sh.Path)), body)
|
||||
fmt.Fprintf(w, sharedMarkdownShell, html.EscapeString(path.Base(sh.Path)), updatedStamp(fi.Time), body)
|
||||
case ".html", ".htm":
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
io.Copy(w, rc)
|
||||
@@ -381,7 +381,20 @@ func (s *Server) handleShared(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// updatedStamp renders the "how old is this?" line a share page owes its
|
||||
// reader — the link promises the latest version, so it has to say when latest
|
||||
// was. Zero time (a source that doesn't know) prints nothing rather than 1970.
|
||||
func updatedStamp(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
t = t.UTC()
|
||||
return fmt.Sprintf(`<div class="updated" title="%s">Last updated %s</div>`,
|
||||
html.EscapeString(t.Format(time.RFC3339)), html.EscapeString(t.Format("2 Jan 2006")))
|
||||
}
|
||||
|
||||
// sharedMarkdownShell wraps rendered markdown in a minimal readable page.
|
||||
// Verbs, in order: title, updated stamp, body.
|
||||
const sharedMarkdownShell = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"><title>%s</title>
|
||||
<style>
|
||||
@@ -405,7 +418,8 @@ table.frontmatter code{white-space:pre-wrap}
|
||||
pre{max-width:100%%}
|
||||
footer.bdrive{margin-top:64px;padding-top:14px;border-top:1px solid #d0d7de;font-size:12.5px;color:#57606a}
|
||||
footer.bdrive a{color:inherit}
|
||||
@media (prefers-color-scheme: dark){footer.bdrive{border-color:#3a3a44;color:#888}}
|
||||
</style></head><body>%s
|
||||
.updated{font-size:12.5px;color:#57606a;margin-bottom:28px}
|
||||
@media (prefers-color-scheme: dark){footer.bdrive{border-color:#3a3a44;color:#888}.updated{color:#888}}
|
||||
</style></head><body>%s%s
|
||||
<footer class="bdrive">Shared with <a href="https://github.com/runbear-io/beardrive" rel="noopener">BearDrive</a> — synced files for AI agent teams</footer>
|
||||
</body></html>`
|
||||
|
||||
@@ -3,6 +3,7 @@ package webapp
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
@@ -365,6 +366,73 @@ func TestShareListAPIsAreStable(t *testing.T) {
|
||||
|
||||
// helpers
|
||||
|
||||
// TestShareLastUpdatedStamp: a share page promises the latest version, so a
|
||||
// markdown page says when latest was — and nothing else gets injected into.
|
||||
func TestShareLastUpdatedStamp(t *testing.T) {
|
||||
srv, p, _, f, h := shareHub(t)
|
||||
const rawHTML = "<h1>Q3</h1><script>alert(1)</script>"
|
||||
const pdf = "%PDF-1.4 fake\n"
|
||||
f.put("dev1", "wiki/deck.pdf", pdf)
|
||||
|
||||
// markdown: the stamp is the FILE's time, human date + precise title=
|
||||
when := time.Date(2026, 3, 14, 9, 26, 53, 0, time.UTC)
|
||||
f.putAt("dev1", "wiki/notes.md", "# Notes\n\nhello **team**", when)
|
||||
token, _ := authedShare(t, srv, h, p.ID, "wiki/notes.md")
|
||||
rec := do(t, h, "GET", "/s/"+token, nil)
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "Last updated 14 Mar 2026") {
|
||||
t.Fatalf("no last-updated stamp: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `title="2026-03-14T09:26:53Z"`) {
|
||||
t.Fatalf("no precise timestamp: %s", body)
|
||||
}
|
||||
// ...and it sits above the content, not after it
|
||||
if strings.Index(body, "Last updated") > strings.Index(body, "<strong>team</strong>") {
|
||||
t.Fatal("stamp must render before the content")
|
||||
}
|
||||
// the sandbox + footer survive
|
||||
if csp := rec.Header().Get("Content-Security-Policy"); csp != "sandbox allow-scripts allow-popups" {
|
||||
t.Fatalf("CSP = %q", csp)
|
||||
}
|
||||
if rec.Header().Get("X-Content-Type-Options") != "nosniff" || rec.Header().Get("Referrer-Policy") != "no-referrer" {
|
||||
t.Fatalf("share headers = %v", rec.Header())
|
||||
}
|
||||
if !strings.Contains(body, "Shared with <a") {
|
||||
t.Fatal("footer went missing")
|
||||
}
|
||||
|
||||
// re-syncing the file moves the stamp; the token does not change
|
||||
f.putAt("dev1", "wiki/notes.md", "# Notes\n\nhello **team**", when.AddDate(0, 0, 40))
|
||||
rec = do(t, h, "GET", "/s/"+token, nil)
|
||||
if !strings.Contains(rec.Body.String(), "Last updated 23 Apr 2026") {
|
||||
t.Fatalf("stamp must track the file, got %s", rec.Body)
|
||||
}
|
||||
|
||||
// zero time: no stamp at all, not a 1970 date
|
||||
f.putAt("dev1", "wiki/notes.md", "# Notes\n\nhello **team**", time.Time{})
|
||||
rec = do(t, h, "GET", "/s/"+token, nil)
|
||||
if strings.Contains(rec.Body.String(), "Last updated") || strings.Contains(rec.Body.String(), `class="updated"`) {
|
||||
t.Fatalf("zero time must print no stamp, got %s", rec.Body)
|
||||
}
|
||||
|
||||
// HTML shares are served byte-for-byte — never injected into
|
||||
htmlTok, _ := authedShare(t, srv, h, p.ID, "wiki/report.html")
|
||||
rec = do(t, h, "GET", "/s/"+htmlTok, nil)
|
||||
if !bytes.Equal(rec.Body.Bytes(), []byte(rawHTML)) {
|
||||
t.Fatalf("html share must be byte-identical, got %q", rec.Body)
|
||||
}
|
||||
|
||||
// so are binaries, Content-Length included
|
||||
pdfTok, _ := authedShare(t, srv, h, p.ID, "wiki/deck.pdf")
|
||||
rec = do(t, h, "GET", "/s/"+pdfTok, nil)
|
||||
if !bytes.Equal(rec.Body.Bytes(), []byte(pdf)) {
|
||||
t.Fatalf("binary share must be unchanged, got %q", rec.Body)
|
||||
}
|
||||
if cl := rec.Header().Get("Content-Length"); cl != fmt.Sprint(len(pdf)) {
|
||||
t.Fatalf("Content-Length = %q, want %d", cl, len(pdf))
|
||||
}
|
||||
}
|
||||
|
||||
func jsonReq(t *testing.T, method, url string, body any) *http.Request {
|
||||
t.Helper()
|
||||
var data []byte
|
||||
|
||||
Reference in New Issue
Block a user