Skip to content

Commit 1d94114

Browse files
authored
Merge branch 'main' into feature/belongs-to-resource-attribute-type
2 parents 2fdc326 + 7c00dd5 commit 1d94114

11 files changed

Lines changed: 510 additions & 61 deletions

File tree

lib/ash_storage/changes/attach.ex

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ defmodule AshStorage.Changes.Attach do
1313
end
1414

1515
@impl true
16-
def change(changeset, opts, _context) do
16+
def change(changeset, opts, context) do
1717
attachment_name = opts[:attachment_name]
18+
context_opts = Ash.Context.to_opts(context)
1819

1920
changeset
2021
|> Ash.Changeset.before_action(fn changeset ->
2122
record = changeset.data
2223
resource = record.__struct__
2324

24-
case do_attach(record, resource, attachment_name, changeset) do
25+
case do_attach(record, resource, attachment_name, changeset, context_opts) do
2526
{:ok, attrs_to_write, attach_context} ->
2627
changeset
2728
|> Ash.Changeset.force_change_attributes(attrs_to_write)
@@ -33,15 +34,22 @@ defmodule AshStorage.Changes.Attach do
3334
end)
3435
|> Ash.Changeset.after_action(fn changeset, record ->
3536
case changeset.context[:__ash_storage_attach__] do
36-
%{blob: blob, attachment_def: attachment_def, service_mod: service_mod, ctx: ctx} =
37-
context ->
37+
%{
38+
blob: blob,
39+
attachment_def: attachment_def,
40+
service_mod: service_mod,
41+
ctx: ctx,
42+
context_opts: context_opts
43+
} = attach_context ->
3844
resource = record.__struct__
3945

40-
with {:ok, _} <- maybe_replace_existing(record, attachment_def, service_mod, ctx),
41-
{:ok, attachment} <- create_attachment(record, attachment_def, blob),
46+
with {:ok, _} <-
47+
maybe_replace_existing(record, attachment_def, service_mod, ctx, context_opts),
48+
{:ok, attachment} <-
49+
create_attachment(record, attachment_def, blob, context_opts),
4250
:ok <- run_eager_variants(blob, attachment_def, resource),
4351
{:ok, blob} <- store_oban_variants(blob, attachment_def, resource) do
44-
if context[:has_oban_analyzers?] do
52+
if attach_context[:has_oban_analyzers?] do
4553
AshOban.run_trigger(blob, :run_pending_analyzers)
4654
end
4755

@@ -63,7 +71,7 @@ defmodule AshStorage.Changes.Attach do
6371
end)
6472
end
6573

66-
defp do_attach(record, resource, attachment_name, changeset) do
74+
defp do_attach(record, resource, attachment_name, changeset, context_opts) do
6775
io = Ash.Changeset.get_argument(changeset, :io)
6876
filename = Ash.Changeset.get_argument(changeset, :filename)
6977

@@ -77,7 +85,7 @@ defmodule AshStorage.Changes.Attach do
7785
ctx = build_context(service_opts, resource, attachment_def, changeset)
7886

7987
with {:ok, blob} <-
80-
upload_and_create_blob(resource, service_mod, ctx, io,
88+
upload_and_create_blob(resource, service_mod, ctx, io, context_opts,
8189
filename: filename,
8290
content_type: content_type,
8391
metadata: metadata
@@ -89,6 +97,7 @@ defmodule AshStorage.Changes.Attach do
8997
attachment_def: attachment_def,
9098
service_mod: service_mod,
9199
ctx: ctx,
100+
context_opts: context_opts,
92101
has_oban_analyzers?: has_oban_analyzers?(attachment_def)
93102
}}
94103
end
@@ -234,7 +243,7 @@ defmodule AshStorage.Changes.Attach do
234243
end
235244
end
236245

237-
defp upload_and_create_blob(resource, service_mod, ctx, io, opts) do
246+
defp upload_and_create_blob(resource, service_mod, ctx, io, context_opts, opts) do
238247
filename = Keyword.fetch!(opts, :filename)
239248
content_type = Keyword.get(opts, :content_type, "application/octet-stream")
240249
metadata = Keyword.get(opts, :metadata, %{})
@@ -244,11 +253,10 @@ defmodule AshStorage.Changes.Attach do
244253
checksum = :crypto.hash(:md5, data) |> Base.encode64()
245254
byte_size = byte_size(data)
246255

247-
with :ok <- service_mod.upload(key, data, ctx) do
256+
with {:ok, extra_blob_attrs} <- normalize_upload(service_mod.upload(key, data, ctx)) do
248257
blob_resource = Info.storage_blob_resource!(resource)
249258

