Skip to content

Commit

Permalink
Allow multiple azure instances
Browse files Browse the repository at this point in the history
Allow passing connection parameters together with the container to blob
functions.

Values from the parameters are prioritized over default configuration.
  • Loading branch information
noaccOS committed Oct 4, 2024
1 parent 73615f9 commit 7b028b3
Show file tree
Hide file tree
Showing 11 changed files with 361 additions and 102 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
123 changes: 75 additions & 48 deletions lib/azurex/blob.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: [
Expand All @@ -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()
Expand All @@ -130,18 +143,24 @@ 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"}
iex> get_blob("filename.txt")
{: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}
Expand All @@ -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}
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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

Expand All @@ -233,24 +255,29 @@ defmodule Azurex.Blob do
iex> Azurex.Blob.list_blobs()
{:ok, "\uFEFF<?xml ...."}
iex> Azurex.Blob.list_blobs(storage_account_name: "name", storage_account_key: "key", container: "container")
{:ok, "\uFEFF<?xml ...."}
iex> 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",
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(connection_params),
storage_account_key: Config.storage_account_key(connection_params)
)
|> HTTPoison.request()
|> case do
Expand All @@ -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
22 changes: 12 additions & 10 deletions lib/azurex/blob/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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: [
Expand All @@ -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()
Expand Down
Loading

0 comments on commit 7b028b3

Please sign in to comment.