Tussle (Tussle v0.4.0)

Copy Markdown View Source

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:

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"},
  ]
end

Usage

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
end

2. 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
end

Or 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/:uid route above mirrors HEAD behavior to restore compatibility. See Tussle.get/2 and Tussle.Routes for 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 * 20
  • storage: module which handle storage file application This library includes only Tussle.Storage.Local but you can install the tus_storage_s3 hex package to use Amazon S3.

  • expiration_period: expire unfinished uploads after a specified number of seconds so they can removed from cache

  • cache: module for handling the temporary uploads metadata This library comes with Tussle.Cache.Memory but you can install the tus_cache_redis hex 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

delete(conn, config)

extension()

The extensions supported regardless of configuration, as a Tus-Extension value.

See extension/1 for the value actually advertised for a given controller.

extension(config)

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.

get(conn, config)

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/1 which 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

head(conn, config)

latest_version()

options(conn, config)

patch(conn, config)

post(conn, config)

str_supported_versions()

supported_versions()