Skip to content

Commit

Permalink
Allow configuration source to be injected
Browse files Browse the repository at this point in the history
Still always read azure connection string from application config, but
allow the element itself to be parameterised. Still default to azurex.

This change does introduce some breaking API changes, notably on
put_blob, to nest params under options (similar to an earlier commit's
work on get_blob).

Co-authored-by: Aman Kumar Singh <[email protected]>
Co-authored-by: Konstantinos Gkinis <[email protected]>
  • Loading branch information
3 people committed Oct 25, 2024
1 parent c4d0445 commit c8f56b2
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 110 deletions.
66 changes: 36 additions & 30 deletions lib/azurex/blob.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ defmodule Azurex.Blob do

@typep optional_string :: String.t() | nil

def list_containers do
def list_containers(conf_element \\ :azurex) do
%HTTPoison.Request{
url: Config.api_url() <> "/",
url: Config.api_url(conf_element) <> "/",
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(conf_element),
storage_account_key: Config.storage_account_key(conf_element)
)
|> HTTPoison.request()
|> case do
Expand Down Expand Up @@ -71,32 +71,34 @@ 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
{params, options} = Keyword.pop(options, :params, [])
{conf_element, _options} = Keyword.pop(options, :config_element, :azurex)
content_type = content_type || "application/octet-stream"

%HTTPoison.Request{
method: :put,
url: get_url(container, name),
url: get_url(container, conf_element, name),
params: params,
body: blob,
headers: [
Expand All @@ -107,8 +109,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(conf_element),
storage_account_key: Config.storage_account_key(conf_element),
content_type: content_type
)
|> HTTPoison.request()
Expand Down Expand Up @@ -180,17 +182,17 @@ defmodule Azurex.Blob do
{:ok, HTTPoison.Response.t()} | {:error, term()}
def copy_blob(source_name, destination_name, container \\ nil) do
content_type = "application/octet-stream"
source_url = get_url(container, source_name)
source_url = get_url(container, :azurex, source_name)
headers = [{"x-ms-copy-source", source_url}, {"content-type", content_type}]

%HTTPoison.Request{
method: :put,
url: get_url(container, destination_name),
url: get_url(container, :azurex, destination_name),
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(:azurex),
storage_account_key: Config.storage_account_key(:azurex),
content_type: content_type
)
|> HTTPoison.request()
Expand All @@ -217,17 +219,18 @@ defmodule Azurex.Blob do
def blob_request(name, container, method, options) do
{params, options} = Keyword.pop(options, :params, [])
{headers, options} = Keyword.pop(options, :headers, [])
{conf_element, options} = Keyword.pop(options, :config_element, :azurex)

%HTTPoison.Request{
method: method,
url: get_url(container, name),
url: get_url(container, conf_element, name),
params: params,
headers: headers,
options: options
}
|> SharedKey.sign(
storage_account_name: Config.storage_account_name(),
storage_account_key: Config.storage_account_key()
storage_account_name: Config.storage_account_name(conf_element),
storage_account_key: Config.storage_account_key(conf_element)
)
end

Expand All @@ -245,18 +248,21 @@ defmodule Azurex.Blob do
@spec list_blobs(optional_string) ::
{:ok, binary()}
| {:error, HTTPoison.AsyncResponse.t() | HTTPoison.Error.t() | HTTPoison.Response.t()}
def list_blobs(container \\ nil, params \\ []) do
def list_blobs(container \\ nil, options \\ []) do
{params, options} = Keyword.pop(options, :params, [])
{conf_element, _options} = Keyword.pop(options, :config_element, :azurex)

%HTTPoison.Request{
url: "#{Config.api_url()}/#{get_container(container)}",
url: "#{Config.api_url(conf_element)}/#{get_container(container, conf_element)}",
params:
[
comp: "list",
restype: "container"
] ++ params
}
|> SharedKey.sign(
storage_account_name: Config.storage_account_name(),
storage_account_key: Config.storage_account_key()
storage_account_name: Config.storage_account_name(conf_element),
storage_account_key: Config.storage_account_key(conf_element)
)
|> HTTPoison.request()
|> case do
Expand All @@ -269,20 +275,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(optional_string, String.t()) :: String.t()
def get_url(container, conf_element) do
"#{Config.api_url(conf_element)}/#{get_container(container, conf_element)}"
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}"
def get_url(container, conf_element, blob_name) do
"#{get_url(container, conf_element)}/#{blob_name}"
end

defp get_container(container) do
container || Config.default_container()
defp get_container(container, conf_element) do
container || Config.default_container(conf_element)
end
end
20 changes: 12 additions & 8 deletions lib/azurex/blob/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ 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
{params, options} = Keyword.pop(options, :params, [])
{conf_element, _options} = Keyword.pop(options, :config_element, :azurex)
block_id = build_block_id()
content_type = "application/octet-stream"
params = [{:comp, "block"}, {:blockid, block_id} | params]

