Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow options like "timeout" to be passed to HTTPoison.get_blob underneath inside options #54

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions lib/azurex/blob.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ defmodule Azurex.Blob do
argument of `nil` will result in the blob being assigned the default content
type `"application/octet-stream"`.

## The `options` Argument

The last argument of this function is a keyword list that can contain the following options:
* `params`: a list of additional query parameters to include in the request
* `headers`: a list of additional headers to include in the request
* any other options that can be passed to `HTTPoison.request/1`

## Examples

iex> put_blob("filename.txt", "file contents", "text/plain")
Expand All @@ -55,7 +62,10 @@ defmodule Azurex.Blob do
iex> put_blob("filename.txt", "file contents", "text/plain", "container")
:ok

iex> put_blob("filename.txt", "file contents", "text/plain", nil, timeout: 10)
iex> put_blob("filename.txt", "file contents", "text/plain", nil, params: [timeout: 10])
:ok

iex> put_blob("filename.txt", "file contents", "text/plain", nil, params: [timeout: 10], headers: [{"x-ms-meta-foo", "bar"}])
:ok

iex> put_blob("filename.txt", "file contents", "text/plain")
Expand All @@ -71,40 +81,43 @@ defmodule Azurex.Blob do
) ::
:ok
| {:error, HTTPoison.AsyncResponse.t() | HTTPoison.Error.t() | HTTPoison.Response.t()}
def put_blob(name, blob, content_type, container \\ nil, params \\ [])
def put_blob(name, blob, content_type, container \\ nil, options \\ [])

def put_blob(name, {:stream, bitstream}, content_type, container, params) do
def put_blob(name, {:stream, bitstream}, content_type, container, options) do
content_type = content_type || "application/octet-stream"

bitstream
|> Stream.transform(
fn -> [] end,
fn chunk, acc ->
with {:ok, block_id} <- Block.put_block(container, chunk, name, params) do
with {:ok, block_id} <- Block.put_block(container, chunk, name, options) do
{[], [block_id | acc]}
end
end,
fn acc ->
Block.put_block_list(acc, container, name, content_type, params)
Block.put_block_list(acc, container, name, content_type, options)
end
)
|> Stream.run()
end

def put_blob(name, blob, content_type, container, params) do
def put_blob(name, blob, content_type, container, options) do
content_type = content_type || "application/octet-stream"
{params, options} = Keyword.pop(options, :params, [])
{headers, options} = Keyword.pop(options, :headers, [])
headers = Enum.map(headers, fn {k, v} -> {to_string(k), v} end)

