Packages

An implementation of resumable upload protocol TUS in Elixir

Current section

Files

Jump to
extus lib actions.ex
Raw

lib/actions.ex

defmodule ExTus.Actions do
import Plug.Conn
alias ExTus.Utils
alias ExTus.UploadInfo
alias ExTus.UploadCache
require Logger
defp storage() do
Application.get_env(:extus, :storage, ExTus.Storage.Local)
end
def options(conn)do
conn
|> handle_preflight_request
|> put_resp_header("Tus-Resumable", ExTus.Config.tus_api_version)
|> put_resp_header("Tus-Version", ExTus.Config.tus_api_version_supported)
|> put_resp_header("Tus-Checksum-Algorithm", "sha1")
|> put_resp_header("Tus-Max-Size", to_string(ExTus.Config.tus_max_file_size))
|> put_resp_header("Tus-Extension", Enum.join(ExTus.Config.extensions, ","))
|> resp(200, "")
end
def head(conn, identifier)do
upload_info = UploadCache.get(identifier)
if is_nil(upload_info) do
conn
|> Utils.set_base_resp
|> resp(404, "")
else
upload_meta =
%{
size: upload_info.size,
filename: upload_info.filename
}
upload_meta =
upload_meta
|> Enum.map(fn{k, v} -> "#{k} #{Base.encode64(to_string(v))}" end)
|> Enum.join(",")
if not upload_meta in ["", nil] do
put_resp_header(conn, "Upload-Metadata", upload_meta)
else
conn
end
|> Utils.set_base_resp
|> Utils.put_cors_headers
|> put_resp_header("Upload-Offset", "#{upload_info.offset}")
|> put_resp_header("Upload-Length", "#{upload_info.size}")
|> resp(200, "")
end
end
def patch(conn, identifier, complete_cb)do
headers = Utils.read_headers(conn)
{offset, _} = Integer.parse(headers["upload-offset"])
upload_info = UploadCache.get(identifier)
[alg, checksum] = String.split(headers["upload-checksum"] || "_ _")
if not is_nil(headers["upload-checksum"]) and not alg in ExTus.Config.hash_algorithms do
conn
|> Utils.set_base_resp
|> resp(400, "Bad Request")
else
#%{size: current_offset} = File.stat!(file_path)
if offset != upload_info.offset do
conn
|> Utils.set_base_resp
|> resp(409, "Conflict")
else
#read data Max chunk size is 8MB, if transferred data > 8MB, ignore it
case read_body(conn) do
{_, binary, conn} ->
data_length = byte_size(binary)
upload_info = Map.put(upload_info, :offset, data_length + upload_info.offset)
# check Checksum if received a checksum digest
if alg in ExTus.Config.hash_algorithms do
alg = if alg == "sha1", do: "sha", else: alg
hash_val = :crypto.hash(String.to_atom(alg), binary)
|> Base.encode32()
if checksum != hash_val do
conn
|> Utils.set_base_resp
|> resp(460, "Checksum Mismatch")
else
write_append_data(conn, upload_info, binary, complete_cb)
end
else
write_append_data(conn, upload_info, binary, complete_cb)
end
{:error, _term} ->
conn
|> Utils.set_base_resp
|> resp(500, "Server error")
end
end
end
end
defp write_append_data(conn, upload_info, binary, complete_cb \\ nil) do
storage.append_data(upload_info, binary)
|> case do
{:ok, upload_info} ->
UploadCache.update(upload_info)
if upload_info.offset >= upload_info.size do
rs = storage.complete_file(upload_info)
# if upload fail remove uploaded file
with {:error, err} <- rs do
Logger.warn inspect err
storage.abort_upload(upload_info)
end
# remove cache info
UploadCache.delete(upload_info.identifier)
if not is_nil(complete_cb) do
complete_cb.(upload_info)
end
end
conn
|> Utils.set_base_resp
|> Utils.put_cors_headers
|> put_resp_header("Upload-Offset", "#{upload_info.offset}")
|> resp(204, "")
{:error, err} ->
Logger.warn inspect err
conn
|> Utils.set_base_resp
|> resp(404, "")
end
end
def post(conn, create_cb)do
headers = Utils.read_headers(conn)
meta = Utils.parse_meta_data(headers["upload-metadata"])
{upload_length, _} = Integer.parse(headers["upload-length"])
if upload_length > ExTus.Config.tus_max_file_size do
conn
|> resp(413, "")
else
file_name =
(meta["filename"] || "")
|> Base.decode64!
{:ok, {identifier, filename}} = storage.initiate_file(file_name)
info = %UploadInfo{
identifier: identifier,
filename: filename,
size: upload_length,
started_at: DateTime.utc_now |> DateTime.to_iso8601
}
UploadCache.put(info)
if create_cb do
create_cb.(info)
end
location = ("#{conn.scheme}://#{conn.host }:#{conn.port}")
|> URI.merge(Path.join(ExTus.Config.upload_url, identifier))
|> to_string
conn
|> put_resp_header("Tus-Resumable", ExTus.Config.tus_api_version)
|> put_resp_header("Location", location)
|> Utils.put_cors_headers
|> resp(201, "")
end
end
def delete(conn, identifier) do
upload_info = UploadCache.get(identifier)
if is_nil(upload_info) do
conn
|> Utils.set_base_resp
|> resp(404, "Not Found")
else
case storage.delete(upload_info) do
:ok ->
conn
|> Utils.set_base_resp
|> Utils.put_cors_headers
|> resp(204, "No Content")
_ ->
conn
|> Utils.set_base_resp
|> resp(500, "Server Error")
end
end
end
def handle_preflight_request(conn) do
headers = Enum.into(conn.req_headers, Map.new)
if headers["access-control-request-method"] do
conn
|> put_resp_header("Access-Control-Allow-Methods", "POST, GET, HEAD, PATCH, DELETE, OPTIONS")
|> put_resp_header("Access-Control-Allow-Origin", "null")
|> put_resp_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata")
|> put_resp_header("Access-Control-Max-Age", "86400")
else
conn
end
end
end