Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:

- name: Publish release candidate
if: github.event.release.prerelease
run: npm publish --access public --tag next --provenance
run: npm publish --access public --tag=canary --provenance

- name: Publish
if: "!github.event.release.prerelease"
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Agent-friendly CLI for managing & debugging Upstash resources from your terminal

## Installation

Requires Node.js 20 or newer.

```bash
npm i -g @upstash/cli
```
Expand Down Expand Up @@ -70,6 +72,7 @@ upstash qstash stats --qstash-id $QSTASH_ID --period 7d
upstash blob create --name my-bucket --visibility private
upstash blob list
upstash blob credentials --bucket-id $BUCKET_ID
upstash blob upload ./assets --bucket-id $BUCKET_ID --prefix assets

# Team
upstash team list
Expand All @@ -78,6 +81,61 @@ upstash team add-member --team-id $TEAM_ID --member-email you@example.com --role

Run `upstash --help` (or `--help` on any subcommand) to discover everything else, and check the [full docs](https://upstash.com/docs/agent-resources/cli) for the complete catalog. `upstash blob credentials` returns temporary S3 credentials for use with AWS CLI, rclone, or an S3 SDK.

## Uploading Blob files and folders

Set `UPSTASH_BLOB_TOKEN` in your environment or `.env` file, then run:

```bash
upstash blob upload ./assets --prefix assets
```

No Upstash login, account email, or management API key is required when using a
bucket token. You can also provide the token explicitly or select another env file:

```bash
upstash blob upload ./assets --token "$BLOB_TOKEN" --prefix assets
upstash --env-path ./uploads.env blob upload ./assets --prefix assets
upstash blob credentials --token "$BLOB_TOKEN"
```

`--token` overrides `UPSTASH_BLOB_TOKEN`. Exported environment variables take
precedence over values loaded from `.env` or `--env-path`. Use the Blob bucket
token, not temporary S3 credentials, so the CLI can refresh credentials throughout
the transfer.

Alternatively, use `--bucket-id $BUCKET_ID` with your saved Upstash login or
Developer API credentials. An explicit bucket ID overrides the ambient token;
`--token` and `--bucket-id` cannot be combined. AWS CLI and manually exported S3
credentials are not needed. A directory uploads its contents recursively: `./assets/images/logo.png`
becomes `assets/images/logo.png` with the prefix above, or `images/logo.png` without
a prefix. A single file uploads under its filename. Content types are inferred
from filenames, falling back to `application/octet-stream`.

The Blob SDK streams files, uses multipart for large files, and refreshes temporary
credentials throughout the upload, including between parts of one large file.
Transient failures are retried. Four files upload concurrently by default; use
`--concurrency 1` to reduce memory usage. Progress goes to stderr and the final JSON
summary goes to stdout. `--quiet` suppresses progress.

```bash
upstash blob upload ./assets --prefix assets --dry-run
upstash blob upload ./assets --prefix assets --skip-existing
```

`--dry-run` lists local files and destination paths without authenticating or
making network requests. By default existing keys are overwritten. `--skip-existing`
skips any existing key **without comparing size or contents**; use it to rerun an
interrupted upload only when the already uploaded objects are the versions you want.
An incomplete individual file starts again on rerun. Files are not deleted from the
bucket. Symlinks and empty directories are skipped.

On a failed file, the command stops scheduling more files, waits for active uploads,
prints a summary with failed and remaining files, and exits unsuccessfully. Ctrl+C
stops scheduling work and closes local streams; in-flight requests may take time
to settle. Completed objects remain in the bucket. A process kill, or a network
failure that also blocks cleanup, may leave an incomplete multipart upload. It does
not expire on its own; remove it with the Blob SDK's `abortStaleMultipartUploads`.

## Telemetry

The CLI identifies itself to the Upstash API on each request, so we can see which
Expand Down
42 changes: 38 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@
"author": "Upstash",
"license": "MIT",
"dependencies": {
"@upstash/blob": "0.0.5",
"commander": "^13.0.0",
"dotenv": "^16.4.5"
"dotenv": "^16.4.5",
"mime": "4.1.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0",
"vitest": "^2.0.0"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
17 changes: 13 additions & 4 deletions src/commands/blob/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,18 @@ interface BucketTokenSource {
unauthorizedRetries: number;
}

function resolveBucketToken(
flags: { bucketId?: string },
export function resolveBucketToken(
flags: { bucketId?: string; token?: string },
command: Command,
): Promise<BucketTokenSource> {
if (flags.token !== undefined) {
if (flags.bucketId !== undefined) {
return Promise.reject(new Error("Use either --token or --bucket-id, not both"));
}
const token = flags.token.trim();
if (!token) return Promise.reject(new Error("--token must be a non-empty Blob bucket token"));
return Promise.resolve({ token, unauthorizedRetries: 0 });
}
if (flags.bucketId) {
const auth = resolveAuth(command);
return request<BlobBucket>(auth, "GET", `/v2/blob/bucket/${flags.bucketId}`).then((bucket) => {
Expand All @@ -179,7 +187,7 @@ function resolveBucketToken(

return Promise.reject(
new Error(
"Blob credentials require either --bucket-id with Upstash account authentication or a non-empty UPSTASH_BLOB_TOKEN environment variable",
"Provide --token, UPSTASH_BLOB_TOKEN in the environment or .env, or --bucket-id with Upstash account authentication",
),
);
}
Expand All @@ -191,14 +199,15 @@ export function registerBlobCredentials(blob: Command): void {
"Get temporary S3 credentials for a Blob bucket; expiresAt is the credential expiry",
)
.option("--bucket-id <id>", "Blob bucket ID")
.option("--token <token>", "Blob bucket token; no management API key needed (overrides UPSTASH_BLOB_TOKEN)")
.addHelpText(
"after",
`
With --bucket-id, a bucket created in the last few minutes is polled for up
to ~30s until provisioning finishes, so it is safe to run right after create.
`,
)
.action(async (flags: { bucketId?: string }, command: Command) => {
.action(async (flags: { bucketId?: string; token?: string }, command: Command) => {
const source = await resolveBucketToken(flags, command);
const credentials = await fetchBlobCredentials(source.token, sleep, {
unauthorizedRetries: source.unauthorizedRetries,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/blob/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { registerBlobList } from "./list.js";
import { registerBlobGet } from "./get.js";
import { registerBlobDelete } from "./delete.js";
import { registerBlobCredentials } from "./credentials.js";
import { registerBlobUpload } from "./upload.js";

export function registerBlob(program: Command): void {
const blob = program.command("blob").description("Manage Blob buckets");
Expand All @@ -13,4 +14,5 @@ export function registerBlob(program: Command): void {
registerBlobGet(blob);
registerBlobDelete(blob);
registerBlobCredentials(blob);
registerBlobUpload(blob);
}
Loading
Loading