Skip to content
dharunashokkumarPublic

About

Upload a file, get a 4-digit pin. One Cloudflare Worker, one R2 bucket, a web UI and two served CLI clients.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

dropbin

CI npm license

Instant file sharing from one workerd process and one S3 bucket, on a box you run.

There are two things you can do — upload and download — and the whole tool is built around only those two. Uploading gets you a PIN; anyone with the PIN and the password can take the thing back out, in a browser or in a terminal. Nothing lists what is stored, anywhere. The only number either front end reports is how much room is left.

A pin holds exactly one thing. Upload a file and the pin holds that file. Upload a folder and it is zipped on your machine first, so the pin holds one zip — and downloading gives you that zip straight back.

 dropbin  files.example.com
 ----------------------------------------------------------------------
   1  upload     a file or a folder
   2  download   with a pin
 ----------------------------------------------------------------------
 [#.....................]  9.71 GB free of 10.0 GB   6 pin(s) stored
 1 or 2 to choose, q to quit

Run it

You need two things: an S3 bucket and a key that can read and write it, and somewhere to run workerd. Any S3 server works — Garage, SeaweedFS, MinIO, AWS — as long as it answers path-style requests (ENDPOINT/bucket/key), which they all do.

npm install                                  # workerd, the only dependency
export ACCESS_PASSWORD='something long'
export S3_ENDPOINT=http://127.0.0.1:3900 S3_BUCKET=dropbin S3_REGION=garage
export S3_KEY_ID=… S3_SECRET=…
npm start                                    # workerd serve workerd.capnp → :8787

workerd.capnp is the whole deployment: the modules, the bindings, the port. Secrets and the endpoint come from the environment, so the file holds none. Put it behind anything that terminates TLS — a reverse proxy, a tunnel — and pass X-Forwarded-Proto: https: the socket trusts that header, so the links dropbin builds, and the clients it hands out, say https://. Running it as a service is a systemd unit with EnvironmentFile= and ExecStart=workerd serve /path/to/workerd.capnp.

dropbin fetches nothing but the S3 endpoint, and the config only lets it reach that: its outbound network allows public and private addresses because the store is usually on the LAN. Tighten storage to the store's address if you like.

The password

changeme until you set ACCESS_PASSWORD — that fallback is in the code. If curl -s -o /dev/null -w '%{http_code}' -H "X-Pass: changeme" https://host/?info=1 says 200, nobody set it.

Changing it invalidates the dp cookie in every browser and every outstanding db open / db qr link, since those are signed with the password as the key.

It gates everything except the two clients (/cli, /cli.ps1), the two installers (/install.sh, /install.ps1), /robots.txt and the favicon. Accepted four ways: ?p=, an X-Pass: header, HTTP basic auth, or the dp cookie the web login sets (one year).

The storage meter

QUOTA_GB in workerd.capnp is what the meter counts down from — 10 GB by default. It is a display figure, not an enforced cap; set the same number as a quota on the bucket if it has to hold.

The db command

Anyone who uses this more than once should install the tool. One command, then db is on the path — pick whichever line suits the machine:

curl -fsSL https://files.example.com/install.sh | sh   # macOS, Linux, WSL
npm i -g dropbin                                       # anywhere npm is
irm https://files.example.com/install.ps1 | iex        # Windows
db                      # the menu

All three land the same thing: a zero-dependency Node package (18 or newer) that lives in cli/, installing db and dropbin as a longer alias. The Worker only advertises it; nothing about the tool runs on the server.

The installers are the shorter way in — no npm, no registry account, one command that fetches the release from GitHub. They need Node 18+ to already be there; the client below is the one that needs nothing at all. Where things land:

files db
install.sh ~/.local/share/dropbin ~/.local/bin
install.ps1 %LOCALAPPDATA%\dropbin\app %LOCALAPPDATA%\dropbin\bin, added to your PATH

Fetched from a deployment, an installer bakes that deployment into the shim it writes, so the db it leaves behind already points at your host — DROP_HOST still overrides it. Fetched from GitHub it bakes in nothing:

curl -fsSL https://raw.githubusercontent.com/dharunashokkumar/dropbin/main/install.sh | sh

Both take the same handful of options — a version to pin, where to put things, and how to undo it:

curl -fsSL https://files.example.com/install.sh | sh -s -- --version 1.1.0
curl -fsSL https://files.example.com/install.sh | sh -s -- --uninstall
sh install.sh --help
# iex cannot pass arguments, so hand it a script block instead
& ([scriptblock]::Create((irm https://files.example.com/install.ps1))) -Version 1.1.0
& ([scriptblock]::Create((irm https://files.example.com/install.ps1))) -Uninstall

Releases are on GitHub, two archives holding the same files: dropbin.tgz for install.sh (and for npm i -g ./dropbin.tgz) and dropbin.zip for install.ps1, which unpacks with Expand-Archive. --tarball and -Archive install one you already have, which is how a machine with no way out to GitHub gets db.

db up ./holiday.png            # a file, random pin
db up ./holiday.png photos     # a file, your own pin
db up ./project                # a folder — zipped here, then sent
db get 4821                    # save it in this folder
db get 4821 ~/Downloads        # save it there
db view 4821                   # print it here, if it is text
db qr 4821                     # a code to point a phone at
db open 4821                   # look at it in a browser
db rm 4821                     # throw the pin away
db free                        # how much room is left

The menu offers the same two options the shell client does, and nothing else:

 dropbin  files.example.com
 --------------------------------------------------------------
   [1] Upload a file or folder
   [2] Download with a pin
   [q] Quit

   Storage: 9.71 GB free of 10.00 GB · 6 pins stored

 Choice:

The password is asked for on every run and stored nowhere — there is no config file and no token cache. DROP_PASS=... skips the prompt for scripts and CI, DROP_HOST=... (or --host) points db at another deployment. After an upload the link goes to the clipboard unless you pass --no-copy. Folders are zipped with node:zlib, so zip, tar and Compress-Archive do not have to exist. Downloads never overwrite: a second copy of holiday.png becomes holiday.png.1. db up and db get print the one useful line — the link, the saved path — on stdout and everything else on stderr, so they pipe cleanly.

The client with nothing to install

For a one-off, or a machine without Node: the Worker hands you a shell script, already pointed at your host.

curl -s https://files.example.com/cli -o drop && bash drop
bash <(curl -s https://files.example.com/cli)     # or in one line

It asks for the password, then shows the two options. Press 1 or 2; q goes back, and q at the top quits.

Upload asks three things and nothing else:

 Upload

   1  file
   2  folder   (zipped before it is sent)
   q  back

  file to upload: ./holiday.png

   1  random pin   (4 digits)
   2  custom pin   (anything you like)

 Uploaded

   pin    4821
   file   [IMG] holiday.png   2.41 MB
   url    https://files.example.com/4821

Download asks for a pin, tells you what is behind it, and lets you take it or read it:

 Download   pin 4821

   [IMG] holiday.png   2.41 MB   2026-09-08 11:39

   1  download   into the current directory
   2  view       in the pager
   q  back

A folder is packed with zip if it is installed and tar -czf otherwise, so the pin holds a .zip or a .tgz. Downloads land in the directory you started from and never overwrite: a second copy becomes name.1. DROP_PASS=… skips the password prompt, DROP_HOST=… points it at a different deployment.

On Windows PowerShell the same client is at /cli.ps1 (folders go through Compress-Archive):

irm https://files.example.com/cli.ps1 -OutFile drop.ps1 ; ./drop.ps1

Or as plain commands

Give it arguments and it skips the menu entirely:

./drop up ./holiday.png          # a file, random pin
./drop up ./holiday.png photos   # a file, your pin
./drop up ./project              # a folder — zipped, then sent
./drop get 4821                  # save it here
./drop get 4821 ~/Downloads      # save it there
./drop view 4821                 # print it
./drop rm 4821                   # throw the pin away
./drop free                      # how much room is left

Or just curl

export PASS=changeme
curl -T ./holiday.png "https://:$PASS@files.example.com/up/"   # upload
curl -OJ "https://files.example.com/4821?p=$PASS"              # download
curl "https://files.example.com/4821?info=1&p=$PASS"           # what is it?
curl -X DELETE "https://files.example.com/4821?p=$PASS"        # throw it away
curl "https://files.example.com/?p=$PASS"                      # room left

curl -T only appends the local filename when the URL ends in / and has no query string — hence the https://:$PASS@host/up/ form, which sends the password as basic auth instead. -H "X-Pin: photos" picks the pin and -H "X-Name: other.png" picks the stored name. In PowerShell write curl.exe; bare curl is an alias for Invoke-WebRequest.

The web page

Open the domain, type the password once, and you get a grey dialog with two buttons and a storage meter — Upload and Download, and nothing else to click.

Upload asks the same three things as the terminal: file or folder, then a random 4-digit pin or one you type, then it sends and shows you the pin. Download is a pin box; type one and you get the file's name, size and date with Download, View and Close.

It works with JavaScript off, except for folders: a folder is zipped in the browser (STORE, no compression) so the server never holds a folder in memory, and that needs JS. With JS off the folder option is greyed out and single files still upload through a plain form post.

API

method path what
GET / the two options and the meter — ?info=1 ?json=1
GET /PIN a browser looks at it, everything else downloads it
GET /PIN?view=1 the bytes inline; ?dl=1 saves them; ranges supported
GET /PIN?info=1 what the pin holds, without fetching it
GET /PIN?k=EXP.SIG a share link: that one pin, read-only, no password
PUT /up raw-body upload — X-Name, X-Pin, ?quiet=1
POST /up multipart: one file field + optional pin
DELETE /PIN throw the pin away
GET /cli, /cli.ps1 the client scripts, pre-pointed at this host
GET /install.sh, /install.ps1 the db installers, pre-pointed the same way
GET /upload, /get the browser's two dialogs

?info=1 is the terminal client's only data feed, one tab-separated line:

  • /?info=1 → pins used quota, all in bytes
  • /PIN?info=1 → name size date

Share links

db open and db qr hand a link to something that cannot be asked for a password — a browser, or a phone pointed at a code — so they sign one instead of just linking it. ?k= is an expiry and an HMAC of the pin, keyed on the password: nothing is stored anywhere, it opens exactly that one pin, it is read-only (a DELETE or an upload with it is refused), and it lasts a week. SHARE_HOURS in workerd.capnp caps how far ahead one may be signed.

A browser landing on a pin gets the file on screen — an image, a video, text, a PDF — with a Download button it has to be asked for. Uploaded files still carry Content-Security-Policy: sandbox and nosniff, so an uploaded page cannot act on the origin while it is being looked at.

Notes

  • A pin is 4 digits by default — 9000 of them, checked for collisions before one is handed out, and widened to 6 digits if they ever all fill up. A custom pin is anything up to 64 characters that is not a slash, a control character or a reserved word (up, cli, get, upload, api, …).
  • Uploading to a pin you have already used replaces what was there. One pin, one thing.
  • Files live until you delete them — there is no expiry job.
  • An upload is read into memory before it is stored, so the process needs roughly the largest upload in RAM. A proxy in front usually sets the real ceiling first: Cloudflare's tunnel and proxy, for one, cap a request body at 100 MB on the free plan. Downloads stream and cost nothing.
  • Folders are zipped by the client and never by the server, which keeps per-byte work — and a whole folder in memory — off the box.
  • Uploaded files are served with Content-Security-Policy: sandbox and nosniff, so an uploaded .html or .svg cannot act on this origin.
  • Pins written by older versions held several files. Those still work: they come back out as one zip, capped at ZIP_LIMIT_MB of hashing, and the individual files stay reachable at /PIN/path/to/file.
  • Local dev: npm start with the S3_* variables pointed at a throwaway bucket. There is nothing to build: workerd.capnp embeds src/ as it is.
  • The db tool is a second npm package in cli/, published as dropbin. It is not bundled into the Worker — only the two clients and the two installers are, as text with __HOST__ swapped for the request origin on the way out.
  • A release is cut by hand: node cli/pack.js builds dropbin.tgz and dropbin.zip out of the same files and refuses if the two disagree, and gh release create attaches them. No workflow deploys or publishes anything.
  • Change anything in this tool and update CLAUDE.md (and this README) to match.

Working on it

Read CLAUDE.md first — it is the architecture, and it explains why most of the obvious features are deliberately absent, what CI checks, and how to verify a change against npm start (there is no test suite). Deploys, GitHub releases and npm publishes are run by hand, not by a workflow.

Found a security problem? Do not open an issue — report it privately. SECURITY.md also covers the threat model and how to harden a deployment: set a real password, prefer X-Pass: over ?p=, and put a rate limit in front of it.

License

MIT © Dharun Ashokkumar

About

Upload a file, get a 4-digit pin. One Cloudflare Worker, one R2 bucket, a web UI and two served CLI clients.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages