Update documentation relating to the public packages and http_upload API

This commit is contained in:
2026-06-07 13:33:27 -05:00
parent c12ec64066
commit 25fbfc4677
12 changed files with 615 additions and 29 deletions

View File

@@ -10,11 +10,12 @@ Run the maintained local example:
go run ./cmd/distributor run --config examples/local-publish.yml
```
Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/bundle` to build, write, parse, and validate local source bundles with the same manifest contract used by the CLI. They can use `gitea.maximumdirect.net/eric/distributor/pkg/upload` to build or validate a bundle and submit it to `distributor serve` with bearer authentication and idempotency keys. See [Source bundle contract](docs/integrations/source-bundle.md) and [HTTP upload contract](docs/integrations/http-upload.md).
Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/upload` and `gitea.maximumdirect.net/eric/distributor/pkg/bundle` to submit compatible bundles to `distributor serve`. See [Upstream producer integration](docs/consumers/api.md).
- [CLI reference](docs/cli.md)
- [Configuration reference](docs/config.md)
- [Operations guide](docs/operations.md)
- [Consumer API guide](docs/consumers/api.md)
- [Troubleshooting](docs/troubleshooting.md)
- [Integration contracts](docs/integrations/source-bundle.md)
- [Development architecture](docs/policy/architecture.md)

127
docs/consumers/api.md Normal file
View File

@@ -0,0 +1,127 @@
# Upstream Producer Integration
Audience: developers and LLM coding agents adding `distributor` support to an upstream Go producer application.
This document is the copyable implementation guide for submitting producer outputs to a `distributor` pipeline whose source backend is `http_upload`.
## Required Inputs
The upstream application needs these values from deployment or operator configuration:
- distributor endpoint: the HTTP server base URL, such as `https://distributor.example.com`;
- upload token: bearer token for exactly one configured `http_upload` pipeline;
- generated files: regular local files to include in the source bundle;
- bundle id: stable identifier for this producer output;
- idempotency key: stable key for retrying the same producer operation.
Do not put destination routing, public URLs, transform settings, or credentials in the source manifest. Those belong in the `distributor` pipeline configuration.
## Recommended Workflow
Use `gitea.maximumdirect.net/eric/distributor/pkg/upload`.
For most producers, use `UploadFiles`. It accepts producer-generated files, builds a temporary valid source bundle with `pkg/bundle`, uploads a gzip-compressed tar archive, and removes temporary files when the call returns.
Use `UploadBundle` only when the producer already assembled a complete bundle directory containing `manifest.json`.
Add the dependency from the upstream application:
```sh
go get gitea.maximumdirect.net/eric/distributor
```
## Minimal Go Example
```go
package reports
import (
"context"
"errors"
"fmt"
"os"
"time"
"gitea.maximumdirect.net/eric/distributor/pkg/bundle"
"gitea.maximumdirect.net/eric/distributor/pkg/upload"
)
func SubmitReport(reportPath, summaryPath string) error {
endpoint := os.Getenv("DISTRIBUTOR_UPLOAD_ENDPOINT")
token := os.Getenv("DISTRIBUTOR_UPLOAD_TOKEN")
if endpoint == "" || token == "" {
return fmt.Errorf("distributor endpoint and token are required")
}
reportID := "weather.hourly.brentwood.2026-06-07T15"
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
client, err := upload.NewClient(upload.ClientOptions{
Endpoint: endpoint,
Token: token,
})
if err != nil {
return err
}
result, err := client.UploadFiles(ctx, upload.UploadFilesOptions{
ID: reportID,
IdempotencyKey: reportID,
Files: []bundle.BundleFile{
{SourcePath: reportPath, Path: "report.md"},
{SourcePath: summaryPath, Path: "summary.txt"},
},
})
if err != nil {
var conflict *upload.IdempotencyConflictError
if errors.As(err, &conflict) {
return fmt.Errorf("idempotency key was reused for different bundle content: %w", err)
}
return err
}
fmt.Printf("distributor accepted run %s\n", result.RunID)
return nil
}
```
## Producer Responsibilities
- Use a stable bundle id for the producer output, such as a report type plus logical timestamp.
- Use a stable idempotency key for cross-process retries of the same producer operation.
- Map each generated file to a clean slash-separated bundle path, such as `report.md` or `assets/chart.png`.
- Include only regular files. Symlinks, directories as files, devices, FIFOs, and sockets are rejected.
- Keep file contents stable after upload inputs are selected. Bundle digests are calculated from file bytes.
- Treat upload success as admission only. `UploadFiles` and `UploadBundle` return after the server accepts and validates the upload, not after all destinations publish.
Valid bundle paths are relative slash paths. They must not be empty, absolute, contain backslashes, contain `.` or `..` path segments, contain empty path segments, or use reserved basenames `manifest.json` or `.distributor.json`.
## Idempotency And Status
`pkg/upload` sends `Idempotency-Key` on every upload. If the caller omits one, the package generates a random key for that call and reuses it for in-process retries. That is enough for transient network retry within one process.
For producer jobs that may retry after process restart, supply a stable key derived from the producer operation, such as the report id or job id. Reusing the same key with the same normalized source manifest returns the original accepted run. Reusing the same key with different source content returns a conflict.
`Status` polls `/runs/<run-id>` while the distributor server retains the in-memory status record. Status values are `accepted`, `queued`, `running`, `succeeded`, and `failed`. Completed records expire according to the server's `server.http.retention` setting, and server restart clears status and idempotency records.
Optional status check:
```go
status, err := client.Status(ctx, result.RunID)
if err != nil {
return err
}
if status.Status == "failed" {
return fmt.Errorf("distributor run failed: %s", status.Error)
}
```
## References
In the `distributor` source tree:
- `docs/consumers/pkg-upload.md`: Go upload package workflow.
- `docs/consumers/pkg-bundle.md`: Go bundle package workflow.
- `docs/integrations/http-upload.md`: canonical HTTP upload wire contract.
- `docs/integrations/source-bundle.md`: canonical source bundle file-format contract.

