Adds a UI checkbox next to the existing Sort dropdown that surfaces
hidden entries when ACL would otherwise allow read. Default off
(matches today's filtered behavior). On toggle, browse re-fetches
the current directory with ?hidden=1 and re-renders.
┌─ browse toolbar ─────────────────────────────────────────────┐
│ Sort: [Name (A→Z) ▾] ☐ Show hidden │
└──────────────────────────────────────────────────────────────┘
Server-side surface:
- internal/fs/tree.go ListDirectory gains an `includeHidden bool`
parameter. The .-prefix filter (previously hard-coded) now also
drops _-prefix entries (matches dispatch's reserved-prefix guard)
and honors the new flag.
- internal/handler/directory.go reads `?hidden=1` from the request
and threads it through.
- cmd/zddc-server/main.go dispatcher relaxes its dot-prefix and
_-prefix guards for GET/HEAD when `?hidden=1` is set, so clicking
a hidden entry's link works. `_app/` (apps cache) stays
unconditionally reserved — those bytes must go through the apps
resolver. Writes to hidden paths stay blocked (the file API has
its own segment check that the flag does NOT relax).
- internal/listing/listing.go: signature parity (the lower-level
helper that's used by tests + non-cascade listing paths).
Security model unchanged: the ACL chain on the parent dir is the only
real gate. Whoever can read the dir can see its contents — toggling
"Show hidden" just stops the client-side filter from masking
.-prefixed and _-prefixed entries. Hidden paths today:
• <dir>/.zddc ACL YAML — already exposed via /.profile/zddc
• <dir>/.converted/<base> cached MD→DOCX/HTML/PDF, same sensitivity as source
• <root>/.zddc.d/tokens/ per-token metadata; filename = sha256(token)
so not bearer-usable. Default root ACL
restricts to admins; matches /.tokens UI.
• <root>/.zddc.d/logs/ access logs; same admins-only audience
• <root>/_app/ cached upstream tool HTML (public)
• <root>/_template/ install.zip scaffolding (public)
None of these contain bearer credentials or secret material that the
existing ACL doesn't already gate. The walls are still the cascade.
87 lines
2.3 KiB
Go
87 lines
2.3 KiB
Go
package listing
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestFromDirEntriesFiltersHidden asserts that both '.' and '_' prefixed
|
|
// entries are excluded from listings — the '.' branch is the long-standing
|
|
// rule (matches dispatch dot-prefix guard); '_' is for operator-managed
|
|
// scaffolding like install.zip's _template/ that should be reachable by
|
|
// direct URL but invisible to browse.
|
|
func TestFromDirEntriesFiltersHidden(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
for _, name := range []string{
|
|
"Project-A",
|
|
"Project-B",
|
|
".zddc", // hidden file
|
|
".devshell", // hidden dir
|
|
"_template", // scaffolding dir
|
|
"_archive", // scaffolding dir
|
|
"_notes.txt", // scaffolding file
|
|
"normal.txt",
|
|
} {
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("ReadDir: %v", err)
|
|
}
|
|
|
|
got, err := FromDirEntries(entries, "/", false)
|
|
if err != nil {
|
|
t.Fatalf("FromDirEntries: %v", err)
|
|
}
|
|
|
|
want := map[string]bool{
|
|
"Project-A": true,
|
|
"Project-B": true,
|
|
"normal.txt": true,
|
|
}
|
|
if len(got) != len(want) {
|
|
var names []string
|
|
for _, e := range got {
|
|
names = append(names, e.Name)
|
|
}
|
|
t.Fatalf("got %d entries (%v), want %d (%v)", len(got), names, len(want), want)
|
|
}
|
|
for _, e := range got {
|
|
if !want[e.Name] {
|
|
t.Errorf("unexpected entry in listing: %q", e.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFromDirEntriesIncludeHidden verifies the includeHidden=true path
|
|
// surfaces dot- and underscore-prefixed entries.
|
|
func TestFromDirEntriesIncludeHidden(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for _, name := range []string{".zddc", "_template", "normal.txt"} {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", name, err)
|
|
}
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("ReadDir: %v", err)
|
|
}
|
|
got, err := FromDirEntries(entries, "/", true)
|
|
if err != nil {
|
|
t.Fatalf("FromDirEntries: %v", err)
|
|
}
|
|
want := map[string]bool{".zddc": true, "_template": true, "normal.txt": true}
|
|
if len(got) != len(want) {
|
|
var names []string
|
|
for _, e := range got {
|
|
names = append(names, e.Name)
|
|
}
|
|
t.Fatalf("got %d entries (%v), want %d", len(got), names, len(want))
|
|
}
|
|
}
|