# Upload and delete files

> File and stdin uploads, progress streaming, --raw flag, vcup rm with proxy or raw URLs, and BLOB_STORE_URL requirement for proxy deletes.

- Repository: MaxLeiter/vcup
- GitHub: https://github.com/MaxLeiter/vcup
- Human docs: https://grok-wiki.com/public/docs/maxleiter-vcup-05c16ae77ebb
- Complete Markdown: https://grok-wiki.com/public/docs/maxleiter-vcup-05c16ae77ebb/llms-full.txt

## Source Files

- `cli.ts`
- `README.md`
- `api/upload.ts`
- `api/delete.ts`
- `api/f.ts`

---

---
title: "Upload and delete files"
description: "File and stdin uploads, progress streaming, --raw flag, vcup rm with proxy or raw URLs, and BLOB_STORE_URL requirement for proxy deletes."
---

The `vcup` CLI in `cli.ts` uploads file or stdin bytes to `POST /api/upload` with a chunked stderr progress bar, prints either the proxy URL (`url`) or Vercel Blob URL (`raw`), and deletes objects via `vcup rm` by sending the resolved blob URL to `DELETE /api/delete`. Proxy URLs (`/f/...`) require client-side `BLOB_STORE_URL` to map slug → store URL before delete; raw URLs skip that step.

## Prerequisites

| Requirement | Where it applies |
| --- | --- |
| `~/.vcuprc` or `VCUP_API_URL` + `VCUP_TOKEN` | Every upload and delete |
| Deployed vcup API with `VCUP_TOKEN` matching the CLI token | `api/upload.ts`, `api/delete.ts` |
| `BLOB_STORE_URL` on the **client** | `vcup rm` when the argument is a proxy URL containing `/f/` |
| `BLOB_STORE_URL` on the **server** | Serving files at `/f/:slug` via `api/f.ts` (not upload itself) |

<Note>
Upload and delete handlers authenticate with the same shared secret. Configure the CLI before running commands — see [Configure the CLI](/configure-cli).
</Note>

## Upload a file

<Steps>
<Step title="Pick a path">
Pass one file path as the first non-flag argument. The CLI resolves it, rejects missing paths and directories, reads the full file into memory, and sets `X-Filename` to `basename(filePath)`.
</Step>
<Step title="Stream with progress">
`progressStream` emits 64 KiB chunks to the request body while rewriting stderr with a percentage bar, transferred/total sizes, and throughput. When the stream finishes, the progress line is cleared.
</Step>
<Step title="Read the printed URL">
On success the CLI prints one line to stdout: the proxy URL by default, or the raw blob URL when `--raw` is present.
</Step>
</Steps>

<RequestExample>
```bash
vcup screenshot.png
```
</RequestExample>

<ResponseExample>
```text
https://your-vcup.vercel.app/f/screenshot-abc123.png
```
</ResponseExample>

The server stores the object with `@vercel/blob` `put()`, `access: "public"`, and `addRandomSuffix: true`, then returns JSON:

<ResponseField name="url" type="string">
Proxy URL: `{baseUrl}/f/{slug}` where `slug` is the blob pathname (random suffix included).
</ResponseField>

<ResponseField name="raw" type="string">
Direct Vercel Blob URL returned by `put()`.
</ResponseField>

`baseUrl` is `VCUP_BASE_URL` when set, otherwise `x-forwarded-proto` + `Host` from the upload request.

## Upload from stdin

When no file argument is given and stdin is not a TTY, the CLI buffers all stdin chunks, uses filename `paste.txt`, and runs the same upload path as a file upload.

<CodeGroup>
```bash title="Pipe text"
echo "hello world" | vcup
```
```bash title="Pipe a file"
cat logs.txt | vcup
```
</CodeGroup>

<Warning>
Stdin uploads always use `X-Filename: paste.txt`. There is no flag to override the name from the CLI.
</Warning>

## `--raw` output

<ParamField body="--raw" type="flag">
When present anywhere in the upload argument list, stdout receives `data.raw` instead of `data.url` from the upload JSON response.
</ParamField>

<RequestExample>
```bash
vcup --raw image.png
```
</RequestExample>

<ResponseExample>
```text
https://abc123.public.blob.vercel-storage.com/image-xyz.png
```
</ResponseExample>

Use `--raw` when you need the store URL for tooling, direct blob access, or `vcup rm` without setting client `BLOB_STORE_URL`.

## Upload request shape

The CLI sends:

| Header | Value |
| --- | --- |
| `Authorization` | `Bearer {token}` from config |
| `X-Filename` | Basename of file path, or `paste.txt` for stdin |

| Body | Behavior |
| --- | --- |
| ReadableStream | Chunked stream from in-memory file/stdin buffer; Bun sets `duplex: "half"` on `fetch` |

:::endpoint POST /api/upload
Authenticated streaming upload to Vercel Blob.

**Required headers:** `Authorization: Bearer <VCUP_TOKEN>`, `X-Filename`

**Success (200):** `Content-Type: application/json` body `{ "url": string, "raw": string }`

**Errors:** `401` invalid/missing token, `400` missing `x-filename`, `405` non-POST
:::

## Delete a file (`vcup rm`)

`vcup rm <url>` and `vcup delete <url>` are equivalent. The URL may be either form returned from upload:

| URL type | Example shape | Client resolution |
| --- | --- | --- |
| Proxy | `https://host/f/{slug}` | If path contains `/f/`, slug = segment after `/f/`; blob URL = `{BLOB_STORE_URL}/{slug}` |
| Raw | `https://….public.blob.vercel-storage.com/...` | Used as-is for `X-Blob-Url` |

<Steps>
<Step title="Resolve blob URL">
Proxy URLs need `process.env.BLOB_STORE_URL` on the machine running the CLI. Without it, the CLI exits with an error instructing you to pass the raw URL or set the variable.
</Step>
<Step title="Call delete API">
`DELETE {VCUP_API_URL}/api/delete` with `Authorization: Bearer {token}` and `X-Blob-Url: {blobUrl}`.
</Step>
<Step title="Confirm">
Success prints `Deleted` to stdout. Non-OK responses print status and body to stderr and exit `1`.
</Step>
</Steps>

<RequestExample>
```bash
# Proxy URL from default upload output
vcup rm https://your-vcup.vercel.app/f/paste-a1b2c3.txt

# Raw blob URL (no client BLOB_STORE_URL needed)
vcup rm https://abc123.public.blob.vercel-storage.com/paste-a1b2c3.txt
```
</RequestExample>

:::endpoint DELETE /api/delete
Authenticated blob deletion via `@vercel/blob` `del()`.

**Required headers:** `Authorization: Bearer <VCUP_TOKEN>`, `X-Blob-Url` (full blob store URL)

**Success (200):** body `Deleted`

**Errors:** `401` unauthorized, `400` missing `x-blob-url`, `405` non-DELETE
:::

### Why proxy deletes need `BLOB_STORE_URL`

Proxy links expose only the slug under `/f/`. The delete API expects the **blob store URL**, not the proxy host path. The CLI reconstructs:

```text
blobUrl = ${BLOB_STORE_URL}/${slug}
```

where `slug` is everything after `/f/` in the pasted URL (may include path segments from `addRandomSuffix`).

<Info>
Server `BLOB_STORE_URL` (in Vercel project env) powers `GET /f/:slug` in `api/f.ts`. Client `BLOB_STORE_URL` is a separate concern used only when deleting via proxy URL from your laptop or CI.
</Info>

## End-to-end flows

```mermaid
sequenceDiagram
  participant User
  participant CLI as cli.ts
  participant Upload as api/upload.ts
  participant Blob as Vercel Blob
  participant Delete as api/delete.ts

  User->>CLI: vcup file.png
  CLI->>CLI: progressStream (64KB chunks)
  CLI->>Upload: POST /api/upload + X-Filename
  Upload->>Blob: put(filename, stream)
  Blob-->>Upload: blob.url
  Upload-->>CLI: JSON url + raw
  CLI-->>User: stdout url (or raw)

  User->>CLI: vcup rm proxy URL
  alt URL contains /f/
    CLI->>CLI: slug + BLOB_STORE_URL
  end
  CLI->>Delete: DELETE + X-Blob-Url
  Delete->>Blob: del(url)
  Delete-->>CLI: 200 Deleted
  CLI-->>User: Deleted
```

## CLI surface summary

| Command | stdin | Output |
| --- | --- | --- |
| `vcup <file>` | No (file path required) | Proxy `url` |
| `vcup --raw <file>` | No | `raw` blob URL |
| `pipe \| vcup` | Yes (`paste.txt`) | Proxy `url` |
| `vcup rm <url>` | No | `Deleted` |
| `vcup --help` | — | Help text |

| Failure (CLI) | Cause |
| --- | --- |
| Exit `1` missing config | No `~/.vcuprc` and incomplete env vars |
| `File not found` | Bad path |
| `Cannot upload a directory` | Directory path |
| `Cannot resolve proxy URL without BLOB_STORE_URL` | `vcup rm` on `/f/` URL without client env |
| `Upload failed` / `Delete failed` | Non-OK HTTP from API |

## Verification

<Steps>
<Step title="Upload">
Run `vcup <small-file>` and open the printed `/f/...` URL in a browser. The proxy should return `200` with `Content-Disposition: inline` when server `BLOB_STORE_URL` is set.
</Step>
<Step title="Raw URL">
Run `vcup --raw <file>` and confirm the hostname matches your blob store public URL.
</Step>
<Step title="Delete">
Run `vcup rm` with the same URL you printed (proxy or raw). A second delete or fetch should fail at the store/proxy layer.
</Step>
</Steps>

<Tip>
For proxy deletes from automation, either export client `BLOB_STORE_URL` to match the deployment store URL, or persist and pass `raw` URLs from `vcup --raw`.
</Tip>

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First upload, open the proxy link, and optional `--raw` output.
</Card>
<Card title="Proxy and raw URLs" href="/proxy-and-raw-urls">
`url` vs `raw`, `/f` rewrite, and when each URL type is required.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full command list, headers, exit codes, and help text.
</Card>
<Card title="API reference" href="/api-reference">
Upload, delete, and proxy GET handlers with status codes.
</Card>
<Card title="Environment variables" href="/environment-variables">
Server and client `BLOB_STORE_URL`, `VCUP_TOKEN`, and `VCUP_BASE_URL`.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
401/400/500/502 cases, directory uploads, and proxy delete errors.
</Card>
</CardGroup>
