All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 5s
A new HTML tool — browse — that lists the contents of any directory.
Designed for ZDDC archives but no ZDDC-specific filtering; just a
straight folder browser with expand/collapse, sort, and name filter.
Modes (auto-detected at page load):
- Online: when served by zddc-server at a folder URL, queries
the same URL with Accept: application/json to load the listing
and renders it. Auto-served as the default at any directory
under ZDDC_ROOT without an index.html (replacing the previous
minimal-HTML stub from directory.go).
- Local: 'Select Directory' button uses FileSystemAccessAPI to
pick any folder on disk; works in Chromium-based browsers.
Features (Phase 1 — what's in this commit):
- Tree view with lazy-loaded folders (children fetched on first
expand).
- Sort by name / size / extension / date (column header click).
- Filter by name substring (toolbar input).
- File click opens in a new tab — for server-backed pages,
routes through zddc-server's normal handler so .archive
redirects + apps cascade overrides + ACL all apply.
Phase 2 deferred:
- ZIP files inline expansion (treat archive entries as virtual
children).
- File preview popup (reuse shared/preview-lib.js).
- Extension multi-select filter.
Wiring:
- browse/ added to top-level ./build's per-tool list, embed
block, versions.txt, and the lockstep release commit + tag set.
All seven tools (archive, transmittal, classifier, mdedit,
landing, form, browse) advance together on stable cuts.
- shared/build-lib.sh: browse added to ZDDC_RELEASE_TOOLS and
verify_channel_links's per-tool loop.
- zddc/internal/apps/embed.go: //go:embed browse.html +
EmbeddedBytes("browse") case.
- zddc/internal/apps/availability.go: browse available at every
directory (same as archive).
- zddc/internal/apps/handler.go: MatchAppHTML routes
/<dir>/browse.html → 'browse'.
- zddc/internal/handler/directory.go: when a directory request
arrives with Accept: text/html and no index.html exists,
serve the embedded browse.html bytes (with a JSON-fallback
if the embedded slot is empty during bootstrap).
75 lines
2.5 KiB
Go
75 lines
2.5 KiB
Go
package apps
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Folder name conventions that gate which tools are virtually available
|
|
// at a given path. The names are case-sensitive; ZDDC convention uses
|
|
// the capitalized forms.
|
|
var (
|
|
folderNamesIncomingWorkingStaging = []string{"Incoming", "Working", "Staging"}
|
|
folderNamesWorking = []string{"Working"}
|
|
folderNamesStaging = []string{"Staging"}
|
|
)
|
|
|
|
// AppAvailableAt reports whether app's virtual HTML can be served at
|
|
// requestDir. Rules:
|
|
//
|
|
// - archive: every directory (multi-project, project, archive, vendor)
|
|
// - browse: every directory (generic file listing — also the default
|
|
// served at folder URLs without an index.html; see directory.go)
|
|
// - classifier: requestDir is, or descends from, a folder named
|
|
// "Incoming", "Working", or "Staging" (the directories where
|
|
// incoming/outgoing files get classified)
|
|
// - mdedit: requestDir is, or descends from, a "Working" folder
|
|
// (where markdown drafts are written and edited)
|
|
// - transmittal: requestDir is, or descends from, a "Staging" folder
|
|
// (where outgoing transmittals are prepared)
|
|
// - landing: only at the deployment root (the project picker)
|
|
//
|
|
// Operators can always drop a real <name>.html file at any path to override
|
|
// — that path is served by the static handler regardless of this function's
|
|
// result. AppAvailableAt is consulted only when no real file exists.
|
|
func AppAvailableAt(root, requestDir, app string) bool {
|
|
root = filepath.Clean(root)
|
|
requestDir = filepath.Clean(requestDir)
|
|
|
|
switch app {
|
|
case "archive":
|
|
return true
|
|
case "browse":
|
|
return true
|
|
case "landing":
|
|
return requestDir == root
|
|
case "classifier":
|
|
return inAncestorWithName(root, requestDir, folderNamesIncomingWorkingStaging)
|
|
case "mdedit":
|
|
return inAncestorWithName(root, requestDir, folderNamesWorking)
|
|
case "transmittal":
|
|
return inAncestorWithName(root, requestDir, folderNamesStaging)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// inAncestorWithName reports whether requestDir is, or has an ancestor
|
|
// (not including root itself), named one of names. The match is on the
|
|
// last segment of each directory in the chain root → requestDir.
|
|
func inAncestorWithName(root, requestDir string, names []string) bool {
|
|
if requestDir == root {
|
|
return false
|
|
}
|
|
rel, err := filepath.Rel(root, requestDir)
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return false
|
|
}
|
|
for _, part := range strings.Split(rel, string(filepath.Separator)) {
|
|
for _, n := range names {
|
|
if part == n {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|