fix(reviewing): case-insensitive on-disk lookup for archive/+staging/+
party/{received,issued}
The synthetic test fixture and many real deployments use PascalCase
folder names (Archive/, PartyB/, Received/, Issued/, Staging/). The
aggregator was hard-coding lowercase joins, which on case-sensitive
filesystems (Linux ext4) meant os.ReadDir returned NotExist and the
listing was empty even when the data was present.
Use zddc.ResolveCanonical to find the on-disk casing for each
canonical segment (archive/, staging/, then per-party received/ and
issued/), and emit URLs with the resolved casing so the dispatcher's
URL canonicalisation is a no-op pass-through.
The case-insensitive lookup was already used elsewhere (file API's
mkdir, tree.go's virtualUserHomeEntry); reviewing/ now matches that
convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
002e034119
commit
d1a5a14132
1 changed files with 43 additions and 19 deletions
|
|
@ -83,22 +83,35 @@ func computePending(ctx context.Context, decider policy.Decider,
|
|||
fsRoot, project, email string) ([]pendingSubmittal, error) {
|
||||
|
||||
projectAbs := filepath.Join(fsRoot, project)
|
||||
archiveAbs := filepath.Join(projectAbs, "archive")
|
||||
stagingAbs := filepath.Join(projectAbs, "staging")
|
||||
|
||||
// Resolve the canonical folder names to whatever case is present
|
||||
// on disk (deployments may use Archive/ Received/ Issued/ Staging/
|
||||
// PascalCase). Empty string means no case variant exists — treated
|
||||
// as missing (empty contribution to the join).
|
||||
archiveOnDisk, _ := zddc.ResolveCanonical(projectAbs, "archive")
|
||||
stagingOnDisk, _ := zddc.ResolveCanonical(projectAbs, "staging")
|
||||
|
||||
// Index staging by tracking → folder name.
|
||||
stagedByTracking := map[string]string{}
|
||||
if entries, err := os.ReadDir(stagingAbs); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
if _, tracking, _, _, ok := zddc.ParseTransmittalFolder(e.Name()); ok {
|
||||
stagedByTracking[tracking] = e.Name()
|
||||
var stagingAbs string
|
||||
if stagingOnDisk != "" {
|
||||
stagingAbs = filepath.Join(projectAbs, stagingOnDisk)
|
||||
if entries, err := os.ReadDir(stagingAbs); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
if _, tracking, _, _, ok := zddc.ParseTransmittalFolder(e.Name()); ok {
|
||||
stagedByTracking[tracking] = e.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if archiveOnDisk == "" {
|
||||
return nil, nil
|
||||
}
|
||||
archiveAbs := filepath.Join(projectAbs, archiveOnDisk)
|
||||
parties, err := os.ReadDir(archiveAbs)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
|
@ -114,7 +127,13 @@ func computePending(ctx context.Context, decider policy.Decider,
|
|||
}
|
||||
party := p.Name()
|
||||
partyAbs := filepath.Join(archiveAbs, party)
|
||||
receivedAbs := filepath.Join(partyAbs, "received")
|
||||
// Per-party canonical folder resolution (Received/ vs received/).
|
||||
receivedSeg, _ := zddc.ResolveCanonical(partyAbs, "received")
|
||||
issuedSeg, _ := zddc.ResolveCanonical(partyAbs, "issued")
|
||||
if receivedSeg == "" {
|
||||
continue // party with no received/ at all → nothing to review
|
||||
}
|
||||
receivedAbs := filepath.Join(partyAbs, receivedSeg)
|
||||
|
||||
// ACL: skip parties whose received/ subtree the caller can't read.
|
||||
// Filtering at the party level is cheaper than per-entry and matches
|
||||
|
|
@ -123,7 +142,10 @@ func computePending(ctx context.Context, decider policy.Decider,
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
receivedURLPrefix := "/" + project + "/archive/" + party + "/received/"
|
||||
// URL prefix preserves the on-disk casing so links resolve
|
||||
// directly against the canonicalisation done by the URL
|
||||
// dispatcher (no additional case-fold round-trip needed).
|
||||
receivedURLPrefix := "/" + project + "/" + archiveOnDisk + "/" + party + "/" + receivedSeg + "/"
|
||||
if allowed, _ := policy.AllowFromChain(ctx, decider, chain, email, receivedURLPrefix); !allowed {
|
||||
continue
|
||||
}
|
||||
|
|
@ -132,13 +154,15 @@ func computePending(ctx context.Context, decider policy.Decider,
|
|||
// is WORM-readable to anyone with party access by design, and
|
||||
// we just need the set membership for matching).
|
||||
issuedTrackings := map[string]bool{}
|
||||
if entries, err := os.ReadDir(filepath.Join(partyAbs, "issued")); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, tracking, _, _, ok := zddc.ParseTransmittalFolder(e.Name()); ok {
|
||||
issuedTrackings[tracking] = true
|
||||
if issuedSeg != "" {
|
||||
if entries, err := os.ReadDir(filepath.Join(partyAbs, issuedSeg)); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, tracking, _, _, ok := zddc.ParseTransmittalFolder(e.Name()); ok {
|
||||
issuedTrackings[tracking] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +196,7 @@ func computePending(ctx context.Context, decider policy.Decider,
|
|||
lastModified: modTime,
|
||||
}
|
||||
if stagedFolder, hasDraft := stagedByTracking[tracking]; hasDraft {
|
||||
sub.stagedURL = "/" + project + "/staging/" + url.PathEscape(stagedFolder) + "/"
|
||||
sub.stagedURL = "/" + project + "/" + stagingOnDisk + "/" + url.PathEscape(stagedFolder) + "/"
|
||||
if stagedInfo, err := os.Stat(filepath.Join(stagingAbs, stagedFolder)); err == nil {
|
||||
if stagedInfo.ModTime().After(modTime) {
|
||||
sub.lastModified = stagedInfo.ModTime()
|
||||
|
|
|
|||
Loading…
Reference in a new issue