ZDDC/zddc/internal/listing/listing.go
ZDDC 72c0552750 feat(browse): "Show hidden" toggle — list .-prefixed and _-prefixed entries
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.
2026-05-13 14:45:41 -05:00

57 lines
1.6 KiB
Go

package listing
import (
"net/url"
"os"
)
// FromDirEntries converts os.DirEntry slice to []FileInfo.
// baseURL is the URL prefix for this directory (must end with "/").
// When includeHidden is false (the default for normal listings),
// entries starting with "." or "_" are excluded. When true (the
// dispatcher passes ?hidden=1 through to here), they're surfaced;
// the caller is responsible for any further policy gating, but
// in practice the existing ACL chain on the parent directory is
// the only gate that matters.
func FromDirEntries(entries []os.DirEntry, baseURL string, includeHidden bool) ([]FileInfo, error) {
var result []FileInfo
for _, entry := range entries {
name := entry.Name()
// Skip empty names always. '.' and '_' prefixes mark hidden
// entries — system/internal state (.zddc, .converted/,
// .zddc.d/) and operator scaffolding (_app, _template). These
// are filtered by default; pass includeHidden=true to expose.
if len(name) == 0 {
continue
}
if !includeHidden && (name[0] == '.' || name[0] == '_') {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
isDir := entry.IsDir()
entryName := name
entryURL := baseURL + url.PathEscape(name)
if isDir {
entryName = name + "/"
entryURL = baseURL + url.PathEscape(name) + "/"
}
fi := FileInfo{
Name: entryName,
Size: info.Size(),
URL: entryURL,
ModTime: info.ModTime(),
Mode: uint32(info.Mode()),
IsDir: isDir,
IsSymlink: false,
}
result = append(result, fi)
}
return result, nil
}