BREAKING CHANGE. Project-level Issued/Received/Incoming folders no
longer carry special semantics. WORM enforcement and auto-ownership
move to the per-party canonical layout:
- WORM mask now triggers on archive/<party>/received/ and
archive/<party>/issued/ (any case, any party)
- Auto-own .zddc writes on first mkdir under working/, staging/,
or archive/<party>/incoming/ (any case)
Predicate API:
- IsAutoOwnPath(parentDir, fsRoot) — replaces IsAutoOwnParent(name)
- IsWormPath(requestPath) — same name, new pattern
- WormFolderLevelIndex unchanged signature, new pattern
Legacy SpecialFolderNames / AutoOwnFolderNames / WormFolderNames /
IsAutoOwnParent are deleted (no Deprecated: stubs — early-development
project, no back-compat to preserve).
Tool availability (apps/availability.go) is case-fold throughout:
- mdedit: descendants of working/
- transmittal: descendants of staging/
- classifier: descendants of working/, staging/, or
archive/<party>/incoming/
Working/, WORKING/, working/ all match identically.
Test fixtures rewritten:
- special_test.go: covers IsAutoOwnPath / IsWormPath /
WormFolderLevelIndex / ResolveCanonical / canonical lists
- availability_test.go: per-party rules, case-fold scenarios
- fileapi_test.go: rolePermissionsTestSetup now seeds
Project-X/archive/Acme/{incoming,issued,received}/ rather than
Vendor/{Incoming,Issued,Received}/ at the project root
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.4 KiB
Go
70 lines
2.4 KiB
Go
package apps
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// AppAvailableAt reports whether app's virtual HTML can be served at
|
|
// requestDir. Rules (case-insensitive on canonical folder names):
|
|
//
|
|
// - archive: every directory (multi-project, project, archive, party)
|
|
// - 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
|
|
// "working", "staging", or "incoming" (the directories where
|
|
// in-flight files get classified)
|
|
// - mdedit: requestDir is, or descends from, a "working" folder
|
|
// (where markdown drafts are written and edited, including review
|
|
// responses drafted in working/<rs-name>/)
|
|
// - 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.
|
|
//
|
|
// In the canonical layout, "incoming" only appears at
|
|
// archive/<party>/incoming/, so checking "any ancestor named incoming"
|
|
// is equivalent to checking "under a per-party incoming folder."
|
|
func AppAvailableAt(root, requestDir, app string) bool {
|
|
root = filepath.Clean(root)
|
|
requestDir = filepath.Clean(requestDir)
|
|
|
|
switch app {
|
|
case "archive", "browse":
|
|
return true
|
|
case "landing":
|
|
return requestDir == root
|
|
case "classifier":
|
|
return inAncestorWithName(root, requestDir, "working", "staging", "incoming")
|
|
case "mdedit":
|
|
return inAncestorWithName(root, requestDir, "working")
|
|
case "transmittal":
|
|
return inAncestorWithName(root, requestDir, "staging")
|
|
}
|
|
return false
|
|
}
|
|
|
|
// inAncestorWithName reports whether requestDir is, or has an ancestor
|
|
// (not including root itself), whose last segment case-folds to one
|
|
// of names. Match is on segment names, case-insensitively.
|
|
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 strings.EqualFold(part, n) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|