Skip to content

Commit 3bd1c57

Browse files
committed
Accept Streams as attachments
Add support for uploading streams to S3.
1 parent 8b058e5 commit 3bd1c57

File tree

4 files changed

+65
-9
lines changed

4 files changed

+65
-9
lines changed

lib/waffle/actions/store.ex

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Waffle.Actions.Store do
1212
* A map with a filename and path keys (eg, a `%Plug.Upload{}`)
1313
1414
* A map with a filename and binary keys (eg, `%{filename: "image.png", binary: <<255,255,255,...>>}`)
15+
* A map with a filename and stream keys (eg, `%{filename: "image.png", stream: %Stream{...}}`)
1516
1617
* A two-element tuple consisting of one of the above file formats as well as a scope map
1718
@@ -50,11 +51,13 @@ defmodule Waffle.Actions.Store do
5051
end
5152
end
5253

53-
def store(definition, {file, scope}) when is_binary(file) or is_map(file) do
54+
def store(definition, {file, scope})
55+
when is_binary(file) or is_map(file) do
5456
put(definition, {Waffle.File.new(file, definition), scope})
5557
end
5658

57-
def store(definition, filepath) when is_binary(filepath) or is_map(filepath) do
59+
def store(definition, filepath)
60+
when is_binary(filepath) or is_map(filepath) do
5861
store(definition, {filepath, nil})
5962
end
6063

lib/waffle/file.ex

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Waffle.File do
22
@moduledoc false
33

4-
defstruct [:path, :file_name, :binary, :is_tempfile?]
4+
defstruct [:path, :file_name, :binary, :is_tempfile?, :stream]
55

66
def generate_temporary_path(item \\ nil) do
77
do_generate_temporary_path(item)
@@ -92,6 +92,13 @@ defmodule Waffle.File do
9292
end
9393
end
9494

95+
#
96+
# Handle a stream
97+
#
98+
def new(%{filename: filename, stream: stream}, _definition) when is_struct(stream) do
99+
%Waffle.File{stream: stream, file_name: Path.basename(filename)}
100+
end
101+
95102
#
96103
# Support functions
97104
#

lib/waffle/storage/s3.ex

+43-6
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,41 @@ defmodule Waffle.Storage.S3 do
192192
end
193193
end
194194

195+
# If the file is a stream, send it to AWS as a multi-part upload
196+
defp do_put(file = %Waffle.File{stream: file_stream}, {s3_bucket, s3_key, s3_options})
197+
when is_struct(file_stream) do
198+
file_stream
199+
|> chunk_stream()
200+
|> S3.upload(s3_bucket, s3_key, s3_options)
201+
|> ExAws.request()
202+
|> case do
203+
{:ok, %{status_code: 200}} -> {:ok, file.file_name}
204+
{:ok, :done} -> {:ok, file.file_name}
205+
{:error, error} -> {:error, error}
206+
end
207+
rescue
208+
e in ExAws.Error ->
209+
Logger.error(inspect(e))
210+
Logger.error(e.message)
211+
{:error, :invalid_bucket}
212+
end
213+
195214
# Stream the file and upload to AWS as a multi-part upload
196215
defp do_put(file, {s3_bucket, s3_key, s3_options}) do
197216
file.path
198217
|> Upload.stream_file()
199218
|> S3.upload(s3_bucket, s3_key, s3_options)
200219
|> ExAws.request()
201220
|> case do
202-
{:ok, %{status_code: 200}} -> {:ok, file.file_name}
203-
{:ok, :done} -> {:ok, file.file_name}
204-
{:error, error} -> {:error, error}
205-
end
221+
{:ok, %{status_code: 200}} -> {:ok, file.file_name}
222+
{:ok, :done} -> {:ok, file.file_name}
223+
{:error, error} -> {:error, error}
224+
end
206225
rescue
207226
e in ExAws.Error ->
208227
Logger.error(inspect(e))
209-
Logger.error(e.message)
210-
{:error, :invalid_bucket}
228+
Logger.error(e.message)
229+
{:error, :invalid_bucket}
211230
end
212231

213232
defp build_url(definition, version, file_and_scope, _options) do
@@ -264,4 +283,22 @@ defmodule Waffle.Storage.S3 do
264283

265284
defp parse_bucket({:system, env_var}) when is_binary(env_var), do: System.get_env(env_var)
266285
defp parse_bucket(name), do: name
286+
287+
defp chunk_stream(stream, chunk_size \\ 5 * 1024 * 1024) do
288+
Stream.chunk_while(
289+
stream,
290+
"",
291+
fn element, acc ->
292+
if String.length(acc) >= chunk_size do
293+
{:cont, acc, element}
294+
else
295+
{:cont, acc <> element}
296+
end
297+
end,
298+
fn
299+
[] -> {:cont, []}
300+
acc -> {:cont, acc, []}
301+
end
302+
)
303+
end
267304
end

test/storage/s3_test.exs

+9
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ defmodule WaffleTest.Storage.S3 do
218218
delete_and_assert_not_found(DummyDefinition, "image.png")
219219
end
220220

221+
@tag :s3
222+
@tag timeout: 15_000
223+
test "public put stream" do
224+
img_map = %{filename: "image.png", stream: File.stream!(@img)}
225+
assert {:ok, "image.png"} == DummyDefinition.store(img_map)
226+
assert_public(DummyDefinition, "image.png")
227+
delete_and_assert_not_found(DummyDefinition, "image.png")
228+
end
229+
221230
@tag :s3
222231
@tag timeout: 15_000
223232
test "private put and signed get" do

0 commit comments

Comments
 (0)