/__w/shurbej/shurbej/_build/test/cover/aggregate/shurbej_http_upload.html

1 -module(shurbej_http_upload).
2 -export([init/2]).
3
4 %% Handles the actual file upload at /upload/:upload_key.
5 %% Zotero may send as application/x-www-form-urlencoded (url-encoded bytes),
6 %% multipart/form-data (file in a "file" part), or raw octet-stream.
7 init(Req0, State) ->
8 18 case cowboy_req:method(Req0) of
9 <<"POST">> ->
10 17 UploadKey = cowboy_req:binding(upload_key, Req0),
11 17 case shurbej_files:get_pending(UploadKey) of
12 {ok, Meta} ->
13 16 case read_file_data(Req0) of
14 {ok, Body, Req1} ->
15 16 handle_store(UploadKey, Meta, Body, Req1, State);
16 {error, too_large, Req1} ->
17
:-(
Req = shurbej_http_common:error_response(413,
18 <<"Uploaded file exceeds max size">>, Req1),
19
:-(
{ok, Req, State}
20 end;
21 {error, not_found} ->
22 1 Req = shurbej_http_common:error_response(404, <<"Unknown upload key">>, Req0),
23 1 {ok, Req, State}
24 end;
25 _ ->
26 1 Req = shurbej_http_common:error_response(405, <<"Method not allowed">>, Req0),
27 1 {ok, Req, State}
28 end.
29
30 handle_store(UploadKey, Meta, Body, Req1, State) ->
31 16 case shurbej_files:store(UploadKey, Meta, Body) of
32 ok ->
33 15 Req = cowboy_req:reply(201, #{}, <<>>, Req1),
34 15 {ok, Req, State};
35 {error, md5_mismatch} ->
36 1 Req = shurbej_http_common:error_response(412,
37 <<"Uploaded file MD5 does not match expected hash">>, Req1),
38 1 {ok, Req, State};
39 {error, zip_too_large} ->
40
:-(
Req = shurbej_http_common:error_response(413,
41 <<"Decompressed upload exceeds max size">>, Req1),
42
:-(
{ok, Req, State};
43 {error, Reason} ->
44
:-(
logger:error("File storage error: ~p", [Reason]),
45
:-(
Req = shurbej_http_common:error_response(500,
46 <<"File storage error">>, Req1),
47
:-(
{ok, Req, State}
48 end.
49
50 %% Read file data, handling different content-types.
51 %% Zotero sends raw bytes with Content-Type: application/x-www-form-urlencoded
52 %% (not actually url-encoded) when prefix/suffix are empty.
53 %% For multipart, extract the "file" part.
54 read_file_data(Req) ->
55 16 Max = max_upload_bytes(),
56 16 case cowboy_req:header(<<"content-type">>, Req) of
57 <<"multipart/form-data", _/binary>> ->
58 1 read_multipart_file(Req, Max);
59 _ ->
60 15 read_full_body(Req, [], 0, Max)
61 end.
62
63 %% Read multipart body, extract the "file" part.
64 read_multipart_file(Req0, Max) ->
65 1 case cowboy_req:read_part(Req0) of
66 {ok, Headers, Req1} ->
67 1 case cow_multipart:form_data(Headers) of
68 {file, <<"file">>, _Filename, _CT} ->
69 1 read_part_body(Req1, [], 0, Max);
70 _ ->
71 %% Skip non-file parts; still enforce the cap so an
72 %% attacker can't stream unbounded data in a junk part.
73
:-(
case read_part_body(Req1, [], 0, Max) of
74
:-(
{ok, _Skip, Req2} -> read_multipart_file(Req2, Max);
75
:-(
{error, _, _} = Err -> Err
76 end
77 end;
78 {done, Req1} ->
79
:-(
{ok, <<>>, Req1}
80 end.
81
82 %% Accumulate chunks, flatten once at the end. Reject if total exceeds Max.
83 read_part_body(Req0, Acc, Size, Max) ->
84 1 case cowboy_req:read_part_body(Req0, #{length => 8_000_000, period => 30000}) of
85 {ok, Body, Req} ->
86 1 NewSize = Size + byte_size(Body),
87 1 case NewSize > Max of
88
:-(
true -> {error, too_large, Req};
89 1 false -> {ok, iolist_to_binary(lists:reverse([Body | Acc])), Req}
90 end;
91 {more, Body, Req} ->
92
:-(
NewSize = Size + byte_size(Body),
93
:-(
case NewSize > Max of
94
:-(
true -> {error, too_large, Req};
95
:-(
false -> read_part_body(Req, [Body | Acc], NewSize, Max)
96 end
97 end.
98
99 read_full_body(Req0, Acc, Size, Max) ->
100 15 case cowboy_req:read_body(Req0, #{length => 8_000_000, period => 30000}) of
101 {ok, Body, Req} ->
102 15 NewSize = Size + byte_size(Body),
103 15 case NewSize > Max of
104
:-(
true -> {error, too_large, Req};
105 15 false -> {ok, iolist_to_binary(lists:reverse([Body | Acc])), Req}
106 end;
107 {more, Body, Req} ->
108
:-(
NewSize = Size + byte_size(Body),
109
:-(
case NewSize > Max of
110
:-(
true -> {error, too_large, Req};
111
:-(
false -> read_full_body(Req, [Body | Acc], NewSize, Max)
112 end
113 end.
114
115 max_upload_bytes() ->
116 16 application:get_env(shurbej, max_upload_bytes, 100 * 1024 * 1024).
Line Hits Source