View File

@@ -0,0 +1,88 @@
# `pkg/bundle`
Audience: upstream Go producer developers and LLM coding agents using `distributor` source bundle helpers.
Import path:
```go
import "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
```
`pkg/bundle` builds, writes, parses, and validates local source bundles. Use it directly when a producer writes bundles for `distributor` to discover, or when a producer wants to assemble and validate a bundle before using another transport.
The canonical source bundle file-format contract is [Source Bundle Contract](../integrations/source-bundle.md).
## Preferred Complete-Bundle Workflow
Use `WriteBundle` when producer-generated files live outside the final bundle root.
```go
manifest, err := bundle.WriteBundle(bundle.WriteBundleOptions{
Root: "/var/spool/distributor/weather/hourly-2026-06-07T15",
ID: "weather.hourly.brentwood.2026-06-07T15",
Files: []bundle.BundleFile{
{SourcePath: "/tmp/weather/report.md", Path: "report.md"},
{SourcePath: "/tmp/weather/summary.txt", Path: "summary.txt"},
},
})
if err != nil {
return err
}
_ = manifest
```
`WriteBundle` copies each source file into a staged bundle root, writes `manifest.json`, validates the staged bundle, and promotes it into place. Set `Overwrite: true` only when the producer intentionally replaces an existing bundle root.
## Existing Bundle Root Workflow
Use `BuildManifest` and `WriteManifest` when files are already staged under the final bundle root.
```go
root := "/var/spool/distributor/weather/hourly-2026-06-07T15"
manifest, err := bundle.BuildManifest(bundle.BuildOptions{
Root: root,
ID: "weather.hourly.brentwood.2026-06-07T15",
Files: []string{"report.md", "summary.txt"},
})
if err != nil {
return err
}
if err := bundle.WriteManifest(root, manifest, bundle.WriteManifestOptions{}); err != nil {
return err
}
if err := bundle.ValidateBundle(root, manifest); err != nil {
return err
}
```
Use `Scan: true` instead of `Files` only when every valid regular file under the root should be included. Scan mode includes dotfiles, skips reserved metadata files, rejects symlinks, and sorts paths lexically.
## Paths And Ordering
Bundle paths are slash-separated paths relative to the bundle root.
Invalid paths include:
- empty paths;
- absolute paths;
- paths containing backslashes;
- `.` or `..` path segments;
- empty path segments;
- any basename of `manifest.json` or `.distributor.json`.
Explicit file lists preserve caller order. File order is part of the bundle digest, so producers should choose it deliberately and keep it stable.
## Validation And Digest Helpers
Use `ValidateBundle` before handing an existing local bundle to another process. It verifies manifest semantics, file existence, regular-file type, file size, per-file SHA-256 digests, and bundle digest.
Useful helpers:
- `LoadManifest`: read `manifest.json` from a bundle root.
- `ParseManifest` and `MarshalManifest`: parse or write manifest bytes.
- `ValidateManifest`: validate manifest-only semantics.
- `FileDigest`, `BundleDigest`, and `ValidateDigest`: digest helpers for diagnostics and tests.
## Boundaries
`pkg/bundle` does not upload bundles, publish destinations, transform Markdown, select pipelines, configure credentials, or write destination state. Those concerns belong to `pkg/upload` or the `distributor` application.

