diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b78500..23be601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added/Changed +- Added support for multiple Azure instances + ### Fixed - Don't crash on connection strings with trailing semicolon diff --git a/README.md b/README.md index f29ecbb..9cd2cc6 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,21 @@ config :azurex, Azurex.Blob.Config, storage_account_connection_string: "Storage=Account;Connection=String" # Required if storage account `name` and `key` not set ``` +Each of these options is then overridable per-request, if you need to work with multiple instances: + +```elixir +Azurex.Blob.list_blobs(container: "other", api_uri: "https://other.blob.net") + +Azurex.Blob.get_blob("file.txt", [ + storage_account_connection_string: "Account=Storage;String=Connection" +]) + +Azurex.Blob.put_blob("file.txt", "contents", "text/plain", [ + storage_account_key: "key", + storage_account_name: "name" +]) +``` + ## Documentation Documentation can be found at [https://hexdocs.pm/azurex](https://hexdocs.pm/azurex). Or generated using [ExDoc](https://github.com/elixir-lang/ex_doc) diff --git a/lib/azurex/blob.ex b/lib/azurex/blob.ex index 3cf1bd9..7512620 100644 --- a/lib/azurex/blob.ex +++ b/lib/azurex/blob.ex @@ -10,14 +10,17 @@ defmodule Azurex.Blob do @typep optional_string :: String.t() | nil - def list_containers do + @spec list_containers(Config.config_overrides()) :: + {:ok, String.t()} + | {:error, HTTPoison.AsyncResponse.t() | HTTPoison.Error.t() | HTTPoison.Response.t()} + def list_containers(overrides \\ []) do %HTTPoison.Request{ - url: Config.api_url() <> "/", + url: Config.api_url(overrides) <> "/", params: [comp: "list"] } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key() + storage_account_name: Config.storage_account_name(overrides), + storage_account_key: Config.storage_account_key(overrides) ) |> HTTPoison.request() |> case do @@ -55,6 +58,15 @@ defmodule Azurex.Blob do iex> put_blob("filename.txt", "file contents", "text/plain", "container") :ok + iex> put_blob("filename.txt", "file contents", "text/plain", [container: "container"]) + :ok + + iex> put_blob("filename.txt", "file contents", "text/plain", [storage_account_name: "name", storage_account_key: "key"]) + :ok + + iex> put_blob("filename.txt", "file contents", "text/plain", [storage_account_connection_string: "AccountName=name;AccountKey=key", container: "container"]) + :ok + iex> put_blob("filename.txt", "file contents", "text/plain", nil, timeout: 10) :ok @@ -66,37 +78,38 @@ defmodule Azurex.Blob do String.t(), binary() | {:stream, Enumerable.t()}, optional_string, - optional_string, + Config.config_overrides(), keyword ) :: :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, overrides \\ [], params \\ []) - def put_blob(name, {:stream, bitstream}, content_type, container, params) do + def put_blob(name, {:stream, bitstream}, content_type, overrides, params) 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(overrides, chunk, name, params) do {[], [block_id | acc]} end end, fn acc -> - Block.put_block_list(acc, container, name, content_type, params) + Block.put_block_list(acc, overrides, name, content_type, params) end ) |> Stream.run() end - def put_blob(name, blob, content_type, container, params) do + def put_blob(name, blob, content_type, overrides, params) do content_type = content_type || "application/octet-stream" + connection_params = Config.get_connection_params(overrides) %HTTPoison.Request{ method: :put, - url: get_url(container, name), + url: get_url(name, connection_params), params: params, body: blob, headers: [ @@ -107,8 +120,8 @@ defmodule Azurex.Blob do options: [recv_timeout: :infinity] } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key(), + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params), content_type: content_type ) |> HTTPoison.request() @@ -130,6 +143,12 @@ defmodule Azurex.Blob do iex> get_blob("filename.txt", "container") {:ok, "file contents"} + iex> get_blob("filename.txt", [storage_account_name: "name", storage_account_key: "key", container: "container"]) + {:ok, "file contents"} + + iex> get_blob("filename.txt", [storage_account_connection_string: "AccountName=name;AccountKey=key"]) + {:ok, "file contents"} + iex> get_blob("filename.txt", nil, timeout: 10) {:ok, "file contents"} @@ -137,11 +156,11 @@ defmodule Azurex.Blob do {:error, %HTTPoison.Response{}} """ - @spec get_blob(String.t(), optional_string) :: + @spec get_blob(String.t(), Config.config_overrides(), keyword) :: {: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, overrides \\ [], params \\ []) do + blob_request(name, overrides, :get, params) |> HTTPoison.request() |> case do {:ok, %{body: blob, status_code: 200}} -> {:ok, blob} @@ -153,11 +172,11 @@ defmodule Azurex.Blob do @doc """ Checks if a blob exists, and returns metadata for the blob if it does """ - @spec head_blob(String.t(), optional_string) :: + @spec head_blob(String.t(), Config.config_overrides(), keyword) :: {:ok, list} | {:error, :not_found | HTTPoison.Error.t() | HTTPoison.Response.t()} - def head_blob(name, container \\ nil, params \\ []) do - blob_request(name, container, :head, params) + def head_blob(name, overrides \\ [], params \\ []) do + blob_request(name, overrides, :head, params) |> HTTPoison.request() |> case do {:ok, %HTTPoison.Response{status_code: 200, headers: details}} -> {:ok, details} @@ -170,24 +189,27 @@ defmodule Azurex.Blob do @doc """ Copies a blob to a destination. + The same configuration options (connection string, container, ...) are applied to both source and destination. + Note: Azure’s ‘[Copy Blob from URL](https://learn.microsoft.com/en-us/rest/api/storageservices/copy-blob-from-url)’ operation has a maximum size of 256 MiB. """ - @spec copy_blob(String.t(), String.t(), optional_string) :: + @spec copy_blob(String.t(), String.t(), Config.config_overrides()) :: {:ok, HTTPoison.Response.t()} | {:error, term()} - def copy_blob(source_name, destination_name, container \\ nil) do + def copy_blob(source_name, destination_name, overrides \\ []) do content_type = "application/octet-stream" - source_url = get_url(container, source_name) + connection_params = Config.get_connection_params(overrides) + source_url = get_url(source_name, connection_params) headers = [{"x-ms-copy-source", source_url}, {"content-type", content_type}] %HTTPoison.Request{ method: :put, - url: get_url(container, destination_name), + url: get_url(destination_name, connection_params), headers: headers } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key(), + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params), content_type: content_type ) |> HTTPoison.request() @@ -198,10 +220,10 @@ defmodule Azurex.Blob do end end - @spec delete_blob(String.t(), optional_string()) :: + @spec delete_blob(String.t(), Config.config_overrides(), keyword) :: :ok | {:error, :not_found | HTTPoison.Error.t() | HTTPoison.Response.t()} - def delete_blob(name, container \\ nil, params \\ []) do - blob_request(name, container, :delete, params) + def delete_blob(name, overrides \\ [], params \\ []) do + blob_request(name, overrides, :delete, params) |> HTTPoison.request() |> case do {:ok, %HTTPoison.Response{status_code: 202}} -> :ok @@ -211,17 +233,17 @@ defmodule Azurex.Blob do end end - defp blob_request(name, container, method, params, headers \\ [], options \\ []) do + defp blob_request(name, overrides, method, params) do + connection_params = Config.get_connection_params(overrides) + %HTTPoison.Request{ method: method, - url: get_url(container, name), - params: params, - headers: headers, - options: options + url: get_url(name, connection_params), + params: params } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key() + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params) ) end @@ -233,15 +255,20 @@ defmodule Azurex.Blob do iex> Azurex.Blob.list_blobs() {:ok, "\uFEFF Azurex.Blob.list_blobs(storage_account_name: "name", storage_account_key: "key", container: "container") + {:ok, "\uFEFF Azurex.Blob.list_blobs() {:error, %HTTPoison.Response{}} """ - @spec list_blobs(optional_string) :: + @spec list_blobs(Config.config_overrides()) :: {:ok, binary()} | {:error, HTTPoison.AsyncResponse.t() | HTTPoison.Error.t() | HTTPoison.Response.t()} - def list_blobs(container \\ nil, params \\ []) do + def list_blobs(overrides \\ [], params \\ []) do + connection_params = Config.get_connection_params(overrides) + %HTTPoison.Request{ - url: "#{Config.api_url()}/#{get_container(container)}", + url: get_url(connection_params), params: [ comp: "list", @@ -249,8 +276,8 @@ defmodule Azurex.Blob do ] ++ params } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key() + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params) ) |> HTTPoison.request() |> case do @@ -263,20 +290,20 @@ defmodule Azurex.Blob do @doc """ Returns the url for a container (defaults to the one in `Azurex.Blob.Config`) """ - @spec get_url(optional_string) :: String.t() - def get_url(container) do - "#{Config.api_url()}/#{get_container(container)}" + @spec get_url(keyword) :: String.t() + def get_url(connection_params) do + "#{Config.api_url(connection_params)}/#{get_container(connection_params)}" end @doc """ Returns the url for a file in a container (defaults to the one in `Azurex.Blob.Config`) """ - @spec get_url(optional_string, String.t()) :: String.t() - def get_url(container, blob_name) do - "#{get_url(container)}/#{blob_name}" + @spec get_url(String.t(), keyword) :: String.t() + def get_url(blob_name, connection_params) do + "#{get_url(connection_params)}/#{blob_name}" end - defp get_container(container) do - container || Config.default_container() + defp get_container(connection_params) do + Keyword.get(connection_params, :container) || Config.default_container() end end diff --git a/lib/azurex/blob/block.ex b/lib/azurex/blob/block.ex index a89811e..53e3a49 100644 --- a/lib/azurex/blob/block.ex +++ b/lib/azurex/blob/block.ex @@ -17,16 +17,17 @@ defmodule Azurex.Blob.Block do On success, returns an :ok tuple with the base64 encoded block_id. """ - @spec put_block(String.t(), bitstring(), String.t(), list()) :: + @spec put_block(Config.config_overrides(), bitstring(), String.t(), list()) :: {:ok, String.t()} | {:error, term()} - def put_block(container, chunk, name, params) do + def put_block(overrides \\ [], chunk, name, params) do block_id = build_block_id() content_type = "application/octet-stream" params = [{:comp, "block"}, {:blockid, block_id} | params] + connection_params = Config.get_connection_params(overrides) %HTTPoison.Request{ method: :put, - url: Blob.get_url(container, name), + url: Blob.get_url(name, connection_params), params: params, body: chunk, headers: [ @@ -35,8 +36,8 @@ defmodule Azurex.Blob.Block do ] } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key(), + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params), content_type: content_type ) |> HTTPoison.request() @@ -52,12 +53,13 @@ defmodule Azurex.Blob.Block do Block IDs should be base64 encoded, as returned by put_block/2. """ - @spec put_block_list(list(), String.t(), String.t(), String.t() | nil, list()) :: + @spec put_block_list(list(), Config.config_overrides(), 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, overrides \\ [], name, blob_content_type, params) do params = [{:comp, "blocklist"} | params] content_type = "text/plain; charset=UTF-8" blob_content_type = blob_content_type || "application/octet-stream" + connection_params = Config.get_connection_params(overrides) blocks = block_ids @@ -74,7 +76,7 @@ defmodule Azurex.Blob.Block do %HTTPoison.Request{ method: :put, - url: Blob.get_url(container, name), + url: Blob.get_url(name, connection_params), params: params, body: body, headers: [ @@ -83,8 +85,8 @@ defmodule Azurex.Blob.Block do ] } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key(), + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params), content_type: content_type ) |> HTTPoison.request() diff --git a/lib/azurex/blob/config.ex b/lib/azurex/blob/config.ex index ec7b298..b38fe73 100644 --- a/lib/azurex/blob/config.ex +++ b/lib/azurex/blob/config.ex @@ -3,6 +3,8 @@ defmodule Azurex.Blob.Config do Azurex Blob Config """ + @type config_overrides :: String.t() | keyword + @missing_envs_error_msg """ Azurex.Blob.Config: `storage_account_name` and `storage_account_key` or `storage_account_connection_string` required. @@ -14,13 +16,13 @@ defmodule Azurex.Blob.Config do Azure endpoint url, optional Defaults to `https://{name}.blob.core.windows.net` where `name` is the `storage_account_name` """ - @spec api_url :: String.t() - def api_url do - cond do - api_url = Keyword.get(conf(), :api_url) -> api_url - api_url = get_connection_string_value("BlobEndpoint") -> api_url - true -> "https://#{storage_account_name()}.blob.core.windows.net" - end + @spec api_url(keyword) :: String.t() + def api_url(connection_params \\ []) do + Keyword.get(connection_params, :api_url) || + get_connection_string_from_params("BlobEndpoint", connection_params) || + Keyword.get(conf(), :api_url) || + get_connection_string_value("BlobEndpoint") || + "https://#{storage_account_name(connection_params)}.blob.core.windows.net" end @doc """ @@ -36,35 +38,40 @@ defmodule Azurex.Blob.Config do Azure storage account name. Required if `storage_account_connection_string` not set. """ - @spec storage_account_name :: String.t() - def storage_account_name do - case Keyword.get(conf(), :storage_account_name) do - nil -> get_connection_string_value("AccountName") - storage_account_name -> storage_account_name - end || raise @missing_envs_error_msg + @spec storage_account_name(keyword) :: String.t() + def storage_account_name(connection_params \\ []) do + Keyword.get(connection_params, :storage_account_name) || + get_connection_string_from_params("AccountName", connection_params) || + Keyword.get(conf(), :storage_account_name) || + get_connection_string_value("AccountName") || + raise @missing_envs_error_msg end @doc """ Azure storage account access key. Base64 encoded, as provided by azure UI. Required if `storage_account_connection_string` not set. """ - @spec storage_account_key :: binary - def storage_account_key do - case Keyword.get(conf(), :storage_account_key) do - nil -> get_connection_string_value("AccountKey") - key -> key - end - |> Kernel.||(raise @missing_envs_error_msg) - |> Base.decode64!() + @spec storage_account_key(keyword) :: binary + def storage_account_key(connection_params \\ []) do + encoded_account_key = + Keyword.get(connection_params, :storage_account_key) || + get_connection_string_from_params("AccountKey", connection_params) || + Keyword.get(conf(), :storage_account_key) || + get_connection_string_value("AccountKey") || + raise @missing_envs_error_msg + + Base.decode64!(encoded_account_key) end @doc """ Azure storage account connection string. Required if `storage_account_name` or `storage_account_key` not set. """ - @spec storage_account_connection_string :: String.t() | nil - def storage_account_connection_string, - do: Keyword.get(conf(), :storage_account_connection_string) + @spec storage_account_connection_string(keyword) :: String.t() | nil + def storage_account_connection_string(connection_params \\ []) do + Keyword.get(connection_params, :storage_account_connection_string) || + Keyword.get(conf(), :storage_account_connection_string) + end @spec parse_connection_string(nil | binary) :: map @doc """ @@ -96,10 +103,27 @@ defmodule Azurex.Blob.Config do @doc """ Returns the value in the connection string given the string key. """ - @spec get_connection_string_value(String.t()) :: String.t() | nil - def get_connection_string_value(key) do - storage_account_connection_string() - |> parse_connection_string + @spec get_connection_string_value(String.t(), config_overrides) :: String.t() | nil + def get_connection_string_value(key, connection_params \\ []) do + storage_account_connection_string(connection_params) + |> parse_connection_string() + |> Map.get(key) + end + + @doc """ + Returns the given configuration keyword list. + If the parameter is a string, it is interpreted as the container for backwards compatibility. + """ + @spec get_connection_params(config_overrides | nil) :: keyword() + def get_connection_params(nil), do: [] + def get_connection_params(container) when is_binary(container), do: [container: container] + def get_connection_params(config), do: config + + @spec get_connection_string_from_params(String.t(), config_overrides) :: String.t() | nil + defp get_connection_string_from_params(key, connection_params) do + # Needed to prioritize keys from parameters + Keyword.get(connection_params, :storage_account_connection_string) + |> parse_connection_string() |> Map.get(key) end end diff --git a/lib/azurex/blob/container.ex b/lib/azurex/blob/container.ex index 0546be7..7198bd3 100644 --- a/lib/azurex/blob/container.ex +++ b/lib/azurex/blob/container.ex @@ -5,15 +5,17 @@ defmodule Azurex.Blob.Container do alias Azurex.Blob.Config alias Azurex.Authorization.SharedKey - def head_container(container) do + def head_container(container, overrides \\ []) do + connection_params = Config.get_connection_params(overrides) + %HTTPoison.Request{ - url: Config.api_url() <> "/" <> container, + url: Config.api_url(connection_params) <> "/" <> container, params: [restype: "container"], method: :head } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key() + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params) ) |> HTTPoison.request() |> case do @@ -24,15 +26,17 @@ defmodule Azurex.Blob.Container do end end - def create(container) do + def create(container, overrides \\ []) do + connection_params = Config.get_connection_params(overrides) + %HTTPoison.Request{ - url: Config.api_url() <> "/" <> container, + url: Config.api_url(connection_params) <> "/" <> container, params: [restype: "container"], method: :put } |> SharedKey.sign( - storage_account_name: Config.storage_account_name(), - storage_account_key: Config.storage_account_key(), + storage_account_name: Config.storage_account_name(connection_params), + storage_account_key: Config.storage_account_key(connection_params), content_type: "application/octet-stream" ) |> HTTPoison.request() diff --git a/lib/azurex/blob/shared_access_signature.ex b/lib/azurex/blob/shared_access_signature.ex index d815a57..9c0c57f 100644 --- a/lib/azurex/blob/shared_access_signature.ex +++ b/lib/azurex/blob/shared_access_signature.ex @@ -6,11 +6,13 @@ defmodule Azurex.Blob.SharedAccessSignature do https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas """ + alias Azurex.Blob.Config + @doc """ - Generates a SAS url on a resource in a given container. + Generates a SAS url on a resource. ## Params - - container: the storage container name + - overrides: use different configuration options for the azure connection. If the parameter is a string, it is treated as the container for backwards compatibility - resource: the path to the resource (blob, container, directory...) - opts: an optional keyword list with following options - resource_type: one of :blob / :blob_version / :blob_snapshot / :container / directory @@ -20,16 +22,22 @@ defmodule Azurex.Blob.SharedAccessSignature do - expiry: a tuple to set how long before the SAS url expires. Defaults to `{:second, 3600}`. ## Examples + - `SharedAccessSignature.sas_url("/")` + - `SharedAccessSignature.sas_url([], "/", permissions: [:read, :write])` - `SharedAccessSignature.sas_url("my_container", "/", permissions: [:write], expiry: {:day, 2})` - `SharedAccessSignature.sas_url("my_container", "foo/song.mp3", resource_type: :blob)` + - `SharedAccessSignature.sas_url([storage_account_connection_string: "AccountName=name;AccountKey=key", container: "my_container"], "/")` + - `SharedAccessSignature.sas_url([storage_account_name: "name", storage_account_key: "key"], "bar/image.jpg", resource_type: :blob)` """ - @spec sas_url(String.t(), String.t(), [{atom(), any()}]) :: String.t() - def sas_url(container, resource, opts \\ []) do - base_url = Azurex.Blob.Config.api_url() + @spec sas_url(Config.config_overrides(), String.t(), [{atom(), any()}]) :: String.t() + def sas_url(overrides \\ [], resource, opts \\ []) do + connection_params = Config.get_connection_params(overrides) + base_url = Config.api_url(connection_params) resource_type = Keyword.get(opts, :resource_type, :container) permissions = Keyword.get(opts, :permissions, [:read]) from = Keyword.get(opts, :from, DateTime.utc_now()) expiry = Keyword.get(opts, :expiry, {:second, 3600}) + container = Keyword.get(connection_params, :container) || Config.default_container() resource = Path.join(container, resource) token = @@ -38,8 +46,8 @@ defmodule Azurex.Blob.SharedAccessSignature do resource, {from, expiry}, permissions, - Azurex.Blob.Config.storage_account_name(), - Azurex.Blob.Config.storage_account_key() + Config.storage_account_name(connection_params), + Config.storage_account_key(connection_params) ) "#{Path.join(base_url, resource)}?#{token}" diff --git a/test/azurex/blob/config_test.exs b/test/azurex/blob/config_test.exs index 8cac19b..476ebdb 100644 --- a/test/azurex/blob/config_test.exs +++ b/test/azurex/blob/config_test.exs @@ -33,6 +33,15 @@ defmodule Azurex.Blob.ConfigTest do put_config() assert_raise RuntimeError, &storage_account_name/0 end + + test "prioritizes values from parameters" do + put_config(storage_account_name: "samplename") + + assert storage_account_name(storage_account_name: "othername") == "othername" + + assert storage_account_name(storage_account_connection_string: @sample_connection_string) == + "cs_samplename" + end end describe "storage_account_key/0" do @@ -47,6 +56,15 @@ defmodule Azurex.Blob.ConfigTest do assert storage_account_key() == "cs_sample_key" end + test "prioritizes values from parameters" do + put_config(storage_account_key: Base.encode64("sample key")) + + assert storage_account_key(storage_account_key: Base.encode64("other key")) == "other key" + + assert storage_account_key(storage_account_connection_string: @sample_connection_string) == + "cs_sample_key" + end + test "error no env set" do put_config() assert_raise RuntimeError, &storage_account_key/0 @@ -94,6 +112,16 @@ defmodule Azurex.Blob.ConfigTest do assert api_url() == "http://127.0.0.1:10000/devstoreaccount1" end + test "prioritizes values from parameters" do + assert api_url(api_url: "https://example.com") == "https://example.com" + + connection_string = + @sample_connection_string <> ";BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1" + + assert api_url(storage_account_connection_string: connection_string) == + "http://127.0.0.1:10000/devstoreaccount1" + end + test "error no env set" do put_config() assert_raise RuntimeError, &api_url/0 @@ -107,6 +135,13 @@ defmodule Azurex.Blob.ConfigTest do assert get_connection_string_value("Key") == "value" end + test "proiritizes value from parameters" do + put_config(storage_account_connection_string: "Key=value") + + assert get_connection_string_value("Key", storage_account_connection_string: "Key=other") == + "other" + end + test "env not in connection_string" do put_config(storage_account_connection_string: "Key=value") @@ -119,4 +154,16 @@ defmodule Azurex.Blob.ConfigTest do assert get_connection_string_value("Invalid") == nil end end + + describe "get_connection_params/1" do + test "assumes it's a container if it's given a string" do + assert get_connection_params("container_name") == [container: "container_name"] + end + + test "returns a keyword list as is" do + keyword = [storage_account_name: "name", container: "container"] + + assert get_connection_params(keyword) == keyword + end + end end diff --git a/test/azurex/blob/shared_access_signature_test.exs b/test/azurex/blob/shared_access_signature_test.exs index f46848c..67976e5 100644 --- a/test/azurex/blob/shared_access_signature_test.exs +++ b/test/azurex/blob/shared_access_signature_test.exs @@ -2,7 +2,7 @@ defmodule Azurex.Blob.SharedAccessSignatureTest do use ExUnit.Case import Azurex.Blob.SharedAccessSignature - setup_all do + setup do Application.put_env(:azurex, Azurex.Blob.Config, storage_account_name: "storage_account", storage_account_key: Base.encode64("secretkey") @@ -31,7 +31,36 @@ defmodule Azurex.Blob.SharedAccessSignatureTest do ) end + test "defaults to parameters from overrides" do + url_with_env = sas_url(container(), blob(), from: now()) + + connection_params = Keyword.put(delete_env(), :container, container()) + url_with_overrides = sas_url(connection_params, blob(), from: now()) + + assert url_with_env == url_with_overrides + end + + test "wihout overrides, operates on default container" do + env = + Application.get_env(:azurex, Azurex.Blob.Config) + |> Keyword.put(:default_container, container()) + + # Reapply environment but with default container + Application.put_env(:azurex, Azurex.Blob.Config, env) + + assert sas_url([], "/", from: now()) == sas_url(container(), "/", from: now()) + end + defp container, do: "my_container" defp blob, do: "/folder/blob.mp4" defp now, do: ~U[2022-10-10 10:10:00Z] + + defp delete_env do + env = Application.get_env(:azurex, Azurex.Blob.Config) + container = env[:default_container] + Application.put_env(:azurex, Azurex.Blob.Config, []) + + env + |> Keyword.put(:container, container) + end end diff --git a/test/integration/blob_integration_test.exs b/test/integration/blob_integration_test.exs index 06377e7..13c5db6 100644 --- a/test/integration/blob_integration_test.exs +++ b/test/integration/blob_integration_test.exs @@ -84,6 +84,21 @@ defmodule Azurex.BlobIntegrationTests do timeout: 10 ) == {:ok, @sample_file_contents} end + + test "with overrides from put_blob" do + blob_name = make_blob_name() + config = Keyword.put(delete_env(), :container, @integration_testing_container) + + Blob.put_blob(blob_name, @sample_file_contents, "text/plain", config) + + assert Blob.get_blob(blob_name, config) == + {:ok, @sample_file_contents} + + AzuriteSetup.set_env() + + assert Blob.get_blob(blob_name, @integration_testing_container) == + {:ok, @sample_file_contents} + end end describe "head blob" do @@ -122,6 +137,29 @@ defmodule Azurex.BlobIntegrationTests do assert headers["content-md5"] == :crypto.hash(:md5, @sample_file_contents) |> Base.encode64() end + + test "with parameters" do + blob_name = make_blob_name() + default_config = delete_env() + config = Keyword.put(default_config, :container, @integration_testing_container) + + assert Blob.put_blob( + blob_name, + @sample_file_contents, + "text/plain", + config + ) == :ok + + assert {:error, :not_found} = Blob.head_blob(blob_name, default_config) + assert {:ok, headers} = Blob.head_blob(blob_name, config) + headers = Map.new(headers) + + assert headers["content-md5"] == + :crypto.hash(:md5, @sample_file_contents) |> Base.encode64() + + AzuriteSetup.set_env() + assert {:error, :not_found} = Blob.head_blob(blob_name) + end end describe "copying a blob" do @@ -140,6 +178,14 @@ defmodule Azurex.BlobIntegrationTests do assert {:ok, @sample_file_contents} = Blob.get_blob(destination_blob) end + test "accepts connection parameters", %{source_blob: source_blob} do + destination_blob = "dest_blob" + config = delete_env() + + assert {:ok, _} = Blob.copy_blob(source_blob, destination_blob, config) + assert {:ok, @sample_file_contents} = Blob.get_blob(destination_blob, config) + end + test "returns error when source blob does not exist", _context do destination_blob = "dest_blob" assert {:error, _} = Blob.copy_blob("does_not_exist", destination_blob) @@ -155,6 +201,11 @@ defmodule Azurex.BlobIntegrationTests do assert {:ok, _result_not_checked} = Blob.list_blobs(@integration_testing_container) end + test "passing container as connection parameter, not checking result" do + assert {:ok, _result_not_checked} = + Blob.list_blobs(container: @integration_testing_container) + end + test "passing container and params, not checking result" do assert {:ok, _result_not_checked} = Blob.list_blobs(@integration_testing_container, timeout: 10) @@ -181,6 +232,22 @@ defmodule Azurex.BlobIntegrationTests do assert {:error, :not_found} = Blob.head_blob(blob_name) end + + test "delete_blob/3 deletes the blob from the container, with connection parameters" do + blob_name = make_blob_name() + config = delete_env() + + assert Blob.put_blob( + blob_name, + @sample_file_contents, + "text/plain", + config + ) == :ok + + assert :ok = Blob.delete_blob(blob_name, config) + + assert {:error, :not_found} = Blob.head_blob(blob_name, config) + end end defp make_blob_name do @@ -191,4 +258,13 @@ defmodule Azurex.BlobIntegrationTests do "#{escaped_time}.txt" end + + defp delete_env do + env = Application.get_env(:azurex, Azurex.Blob.Config) + container = env[:default_container] + Application.put_env(:azurex, Azurex.Blob.Config, []) + + env + |> Keyword.put(:container, container) + end end diff --git a/test/integration/container_integration_test.exs b/test/integration/container_integration_test.exs index ecb6c47..94490f7 100644 --- a/test/integration/container_integration_test.exs +++ b/test/integration/container_integration_test.exs @@ -15,6 +15,11 @@ defmodule Azurex.ContainerIntegrationTests do assert {:ok, _} = Container.head_container(@integration_testing_container) end + test "accepts connection parameters" do + config = delete_env() + assert {:ok, _} = Container.head_container(@integration_testing_container, config) + end + test "returns not_found when the container does not exist" do assert {:error, :not_found} = Container.head_container("thiscontainershouldnotexist") end @@ -35,6 +40,17 @@ defmodule Azurex.ContainerIntegrationTests do assert {:ok, _} = Container.head_container(container_name) end + test "accepts connection parameters" do + config = delete_env() + + container_name = + for _ <- 1..32, into: "", do: <> + + assert {:error, :not_found} = Container.head_container(container_name, config) + assert {:ok, ^container_name} = Container.create(container_name, config) + assert {:ok, _} = Container.head_container(container_name, config) + end + test "returns an error if the container already exists" do assert {:error, :already_exists} = Container.create(@integration_testing_container) end @@ -43,4 +59,13 @@ defmodule Azurex.ContainerIntegrationTests do assert {:error, %HTTPoison.Response{status_code: 400}} = Container.create("@$1") end end + + defp delete_env do + env = Application.get_env(:azurex, Azurex.Blob.Config) + container = env[:default_container] + Application.put_env(:azurex, Azurex.Blob.Config, []) + + env + |> Keyword.put(:container, container) + end end