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.
57 lines
1.6 KiB
Go
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
|
|
}
|