View File

@@ -0,0 +1,118 @@
# `pkg/upload`
Audience: upstream Go producer developers and LLM coding agents submitting bundles to `distributor serve`.
Import path:
```go
import "gitea.maximumdirect.net/eric/distributor/pkg/upload"
```
`pkg/upload` is the producer-facing HTTP upload client. It builds on `pkg/bundle`, packages valid source bundles as gzip-compressed tar archives, sends bearer authentication, includes idempotency keys, and exposes a status polling helper.
`UploadFiles` examples also use:
```go
import "gitea.maximumdirect.net/eric/distributor/pkg/bundle"
```
The canonical HTTP wire contract is [HTTP Upload API Contract](../integrations/http-upload.md).
## Client Construction
```go
client, err := upload.NewClient(upload.ClientOptions{
Endpoint: "https://distributor.example.com",
Token: token,
})
if err != nil {
return err
}
```
`Endpoint` is the distributor server base URL. The client derives `/upload` and `/runs/<run-id>`. `Token` is required and is sent as `Authorization: Bearer <token>`. Token values are redacted from client errors.
`HTTPClient` and `Retry` are optional. Defaults use a 30 second HTTP timeout and safe retry settings.
## Upload Producer Files
Use `UploadFiles` when the producer has generated output files but has not assembled a bundle directory.
```go
result, err := client.UploadFiles(ctx, upload.UploadFilesOptions{
ID: "weather.hourly.brentwood.2026-06-07T15",
IdempotencyKey: "weather.hourly.brentwood.2026-06-07T15",
Files: []bundle.BundleFile{
{SourcePath: "/tmp/weather/report.md", Path: "report.md"},
{SourcePath: "/tmp/weather/summary.txt", Path: "summary.txt"},
},
})
if err != nil {
return err
}
_ = result.RunID
```
`UploadFiles` creates a temporary bundle, writes and validates a manifest, uploads the archive, and removes temporary files when the call returns. It does not write into producer source directories.
## Upload An Existing Bundle
Use `UploadBundle` when the producer already has a complete local bundle root containing `manifest.json`.
```go
result, err := client.UploadBundle(ctx, upload.UploadBundleOptions{
Root: "/var/spool/weather/hourly-2026-06-07T15",
IdempotencyKey: "weather.hourly.brentwood.2026-06-07T15",
})
if err != nil {
return err
}
_ = result.RunID
```
`UploadBundle` validates the local bundle by default and uploads only `manifest.json` plus manifest-listed files. Unlisted files are not uploaded.
## Result And Status
Upload success means the server returned `202 Accepted` after staging and validating the upload. It does not mean all configured destinations have published.
Poll status while the server retains the in-memory run record:
```go
status, err := client.Status(ctx, result.RunID)
if err != nil {
return err
}
if status.Status == "failed" {
return fmt.Errorf("distributor run failed: %s", status.Error)
}
```
Status values are `accepted`, `queued`, `running`, `succeeded`, and `failed`. Completed records expire according to `server.http.retention`; server restart clears run status and idempotency records.
## Idempotency And Retry
Every upload request includes `Idempotency-Key`.
If `IdempotencyKey` is omitted, the client generates a random 128-bit lowercase hexadecimal key for that upload operation and reuses it for retries within the same call. For cross-process retry safety, producers should pass a stable key derived from the producer job or report id.
The client retries only safe cases:
- `503 Service Unavailable`;
- temporary network errors;
- ambiguous mid-upload failures.
It does not retry after `202 Accepted` and does not retry `400`, `401`, `409`, `413`, or `415`.
Detect conflicting key reuse with `errors.As`:
```go
var conflict *upload.IdempotencyConflictError
if errors.As(err, &conflict) {
return fmt.Errorf("idempotency key was reused for different bundle content: %w", err)
}
```
## Boundaries
`pkg/upload` does not configure server pipelines, choose destinations, wait for publication completion automatically, persist client queues, provide durable idempotency across server restarts, or expose destination state. It submits complete source bundles to the configured HTTP upload API.

