Skip to content

Commit 5cf4759

Browse files
authored
Make S3 service configurable (#190)
1 parent 6dc6181 commit 5cf4759

File tree

23 files changed

+501
-605
lines changed

23 files changed

+501
-605
lines changed

.env.sample

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ BASE_URL=http://localhost:4000
33
# SECURE_COOKIE=false
44

55
DATABASE_URL=postgres://claper:claper@db:5432/claper
6-
SECRET_KEY_BASE=0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG # Generate with `mix phx.gen.secret`
6+
SECRET_KEY_BASE=0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG # Generate with `mix phx.gen.secret`
77
# ⚠️ Don't use this exact value for SECRET_KEY_BASE or someone would be able to sign a cookie with user_id=1 and log in as the admin!
88

99
# Storage configuration
@@ -12,10 +12,19 @@ PRESENTATION_STORAGE=local
1212
PRESENTATION_STORAGE_DIR=/app/uploads
1313
#MAX_FILE_SIZE_MB=15
1414

15-
#AWS_ACCESS_KEY_ID=xxx
16-
#AWS_SECRET_ACCESS_KEY=xxx
17-
#AWS_REGION=eu-west-3
18-
#AWS_PRES_BUCKET=xxx
15+
# The standard AWS environment variables
16+
#S3_ACCESS_KEY_ID=xxx
17+
#S3_SECRET_ACCESS_KEY=xxx
18+
#S3_REGION=eu-west-3
19+
#S3_BUCKET=xxx
20+
21+
# If you're using an alternative S3-compatible service, port optional
22+
#S3_SCHEME=https://
23+
#S3_HOST=www.example.com
24+
#S3_PORT=443
25+
26+
# If the public S3-compatible URL is different from the one used to write data
27+
#S3_PUBLIC_URL=https://www.example.com
1928

2029
# Mail configuration
2130

@@ -50,4 +59,4 @@ MAIL_FROM_NAME=Claper
5059
# OIDC_SCOPES="openid email profile"
5160
# OIDC_LOGO_URL=""
5261
# OIDC_PROPERTY_MAPPINGS="roles:custom_attributes.roles,organization:custom_attributes.organization"
53-
# OIDC_AUTO_REDIRECT_LOGIN=true
62+
# OIDC_AUTO_REDIRECT_LOGIN=true

assets/js/app.js

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -685,35 +685,7 @@ Hooks.CSVDownloader = {
685685
// Merge our custom hooks with the existing hooks
686686
Object.assign(Hooks, CustomHooks);
687687

688-
let Uploaders = {};
689-
690-
Uploaders.S3 = function (entries, onViewError) {
691-
entries.forEach((entry) => {
692-
let formData = new FormData();
693-
let { url, fields } = entry.meta;
694-
Object.entries(fields).forEach(([key, val]) => formData.append(key, val));
695-
formData.append("file", entry.file);
696-
let xhr = new XMLHttpRequest();
697-
onViewError(() => xhr.abort());
698-
xhr.onload = () =>
699-
xhr.status === 204 ? entry.progress(100) : entry.error();
700-
xhr.onerror = () => entry.error();
701-
xhr.upload.addEventListener("progress", (event) => {
702-
if (event.lengthComputable) {
703-
let percent = Math.round((event.loaded / event.total) * 100);
704-
if (percent < 100) {
705-
entry.progress(percent);
706-
}
707-
}
708-
});
709-
710-
xhr.open("POST", url, true);
711-
xhr.send(formData);
712-
});
713-
};
714-
715688
let liveSocket = new LiveSocket("/live", Socket, {
716-
uploaders: Uploaders,
717689
params: {
718690
_csrf_token: csrfToken,
719691
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,

charts/claper/templates/_env.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616
value: claper.co
1717
- name: DATABASE_URL
1818
value: postgresql://claper:claper@10.0.0.6:6432/claper
19-
- name: AWS_ACCESS_KEY_ID
19+
- name: S3_ACCESS_KEY_ID
2020
value: XXX
21-
- name: AWS_PRES_BUCKET
21+
- name: S3_BUCKET
2222
value: XXX
2323
- name: POOL_SIZE
2424
value: "20"
25-
- name: AWS_REGION
25+
- name: S3_REGION
2626
value: eu-west-3
27-
- name: AWS_SECRET_ACCESS_KEY
27+
- name: S3_SECRET_ACCESS_KEY
2828
value: XXX
2929
- name: PRESENTATION_STORAGE
3030
value: s3

config/runtime.exs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,39 @@ smtp_tls = get_var_from_path_or_env(config_dir, "SMTP_TLS", "always")
8686
smtp_auth = get_var_from_path_or_env(config_dir, "SMTP_AUTH", "always")
8787
smtp_port = get_int_from_path_or_env(config_dir, "SMTP_PORT", 25)
8888

89-
aws_access_key_id = get_var_from_path_or_env(config_dir, "AWS_ACCESS_KEY_ID", nil)
90-
aws_secret_access_key = get_var_from_path_or_env(config_dir, "AWS_SECRET_ACCESS_KEY", nil)
91-
aws_region = get_var_from_path_or_env(config_dir, "AWS_REGION", nil)
89+
storage = get_var_from_path_or_env(config_dir, "PRESENTATION_STORAGE", "local")
90+
if storage not in ["local", "s3"], do: raise("Invalid PRESENTATION_STORAGE value #{storage}")
91+
92+
s3_access_key_id = get_var_from_path_or_env(config_dir, "S3_ACCESS_KEY_ID")
93+
s3_secret_access_key = get_var_from_path_or_env(config_dir, "S3_SECRET_ACCESS_KEY")
94+
s3_region = get_var_from_path_or_env(config_dir, "S3_REGION")
95+
s3_bucket = get_var_from_path_or_env(config_dir, "S3_BUCKET")
96+
97+
if storage == "s3" and
98+
not Enum.all?([s3_access_key_id, s3_secret_access_key, s3_region, s3_bucket]) do
99+
raise(
100+
"S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION and S3_BUCKET required when PRESENTATION_STORAGE=s3"
101+
)
102+
end
103+
104+
s3_scheme = get_var_from_path_or_env(config_dir, "S3_SCHEME")
105+
s3_host = get_var_from_path_or_env(config_dir, "S3_HOST")
106+
s3_port = get_var_from_path_or_env(config_dir, "S3_PORT")
107+
108+
if storage == "s3" do
109+
if !!s3_scheme and !s3_host, do: "S3_HOST required if S3_SCHEME is set"
110+
if !s3_scheme and !!s3_host, do: "S3_SCHEME required if S3_HOST is set"
111+
end
112+
113+
s3_public_url =
114+
get_var_from_path_or_env(
115+
config_dir,
116+
"S3_PUBLIC_URL",
117+
if(s3_scheme && s3_host,
118+
do: s3_scheme <> s3_host <> if(s3_port, do: ":#{s3_port}", else: ""),
119+
else: "https://#{s3_bucket}.s3.#{s3_region}.amazonaws.com"
120+
)
121+
)
92122

93123
same_site_cookie = get_var_from_path_or_env(config_dir, "SAME_SITE_COOKIE", "Lax")
94124

@@ -176,9 +206,10 @@ config :claper,
176206

177207
config :claper, :presentations,
178208
max_file_size: max_file_size,
179-
storage: get_var_from_path_or_env(config_dir, "PRESENTATION_STORAGE", "local"),
180-
aws_bucket: get_var_from_path_or_env(config_dir, "AWS_PRES_BUCKET", nil),
181-
resolution: get_var_from_path_or_env(config_dir, "GS_JPG_RESOLUTION", "300x300")
209+
storage: storage,
210+
s3_bucket: s3_bucket,
211+
resolution: get_var_from_path_or_env(config_dir, "GS_JPG_RESOLUTION", "300x300"),
212+
s3_public_url: s3_public_url
182213

183214
config :claper, :mail,
184215
from: get_var_from_path_or_env(config_dir, "MAIL_FROM", "noreply@claper.co"),
@@ -227,9 +258,10 @@ case mail_transport do
227258
end
228259

229260
config :ex_aws,
230-
access_key_id: aws_access_key_id,
231-
secret_access_key: aws_secret_access_key,
232-
region: aws_region,
233-
normalize_path: false
261+
access_key_id: s3_access_key_id,
262+
secret_access_key: s3_secret_access_key,
263+
region: s3_region,
264+
normalize_path: false,
265+
s3: [scheme: s3_scheme, host: s3_host, port: s3_port]
234266

235267
config :swoosh, :api_client, Swoosh.ApiClient.Finch

lib/claper/presentations.ex

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,65 @@ defmodule Claper.Presentations do
3131
def get_presentation_files_by_hash(hash) when is_nil(hash),
3232
do: []
3333

34+
@doc """
35+
Returns a list of JPG slide URLs for a given presentation.
36+
37+
When a `Claper.Presentations.PresentationFile{}` struct is provided, the
38+
function builds the list of URLs programmatically from the `hash` and
39+
`length` fields.
40+
41+
When an integer or binary `hash` is provided, it queries the database for the
42+
associated presentation file and builds the list of URLs programmatically
43+
from that.
44+
45+
When `nil` is provided or when no presentation file is found for the given
46+
`hash`, it returns an empty list.
47+
"""
48+
def get_slide_urls(hash_or_presentation_file)
49+
50+
def get_slide_urls(nil), do: []
51+
52+
def get_slide_urls(hash) when is_integer(hash), do: get_slide_urls(to_string(hash))
53+
54+
def get_slide_urls(hash) when is_binary(hash) do
55+
case Repo.get_by(PresentationFile, hash: hash) do
56+
nil ->
57+
[]
58+
59+
presentation ->
60+
get_slide_urls(hash, presentation.length)
61+
end
62+
end
63+
64+
def get_slide_urls(%PresentationFile{} = presentation) do
65+
get_slide_urls(presentation.hash, presentation.length)
66+
end
67+
68+
@doc """
69+
Returns a list of JPG slide URLs for a given presentation `hash` and
70+
`length`. See also `get_slide_urls/1`.
71+
"""
72+
def get_slide_urls(hash, length) when is_binary(hash) and is_integer(length) do
73+
config = Application.get_env(:claper, :presentations)
74+
75+
case Keyword.fetch!(config, :storage) do
76+
"local" ->
77+
for index <- 1..length do
78+
"/uploads/#{hash}/#{index}.jpg"
79+
end
80+
81+
"s3" ->
82+
base_url = Keyword.fetch!(config, :s3_public_url)
83+
84+
for index <- 1..length do
85+
base_url <> "/presentations/#{hash}/#{index}.jpg"
86+
end
87+
88+
storage ->
89+
raise "Unrecognised presentations storage value #{storage}"
90+
end
91+
end
92+
3493
@doc """
3594
Creates a presentation_files.
3695

lib/claper/tasks/converter.ex

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ defmodule Claper.Tasks.Converter do
55
"""
66

77
alias Claper.Events
8-
alias ExAws.S3
98
alias Porcelain.Result
109

1110
@doc """
@@ -50,11 +49,11 @@ defmodule Claper.Tasks.Converter do
5049
)
5150
else
5251
stream =
53-
ExAws.S3.list_objects(get_aws_bucket(), prefix: "presentations/#{hash}")
52+
ExAws.S3.list_objects(get_s3_bucket(), prefix: "presentations/#{hash}")
5453
|> ExAws.stream!()
5554
|> Stream.map(& &1.key)
5655

57-
ExAws.S3.delete_all_objects(get_aws_bucket(), stream) |> ExAws.request()
56+
ExAws.S3.delete_all_objects(get_s3_bucket(), stream) |> ExAws.request()
5857
end
5958
end
6059

@@ -134,9 +133,9 @@ defmodule Claper.Tasks.Converter do
134133
IO.puts("Uploads #{f} to presentations/#{new_hash}/#{Path.basename(f)}")
135134

136135
f
137-
|> S3.Upload.stream_file()
138-
|> S3.upload(
139-
get_aws_bucket(),
136+
|> ExAws.S3.Upload.stream_file()
137+
|> ExAws.S3.upload(
138+
get_s3_bucket(),
140139
"presentations/#{new_hash}/#{Path.basename(f)}",
141140
acl: "public-read"
142141
)
@@ -187,8 +186,8 @@ defmodule Claper.Tasks.Converter do
187186
Application.get_env(:claper, :storage_dir)
188187
end
189188

190-
defp get_aws_bucket do
191-
Application.get_env(:claper, :presentations) |> Keyword.get(:aws_bucket)
189+
defp get_s3_bucket do
190+
Application.get_env(:claper, :presentations) |> Keyword.get(:s3_bucket)
192191
end
193192

194193
defp get_resolution do

lib/claper_web/live/event_live/manage.ex

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
defmodule ClaperWeb.EventLive.Manage do
22
use ClaperWeb, :live_view
33

4+
alias Claper.{Embeds, Forms, Polls, Presentations, Quizzes}
45
alias ClaperWeb.Presence
5-
alias Claper.Polls
6-
alias Claper.Forms
7-
alias Claper.Embeds
8-
alias Claper.Quizzes
96

107
@impl true
118
def mount(%{"code" => code}, session, socket) do

lib/claper_web/live/event_live/manage.html.heex

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -669,30 +669,22 @@
669669
id="slides-layout"
670670
class="flex overflow-x-auto w-full md:h-full"
671671
>
672-
<%= for index <- 0..max(0, @event.presentation_file.length-1) do %>
673-
<button
674-
id={"slide-preview-#{index}"}
675-
phx-click="current-page"
676-
phx-value-page={index}
677-
class="h-full w-full contents"
678-
>
679-
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
680-
<img
681-
class={"#{if @state.position==index, do: "border-4 border-primary-500" , else: "opacity-20" }
682-
transition-all object-contain"}
683-
src={"/uploads/#{@event.presentation_file.hash}/#{index+1}.jpg"}
684-
/>
685-
<% else %>
686-
<img
687-
class={"#{if @state.position==index, do: "border-4 border-primary-500" , else: "opacity-20" }
672+
<button
673+
:for={
674+
{src, index} <-
675+
Presentations.get_slide_urls(@event.presentation_file) |> Enum.with_index(0)
676+
}
677+
id={"slide-preview-#{index}"}
678+
phx-click="current-page"
679+
phx-value-page={index}
680+
class="h-full w-full contents"
681+
>
682+
<img
683+
src={src}
684+
class={"#{if @state.position==index, do: "border-4 border-primary-500" , else: "opacity-20" }
688685
transition-all object-contain"}
689-
src={"https://#{Application.get_env(:claper, :presentations) |>
690-
Keyword.get(:aws_bucket)}.s3.#{Application.get_env(:ex_aws,
691-
:region)}.amazonaws.com/presentations/#{@event.presentation_file.hash}/#{index+1}.jpg"}
692-
/>
693-
<% end %>
694-
</button>
695-
<% end %>
686+
/>
687+
</button>
696688
</div>
697689
<div
698690
:if={@event.presentation_file.length > 0}

lib/claper_web/live/event_live/presenter.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ defmodule ClaperWeb.EventLive.Presenter do
66
alias Claper.Polls.Poll
77
alias Claper.Forms.Form
88
alias Claper.Quizzes.Quiz
9+
alias Claper.Presentations
10+
911
@impl true
1012
def mount(%{"code" => code} = params, session, socket) do
1113
with %{"locale" => locale} <- session do

lib/claper_web/live/event_live/presenter.html.heex

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -216,21 +216,11 @@
216216
</div>
217217
<% end %>
218218
<div class={"#{if @current_embed, do: "hidden", else: ""} text-center"} id="slider">
219-
<%= for index <- 1..max(1, @event.presentation_file.length) do %>
220-
<%= if @event.presentation_file.length > 0 do %>
221-
<%= if Application.get_env(:claper, :presentations) |> Keyword.get(:storage) == "local" do %>
222-
<img
223-
class="max-h-screen w-auto!"
224-
src={"/uploads/#{@event.presentation_file.hash}/#{index}.jpg"}
225-
/>
226-
<% else %>
227-
<img
228-
class=" max-h-screen w-auto!"
229-
src={"https://#{Application.get_env(:claper, :presentations) |> Keyword.get(:aws_bucket)}.s3.#{Application.get_env(:ex_aws, :region)}.amazonaws.com/presentations/#{@event.presentation_file.hash}/#{index}.jpg"}
230-
/>
231-
<% end %>
232-
<% end %>
233-
<% end %>
219+
<img
220+
:for={src <- Presentations.get_slide_urls(@event.presentation_file)}
221+
src={src}
222+
class="max-h-screen w-auto!"
223+
/>
234224
</div>
235225
</div>
236226
</div>

0 commit comments

Comments
 (0)