250-
Ash.create(
251-
blob_resource,
259+
blob_attrs =
252260
%{
253261
key: key,
254262
filename: filename,
@@ -258,12 +266,17 @@ defmodule AshStorage.Changes.Attach do
258266
service_name: service_mod,
259267
service_opts: persistable_service_opts(service_mod, ctx.service_opts),
260268
metadata: metadata
261-
},
262-
action: :create
263-
)
269+
}
270+
|> Map.merge(extra_blob_attrs)
271+
272+
Ash.create(blob_resource, blob_attrs, Keyword.merge(context_opts, action: :create))
264273
end
265274
end
266275

276+
defp normalize_upload(:ok), do: {:ok, %{}}
277+
defp normalize_upload({:ok, attrs}) when is_map(attrs), do: {:ok, attrs}
278+
defp normalize_upload({:error, _} = error), do: error
279+
267280
# -- IO helpers --
268281

269282
defp read_io(%Ash.Type.File{} = file) do
@@ -325,17 +338,24 @@ defmodule AshStorage.Changes.Attach do
325338
# -- Attachment helpers --
326339

327340
# sobelow_skip ["DOS.BinToAtom"]
328-
defp maybe_replace_existing(record, %{type: :one} = attachment_def, service_mod, ctx) do
329-
case find_attachments(record, attachment_def) do
341+
defp maybe_replace_existing(
342+
record,
343+
%{type: :one} = attachment_def,
344+
service_mod,
345+
ctx,
346+
context_opts
347+
) do
348+
case find_attachments(record, attachment_def, context_opts) do
330349
{:ok, []} -> {:ok, :noop}
331-
{:ok, existing} -> purge_attachments(existing, service_mod, ctx)
350+
{:ok, existing} -> purge_attachments(existing, service_mod, ctx, context_opts)
332351
end
333352
end
334353

335-
defp maybe_replace_existing(_record, %{type: :many}, _service_mod, _ctx), do: {:ok, :noop}
354+
defp maybe_replace_existing(_record, %{type: :many}, _service_mod, _ctx, _context_opts),
355+
do: {:ok, :noop}
336356

337357
# sobelow_skip ["DOS.BinToAtom"]
338-
defp create_attachment(record, attachment_def, blob) do
358+
defp create_attachment(record, attachment_def, blob, context_opts) do
339359
resource = record.__struct__
340360
attachment_resource = Info.storage_attachment_resource!(resource)
341361

@@ -367,11 +387,11 @@ defmodule AshStorage.Changes.Attach do
367387
}
368388
end
369389

370-
Ash.create(attachment_resource, params, action: :create)
390+
Ash.create(attachment_resource, params, Keyword.merge(context_opts, action: :create))
371391
end
372392

373393
# sobelow_skip ["DOS.BinToAtom"]
374-
defp find_attachments(record, attachment_def) do
394+
defp find_attachments(record, attachment_def, context_opts) do
375395
resource = record.__struct__
376396
attachment_resource = Info.storage_attachment_resource!(resource)
377397
record_id = Map.get(record, :id) |> to_string()
@@ -398,16 +418,19 @@ defmodule AshStorage.Changes.Attach do
398418
attachment_resource
399419
|> Ash.Query.filter(^filter)
400420
|> Ash.Query.load(:blob)
421+
|> Ash.Query.set_tenant(context_opts[:tenant])
401422
|> Ash.read()
402423
end
403424