View File

@@ -108,7 +108,7 @@ The uploaded archive size and extracted bundle size are bounded by the selected
## Go Producer Helper
Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/upload` to build or validate source bundles, package them as gzip-compressed tar archives, and submit them to this API:
Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/upload` to build or validate source bundles, package them as gzip-compressed tar archives, and submit them to this API. See [Upstream Producer Integration](../consumers/api.md) for the copyable upstream implementation guide and [`pkg/upload`](../consumers/pkg-upload.md) for package-specific workflow guidance.
```go
client, err := upload.NewClient(upload.ClientOptions{

View File

@@ -60,7 +60,7 @@ File order is significant. Explicit file lists preserve caller order. Scan mode
## Producer APIs
Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/bundle` to build and validate this contract:
Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/bundle` to build and validate this contract. See [`pkg/bundle`](../consumers/pkg-bundle.md) for producer workflow guidance.
- `BuildManifest`: builds a manifest from explicit file paths or scan mode.
- `WriteManifest`: writes `manifest.json`, optionally replacing an existing manifest.
@@ -68,7 +68,7 @@ Go producers can use `gitea.maximumdirect.net/eric/distributor/pkg/bundle` to bu
- `LoadManifest`, `ParseManifest`, `ValidateManifest`, and `ValidateBundle`: parse and validate local bundles.
- `FileDigest`, `BundleDigest`, and `ValidateDigest`: digest helpers.
Go producers that submit bundles to `distributor serve` can use `gitea.maximumdirect.net/eric/distributor/pkg/upload`. It builds on `pkg/bundle`, packages valid bundles as gzip-compressed tar uploads, sends bearer authentication, and includes idempotency keys for safe retry behavior. See [HTTP Upload API Contract](http-upload.md).
Go producers that submit bundles to `distributor serve` can use `gitea.maximumdirect.net/eric/distributor/pkg/upload`. See [Upstream Producer Integration](../consumers/api.md) and [HTTP Upload API Contract](http-upload.md).
CLI producers can use:

View File

@@ -148,18 +148,7 @@ curl -X POST http://127.0.0.1:8080/upload \
--data-binary @bundle.tar.gz
```
Go producer applications can use `pkg/upload` instead of constructing archives and HTTP requests directly. The package sends `Idempotency-Key` on every upload, derives `/upload` from the configured endpoint, and reuses the same key and replayable request body for safe retries:
```go
client, err := upload.NewClient(upload.ClientOptions{
Endpoint: "http://127.0.0.1:8080",
Token: token,
})
result, err := client.UploadBundle(ctx, upload.UploadBundleOptions{
Root: "examples/source-bundle",
IdempotencyKey: "producer.run.20260604T120000Z",
})
```
Go producer applications can use `pkg/upload` instead of constructing archives and HTTP requests directly. See [Upstream Producer Integration](consumers/api.md) for the copyable producer implementation guide.
The maintained example client uses the local upload server and reads the token from `DISTRIBUTOR_EXAMPLE_UPLOAD_TOKEN`. It generates an idempotency key by default; set `DISTRIBUTOR_EXAMPLE_UPLOAD_IDEMPOTENCY_KEY` when a retry must be stable across separate process runs.

