# drop A small temporary file drop in the spirit of [transfer.sh](https://github.com/dutchcoders/transfer.sh), but plain PHP — no Go binary, no daemon, no database. One script, one storage folder. ## Usage ```sh curl --upload-file ./hello.txt https://tool.medowar.de/drop/ ``` The response body is the download URL: ``` https://tool.medowar.de/drop/9f2c1ab73d40/hello.txt ``` **Mind the trailing slash** — `curl --upload-file` only appends the local filename when the URL ends in `/`. Without it the file is stored as `upload.bin` (or as whatever `X-Filename:` says). Optional request headers: | Header | Meaning | | --------------------- | ---------------------------------------------------- | | `Max-Days: 1` | Expire after 1 days instead of the default 3 (max 30). | | `Max-Downloads: 1` | One-shot link — deleted after that many downloads. | | `X-Filename: name.txt`| Filename when the URL carries none. | Show the response headers to get the delete URL: ```sh curl -D- -H 'Max-Downloads: 1' --upload-file ./secret.zip https://tool.medowar.de/drop/ ``` ``` X-Url-Delete: https://tool.medowar.de/drop/d/9f2c1ab73d40/8f3c… X-Expires: Wed, 19 Aug 2026 09:12:44 GMT ``` Delete early: ```sh curl -X DELETE https://tool.medowar.de/drop/d/9f2c1ab73d40/8f3c… ``` Opening that delete URL in a browser shows a confirmation page instead of deleting straight away, so link previews and prefetchers can't wipe a file. There is also a browser UI at `/drop/` with drag & drop and an upload progress bar, and `?meta=1` on a download URL returns the file's metadata as JSON. ## Expiry Every file gets an expiry timestamp at upload time (default **14 days**). Expired files are refused on download and physically removed by a sweep that runs on roughly every 20th request — there is no cron job to set up. If the drop is idle for a long time, files simply linger on disk until the next request; add a cron entry if you want that tightened: ```sh */30 * * * * curl -sf -o /dev/null https://tool.medowar.de/drop/ ``` ## Files | File | Purpose | | --------------- | ----------------------------------------------------------------- | | `index.php` | Everything: router, upload, download, delete, web UI. | | `.htaccess` | Rewrites all paths to `index.php`, raises `LimitRequestBody`. | | `data/` | One directory per file: `blob` + `meta.json`. Git-ignored. | | `data/.htaccess`| Denies direct web access to stored files. | ## Limits Set at the top of `index.php`: | Constant | Default | Meaning | | -------------------- | ------- | -------------------------------- | | `MAX_FILE_BYTES` | 512 MB | Per file. Keep `LimitRequestBody` in `.htaccess` in sync. | | `MAX_TOTAL_BYTES` | 10 GB | Whole drop; further uploads get a 507. | | `DEFAULT_DAYS` | 3 | Default expiry. | | `MAX_DAYS` | 30 | Ceiling for `Max-Days`. | | `MAX_PER_IP_HOUR` | 60 | Uploads per IP per hour. | | `GC_CHANCE` | 20 | 1-in-N requests sweep expired files. | ## Notes / security - **Links are the only access control.** IDs are 48 bits of randomness and the drop has no listing, but anyone holding a URL can download the file. Use `Max-Downloads: 1` for anything sensitive — or encrypt before uploading. - Downloads are always sent as `application/octet-stream` with `Content-Disposition: attachment` and `nosniff`. This host serves other tools from the same origin, so an uploaded `.html` must never render here. - Uploads stream to disk in 256 KB chunks, so PHP's `memory_limit` is not the constraint; `LimitRequestBody` and the PHP `max_execution_time` are. - `upload_max_filesize` / `post_max_size` only apply to the browser form fallback — a raw `PUT` body bypasses PHP's multipart parser entirely. - Filenames are reduced to a single path component and stripped of control characters; the on-disk name is always `blob`, so the client's name never touches the filesystem. - The download counter is incremented *before* the transfer starts, so aborting a download can't buy extra pulls on a one-shot link.