feat(zddc-server): no-slash canonical folders → default tool, even
when missing on disk
Mirror of the existing IsDir-branch behavior at line 873
(<project>/working → mdedit, <project>/staging → transmittal,
<project>/archive → archive) for the case where the folder doesn't
exist on disk yet. Without this, GET <project>/working on a fresh
project 404s instead of opening mdedit rooted at the (virtual)
working directory.
Behavior matrix for canonical project-root folders that don't yet
exist on disk:
GET <project>/archive → archive tool (project-root mode)
GET <project>/archive/ → empty browse listing
GET <project>/working → mdedit rooted at working/
GET <project>/working/ → empty browse listing (with synthetic
<viewer-email>/ home entry)
GET <project>/staging → transmittal rooted at staging/
GET <project>/staging/ → empty browse listing
GET <project>/reviewing → 301 to /reviewing/ (no default app)
GET <project>/reviewing/ → empty browse listing
GET <project>/random → 404 (still — non-canonical)
GET <project>/random/ → 404 (still — non-canonical)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
03babd34d2
commit
b3cea9b7a8
2 changed files with 72 additions and 11 deletions
|
|
@ -841,18 +841,38 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
|
|||
}
|
||||
}
|
||||
}
|
||||
// Empty-listing fallback for canonical project-root folders.
|
||||
// <project>/{archive,working,staging,reviewing}/ that don't
|
||||
// yet exist on disk should render as a usable empty
|
||||
// directory view rather than 404, so the stage-strip nav
|
||||
// links land on a real page on a fresh project. The
|
||||
// matching read-side fallback in fs.ListDirectory returns
|
||||
// 200 + [] for the same paths; we fall through to
|
||||
// ServeDirectory which goes through that path and applies
|
||||
// ACL via the parent project's .zddc cascade.
|
||||
// Canonical project-root folder fallback. <project>/{archive,
|
||||
// working,staging,reviewing}[/] should land on a usable view
|
||||
// (default tool or empty listing) rather than 404, so the
|
||||
// stage-strip nav works on a fresh project that hasn't yet
|
||||
// been written to. Two shapes:
|
||||
//
|
||||
// <project>/working → mdedit rooted at working/
|
||||
// (matches the existing IsDir branch
|
||||
// for an existing folder)
|
||||
// <project>/working/ → ServeDirectory → fs.ListDirectory
|
||||
// returns 200 + [] for the empty case
|
||||
//
|
||||
// reviewing/ has no default app, so the no-slash form 301s
|
||||
// to the slash form.
|
||||
if (r.Method == http.MethodGet || r.Method == http.MethodHead) &&
|
||||
strings.HasSuffix(urlPath, "/") &&
|
||||
zddc.IsProjectRootFolder(strings.TrimPrefix(strings.TrimSuffix(urlPath, "/"), "/")) {
|
||||
zddc.IsProjectRootFolder(strings.Trim(strings.TrimPrefix(urlPath, "/"), "/")) {
|
||||
if !strings.HasSuffix(urlPath, "/") {
|
||||
if app := apps.DefaultAppAt(cfg.Root, absPath); app != "" && appsSrv != nil {
|
||||
if apps.AppAvailableAt(cfg.Root, absPath, app) {
|
||||
chain, _ := zddc.EffectivePolicy(cfg.Root, absPath)
|
||||
if allowed, _ := policy.AllowFromChain(r.Context(), handler.DeciderFromContext(r), chain, email, urlPath); !allowed {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
appsSrv.Serve(w, r, app, chain, absPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
// No default app (reviewing/) — redirect to slash form.
|
||||
http.Redirect(w, r, urlPath+"/", http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
handler.ServeDirectory(cfg, w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -517,6 +517,47 @@ func TestDispatchEmptyCanonicalProjectFolders(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// No-trailing-slash form on a canonical folder → default app
|
||||
// (mdedit for working/, transmittal for staging/, archive for
|
||||
// archive/). Mirror of the existing "no-slash → default app"
|
||||
// behavior at the IsDir branch, extended to cover the case where
|
||||
// the folder doesn't exist on disk yet.
|
||||
noSlashDefaultApp := []struct {
|
||||
stage string
|
||||
expect string // substring that should appear in the response body
|
||||
}{
|
||||
{"working", "ZDDC Markdown"},
|
||||
{"staging", "ZDDC Transmittal"},
|
||||
{"archive", "ZDDC Archive"},
|
||||
}
|
||||
for _, tc := range noSlashDefaultApp {
|
||||
t.Run("no-slash/"+tc.stage+" → default app", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/Project/"+tc.stage, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
dispatch(cfg, idx, ring, appsSrv, nil, rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), tc.expect) {
|
||||
t.Errorf("%s/ body missing %q", tc.stage, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// reviewing/ has no default tool — no-slash form should 301 to
|
||||
// the slash form (which then renders the empty listing).
|
||||
t.Run("no-slash/reviewing → 301 to slash", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/Project/reviewing", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
dispatch(cfg, idx, ring, appsSrv, nil, rec, req)
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("status=%d, want 301", rec.Code)
|
||||
}
|
||||
if loc := rec.Header().Get("Location"); loc != "/Project/reviewing/" {
|
||||
t.Errorf("Location=%q, want %q", loc, "/Project/reviewing/")
|
||||
}
|
||||
})
|
||||
|
||||
// Non-canonical missing folder still 404s (the fallback is
|
||||
// scoped to the four canonical names, not a blanket "missing →
|
||||
// empty" rule).
|
||||
|
|
|
|||
Loading…
Reference in a new issue