View File

@@ -197,6 +197,7 @@ Use this current layout unless the project has a documented reason to differ:
- `cmd/distributor`: application entrypoint only.
- `pkg/bundle`: public producer-facing source manifest model, digest logic, parsing, manifest building, complete local bundle writing, and local validation helpers.
- `pkg/upload`: public producer-facing HTTP upload client built on `pkg/bundle`.
- `internal/app`: application orchestration and top-level use cases.
- `internal/cli`: CLI command definitions, flags, argument parsing, and command wiring.
- `internal/config`: configuration structs, defaults, loading, precedence, and validation.
@@ -323,9 +324,9 @@ Important tests include:
Documentation should follow the project documentation policy. Keep user docs focused on implemented behavior. Put future, planned, or aspirational work only under `docs/roadmap/`.
When changing architecture, config, CLI behavior, adapters, manifest/state contracts, transform behavior, publish behavior, or component contracts, update the relevant docs and examples in the same change.
When changing architecture, config, CLI behavior, adapters, manifest/state contracts, transform behavior, publish behavior, public package/API behavior, or component contracts, update the relevant docs and examples in the same change.
The source manifest and destination `.distributor.json` schemas should have canonical documentation once implemented. Example configs should be valid and load-tested where practical.
The source manifest and destination `.distributor.json` schemas should have canonical documentation once implemented. Producer-facing package and API workflows belong under `docs/consumers/`. Example configs should be valid and load-tested where practical.
## Non-Goals

View File

@@ -23,7 +23,7 @@ Use it with `docs/policy/architecture.md` and `docs/policy/documentation.md`.
- `internal/transform/markdown`: Markdown-to-HTML transform.
- `internal/notify`: notification interface and current no-op notifier.
- `internal/testutil`: shared test fixtures. Production code must not import this package.
- `docs`: current user, operator, policy, internal, and roadmap documentation.
- `docs`: current user, operator, consumer, integration, policy, internal, and roadmap documentation.
- `examples`: copyable example configs and source bundles.
Do not create new top-level package families such as public `pkg/...` packages
@@ -211,5 +211,7 @@ Follow `docs/policy/documentation.md`.
- Keep `docs/config.md` canonical for user-facing config reference.
- Keep `docs/cli.md` canonical for command syntax and workflows.
- Keep `docs/operations.md` canonical for operational and recovery behavior.
- Keep `docs/consumers/` canonical for public package and consumer API workflows.
- Keep `docs/integrations/` canonical for external file-format and wire-protocol contracts.
- Keep `docs/internal/` focused on implemented package contracts.
- Update docs in the same change as behavior when public behavior, config, CLI, examples, or internal contracts change.
- Update docs in the same change as behavior when public behavior, public packages/APIs, config, CLI, examples, or internal contracts change.

View File

