An implementation of a tus.io server in Elixir
tus is a protocol based on HTTP for resumable file uploads. Resumable means that an upload can be interrupted at any moment and can be resumed without re-uploading the previous data again.
An interruption may happen willingly, if the user wants to pause, or by accident in case of an network issue or server outage.
It's currently capable of accepting uploads with arbitrary sizes and storing them locally on disk. Due to its modularization and extensibility, support for any cloud provider could easily be added.
Features
This library implements the core TUS API v1.0.0 protocol and the following extensions:
- Creation
- Creation With Upload -- send the first chunk with the creation request
- Termination
- Expiration -- advertised only when
expiration_periodis configured
Deferring the upload's length is not supported; Upload-Length must be known
at creation time.
Installation
Add this repo to your list of dependencies in mix.exs:
def deps do
[
{:tussle, "~> 0.4.0"},
]
endUsage
1. Add new controller(s)
defmodule DemoWeb.UploadController do
use DemoWeb, :controller
use Tussle.Controller
# start upload optional callback
def on_begin_upload(file) do
...
:ok # or {:error, reason} to reject the uplaod
end
# Completed upload optional callback
def on_complete_upload(file) do
...
end
end2. Add routes for each of your upload controllers
The simplest way is to use the Tussle.Routes macro in your router:
defmodule DemoWeb.Router do
use DemoWeb, :router
import Tussle.Routes
scope "/files", DemoWeb do
pipe_through :api
add_tus_routes UploadController
end
endOr define routes manually:
scope "/files", DemoWeb do
options "/", UploadController, :options
post "/", UploadController, :post
match :head, "/:uid", UploadController, :head
get "/:uid", UploadController, :get # CloudFlare compatibility
patch "/:uid", UploadController, :patch
delete "/:uid", UploadController, :delete
end⚠️ CloudFlare Note: CloudFlare's caching layer converts HEAD requests to GET, which unexpectedly violates the expectations of the TUS protocol. The
get/:uidroute above mirrors HEAD behavior to restore compatibility. SeeTussle.get/2andTussle.Routesfor details.
3. Add config for each controller (see next section)
Configuration (the global way)
# List here all of your upload controllers
config :tussle, controllers: [DemoWeb.UploadController]
# This is the config for the DemoWeb.UploadController
config :tussle, DemoWeb.UploadController,
storage: Tussle.Storage.Local,
base_path: "priv/static/files/",
# expire ttl for a cache entry, in seconds. If missing Expiration Protocol is not enabled
expiration_period: 300,
cache: Tussle.Cache.Memory,
# max supported file size, in bytes (default 20 MB)
max_size: 1024 * 1024 * 20storage: module which handle storage file application This library includes onlyTussle.Storage.Localbut you can install thetus_storage_s3hex package to use Amazon S3.expiration_period: expire unfinished uploads after a specified number of seconds so they can removed from cachecache: module for handling the temporary uploads metadata This library comes withTussle.Cache.Memorybut you can install thetus_cache_redishex package to use a Redis based one.max_size: hard limit on the maximum size an uploaded file can have
Options for Tussle.Storage.Local
base_path: where in the filesystem the uploaded files'll be stored
CORS
Tussle sets no CORS headers itself. For cross-origin browser uploads the host
application must expose the response headers clients read back, at minimum
Location and Upload-Offset; a browser hides them from JavaScript
otherwise, leaving uploads unable to resume. See the README for a worked
example and the full header list.
Summary
Functions
The extensions supported regardless of configuration, as a Tus-Extension value.
The extensions supported for config, as a Tus-Extension value.
Handles GET requests for upload metadata.
Functions
The extensions supported regardless of configuration, as a Tus-Extension value.
See extension/1 for the value actually advertised for a given controller.
The extensions supported for config, as a Tus-Extension value.
The Expiration extension is only advertised when an :expiration_period is
configured, since without one no Upload-Expires header is ever sent.
Handles GET requests for upload metadata.
This is not part of the TUS specification, which only defines HEAD for retrieving upload metadata. However, some CDN/proxy configurations (most notably CloudFlare) convert HEAD requests to GET requests, which unexpectedly violates the expectations of the TUS protocol.
This function delegates to head/2 and returns the same headers (Upload-Offset,
Upload-Length, etc.) with an empty body, making it functionally equivalent to
HEAD for clients.
Why This Exists
⚠️ CloudFlare Compatibility Note
CloudFlare's caching layer converts HEAD requests to GET requests. The TUS protocol specifies HEAD for metadata retrieval, so this conversion can cause requests to not match HEAD routes, resulting in 404 errors. Adding a GET route that mirrors HEAD behavior restores compatibility.
If you're using CloudFlare or similar CDNs with TUS, add both HEAD and GET routes, or use
Tussle.Routes.add_tus_routes/1which includes both automatically.
Example Routes
scope "/files", DemoWeb do
options "/", UploadController, :options
post "/", UploadController, :post
match :head, "/:uid", UploadController, :head
get "/:uid", UploadController, :get # CloudFlare compatibility
patch "/:uid", UploadController, :patch
delete "/:uid", UploadController, :delete
end