From 55abce3448a9e1b421f9d8ade5d63274333d1374 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 7 May 2026 09:18:08 -0500 Subject: [PATCH] feat(fileapi): mirror staging transmittal folders into working/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a folder is created under /staging/ whose name parses as a ZDDC transmittal folder (YYYY-MM-DD_ () - ) and whose tracking number contains -TRN- or -SUB-, also create the same- named folder under <project>/working/ as a drafting space for staff. The mirror is one-way and one-shot: created at staging-mkdir time only. Renames and deletions of either side are not propagated. The transmittal client orchestrates cleanup at issue time (move files to archive/<recipient>/issued/, then delete both staging and working siblings) — the server stays out of that decision. -MDL- tracking deliberately skips the mirror; MDL deliverables live in archive/<party>/mdl/ rows, not via the working↔staging pairing. Implementation: mirrorStagingToWorking() in fileapi.go, called after a successful serveFileMkdir. EnsureCanonicalAncestors handles working/'s own auto-own .zddc; the mirror folder gets its own creator-grant on top. Six new tests cover -TRN-/-SUB- mirror, -MDL- skip, non-transmittal name skip, deep-path skip, and idempotency over a pre-existing sibling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- zddc/internal/handler/fileapi.go | 68 ++++++++++++ zddc/internal/handler/fileapi_test.go | 144 ++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) diff --git a/zddc/internal/handler/fileapi.go b/zddc/internal/handler/fileapi.go index 85e9590..25c2e38 100644 --- a/zddc/internal/handler/fileapi.go +++ b/zddc/internal/handler/fileapi.go @@ -581,11 +581,79 @@ func serveFileMkdir(cfg config.Config, w http.ResponseWriter, r *http.Request) { } } + // Staging↔working mirror: when a folder created under staging/ matches + // the ZDDC transmittal-folder grammar AND its tracking number contains + // -SUB- or -TRN-, also create the same-named folder under working/ as + // a drafting space for staff. The mirror is one-way and one-shot — + // renames or deletions of either side are not propagated. + if email != "" { + mirrorStagingToWorking(cfg, abs, email) + } + w.Header().Set("X-ZDDC-Source", "fileapi:mkdir") w.WriteHeader(http.StatusCreated) auditFile(r, "mkdir", cleanURL, http.StatusCreated, 0, nil) } +// mirrorStagingToWorking creates a paired drafting folder under working/ +// when newAbs is a transmittal-named folder under <project>/staging/. Best +// effort — failures are logged but do not affect the staging mkdir result. +// +// Eligibility: +// - newAbs's parent is exactly <project>/staging/ (case-fold) +// - filepath.Base(newAbs) parses as a transmittal folder +// (YYYY-MM-DD_<tracking> (<status>) - <title>) +// - tracking contains -SUB- or -TRN- (case-fold) +// +// Side effects on success: +// - <project>/working/ created if missing, with auto-own .zddc seeded +// (via EnsureCanonicalAncestors) +// - <project>/working/<sameName>/ created if missing, with its own +// auto-own .zddc (it's a child of the working/ canonical folder) +func mirrorStagingToWorking(cfg config.Config, newAbs, email string) { + rel, err := filepath.Rel(cfg.Root, newAbs) + if err != nil { + return + } + rel = filepath.ToSlash(rel) + parts := strings.Split(rel, "/") + if len(parts) != 3 { + // Mirror only fires for direct children of staging/. Deeper paths + // (staging/<name>/sub/) are user-managed. + return + } + if !strings.EqualFold(parts[1], "staging") { + return + } + + name := parts[2] + _, tracking, _, _, ok := zddc.ParseTransmittalFolder(name) + if !ok || !zddc.IsTrnOrSubTracking(tracking) { + return + } + + mirrorPath := filepath.Join(cfg.Root, parts[0], "working", name) + // Idempotent: skip if the working sibling already exists. + if info, err := os.Stat(mirrorPath); err == nil && info.IsDir() { + return + } + + // EnsureCanonicalAncestors creates working/ (with its own auto-own .zddc) + // if missing; we then MkdirAll the mirror folder itself and seed its + // auto-own grant. + if _, err := zddc.EnsureCanonicalAncestors(cfg.Root, mirrorPath, email, 0o755); err != nil { + slog.Warn("mirror: ensure ancestors", "path", mirrorPath, "err", err) + return + } + if err := os.MkdirAll(mirrorPath, 0o755); err != nil { + slog.Warn("mirror: mkdir", "path", mirrorPath, "err", err) + return + } + if err := zddc.WriteAutoOwnZddc(mirrorPath, email); err != nil { + slog.Warn("mirror: auto-own .zddc", "path", mirrorPath, "err", err) + } +} + // auditFile emits a structured log line for each file API operation. // AccessLogMiddleware already logs every request — this adds an // op-tagged line so audit consumers can filter by operation without diff --git a/zddc/internal/handler/fileapi_test.go b/zddc/internal/handler/fileapi_test.go index 8393696..da5971b 100644 --- a/zddc/internal/handler/fileapi_test.go +++ b/zddc/internal/handler/fileapi_test.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "strings" @@ -663,3 +664,146 @@ func TestFileAPI_StrictMode_AncestorDenyAbsolute(t *testing.T) { t.Fatalf("strict mode: vendor should be denied by root explicit-deny, got %d: %s", rec.Code, rec.Body.String()) } } + +// --- staging↔working mirror ------------------------------------------------- + +// stagingMirrorURL builds a URL-safe target path for a transmittal folder +// name with spaces and parens, mirroring how a real client would encode it. +func stagingMirrorURL(project, folder string) string { + return "/" + project + "/staging/" + url.PathEscape(folder) + "/" +} + +func TestFileAPI_StagingMirror_TRN(t *testing.T) { + _, do, root := fileAPITestSetup(t, nil, nil) + + folder := "2026-06-15_proj-EM-TRN-0042 (DFT) - Foundation Plans" + rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{ + "X-ZDDC-Op": "mkdir", + }) + if rec.Code != http.StatusCreated { + t.Fatalf("staging mkdir: want 201, got %d: %s", rec.Code, rec.Body.String()) + } + + // Staging side exists with auto-own. + stagingDir := filepath.Join(root, "Proj/staging", folder) + if info, err := os.Stat(stagingDir); err != nil || !info.IsDir() { + t.Fatalf("staging folder not created: err=%v", err) + } + if _, err := os.Stat(filepath.Join(stagingDir, ".zddc")); err != nil { + t.Errorf("staging auto-own .zddc missing: %v", err) + } + + // Working mirror exists with auto-own. + workingDir := filepath.Join(root, "Proj/working", folder) + if info, err := os.Stat(workingDir); err != nil || !info.IsDir() { + t.Fatalf("working mirror not created: err=%v", err) + } + mirrorZ, err := os.ReadFile(filepath.Join(workingDir, ".zddc")) + if err != nil { + t.Fatalf("working mirror auto-own .zddc missing: %v", err) + } + if !strings.Contains(string(mirrorZ), "alice@example.com: rwcda") { + t.Errorf("mirror .zddc missing creator grant: %s", mirrorZ) + } +} + +func TestFileAPI_StagingMirror_SUB(t *testing.T) { + _, do, root := fileAPITestSetup(t, nil, nil) + + folder := "2026-07-01_vendor-EM-SUB-0017 (RSA) - Review Notes" + rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{ + "X-ZDDC-Op": "mkdir", + }) + if rec.Code != http.StatusCreated { + t.Fatalf("mkdir: want 201, got %d", rec.Code) + } + if _, err := os.Stat(filepath.Join(root, "Proj/working", folder)); err != nil { + t.Errorf("SUB-tracked folder should mirror; got err=%v", err) + } +} + +func TestFileAPI_StagingMirror_NonTransmittalNameSkipped(t *testing.T) { + _, do, root := fileAPITestSetup(t, nil, nil) + + rec := do(http.MethodPost, "/Proj/staging/scratch/", "alice@example.com", nil, map[string]string{ + "X-ZDDC-Op": "mkdir", + }) + if rec.Code != http.StatusCreated { + t.Fatalf("mkdir: want 201, got %d", rec.Code) + } + // staging/scratch/ exists. + if _, err := os.Stat(filepath.Join(root, "Proj/staging/scratch")); err != nil { + t.Fatalf("staging/scratch not created: %v", err) + } + // No working/ sibling — name doesn't parse as transmittal. + if _, err := os.Stat(filepath.Join(root, "Proj/working/scratch")); !os.IsNotExist(err) { + t.Errorf("non-transmittal name must NOT mirror; got err=%v", err) + } +} + +func TestFileAPI_StagingMirror_MdlTrackingSkipped(t *testing.T) { + _, do, root := fileAPITestSetup(t, nil, nil) + + folder := "2026-06-15_proj-EM-MDL-0001 (IFR) - Master Deliverables List" + rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{ + "X-ZDDC-Op": "mkdir", + }) + if rec.Code != http.StatusCreated { + t.Fatalf("mkdir: want 201, got %d", rec.Code) + } + // MDL deliverables are tracked in archive/<party>/mdl/, not via the + // working↔staging pairing — no mirror. + if _, err := os.Stat(filepath.Join(root, "Proj/working", folder)); !os.IsNotExist(err) { + t.Errorf("-MDL- tracking must NOT mirror; got err=%v", err) + } +} + +func TestFileAPI_StagingMirror_DeepPathSkipped(t *testing.T) { + _, do, root := fileAPITestSetup(t, nil, nil) + + // mkdir of staging/<name>/sub/ (depth 4) — only depth-3 (immediate + // child of staging/) qualifies for mirroring. + folder := "2026-06-15_proj-EM-TRN-0042 (DFT) - x" + if err := os.MkdirAll(filepath.Join(root, "Proj/staging", folder), 0o755); err != nil { + t.Fatal(err) + } + rec := do(http.MethodPost, "/Proj/staging/"+url.PathEscape(folder)+"/sub/", "alice@example.com", nil, map[string]string{ + "X-ZDDC-Op": "mkdir", + }) + if rec.Code != http.StatusCreated && rec.Code != http.StatusOK { + t.Fatalf("deep mkdir: got %d: %s", rec.Code, rec.Body.String()) + } + // The transmittal folder did not get a mirror retroactively because + // the mirror only fires on depth-3 mkdirs. + if _, err := os.Stat(filepath.Join(root, "Proj/working", folder)); !os.IsNotExist(err) { + t.Errorf("deep mkdir should not retroactively mirror; got err=%v", err) + } +} + +func TestFileAPI_StagingMirror_Idempotent(t *testing.T) { + _, do, root := fileAPITestSetup(t, nil, nil) + + // Pre-create the working sibling with a sentinel file so we can detect + // if the mirror code blew it away. + folder := "2026-06-15_proj-EM-TRN-0042 (DFT) - existing" + mirrorDir := filepath.Join(root, "Proj/working", folder) + if err := os.MkdirAll(mirrorDir, 0o755); err != nil { + t.Fatal(err) + } + sentinel := filepath.Join(mirrorDir, "preexisting.md") + if err := os.WriteFile(sentinel, []byte("user content"), 0o644); err != nil { + t.Fatal(err) + } + + rec := do(http.MethodPost, stagingMirrorURL("Proj", folder), "alice@example.com", nil, map[string]string{ + "X-ZDDC-Op": "mkdir", + }) + if rec.Code != http.StatusCreated { + t.Fatalf("mkdir: want 201, got %d", rec.Code) + } + // Sentinel still exists — mirror was idempotent (no-op when sibling + // already present). + if _, err := os.Stat(sentinel); err != nil { + t.Errorf("idempotency: pre-existing content gone: %v", err) + } +}