files: make ~member browsable before first session

A valid member's ~name 404'd until they'd opened their first SFTP/web
session, because joining onto a not-yet-created site dir tripped the
safeJoin escape guard (it walks up to sites/, outside the per-user root).
AnonRoot now materializes the idempotent site dir for a known member, so
~name renders an empty listing the moment the account exists. Missing
sub-paths and unknown members still 404. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-26 01:03:12 +00:00
parent 192b117fc1
commit 87fb2b4a1f
3 changed files with 48 additions and 1 deletions

View file

@ -386,6 +386,14 @@ func (h *webSrv) handleAnon(w http.ResponseWriter, r *http.Request) {
}
fi, err := os.Stat(real)
if err != nil {
// A known member whose site dir hasn't been created yet (it is created
// lazily on their first files session) renders as an empty listing — not
// a 404 — so ~name is reachable as soon as the account exists. A missing
// sub-path still 404s.
if os.IsNotExist(err) && path.Clean("/"+strings.TrimPrefix(rel, "/")) == "/" {
h.renderAnonDir(w, prefix, heading, rel, real)
return
}
http.NotFound(w, r)
return
}
@ -408,10 +416,11 @@ func (h *webSrv) handleAnon(w http.ResponseWriter, r *http.Request) {
// on-disk directory (already confined by handleAnon).
func (h *webSrv) renderAnonDir(w http.ResponseWriter, prefix, heading, rel, real string) {
des, err := os.ReadDir(real)
if err != nil {
if err != nil && !os.IsNotExist(err) {
http.Error(w, "cannot list files", http.StatusInternalServerError)
return
}
// A not-yet-created site dir lists as empty (des is nil).
rel = path.Clean("/" + strings.TrimPrefix(rel, "/"))
data := anonData{Title: h.cfg.Title, CurPath: heading}
if rel != "/" {