Packages

phoenix_kit

1.7.39
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
phoenix_kit lib modules storage services file_server.ex
Raw

lib/modules/storage/services/file_server.ex

defmodule PhoenixKit.Modules.Storage.FileServer do
@moduledoc """
File serving logic with multi-location failover support.
Handles retrieving files from storage locations with automatic failover,
CDN integration, and proper HTTP header management.
## Features
- Query file locations from database
- Priority-based location ordering
- Automatic failover to next location on failure
- CDN redirect support
- HTTP header generation for streaming
- Range request support (HTTP 206) for video streaming
## Examples
iex> {:ok, file_info} = PhoenixKit.Modules.Storage.FileServer.get_file_location(
...> "018e3c4a-9f6b-7890",
...> "thumbnail"
...> )
iex> file_info.path
"/path/018e3c4a-9f6b-7890-thumbnail.jpg"
iex> file_info.bucket.cdn_url
"https://cdn.example.com"
"""
import Ecto.Query
alias PhoenixKit.Modules.Storage.FileInstance
alias PhoenixKit.Modules.Storage.FileLocation
@doc """
Get file location with priority-ordered failover list.
Queries the database for all active locations of a file instance,
ordered by bucket priority and location priority for automatic failover.
## Arguments
- `file_id` (binary) - File UUID v7
- `instance_name` (binary) - Variant name (e.g., "thumbnail", "medium")
## Returns
- `{:ok, file_info}` - Contains file metadata and location options
- `{:error, :not_found}` - File instance or locations not found
- `{:error, :no_active_locations}` - No active storage locations available
## Example Response
{:ok, %{
instance: %FileInstance{...},
locations: [
%FileLocation{
path: "/path/018e3c4a-thumbnail.jpg",
bucket: %Bucket{
name: "Local SSD",
provider: "local",
cdn_url: nil,
priority: 1
},
priority: 0,
status: "active"
},
%FileLocation{
path: "/path/018e3c4a-thumbnail.jpg",
bucket: %Bucket{
name: "Backblaze B2",
provider: "b2",
cdn_url: "https://cdn.example.com",
priority: 2
},
priority: 0,
status: "active"
}
]
}}
"""
def get_file_location(file_id, instance_name)
when is_binary(file_id) and is_binary(instance_name) do
repo = get_repo()
# Query for file instance with all its locations
query =
from fi in FileInstance,
where: fi.file_id == ^file_id and fi.variant_name == ^instance_name,
preload: [
locations: [
bucket: []
]
]
case repo.one(query) do
nil ->
{:error, :not_found}
instance ->
# Filter for active locations only
active_locations =
instance.locations
|> Enum.filter(&(&1.status == "active"))
|> Enum.sort_by(&location_priority/1)
case active_locations do
[] ->
{:error, :no_active_locations}
locations ->
{:ok,
%{
instance: instance,
locations: locations
}}
end
end
end
@doc """
Generate HTTP headers for file serving.
Creates proper response headers including Content-Type, Content-Length,
Cache-Control, and ETag for file streaming.
## Arguments
- `file_instance` (FileInstance) - File instance record with mime_type and size
- `options` (keyword) - Options for header generation
- `:cache_control` - Cache-Control header (default: "max-age=31536000, public, immutable")
- `:etag` - Include ETag header (default: true)
## Returns
Keyword list of HTTP headers.
## Example
iex> headers = PhoenixKit.Modules.Storage.FileServer.http_headers(file_instance)
iex> headers[:content_type]
"image/jpeg"
iex> headers[:cache_control]
"max-age=31536000, public, immutable"
"""
def http_headers(file_instance, options \\ []) do
cache_control =
Keyword.get(options, :cache_control, "max-age=31536000, public, immutable")
include_etag = Keyword.get(options, :etag, true)
headers = [
content_type: file_instance.mime_type,
content_length: file_instance.size,
cache_control: cache_control,
x_sendfile: :disabled
]
if include_etag do
Keyword.put(headers, :etag, "\"#{file_instance.checksum}\"")
else
headers
end
end
@doc """
Handle range request for partial file serving (video streaming).
Parses Range header and returns start/end positions for 206 Partial Content response.
## Arguments
- `range_header` (binary) - Range header value (e.g., "bytes=0-1023")
- `file_size` (integer) - Total file size in bytes
## Returns
- `{:ok, start, end, headers}` - Valid range with response headers
- `{:error, :invalid_range}` - Malformed range header
- `{:error, :range_out_of_bounds}` - Range exceeds file size
## Example
iex> PhoenixKit.Modules.Storage.FileServer.parse_range_header(
...> "bytes=0-1023",
...> 5000
...> )
{:ok, 0, 1023, [content_range: "bytes 0-1023/5000"]}
iex> PhoenixKit.Modules.Storage.FileServer.parse_range_header(
...> "bytes=1000-",
...> 5000
...> )
{:ok, 1000, 4999, [content_range: "bytes 1000-4999/5000"]}
"""
def parse_range_header(range_header, file_size) when is_binary(range_header) do
case String.split(range_header, "=") do
["bytes", range_spec] ->
parse_range_spec(range_spec, file_size)
_ ->
{:error, :invalid_range}
end
end
def parse_range_header(nil, _file_size) do
:no_range
end
@doc """
Check if a file location is accessible (has a path).
Verifies that a file location record contains a valid storage path.
## Arguments
- `location` (FileLocation) - File location record with preloaded bucket
## Returns
Boolean indicating if location has a path.
"""
def location_accessible?(%FileLocation{path: path}) when is_binary(path) do
String.length(path) > 0
end
def location_accessible?(_), do: false
# Private Helpers
@doc false
defp location_priority(%FileLocation{} = location) do
# Sort by: bucket priority first (lower = higher priority), then location priority
bucket_priority = if location.bucket, do: location.bucket.priority, else: 999
location_priority = location.priority || 0
{bucket_priority, location_priority}
end
@doc false
defp parse_range_spec(range_spec, file_size) do
case String.split(range_spec, "-") do
[start_str, end_str] ->
start_pos = String.to_integer(start_str)
end_pos =
if String.length(end_str) > 0 do
String.to_integer(end_str)
else
file_size - 1
end
if start_pos >= 0 and end_pos >= start_pos and end_pos < file_size do
content_range = "bytes #{start_pos}-#{end_pos}/#{file_size}"
{:ok, start_pos, end_pos, content_range: content_range}
else
{:error, :range_out_of_bounds}
end
_ ->
{:error, :invalid_range}
end
rescue
ArgumentError ->
{:error, :invalid_range}
end
@doc false
defp get_repo do
PhoenixKit.Config.get_repo!()
end
end