S3 proxy that allows uploading and downloading files with a simple query param token, so links can be easily shared.
  • Rust 88.5%
  • Nix 8.8%
  • Dockerfile 2.7%
Find a file
2026-04-15 17:41:24 +02:00
.cargo Initial commit 2026-04-13 11:16:34 +02:00
crates Fix meta data handling 2026-04-15 17:41:24 +02:00
.dockerignore Initial commit 2026-04-13 11:16:34 +02:00
.envrc Initial commit 2026-04-13 11:16:34 +02:00
.gitignore Initial commit 2026-04-13 11:16:34 +02:00
Cargo.lock Initial commit 2026-04-13 11:16:34 +02:00
Cargo.toml Initial commit 2026-04-13 11:16:34 +02:00
devenv.lock Initial commit 2026-04-13 11:16:34 +02:00
devenv.nix Initial commit 2026-04-13 11:16:34 +02:00
devenv.yaml Initial commit 2026-04-13 11:16:34 +02:00
Dockerfile Initial commit 2026-04-13 11:16:34 +02:00
flake.nix Initial commit 2026-04-13 11:16:34 +02:00
README.md Fix metadata mime type handling 2026-04-15 11:29:30 +02:00
rust-toolchain.toml Initial commit 2026-04-13 11:16:34 +02:00

Dhole

A Rust workspace containing:

  • dhole_server - An HTTP proxy server that authenticates requests via path-scoped tokens and redirects to presigned S3 URLs
  • dhole - A CLI tool for uploading, downloading, listing, and deleting files through the proxy

Architecture

┌─────────┐      ┌──────────────┐      ┌─────────┐
│  Client │─────▶│ dhole_server │─────▶│   S3    │
│ (dhole) │◀─────│  (redirect)  │      │ (Wasabi)│
└─────────┘      └──────────────┘      └─────────┘

The proxy validates tokens and returns presigned URLs:

  • GET requests: Redirect (307) to presigned download URL
  • PUT/POST requests: Return JSON with presigned upload URL
  • DELETE requests: Delete object directly via S3 API
  • LIST requests: Return JSON listing of objects

Token System

Dhole uses a hierarchical token system for fine-grained access control:

  • Admin Token: Root token that can create/revoke other tokens for any path
  • Path-scoped Tokens: Tokens that grant specific permissions (read, write, delete, list, admin) for a path prefix and all its subpaths

Permissions

Permission Description
read Download files (GET)
write Upload files (PUT/POST)
delete Delete files (DELETE)
list List files
admin Create tokens for this path and subpaths

dhole_server

Configuration (Environment Variables)

Variable Description Default
S3_BUCKET S3 bucket name (required)
S3_REGION S3 region us-east-1
S3_ENDPOINT S3 endpoint URL Auto-derived from region for Wasabi
BIND_ADDR Server bind address 0.0.0.0:3000
PRESIGN_EXPIRY_SECS Presigned URL expiry 900
TOKEN_DB_PATH Path to token database file tokens.db
ADMIN_TOKEN Admin token (auto-generated if not set on first run) (generated)
CLI_CONFIG_PATH Path to write CLI config with admin token ~/.config/dhole/config.toml

Running

# First run - generates admin token and writes CLI config to ~/.config/dhole/config.toml
S3_BUCKET=my-bucket cargo run -p dhole_server

# Or set your own admin token
S3_BUCKET=my-bucket ADMIN_TOKEN=your-secret-admin-token cargo run -p dhole_server

# Specify custom CLI config location
S3_BUCKET=my-bucket CLI_CONFIG_PATH=/path/to/config.toml cargo run -p dhole_server

On first startup, the server automatically writes a CLI config file with the admin token, so you can immediately use dhole commands without manual configuration.

Endpoints

Media Operations

  • GET /media/{key}?token=<token> - Redirects to presigned download URL
  • PUT /media/{key}?token=<token>[&content_type=<mime>] - Redirects to presigned upload URL
  • POST /media/{key}?token=<token>[&content_type=<mime>] - Redirects to presigned upload URL
  • PATCH /media/{key}?token=<token> - Update object metadata (content-type)
  • DELETE /media/{key}?token=<token> - Deletes the object

Content type is determined by (in order of priority):

  1. content_type query parameter
  2. Content-Type header
  3. Auto-detected from file extension
  4. Default: application/octet-stream

Listing

  • GET /list?token=<token>&recursive=<bool>&prefix=<prefix> - List objects
  • GET /list/{prefix}?token=<token>&recursive=<bool> - List objects with prefix

Token Management (Admin)

  • POST /admin/tokens?token=<admin_token> - Create a new token
  • GET /admin/tokens?token=<admin_token> - List all tokens
  • POST /admin/tokens/revoke?token=<admin_token> - Revoke a token

Health

  • GET /health - Health check

Token Management Examples

# Create a read-only token for "public/" path
curl -X POST "http://localhost:3000/admin/tokens?token=<admin_token>" \
  -H "Content-Type: application/json" \
  -d '{"path_prefix": "public/", "permissions": ["read", "list"]}'

# Create a full-access token for "uploads/" path
curl -X POST "http://localhost:3000/admin/tokens?token=<admin_token>" \
  -H "Content-Type: application/json" \
  -d '{"path_prefix": "uploads/", "permissions": ["read", "write", "delete", "list"]}'

# List all tokens
curl "http://localhost:3000/admin/tokens?token=<admin_token>"

