Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multipart.add_file_content_with_name (issue #69) #71

Merged
merged 1 commit into from Oct 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/maxwell/adapter/hackney.ex
Expand Up @@ -15,7 +15,11 @@ if Code.ensure_loaded?(:hackney) do
format_response(result, conn)
end

def send_multipart(conn), do: send_direct(conn)
def send_multipart(conn) do
%Conn{req_body: {:multipart, multiparts}} = conn
{req_headers, req_body} = Util.multipart_encode(conn, multiparts)
send_direct(%Conn{conn | req_headers: req_headers, req_body: req_body})
end

def send_file(conn), do: send_direct(conn)

Expand Down
95 changes: 79 additions & 16 deletions lib/maxwell/multipart.ex
Expand Up @@ -9,9 +9,13 @@ defmodule Maxwell.Multipart do
@type disposition_t :: {String.t, params_t}
@type boundary_t :: String.t
@type name_t :: String.t
@type file_content_t :: binary
@type part_t :: {:file, Path.t}
| {:file, Path.t, headers_t}
| {:file, Path.t, disposition_t, headers_t}
| {:file_content, file_content_t, String.t}
| {:file_content, file_content_t, String.t, headers_t}
| {:file_content, file_content_t, String.t, disposition_t, headers_t}
| {:mp_mixed, String.t, boundary_t}
| {:mp_mixed_eof, boundary_t}
| {name_t, binary}
Expand Down Expand Up @@ -50,6 +54,28 @@ defmodule Maxwell.Multipart do
append_part(multipart, {:file, path, disposition, extra_headers})
end

@spec add_file_content(t, file_content_t, String.t) :: t
def add_file_content(multipart, file_content, filename) do
append_part(multipart, {:file_content, file_content, filename})
end

@spec add_file_content(t, file_content_t, String.t, headers_t) :: t
def add_file_content(multipart, file_content, filename, extra_headers) do
append_part(multipart, {:file_content, file_content, filename, extra_headers})
end

@spec add_file_content(t, file_content_t, String.t, disposition_t, headers_t) :: t
def add_file_content(multipart, file_content, filename, disposition, extra_headers) do
append_part(multipart, {:file_content, file_content, filename, disposition, extra_headers})
end

@spec add_file_content_with_name(t, file_content_t, String.t, String.t) :: t
@spec add_file_content_with_name(t, file_content_t, String.t, String.t, headers_t) :: t
def add_file_content_with_name(multipart, file_content, filename, name, extra_headers \\ []) do
disposition = {"form-data", [{"name", name}, {"filename", filename}]}
append_part(multipart, {:file_content, file_content, filename, disposition, extra_headers})
end

@spec add_field(t, String.t, binary) :: t
def add_field(multipart, name, value) when is_binary(name) and is_binary(value) do
append_part(multipart, {name, value})
Expand Down Expand Up @@ -78,11 +104,14 @@ defmodule Maxwell.Multipart do
1. `{:file, path}`
2. `{:file, path, extra_headers}`
3. `{:file, path, disposition, extra_headers}`
4. `{:mp_mixed, name, mixed_boundary}`
5. `{:mp_mixed_eof, mixed_boundary}`
6. `{name, bin_data}`
7. `{name, bin_data, extra_headers}`
8. `{name, bin_data, disposition, extra_headers}`
4. `{:file_content, file_content, filename}`
5. `{:file_content, file_content, filename, extra_headers}`
6. `{:file_content, file_content, filename, disposition, extra_headers}`
7. `{:mp_mixed, name, mixed_boundary}`
8. `{:mp_mixed_eof, mixed_boundary}`
9. `{name, bin_data}`
10. `{name, bin_data, extra_headers}`
11. `{name, bin_data, disposition, extra_headers}`

Returns `{body_binary, size}`

Expand All @@ -95,14 +124,17 @@ defmodule Maxwell.Multipart do
* `boundary` - multipart boundary.
* `parts` - receives lists list's member format:

1. `{:file, path}`
2. `{:file, path, extra_headers}`
3. `{:file, path, disposition, extra_headers}`
4. `{:mp_mixed, name, mixed_boundary}`
5. `{:mp_mixed_eof, mixed_boundary}`
6. `{name, bin_data}`
7. `{name, bin_data, extra_headers}`
8. `{name, bin_data, disposition, extra_headers}`
1. `{:file, path}`
2. `{:file, path, extra_headers}`
3. `{:file, path, disposition, extra_headers}`
4. `{:file_content, file_content, filename}`
5. `{:file_content, file_content, filename, extra_headers}`
6. `{:file_content, file_content, filename, disposition, extra_headers}`
7. `{:mp_mixed, name, mixed_boundary}`
8. `{:mp_mixed_eof, mixed_boundary}`
9. `{name, bin_data}`
10. `{name, bin_data, extra_headers}`
11. `{name, bin_data, disposition, extra_headers}`

"""
@spec encode_form(boundary :: boundary_t, parts :: [part_t]) :: {boundary_t, integer}
Expand Down Expand Up @@ -132,8 +164,8 @@ defmodule Maxwell.Multipart do
"""
@spec len_mp_stream(boundary :: boundary_t, parts :: [part_t]) :: integer
def len_mp_stream(boundary, parts) do
size = Enum.reduce(parts, 0, fn(
{:file, path}, acc_size) ->
size = Enum.reduce(parts, 0, fn
({:file, path}, acc_size) ->
{mp_header, len} = mp_file_header(%{path: path}, boundary)
acc_size + byte_size(mp_header) + len + @eof_size
({:file, path, extra_headers}, acc_size) ->
Expand All @@ -143,6 +175,16 @@ defmodule Maxwell.Multipart do
file = %{path: path, extra_headers: extra_headers, disposition: disposition}
{mp_header, len} = mp_file_header(file, boundary)
acc_size + byte_size(mp_header) + len + @eof_size
({:file_content, file_content, filename}, acc_size) ->
{mp_header, len} = mp_file_header(%{path: filename, filesize: byte_size(file_content)}, boundary)
acc_size + byte_size(mp_header) + len + @eof_size
({:file_content, file_content, filename, extra_headers}, acc_size) ->
{mp_header, len} = mp_file_header(%{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers}, boundary)
acc_size + byte_size(mp_header) + len + @eof_size
({:file_content, file_content, filename, disposition, extra_headers}, acc_size) ->
file = %{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers, disposition: disposition}
{mp_header, len} = mp_file_header(file, boundary)
acc_size + byte_size(mp_header) + len + @eof_size
({:mp_mixed, name, mixed_boundary}, acc_size) ->
{mp_header, _} = mp_mixed_header(name, mixed_boundary)
acc_size + byte_size(mp_header) + @eof_size + byte_size(mp_eof(mixed_boundary))
Expand Down Expand Up @@ -190,6 +232,27 @@ defmodule Maxwell.Multipart do
encode_form(parts, boundary, acc, acc_size)
end

defp encode_form([{:file_content, file_content, filename}|parts], boundary, acc, acc_size) do
{mp_header, len} = mp_file_header(%{path: filename, filesize: byte_size(file_content)}, boundary)
acc_size = acc_size + byte_size(mp_header) + len + @eof_size
acc = acc <> mp_header <> file_content <> "\r\n"
encode_form(parts, boundary, acc, acc_size)
end
defp encode_form([{:file_content, file_content, filename, extra_headers}|parts], boundary, acc, acc_size) do
file = %{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers}
{mp_header, len} = mp_file_header(file, boundary)
acc_size = acc_size + byte_size(mp_header) + len + @eof_size
acc = acc <> mp_header <> file_content <> "\r\n"
encode_form(parts, boundary, acc, acc_size)
end
defp encode_form([{:file_content, file_content, filename, disposition, extra_headers}|parts], boundary, acc, acc_size) do
file = %{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers, disposition: disposition}
{mp_header, len} = mp_file_header(file, boundary)
acc_size = acc_size + byte_size(mp_header) + len + @eof_size
acc = acc <> mp_header <> file_content <> "\r\n"
encode_form(parts, boundary, acc, acc_size)
end

defp encode_form([{:mp_mixed, name, mixed_boundary}|parts], boundary, acc, acc_size) do
{mp_header, _} = mp_mixed_header(name, mixed_boundary)
acc_size = acc_size + byte_size(mp_header) + @eof_size
Expand Down Expand Up @@ -228,7 +291,7 @@ defmodule Maxwell.Multipart do
file_name = path |> :filename.basename |> to_string
{disposition, params} = file[:disposition] || {"form-data", [{"name", "\"file\""}, {"filename", "\"" <> file_name <> "\""}]}
ctype = :mimerl.filename(path)
len = :filelib.file_size(path)
len = file[:filesize] || :filelib.file_size(path)

extra_headers = file[:extra_headers] || []
extra_headers = extra_headers |> Enum.map(fn({k, v}) -> {String.downcase(k), v} end)
Expand Down
92 changes: 92 additions & 0 deletions test/maxwell/multipart_test.exs
Expand Up @@ -57,6 +57,42 @@ defmodule Maxwell.MultipartTest do
assert String.replace(body, boundary, "") == "--\r\ncontent-length: 47\r\ncontent-disposition: form-data; name=content; filename=test/maxwell/multipart_test_file.sh\r\ncontent-type: image/jpeg\r\n\r\n#!/usr/bin/env bash\necho \"test multipart file\"\n\r\n----\r\n"
end

test "File Content base" do
boundary = Multipart.new_boundary
filename = "test.sh"
file_content = "xxx"
{body, size} = Multipart.encode_form(boundary, [{:file_content, file_content, filename}])
assert size == 219
assert String.starts_with?(body, "--" <> boundary) == true
assert String.ends_with?(body, boundary <> "--\r\n") == true
assert String.replace(body, boundary, "") == "--\r\ncontent-length: 3\r\ncontent-disposition: form-data; name=\"file\"; filename=\"test.sh\"\r\ncontent-type: application/x-sh\r\n\r\nxxx\r\n----\r\n"
end

test "File Content ExtraHeaders" do
boundary = Multipart.new_boundary
filename = "test.sh"
file_content = "xxx"
extra_headers = [{"Content-Type", "image/jpeg"}]
{body, size} = Multipart.encode_form(boundary, [{:file_content, file_content, filename, extra_headers}])
assert size == 213
assert String.starts_with?(body, "--" <> boundary) == true
assert String.ends_with?(body, boundary <> "--\r\n") == true
assert String.replace(body, boundary, "") == "--\r\ncontent-length: 3\r\ncontent-disposition: form-data; name=\"file\"; filename=\"test.sh\"\r\ncontent-type: image/jpeg\r\n\r\nxxx\r\n----\r\n"
end

test "File Content Disposition" do
boundary = Multipart.new_boundary
filename = "test.sh"
file_content = "xxx"
extra_headers = [{"Content-Type", "image/jpeg"}]
disposition = {'form-data', [{"name", "content"}, {"filename", filename}]}
{body, size} = Multipart.encode_form(boundary, [{:file_content, file_content, filename, disposition, extra_headers}])
assert size == 212
assert String.starts_with?(body, "--" <> boundary) == true
assert String.ends_with?(body, boundary <> "--\r\n") == true
assert String.replace(body, boundary, "") == "--\r\ncontent-length: 3\r\ncontent-disposition: form-data; name=content; filename=test.sh\r\ncontent-type: image/jpeg\r\n\r\nxxx\r\n----\r\n"
end

test "mp_mixed name mixedboudnary" do
boundary = Multipart.new_boundary
name = "mp_mixed_test_name"
Expand Down Expand Up @@ -149,6 +185,33 @@ defmodule Maxwell.MultipartTest do
assert size == 239
end

test "file_content content filename stream len" do
boundary = Multipart.new_boundary
filename = "test.sh"
file_content = "xxx"
size = Multipart.len_mp_stream(boundary, [{:file_content, file_content, filename}])
assert size == 219
end

test "file_content content filename extra_headers stream len" do
boundary = Multipart.new_boundary
extra_headers = [{"Content-Type", "image/jpeg"}]
filename = "test.sh"
file_content = "xxx"
size = Multipart.len_mp_stream(boundary, [{:file_content, file_content, filename, extra_headers}])
assert size == 213
end

test "file_content content filename disposition extra_headers stream len" do
boundary = Multipart.new_boundary
extra_headers = [{"Content-Type", "image/jpeg"}]
filename = "test.sh"
file_content = "xxx"
disposition = {"form-data", [{"name", "content"}]}
size = Multipart.len_mp_stream(boundary, [{:file_content, file_content, filename, disposition, extra_headers}])
assert size == 194
end

test "mp_mixed stream len" do
boundary = Multipart.new_boundary
mixed_boundary = Multipart.new_boundary
Expand Down Expand Up @@ -259,6 +322,35 @@ defmodule Maxwell.MultipartTest do
== {:multipart, [{:file, "test.png", {"form-data", [{"name", "media"}, {"filename", "test.png"}]}, headers}]}
end

test "add_file_content/3 should add a file_content part" do
assert Multipart.new |> Multipart.add_file_content("xxx", "test.txt")
== {:multipart, [{:file_content, "xxx", "test.txt"}]}
end

test "add_file_content/4 should add a file_content part with headers" do
headers = [{"content-type", "image/png"}]
assert Multipart.new |> Multipart.add_file_content("xxx", "test.png", headers)
== {:multipart, [{:file_content, "xxx", "test.png", headers}]}
end

test "add_file_content/5 should add a file part with disposition and headers" do
disposition = {"form-data", [{"name", "content"}, {"testname", "name"}]}
headers = [{"content-type", "image/png"}]
assert Multipart.new |> Multipart.add_file_content("xxx", "test.png", disposition, headers)
== {:multipart, [{:file_content, "xxx", "test.png", disposition, headers}]}
end

test "add_file_content_with_name/4 should add a file_content with name" do
assert Multipart.new |> Multipart.add_file_content_with_name("xxx", "test.png", "media")
== {:multipart, [{:file_content, "xxx", "test.png", {"form-data", [{"name", "media"}, {"filename", "test.png"}]}, []}]}
end

test "add_file_content_with_name/5 should add a file_content with name and headers" do
headers = [{"content-type", "image/png"}]
assert Multipart.new |> Multipart.add_file_content_with_name("xxx", "test.png", "media", headers)
== {:multipart, [{:file_content, "xxx", "test.png", {"form-data", [{"name", "media"}, {"filename", "test.png"}]}, headers}]}
end

test "add_field/3 should add a data part" do
assert Multipart.new |> Multipart.add_field("key", "value")
== {:multipart, [{"key", "value"}]}
Expand Down