Packages
phoenix_kit
1.7.113
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/storage/providers/s3.ex
defmodule PhoenixKit.Modules.Storage.Providers.S3 do
@moduledoc """
AWS S3 storage provider.
Stores files in Amazon S3 buckets using the ExAWS library.
Supports all S3-compatible services (like Backblaze B2, Cloudflare R2, Tigris).
Files under 5 MB are uploaded via `put_object` (single request).
Files at or above 5 MB use multipart upload via `ExAws.S3.upload/4`
with streaming and concurrent chunk uploads.
"""
require Logger
alias ExAws.S3.Upload
@behaviour PhoenixKit.Modules.Storage.Provider
# Files at or above this size use multipart upload
@multipart_threshold 5 * 1024 * 1024
@impl true
def store_file(bucket, source_path, destination_path, opts \\ []) do
case File.stat(source_path) do
{:ok, %{size: size}} when size >= @multipart_threshold ->
multipart_upload(bucket, source_path, destination_path, opts)
{:ok, _stat} ->
simple_upload(bucket, source_path, destination_path, opts)
{:error, reason} ->
Logger.error("S3 upload: cannot access source file #{source_path}: #{inspect(reason)}")
{:error, "Cannot access source file: #{inspect(reason)}"}
end
rescue
error ->
Logger.error("S3 upload exception for #{bucket.name}: #{Exception.message(error)}")
{:error, "Error storing file to S3: #{inspect(error)}"}
end
@impl true
def retrieve_file(bucket, file_path, destination_path) do
destination_dir = Path.dirname(destination_path)
File.mkdir_p!(destination_dir)
case ExAws.S3.download_file(bucket.bucket_name, file_path, destination_path)
|> ExAws.request(aws_config(bucket)) do
{:ok, _result} -> :ok
{:error, reason} -> {:error, "Failed to download from S3: #{inspect(reason)}"}
end
rescue
error -> {:error, "Error retrieving file from S3: #{inspect(error)}"}
end
@impl true
def delete_file(bucket, file_path) do
case ExAws.S3.delete_object(bucket.bucket_name, file_path)
|> ExAws.request(aws_config(bucket)) do
{:ok, _result} -> :ok
{:error, reason} -> {:error, "Failed to delete from S3: #{inspect(reason)}"}
end
rescue
error -> {:error, "Error deleting file from S3: #{inspect(error)}"}
end
@impl true
def file_exists?(bucket, file_path) do
case ExAws.S3.head_object(bucket.bucket_name, file_path)
|> ExAws.request(aws_config(bucket)) do
{:ok, _result} -> true
{:error, {:http_error, 404, _}} -> false
{:error, _reason} -> false
end
rescue
_error -> false
end
@impl true
def public_url(bucket, file_path) do
if bucket.cdn_url do
"#{bucket.cdn_url}/#{file_path}"
else
region = bucket.region || "us-east-1"
"https://#{bucket.bucket_name}.s3.#{region}.amazonaws.com/#{file_path}"
end
end
@impl true
def test_connection(bucket) do
case ExAws.S3.list_objects(bucket.bucket_name, max_keys: 1)
|> ExAws.request(aws_config(bucket)) do
{:ok, _result} -> :ok
{:error, {:http_error, 403, _}} -> {:error, "Access denied - check permissions"}
{:error, {:http_error, 404, _}} -> {:error, "Bucket not found"}
{:error, reason} -> {:error, "S3 connection test failed: #{inspect(reason)}"}
end
rescue
error -> {:error, "Error testing S3 connection: #{inspect(error)}"}
end
# Single-request upload for small files (<5 MB).
# Reads entire file into memory and sends in one PUT request.
defp simple_upload(bucket, source_path, destination_path, opts) do
content_type = Keyword.get(opts, :content_type)
case File.read(source_path) do
{:ok, file_content} ->
put_opts =
[{:acl, Keyword.get(opts, :acl, "private")}] ++
if(content_type, do: [{:content_type, content_type}], else: [])
case ExAws.S3.put_object(bucket.bucket_name, destination_path, file_content, put_opts)
|> ExAws.request(aws_config(bucket)) do
{:ok, _result} ->
{:ok, public_url(bucket, destination_path)}
{:error, reason} ->
Logger.error("S3 put_object failed for #{bucket.name}: #{inspect(reason)}")
{:error, "Failed to upload to S3: #{inspect(reason)}"}
end
{:error, reason} ->
Logger.error("S3 upload: cannot read source file #{source_path}: #{inspect(reason)}")
{:error, "Cannot read source file: #{inspect(reason)}"}
end
end
# Multipart streaming upload for large files (>=5 MB).
# Streams file in chunks with concurrent part uploads.
defp multipart_upload(bucket, source_path, destination_path, opts) do
content_type = Keyword.get(opts, :content_type)
upload_opts =
[acl: Keyword.get(opts, :acl, "private"), max_concurrency: 4, timeout: 60_000] ++
if(content_type, do: [content_type: content_type], else: [])
case source_path
|> Upload.stream_file()
|> ExAws.S3.upload(bucket.bucket_name, destination_path, upload_opts)
|> ExAws.request(aws_config(bucket)) do
{:ok, _result} ->
{:ok, public_url(bucket, destination_path)}
{:error, reason} ->
Logger.error("S3 multipart upload failed for #{bucket.name}: #{inspect(reason)}")
{:error, "Failed multipart upload to S3: #{inspect(reason)}"}
end
end
# Build per-request ExAws config from bucket credentials.
# Passed to ExAws.request/2 instead of using global Application.put_env.
defp aws_config(bucket) do
config = [
access_key_id: bucket.access_key_id,
secret_access_key: bucket.secret_access_key,
region: bucket.region || "us-east-1"
]
if bucket.endpoint do
config ++ [host: bucket.endpoint, scheme: "https://"]
else
config
end
end
end