@@ -2,12 +2,13 @@
## Purpose
Project documentation must help four audiences:
Project documentation must help five audiences:
1. users who need to run the application;
2. administrators/operators who need to configure and operate it;
3. developers who need to understand and change it safely;
4. LLM coding agents that need clear scope, boundaries, and invariants.
4. LLM coding agents that need clear scope, boundaries, and invariants;
5. developers and LLM coding agents integrating this project from another codebase.
Docs should be accurate, concise, task-oriented, and organized by audience. Prefer links to canonical docs over repetition.
@@ -46,7 +47,9 @@ Canonical homes:
- CLI reference: `docs/cli.md`
- operations and recovery: `docs/operations.md`
- troubleshooting: `docs/troubleshooting.md`
- public API/package consumer guidance: `docs/consumers/`
- implemented internals: `docs/internal/`
- external protocol, service, and file-format contracts: `docs/integrations/`
- future work: `docs/roadmap/`
- contributor workflow: `docs/policy/development.md`
- copyable examples: `examples/`
@@ -119,6 +122,15 @@ Recommended:
- `docs/troubleshooting.md`
- validated examples under `examples/`
### Project with public packages or consumer APIs
Required:
- `docs/consumers/api.md`
- one `docs/consumers/pkg-<name>.md` file per public package, if public packages exist
Recommended:
- copyable consumer examples under `examples/`, if practical
## Required Documents
### README.md
@@ -244,6 +256,33 @@ Each entry should include:
- safe fix;
- relevant links.
### docs/consumers/
**Audience:** developers and LLM coding agents integrating this project from another codebase
Required for projects with public packages, SDKs, client APIs, plugin APIs, or other application-facing integration surfaces.
This directory describes how an external codebase should consume the project's public API. It should be task-oriented and copyable where useful. It is not the place for internal implementation details or operator procedures.
`docs/consumers/api.md` should provide the consumer-facing overview and primary implementation workflow. It should include:
1. intended consumer audience and use cases;
2. required inputs supplied by operators or deployment configuration;
3. recommended public package or API workflow;
4. minimal copyable example;
5. consumer responsibilities and boundaries;
6. retry, idempotency, or status behavior, if applicable;
7. links to package-specific docs and canonical integration contracts.
Package-specific docs should be named `pkg-<name>.md` and should include:
1. import path;
2. intended use cases;
3. primary types and functions needed by consumers;
4. minimal examples;
5. validation, error, retry, and boundary behavior;
6. links to canonical file-format or wire-protocol contracts.
### docs/internal/
**Audience:** developers, LLM coding agents
@@ -289,7 +328,7 @@ Roadmap docs should not be confused with current behavior.
Required for projects that depend on external CLIs, APIs, services, protocols, or file formats where the integration contract is important to maintain.
This directory contains concise, versioned reference notes for external integration contracts. It should document only the parts of the external system that this project actually uses.
This directory contains concise, versioned reference notes for external integration contracts. It should document only the parts of the external system that this project actually uses or exposes.
Use one file per integration where useful.
@@ -348,6 +387,7 @@ Before merging documentation changes, verify:
- `docs/policy/architecture.md` describes development principles.
- Future work appears only under `docs/roadmap/`.
- User-facing docs avoid unnecessary internals.
- Consumer-facing docs explain public APIs without duplicating integration contracts.
- Developer-facing docs preserve boundaries and invariants.
- Config examples match the schema.
- CLI examples match real commands and flags.

View File

