Skip to content
s3nd
Menu

@s3nd/cli

A file between two machines, with a code.

Move a file with put and get, check that a bucket is actually set up to hold transfers with doctor, and keep the settings in a file you can commit. Straight to S3 with nothing deployed, or through your own server with a token.

npm install -g @s3nd/clinpx @s3nd/cli doctor
$ s3nd put ./report.pdf
report.pdf · 284 kB · expires in 1 day
K7QP2M4X

$ CODE=$(s3nd put ./report.pdf)          # the code is stdout, the rest is stderr
$ tar cz ./project | s3nd put - --name project.tar.gz

# on the other machine
$ s3nd get K7QP2M4X
Wrote /home/you/report.pdf · 284 kB

$ s3nd get K7QP2M4X -o -  | less        # or to stdout
$ s3nd rm K7QP2M4X
Burned K7QP2M4X

init and doctor

The command worth running first.

S3 misconfiguration fails late and vaguely. doctor performs the operations s3nd actually needs and reports what happened, rather than reading your policy and reasoning about it. The probe object is deleted before it returns.

a starting point per provider
$ s3nd init --provider r2 --bucket transfers
Wrote /home/you/transfers/s3nd.config.json

Put the three values in .env, and keep it out of git:
  R2_ACCOUNT_ID=
  R2_ACCESS_KEY_ID=
  R2_SECRET_ACCESS_KEY=
The R2 API token needs Object Read & Write on this bucket, and nothing else.
Give the bucket a lifecycle rule that deletes objects under "transfers/" after a day or two.
Run `s3nd doctor` it performs the operations s3nd needs and reports what happened.
and the check that earns the command
$ s3nd doctor
Using /home/you/transfers/s3nd.config.json
 Configuration: bucket "transfers", region "auto"
 Credentials: resolved, key ends in 1a2b
 Bucket reachable: HeadBucket succeeded
 Write, read, delete: round-tripped a probe object
! Expiry cleanup: no enabled expiration rule
 Add an S3 lifecycle rule that expires objects under this bucket after a day or
    two. Without it, expired transfers stay stored and billed.

1 check(s) failed.

That last check is the one nobody discovers until a bill arrives. expiresIn stops a transfer being handed over; only a lifecycle rule deletes the object. Pointed at a server, doctor checks the one thing that matters there, a real round trip, and exits non-zero on failure, so it works as a deployment smoke test.

Configuration

Committable settings, uncommittable keys.

${VAR} is read from the environment, and envFile names a file to load first without overwriting what the shell already set. Profiles hold several setups in one file. A flag beats a variable, which beats the file.

s3nd.config.json
{
  "envFile": ".env",
  "profiles": {
    "r2": {
      "bucket": "transfers",
      "region": "auto",
      "endpoint": "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com",
      "expiresIn": "24h",
      "credentials": { "accessKeyId": "${R2_ACCESS_KEY_ID}", "secretAccessKey": "${R2_SECRET_ACCESS_KEY}" }
    },
    "local": { "bucket": "transfers", "endpoint": "http://localhost:9000" },
    "prod":  { "remote": "https://drop.example.com/api/transfers", "token": "${S3ND_TOKEN}" }
  }
}
$ s3nd -p local doctor          # against a MinIO container, offline
$ s3nd -p r2 put ./report.pdf
$ s3nd -p prod put ./report.pdf # through your server, with a token

$ s3nd config                   # which value won, and where it came from
file         /home/you/transfers/s3nd.config.json (profile "r2")
mode         straight to S3
bucket       transfers                                 $S3ND_BUCKET
endpoint     https://8c4….r2.cloudflarestorage.com     s3nd.config.json (r2)
credentials  …1a2b                                     s3nd.config.json (r2)
expires in   1 day                                     s3nd.config.json (r2)

The file is looked for from the working directory upwards, then in ~/.config/s3nd/config.json. s3nd config masks the access key id and never prints the secret or the token. Environment-only works too, which is the shape CI wants.

Against your own server

One implementation, two wirings.

Every command takes --remote, pointing it at a deployment of the transfer protocol instead of at S3. The CLI has exactly one implementation, the protocol client, wired either to fetch or straight into the handler in the same process. The two modes cannot drift apart.

$ s3nd --remote https://drop.example.com/api/transfers --token "$TOKEN" put ./report.pdf
K7QP2M4X

$ s3nd doctor --remote https://drop.example.com/api/transfers --token "$TOKEN"
 Server: https://drop.example.com/api/transfers answered
 Create, read, delete: round-tripped code 8WTXQC8R

The practical consequence: the machine you run this from needs a token for your own deployment rather than S3 credentials. That is the shape for a team, where handing every laptop the bucket keys is not.

s3nd init --provider remote writes the matching starter, and doctor --remote proves the server answers before anyone depends on it.

Setting up a server

Reference

Six commands, a dozen options.

Commands

put <file>
Store a file and print the code to carry. "-" reads stdin
get <code>
Fetch what a code points at, to the stored filename, -o, or stdout
rm <code>
Burn a code
doctor
Check this setup can actually store transfers. Exits non-zero when a check fails
init
Write a starter s3nd.config.json for aws, r2, minio, scaleway, wasabi or remote
config
Print the resolved configuration, and where each value came from

Options

-c, --config
Configuration file to read
-p, --profile
Profile to use inside it
--env-file
Read KEY=value pairs from this file first
--bucket, --prefix, --region, --endpoint
Bucket settings, overriding the file and the environment
--expires-in
3600, 30m, 24h, 7d, or never
--remote, --token
Talk to a s3nd server instead of S3 directly
--name
Filename to store the transfer under
--json
Machine-readable output, for scripts and for doctor in CI

Configuration otherwise comes from the same environment variables as the library: S3ND_BUCKET, S3ND_ENDPOINT, S3ND_REMOTE, S3ND_TOKEN and the usual AWS credentials. Node 20 or later; the argument parser is node:util's parseArgs, so there is nothing else to install.

Put a file. Hand over the code.

Point it at the bucket you already pay for. Nothing to deploy, nothing to sign up for, nothing in the middle.

npm install -g @s3nd/clis3nd init --provider r2 --bucket drops3nd put ./anything.zip