%HTTPoison.Request{
method: :put,
url: Blob.get_url(container, name),
url: Blob.get_url(container, conf_element, name),
params: params,
body: chunk,
headers: [
Expand All @@ -35,8 +37,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(conf_element),
storage_account_key: Config.storage_account_key(conf_element),
content_type: content_type
)
|> HTTPoison.request()
Expand All @@ -54,7 +56,9 @@ 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, [])
{conf_element, _options} = Keyword.pop(options, :config_element, :azurex)
params = [{:comp, "blocklist"} | params]
content_type = "text/plain; charset=UTF-8"
blob_content_type = blob_content_type || "application/octet-stream"
Expand All @@ -74,7 +78,7 @@ defmodule Azurex.Blob.Block do

%HTTPoison.Request{
method: :put,
url: Blob.get_url(container, name),
url: Blob.get_url(container, conf_element, name),
params: params,
body: body,
headers: [
Expand All @@ -83,8 +87,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(conf_element),
storage_account_key: Config.storage_account_key(conf_element),
content_type: content_type
)
|> HTTPoison.request()
Expand Down
47 changes: 24 additions & 23 deletions lib/azurex/blob/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,39 @@ defmodule Azurex.Blob.Config do
or `storage_account_connection_string` required.
"""

defp conf, do: Application.get_env(:azurex, __MODULE__, [])
defp conf(config_element), do: Application.get_env(config_element, __MODULE__, [])

@doc """
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
@spec api_url(atom()) :: String.t()
# TODO this, and below functions are public therefore we should probably default this
def api_url(config_element) 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"
api_url = Keyword.get(conf(config_element), :api_url) -> api_url
api_url = get_connection_string_value("BlobEndpoint", config_element) -> api_url
true -> "https://#{storage_account_name(config_element)}.blob.core.windows.net"
end
end

@doc """
Azure container name, optional.
"""
@spec default_container :: String.t() | nil
def default_container do
Keyword.get(conf(), :default_container) ||
@spec default_container(atom()) :: String.t() | nil
def default_container(config_element) do
Keyword.get(conf(config_element), :default_container) ||
raise "Must specify `container` because the default container was not provided."
end

@doc """
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")
@spec storage_account_name(atom()) :: String.t()
def storage_account_name(config_element) do
case Keyword.get(conf(config_element), :storage_account_name) do
nil -> get_connection_string_value("AccountName", config_element)
storage_account_name -> storage_account_name
end || raise @missing_envs_error_msg
end
Expand All @@ -48,10 +49,10 @@ defmodule Azurex.Blob.Config do
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")
@spec storage_account_key(atom()) :: binary
def storage_account_key(config_element) do
case Keyword.get(conf(config_element), :storage_account_key) do
nil -> get_connection_string_value("AccountKey", config_element)
key -> key
end
|> Kernel.||(raise @missing_envs_error_msg)
Expand All @@ -62,9 +63,9 @@ defmodule Azurex.Blob.Config do
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(atom()) :: String.t() | nil
def storage_account_connection_string(config_element),
do: Keyword.get(conf(config_element), :storage_account_connection_string)

@spec parse_connection_string(nil | binary) :: map
@doc """
Expand Down Expand Up @@ -96,9 +97,9 @@ 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()
@spec get_connection_string_value(String.t(), atom()) :: String.t() | nil
def get_connection_string_value(key, config_element) do
storage_account_connection_string(config_element)
|> parse_connection_string
|> Map.get(key)
end
Expand Down
16 changes: 8 additions & 8 deletions lib/azurex/blob/container.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ defmodule Azurex.Blob.Container do
alias Azurex.Blob.Config
alias Azurex.Authorization.SharedKey

def head_container(container) do
def head_container(container, conf_element \\ :azurex) do
%HTTPoison.Request{
url: Config.api_url() <> "/" <> container,
url: Config.api_url(conf_element) <> "/" <> 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(conf_element),
storage_account_key: Config.storage_account_key(conf_element)
)
|> HTTPoison.request()
|> case do
Expand All @@ -24,15 +24,15 @@ defmodule Azurex.Blob.Container do
end
end

def create(container) do
def create(container, conf_element \\ :azurex) do
%HTTPoison.Request{
url: Config.api_url() <> "/" <> container,
url: Config.api_url(conf_element) <> "/" <> 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(conf_element),
storage_account_key: Config.storage_account_key(conf_element),
content_type: "application/octet-stream"
)
|> HTTPoison.request()
Expand Down
6 changes: 3 additions & 3 deletions lib/azurex/blob/shared_access_signature.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defmodule Azurex.Blob.SharedAccessSignature do
"""
@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()
base_url = Azurex.Blob.Config.api_url(:azurex)
resource_type = Keyword.get(opts, :resource_type, :container)
permissions = Keyword.get(opts, :permissions, [:read])
from = Keyword.get(opts, :from, DateTime.utc_now())
Expand All @@ -38,8 +38,8 @@ defmodule Azurex.Blob.SharedAccessSignature do
resource,
{from, expiry},
permissions,
Azurex.Blob.Config.storage_account_name(),
Azurex.Blob.Config.storage_account_key()
Azurex.Blob.Config.storage_account_name(:azurex),
Azurex.Blob.Config.storage_account_key(:azurex)
)

"#{Path.join(base_url, resource)}?#{token}"
Expand Down
Loading

0 comments on commit c8f56b2

Please sign in to comment.