@@ -1,9 +1,104 @@
// Package bundle provides producer-facing helpers for distributor source
// bundle manifests.
// bundles.
//
// A source bundle is a local directory containing a manifest.json file and the
// files listed by that manifest. This package owns the public manifest model,
// digest calculation, path validation, manifest parsing, manifest building,
// local bundle writing, and local bundle validation used by Go producer
// applications.
// A source bundle is a local directory containing manifest.json and the files
// listed by that manifest. Producer applications use this package when they
// need to generate manifests, validate bundles locally, or write complete
// bundle directories for distributor to discover, upload, or publish.
//
// # Bundle Contract
//
// The source manifest is the producer-to-distributor contract. It is named by
// ManifestName, currently "manifest.json", and uses SchemaVersion, currently 1.
// A Manifest contains:
//
// - SchemaVersion: the source manifest schema version.
// - ID: the producer's stable bundle identifier.
// - Digest: the canonical digest of the ordered file records.
// - Created: an RFC3339 timestamp when marshaled to JSON.
// - Files: an ordered list of ManifestFile records.
//
// Each ManifestFile records a slash-separated bundle-relative Path, a lowercase
// sha256:<64 hex> SHA256 digest, and a byte Size. File order is significant for
// the bundle digest and should be chosen deliberately by the producer. Explicit
// file lists preserve caller order; scan mode sorts by slash-separated path.
//
// # Path Rules
//
// Public bundle paths are always slash-separated and relative to the bundle
// root. ValidateSourcePath rejects empty paths, absolute paths, path traversal,
// dot segments, backslashes, and reserved manifest/state paths. Source files
// must be regular files; symlinks and other special files are rejected.
//
// BuildManifest with Scan true recursively scans Root, includes regular files
// including dotfiles, excludes manifest.json and .distributor.json, rejects
// symlinks, and sorts paths lexically. BuildManifest with Files uses exactly
// the caller-provided paths and preserves their order. Exactly one selection
// mode must be used.
//
// # Manifest Workflows
//
// BuildManifest reads existing files under a local root, calculates each
// ManifestFile, defaults a zero Created value to the current UTC time, calculates
// the bundle digest, and validates the result. WriteManifest writes
// manifest.json and fails if it already exists unless WriteManifestOptions has
// Overwrite set. LoadManifest reads and parses manifest.json. ParseManifest and
// MarshalManifest are useful when an application stores or transmits manifest
// bytes directly; MarshalManifest validates before writing deterministic,
// indented JSON with a trailing newline.
//
// ValidateManifest checks manifest-only semantics, including schema version,
// required fields, path safety, duplicate file paths, digest syntax, file sizes,
// and bundle digest. ValidateBundle checks a supplied Manifest against local
// files under a root, including existence, regular-file type, size, SHA-256
// digest, path safety, and bundle digest.
//
// # Complete Bundle Writing
//
// WriteBundle is the most convenient producer workflow when source files live
// outside the final bundle directory. It copies each BundleFile.SourcePath into
// a staged bundle at BundleFile.Path, builds and writes a compliant manifest,
// validates the staged bundle, and promotes it to WriteBundleOptions.Root.
// Overwrite permits replacement of an existing bundle root using a best-effort
// sibling temporary and backup strategy.
//
// # Digest Helpers
//
// FileDigest returns the sha256:<64 hex> digest for file bytes. BundleDigest
// returns the canonical bundle digest for an ordered []ManifestFile.
// CanonicalFilePayload returns the JSON payload used by BundleDigest, which is
// mainly useful for tests and diagnostics. ValidateDigest checks digest syntax.
//
// Example: build and write a manifest for files already under a bundle root.
//
// root := "/var/lib/reports/daily-2026-06-06"
// manifest, err := bundle.BuildManifest(bundle.BuildOptions{
// Root: root,
// ID: "reports.daily.2026-06-06",
// Files: []string{"report.md", "summary.txt"},
// })
// if err != nil {
// return err
// }
// if err := bundle.WriteManifest(root, manifest, bundle.WriteManifestOptions{}); err != nil {
// return err
// }
// if err := bundle.ValidateBundle(root, manifest); err != nil {
// return err
// }
//
// Example: create a complete bundle from producer-generated files.
//
// manifest, err := bundle.WriteBundle(bundle.WriteBundleOptions{
// Root: "/var/lib/distributor-source/daily-2026-06-06",
// ID: "reports.daily.2026-06-06",
// Files: []bundle.BundleFile{
// {SourcePath: "/tmp/report.md", Path: "report.md"},
// {SourcePath: "/tmp/summary.txt", Path: "summary.txt"},
// },
// })
// if err != nil {
// return err
// }
// _ = manifest
package bundle

125
pkg/upload/doc.go Normal file
View File