404-
defp purge_attachments(attachments, service_mod, ctx) do
425+
defp purge_attachments(attachments, service_mod, ctx, context_opts) do
426+
destroy_opts = Keyword.merge(context_opts, action: :destroy, return_destroyed?: true)
427+
405428
Enum.reduce_while(attachments, {:ok, []}, fn att, {:ok, acc} ->
406429
blob = att.blob
407430

408431
with :ok <- service_mod.delete(blob.key, ctx),
409-
{:ok, _} <- Ash.destroy(att, action: :destroy, return_destroyed?: true),
410-
{:ok, _} <- Ash.destroy(blob, action: :destroy, return_destroyed?: true) do
432+
{:ok, _} <- Ash.destroy(att, destroy_opts),
433+
{:ok, _} <- Ash.destroy(blob, destroy_opts) do
411434
{:cont, {:ok, [att | acc]}}
412435
else
413436
{:error, error} -> {:halt, {:error, error}}

lib/ash_storage/changes/attach_blob.ex

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ defmodule AshStorage.Changes.AttachBlob do
4242

4343
# sobelow_skip ["DOS.BinToAtom"]
4444
@impl true
45-
def change(changeset, opts, _context) do
45+
def change(changeset, opts, context) do
4646
argument_name = opts[:argument]
4747
attachment_name = opts[:attachment]
4848

@@ -53,13 +53,16 @@ defmodule AshStorage.Changes.AttachBlob do
5353
{:ok, record}
5454
else
5555
resource = record.__struct__
56+
context_opts = Ash.Context.to_opts(context)
5657

5758
with {:ok, attachment_def} <- Info.attachment(resource, attachment_name),
5859
{:ok, {service_mod, service_opts}} <- resolve_service(resource, attachment_def),
5960
ctx = build_context(service_opts, resource, attachment_def, changeset),
60-
{:ok, blob} <- fetch_blob(resource, blob_id),
61-
{:ok, _} <- maybe_replace_existing(record, attachment_def, service_mod, ctx),
62-
{:ok, attachment} <- create_attachment(record, attachment_def, blob) do
61+
{:ok, blob} <- fetch_blob(resource, blob_id, context_opts),
62+
{:ok, _} <-
63+
maybe_replace_existing(record, attachment_def, service_mod, ctx, context_opts),
64+
{:ok, attachment} <-
65+
create_attachment(record, attachment_def, blob, context_opts) do
6366
record =
6467
record
6568
|> Ash.Resource.put_metadata(:"#{attachment_name}_blob", blob)
@@ -87,26 +90,33 @@ defmodule AshStorage.Changes.AttachBlob do
8790
)
8891
end
8992

90-
defp fetch_blob(resource, blob_id) do
93+
defp fetch_blob(resource, blob_id, context_opts) do
9194
blob_resource = Info.storage_blob_resource!(resource)
9295

93-
case Ash.get(blob_resource, blob_id) do
96+
case Ash.get(blob_resource, blob_id, context_opts) do
9497
{:ok, blob} -> {:ok, blob}
9598
{:error, _} -> {:error, :blob_not_found}
9699
end
97100
end
98101

99-
defp maybe_replace_existing(record, %{type: :one} = attachment_def, service_mod, ctx) do
100-
case find_attachments(record, attachment_def) do
102+
defp maybe_replace_existing(
103+
record,
104+
%{type: :one} = attachment_def,
105+
service_mod,
106+
ctx,
107+
context_opts
108+
) do
109+
case find_attachments(record, attachment_def, context_opts) do
101110
{:ok, []} -> {:ok, :noop}
102-
{:ok, existing} -> purge_attachments(existing, service_mod, ctx)
111+
{:ok, existing} -> purge_attachments(existing, service_mod, ctx, context_opts)
103112
end
104113
end
105114

106-
defp maybe_replace_existing(_record, %{type: :many}, _service_mod, _ctx), do: {:ok, :noop}
115+
defp maybe_replace_existing(_record, %{type: :many}, _service_mod, _ctx, _context_opts),
116+
do: {:ok, :noop}
107117

108118
# sobelow_skip ["DOS.BinToAtom"]
109-
defp create_attachment(record, attachment_def, blob) do
119+
defp create_attachment(record, attachment_def, blob, context_opts) do
110120
resource = record.__struct__
111121
attachment_resource = Info.storage_attachment_resource!(resource)
112122
record_id = Map.get(record, :id) |> to_string()
@@ -137,11 +147,11 @@ defmodule AshStorage.Changes.AttachBlob do
137147
}
138148
end
139149

140-
Ash.create(attachment_resource, params, action: :create)
150+
Ash.create(attachment_resource, params, Keyword.merge(context_opts, action: :create))
141151
end
142152

143153
# sobelow_skip ["DOS.BinToAtom"]
144-
defp find_attachments(record, attachment_def) do
154+
defp find_attachments(record, attachment_def, context_opts) do
145155
resource = record.__struct__
146156
attachment_resource = Info.storage_attachment_resource!(resource)
147157
record_id = Map.get(record, :id) |> to_string()
@@ -168,16 +178,19 @@ defmodule AshStorage.Changes.AttachBlob do
168178
attachment_resource
169179
|> Ash.Query.filter(^filter)
170180
|> Ash.Query.load(:blob)
181+
|> Ash.Query.set_tenant(context_opts[:tenant])
171182
|> Ash.read()
172183
end
173184

