# Uploader

A small, self-hosted **file uploader** built to work in ancient browsers —
tested against the constraints of **QtWeb 3.8.5** (old WebKit, no modern JS/CSS)
— while behaving like a normal modern web app everywhere else.

It is a Flask server-rendered app: a plain multipart `<form>` does the upload,
so it works **with JavaScript completely disabled**. An optional, feature-detected
ES5 script adds drag-and-drop and a progress bar in modern browsers, and does
nothing at all in QtWeb.

## Features

- Upload one or many files with a standard multipart form (no JS required).
- Upload into the current folder, an existing folder, or a brand-new one.
- Browse folders with breadcrumbs, download and preview files.
- **Delete** via a dedicated confirmation page (never `confirm()`, which QtWeb blocks).
- **Paste text and save as a file** — a fallback for machines with no usable
  file picker.
- Create folders; download the whole tree as a `.zip`.
- Password login (with brute-force lockout) and change-password page.
- All paths are jailed to the uploads root — path traversal is rejected.
- Configurable maximum upload size with a friendly 413 page.

## Quick start

```bash
cd uploader
python3 -m venv .venv && . .venv/bin/activate   # or: run.sh / run.bat
pip install -r requirements.txt
python app.py
```

Then open <http://127.0.0.1:5000/>.

On first run with no `UPLOADER_PASSWORD` set, a random password is generated and
printed in the console; set the env var to choose your own. Uploads are stored
in `./uploads` by default.

Windows users: double-click `run.bat` (or run `run.ps1` in PowerShell).

## Configuration

Every setting is optional. See `.env.example`.

| Variable | Default | Meaning |
|---|---|---|
| `UPLOADER_ROOT` | `./uploads` | Folder where uploads are stored. |
| `UPLOADER_HOST` | `0.0.0.0` | Bind address for the dev server. |
| `UPLOADER_PORT` | `5000` | Bind port. |
| `UPLOADER_MAX_UPLOAD` | `536870912` (512 MiB) | Max bytes per upload request. |
| `UPLOADER_PASSWORD` | random (printed once) | Login password. |
| `UPLOADER_SECRET` | random (persisted) | Flask session secret. |
| `UPLOADER_NO_AUTH` | unset | Set to `1` to disable login (trusted local use). |
| `UPLOADER_DEBUG` | unset | Set to `1` for Flask debug mode. |

Auth state (password hash + secret) is persisted in `instance/auth.json`
(mode `600`).

## Why it looks the way it does

QtWeb 3.8.5 has no WebGL, no WebSockets, no `fetch`/`Promise`, no flexbox/grid,
and blocks `prompt()`/`confirm()`. It also can't reliably use the File API, so
drag-and-drop upload is impossible. This app therefore:

- uses tables and plain forms instead of CSS layout systems;
- uses a confirmation page instead of `confirm()`;
- keeps the drag-and-drop enhancement strictly optional and guarded by
  `window.FormData && xhr.upload`.

## Production / deployment

See [DEPLOY.md](DEPLOY.md). In short:

```bash
gunicorn -w 2 -k gthread --threads 8 -b 127.0.0.1:5000 wsgi:application
```

or Docker:

```bash
docker compose up -d --build
```

The app is stateless between requests, so multiple workers are safe; threaded
workers are recommended so a slow transfer does not block others.

## Tests

There are headless smoke tests in `tests/`:

```bash
python3 tests/smoke_test.py
```

They exercise every route, authentication (wrong/right password), the full
upload → list → download → delete flow, and path-traversal attempts.

## License

MIT — see [LICENSE](LICENSE).
