# Storage
[](https://github.com/phoenix-contrib/storage/actions/workflows/ci.yml)
[](https://hex.pm/packages/phoenix_contrib_storage)
[](https://hexdocs.pm/phoenix_contrib_storage/)
[](https://hex.pm/packages/phoenix_contrib_storage)
ActiveStorage-like file storage for Phoenix. All things file uploads for your Phoenix app following the design principles applied in Rails ActiveStorage but adapted to the Phoenix framework.
## Features
- **Multiple storage backends**: Local filesystem, S3, and more
- **Direct uploads**: Signed URLs for client-side uploads
- **Image processing**: Built-in support for variants and transformations
- **Ecto integration**: Seamless attachment associations with your schemas
- **Phoenix LiveView support**: Helper functions for file uploads
- **Configurable**: Easy configuration for different environments
## Installation
Add `phoenix_contrib_storage` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:phoenix_contrib_storage, "~> 0.1.0"}
]
end
```
## Configuration
Add to your `config/config.exs`:
```elixir
config :phoenix_contrib_storage,
repo: MyApp.Repo,
default_service: :local,
services: %{
local: {Storage.Services.Local, root: "priv/storage"},
s3: {Storage.Services.S3, bucket: "my-bucket", region: "us-east-1"}
}
```
## Database Setup
Run the migrations to create the necessary tables:
```bash
mix ecto.gen.migration create_storage_tables
```
Copy the contents from `priv/repo/migrations/` or use the provided migration files.
## Usage
### Basic File Upload
```elixir
# Upload a file
{:ok, blob} = Storage.put_file("/path/to/file.jpg", filename: "avatar.jpg")
# Get file URL
url = Storage.Blob.url(blob)
```
### Schema Integration
```elixir
defmodule MyApp.User do
use Ecto.Schema
use Storage.Attachment
schema "users" do
field :name, :string
# Single file attachment
has_one_attached :avatar
# Multiple file attachments
has_many_attached :documents
timestamps()
end
end
# Usage
user = MyApp.Repo.get!(User, 1)
# Attach files
Storage.Attachment.attach_one(user, :avatar, blob)
Storage.Attachment.attach_many(user, :documents, [blob1, blob2])
# Check if attached
user.avatar_attached?(user) # true/false
# Get attachments
avatar = user.avatar(user)
documents = user.documents(user)
```
### Phoenix LiveView Integration
```elixir
defmodule MyAppWeb.UserLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
socket =
socket
|> allow_upload(:avatar, Storage.LiveView.upload_options(
accept: ~w(.jpg .jpeg .png),
max_entries: 1,
max_file_size: 5_000_000
))
{:ok, socket}
end
def handle_event("save", %{"user" => user_params}, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->
{:ok, Storage.LiveView.consume_uploaded_entry(path, entry)}
end)
# Use uploaded_files with your changeset...
{:noreply, socket}
end
end
```
### File Serving
Add to your router:
```elixir
# Basic file serving
get "/storage/:key", Storage.Controller, :serve
# With filename for SEO-friendly URLs
get "/storage/:key/:filename", Storage.Controller, :serve_with_filename
# Force download
get "/storage/:key/download", Storage.Controller, :download
```
### Direct Uploads (S3)
```elixir
# Generate signed URL for direct upload
{:ok, upload_url} = Storage.signed_url_for_direct_upload(
filename: "document.pdf",
content_type: "application/pdf"
)
# Use in your frontend for direct uploads
```
## Storage Services
### Local Filesystem
```elixir
config :phoenix_contrib_storage,
services: %{
local: {Storage.Services.Local, root: "priv/storage"}
}
```
### Amazon S3
```elixir
config :phoenix_contrib_storage,
services: %{
s3: {Storage.Services.S3,
bucket: "my-bucket",
region: "us-east-1",
access_key_id: "...",
secret_access_key: "..."
}
}
```
## API Reference
### Storage
- `Storage.put_file/2` - Upload a file and create a blob
- `Storage.get_file/1` - Retrieve file data
- `Storage.delete_file/1` - Delete a file
- `Storage.signed_url_for_direct_upload/1` - Generate signed upload URL
### Storage.Blob
- `Storage.Blob.url/2` - Generate URL for a blob
- `Storage.Blob.image?/1` - Check if blob is an image
- `Storage.Blob.human_size/1` - Get human-readable file size
### Storage.Attachment
- `has_one_attached/1` - Define single file attachment
- `has_many_attached/1` - Define multiple file attachments
- `attach_one/3` - Attach single file to record
- `attach_many/3` - Attach multiple files to record
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
### Development Setup
1. Clone the repository
2. Install dependencies: `mix deps.get`
3. Run tests: `mix test`
4. Check code quality: `mix credo --strict`
5. Check types: `mix dialyzer`
6. Format code: `mix format`
### Code Quality
This project maintains high code quality standards:
- **Tests**: Comprehensive test suite with >90% coverage
- **Linting**: Code linting with Credo in strict mode
- **Type Checking**: Static analysis with Dialyzer
- **Formatting**: Consistent code formatting with `mix format`
- **Documentation**: All public functions are documented
- **CI/CD**: Automated testing, linting, and type checking on all PRs
## License
MIT License. See [LICENSE](LICENSE) for details.