Go SDK for Upstash Blob. Standard library only.
go get github.com/upstash/blob-goimport blob "github.com/upstash/blob-go"
bucket, err := blob.NewBucketFromEnv() // reads UPSTASH_BLOB_TOKEN, or blob.NewBucket(token)
f, _ := os.Open("me.png")
b, err := bucket.Put(ctx, "avatars/me.png", f, nil)
b.URL // "" on a private bucket
b.ETag
got, err := bucket.Get(ctx, "avatars/me.png") // record + Body; close it
info, err := bucket.Info(ctx, "avatars/me.png")
ok, err := bucket.Exists(ctx, "avatars/me.png")
page, err := bucket.List(ctx, &blob.ListOptions{Prefix: "avatars/", Limit: 100}) // page.Cursor
bucket.Copy(ctx, "tmp/9f3c", "avatars/7.png", nil)
bucket.Move(ctx, "tmp/9f3c", "avatars/7.png", &blob.CopyOptions{ContentType: "image/png"})
bucket.Delete(ctx, "a.png") // also DeleteMany, DeletePrefix, DeleteAll
blob.UpdateJSON(ctx, bucket, "u/7.json", func(prev *Settings) (Settings, error) { ... }, nil)Puttakes anyio.Reader. A*bytes.Reader,*strings.Reader,*bytes.Buffer, regular*os.Fileor otherio.ReaderAtthat is anio.Seekeris measured, read in place, and retried on a failed request. Any otherio.Seekeris measured and goes up once. Any other reader needsSize, and streams up once, orMaxSize, and is buffered up to it and retried. A file's content type comes from its extension, elseapplication/octet-stream.ContentTypes: []string{"image/*"}checks the declared type and the body's first bytes;MaxSize: 20*blob.MBrefuses anything larger, while streaming too.- A body over 16 MB goes up as multipart, four parts at a time;
Multipart: blob.MultipartOver(100*blob.MB),blob.MultipartAlwaysorblob.MultipartNevermove the line.NoOverwriteandIfUnchangedare single-PUT only. GetandInfofail withnot_foundrather than returning nil;Listreturns keys, sizes, etags and URLs, without metadata.UpdateJSONis a compare-and-set loop, retried on conflict up toMaxAttemptstimes (default 6).CopyandMovetakeContentType,Cache,Metadata; whatever is not given is carried over.Moveis a copy plus a delete: a failed delete ismove_left_a_copy.- Metadata keys are lowercase and values printable ASCII; anything else is
invalid_input. Cachetakesblob.CacheImmutable,blob.CacheRevalidate,blob.CacheNoStore, a duration ("15m","3600","1h30m"), or a verbatim header ("s-maxage=60, public").- Sizes count bytes.
blob.KB,MB,GB,TBare powers of 1,000 andKiB,MiB,GiB,TiBpowers of 1,024:32*blob.MBrefuses a 32 MiB file.blob.ParseSize("32MiB")reads one from config. Options.Retryconfigures retries (Retries,Backoff,Disabled); a Retry-After from the service wins over a custom backoff.
read, err := bucket.SignedReadURL(ctx, "private/report.pdf", &blob.SignedReadURLOptions{DownloadAs: "report.pdf"})
up, err := bucket.SignedUploadURL(ctx, "u/7/report.pdf", &blob.SignedUploadURLOptions{ContentType: "application/pdf", Size: size})
// PUT up.URL with up.Headers, and Request.ContentLength set to the sizeLinks are signed by Upstash and live at most 10 minutes (read default: 5); ExpiresAt is the
answer per link. Headers on an upload URL are pinned into the signature. A private bucket has no
URL on any blob and PublicURL returns "".
NewUploadHandler serves the protocol of @upstash/blob/browser and @upstash/blob/react, so a
React app uploads straight to storage through a Go backend, with multipart, resume, progress and a
completion callback.
uploads, err := blob.NewUploadHandler(blob.UploadHandlerOptions[*User]{
Context: requireUser, // func(*http.Request) (*User, error); return a 401 to refuse
OnBeforeUpload: func(a blob.BeforeUploadArgs[*User]) (blob.BeforeUploadResult, error) {
return blob.BeforeUploadResult{Path: blob.UniquePath("u/" + a.Ctx.ID + "/" + a.File.Name)}, nil
},
OnUploadComplete: func(a blob.UploadCompleteArgs[*User]) (any, error) {
return map[string]string{"url": a.URL}, nil // reaches the browser as upload.blob.data
},
Routes: map[string]blob.UploadRoute[*User]{
"avatar": {ContentTypes: []string{"image/*"}, MaxSize: 5 * blob.MB},
"video": {ContentTypes: []string{"video/*"}, MaxSize: 2 * blob.GB},
},
})
http.Handle("/api/upload", uploads)const upload = useUpload('avatar'); // POSTs to /api/upload?route=avatar- Leave
Bucketnil to useUPSTASH_BLOB_TOKEN. Every handler option is a default a route may replace; on a route,ContentTypes: []string{}orMaxSize: blob.NoLimitclears the handler's. Inputvalidates what the browser sends asinput;blob.JSONInput[T]()decodes it into aT.OnUploadCompletemay run more than once, or never if the browser stops before the last phase; write idempotently onUploadID, which is stable across deliveries. Returning an error deletes the object.Staterides in a signed but readable token: an id, never a secret.OnErrorsees every refusal; return a*blob.Errorto answer with it. A callback error that is or wraps a*blob.Erroranswers with its code; one with aStatusCode() intmethod answers with that status; any other error is logged and answered as a 500 with no detail.- The handler speaks the same versioned upload protocol as
@upstash/blob'suploadHandler, so any release of the browser client works against it.
A multipart upload that was never finished is invisible to List and blocks bucket deletion. The
SDK aborts the ones it starts; sweep the rest from a cron:
bucket.AbortStaleMultipartUploads(ctx, 24*time.Hour, "uploads/")c, err := bucket.S3(ctx)
cfg, _ := config.LoadDefaultConfig(ctx, config.WithRegion(c.Region),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(c.AccessKeyID, c.SecretAccessKey, c.SessionToken)))
s3c := s3.NewFromConfig(cfg, func(o *s3.Options) { o.BaseEndpoint = &c.Endpoint })The credentials are short-lived; call S3 again after c.Expires.
Everything the SDK refuses is a *blob.Error with a Code from a closed list, a Status, and a
printable message. errors.Is(err, blob.ErrNotFound) and blob.HasCode(err, blob.CodeTooLarge)
match by code; its JSON is the shape the TypeScript SDK's browser half decodes.
Credential and signing requests carry the SDK version, Go version, and platform. Set
UPSTASH_DISABLE_TELEMETRY or Options.DisableTelemetry.
go vet ./... && go test -race ./...Live tests run against real buckets when UPSTASH_BLOB_PUBLIC_TOKEN and
UPSTASH_BLOB_PRIVATE_TOKEN are set, and write only under test/go/.