Skip to content
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
102 changes: 63 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
[hex-img]: http://img.shields.io/hexpm/v/waffle.svg

[hexdocs-img]: http://img.shields.io/badge/hexdocs-documentation-brightgreen.svg

[evrone-img]: https://img.shields.io/badge/Sponsored_by-Evrone-brightgreen.svg

# Waffle [![Sponsored by Evrone][evrone-img]](https://evrone.com?utm_source=waffle)
Expand All @@ -11,14 +9,14 @@

![Waffle is a flexible file upload library for Elixir](https://elixir-waffle.github.io/waffle/assets/logo.svg)

Waffle is a flexible file upload library for Elixir with straightforward integrations for Amazon S3 and ImageMagick.
Waffle is a flexible file upload library for Elixir with straightforward integrations for Amazon S3, Azure Blob Storage, and ImageMagick.

[Documentation](https://hexdocs.pm/waffle)

## Installation

Add the latest stable release to your `mix.exs` file, along with the
required dependencies for `ExAws` if appropriate:
required dependencies for your chosen storage provider:

```elixir
defp deps do
Expand All @@ -29,7 +27,11 @@ defp deps do
{:ex_aws, "~> 2.1.2"},
{:ex_aws_s3, "~> 2.0"},
{:hackney, "~> 1.9"},
{:sweet_xml, "~> 0.6"}
{:sweet_xml, "~> 0.6"},

# If using Azure Blob Storage:
{:req, "~> 0.4"},
{:timex, "~> 3.7"}
]
end
```
Expand All @@ -45,10 +47,11 @@ After installing Waffle, another two things should be done:

### Setup a storage provider

Waffle has two built-in storage providers:
Waffle has three built-in storage providers:

* `Waffle.Storage.Local`
* `Waffle.Storage.S3`
- `Waffle.Storage.Local`
- `Waffle.Storage.S3`
- `Waffle.Storage.Azure`

[Other available storage providers](#other-storage-providers)
are supported by the community.
Expand All @@ -74,16 +77,28 @@ config :ex_aws,
# any configurations provided by https://github.com/ex-aws/ex_aws
```

An example for setting up `Waffle.Storage.Azure`:

```elixir
config :waffle,
storage: Waffle.Storage.Azure,
storage_account: "mystorageaccount", # or {:system, "AZURE_STORAGE_ACCOUNT"}
container: "uploads", # or {:system, "AZURE_STORAGE_CONTAINER"}
access_key: "your-access-key", # or {:system, "AZURE_ACCESS_KEY"}
public_access: false, # Set to true for public access
expiry_in_minutes: 60 # SAS token expiry for private access
```

### Define a definition module

Waffle requires a **definition module** which contains the relevant
functions to store and retrieve files:

* Optional transformations of the uploaded file
* Where to put your files (the storage directory)
* How to name your files
* How to secure your files (private? Or publicly accessible?)
* Default placeholders
- Optional transformations of the uploaded file
- Where to put your files (the storage directory)
- How to name your files
- How to secure your files (private? Or publicly accessible?)
- Default placeholders

This module can be created manually or generated by `mix waffle.g`
automatically.
Expand All @@ -98,34 +113,35 @@ Check this file for descriptions of configurable options.

## Examples

* [An example for Local storage driver](documentation/examples/local.md)
* [An example for S3 storage driver](documentation/examples/s3.md)
- [An example for Local storage driver](documentation/examples/local.md)
- [An example for S3 storage driver](documentation/examples/s3.md)
- [An example for Azure Blob Storage driver](documentation/examples/azure.md)

## Usage with Ecto

Waffle comes with a companion package for use with Ecto. If you
intend to use Waffle with Ecto, it is highly recommended you also
add the
[`waffle_ecto`](https://github.com/elixir-waffle/waffle_ecto)
dependency. Benefits include:
dependency. Benefits include:

* Changeset integration
* Versioned urls for cache busting (`.../thumb.png?v=63601457477`)
- Changeset integration
- Versioned urls for cache busting (`.../thumb.png?v=63601457477`)

## Other Storage Providers

* **Rackspace** - [arc_rackspace](https://github.com/lokalebasen/arc_rackspace)
- **Rackspace** - [arc_rackspace](https://github.com/lokalebasen/arc_rackspace)

- **Manta** - [arc_manta](https://github.com/onyxrev/arc_manta)

* **Manta** - [arc_manta](https://github.com/onyxrev/arc_manta)
- **OVH** - [arc_ovh](https://github.com/stephenmoloney/arc_ovh)

* **OVH** - [arc_ovh](https://github.com/stephenmoloney/arc_ovh)
- **Google Cloud Storage** - [waffle_gcs](https://github.com/elixir-waffle/waffle_gcs)

* **Google Cloud Storage** - [waffle_gcs](https://github.com/elixir-waffle/waffle_gcs)
- **Microsoft Azure Storage** - Built-in `Waffle.Storage.Azure` or [arc_azure](https://github.com/phil-a/arc_azure)

* **Microsoft Azure Storage** - [arc_azure](https://github.com/phil-a/arc_azure)
- **Aliyun OSS Storage** - [waffle_aliyun_oss](https://github.com/ug0/waffle_aliyun_oss)

* **Aliyun OSS Storage** - [waffle_aliyun_oss](https://github.com/ug0/waffle_aliyun_oss)

## Testing

The basic test suite can be run with without supplying any S3 information:
Expand All @@ -140,13 +156,21 @@ access.

The following environment variables will be used by the test suite:

* WAFFLE_TEST_BUCKET
* WAFFLE_TEST_BUCKET2
* WAFFLE_TEST_S3_KEY
* WAFFLE_TEST_S3_SECRET
* WAFFLE_TEST_REGION
**For S3 testing:**

- WAFFLE_TEST_BUCKET
- WAFFLE_TEST_BUCKET2
- WAFFLE_TEST_S3_KEY
- WAFFLE_TEST_S3_SECRET
- WAFFLE_TEST_REGION

**For Azure testing:**

- AZURE_STORAGE_ACCOUNT
- AZURE_STORAGE_CONTAINER
- AZURE_ACCESS_KEY

After setting these variables, you can run the full test suite with `mix test --include s3:true`.
After setting these variables, you can run the full test suite with `mix test --include s3:true` or `mix test --include azure:true`.

## Attribution

Expand All @@ -160,14 +184,14 @@ Copyright 2019 Boris Kuznetsov <me@achempion.com>

Copyright 2015 Sean Stavropoulos

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
194 changes: 194 additions & 0 deletions documentation/examples/azure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Azure Blob Storage

This guide will help you set up Waffle to work with Azure Blob Storage.

## Configuration

Add the following to your `config/config.exs`:

```elixir
config :waffle,
storage: Waffle.Storage.Azure,
storage_account: {:system, "AZURE_STORAGE_ACCOUNT"},
container: {:system, "AZURE_STORAGE_CONTAINER"},
access_key: {:system, "AZURE_ACCESS_KEY"},
public_access: false,
expiry_in_minutes: 60
```

Or with direct values:

```elixir
config :waffle,
storage: Waffle.Storage.Azure,
storage_account: "mystorageaccount",
container: "uploads",
access_key: "your-access-key",
public_access: false,
expiry_in_minutes: 60
```

## Environment Variables

Set the following environment variables:

```bash
export AZURE_STORAGE_ACCOUNT="mystorageaccount"
export AZURE_STORAGE_CONTAINER="uploads"
export AZURE_ACCESS_KEY="your-access-key"
```

## Definition Module

Create a definition module for your uploads:

```elixir
defmodule MyApp.Avatar do
use Waffle.Definition

@extension_whitelist ~w(.jpg .jpeg .gif .png)

def validate({file, _}) do
file_extension = file.file_name |> Path.extname() |> String.downcase()

case Enum.member?(@extension_whitelist, file_extension) do
true -> :ok
false -> {:error, "invalid file type"}
end
end

def storage_dir(version, {file, scope}) do
"uploads/users/avatars/#{scope.id}"
end

# Optional: Override container per definition
def container({_file, scope}), do: scope.container || container()

# Optional: Override storage account per definition
def storage_account({_file, scope}), do: scope.storage_account || storage_account()

# Optional: Custom Azure blob headers
def azure_blob_headers(version, {file, scope}) do
[content_type: MIME.from_path(file.file_name)]
end
end
```

## Usage

### Storing Files

```elixir
# Store a file
{:ok, file_name} = MyApp.Avatar.store({%Plug.Upload{path: "/tmp/avatar.jpg", filename: "avatar.jpg"}, user})

# Store with custom scope
{:ok, file_name} = MyApp.Avatar.store({%Plug.Upload{path: "/tmp/avatar.jpg", filename: "avatar.jpg"}, %{id: 123, container: "custom-container"}})
```

### Generating URLs

```elixir
# Generate public URL (if public_access is true)
url = MyApp.Avatar.url({file_name, user})

# Generate signed URL (if public_access is false)
url = MyApp.Avatar.url({file_name, user}, signed: true)

# Generate signed URL with custom expiry
url = MyApp.Avatar.url({file_name, user}, signed: true, expires_in: 3600) # 1 hour
```

### Deleting Files

```elixir
# Delete a file
:ok = MyApp.Avatar.delete({file_name, user})
```

## Multiple Containers

You can use different containers for different uploaders:

```elixir
defmodule MyApp.Document do
use Waffle.Definition

def container, do: "documents"
end

defmodule MyApp.Image do
use Waffle.Definition

def container, do: "images"
end
```

## Public vs Private Access

### Public Access

When `public_access` is set to `true`, files are accessible via direct URLs without authentication:

```elixir
config :waffle,
public_access: true
```

### Private Access (Default)

When `public_access` is `false` (default), files are accessed via signed URLs with SAS tokens:

```elixir
config :waffle,
public_access: false,
expiry_in_minutes: 60 # SAS token expires in 60 minutes
```

## Custom Headers

You can specify custom headers for Azure blob storage:

```elixir
def azure_blob_headers(version, {file, scope}) do
[
content_type: MIME.from_path(file.file_name),
cache_control: "public, max-age=31536000",
content_disposition: "inline; filename=\"#{file.file_name}\""
]
end
```

## Dependencies

Make sure to add the required dependencies to your `mix.exs`:

```elixir
defp deps do
[
{:waffle, "~> 1.1"},
{:req, "~> 0.4"},
{:timex, "~> 3.7"}
]
end
```

## Error Handling

The Azure storage adapter will return appropriate error tuples:

```elixir
case MyApp.Avatar.store({file, user}) do
{:ok, file_name} ->
# Success
{:error, reason} ->
# Handle error
end
```

Common error scenarios:

- Invalid storage account or container
- Missing access key
- Network connectivity issues
- File read errors
Loading