# Revoke a token
curl -X POST "http://localhost:3000/admin/tokens/revoke?token=<admin_token>" \
  -H "Content-Type: application/json" \
  -d '{"token": "<token_to_revoke>"}'

dhole (CLI)

Configuration

Config file location: ~/.config/dhole/config.toml

proxy_url = "http://localhost:3000"

# Admin token for managing other tokens
admin_token = "your-admin-token"

# Default tokens (used when no path-specific token matches)
[default_tokens]
read = "your-default-read-token"
write = "your-default-write-token"
delete = "your-default-delete-token"
list = "your-default-list-token"

# Path-specific tokens (optional)
[paths."public/"]
read = "public-read-token"
list = "public-list-token"

[paths."uploads/"]
read = "uploads-read-token"
write = "uploads-write-token"
delete = "uploads-delete-token"
list = "uploads-list-token"

Usage

# Initialize config
dhole config init

# Set proxy URL and admin token
dhole config set --proxy-url http://localhost:3000
dhole config set --admin-token your-admin-token

# Set default tokens
dhole config set --read-token your-read-token
dhole config set --write-token your-write-token
dhole config set --delete-token your-delete-token
dhole config set --list-token your-list-token

# Set path-specific tokens
dhole config set-path "uploads/" --read-token abc123 --write-token def456

# Remove path-specific tokens
dhole config remove-path "uploads/"

# Show configuration
dhole config show

# Show config file path
dhole config path

File Operations

# Upload a file (content-type auto-detected from extension)
dhole upload path/to/key.txt --file ./local-file.txt

# Upload with explicit content type
dhole upload path/to/key.txt --file ./local-file.txt --content-type text/plain

# Upload string data
dhole upload path/to/key.txt --data "Hello, World!"

# Upload from stdin
echo "Hello from stdin" | dhole upload path/to/key.txt --stdin

# Upload a directory recursively
dhole upload remote/prefix/ --file ./local-dir --recursive

# Download to file
dhole download path/to/key.txt --output ./local-file.txt

# Download to stdout
dhole download path/to/key.txt

# Download recursively
dhole download remote/prefix/ --output ./local-dir --recursive

# List files
dhole list
dhole list some/prefix/
dhole list some/prefix/ --recursive

# Delete a file
dhole delete path/to/key.txt

# Delete recursively
dhole delete some/prefix/ --recursive

Token Management (via CLI)

When you create a token, it's automatically saved to your config file for the appropriate path and permissions.

# Create a new token (auto-saved to config)
dhole token create --path "public/" --permissions read,list

# Create a token with multiple permissions (auto-saved)
dhole token create --path "uploads/" --permissions read,write,delete,list

# Create a root token (empty path, saved to default_tokens)
dhole token create --permissions read,list

# Create a token without saving to config
dhole token create --path "temp/" --permissions read --no-save

# List all tokens
dhole token list

# Revoke a token
dhole token revoke <token>

Building

Cargo

cargo build --release

Binaries will be in target/release/:

  • dhole_server
  • dhole

Docker

# Build the image
docker build -t dhole .

# Run the server
docker run -d --name dhole-server \
  -p 3000:3000 \
  -v dhole-data:/data \
  -e S3_BUCKET=my-bucket \
  -e S3_REGION=eu-central-2 \
  -e AWS_ACCESS_KEY_ID=your-key \
  -e AWS_SECRET_ACCESS_KEY=your-secret \
  dhole

# Use the CLI from the same container (shares config with server)
docker exec dhole-server dhole token list
docker exec dhole-server dhole token create --path "public/" --permissions read,list
docker exec dhole-server dhole list

# Or run CLI in a separate container with shared volume
docker run --rm -v dhole-data:/data -e HOME=/tmp \
  -e XDG_CONFIG_HOME=/data \
  dhole dhole --help

The server automatically writes a CLI config file to /data/cli-config.toml on first startup, which is symlinked to the default CLI config location. This allows you to use dhole commands inside the container without any manual setup.

Nix

# Build both packages
nix build

# Build just the CLI
nix build .#dhole

# Build just the server
nix build .#dhole_server

# Build Docker image via Nix
nix build .#docker
docker load < result

# Run in development shell
nix develop

# Install to your profile
nix profile install .#dhole
nix profile install .#dhole_server

Using as a Nix overlay

# In your flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    dhole.url = "github:your-repo/dhole";
  };

  outputs = { self, nixpkgs, dhole, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ({ pkgs, ... }: {
          nixpkgs.overlays = [ dhole.overlays.default ];
          environment.systemPackages = [
            pkgs.dhole
            pkgs.dhole_server
          ];
        })
      ];
    };
  };
}

NixOS service module

{ config, pkgs, ... }:
{
  systemd.services.dhole = {
    description = "Dhole S3 Proxy";
    after = [ "network.target" ];
    wantedBy = [ "multi-user.target" ];

    environment = {
      S3_BUCKET = "my-bucket";
      S3_ENDPOINT = "https://s3.wasabisys.com";
      S3_REGION = "eu-central-2";
      BIND_ADDR = "127.0.0.1:3000";
      TOKEN_DB_PATH = "/var/lib/dhole/tokens.db";
    };

    serviceConfig = {
      Type = "simple";
      ExecStart = "${pkgs.dhole_server}/bin/dhole_server";
      DynamicUser = true;
      StateDirectory = "dhole";
      EnvironmentFile = "/run/secrets/dhole-env"; # For AWS credentials
    };
  };
}

License

See LICENSE file.