Skip to content

jankkm/owntransfer

Repository files navigation

OwnTransfer

Self-hosted file sharing for teams. Authenticated users send files to external recipients or collect uploads via inbound file requests. Public links are password-protected, time-limited, and configurable with download/upload caps.

Built with FastAPI, server-rendered HTML, and SQLite or PostgreSQL.

Screenshots

Screenshots coming soon.

Features

Transfers (outbound)

  • Upload one or more files and share a public download link
  • Optional password, expiry date, and download limit (0 = unlimited)
  • Email share links to recipients (when SMTP is configured)
  • Optional notification when someone downloads
  • Enable or disable a link without deleting it
  • Edit transfers after creation, including extending expiry on expired links
  • Regenerate public token (invalidates the old link)

File requests (inbound)

  • Create a link for external users to upload files to you
  • Staged multi-file upload with optional password and upload limits
  • Download received uploads as a ZIP from the dashboard
  • Same enable/disable, edit, expiry, and link-regeneration workflow as transfers

Public access

  • Unguessable link tokens (secrets.token_urlsafe(32))
  • Password unlock with signed cookies (no account required for recipients)
  • ZIP or per-file download for transfers
  • Rate limiting on public endpoints (30 requests/minute per IP by default)
  • Unknown tokens redirect to login; expired or disabled links return 403/410 without treating them as attacks

Authentication

  • OAuth2 — Microsoft Entra ID out of the box (extensible to more providers)
  • Local login — email/password backup on the login screen (can be disabled)
  • First-boot setup wizard creates the initial admin account
  • OAuth users are auto-provisioned on first login (matched by email)

Admin panel

Area What you can configure
Branding App name, logo, color scheme
Limits Max file size, default expiry, max share lifetime, default download limit, purge grace period, file extension blocklist, local login, user-sent share emails
SMTP Outbound mail for share links and notifications
Email templates Editable Jinja2 subjects and HTML bodies for all notification types
Shares Overview of all transfers and requests across users; edit or delete any share
Users Create users, promote/demote admins, reset passwords, delete accounts
Impressum Optional legal notice page (Markdown)
Audit log Recent admin and system actions

Email notifications

When SMTP is configured, the app can send:

  • Share link emails to transfer recipients
  • File-request link emails
  • Upload-received notifications to the request owner
  • Download notifications (when enabled per transfer)
  • Expired unused — when a transfer or file request expires with zero downloads/uploads
  • Deletion reminder — configurable days before permanent purge (global setting; 0 = disabled)

All templates (subject and body) are editable under Admin → Email templates.

Lifecycle and cleanup

A background job runs every 15 minutes to:

  1. Mark shares past their expiry date as expired
  2. Email owners when an expired share had no downloads or uploads
  3. Email owners before auto-delete, if Deletion reminder is enabled in admin limits
  4. Permanently delete expired shares after a configurable purge grace period (days; 0 = auto-delete disabled)

During the grace period, shares show an Expired and Deletion pending badge in the UI. Extending expiry on the edit page clears the expired state and resets notification flags.

Status badges

Transfers and file requests display clear status badges: Active, Expired, Disabled, download/upload limit reached, Password protected, Notifications (transfers with download alerts), and Deletion pending.

Quick start

git clone https://github.com/your-org/owntransfer.git
cd owntransfer
cp .env.example .env
# Set SECRET_KEY in .env (see Configuration below)
docker compose up --build

On first boot, the app prints a one-time setup token to the container logs. Look for a line like:

Setup token (required once): <long-random-string>

Open http://localhost:8080/setup, paste that token, and create your admin account. If you restart the container before finishing setup, check the logs again — a new token is generated each time.

To use a fixed token instead (for example in automation), set SETUP_TOKEN in .env before starting the app.

Configuration

Most settings live in the database and are managed from the admin panel after first boot. Environment variables seed defaults on startup.

See .env.example for the full list. Important variables:

Variable Purpose
SECRET_KEY Session signing — required; generate a long random string
SETUP_TOKEN Optional fixed token for the first-boot setup wizard; when unset, a random token is logged at startup
BASE_URL Public URL used in share links and emails (e.g. https://transfer.example.com)
PUBLIC_SCHEME http or https — override scheme behind a TLS-terminating reverse proxy (OAuth callbacks)
DB_BACKEND sqlite (default) or postgres
SQLITE_PATH SQLite database file (default /data/owntransfer.db)
DATABASE_URL Optional full database URL override
UPLOAD_DIR Local file storage path (default /data/uploads)
UPLOAD_CONCURRENCY Default for Admin → Limits → Parallel file uploads on first boot (default 5, range 1–50)
DISPLAY_TIMEZONE IANA timezone for UI and emails (e.g. Europe/Berlin)
ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET Microsoft OAuth (optional)
SMTP_* Email defaults (overridable in admin after first boot)
TRUST_PROXY_HEADERS Set true behind a reverse proxy so client IPs and URLs are correct
TRUSTED_PROXY_HOPS / TRUSTED_PROXY_IPS Required when proxy headers are enabled — limit which peers may send X-Forwarded-For / X-Real-IP

PostgreSQL

docker compose -f docker-compose.yml -f docker-compose.postgres.yml up --build

Or set DB_BACKEND=postgres and DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/db in .env.

Secrets from files

Any environment variable supports a VAR_FILE companion that points to a file whose contents become VAR. This works for all settings (for example SECRET_KEY_FILE, POSTGRES_PASSWORD_FILE, ENTRA_CLIENT_SECRET_FILE). When both are set, the file value takes precedence.

Reverse proxy

Run OwnTransfer behind nginx, Caddy, or Traefik in production. Set BASE_URL to your public HTTPS URL. If the app receives HTTP internally behind TLS termination, set PUBLIC_SCHEME=https so OAuth callbacks and other generated URLs use HTTPS. Enable TRUST_PROXY_HEADERS=true so security logging and rate limiting see the real client IP (and so X-Forwarded-Proto is honored when present). Also set TRUSTED_PROXY_IPS to the proxy's address (for example 127.0.0.1 or your Docker network CIDR). If TRUSTED_PROXY_IPS is empty, the app logs a startup warning because any client could spoof forwarded headers.

Large multi-file uploads need a longer proxy timeout than the common 60-second default. Without this, uploads fail with 502 Bad Gateway once the limit is hit. Examples:

nginx

proxy_read_timeout 600s;
proxy_send_timeout 600s;
client_body_timeout 600s;

Traefik (Docker labels on the router/service, or static/dynamic file):

# Dynamic configuration (e.g. dynamic.yml)
http:
  services:
    owntransfer:
      loadBalancer:
        servers:
          - url: "http://owntransfer-app-1:8080"
        responseForwarding:
          flushInterval: 100ms
  middlewares:
    owntransfer-timeouts:
      buffering:
        maxRequestBodyBytes: 0  # disable buffering for large uploads
  routers:
    owntransfer:
      rule: "Host(`your-domain`)"
      service: owntransfer
      middlewares:
        - owntransfer-timeouts

For Traefik v2/v3, set transport timeouts on the serversTransport (or per-service in newer versions):

# dynamic.yml — serversTransport
serversTransports:
  owntransfer-transport:
    forwardingTimeouts:
      dialTimeout: 30s
      responseHeaderTimeout: 600s
      idleConnTimeout: 600s

Link the transport to your service via serversTransport: owntransfer-transport@file.

Caddy — increase response_header_timeout / upstream timeouts in your site block as needed.

Microsoft Entra ID

  1. Azure Portal → App registrationsNew registration

  2. Redirect URI (Web): https://your-domain/auth/oauth/entra/callback

  3. Create a client secret under Certificates & secrets

  4. API permissions: openid, email, profile (Microsoft Graph, delegated)

  5. Add to .env:

    ENTRA_TENANT_ID=your-tenant-id
    ENTRA_CLIENT_ID=your-client-id
    ENTRA_CLIENT_SECRET=your-client-secret
    BASE_URL=https://your-domain
    

Promote OAuth users to admin in Admin → Users after their first login.

Deployment

Docker

The included docker-compose.yml mounts a named volume at /data for the database and uploads. For production, place a reverse proxy in front and set a strong SECRET_KEY.

Persistent storage

Mount a volume (or bind mount) at /data so the SQLite database and uploaded files survive container restarts. Set UPLOAD_DIR=/data/uploads and SQLITE_PATH=/data/owntransfer.db.

The app uses a storage abstraction (app/services/storage/). Local disk is the default; S3-compatible backends can be wired in without changing business logic.

Kubernetes

Run the same container image with a persistent volume claim for /data, configure secrets for SECRET_KEY and database credentials, and expose the service through an ingress controller.

Security

  • First-boot setup requires a one-time token (printed to container logs, or set via SETUP_TOKEN)
  • Public link tokens are cryptographically random; passwords are bcrypt-hashed
  • Rate limiting on public download and upload routes
  • Session cookies are HTTP-only
  • File extension blocklist configurable in admin
  • Audit log for administrative actions

Fail2ban

OwnTransfer writes structured WARNING lines to stdout for events that indicate probing or brute force:

Event Trigger
invalid_login Failed local login
invalid_transfer_link Unknown /d/{token}
invalid_request_link Unknown /r/{token}

Expired, disabled, or limit-reached links are not logged — the token exists; access is denied with 403/410.

Example log line:

2026-06-22 12:00:00 WARNING [owntransfer.security] event=invalid_login ip=203.0.113.10 method=POST [email protected]

Filter — save as /etc/fail2ban/filter.d/owntransfer.conf:

[Definition]
failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} WARNING \[owntransfer\.security\] event=(invalid_login|invalid_transfer_link|invalid_request_link) ip=<HOST>.*$
ignoreregex =
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S

