javascript · cloudflare workers · r2

dropbin

upload a file, get a 4-digit pin. one worker, one bucket, and nothing that lists what's inside.

javascript cloudflare workers r2 serverless cli zero dependencies hmac share links mit

what it is

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's zipped on your machine first, so the pin holds one zip — and downloading gives you that zip straight back.

terminalfiles.dharun.dev
 dropbin  files.dharun.dev
 ----------------------------------------------------------------------
   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
1 + 1one worker, one bucket
zeroruntime dependencies
90004-digit pins, then 6
mitlicense

how it fits together

one worker holds everything: the web dialogs, the api, and — as plain text with the host swapped in on the way out — the two shell clients and the two installers. r2 holds the bytes. there's no database, no queue, and no build step.

dropbin architecture four clients — a browser, the db command, the served shell client, and plain curl — all speak to one cloudflare worker, which reads and writes one r2 bucket. CLIENTS browser · web ui db · node client drop · shell client plain curl EDGE cloudflare worker password gate pin allocation the two web dialogs /cli · /cli.ps1 /install.sh · .ps1 hmac share links get / put STORAGE r2 bucket one object per pin no database · no listing endpoint · the meter is the only number reported folders are zipped by the client, never by the worker — cpu stays free
everything the tool does happens in one worker; r2 is the only state.

the pin flow

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 isn't a slash, a control character, or a reserved word.

upload and download flow upload sends a file or a locally zipped folder, the worker finds a free pin and stores one object; download presents a pin and password, the worker reports what the pin holds and streams the bytes back. UPLOAD file or folder zipped locally free pin found pin 4821 DOWNLOAD pin + password worker checks name, size, date bytes back a download never overwrites — a second copy becomes holiday.png.1
two paths, and the tool deliberately has no third one.

setup · deploy your own

four commands, about five minutes. you need node 18 or newer, and r2 enabled on the cloudflare account.

  1. install and log in
    sh
    npm install
    npx wrangler login                       # opens a browser, once
  2. create the bucket, then deploy
    sh
    npx wrangler r2 bucket create dropbin    # needs R2 on the account
    npx wrangler deploy

    that prints a https://dropbin.<you>.workers.dev url, which already works.

  3. point it at your own domain
    wrangler.toml
    [[routes]]
    pattern = "files.example.com"
    custom_domain = true

    deploy again. the zone has to be on the same cloudflare account; wrangler makes the dns record itself.

  4. set the password
    sh
    npx wrangler secret put ACCESS_PASSWORD  # takes effect at once
    npx wrangler secret list                 # [] means it is still changeme

    it's changeme until you set one — and that fallback lives in the code, not in wrangler.toml, which deliberately keeps the password out of [vars]. changing it invalidates every browser cookie and every outstanding share link, since those are signed with the password as the key.

setup · install the client

anyone who uses this more than once should install the tool. one command, then db is on the path.

shmacos · linux · wsl
curl -fsSL https://files.dharun.dev/install.sh | sh
npm i -g dropbin                         # or, anywhere npm is
powershellwindows
irm https://files.dharun.dev/install.ps1 | iex

all three land the same thing: a zero-dependency node package (18 or newer) installing db, with dropbin as a longer alias. fetched from a deployment, an installer bakes that host into the shim it writes, so the db it leaves behind already points at your instance — DROP_HOST still overrides it.

installerfilesthe db shim
install.sh~/.local/share/dropbin~/.local/bin
install.ps1%LOCALAPPDATA%\dropbin\app…\dropbin\bin, added to PATH

the db command

sh
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 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 password is asked for on every run and stored nowhere — there's no config file and no token cache. DROP_PASS skips the prompt for scripts and ci, DROP_HOST points db at another deployment. db up and db get print the one useful line on stdout and everything else on stderr, so they pipe cleanly.

nothing to install

for a one-off, or a machine without node: the worker hands you a shell client, already pointed at your host.

sh
bash <(curl -s https://files.dharun.dev/cli)
irm https://files.dharun.dev/cli.ps1 -OutFile drop.ps1 ; ./drop.ps1   # windows

or skip the clients entirely and use curl:

sh
export PASS=changeme
curl -T ./holiday.png "https://:[email protected]/up/"   # upload
curl -OJ "https://files.dharun.dev/4821?p=$PASS"              # download
curl -X DELETE "https://files.dharun.dev/4821?p=$PASS"        # throw it away

curl -T only appends the local filename when the url ends in / and has no query string — hence the basic-auth form above. in powershell write curl.exe; bare curl is an alias for Invoke-WebRequest.


the api

methodpathwhat it does
GET/the two options and the meter
GET/PINa browser looks at it, everything else downloads it
GET/PIN?info=1what the pin holds, without fetching it
GET/PIN?k=EXP.SIGa share link: that one pin, read-only, no password
PUT/upraw-body upload — X-Name, X-Pin
POST/upmultipart: one file field, optional pin
DELETE/PINthrow the pin away
GET/cli · /cli.ps1the clients, pre-pointed at this host

share links

db open and db qr hand a link to something that can't 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's read-only, and it lasts a week.

uploads are sandboxed — every stored file is served with Content-Security-Policy: sandbox and nosniff, so an uploaded page can't act on the origin
one pin, one thing — uploading to a pin you've used replaces what was there
no expiry job — files live until you delete them
the meter is a display figureQUOTA_GB counts down from r2's free 10 gb; it isn't an enforced cap
100 mb an upload — that's the free-plan worker request-body ceiling, not a choice
it works with javascript off — except folders, which are zipped in the browser so the worker never spends cpu on them
the interesting constraint here was cpu, not storage. a free-plan worker gets ten milliseconds a request, so zipping a folder on the server was never going to work — the client does it, and uploads stay pure i/o with no ceiling at all.

mit licensed. deploys, github releases and npm publishes are run by hand, not by a workflow. the threat model and hardening notes are in security.md.


built by dharun ashokkumar · all projects