+ A single self-contained binary to share files and clipboard content between devices—no database, dependencies, or mandatory configuration.
+
-Upgopher is a zero-dependency Go web server for file sharing. It provides a browser-based interface for uploading, downloading, and managing files, with optional HTTPS, basic authentication, and read-only mode. Distributed as a single self-contained binary with no external runtime dependencies.
+
-## Features
-* Users can upload files by selecting a file and clicking the "Upload" button
-* Uploaded files are stored in the "uploads" directory by default, but the directory can be changed using the -dir flag
-* Users can view a list of the uploaded files by visiting the root URL
-* Basic authentication is available to restrict access to the server. To use it, set the -user and -pass flags with the desired username and password.
-* Traffic via HTTPS with self-signed certificate generation or custom certificates
-* Browse through folders and upload files with drag-and-drop support
-* Directory tree sidebar with expand/collapse controls
-* Breadcrumb navigation with clickable path segments
-* Copy file URLs to clipboard with one click for easy sharing
-* Search within text files directly from the web interface
-* Create custom path aliases for easy file access
-* Shared clipboard for cross-device text and screenshot sharing
-* Zip folder download functionality
-* Option to hide hidden files with the -disable-hidden-files flag
-* Readonly mode to disable uploads and deletions while allowing downloads
+## Start sharing in three steps
-
-
-## Installation
-
-
-### Automatically
-
-Just run this command in your terminal with go installed.
```bash
+# 1. Install
go install github.com/wanetty/upgopher@latest
-```
-### Releases
+# 2. Share a directory
+upgopher -dir ./shared
-Go to the [releases](https://github.com/wanetty/upgopher/releases) section and get the one you need.
-
-### Manual
-
-Just build it yourself
-
-```bash
-git clone https://github.com/wanetty/upgopher.git
-cd upgopher
-go build
+# 3. Open http://localhost:9090 on this or another device
```
-### Docker
-```bash
-docker build . -t upgopher
-docker run --name upgopher -p 9090:9090 upgopher
-```
+Upgopher binds to all interfaces by default. Use Basic Auth, a trusted local
+network, a VPN, or a properly configured reverse proxy before exposing it beyond
+your machine.
-## Usage
+## Install
-### Help Output:
+| Method | Command |
+| --- | --- |
+| Homebrew | `brew install --cask wanetty/tap/upgopher` |
+| Scoop | `scoop bucket add wanetty https://github.com/wanetty/scoop-bucket` then `scoop install upgopher` |
+| Docker | `docker run --rm -p 9090:9090 -v "$PWD:/data" wanetty/upgopher:1.20.0` |
+| Go | `go install github.com/wanetty/upgopher@latest` |
+| Binary | Download the archive for your OS from [GitHub Releases](https://github.com/wanetty/upgopher/releases/latest) and verify it with `checksums.txt` |
-```bash
-./upgopher -h
-Usage of ./upgopher:
- -cert string
- HTTPS certificate
- -dir string
- directory path (default "./uploads")
- -disable-hidden-files
- disable showing hidden files
- -key string
- private key for HTTPS
- -max-upload-size int
- maximum upload size in GB (0 means unlimited)
- -max-tabs int
- maximum number of shared clipboard tabs
- -pass string
- password for authentication
- -port int
- port number (default 9090)
- -q quiet mode
- -read-timeout duration
- server read timeout (0 means unlimited)
- -read-header-timeout duration
- server read header timeout
- -write-timeout duration
- server write timeout (0 means unlimited)
- -readonly
- readonly mode (disable upload and delete operations)
- -ssl
- use HTTPS on port 443 by default. (If you don't put cert and key, it will generate a self-signed certificate)
- -user string
-```
+The project-maintained container is also published as
+`ghcr.io/wanetty/upgopher`. See the
+[installation guide](https://wanetty.github.io/upgopher/getting-started.html)
+for platform-specific instructions and persistent Docker examples.
-### Examples
+## What it is good at
-**Basic usage:**
-```bash
-./upgopher
-```
-This will start the server on the default port (9090) and store uploaded files in the ./uploads directory.
+- **Move files across a LAN.** Open a browser on a phone, laptop, VM, or lab
+ machine and upload or download without setting up a client.
+- **Share text and screenshots between devices.** The built-in shared clipboard
+ supports multiple tabs and optional per-tab protection.
+- **Expose a development folder temporarily.** Serve build output, logs, or test
+ artifacts with one command.
+- **Publish a read-only download portal.** Keep browsing and downloads available
+ while disabling uploads and deletion with `-readonly`.
+- **Run a small homelab file drop.** Put Upgopher behind your VPN, tunnel, or
+ reverse proxy when you need a compact browser-based tool instead of a storage
+ platform.
-**Custom port and directory:**
-```bash
-./upgopher -port 8080 -dir "/path/to/files"
-```
+## Included
-**With basic authentication:**
-```bash
-./upgopher -user admin -pass secretpassword
-```
+**Files:** drag-and-drop file and folder uploads, download, directory creation
+and deletion, ZIP downloads, search inside text files, breadcrumbs, and a
+directory tree.
-**With HTTPS (self-signed certificate):**
-```bash
-./upgopher -ssl
-```
+**Sharing:** copyable direct links, custom path aliases, and a shared clipboard
+for text and screenshots.
-**With HTTPS (custom certificate):**
-```bash
-./upgopher -ssl -cert /path/to/cert.pem -key /path/to/key.pem
-```
+**Controls:** optional Basic Auth, built-in HTTPS with a self-signed or custom
+certificate, read-only mode, hidden-file controls, upload limits, and server
+timeouts.
-**Hide hidden files:**
-```bash
-./upgopher -disable-hidden-files
-```
+Everything required by the UI is embedded in the binary. Upgopher uses only the
+Go standard library and does not require a database or configuration file.
-**Readonly mode (disable uploads and deletions):**
-```bash
-./upgopher -readonly
-```
+## Choosing the right tool
-**Limit upload size to 1 GB:**
-```bash
-./upgopher -max-upload-size 1
-```
+Comparison checked for Upgopher v1.20.0 in July 2026. Projects evolve; follow
+the linked sources for their current feature sets.
-**Limit shared clipboard tabs to 5:**
-```bash
-./upgopher -max-tabs 5
-```
+| Tool | Artifact / runtime | Upload and management | Auth model | Users / roles | HTTPS | WebDAV | Resumable uploads | Shared clipboard | Best fit |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| **Upgopher** | Self-contained Go binary | Browser file manager | Optional global Basic Auth | No | Built in | No | No | Text and screenshots | The smallest step from a folder to a practical browser sharing UI |
+| [`python -m http.server`](https://docs.python.org/3/library/http.server.html) | Python runtime | Browse and download | No built-in access control | No | CLI TLS options in current Python | No | No | No | Temporary, read-only serving during local development |
+| [Dufs](https://github.com/sigoden/dufs) | Single Rust binary | Upload, edit, search, archive | Access controls | Account rules | Built in | Yes | Yes | No | A compact server when WebDAV or resumable transfers matter |
+| [File Browser](https://filebrowser.org/) | Binary or container with persistent state | Full web file administration | Login and persistent auth | Yes | Commonly via proxy | No | No | No | Multi-user file management with administration and permissions |
+| [Copyparty](https://github.com/9001/copyparty) | Python runtime or self-extracting bundle | Extensive file and media features | Accounts and per-volume permissions | Yes | Built in | Yes | Yes | No | Advanced transfers, protocols, indexing, deduplication, and media workflows |
-**Set a custom read timeout (for large uploads on slower links):**
-```bash
-./upgopher -read-timeout 30m
-```
+Upgopher deliberately does **not** provide WebDAV, resumable uploads,
+multi-user roles, storage backends, or a persistent administration database. If
+one of those is central to your setup, one of the alternatives above is likely
+a better choice.
-**Recommended for large uploads:**
-```bash
-./upgopher -read-timeout 0 -write-timeout 0
-```
+## Common commands
-Note: Cloudflare Quick Tunnels are intended for testing and can impose limits. For reliable large uploads, prefer a full Cloudflare Tunnel.
-
-
-## Security
-
-### Reporting Vulnerabilities
-
-If you discover a security vulnerability, please contact [@gm_eduard](https://twitter.com/gm_eduard/) directly. Please do not open a public issue.
-
-
-## License
-This project is licensed under the MIT License. See the LICENSE file for details.
+```bash
+# Protect access
+upgopher -dir ./shared -user admin -pass 'choose-a-strong-password'
-## Development
+# Download-only portal
+upgopher -dir ./public -readonly
-### Building from Source
+# Built-in HTTPS with an ephemeral self-signed certificate
+upgopher -dir ./shared -ssl
-```bash
-git clone https://github.com/wanetty/upgopher.git
-cd upgopher
-go build -o upgopher
+# Custom port and a 1 GiB upload limit
+upgopher -dir ./shared -port 8080 -max-upload-size 1
```
-### Running Tests
+Read the full [configuration reference](https://wanetty.github.io/upgopher/configuration.html).
-```bash
-# All tests
-go test -v ./...
+## Security and limits
-# Only fast tests (skip time-based tests)
-go test -v -short ./...
+- Treat an unauthenticated instance as public to every device that can reach
+ its port.
+- Prefer a VPN or a reverse proxy with trusted TLS for internet-facing use.
+- `-ssl` without `-cert` and `-key` creates a new self-signed certificate at
+ startup; clients will not trust it automatically.
+- Basic Auth is global. Upgopher does not provide separate user accounts or
+ per-folder permissions.
+- Clipboard data and custom path aliases are held in memory and disappear when
+ the process restarts.
+- A read-only instance prevents uploads and deletion through Upgopher; it does
+ not change host filesystem permissions.
-# With coverage
-go test -cover ./...
-```
+See the [security guide](https://wanetty.github.io/upgopher/security.html).
+Report vulnerabilities privately to [@gm_eduard](https://twitter.com/gm_eduard);
+do not open a public issue.
+
+## Documentation
-### Contributing
+- [Website and overview](https://wanetty.github.io/upgopher/)
+- [Getting started](https://wanetty.github.io/upgopher/getting-started.html)
+- [Configuration reference](https://wanetty.github.io/upgopher/configuration.html)
+- [Security](https://wanetty.github.io/upgopher/security.html)
+- [Troubleshooting](https://wanetty.github.io/upgopher/troubleshooting.html)
+- [Contributing](./CONTRIBUTING.md)
-1. Fork the repository
-2. Create a feature branch (`git checkout -b feature/amazing-feature`)
-3. Make your changes
-4. Run tests (`go test -v ./...`)
-5. Commit your changes (`git commit -m 'Add amazing feature'`)
-6. Push to the branch (`git push origin feature/amazing-feature`)
-7. Open a Pull Request
+Upgopher is available under the [MIT License](./LICENSE).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..7720afd
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,23 @@
+# Security policy
+
+## Supported versions
+
+Security fixes are provided for the latest released version of Upgopher.
+Reproduce a report against that version when possible.
+
+## Reporting a vulnerability
+
+Contact [@gm_eduard](https://twitter.com/gm_eduard/) privately. Include:
+
+- the affected Upgopher version and operating system;
+- a minimal reproduction;
+- the expected and observed behavior;
+- the security impact;
+- logs or screenshots with credentials and private paths removed.
+
+Do not open a public issue before disclosure has been coordinated. You should
+receive an acknowledgement within seven days. Timelines for validation, a fix,
+and public disclosure will depend on severity and reproducibility.
+
+Please do not access data that is not yours, disrupt third-party systems, or
+retain sensitive data while researching a report.
diff --git a/distribution/README.md b/distribution/README.md
new file mode 100644
index 0000000..ebcc42e
--- /dev/null
+++ b/distribution/README.md
@@ -0,0 +1,34 @@
+# Distribution repositories and secrets
+
+Upgopher publishes release metadata to two project-owned repositories:
+
+- `wanetty/homebrew-tap`
+- `wanetty/scoop-bucket`
+
+Create both as public repositories with a `main` branch before pushing the
+v1.20.0 tag. The release workflow writes generated files directly to that
+branch.
+
+## Required GitHub Actions secrets
+
+Configure these in `wanetty/upgopher`:
+
+| Secret | Scope |
+| --- | --- |
+| `DISTRIBUTION_GITHUB_TOKEN` | Fine-grained token with repository Contents read/write for `homebrew-tap` and `scoop-bucket` only |
+| `DOCKERHUB_USERNAME` | Docker Hub account name |
+| `DOCKERHUB_TOKEN` | Docker Hub access token with permission to update `wanetty/upgopher` |
+
+The built-in `GITHUB_TOKEN` publishes release assets and the GHCR image. Do not
+reuse a personal password or a broadly scoped classic token.
+
+Initialize `homebrew-tap` with a short README and initialize `scoop-bucket` with
+a short README. GoReleaser creates the Cask and Scoop manifest on the first
+tagged release. The release workflow then adds Scoop `checkver` and
+`autoupdate` metadata while preserving GoReleaser's architecture-specific
+SHA-256 hashes.
+
+## GitHub Pages
+
+In repository settings, select **GitHub Actions** as the Pages source. The
+`Pages` workflow publishes the contents of `/docs` on changes to `main`.
diff --git a/distribution/adoption-baseline.md b/distribution/adoption-baseline.md
new file mode 100644
index 0000000..c05b363
--- /dev/null
+++ b/distribution/adoption-baseline.md
@@ -0,0 +1,20 @@
+# Adoption baseline
+
+Capture platform-native metrics immediately before publishing v1.20.0, then
+repeat the same fields after 7 and 30 days. Do not add third-party analytics to
+the website.
+
+| Metric | Pre-release | Day 7 | Day 30 |
+| --- | ---: | ---: | ---: |
+| GitHub stars | 65 (observed 2026-07-29) | | |
+| GitHub forks | 4 (observed 2026-07-29) | | |
+| GitHub clones, previous 14 days | Record at release | | |
+| GitHub release downloads | 0 for v1.20.0 | | |
+| Docker Hub pulls | Record at release | | |
+| GHCR downloads | Record at release | | |
+| Homebrew installs | Record when available | | |
+| Scoop installs | Not exposed centrally; note support signals | | |
+
+Also record the release publication timestamp, website publication timestamp,
+and awesome-selfhosted pull request URL so that changes can be interpreted
+against distribution events.
diff --git a/distribution/awesome-selfhosted.md b/distribution/awesome-selfhosted.md
new file mode 100644
index 0000000..ff0abe5
--- /dev/null
+++ b/distribution/awesome-selfhosted.md
@@ -0,0 +1,24 @@
+# awesome-selfhosted submission
+
+Target category: **File Transfer & Synchronization**
+
+Proposed description (184 characters):
+
+> Self-contained cross-platform file server with a browser UI for uploads,
+> downloads, folder management, a shared text/image clipboard, optional Basic
+> Auth and HTTPS, and read-only mode.
+
+Suggested metadata:
+
+- Website: `https://wanetty.github.io/upgopher/`
+- Source: `https://github.com/wanetty/upgopher`
+- License: `MIT`
+- Language/platform: `Go`
+- Container: `ghcr.io/wanetty/upgopher:1.20.0`
+
+Before opening the pull request, check the current contribution template and
+machine-readable schema in
+[`awesome-selfhosted/awesome-selfhosted`](https://github.com/awesome-selfhosted/awesome-selfhosted).
+Search open and closed pull requests for Upgopher, then use the repository's
+generator or validation command rather than manually guessing its current data
+format.
diff --git a/distribution/release-checklist.md b/distribution/release-checklist.md
new file mode 100644
index 0000000..7ac65d7
--- /dev/null
+++ b/distribution/release-checklist.md
@@ -0,0 +1,51 @@
+# v1.20.0 release checklist
+
+## Before tagging
+
+- [ ] `make test`, `make test-race`, `make lint`, and `make build` pass.
+- [ ] `goreleaser check` passes with the current GoReleaser v2.
+- [ ] `scripts/check-community-scripts-contract.sh` passes against a release
+ snapshot, preserving the Linux `amd64`/`arm64` archive names and embedded
+ `upgopher` path consumed by the Proxmox Community Script.
+- [ ] All links in README and `/docs` resolve.
+- [ ] The website is checked at desktop and mobile widths with keyboard-only
+ navigation and reduced motion.
+- [ ] `docs/assets/upgopher-demo.gif` is below 10 MB and contains no private
+ data or paths.
+- [ ] `wanetty/homebrew-tap` and `wanetty/scoop-bucket` exist and the
+ distribution token can write only to those repositories.
+- [ ] Docker Hub contains the public `wanetty/upgopher` repository and its token
+ is configured.
+- [ ] GitHub Pages uses GitHub Actions as its source.
+- [ ] Copy `.github/release-notes/v1.20.0.md` into the release draft body.
+
+## Create the draft
+
+```bash
+git tag -a v1.20.0 -m "Upgopher v1.20.0"
+git push origin v1.20.0
+```
+
+Do not publish the draft until both release workflows finish successfully.
+
+## Verify artifacts
+
+- [ ] Linux, macOS, and Windows archives exist for each configured architecture.
+- [ ] `checksums.txt` matches downloaded sample archives.
+- [ ] Extracted binaries run with `-h`.
+- [ ] An existing Community Scripts LXC updates to v1.20.0 without changing
+ `/opt/upgopher/uploads` or `/etc/systemd/system/upgopher.service`.
+- [ ] Homebrew installs and uninstalls on macOS arm64 and amd64.
+- [ ] Scoop installs, updates, and uninstalls on Windows amd64 and arm64.
+- [ ] Both container registries expose `1.20.0`, `1.20`, `1`, and `latest`.
+- [ ] `linux/amd64` and `linux/arm64` images start as UID/GID `65532`.
+- [ ] Container file upload, read-only mode, Basic Auth, custom TLS, and ZIP
+ download work with a bind-mounted `/data`.
+- [ ] The website and README installation commands point to public artifacts.
+
+## Publish and follow up
+
+- [ ] Publish the GitHub release.
+- [ ] Submit the prepared awesome-selfhosted entry.
+- [ ] Record the publication timestamp in `adoption-baseline.md`.
+- [ ] Record the same metrics after 7 and 30 days.
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/docs/.nojekyll
@@ -0,0 +1 @@
+
diff --git a/docs/assets/clipboard.png b/docs/assets/clipboard.png
new file mode 100644
index 0000000..efc2191
Binary files /dev/null and b/docs/assets/clipboard.png differ
diff --git a/docs/assets/directory-tree.png b/docs/assets/directory-tree.png
new file mode 100644
index 0000000..c488211
Binary files /dev/null and b/docs/assets/directory-tree.png differ
diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico
new file mode 100644
index 0000000..c7da3f8
Binary files /dev/null and b/docs/assets/favicon.ico differ
diff --git a/docs/assets/file-manager.png b/docs/assets/file-manager.png
new file mode 100644
index 0000000..8d654bf
Binary files /dev/null and b/docs/assets/file-manager.png differ
diff --git a/docs/assets/logopher.webp b/docs/assets/logopher.webp
new file mode 100644
index 0000000..69b46e7
Binary files /dev/null and b/docs/assets/logopher.webp differ
diff --git a/docs/assets/mobile.png b/docs/assets/mobile.png
new file mode 100644
index 0000000..d76918b
Binary files /dev/null and b/docs/assets/mobile.png differ
diff --git a/docs/assets/site.css b/docs/assets/site.css
new file mode 100644
index 0000000..1db63de
--- /dev/null
+++ b/docs/assets/site.css
@@ -0,0 +1,716 @@
+:root {
+ --paper: #f5f5f5;
+ --paper-deep: #eef7f2;
+ --ink: #333333;
+ --ink-soft: #5f6b66;
+ --signal: #009879;
+ --signal-dark: #3a8c5b;
+ --moss: #45bc75;
+ --line: #dddddd;
+ --white: #ffffff;
+ --shadow: 0 12px 34px rgba(0, 0, 0, 0.1);
+ --max: 1160px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ color: var(--ink);
+ background: var(--paper);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ line-height: 1.65;
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ width: 520px;
+ height: 520px;
+ top: -230px;
+ right: -160px;
+ z-index: -1;
+ pointer-events: none;
+ border-radius: 50%;
+ background: rgba(0, 152, 121, 0.075);
+}
+
+a {
+ color: var(--signal-dark);
+ text-decoration-thickness: 0.09em;
+ text-underline-offset: 0.16em;
+}
+
+a:hover {
+ color: var(--signal);
+}
+
+img {
+ max-width: 100%;
+}
+
+.skip-link {
+ position: absolute;
+ left: 1rem;
+ top: -5rem;
+ z-index: 20;
+ padding: 0.7rem 1rem;
+ color: var(--white);
+ background: var(--ink);
+}
+
+.skip-link:focus {
+ top: 1rem;
+}
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ border-bottom: 1px solid var(--line);
+ background: rgba(245, 245, 245, 0.94);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+ backdrop-filter: blur(10px);
+}
+
+.nav {
+ width: min(calc(100% - 2rem), var(--max));
+ min-height: 70px;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.75rem;
+ color: var(--ink);
+ font-family: inherit;
+ font-size: 1.35rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.brand img {
+ width: 38px;
+ height: 38px;
+ object-fit: contain;
+}
+
+.nav-links {
+ display: flex;
+ align-items: center;
+ gap: 1.4rem;
+ font-size: 0.88rem;
+ font-weight: 650;
+}
+
+.nav-links a {
+ color: var(--ink);
+ text-decoration: none;
+}
+
+.nav-links a[aria-current="page"],
+.nav-links a:hover {
+ color: var(--signal-dark);
+}
+
+.nav-toggle {
+ display: none;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 0.55rem 0.7rem;
+ color: var(--ink);
+ background: var(--white);
+ font: 700 0.8rem inherit;
+}
+
+.wrap {
+ width: min(calc(100% - 2rem), var(--max));
+ margin: 0 auto;
+}
+
+.hero {
+ display: grid;
+ grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr);
+ gap: clamp(2rem, 6vw, 6rem);
+ align-items: center;
+ min-height: calc(100vh - 70px);
+ padding: 5rem 0;
+}
+
+.eyebrow,
+.section-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.6rem;
+ margin: 0 0 1rem;
+ color: var(--signal-dark);
+ font: 750 0.78rem ui-monospace, "SFMono-Regular", Consolas, monospace;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.eyebrow::before,
+.section-label::before {
+ content: "";
+ width: 28px;
+ height: 3px;
+ background: var(--signal);
+}
+
+h1,
+h2,
+h3 {
+ font-family: inherit;
+ line-height: 1.08;
+}
+
+h1 {
+ max-width: 760px;
+ margin: 0;
+ font-size: clamp(3.2rem, 8.5vw, 7rem);
+ font-weight: 780;
+ letter-spacing: -0.055em;
+}
+
+h1 em {
+ color: var(--signal-dark);
+ font-style: normal;
+ font-weight: 780;
+}
+
+.hero-copy {
+ max-width: 650px;
+ margin: 1.6rem 0 0;
+ color: var(--ink-soft);
+ font-size: clamp(1.05rem, 2vw, 1.3rem);
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.8rem;
+ margin-top: 2rem;
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 48px;
+ border: 1px solid var(--signal);
+ border-radius: 8px;
+ padding: 0.72rem 1.1rem;
+ color: var(--white);
+ background: var(--signal);
+ box-shadow: 0 3px 10px rgba(0, 152, 121, 0.2);
+ font: 700 0.82rem inherit;
+ text-decoration: none;
+ transition: transform 140ms ease, box-shadow 140ms ease, background 140ms ease;
+}
+
+.button.secondary {
+ color: var(--ink);
+ background: var(--white);
+ border-color: var(--line);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.button:hover {
+ transform: translateY(-1px);
+ color: var(--white);
+ background: var(--signal-dark);
+ box-shadow: 0 6px 14px rgba(0, 152, 121, 0.24);
+}
+
+.button.secondary:hover {
+ color: var(--ink);
+ background: #f9f9f9;
+ box-shadow: 0 5px 14px rgba(0, 0, 0, 0.09);
+}
+
+.hero-card {
+ position: relative;
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ padding: 1rem;
+ background: var(--white);
+ box-shadow: var(--shadow);
+}
+
+.hero-card::before {
+ content: "LIVE TRANSFER / :9090";
+ display: block;
+ margin-bottom: 0.8rem;
+ color: var(--signal-dark);
+ font: 800 0.72rem ui-monospace, monospace;
+ letter-spacing: 0.08em;
+}
+
+.hero-card img {
+ display: block;
+ width: 100%;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+}
+
+.proof-strip {
+ background: #173c32;
+ color: var(--white);
+}
+
+.proof-list {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+}
+
+.proof-item {
+ padding: 1.2rem;
+ border-right: 1px solid #627078;
+ text-align: center;
+}
+
+.proof-item:last-child {
+ border-right: 0;
+}
+
+.proof-item strong {
+ display: block;
+ color: #7ae3ad;
+ font: 800 1.05rem ui-monospace, monospace;
+}
+
+.proof-item span {
+ font-size: 0.84rem;
+}
+
+.section {
+ padding: clamp(4.5rem, 9vw, 8rem) 0;
+}
+
+.section.alt {
+ border-block: 1px solid #dce9e2;
+ background: var(--paper-deep);
+}
+
+.section-head {
+ display: grid;
+ grid-template-columns: minmax(0, 0.8fr) minmax(280px, 1.2fr);
+ gap: 2rem;
+ align-items: end;
+ margin-bottom: 3rem;
+}
+
+.section h2,
+.doc-hero h1 {
+ margin: 0;
+ font-size: clamp(2.5rem, 6vw, 4.8rem);
+ letter-spacing: -0.045em;
+}
+
+.section-intro {
+ max-width: 650px;
+ margin: 0;
+ color: var(--ink-soft);
+ font-size: 1.1rem;
+}
+
+.use-grid,
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.card {
+ position: relative;
+ min-height: 220px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ padding: 1.5rem;
+ background: var(--white);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.07);
+}
+
+.card:nth-child(2n) {
+ transform: translateY(0.7rem);
+}
+
+.card .number {
+ color: var(--signal-dark);
+ font: 800 0.82rem ui-monospace, monospace;
+}
+
+.card h3 {
+ margin: 2.2rem 0 0.7rem;
+ font-size: 1.55rem;
+}
+
+.card p {
+ margin: 0;
+ color: var(--ink-soft);
+}
+
+.terminal {
+ overflow: hidden;
+ border: 1px solid #285c4d;
+ border-radius: 12px;
+ background: #173c32;
+ color: #eff5e9;
+ box-shadow: var(--shadow);
+}
+
+.terminal-head {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ border-bottom: 1px solid #435058;
+ padding: 0.75rem 1rem;
+ color: #a8b5b8;
+ font: 0.75rem ui-monospace, monospace;
+}
+
+.terminal-head i {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: #7ae3ad;
+}
+
+.terminal pre {
+ overflow-x: auto;
+ margin: 0;
+ padding: 1.4rem;
+ font: 0.88rem/1.8 ui-monospace, "SFMono-Regular", Consolas, monospace;
+}
+
+.terminal .prompt {
+ color: #7ae3ad;
+}
+
+.install-layout {
+ display: grid;
+ grid-template-columns: 0.7fr 1.3fr;
+ gap: clamp(2rem, 6vw, 6rem);
+ align-items: center;
+}
+
+.install-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+ margin-bottom: 0.9rem;
+}
+
+.install-tab {
+ border: 1px solid #647178;
+ padding: 0.5rem 0.7rem;
+ color: #b8c2c4;
+ background: transparent;
+ font: 700 0.75rem ui-monospace, monospace;
+ cursor: pointer;
+}
+
+.install-tab[aria-selected="true"] {
+ border-color: #7ae3ad;
+ color: #173c32;
+ background: #7ae3ad;
+}
+
+.install-panel[hidden] {
+ display: none;
+}
+
+.shot-grid {
+ display: grid;
+ grid-template-columns: 1.25fr 0.75fr;
+ gap: 1rem;
+}
+
+.shot {
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ padding: 0.65rem;
+ background: var(--white);
+ box-shadow: 0 5px 18px rgba(0, 0, 0, 0.08);
+}
+
+.shot img {
+ display: block;
+ width: 100%;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+}
+
+.shot figcaption {
+ padding: 0.65rem 0.2rem 0.1rem;
+ color: var(--ink-soft);
+ font: 0.72rem ui-monospace, monospace;
+ text-transform: uppercase;
+}
+
+.table-wrap {
+ overflow-x: auto;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: var(--white);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.06);
+}
+
+table {
+ width: 100%;
+ min-width: 760px;
+ border-collapse: collapse;
+}
+
+th,
+td {
+ border-bottom: 1px solid var(--line);
+ padding: 0.9rem 1rem;
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ color: var(--white);
+ background: var(--signal);
+ font: 800 0.75rem ui-monospace, monospace;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+tr:last-child td {
+ border-bottom: 0;
+}
+
+tr.highlight td {
+ background: #e9f7f0;
+ font-weight: 650;
+}
+
+.doc-hero {
+ padding: 6rem 0 3rem;
+ border-bottom: 1px solid var(--line);
+ background: var(--paper-deep);
+}
+
+.doc-hero p {
+ max-width: 720px;
+ color: var(--ink-soft);
+ font-size: 1.15rem;
+}
+
+.doc-layout {
+ display: grid;
+ grid-template-columns: 220px minmax(0, 760px);
+ gap: 4rem;
+ align-items: start;
+ padding: 4rem 0 7rem;
+}
+
+.toc {
+ position: sticky;
+ top: 100px;
+ border-left: 3px solid var(--signal);
+ padding-left: 1rem;
+}
+
+.toc strong {
+ font: 800 0.74rem ui-monospace, monospace;
+ text-transform: uppercase;
+}
+
+.toc a {
+ display: block;
+ margin-top: 0.65rem;
+ color: var(--ink-soft);
+ font-size: 0.9rem;
+ text-decoration: none;
+}
+
+.doc-content h2 {
+ margin: 3.5rem 0 1rem;
+ font-size: 2.15rem;
+}
+
+.doc-content h2:first-child {
+ margin-top: 0;
+}
+
+.doc-content h3 {
+ margin: 2rem 0 0.6rem;
+ font-size: 1.35rem;
+}
+
+.doc-content code:not(pre code) {
+ border: 1px solid var(--line);
+ padding: 0.08rem 0.32rem;
+ background: var(--white);
+ font: 0.88em ui-monospace, monospace;
+}
+
+.doc-content pre {
+ overflow-x: auto;
+ border-left: 5px solid var(--signal);
+ padding: 1rem 1.2rem;
+ color: #eff5e9;
+ border-radius: 0 8px 8px 0;
+ background: #173c32;
+ font: 0.88rem/1.7 ui-monospace, monospace;
+}
+
+.callout {
+ margin: 1.5rem 0;
+ border: 1px solid #b8ddc9;
+ border-radius: 10px;
+ padding: 1rem 1.2rem;
+ background: #e9f7f0;
+ box-shadow: 0 3px 12px rgba(0, 0, 0, 0.06);
+}
+
+.callout strong {
+ font-family: ui-monospace, monospace;
+ text-transform: uppercase;
+}
+
+.cta {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 2rem;
+ align-items: center;
+ border: 0;
+ border-radius: 16px;
+ padding: clamp(1.8rem, 5vw, 3.5rem);
+ background: var(--signal);
+ box-shadow: var(--shadow);
+}
+
+.cta h2 {
+ max-width: 720px;
+ color: var(--white);
+}
+
+.site-footer {
+ border-top: 0;
+ padding: 2.5rem 0;
+ background: #173c32;
+ color: #ccd4d5;
+}
+
+.footer-inner {
+ display: flex;
+ justify-content: space-between;
+ gap: 2rem;
+ align-items: center;
+}
+
+.site-footer a {
+ color: #7ae3ad;
+}
+
+@media (max-width: 900px) {
+ .hero,
+ .section-head,
+ .install-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .hero {
+ min-height: auto;
+ }
+
+ .hero-card {
+ max-width: 700px;
+ }
+
+ .proof-list {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .proof-item:nth-child(2) {
+ border-right: 0;
+ }
+
+ .proof-item:nth-child(-n + 2) {
+ border-bottom: 1px solid #627078;
+ }
+
+ .use-grid,
+ .feature-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .doc-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .toc {
+ position: static;
+ }
+}
+
+@media (max-width: 680px) {
+ .nav-toggle {
+ display: block;
+ }
+
+ .nav-links {
+ position: absolute;
+ inset: 70px 0 auto;
+ display: none;
+ align-items: stretch;
+ border-bottom: 1px solid var(--line);
+ padding: 1rem;
+ background: var(--paper);
+ box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
+ }
+
+ .nav-links.open {
+ display: grid;
+ }
+
+ h1 {
+ font-size: clamp(3rem, 18vw, 5rem);
+ }
+
+ .use-grid,
+ .feature-grid,
+ .shot-grid,
+ .cta {
+ grid-template-columns: 1fr;
+ }
+
+ .card:nth-child(2n) {
+ transform: none;
+ }
+
+ .proof-list {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .footer-inner {
+ display: block;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/docs/assets/site.js b/docs/assets/site.js
new file mode 100644
index 0000000..84494d0
--- /dev/null
+++ b/docs/assets/site.js
@@ -0,0 +1,23 @@
+(function () {
+ var toggle = document.querySelector(".nav-toggle");
+ var links = document.querySelector(".nav-links");
+
+ if (toggle && links) {
+ toggle.addEventListener("click", function () {
+ var open = links.classList.toggle("open");
+ toggle.setAttribute("aria-expanded", String(open));
+ });
+ }
+
+ document.querySelectorAll(".install-tab").forEach(function (tab) {
+ tab.addEventListener("click", function () {
+ var target = tab.getAttribute("aria-controls");
+ document.querySelectorAll(".install-tab").forEach(function (item) {
+ item.setAttribute("aria-selected", String(item === tab));
+ });
+ document.querySelectorAll(".install-panel").forEach(function (panel) {
+ panel.hidden = panel.id !== target;
+ });
+ });
+ });
+})();
diff --git a/docs/assets/upgopher-demo.gif b/docs/assets/upgopher-demo.gif
new file mode 100644
index 0000000..2ce43c2
Binary files /dev/null and b/docs/assets/upgopher-demo.gif differ
diff --git a/docs/configuration.html b/docs/configuration.html
new file mode 100644
index 0000000..71981ba
--- /dev/null
+++ b/docs/configuration.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+ Configuration reference — Upgopher
+
+
+
+
+
+
+
+
+ Skip to content
+
+
+
+
+
Documentation / 02
Configuration
Upgopher is configured entirely with command-line flags. No configuration file is required.
+
+
+
+
Files and port
+
upgopher -dir ./shared -port 8080
+
-dir selects the shared directory and defaults to ./uploads. The directory is created when it does not exist. -port defaults to 9090, or 443 when -ssl is explicitly enabled without a custom port.
Both flags are required together. Authentication covers the registered application routes globally; Upgopher does not provide multiple accounts or per-folder roles.
+
Read-only mode
+
upgopher -dir ./public -readonly
+
Disables uploads, file and directory deletion, and directory creation through the application while retaining browsing and downloads.
+
Hidden files
+
upgopher -disable-hidden-files
+
Removes the UI control for showing hidden files and keeps them out of listings.
+
+
HTTPS
+
Self-signed certificate
+
upgopher -ssl
+
A new in-memory self-signed certificate is generated at startup. Browsers will warn because it is not signed by a trusted certificate authority.
Upload size is expressed in GiB; zero means unlimited. Read and write timeout values use Go duration syntax such as 30s, 5m, or 1h. Zero disables the respective overall timeout.
+
+
Flag reference
+
+
+
Flag
Default
Purpose
+
+
-dir
./uploads
Directory to share.
+
-port
9090
TCP port; defaults to 443 when only -ssl changes it.
+
-user
empty
Basic Auth username; requires -pass.
+
-pass
empty
Basic Auth password; requires -user.
+
-ssl
false
Enable HTTPS.
+
-cert
empty
PEM certificate path for HTTPS.
+
-key
empty
PEM private key path for HTTPS.
+
-readonly
false
Disable application upload, create, and delete operations.
+
-disable-hidden-files
false
Keep hidden files out of the UI.
+
-max-upload-size
0
Maximum upload size in GiB; zero is unlimited.
+
-max-tabs
10
Maximum shared clipboard tabs.
+
-read-header-timeout
10s
Maximum time to read request headers.
+
-read-timeout
0
Maximum time to read a complete request; zero is unlimited.
+
-write-timeout
0
Maximum response write duration; zero is unlimited.
+
-q
false
Quiet mode.
+
+
+
+
The executable remains the source of truth. Run upgopher -h to inspect the flags in your installed version.
The image runs as a non-root user and serves /data. Ensure that the mounted directory is readable and writable by UID/GID 65532, or use -readonly with read permission only.
Upgopher sits between a read-only development server and a full storage platform: enough interface to be useful, little enough infrastructure to start and remove in minutes.
+
+
+
+ 01 / LAN DROP
+
Move files without a client
+
Open one address from a phone, laptop, VM, or lab machine and start transferring.
+
+
+ 02 / CLIPBOARD
+
Bridge device clipboards
+
Share text and screenshots through protected tabs when native clipboard sync is unavailable.
+
+
+ 03 / DEVELOPMENT
+
Expose a working directory
+
Hand build output, logs, or test artifacts to another device with a single command.
+
+
+ 04 / READ ONLY
+
Open a download portal
+
Keep navigation and downloads available while blocking uploads and deletions.
+
+
+ 05 / HOMELAB
+
Add a compact file drop
+
Run it behind a VPN, tunnel, or reverse proxy without adding an application database.
+
+
+ 06 / SUPPORT
+
Collect or deliver artifacts
+
Give a temporary browser UI to people who should not need a shell or transfer tool.
+
+
+
+
+
+
+
+
+
Install
+
Pick a package. Keep one process.
+
Every method produces the same self-contained server. Package-manager repositories and container images are maintained by the project.
$ go install github.com/wanetty/upgopher@latest
+$ upgopher -dir ./shared
+
+
+
+
+
+
+
+
+
Interface
+
A practical UI, already inside.
+
+
Browse folders, upload with progress, download selections as ZIP, search text files, and move clipboard content across screens.
+
+
+
+
+ File manager / upload and browse
+
+
+
+ Shared clipboard / text and images
+
+
+
+ Directory tree / fast navigation
+
+
+
+ Responsive UI / phone viewport
+
+
+
+
+
+
+
+
+
+
Trade-offs
+
Choose the tool that fits the job.
+
+
Upgopher optimizes for a useful browser interface with minimal setup. It is not trying to replace multi-user storage platforms or protocol-rich transfer servers.
+
+
+
+
+
Tool
Setup
Strength
Choose it when…
+
+
+
Upgopher
One binary
Files + shared clipboard
You want a practical browser UI without a database or config file.
+
python http.server
Python runtime
Read-only serving
You only need to expose local files briefly.
+
Dufs
One binary
WebDAV + resumable transfers
Transfer protocol support matters more than clipboard sharing.
+
File Browser
Persistent app state
Users + administration
You need accounts, roles, and a managed file portal.
+
Copyparty
Python / bundle
Protocols + advanced transfers
You need resumability, indexing, deduplication, or media features.
Upgopher provides useful controls, but the network and host remain part of the security boundary.
+
+
+
+
Security model
+
Upgopher shares one configured directory through HTTP. Filesystem operations validate that resolved paths stay within that directory, user paths in URLs remain encoded, and the application does not intentionally expose absolute host paths in browser-facing errors.
+
All application routes can be wrapped in one optional Basic Auth credential pair. This is suitable for small trusted deployments, not for multi-tenant authorization.
+
+
Network exposure
+
Default: the server binds to 0.0.0.0. Without Basic Auth, anyone who can reach the port can use the enabled operations.
+
For LAN use, apply host firewall rules if the network includes untrusted devices. For remote access, prefer a private VPN or a reverse proxy with trusted TLS and additional access controls. Do not assume an obscure port provides protection.
Both flags are required. The credential protects the application globally and is sent by the browser on requests after login. Basic Auth requires TLS when traffic crosses an untrusted network.
+
Secrets supplied as command arguments may be visible to other privileged users through process inspection. Use host-level isolation appropriate for your environment.
+
+
TLS options
+
# Local testing; clients will show a trust warning
+upgopher -ssl
+
+# Existing trusted certificate and key
+upgopher -ssl -cert /run/secrets/fullchain.pem -key /run/secrets/privkey.pem
+
The automatically generated certificate is self-signed, in memory, and recreated on restart. It encrypts traffic but does not establish a trusted identity. Public deployments should use a trusted certificate, commonly through a reverse proxy.
+
+
Read-only mode
+
upgopher -dir /srv/public -readonly
+
Read-only mode blocks mutating operations exposed by Upgopher. It does not modify filesystem permissions or prevent another process on the host from changing files. Defense in depth means also mounting or granting the directory read-only when practical.
+
+
State and retention
+
Uploaded files remain in the shared directory. Clipboard text, clipboard images, clipboard tab metadata, and custom path aliases are process memory and disappear when Upgopher restarts. Clipboard content should therefore be treated as temporary, not durable or encrypted storage.
+
Temporary ZIP files are created in the operating system temporary directory while downloads are assembled and are removed after serving.
+
+
Report a vulnerability
+
Contact @gm_eduard privately with the affected version, reproduction steps, impact, and any suggested remediation. Please do not open a public issue before coordinating disclosure.
Start with the process, then the local browser, then the network boundary.
+
+
+
+
Another device cannot connect
+
+
Confirm the server is running and note the port in its log.
+
Open http://localhost:9090 on the host itself.
+
Find the host's LAN address and open http://LAN-IP:9090 from the other device.
+
Allow inbound TCP traffic for that port in the host firewall.
+
Confirm both devices can route to each other; guest Wi-Fi often isolates clients.
+
+
When -ssl is enabled, use https://. Its default port becomes 443 unless -port was explicitly supplied.
+
+
Uploads or ZIP downloads report permission errors
+
The process needs read access to shared files, write access for uploads and mutations, and write access to the operating system temporary directory for ZIP creation.
+
Docker bind mounts
+
The project image runs as UID/GID 65532. Grant that identity appropriate access to the host directory, or run the container in read-only mode with readable files.
+
sudo chown -R 65532:65532 /srv/share
+
Choose permissions that match your host's ownership model; do not make a sensitive directory world-writable just to bypass the error.
+
+
Large uploads stop early
+
Check -max-upload-size, reverse-proxy limits, tunnel limits, available disk space, and request timeouts. For slow large transfers, increase or disable the overall read and write timeouts:
+
upgopher -read-timeout 0 -write-timeout 0
+
Upgopher does not implement resumable uploads. An interrupted upload must be restarted.
+
+
The browser warns about the certificate
+
This is expected when using -ssl without -cert and -key. The generated certificate is self-signed. For trusted access, provide a certificate trusted by your devices or terminate TLS at a reverse proxy.
+
+
The login prompt repeats
+
Confirm both -user and -pass are present, credentials are entered exactly, and a reverse proxy is forwarding the Authorization header. Try a private browser window after changing credentials to avoid cached Basic Auth state.
+
+
Collect diagnostics
+
upgopher -h
+go version
+uname -a # macOS or Linux
+
When opening an issue, include the Upgopher version, operating system and architecture, exact command with passwords removed, expected behavior, and sanitized logs. Never publish credentials or absolute paths containing private information.