Current section

Files

Jump to
resx_dropbox lib resx_dropbox.ex
Raw

lib/resx_dropbox.ex

defmodule ResxDropbox do
@moduledoc """
A producer to handle dropbox URIs.
ResxDropbox.open("dbpath:/path/to/file.txt")
ResxDropbox.open("dbid:AAAAAAAAAAAAAAAAAAAAAA")
Add `ResxDropbox` to your list of resx producers.
config :resx,
producers: [ResxDropbox]
### Types
MIME types are inferred from file extension names. Following the behaviour
of `Resx.Producers.File.mime/1`.
### Authorities
Authorities are used to match with a dropbox access token. When no authority
is provided, it will attempt to find an access token for `nil`. These tokens
can be configured by setting the `:token` configuration option for `:resx_dropbox`.
config :resx_dropbox,
token: "TOKEN"
config :resx_dropbox,
token: %{ nil => "TOKEN1", "foo@bar" => "TOKEN2", "main" => "TOKEN3" }
config :resx_dropbox,
token: { MyDropboxTokenRetriever, :to_token, 1 }
The `:token` field should contain either a string which will be the token
used by any authority, or a map of authority keys and token string values,
or a callback function that will be passed the authority and should return
`{ :ok, token }` or `:error` if there is no token for the given authority.
Valid function formats are any callback variant, see `Callback` for more
information.
"""
use Resx.Producer
require Callback
alias Resx.Resource
alias Resx.Resource.Content
alias Resx.Resource.Reference
alias Resx.Resource.Reference.Integrity
defp get_token(name) do
case Application.get_env(:resx_dropbox, :token, %{}) do
to_token when Callback.is_callback(to_token) -> Callback.call(to_token, [name])
tokens when is_map(tokens) -> Map.fetch(tokens, name)
token -> { :ok, token }
end
end
defp to_path(%Reference{ repository: repo }), do: { :ok, repo }
defp to_path(%URI{ scheme: "dbpath", path: nil, authority: authority }), do: { :ok, { authority, { :path, "" } } }
defp to_path(%URI{ scheme: "dbpath", path: "/", authority: authority }), do: { :ok, { authority, { :path, "" } } }
defp to_path(%URI{ scheme: "dbpath", path: path, authority: authority }), do: { :ok, { authority, { :path, path } } }
defp to_path(%URI{ scheme: "dbid", path: "/" }), do: { :error, { :invalid_reference, "no ID" } }
defp to_path(%URI{ scheme: "dbid", path: "/" <> path, authority: authority }), do: { :ok, { authority, { :id, "id:" <> path } } }
defp to_path(%URI{ scheme: "dbid", path: path, authority: authority }) when not is_nil(path), do: { :ok, { authority, { :id, "id:" <> path } } }
defp to_path(uri) when is_binary(uri), do: URI.decode(uri) |> URI.parse |> to_path
defp to_path(_), do: { :error, { :invalid_reference, "not a dropbox reference" } }
defp format_api_error(%{ "error" => %{ ".tag" => "path", "path" => %{ ".tag" => "malformed_path" } } }, _), do: { :error, { :invalid_reference, "invalid path format" } }
defp format_api_error(%{ "error" => %{ ".tag" => "path", "path" => %{ ".tag" => "restricted_content" } } }, _), do: { :error, { :invalid_reference, "content is restricted" } }
defp format_api_error(%{ "error" => %{ ".tag" => "path", "path" => %{ ".tag" => error } } }, path) when error in ["not_found", "not_file", "not_folder"], do: { :error, { :unknown_resource, path } }
defp format_api_error(%{ "error_summary" => summary }, _), do: { :error, { :internal, summary } }
defp format_http_error({ :ok, response = %{ status_code: 400 } }, _, _), do: { :error, { :internal, response.body } }
defp format_http_error({ :ok, response }, path, _) do
case Poison.decode(response.body) do
{ :ok, error } -> format_api_error(error, path)
_ -> { :error, { :internal, response.body } }
end
end
defp format_http_error({ :error, error }, _, action), do: { :error, { :internal, "failed to #{action} due to: #{HTTPoison.Error.message(error)}" } }
defp header(token), do: [{"Authorization", "Bearer #{token}"}]
defp get_header([{ key, value }|_], key), do: value
defp get_header([_|headers], key), do: get_header(headers, key)
defp get_header(_, _), do: nil
defp api_result(response), do: get_header(response.headers, "dropbox-api-result")
defp format_timestamp(timestamp) do
{ :ok, timestamp, _ } = DateTime.from_iso8601(timestamp)
DateTime.to_unix(timestamp)
end
defp timestamp(data, :server), do: data["server_modified"] |> format_timestamp
defp timestamp(data, :client), do: data["client_modified"] |> format_timestamp
defp timestamp(data, nil), do: timestamp(data, Application.get_env(:resx_dropbox, :timestamp, :server))
defp get_metadata(path, token), do: HTTPoison.post("https://api.dropboxapi.com/2/files/get_metadata", Poison.encode!(%{ path: path }), [{"Content-Type", "application/json"}|header(token)])
defp download(path, token), do: HTTPoison.post("https://content.dropboxapi.com/2/files/download", "", [{"Dropbox-API-Arg", Poison.encode!(%{ path: path })}|header(token)])
@impl Resx.Producer
def schemes(), do: ["dbpath", "dbid"]
@doc """
Opens a dropbox resource.
The `:timestamp` allows for choosing between `:server` or `:client` timestamps.
By default the server timestamp is used, or whatever application timestamp
setting was given.
config :resx_dropbox,
timestamp: :client
"""
@impl Resx.Producer
def open(reference, opts \\ []) do
with { :path, { :ok, repo = { name, { _, path } } } } <- { :path, to_path(reference) },
{ :token, { :ok, token }, _ } <- { :token, get_token(name), name },
{ :content, { :ok, response = %HTTPoison.Response{ status_code: 200 } }, _ } <- { :content, download(path, token), path },
{ :data, { :ok, data } } <- { :data, api_result(response) |> Poison.decode } do
content = %Content{
type: Resx.Producers.File.mime(data["name"]),
data: response.body
}
resource = %Resource{
reference: %Reference{
adapter: __MODULE__,
repository: repo,
integrity: %Integrity{
timestamp: timestamp(data, opts[:timestamp]),
checksum: { :dropbox, data["content_hash"] }
}
},
content: content
}
{ :ok, resource }
else
{ :path, error } -> error
{ :token, _, name } -> { :error, { :invalid_reference, "no token for authority (#{inspect name})" } }
{ :content, error, path } -> format_http_error(error, path, "retrieve content")
{ :data, _ } -> { :error, { :internal, "unable to process api result" } }
end
end
@impl Resx.Producer
def exists?(reference) do
with { :path, { :ok, { name, { _, path } } } } <- { :path, to_path(reference) },
{ :token, { :ok, token }, _ } <- { :token, get_token(name), name },
{ :metadata, { :ok, %HTTPoison.Response{ status_code: 200 } }, _ } <- { :metadata, get_metadata(path, token), path } do
{ :ok, true }
else
{ :path, error } -> error
{ :token, _, name } -> { :error, { :invalid_reference, "no token for authority (#{inspect name})" } }
{ :metadata, error, path } ->
case format_http_error(error, path, "retrieve metadata") do
{ :error, { :unknown_resource, _ } } -> { :ok, false }
error -> error
end
end
end
@impl Resx.Producer
def alike?(a, b) do
with { :a, { :ok, repo_a } } <- { :a, to_path(a) },
{ :b, { :ok, repo_b } } <- { :b, to_path(b) } do
case { repo_a, repo_b } do
{ repo, repo } -> true
{ { _, { :id, id } }, { _, { :id, id } } } -> true
{ { name_a, path_a }, { name_b, path_b } } ->
with { :token_a, { :ok, token_a }, _ } <- { :token_a, get_token(name_a), name_a },
{ :token_b, { :ok, token_b }, _ } <- { :token_b, get_token(name_b), name_b } do
a = case path_a do
^path_b when token_a == token_b -> true
{ :id, id } -> id
{ :path, path } ->
with { :metadata, { :ok, response = %HTTPoison.Response{ status_code: 200 } }, _ } <- { :metadata, get_metadata(path, token_a), path },
{ :data, { :ok, data } } <- { :data, response.body |> Poison.decode } do
data["id"]
else
{ :metadata, error, path } -> format_http_error(error, path, "retrieve metadata")
{ :data, _ } -> { :error, { :internal, "unable to process api result" } }
end
end
case { path_b, a } do
{ _, true } -> true
{ { :id, id }, id } -> true
{ { :id, _ }, _ } -> false
{ { :path, path }, id } ->
with { :metadata, { :ok, response = %HTTPoison.Response{ status_code: 200 } }, _ } <- { :metadata, get_metadata(path, token_b), path },
{ :data, { :ok, data } } <- { :data, response.body |> Poison.decode } do
data["id"] == id
else
{ :metadata, error, path } -> format_http_error(error, path, "retrieve metadata")
{ :data, _ } -> { :error, { :internal, "unable to process api result" } }
end
end
else
{ _, _, name } -> { :error, { :invalid_reference, "no token for authority (#{inspect name})" } }
end
end
else
_ -> false
end
end
@impl Resx.Producer
def resource_uri(reference) do
case to_path(reference) do
{ :ok, { nil, { :id, id } } } -> { :ok, URI.encode("db" <> id) }
{ :ok, { authority, { :id, "id:" <> id } } } -> { :ok, URI.encode("dbid://" <> authority <> "/" <> id) }
{ :ok, { nil, { :path, path } } } -> { :ok, URI.encode("dbpath:" <> path) }
{ :ok, { authority, { :path, path } } } -> { :ok, URI.encode("dbpath://" <> authority <> "/" <> path) }
error -> error
end
end
@impl Resx.Producer
def resource_attributes(reference) do
with { :path, { :ok, { name, { _, path } } } } <- { :path, to_path(reference) },
{ :token, { :ok, token }, _ } <- { :token, get_token(name), name },
{ :metadata, { :ok, metadata = %HTTPoison.Response{ status_code: 200 } }, _ } <- { :metadata, HTTPoison.post("https://api.dropboxapi.com/2/files/get_metadata", Poison.encode!(%{ path: path }), [{"Content-Type", "application/json"}|header(token)]), path },
{ :data, { :ok, data } } <- { :data, metadata.body |> Poison.decode } do
{ :ok, data }
else
{ :path, error } -> error
{ :token, _, name } -> { :error, { :invalid_reference, "no token for authority (#{inspect name})" } }
{ :metadata, error, path } -> format_http_error(error, path, "retrieve metadata")
{ :data, _ } -> { :error, { :internal, "unable to process api result" } }
end
end
end