@@ -0,0 +1,125 @@
// Package upload provides producer-facing helpers for submitting distributor
// source bundles to the HTTP upload API.
//
// The package is intended for Go producer applications that already create
// reports or other Markdown bundle contents and want to hand those bundles to a
// running distributor server. It builds on pkg/bundle for manifest generation,
// path validation, digest calculation, local bundle writing, and local bundle
// validation. It does not expose distributor internals, server configuration,
// storage backends, destination state, or publish behavior.
//
// # Client Construction
//
// NewClient creates a Client from ClientOptions. Endpoint is required and must
// be an http or https distributor server base URL without userinfo, query, or
// fragment. The client derives /upload for submissions and /runs/<run-id> for
// status checks. Token is required and is sent as Authorization: Bearer <token>.
// Token values are redacted from errors produced by the client.
//
// HTTPClient is optional. When omitted, the package uses a client with a
// conservative timeout. Retry is optional; zero values select safe defaults.
// RetryOptions.MaxAttempts, BaseDelay, and MaxDelay must not be negative, and
// MaxDelay must be greater than or equal to BaseDelay.
//
// # Upload Workflows
//
// UploadBundle uploads an existing local source bundle root. The root must
// contain manifest.json. By default, UploadBundle loads the manifest and
// validates the complete local bundle with pkg/bundle before making any HTTP
// request. The generated gzip-compressed tar archive contains manifest.json and
// exactly the manifest-listed files; unlisted files are not uploaded.
//
// UploadFiles is the convenience workflow for producer applications that have
// generated files but have not yet assembled a bundle directory. It uses
// pkg/bundle to create a temporary complete bundle from explicit
// bundle.BundleFile values, validates it by default, archives it, uploads it,
// and removes temporary files when the call returns. UploadFiles does not write
// into producer source directories. A zero Created timestamp follows
// pkg/bundle defaulting behavior.
//
// Validation is enabled by default. Set DisableValidation when the application
// has already performed equivalent local validation and wants to skip the
// package's validation step. Validate and DisableValidation must not both be
// true.
//
// # Idempotency And Retry
//
// Every upload request includes Idempotency-Key. If UploadBundleOptions or
// UploadFilesOptions provides IdempotencyKey, the client validates and uses
// that value. Otherwise, it generates a random 128-bit lowercase hexadecimal
// key once for that upload operation and reuses it for all retries from that
// call.
//
// Generated idempotency keys are useful for retrying transient failures within
// a single process call. Producers that need cross-process retry safety should
// provide their own stable key, such as a key derived from the producer job id
// or report id. Valid keys are non-empty ASCII strings up to 128 bytes using
// letters, digits, '.', '_', '-', and ':'.
//
// The client retries only safe cases: 503 Service Unavailable, temporary
// network errors, and ambiguous mid-upload failures. Retries use the same
// idempotency key and replayable gzip archive body. The client does not retry
// 400, 401, 409, 413, 415, or any response after 202 Accepted. Context
// cancellation is honored before each attempt and while waiting between
// retries.
//
// # Results And Errors
//
// Result represents upload admission. A successful UploadBundle or UploadFiles
// call means the server accepted the upload and returned a run id; it does not
// mean the asynchronous distribution run has finished successfully.
//
// Status fetches the current server status for a run id and returns RunStatus.
// This is a separate polling helper; upload calls do not wait for publication
// completion.
//
// Non-2xx upload and status responses return *HTTPError when the server status
// can be represented as an HTTP failure. HTTPError includes the numeric status
// code, HTTP status string, response message, and server retryable flag when
// present. A 409 Conflict response is returned as *IdempotencyConflictError,
// which wraps HTTPError and can be detected with errors.As.
//
// Example: upload producer files with a stable idempotency key.
//
// ctx := context.Background()
// client, err := upload.NewClient(upload.ClientOptions{
// Endpoint: "https://distributor.example.com",
// Token: os.Getenv("DISTRIBUTOR_UPLOAD_TOKEN"),
// })
// if err != nil {
// return err
// }
//
// result, err := client.UploadFiles(ctx, upload.UploadFilesOptions{
// ID: "reports.daily.2026-06-06",
// IdempotencyKey: "reports.daily.2026-06-06",
// Files: []bundle.BundleFile{
// {SourcePath: "/tmp/report.md", Path: "report.md"},
// {SourcePath: "/tmp/summary.txt", Path: "summary.txt"},
// },
// })
// if err != nil {
// var conflict *upload.IdempotencyConflictError
// if errors.As(err, &conflict) {
// return fmt.Errorf("upload conflicts with an earlier different bundle: %w", err)
// }
// return err
// }
//
// status, err := client.Status(ctx, result.RunID)
// if err != nil {
// return err
// }
// _ = status
//
// Example: upload an existing bundle root.
//
// result, err := client.UploadBundle(ctx, upload.UploadBundleOptions{
// Root: "/var/lib/reports/daily-2026-06-06",
// IdempotencyKey: "reports.daily.2026-06-06",
// })
// if err != nil {
// return err
// }
// _ = result
package upload