%HTTPoison.Request{
method: :put,
url: get_url(container, name),
params: params,
body: blob,
headers: [
{"x-ms-blob-type", "BlockBlob"}
{"x-ms-blob-type", "BlockBlob"} | headers
],
# Blob storage only answers when the whole file has been uploaded, so recv_timeout
# is not applicable for the put request, so we set it to infinity
options: [recv_timeout: :infinity]
options: [recv_timeout: :infinity] ++ options
}
|> SharedKey.sign(
storage_account_name: Config.storage_account_name(),
Expand Down Expand Up @@ -133,15 +146,18 @@ defmodule Azurex.Blob do
iex> get_blob("filename.txt", nil, timeout: 10)
{:ok, "file contents"}

iex> get_blob("filename.txt", nil, timeout: 10, headers: [{"x-ms-range", "bytes=0-10"}])
{:ok, "file contents"}

iex> get_blob("filename.txt")
{:error, %HTTPoison.Response{}}

"""
@spec get_blob(String.t(), optional_string) ::
{:ok, binary()}
| {:error, HTTPoison.AsyncResponse.t() | HTTPoison.Error.t() | HTTPoison.Response.t()}
def get_blob(name, container \\ nil, params \\ []) do
blob_request(name, container, :get, params)
def get_blob(name, container \\ nil, options \\ []) do
blob_request(name, container, :get, options)
|> HTTPoison.request()
|> case do
{:ok, %{body: blob, status_code: 200}} -> {:ok, blob}
Expand Down Expand Up @@ -211,7 +227,10 @@ defmodule Azurex.Blob do
end
end

defp blob_request(name, container, method, params, headers \\ [], options \\ []) do
def blob_request(name, container, method, options) do
{params, options} = Keyword.pop(options, :params, [])
{headers, options} = Keyword.pop(options, :headers, [])

%HTTPoison.Request{
method: method,
url: get_url(container, name),
Expand Down
20 changes: 14 additions & 6 deletions lib/azurex/blob/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ defmodule Azurex.Blob.Block do
"""
@spec put_block(String.t(), bitstring(), String.t(), list()) ::
{:ok, String.t()} | {:error, term()}
def put_block(container, chunk, name, params) do
def put_block(container, chunk, name, options) do
block_id = build_block_id()
content_type = "application/octet-stream"
{params, options} = Keyword.pop(options, :params, [])
{headers, options} = Keyword.pop(options, :headers, [])
headers = Enum.map(headers, fn {k, v} -> {to_string(k), v} end)
params = [{:comp, "block"}, {:blockid, block_id} | params]

%HTTPoison.Request{
Expand All @@ -31,8 +34,9 @@ defmodule Azurex.Blob.Block do
body: chunk,
headers: [
{"content-type", content_type},
{"content-length", byte_size(chunk)}
]
{"content-length", byte_size(chunk)} | headers
],
options: options
}
|> SharedKey.sign(
storage_account_name: Config.storage_account_name(),
Expand All @@ -54,7 +58,10 @@ defmodule Azurex.Blob.Block do
"""
@spec put_block_list(list(), String.t(), String.t(), String.t() | nil, list()) ::
:ok | {:error, term()}
def put_block_list(block_ids, container, name, blob_content_type, params) do
def put_block_list(block_ids, container, name, blob_content_type, options) do
{params, options} = Keyword.pop(options, :params, [])
{headers, options} = Keyword.pop(options, :headers, [])
headers = Enum.map(headers, fn {k, v} -> {to_string(k), v} end)
params = [{:comp, "blocklist"} | params]
content_type = "text/plain; charset=UTF-8"
blob_content_type = blob_content_type || "application/octet-stream"
Expand All @@ -79,8 +86,9 @@ defmodule Azurex.Blob.Block do
body: body,
headers: [
{"content-type", content_type},
{"x-ms-blob-content-type", blob_content_type}
]
{"x-ms-blob-content-type", blob_content_type} | headers
],
options: options
}
|> SharedKey.sign(
storage_account_name: Config.storage_account_name(),
Expand Down
29 changes: 29 additions & 0 deletions test/azurex_test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,33 @@
defmodule AzurexTest do
use ExUnit.Case
doctest Azurex

test "ensure HTTPoison Request has correct options, headers and params" do
Application.put_env(:azurex, Azurex.Blob.Config,
storage_account_name: "dummystorageaccount",
storage_account_key:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
)

request =
Azurex.Blob.blob_request("name", "container", :get,
recv_timeout: 60_000,
timeout: 60_000,
params: [param1: "value1"],
headers: [{"Content-Type", "application/json"}]
)

assert %HTTPoison.Request{
method: :get,
url: _,
params: [param1: "value1"],
headers: [
{"Authorization", _},
{"x-ms-version", _},
{"x-ms-date", _},
{"Content-Type", "application/json"}
],
options: [recv_timeout: 60_000, timeout: 60_000]
} = request
end
end
24 changes: 21 additions & 3 deletions test/integration/blob_integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,18 @@ defmodule Azurex.BlobIntegrationTests do
) == {:ok, @sample_file_contents}
end

test "passing container and params" do
test "passing container and options with params and headers" do
blob_name = make_blob_name()

assert Blob.put_blob(
blob_name,
@sample_file_contents,
"text/plain",
@integration_testing_container,
timeout: 10,
ignored_param: "ignored_param_value"
options: [
params: [timeout: 10, ignored_param: "ignored_param_value"],
headers: [ignored_header: "ignored_header_value"]
]
) == :ok

assert Blob.get_blob(
Expand Down Expand Up @@ -122,6 +124,22 @@ defmodule Azurex.BlobIntegrationTests do
assert headers["content-md5"] ==
:crypto.hash(:md5, @sample_file_contents) |> Base.encode64()
end

test "passing custom headers" do
blob_name = make_blob_name()

assert Blob.put_blob(
blob_name,
@sample_file_contents,
"text/plain",
nil,
headers: ["x-ms-meta-foo": "bar"]
) == :ok

assert {:ok, headers} = Blob.head_blob(blob_name)
headers = Map.new(headers)
assert headers["x-ms-meta-foo"] == "bar"
end
end

describe "copying a blob" do
Expand Down