diff --git a/lib/azurex/blob.ex b/lib/azurex/blob.ex index a189f7c..48a5dd6 100644 --- a/lib/azurex/blob.ex +++ b/lib/azurex/blob.ex @@ -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") @@ -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") @@ -71,28 +81,31 @@ 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, @@ -100,11 +113,11 @@ defmodule Azurex.Blob do 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(), diff --git a/lib/azurex/blob/block.ex b/lib/azurex/blob/block.ex index a89811e..e8f6177 100644 --- a/lib/azurex/blob/block.ex +++ b/lib/azurex/blob/block.ex @@ -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{ @@ -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(), @@ -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" @@ -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(), diff --git a/test/integration/blob_integration_test.exs b/test/integration/blob_integration_test.exs index 06377e7..98b0beb 100644 --- a/test/integration/blob_integration_test.exs +++ b/test/integration/blob_integration_test.exs @@ -66,7 +66,7 @@ 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( @@ -74,8 +74,10 @@ defmodule Azurex.BlobIntegrationTests do @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( @@ -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