We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Jump to file
- ∟ mix.exs
- ∟ assets/js/app.js
- ∟ assets/js/uploaders/index.js
- ∟ assets/js/uploaders/live-uploader.js
- ∟ config/dev.exs
- ∟ config/runtime.exs
- ∟ config/test.exs
- ∟ cors.json
- ∟ lib/my_app/application.ex
- ∟ lib/my_app/cloud.ex
- ∟ lib/my_app/storage.ex
- ∟ lib/my_app/storage/object.ex
- ∟ lib/my_app/storage/object_uploaded_by_user.ex
- ∟ lib/my_app_web/controllers/storage_controller.ex
- ∟ lib/my_app_web/live/storage_live.ex
- ∟ lib/my_app_web/live/storage_live/components.ex
- ∟ lib/my_app_web/router.ex
- ∟ priv/repo/migrations/20250627172533_create_storage_objects.exs
- ∟ test/my_app/storage/object_test.exs
- ∟ test/my_app/storage/object_uploaded_by_user_test.exs
- ∟ test/my_app/storage_test.exs
- ∟ test/my_app_web/controllers/storage_controller_test.exs
Install deps
Install the following dependencies to get started. You can find their documentation on HexDocs.
- Buckets
- hexdocs.pm/buckets
- Size
- hexdocs.pm/size
Adding uploaders in JS
The uploaders module is imported and passed to the
LiveSocket
configuration. This change enables the frontend to handle file uploads using the configured
uploader, which in this case is set up to use Google Cloud Storage (GCS).
|
|
|
|
|
|
|
|
+ |
|
|
|
|
|
|
|
+ |
|
|
|
|
|
|
Defining uploaders
A new file uploaders.js exports the GCS uploader.
|
|
+ |
|
Live uploader
The uploader is implemented in uploaders/live-uploader.js, handling the actual upload process via XMLHttpRequest. This script manages the file upload lifecycle, providing progress updates and handling errors.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Cloud configuration
Configure your buckets cloud module for working with file objects. By default this guide uses the local volume adapter in development and the Google Cloud Storage adapter for production.
In Phoenix 1.7.14, the configuration format has changed slightly to support the new bucket options.
|
|
|
|
|
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
|
|
|
CORS
When uploading to Google Cloud, you must apply a CORS configuration to your bucket. This is an example of what that configuration looks like. See the GCS strategy in buckets for details.
Application
Add your cloud module to your application supervision tree. This will handle any background operations required by the buckets adapter you're using, like keep auth tokens fresh.
|
|
|
|
|
|
|
|
+ |
|
|
|
|
|
|
Cloud
This is your cloud module, which you'll use to manage objects. This is a concept coming from our
buckets dependency, think of this module similarly to your
MyApp.Repo
module, but for files.
|
|
+ |
|
+ |
|
+ |
|
Storage module
The storage.ex module encapsulates all interactions with storage objects. This module provides various functionalities, including creating, retrieving, listing, and marking objects as deleted.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Object schema
The object.ex module defines the storage object schema and associated changesets. This file manages object data, including filename normalization, schema definition, and database operations.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Object uploaded by user schema
The object_uploaded_by_user.ex module links objects with the users who uploaded them.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Storage controller
A new controller, StorageController, is introduced to handle file downloads. The controller module exposes a function that generates a secure URL for downloading files. A download action verifies the token and retrieves the file for download, sending it back to the client.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
External uploads in LiveView
StorageUploadLive provides a dynamic interface for users to upload files. Users can drag and drop files or browse to upload, the file in put provides real-time feedback on the upload progress. Once stored, uploaded files may be deleted or downloaded.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Storage components
To keep our LiveView module less cluttered, we add a separate components module which contains the components specific to Storage. These can be referenced by other features in the future, and more components can be added as our upload interface changes.
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Router updates
Routes for the new file storage features are added to the router. Defines a route for downloading files, a LiveView route for the file upload interface, and configures a local volume for development file storage.
|
|
|
|
|
|
|
|
+ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ |
|
+ |
|
|
|
|
|
|
|
|
|
+ |
|
+ |
|
|
|
|
|
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
|
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
+ |
|
Install with Claude Code
Write instructions to implement this feature to your project directory in a LLM-friendly format, then have Claude take care of the rest! Requires Claude Code to be installed.
curl "https://elixir-saas.com/llms/p/storage.md?v=1.8.0-rc.3&f=impl" > storage.md;
curl "https://elixir-saas.com/llms/p/storage.md?v=1.8.0-rc.3&f=test" > storage_tests.md;
claude "Implement the feature that is documented in: storage.md, then add the tests documented in: storage_tests.md." --allowedTools "Write Edit Bash(mix:*) Bash(mkdir:*)";