Jail — save as /etc/fail2ban/jail.d/owntransfer.conf (adjust logpath):

[owntransfer]
enabled = true
port = http,https
filter = owntransfer
logpath = /var/log/owntransfer/app.log
maxretry = 5
findtime = 600
bantime = 3600

Point logpath at your application log file. With Docker, forward container stdout to a file or use your platform's log shipping. Enable TRUST_PROXY_HEADERS behind a reverse proxy so ip= is the real client address.

Localization

The UI supports English (en), German (de), and Spanish (es). More languages can be added by extending SUPPORTED_LOCALES in app/i18n/__init__.py and creating a new catalog under app/locales/.

How the active language is chosen:

  1. locale cookie — set when a user picks a language in the footer switcher
  2. Accept-Language browser header — used on first visit
  3. DEFAULT_LOCALE environment variable — fallback (default en)

Admin-editable legal pages and custom email template overrides are not localized; built-in UI strings and default email templates are.

Developer workflow (after changing user-facing strings):

pybabel extract -F babel.cfg -o app/locales/messages.pot .
pybabel update -i app/locales/messages.pot -d app/locales -l de
pybabel update -i app/locales/messages.pot -d app/locales -l es
# Edit app/locales/de/LC_MESSAGES/messages.po (or run scripts/fill_de_translations.py)
# Edit app/locales/es/LC_MESSAGES/messages.po (or run scripts/fill_es_translations.py)
pybabel compile -d app/locales

Development

Requirements: Python 3.11+, Node.js 18+ (for Tailwind CSS builds)

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

export DB_BACKEND=sqlite
export SQLITE_PATH=./owntransfer.db
export UPLOAD_DIR=./uploads
export SECRET_KEY=dev-secret

uvicorn app.main:app --reload --port 8080
pytest

After changing HTML templates or frontend classes in app/static/*.js, rebuild the CSS locally:

npm install
npm run build:css

Docker images (including GitHub Actions releases) build app/static/tailwind.css automatically during docker build.

The database schema is created automatically on startup via ensure_schema in the application lifespan.

License

Copyright (C) 2026 Jan K.

OwnTransfer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3.0 (AGPL-3.0).

If you run a modified version of this software as a network service, you must make the corresponding source code available to users interacting with it over a network.

About

Self-hosted file sharing for teams. Authenticated users send files to external recipients or collect uploads via inbound file requests.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors