uploads

package
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package uploads is a thin HTTP client for Shannon Cloud's /api/v1/uploads endpoints — POST to publish a file, GET to list the current user's still- active uploads, DELETE to retract one. POST streams a multipart/form-data body via io.Pipe so 50 MiB files never sit in memory in full. All three methods classify HTTP responses into typed errors that callers can branch on and retry transient failures (5xx + network) with exponential backoff.

Index

Constants

View Source
const (
	KindSessionShare = "session_share"
	KindReport       = "report"
	KindLandingPage  = "landing_page"
	KindImage        = "image"
	KindOther        = "other"
)

Kind enumerates the business-purpose buckets Cloud accepts on POST /uploads. Cloud enforces these via a CHECK constraint (migration 130) — adding a new value here without a corresponding cloud migration causes 400 invalid_kind. Leaving Kind empty in UploadOptions lets Cloud default to "other" server-side.

Variables

View Source
var (
	// ErrUnauthorized is a 401. Permanent — fix the API key.
	ErrUnauthorized = errors.New("upload: unauthorized")
	// ErrBadRequest is a 400 (missing_file, malformed_multipart). Permanent — client bug.
	ErrBadRequest = errors.New("upload: bad request")
	// ErrEndpointNotFound is a 404. Permanent — the gateway answered but does
	// not have /api/v1/uploads mounted. Usually means the deployment doesn't
	// include the uploads handler yet, or cloud.endpoint points at a wrong host.
	ErrEndpointNotFound = errors.New("upload: endpoint not deployed")
	// ErrInvalidKind is a 400 with code "invalid_kind" — the kind value is not
	// in Cloud's CHECK-constrained whitelist. Permanent client bug; same shape
	// as ErrBadRequest but split out so daemon handler and tool layer can map
	// it to a more actionable error message instead of generic "bad request".
	ErrInvalidKind = errors.New("upload: invalid kind")
	// ErrFileTooLarge is a 413. Permanent — file exceeds the 50 MiB server limit.
	ErrFileTooLarge = errors.New("upload: file too large")
	// ErrServerConfig is a 500 with code "s3_unconfigured". Permanent — server-side fix needed.
	ErrServerConfig = errors.New("upload: server misconfigured")
	// ErrNotFound is a 404 on the Delete path — the upload id does not exist,
	// has already been retracted, or belongs to another user. Cloud
	// deliberately conflates these three cases to avoid existence leaks, so
	// callers must surface a single "not found or already retracted" message
	// without trying to disambiguate.
	ErrNotFound = errors.New("upload: not found")
	// ErrTransient wraps 500 (other reasons) / 502 / 503 / 504 / network errors.
	// The client retries these internally before returning; once returned, retries
	// have already been exhausted.
	ErrTransient = errors.New("upload: transient")
)

Sentinel errors. Callers wrap with errors.Is to decide retry policy and how to surface the failure to the user.

Functions

func IsValidKind added in v0.1.11

func IsValidKind(s string) bool

IsValidKind reports whether s is in the upload-kind whitelist. Empty string is NOT valid — callers that want "no preference" should pass "" to Upload and let Cloud default to "other", but list filters require an explicit value.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client posts files to the Cloud uploads endpoint.

func NewClient

func NewClient(baseURL, apiKey string, httpClient *http.Client) *Client

NewClient builds a Client. baseURL should be the gateway base (no trailing slash, e.g. "https://api-dev.shannon.run"). httpClient is required — pass the GatewayClient's existing *http.Client so timeouts and any future tracing transport are inherited rather than reinvented.

func (*Client) Delete added in v0.1.9

func (c *Client) Delete(ctx context.Context, id string) (*DeleteResponse, error)

Delete calls DELETE /api/v1/uploads/{id} and returns the parsed response. id must be a UUID; the client does not pre-validate format (cloud answers 404 for malformed ids, same as for legitimately missing ones).

Delete is idempotent on the server (a second call after a successful first returns 404 because deleted_at is non-NULL and the WHERE clause filters the row out), so 5xx retries are safe: the worst case is one extra 404 on a later retry, which the caller surfaces as "already retracted".

func (*Client) List added in v0.1.9

func (c *Client) List(ctx context.Context, opts ListOptions) (*ListResponse, error)

List calls GET /api/v1/uploads with the supplied options and returns the parsed response. Limit/Offset are passed through as-is; cloud clamps limit to [1, 100] internally (and 0 → default 20), but callers are encouraged to validate before calling so error messages stay close to the user. A non-empty Kind not in validUploadKinds short-circuits with ErrBadRequest.

func (*Client) Upload

func (c *Client) Upload(
	ctx context.Context,
	openBody func() (io.ReadCloser, error),
	opts UploadOptions,
) (*UploadResponse, error)

Upload streams body to /api/v1/uploads as multipart/form-data and returns the parsed response. openBody is a factory: it MUST be cheap to call and MUST return a fresh, fully-rewound reader each time, because each retry reissues the request and consumes the body afresh.

All UploadOptions fields are optional. Filename empty falls back to "upload"; ContentType empty defers to the server's extension sniff; Kind empty lets Cloud default to "other"; Metadata nil/empty omits the field entirely. A non-empty Kind not in validUploadKinds short-circuits with ErrBadRequest before any HTTP call (defends against typos before they cost a round trip).

func (*Client) UploadEphemeral added in v0.2.6

func (c *Client) UploadEphemeral(
	ctx context.Context,
	openBody func() (io.ReadCloser, error),
	opts UploadOptions,
) (*UploadResponse, error)

UploadEphemeral streams body to /api/v1/uploads/ephemeral and returns the parsed response. Unlike Upload, the ephemeral endpoint does NOT record the file in the user's upload library (it never shows up in List / the Published Files UI and can't be retracted) — it just stores the bytes on the public CDN and returns an unguessable permanent URL. Use it for assets that are referenced by URL but aren't user-managed "published files", e.g. agent avatars. Only Filename and ContentType are honored; Kind/Metadata are ignored by this endpoint (it has no library row to classify).

type DeleteResponse added in v0.1.9

type DeleteResponse struct {
	Deleted            bool   `json:"deleted"`
	ID                 string `json:"id"`
	CDNEvictionSeconds int    `json:"cdn_eviction_seconds"`
}

DeleteResponse mirrors the JSON returned by DELETE /api/v1/uploads/{id} on success. CDNEvictionSeconds is the worst-case window during which CloudFront edge nodes may still serve cached content — surface it to the user so they don't think the retract silently failed when the URL "still works" for a few minutes.

type ListOptions added in v0.1.11

type ListOptions struct {
	Limit  int
	Offset int
	// Kind filters the response to a single business-purpose bucket. Empty
	// returns all kinds. A non-empty value not in validUploadKinds returns
	// ErrBadRequest before any HTTP call.
	Kind string
}

ListOptions bundles the query parameters for GET /api/v1/uploads. Zero values for Limit and Offset map to Cloud defaults (limit=20, offset=0); empty Kind disables the filter.

type ListResponse added in v0.1.9

type ListResponse struct {
	Uploads    []UploadEntry `json:"uploads"`
	TotalCount int           `json:"total_count"`
}

ListResponse mirrors the JSON returned by GET /api/v1/uploads on success. TotalCount is the user's active (non-deleted) file count under the current filters — it is not "everything they've ever published".

type UploadEntry added in v0.1.9

type UploadEntry struct {
	ID          string          `json:"id"`
	URL         string          `json:"url"`
	Filename    string          `json:"filename"`
	ContentType string          `json:"content_type"`
	Size        int64           `json:"size"`
	Kind        string          `json:"kind,omitempty"`
	Metadata    json.RawMessage `json:"metadata,omitempty"`
	CreatedAt   string          `json:"created_at"` // RFC3339 UTC
}

UploadEntry is a single record in GET /api/v1/uploads. Cloud omits s3_key / tenant_id / user_id by design — do not assume they exist.

type UploadOptions added in v0.1.11

type UploadOptions struct {
	// Filename overrides the multipart Part filename. Empty falls back to
	// "upload" (server then sniffs by extension on the bytes).
	Filename string
	// ContentType overrides the stored MIME. Empty falls back to the server's
	// extension-based sniff, ending in application/octet-stream.
	ContentType string
	// Kind is the business-purpose bucket — see KindXxx constants. Empty is
	// allowed and means "let Cloud default to other". A non-empty value not in
	// validUploadKinds short-circuits with ErrBadRequest before any HTTP call.
	Kind string
	// Metadata is a pre-marshaled JSON object (≤ 8 KiB). Empty/nil = no
	// metadata field on the multipart envelope; Cloud stores {} in that case.
	// Must be a JSON object — arrays / scalars are rejected by Cloud with
	// invalid_metadata, but the client doesn't pre-validate shape (callers are
	// expected to marshal from a struct/map).
	Metadata json.RawMessage
}

UploadOptions bundles the optional knobs for POST /api/v1/uploads. All fields are optional individually; the empty struct uploads as application/ octet-stream with no business classification (Cloud defaults Kind to "other").

type UploadResponse

type UploadResponse struct {
	URL         string          `json:"url"`
	Key         string          `json:"key"`
	Size        int64           `json:"size"`
	ContentType string          `json:"content_type"`
	Kind        string          `json:"kind,omitempty"`
	Metadata    json.RawMessage `json:"metadata,omitempty"`
}

UploadResponse mirrors the JSON returned by POST /api/v1/uploads on success. Use URL directly — its path segments are already percent-encoded server-side.