diff options
author | Nick Vatamaniuc <vatamane@apache.org> | 2017-08-31 10:29:21 -0400 |
---|---|---|
committer | Nick Vatamaniuc <vatamane@apache.org> | 2017-09-07 17:52:47 -0400 |
commit | fde4f0d72f8298c7212296eb49fe03a97eb758f4 (patch) | |
tree | 159ccf12362821749df5f9e1c3a9e386b18fd9c7 | |
parent | ff6e5764c4d574fdd175f009cd7e35645d605a38 (diff) | |
download | couchdb-issue-769.tar.gz |
Implement attachment size limitsissue-769
Currently CouchDB has configurable single document body size limits, as well as
http request body limits, and this commit implements attachment size limit.
Maximum attachment size can be configured with:
```
[couchdb]
max_attachment_size = Bytes | infinity
```
`infinity` (i.e. no maximum) is the default value it also preserves the current
behavior.
Fixes #769
-rw-r--r-- | rel/overlay/etc/default.ini | 2 | ||||
-rw-r--r-- | src/chttpd/src/chttpd.erl | 2 | ||||
-rw-r--r-- | src/chttpd/test/chttpd_db_attachment_size_tests.erl | 206 | ||||
-rw-r--r-- | src/couch/src/couch_att.erl | 35 | ||||
-rw-r--r-- | src/couch/src/couch_doc.erl | 24 | ||||
-rw-r--r-- | src/couch/src/couch_httpd.erl | 4 | ||||
-rw-r--r-- | src/couch/test/couch_doc_tests.erl | 1 | ||||
-rw-r--r-- | src/couch/test/couchdb_attachments_tests.erl | 132 | ||||
-rw-r--r-- | src/couch_replicator/src/couch_replicator_api_wrap.erl | 4 | ||||
-rw-r--r-- | src/couch_replicator/test/couch_replicator_attachments_too_large.erl | 104 | ||||
-rw-r--r-- | src/fabric/src/fabric_doc_attachments.erl | 28 | ||||
-rw-r--r-- | src/fabric/src/fabric_doc_update.erl | 4 |
12 files changed, 526 insertions, 20 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index 30b2efa5c..fa1124d3d 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -43,6 +43,8 @@ changes_doc_ids_optimization_threshold = 100 ; Single documents that exceed this value in a bulk request will receive a ; too_large error. The max_http_request_size still takes precedence. ;single_max_doc_size = 1048576 +; Maximum attachment size. +; max_attachment_size = infinity [cluster] q=8 diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl index 425f95a6f..6be0d1848 100644 --- a/src/chttpd/src/chttpd.erl +++ b/src/chttpd/src/chttpd.erl @@ -897,6 +897,8 @@ error_info({missing_stub, Reason}) -> {412, <<"missing_stub">>, Reason}; error_info(request_entity_too_large) -> {413, <<"too_large">>, <<"the request entity is too large">>}; +error_info({request_entity_too_large, {attachment, AttName}}) -> + {413, <<"attachment_too_large">>, AttName}; error_info({request_entity_too_large, DocID}) -> {413, <<"document_too_large">>, DocID}; error_info({error, security_migration_updates_disabled}) -> diff --git a/src/chttpd/test/chttpd_db_attachment_size_tests.erl b/src/chttpd/test/chttpd_db_attachment_size_tests.erl new file mode 100644 index 000000000..0ab08dd80 --- /dev/null +++ b/src/chttpd/test/chttpd_db_attachment_size_tests.erl @@ -0,0 +1,206 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(chttpd_db_attachment_size_tests). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). + +-define(USER, "chttpd_db_att_test_admin"). +-define(PASS, "pass"). +-define(AUTH, {basic_auth, {?USER, ?PASS}}). +-define(CONTENT_JSON, {"Content-Type", "application/json"}). +-define(CONTENT_MULTI_RELATED, {"Content-Type", + "multipart/related;boundary=\"bound\""}). + + +setup() -> + Hashed = couch_passwords:hash_admin_password(?PASS), + ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false), + ok = config:set("couchdb", "max_attachment_size", "50", _Persist=false), + TmpDb = ?tempdb(), + Addr = config:get("chttpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)), + Url = "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(TmpDb), + create_db(Url), + add_doc(Url, "doc1"), + Url. + + +teardown(Url) -> + delete_db(Url), + ok = config:delete("admins", ?USER, _Persist=false), + ok = config:delete("couchdb", "max_attachment_size"). + + +attachment_size_test_() -> + { + "chttpd max_attachment_size tests", + { + setup, + fun chttpd_test_util:start_couch/0, + fun chttpd_test_util:stop_couch/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun put_inline/1, + fun put_simple/1, + fun put_simple_chunked/1, + fun put_mp_related/1 + ] + } + } + }. + + +put_inline(Url) -> + ?_test(begin + Status = put_inline(Url, "doc2", 50), + ?assert(Status =:= 201 orelse Status =:= 202), + ?assertEqual(413, put_inline(Url, "doc3", 51)) + end). + + +put_simple(Url) -> + ?_test(begin + Headers = [{"Content-Type", "app/binary"}], + Rev1 = doc_rev(Url, "doc1"), + Data1 = data(50), + Status1 = put_req(Url ++ "/doc1/att2?rev=" ++ Rev1, Headers, Data1), + ?assert(Status1 =:= 201 orelse Status1 =:= 202), + Data2 = data(51), + Rev2 = doc_rev(Url, "doc1"), + Status2 = put_req(Url ++ "/doc1/att3?rev=" ++ Rev2, Headers, Data2), + ?assertEqual(413, Status2) + end). + + +put_simple_chunked(Url) -> + ?_test(begin + Headers = [{"Content-Type", "app/binary"}], + Rev1 = doc_rev(Url, "doc1"), + DataFun1 = data_stream_fun(50), + Status1 = put_req_chunked(Url ++ "/doc1/att2?rev=" ++ Rev1, Headers, DataFun1), + ?assert(Status1 =:= 201 orelse Status1 =:= 202), + DataFun2 = data_stream_fun(51), + Rev2 = doc_rev(Url, "doc1"), + Status2 = put_req_chunked(Url ++ "/doc1/att3?rev=" ++ Rev2, Headers, DataFun2), + ?assertEqual(413, Status2) + end). + + +put_mp_related(Url) -> + ?_test(begin + Headers = [?CONTENT_MULTI_RELATED], + Body1 = mp_body(50), + Status1 = put_req(Url ++ "/doc2", Headers, Body1), + ?assert(Status1 =:= 201 orelse Status1 =:= 202), + Body2 = mp_body(51), + Status2 = put_req(Url ++ "/doc3", Headers, Body2), + ?assertEqual(413, Status2) + end). + + +% Helper functions + +create_db(Url) -> + Status = put_req(Url, "{}"), + ?assert(Status =:= 201 orelse Status =:= 202). + + +add_doc(Url, DocId) -> + Status = put_req(Url ++ "/" ++ DocId, "{}"), + ?assert(Status =:= 201 orelse Status =:= 202). + + +delete_db(Url) -> + {ok, 200, _, _} = test_request:delete(Url, [?AUTH]). + + +put_inline(Url, DocId, Size) -> + Doc = "{\"_attachments\": {\"att1\":{" + "\"content_type\": \"app/binary\", " + "\"data\": \"" ++ data_b64(Size) ++ "\"" + "}}}", + put_req(Url ++ "/" ++ DocId, Doc). + + +mp_body(AttSize) -> + AttData = data(AttSize), + SizeStr = integer_to_list(AttSize), + string:join([ + "--bound", + + "Content-Type: application/json", + + "", + + "{\"_id\":\"doc2\", \"_attachments\":{\"att\":" + "{\"content_type\":\"app/binary\", \"length\":" ++ SizeStr ++ "," + "\"follows\":true}}}", + + "--bound", + + "Content-Disposition: attachment; filename=\"att\"", + + "Content-Type: app/binary", + + "", + + AttData, + + "--bound--" + ], "\r\n"). + + +doc_rev(Url, DocId) -> + {200, ResultProps} = get_req(Url ++ "/" ++ DocId), + {<<"_rev">>, BinRev} = lists:keyfind(<<"_rev">>, 1, ResultProps), + binary_to_list(BinRev). + + +put_req(Url, Body) -> + put_req(Url, [], Body). + + +put_req(Url, Headers, Body) -> + {ok, Status, _, _} = test_request:put(Url, Headers ++ [?AUTH], Body), + Status. + + +put_req_chunked(Url, Headers, Body) -> + Opts = [{transfer_encoding, {chunked, 1}}], + {ok, Status, _, _} = test_request:put(Url, Headers ++ [?AUTH], Body, Opts), + Status. + + +get_req(Url) -> + {ok, Status, _, ResultBody} = test_request:get(Url, [?CONTENT_JSON, ?AUTH]), + {[_ | _] = ResultProps} = ?JSON_DECODE(ResultBody), + {Status, ResultProps}. + +% Data streaming generator for ibrowse client. ibrowse will repeatedly call the +% function with State and it should return {ok, Data, NewState} or eof at end. +data_stream_fun(Size) -> + Fun = fun(0) -> eof; (BytesLeft) -> + {ok, <<"x">>, BytesLeft - 1} + end, + {Fun, Size}. + + +data(Size) -> + string:copies("x", Size). + + +data_b64(Size) -> + base64:encode_to_string(data(Size)). diff --git a/src/couch/src/couch_att.erl b/src/couch/src/couch_att.erl index 9d38cfae2..e78d6ef11 100644 --- a/src/couch/src/couch_att.erl +++ b/src/couch/src/couch_att.erl @@ -50,6 +50,11 @@ downgrade/1 ]). +-export([ + max_attachment_size/0, + validate_attachment_size/3 +]). + -compile(nowarn_deprecated_type). -export_type([att/0]). @@ -500,6 +505,8 @@ flush_data(Fd, Data, Att) when is_binary(Data) -> couch_stream:write(OutputStream, Data) end); flush_data(Fd, Fun, Att) when is_function(Fun) -> + AttName = fetch(name, Att), + MaxAttSize = max_attachment_size(), case fetch(att_len, Att) of undefined -> couch_db:with_stream(Fd, Att, fun(OutputStream) -> @@ -510,7 +517,7 @@ flush_data(Fd, Fun, Att) when is_function(Fun) -> % WriterFun({0, _Footers}, State) % Called with Length == 0 on the last time. % WriterFun returns NewState. - fun({0, Footers}, _) -> + fun({0, Footers}, _Total) -> F = mochiweb_headers:from_binary(Footers), case mochiweb_headers:get_value("Content-MD5", F) of undefined -> @@ -518,11 +525,15 @@ flush_data(Fd, Fun, Att) when is_function(Fun) -> Md5 -> {md5, base64:decode(Md5)} end; - ({_Length, Chunk}, _) -> - couch_stream:write(OutputStream, Chunk) - end, ok) + ({Length, Chunk}, Total0) -> + Total = Total0 + Length, + validate_attachment_size(AttName, Total, MaxAttSize), + couch_stream:write(OutputStream, Chunk), + Total + end, 0) end); AttLen -> + validate_attachment_size(AttName, AttLen, MaxAttSize), couch_db:with_stream(Fd, Att, fun(OutputStream) -> write_streamed_attachment(OutputStream, Fun, AttLen) end) @@ -680,6 +691,22 @@ upgrade_encoding(false) -> identity; upgrade_encoding(Encoding) -> Encoding. +max_attachment_size() -> + case config:get("couchdb", "max_attachment_size", "infinity") of + "infinity" -> + infinity; + MaxAttSize -> + list_to_integer(MaxAttSize) + end. + + +validate_attachment_size(AttName, AttSize, MaxAttSize) + when is_integer(AttSize), AttSize > MaxAttSize -> + throw({request_entity_too_large, {attachment, AttName}}); +validate_attachment_size(_AttName, _AttSize, _MAxAttSize) -> + ok. + + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/couch/src/couch_doc.erl b/src/couch/src/couch_doc.erl index 381ad4b4f..2f792bf15 100644 --- a/src/couch/src/couch_doc.erl +++ b/src/couch/src/couch_doc.erl @@ -16,6 +16,7 @@ -export([from_json_obj/1, from_json_obj_validate/1, to_json_obj/2,has_stubs/1, merge_stubs/2]). -export([validate_docid/1, get_validate_doc_fun/1]). -export([doc_from_multi_part_stream/2, doc_from_multi_part_stream/3]). +-export([doc_from_multi_part_stream/4]). -export([doc_to_multi_part_stream/5, len_doc_to_multi_part_stream/4]). -export([restart_open_doc_revs/3]). -export([to_path/1]). @@ -129,11 +130,24 @@ from_json_obj_validate(EJson) -> Doc = from_json_obj(EJson), case erlang:external_size(Doc#doc.body) =< MaxSize of true -> + validate_attachment_sizes(Doc#doc.atts), Doc; false -> throw({request_entity_too_large, Doc#doc.id}) end. + +validate_attachment_sizes([]) -> + ok; +validate_attachment_sizes(Atts) -> + MaxAttSize = couch_att:max_attachment_size(), + lists:foreach(fun(Att) -> + AttName = couch_att:fetch(name, Att), + AttSize = couch_att:fetch(att_len, Att), + couch_att:validate_attachment_size(AttName, AttSize, MaxAttSize) + end, Atts). + + from_json_obj({Props}) -> transfer_fields(Props, #doc{body=[]}); @@ -420,11 +434,19 @@ doc_from_multi_part_stream(ContentType, DataFun) -> doc_from_multi_part_stream(ContentType, DataFun, make_ref()). doc_from_multi_part_stream(ContentType, DataFun, Ref) -> + doc_from_multi_part_stream(ContentType, DataFun, Ref, true). + +doc_from_multi_part_stream(ContentType, DataFun, Ref, ValidateDocLimits) -> case couch_httpd_multipart:decode_multipart_stream(ContentType, DataFun, Ref) of {{started_open_doc_revs, NewRef}, Parser, _ParserRef} -> restart_open_doc_revs(Parser, Ref, NewRef); {{doc_bytes, Ref, DocBytes}, Parser, ParserRef} -> - Doc = from_json_obj_validate(?JSON_DECODE(DocBytes)), + Doc = case ValidateDocLimits of + true -> + from_json_obj_validate(?JSON_DECODE(DocBytes)); + false -> + from_json_obj(?JSON_DECODE(DocBytes)) + end, erlang:put(mochiweb_request_recv, true), % we'll send the Parser process ID to the remote nodes so they can % retrieve their own copies of the attachment data diff --git a/src/couch/src/couch_httpd.erl b/src/couch/src/couch_httpd.erl index faaf080d9..b3bbd5baa 100644 --- a/src/couch/src/couch_httpd.erl +++ b/src/couch/src/couch_httpd.erl @@ -885,6 +885,10 @@ error_info(file_exists) -> "created, the file already exists.">>}; error_info(request_entity_too_large) -> {413, <<"too_large">>, <<"the request entity is too large">>}; +error_info({request_entity_too_large, {attachment, AttName}}) -> + {413, <<"attachment_too_large">>, AttName}; +error_info({request_entity_too_large, DocID}) -> + {413, <<"document_too_large">>, DocID}; error_info(request_uri_too_long) -> {414, <<"too_long">>, <<"the request uri is too long">>}; error_info({bad_ctype, Reason}) -> diff --git a/src/couch/test/couch_doc_tests.erl b/src/couch/test/couch_doc_tests.erl index d24cd67c0..5d0448a9e 100644 --- a/src/couch/test/couch_doc_tests.erl +++ b/src/couch/test/couch_doc_tests.erl @@ -131,6 +131,7 @@ mock_config_max_document_id_length() -> ok = meck:new(config, [passthrough]), meck:expect(config, get, fun("couchdb", "max_document_id_length", "infinity") -> "1024"; + ("couchdb", "max_attachment_size", "infinity") -> "infinity"; (Key, Val, Default) -> meck:passthrough([Key, Val, Default]) end ). diff --git a/src/couch/test/couchdb_attachments_tests.erl b/src/couch/test/couchdb_attachments_tests.erl index 88f53a189..4536ba6b2 100644 --- a/src/couch/test/couchdb_attachments_tests.erl +++ b/src/couch/test/couchdb_attachments_tests.erl @@ -14,6 +14,7 @@ -include_lib("couch/include/couch_eunit.hrl"). -include_lib("couch/include/couch_db.hrl"). +-include_lib("mem3/include/mem3.hrl"). -define(COMPRESSION_LEVEL, 8). -define(ATT_BIN_NAME, <<"logo.png">>). @@ -515,6 +516,137 @@ should_create_compressible_att_with_ctype_params({Host, DbName}) -> end)}. +compact_after_lowering_attachment_size_limit_test_() -> + { + "Compact after lowering attachment size limit", + { + foreach, + fun() -> + Ctx = test_util:start_couch(), + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]), + ok = couch_db:close(Db), + {Ctx, DbName} + end, + fun({Ctx, DbName}) -> + config:delete("couchdb", "max_attachment_size"), + ok = couch_server:delete(DbName, [?ADMIN_CTX]), + test_util:stop_couch(Ctx) + end, + [ + fun should_compact_after_lowering_attachment_size_limit/1 + ] + } + }. + + +should_compact_after_lowering_attachment_size_limit({_Ctx, DbName}) -> + {timeout, ?TIMEOUT_EUNIT, ?_test(begin + {ok, Db1} = couch_db:open(DbName, [?ADMIN_CTX]), + Doc1 = #doc{id = <<"doc1">>, atts = att(1000)}, + {ok, _} = couch_db:update_doc(Db1, Doc1, []), + couch_db:close(Db1), + config:set("couchdb", "max_attachment_size", "1", _Persist = false), + compact_db(DbName), + {ok, Db2} = couch_db:open_int(DbName, []), + {ok, Doc2} = couch_db:open_doc(Db2, <<"doc1">>), + couch_db:close(Db2), + [Att] = Doc2#doc.atts, + ?assertEqual(1000, couch_att:fetch(att_len, Att)) + end)}. + + +att(Size) when is_integer(Size), Size >= 1 -> + [couch_att:new([ + {name, <<"att">>}, + {type, <<"app/binary">>}, + {att_len, Size}, + {data, fun(_Bytes) -> + << <<"x">> || _ <- lists:seq(1, Size) >> + end} + ])]. + + +compact_db(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, _CompactPid} = couch_db:start_compact(Db), + wait_compaction(DbName, "database", ?LINE), + ok = couch_db:close(Db). + + +wait_compaction(DbName, Kind, Line) -> + WaitFun = fun() -> + case is_compaction_running(DbName) of + true -> wait; + false -> ok + end + end, + case test_util:wait(WaitFun, ?TIMEOUT) of + timeout -> + erlang:error({assertion_failed, + [{module, ?MODULE}, + {line, Line}, + {reason, "Timeout waiting for " + ++ Kind + ++ " database compaction"}]}); + _ -> + ok + end. + + +is_compaction_running(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + {ok, DbInfo} = couch_db:get_db_info(Db), + couch_db:close(Db), + couch_util:get_value(compact_running, DbInfo) =:= true. + + +internal_replication_after_lowering_attachment_size_limit_test_() -> + { + "Internal replication after lowering max attachment size", + { + foreach, + fun() -> + Ctx = test_util:start_couch([mem3]), + SrcName = ?tempdb(), + {ok, SrcDb} = couch_db:create(SrcName, [?ADMIN_CTX]), + ok = couch_db:close(SrcDb), + TgtName = ?tempdb(), + {ok, TgtDb} = couch_db:create(TgtName, [?ADMIN_CTX]), + ok = couch_db:close(TgtDb), + {Ctx, SrcName, TgtName} + end, + fun({Ctx, SrcName, TgtName}) -> + config:delete("couchdb", "max_attachment_size"), + ok = couch_server:delete(SrcName, [?ADMIN_CTX]), + ok = couch_server:delete(TgtName, [?ADMIN_CTX]), + test_util:stop_couch(Ctx) + end, + [ + fun should_replicate_after_lowering_attachment_size/1 + ] + } + }. + +should_replicate_after_lowering_attachment_size({_Ctx, SrcName, TgtName}) -> + {timeout, ?TIMEOUT_EUNIT, ?_test(begin + {ok, SrcDb} = couch_db:open(SrcName, [?ADMIN_CTX]), + SrcDoc = #doc{id = <<"doc">>, atts = att(1000)}, + {ok, _} = couch_db:update_doc(SrcDb, SrcDoc, []), + couch_db:close(SrcDb), + config:set("couchdb", "max_attachment_size", "1", _Persist = false), + % Create a pair of "fake" shards + SrcShard = #shard{name = SrcName, node = node()}, + TgtShard = #shard{name = TgtName, node = node()}, + mem3_rep:go(SrcShard, TgtShard, []), + {ok, TgtDb} = couch_db:open_int(TgtName, []), + {ok, TgtDoc} = couch_db:open_doc(TgtDb, <<"doc">>), + couch_db:close(TgtDb), + [Att] = TgtDoc#doc.atts, + ?assertEqual(1000, couch_att:fetch(att_len, Att)) + end)}. + + get_json(Json, Path) -> couch_util:get_nested_json_value(Json, Path). diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl index 91d7d7ae5..a2ef60fa3 100644 --- a/src/couch_replicator/src/couch_replicator_api_wrap.erl +++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl @@ -711,10 +711,12 @@ receive_docs(Streamer, UserFun, Ref, UserAcc) -> {headers, Ref, Headers} -> case header_value("content-type", Headers) of {"multipart/related", _} = ContentType -> + % Skip document body and attachment size limits validation here + % since these should be validated by the replication target case couch_doc:doc_from_multi_part_stream( ContentType, fun() -> receive_doc_data(Streamer, Ref) end, - Ref) of + Ref, _ValidateDocLimits = false) of {ok, Doc, WaitFun, Parser} -> case run_user_fun(UserFun, {ok, Doc}, UserAcc, Ref) of {ok, UserAcc2} -> diff --git a/src/couch_replicator/test/couch_replicator_attachments_too_large.erl b/src/couch_replicator/test/couch_replicator_attachments_too_large.erl new file mode 100644 index 000000000..7fe84d2d9 --- /dev/null +++ b/src/couch_replicator/test/couch_replicator_attachments_too_large.erl @@ -0,0 +1,104 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_replicator_attachments_too_large). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). +-include_lib("couch_replicator/src/couch_replicator.hrl"). + + +setup(_) -> + Ctx = test_util:start_couch([couch_replicator]), + Source = create_db(), + create_doc_with_attachment(Source, <<"doc">>, 1000), + Target = create_db(), + {Ctx, {Source, Target}}. + + +teardown(_, {Ctx, {Source, Target}}) -> + delete_db(Source), + delete_db(Target), + config:delete("couchdb", "max_attachment_size"), + ok = test_util:stop_couch(Ctx). + + +attachment_too_large_replication_test_() -> + Pairs = [{local, remote}, {remote, local}, {remote, remote}], + { + "Attachment size too large replication tests", + { + foreachx, + fun setup/1, fun teardown/2, + [{Pair, fun should_succeed/2} || Pair <- Pairs] ++ + [{Pair, fun should_fail/2} || Pair <- Pairs] + } + }. + + +should_succeed({From, To}, {_Ctx, {Source, Target}}) -> + RepObject = {[ + {<<"source">>, db_url(From, Source)}, + {<<"target">>, db_url(To, Target)} + ]}, + config:set("couchdb", "max_attachment_size", "1000", _Persist = false), + {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER), + ?_assertEqual(ok, couch_replicator_test_helper:compare_dbs(Source, Target)). + + +should_fail({From, To}, {_Ctx, {Source, Target}}) -> + RepObject = {[ + {<<"source">>, db_url(From, Source)}, + {<<"target">>, db_url(To, Target)} + ]}, + config:set("couchdb", "max_attachment_size", "999", _Persist = false), + {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER), + ?_assertError({badmatch, {not_found, missing}}, + couch_replicator_test_helper:compare_dbs(Source, Target)). + + +create_db() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]), + ok = couch_db:close(Db), + DbName. + + +create_doc_with_attachment(DbName, DocId, AttSize) -> + {ok, Db} = couch_db:open(DbName, [?ADMIN_CTX]), + Doc = #doc{id = DocId, atts = att(AttSize)}, + {ok, _} = couch_db:update_doc(Db, Doc, []), + couch_db:close(Db), + ok. + + +att(Size) when is_integer(Size), Size >= 1 -> + [couch_att:new([ + {name, <<"att">>}, + {type, <<"app/binary">>}, + {att_len, Size}, + {data, fun(_Bytes) -> + << <<"x">> || _ <- lists:seq(1, Size) >> + end} + ])]. + + +delete_db(DbName) -> + ok = couch_server:delete(DbName, [?ADMIN_CTX]). + + +db_url(local, DbName) -> + DbName; +db_url(remote, DbName) -> + Addr = config:get("httpd", "bind_address", "127.0.0.1"), + Port = mochiweb_socket_server:get(couch_httpd, port), + ?l2b(io_lib:format("http://~s:~b/~s", [Addr, Port, DbName])). diff --git a/src/fabric/src/fabric_doc_attachments.erl b/src/fabric/src/fabric_doc_attachments.erl index 8b8123fa9..47854d189 100644 --- a/src/fabric/src/fabric_doc_attachments.erl +++ b/src/fabric/src/fabric_doc_attachments.erl @@ -24,8 +24,8 @@ receiver(_Req, {unknown_transfer_encoding, Unknown}) -> exit({unknown_transfer_encoding, Unknown}); receiver(Req, chunked) -> MiddleMan = spawn(fun() -> middleman(Req, chunked) end), - fun(4096, ChunkFun, ok) -> - write_chunks(MiddleMan, ChunkFun) + fun(4096, ChunkFun, State) -> + write_chunks(MiddleMan, ChunkFun, State) end; receiver(_Req, 0) -> <<"">>; @@ -63,27 +63,29 @@ maybe_send_continue(#httpd{mochi_req = MochiReq} = Req) -> end end. -write_chunks(MiddleMan, ChunkFun) -> +write_chunks(MiddleMan, ChunkFun, State) -> MiddleMan ! {self(), gimme_data}, Timeout = fabric_util:attachments_timeout(), receive {MiddleMan, ChunkRecordList} -> rexi:reply(attachment_chunk_received), - case flush_chunks(ChunkRecordList, ChunkFun) of - continue -> write_chunks(MiddleMan, ChunkFun); - done -> ok + case flush_chunks(ChunkRecordList, ChunkFun, State) of + {continue, NewState} -> + write_chunks(MiddleMan, ChunkFun, NewState); + {done, NewState} -> + NewState end after Timeout -> exit(timeout) end. -flush_chunks([], _ChunkFun) -> - continue; -flush_chunks([{0, _}], _ChunkFun) -> - done; -flush_chunks([Chunk | Rest], ChunkFun) -> - ChunkFun(Chunk, ok), - flush_chunks(Rest, ChunkFun). +flush_chunks([], _ChunkFun, State) -> + {continue, State}; +flush_chunks([{0, _}], _ChunkFun, State) -> + {done, State}; +flush_chunks([Chunk | Rest], ChunkFun, State) -> + NewState = ChunkFun(Chunk, State), + flush_chunks(Rest, ChunkFun, NewState). receive_unchunked_attachment(_Req, 0) -> ok; diff --git a/src/fabric/src/fabric_doc_update.erl b/src/fabric/src/fabric_doc_update.erl index 10e254ff5..214566d91 100644 --- a/src/fabric/src/fabric_doc_update.erl +++ b/src/fabric/src/fabric_doc_update.erl @@ -96,7 +96,9 @@ handle_message({not_found, no_db_file} = X, Worker, Acc0) -> Docs = couch_util:get_value(Worker, GroupedDocs), handle_message({ok, [X || _D <- Docs]}, Worker, Acc0); handle_message({bad_request, Msg}, _, _) -> - throw({bad_request, Msg}). + throw({bad_request, Msg}); +handle_message({request_entity_too_large, Entity}, _, _) -> + throw({request_entity_too_large, Entity}). before_doc_update(DbName, Docs, Opts) -> case {fabric_util:is_replicator_db(DbName), fabric_util:is_users_db(DbName)} of |