- Rust 88.5%
- Nix 8.8%
- Dockerfile 2.7%
| .cargo | ||
| crates | ||
| .dockerignore | ||
| .envrc | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| devenv.lock | ||
| devenv.nix | ||
| devenv.yaml | ||
| Dockerfile | ||
| flake.nix | ||
| README.md | ||
| rust-toolchain.toml | ||
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 URLPUT /media/{key}?token=<token>[&content_type=<mime>]- Redirects to presigned upload URLPOST /media/{key}?token=<token>[&content_type=<mime>]- Redirects to presigned upload URLPATCH /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):
content_typequery parameterContent-Typeheader- Auto-detected from file extension
- Default:
application/octet-stream
Listing
GET /list?token=<token>&recursive=<bool>&prefix=<prefix>- List objectsGET /list/{prefix}?token=<token>&recursive=<bool>- List objects with prefix
Token Management (Admin)
POST /admin/tokens?token=<admin_token>- Create a new tokenGET /admin/tokens?token=<admin_token>- List all tokensPOST /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_serverdhole
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.