174-
defp purge_attachments(attachments, service_mod, ctx) do
185+
defp purge_attachments(attachments, service_mod, ctx, context_opts) do
186+
destroy_opts = Keyword.merge(context_opts, action: :destroy, return_destroyed?: true)
187+
175188
Enum.reduce_while(attachments, {:ok, []}, fn att, {:ok, acc} ->
176189
blob = att.blob
177190

178191
with :ok <- service_mod.delete(blob.key, ctx),
179-
{:ok, _} <- Ash.destroy(att, action: :destroy, return_destroyed?: true),
180-
{:ok, _} <- Ash.destroy(blob, action: :destroy, return_destroyed?: true) do
192+
{:ok, _} <- Ash.destroy(att, destroy_opts),
193+
{:ok, _} <- Ash.destroy(blob, destroy_opts) do
181194
{:cont, {:ok, [att | acc]}}
182195
else
183196
{:error, error} -> {:halt, {:error, error}}

lib/ash_storage/changes/handle_file_argument.ex

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,10 @@ defmodule AshStorage.Changes.HandleFileArgument do
294294
checksum = :crypto.hash(:md5, data) |> Base.encode64()
295295
byte_size = byte_size(data)
296296

297-
with :ok <- service_mod.upload(key, data, ctx) do
297+
with {:ok, extra_blob_attrs} <- normalize_upload(service_mod.upload(key, data, ctx)) do
298298
blob_resource = Info.storage_blob_resource!(resource)
299299

300-
Ash.create(
301-
blob_resource,
300+
blob_attrs =
302301
%{
303302
key: key,
304303
filename: filename,
@@ -308,12 +307,17 @@ defmodule AshStorage.Changes.HandleFileArgument do
308307
service_name: service_mod,
309308
service_opts: persistable_service_opts(service_mod, ctx.service_opts),
310309
metadata: %{}
311-
},
312-
action: :create
313-
)
310+
}
311+
|> Map.merge(extra_blob_attrs)
312+
313+
Ash.create(blob_resource, blob_attrs, action: :create)
314314
end
315315
end
316316

317+
defp normalize_upload(:ok), do: {:ok, %{}}
318+
defp normalize_upload({:ok, attrs}) when is_map(attrs), do: {:ok, attrs}
319+
defp normalize_upload({:error, _} = error), do: error
320+
317321
defp read_io(%Ash.Type.File{} = file) do
318322
{:ok, device} = Ash.Type.File.open(file, [:read, :binary])
319323
data = IO.binread(device, :eof)

lib/ash_storage/operations.ex

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,14 @@ defmodule AshStorage.Operations do
7272
{:ok, {service_mod, service_opts}} <- resolve_service(resource, attachment_def) do
7373
ctx = build_context(service_opts, resource, attachment_def, opts)
7474

75-
filename = Keyword.fetch!(opts, :filename)
76-
content_type = Keyword.get(opts, :content_type, "application/octet-stream")
77-
byte_size = Keyword.get(opts, :byte_size, 0)
78-
checksum = Keyword.get(opts, :checksum, "")
79-
metadata = Keyword.get(opts, :metadata, %{})
75+
{arg_opts, action_opts} =
76+
Keyword.split(opts, [:filename, :content_type, :byte_size, :checksum, :metadata])
77+
78+
filename = Keyword.fetch!(arg_opts, :filename)
79+
content_type = Keyword.get(arg_opts, :content_type, "application/octet-stream")
80+
byte_size = Keyword.get(arg_opts, :byte_size, 0)
81+
checksum = Keyword.get(arg_opts, :checksum, "")
82+
metadata = Keyword.get(arg_opts, :metadata, %{})
8083

8184
key = AshStorage.generate_key()
8285
blob_resource = Info.storage_blob_resource!(resource)
@@ -94,7 +97,7 @@ defmodule AshStorage.Operations do
9497
service_opts: persistable_service_opts(service_mod, service_opts),
9598
metadata: metadata
9699
},
97-
action: :create
100+
Keyword.merge(action_opts, action: :create)
98101
),
99102
{:ok, upload_info} <- service_mod.direct_upload(key, ctx) do
100103
{:ok, Map.put(upload_info, :blob, blob)}

lib/ash_storage/service.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ defmodule AshStorage.Service do
3232

3333
@doc """
3434
Upload a file to the storage service.
35+
36+
May return `:ok` or `{:ok, extra_blob_attrs}`. When a map is returned, its entries
37+
are merged into the blob record on creation. This allows wrapping services (e.g.
38+
encryption) to store per-file metadata such as encryption keys on the blob.
3539
"""
3640
@callback upload(key(), iodata() | File.Stream.t(), Context.t()) ::
37-
:ok | {:error, term()}
41+
:ok | {:ok, map()} | {:error, term()}
3842

3943
@doc """
4044
Download a file from the storage service.

0 commit comments

Comments
 (0)