nwsync: upload sink, key-addressed CLI, fail-closed publication marker (#73)
build-binaries / build-binaries (push) Successful in 2m18s
build-binaries / build-binaries (push) Successful in 2m18s
Builds the code half of #60, and closes the CLI gap the #53 sweep found: PR #71 shipped #53's original surface rather than the one #56, #62 and #65 settled. ## What lands **`depot.KeyStore`** — `ProbeKey` / `PutReader` / `GetKey`, addressing the zone by object key rather than by depot sha. NWSync cannot use the sha-addressed path: a blob is named after the sha1 of its *uncompressed* bytes while the body uploaded is the compressed form, and Bunny's `Checksum` header is sha256 of the body. Per #55 this reuses `internal/depot`'s `httpBackend` — same IPv4-pinned transport, same retry, same tri-state probe — and the sha-addressed `Backend` is now rewritten on top of it. No second HTTP client, no per-instance hash-function fields: the caller passes the key and the checksum, which turned out simpler than #55 expected. **A sink in `internal/nwsync`** — the zone by default, a local tree under `--out DIR` as the conformance path. Blobs upload as they are produced and the index lands last, so the presence of an index is the publication marker. A blob already in the zone is skipped via #55's probe *without* paying for compression (the body is a thunk) — which matters for the backfill, where compression is the expensive part. **The settled CLI** ``` nwsync emit [--as NAME] [--out DIR] <artifact-key> <file> nwsync assemble --group-id N [--tlk-key KEY] [--out DIR] <artifact-key>... ``` Artifact keys are depot keys; an index lives beside its artifact with the extension replaced (#62), derived in exactly one place so `emit` and `assemble` cannot disagree. Flags may now follow positionals — Go's `flag` stops at the first non-flag argument, which cost a run during #59. **Fail-closed in two places** — an artifact key whose embedded digest does not match the file is refused (publishing an index under the wrong key silently pairs a manifest with the wrong artifact), and `assemble` refuses an artifact with no index rather than publishing a manifest missing a hak. ## Checks `make check` green. Six new tests run against a Bunny-shaped `httptest` zone that verifies the `Checksum` header the way Bunny does: blobs-then-index ordering, skip-if-present, no index after a failed upload, key/file mismatch, and assemble reading indexes back out of the zone. Conformance re-run through the new CLI against upstream `nwn_nwsync_write` 2.1.2 over `sow_vfxs_01.hak` (#59's oracle): the manifest is still **byte-identical**. ## Not in this PR - **Live upload against the real zone.** The nwsync zone and its credential are #61, still open. Everything here is proven against a fake zone only. - **Consumer wiring** — #65, in the three producer repos. - **The mid-hak failure *policy*.** The mechanism is here (fail closed, orphan blobs left, re-run resumes); whether a module release may proceed when an emit failed is a human call, still open on #60. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #73 Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #73.
This commit is contained in:
+54
-43
@@ -12,7 +12,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,37 +44,63 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
||||
|
||||
func printRunUsage(w io.Writer) {
|
||||
fmt.Fprint(w, `usage:
|
||||
nwsync emit <artifact> --out DIR
|
||||
nwsync assemble --order NAMES --entries DIR --out DIR [--group-id N]
|
||||
nwsync emit [--as NAME] [--out DIR] <artifact-key> <file>
|
||||
nwsync assemble --group-id N [--tlk-key KEY] [--out DIR] <artifact-key>...
|
||||
|
||||
emit explodes one .hak/.erf or one loose file (the TLK) into NWSync blobs plus
|
||||
a NSYM manifest covering only that artifact. assemble merges those per-artifact
|
||||
manifests into one, reading no bulk data.
|
||||
a NSYM index covering only that artifact, and uploads both. assemble merges
|
||||
those indexes into one manifest, reading no bulk data. Artifact keys are depot
|
||||
keys; an index lives beside its artifact, with the extension replaced.
|
||||
|
||||
--out DIR writes to a local repository tree instead of uploading, which is the
|
||||
conformance path against upstream nwn_nwsync_write. Without it, the zone comes
|
||||
from NWSYNC_STORAGE_ZONE, NWSYNC_STORAGE_PASSWORD and BUNNY_STORAGE_HOST.
|
||||
`)
|
||||
}
|
||||
|
||||
// parseArgs parses flags that may appear before, after or between positionals.
|
||||
// Go's flag package stops at the first non-flag argument, which turns
|
||||
// `emit <key> <file> --out DIR` into a confusing arity error.
|
||||
func parseArgs(fs *flag.FlagSet, args []string) ([]string, error) {
|
||||
var positional []string
|
||||
for {
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rest := fs.Args()
|
||||
if len(rest) == 0 {
|
||||
return positional, nil
|
||||
}
|
||||
positional = append(positional, rest[0])
|
||||
args = rest[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func runEmit(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("emit", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
out := fs.String("out", "", "output directory (blobs plus the per-artifact manifest)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
as := fs.String("as", "", "published name of the artifact, when it differs from the key")
|
||||
out := fs.String("out", "", "write to a local repository tree instead of uploading")
|
||||
positional, err := parseArgs(fs, args)
|
||||
if err != nil {
|
||||
return exitUsage
|
||||
}
|
||||
if fs.NArg() != 1 {
|
||||
fmt.Fprintf(stderr, "nwsync emit: exactly one artifact is required\n")
|
||||
return exitUsage
|
||||
}
|
||||
if *out == "" {
|
||||
fmt.Fprintf(stderr, "nwsync emit: --out is required\n")
|
||||
if len(positional) != 2 {
|
||||
fmt.Fprintf(stderr, "nwsync emit: <artifact-key> and <file> are both required\n")
|
||||
return exitUsage
|
||||
}
|
||||
|
||||
result, err := Emit(fs.Arg(0), *out)
|
||||
result, err := Emit(EmitOptions{
|
||||
ArtifactKey: positional[0],
|
||||
ArtifactPath: positional[1],
|
||||
As: *as,
|
||||
OutDir: *out,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "nwsync emit: %v\n", err)
|
||||
return exitInternal
|
||||
}
|
||||
fmt.Fprintf(stdout, "emitted %s: %d resources, %d new blobs, manifest %s\n",
|
||||
fmt.Fprintf(stdout, "emitted %s: %d resources, %d new blobs, index %s\n",
|
||||
result.Name, result.Entries, result.BlobsWritten, result.ManifestPath)
|
||||
return exitOK
|
||||
}
|
||||
@@ -83,47 +108,33 @@ func runEmit(args []string, stdout, stderr io.Writer) int {
|
||||
func runAssemble(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("assemble", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
order := fs.String("order", "", "comma-separated artifact names, highest priority first")
|
||||
entries := fs.String("entries", "", "directory holding the per-artifact .nsym files")
|
||||
out := fs.String("out", "", "repository root; the manifest lands in <out>/manifests")
|
||||
tlkKey := fs.String("tlk-key", "", "depot key of the TLK, which shadows nothing and merges last")
|
||||
out := fs.String("out", "", "write to a local repository tree instead of uploading")
|
||||
groupID := fs.Int("group-id", 0, "NWSync group id (1 = current, 2 = testing; 0 omits it)")
|
||||
moduleName := fs.String("module-name", "", "module name recorded in the sidecar")
|
||||
description := fs.String("description", "", "description recorded in the sidecar")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
positional, err := parseArgs(fs, args)
|
||||
if err != nil {
|
||||
return exitUsage
|
||||
}
|
||||
switch {
|
||||
case *order == "":
|
||||
fmt.Fprintf(stderr, "nwsync assemble: --order is required\n")
|
||||
if len(positional) == 0 {
|
||||
fmt.Fprintf(stderr, "nwsync assemble: at least one artifact key is required\n")
|
||||
return exitUsage
|
||||
case *entries == "":
|
||||
fmt.Fprintf(stderr, "nwsync assemble: --entries is required\n")
|
||||
return exitUsage
|
||||
case *out == "":
|
||||
fmt.Fprintf(stderr, "nwsync assemble: --out is required\n")
|
||||
return exitUsage
|
||||
}
|
||||
|
||||
names := make([]string, 0, 8)
|
||||
for _, name := range strings.Split(*order, ",") {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := Assemble(AssembleOptions{
|
||||
Order: names,
|
||||
EntriesDir: *entries,
|
||||
OutDir: *out,
|
||||
GroupID: *groupID,
|
||||
ModuleName: *moduleName,
|
||||
Description: *description,
|
||||
ArtifactKeys: positional,
|
||||
TLKKey: *tlkKey,
|
||||
OutDir: *out,
|
||||
GroupID: *groupID,
|
||||
ModuleName: *moduleName,
|
||||
Description: *description,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "nwsync assemble: %v\n", err)
|
||||
return exitInternal
|
||||
}
|
||||
fmt.Fprintf(stdout, "assembled %s: %d resources from %d artifacts\n",
|
||||
result.SHA1, result.Entries, len(names))
|
||||
fmt.Fprintf(stdout, "assembled manifest %s: %d resources, %s\n",
|
||||
result.SHA1, result.Entries, result.ManifestPath)
|
||||
return exitOK
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user