@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 K7QP2M4Xinit 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.
$ 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.$ 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.
{
"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 8WTXQC8RThe 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.
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