Skip to content

zoickx/theseus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Theseus

A self-hosted file server for stable, long-lived links. Permanent uploads have no app-level expiration.

Theseus treats URLs as the stable interface; the rest of the deployment is replaceable. Theseus is stateless - all data lives on a swappable storage backend via rclone, in a plain directory layout that can be inspected and migrated with basic tools. The server can be rebuilt, rewritten, or moved to a different host while preserving existing links.

Files are served verbatim and inline by design: image URLs return images, PDF URLs return PDFs, and so on. There are no download pages or intermediate redirects. Documents, scripts, and other tools can reference the same URLs directly.

This rules out end-to-end encryption - the server must read files to serve them. If your storage provider is untrusted, use rclone crypt as a backend wrapper for transparent server-side encryption at rest.

There are two storage tiers: permanent uploads (/p/) are a personal write-once archive intended to never be deleted, while temporary uploads (/t/) auto-expire after a configurable TTL.

Features

  • CLI and browser uploads - curl -T file.txt https://files.example.com or use the web form
  • Rclone storage backends - S3, B2, SFTP, local disk, or anything else rclone supports
  • Permanent and temporary uploads - /p/ for your archive, /t/ for ephemeral sharing
  • Archive extraction - upload .tar.gz, .tar.bz2, .tar.zst, or .tar and extract server-side
  • Private links - each upload gets a 128-bit random key; no directory listing, no enumeration; only those with the link can access a file
  • Directory browsing - navigate extracted archives and multi-file uploads in the browser
  • Stateless server - all state lives in the storage backend; plain files, no database
  • Security controls - Argon2id auth, sandboxed file I/O, tar bomb protection, CSP headers
  • Admin dashboard - view and delete uploads through a web interface

Setup

1. Create config files

theseus.json (see Configuration for all options):

{
    "admin_password": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH",
    "rclone_remote": "myremote:",
    "base_url": "https://files.example.com",
    "upload_key": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH"
}

Generate Argon2id hashes with:

echo -n 'your-password' | argon2 "$(openssl rand -base64 16)" -id -e

rclone.conf - configure your storage backend with rclone config.

2. Deploy with Compose

# compose.yml
services:
  theseus:
    image: ghcr.io/zoickx/theseus:latest
    expose:
      - 7714
    devices:
      - /dev/fuse
    cap_add:
      - SYS_ADMIN
    secrets:
      - theseus.json
      - rclone.conf

secrets:
  theseus.json:
    file: ./theseus.json
  rclone.conf:
    file: ./rclone.conf
podman compose up -d

The container runs as a non-root user. SYS_ADMIN is required for FUSE mounts.

Theseus does not handle TLS or rate limiting - put it behind a reverse proxy (Caddy, nginx, etc.) in production.

The container starts rclone (mounting your remote to /mnt/storage), a cron daemon for TTL cleanup, and the HTTP server - all supervised by the entrypoint script under tini.

Usage

Uploading

CLI (curl):

# Temporary upload (auto-expires)
curl -T file.txt https://files.example.com

# With authentication
curl -T file.txt -H "Authorization: Bearer YOUR_KEY" https://files.example.com

# Upload and extract an archive
curl -T site.tar.gz https://files.example.com?extract=true

# Permanent upload (if enabled)
curl -T file.txt https://files.example.com/p/

Browser: visit the root URL to use the web upload form.

Downloading

Files are served at their upload path:

https://files.example.com/t/{key}/filename.txt     # temporary
https://files.example.com/p/{key}/filename.txt      # permanent

Extracted archives serve their full directory tree. Append ?download=true to force a download instead of inline display.

Admin

Navigate to /admin/ and log in with the admin password to view all uploads and delete individual files or entire upload keys.

API

Method Path Description
PUT / Upload to temp storage
PUT /t/ Upload to temp storage (explicit)
PUT /p/ Upload to permanent storage
POST / Browser multipart upload
GET /t/{key}/... Download from temp
GET /p/{key}/... Download from permanent
GET /admin/ Admin dashboard

Query parameters: ?filename=<name> (override filename), ?extract=true (extract archive), ?download=true (force download).

Authentication: Authorization: Bearer <upload_key> header, or via the browser form. Admin uses session cookies.

Configuration

Configuration is loaded from /run/secrets/theseus.json (override with CONFIG_FILE env var).

Required

Option Type Description
admin_password string Argon2id hash in PHC format for admin dashboard login
rclone_remote string Rclone remote name as configured in rclone.conf (include trailing colon)

Recommended

Option Type Default Description
upload_key string Argon2id hash in PHC format for upload authentication (empty = anyone can upload). Plaintext key is used as Bearer token with curl or entered via the web upload form.
allow_permanent bool false Enable /p/ endpoint for permanent uploads
max_upload_size string "10G" Maximum upload size (e.g., "500M", "2GB")
temp_ttl string "672h" Time-to-live for temporary uploads. Go duration format (h/m/s only).
base_url string Public URL for generating links in responses (no trailing slash)
vfs_cache_max_size string Max size for rclone VFS cache (e.g., "10G")

Optional

Option Type Default Description
listen_addr string ":7714" Address and port for the HTTP server
max_compression_ratio int 20 Max extracted size as multiple of max_upload_size. Tar bomb protection.
cleanup_schedule string "0 4 * * *" Cron schedule for TTL cleanup (daily at 4am)
graceful_shutdown_timeout int 30 Seconds to wait for graceful shutdown

Example

{
    "admin_password": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH",
    "rclone_remote": "myremote:",
    "base_url": "https://files.example.com",
    "allow_permanent": false,
    "listen_addr": ":7714",
    "max_upload_size": "10G",
    "temp_ttl": "672h",
    "cleanup_schedule": "0 4 * * *",
    "graceful_shutdown_timeout": 30,
    "upload_key": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH",
    "vfs_cache_max_size": "10G"
}

Storage layout

/mnt/storage/
├── t/                          # temporary uploads
│   └── {22-char-base62-key}/
│       ├── filename.ext
│       └── filename.ext.meta.json
└── p/                          # permanent uploads
    └── {22-char-base62-key}/
        ├── filename.ext
        └── filename.ext.meta.json

Each upload gets a unique 22-character base62 key (128 bits of randomness). Metadata sidecars store SHA256 checksum, content type, and upload timestamp.

Security

  • Authentication - Argon2id password hashing for both upload keys and admin login
  • Sessions - HMAC-SHA256 signed cookies with IP binding
  • File I/O - sandboxed with os.Root to prevent path traversal
  • Archive safety - rejects symlinks, hard links, device files; enforces compression ratio limits
  • Content serving - Content-Security-Policy: sandbox on all served files
  • Cookies - HttpOnly, Secure, SameSite=Strict
  • MIME validation - permanent uploads require detected MIME type to match file extension

About

Self-hosted file server built for permanence

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors