Packages

A Plug for using HTTP Byte Serving in Plug applications.

Current section

Files

Jump to
plug_byte_serve lib plug_byte_serve.ex
Raw

lib/plug_byte_serve.ex

defmodule PlugByteServe do
@moduledoc """
A plug for streaming files with HTTP Byte Serving.
## Example
defmodule GetServedApp do
import Plug.Conn
use Plug.Router
plug PlugByteServe, path: "/tmp", file: "/tmp/file"
plug :match
plug :dispatch
get "/bytes" do
conn
|> send_resp()
end
end
# defmodule GetServedApp do
# import Plug.Conn
# use Plug.Router
#
# plug PlugByteServe
# plug :match
# plug :dispatch
#
# get "/bytes" do
# dynamic_file_path = "/tmp/" <> "file"
#
# conn
# |> PlugByteServe.call({file: dynamic_file_path})
# |> send_resp()
# end
# end
"""
alias Plug.Conn
import Plug.Conn, only: [get_req_header: 2,
put_resp_header: 3,
resp: 3,
send_resp: 3,
send_file: 5]
def init(opts \\ []) do
path = Keyword.get(opts, :path)
file = Keyword.get(opts, :file)
limit = Keyword.get(opts, :limit)
[path: path, file: file, limit: limit]
end
def call(%Conn{method: "HEAD"} = conn, opts) do
[path: _path, file: file, limit: _limit] = get_opts(opts)
{:ok, file_info} = get_file_info(opts)
content_type = Plug.MIME.path(file)
conn
|> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("content-type", content_type)
|> put_resp_header("content-length", "#{file_info.size}")
|> resp(200, "")
end
def call(%Conn{method: "GET"} = conn, opts) do
conn
|> find_range(opts)
|> send_f(opts)
end
def call(conn, _type, _subtype, _headers, _opts) do
{:next, conn}
end
defp get_opts(opts) do
path = Keyword.get(opts, :path, System.cwd)
file = Keyword.fetch!(opts, :file)
limit = Keyword.get(opts, :limit)
[path: path, file: file, limit: limit]
end
defp get_file(opts) do
[path: path, file: file, limit: _] = get_opts(opts)
path <> "/" <> file
end
defp get_file_info(opts) do
opts
|> get_opts
|> get_file
|> File.stat
end
defp find_range(conn, opts) do
[path: _, file: _, limit: limit] = get_opts(opts)
{:ok, file_info} = get_file_info(opts)
hdr_range =
case hdr_range = get_req_header(conn, "range") do
[] -> []
_ -> String.split(List.last(hdr_range), ["=", "-"])
end
[_range_type, range_start, range_end] =
case hdr_range do
["bytes", range_start, ""] -> ["bytes", range_start, "#{file_info.size - 1}"]
["bytes", "", range_end] -> ["bytes", "0", range_end]
["bytes", range_start, range_end] -> ["bytes", range_start, range_end]
_ -> ["bytes", "0", "#{file_info.size - 1}"]
end
[range_start, range_end] = Enum.map([range_start, range_end], fn(x) -> String.to_integer(x) end)
# Check range is within file
{status, r_start, r_end} =
cond do
range_start > range_end -> {416, 0, file_info.size - 1}
range_start >= file_info.size -> {416, 0, file_info.size - 1}
range_start < 0 and range_end >= file_info.size -> {416, 0, file_info.size - 1}
range_start >= 0 and range_end < file_info.size -> {206, range_start, range_end}
range_start >= 0 and range_end >= file_info.size -> {206, range_start, file_info.size - 1}
range_start < 0 and range_end < file_info.size -> {206, 0, range_end}
end
limit =
case limit do
nil -> file_info.size
_ -> limit
end
r_limit = r_end - r_start + 1
r_limit =
cond do
r_limit > limit -> limit
true -> r_limit
end
{conn, status, r_start, r_end, r_limit}
end
defp send_f({conn, status, range_start, range_end, range_limit}, opts) do
file = get_file(opts)
{:ok, file_info} = get_file_info(opts)
content_type = Plug.MIME.path(file)
conn
|> put_resp_header("accept-ranges", "bytes")
|> put_resp_header("content-type", content_type)
|> put_resp_header("content-length", "#{range_limit}")
|> put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{file_info.size}")
|> send_status(status, file, range_start, range_limit)
end
defp send_status(conn, 206, file, range_start, range_limit) do
send_file(conn, 206, file, range_start, range_limit)
end
defp send_status(conn, 416, _file, _range_start, _range_limit) do
resp(conn, 416, "")
end
end