blob: f1f52437123e0b250f374a7d9c67843fd6017887 [file] [log] [blame]
% 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(fabric2_util).
-export([
revinfo_to_revs/1,
revinfo_to_path/1,
sort_revinfos/1,
rev_size/1,
ldoc_size/1,
seq_zero_vs/0,
seq_max_vs/0,
user_ctx_to_json/1,
validate_security_object/1,
hash_atts/1,
dbname_ends_with/2,
get_value/2,
get_value/3,
to_hex/1,
from_hex/1,
uuid/0,
encode_all_doc_key/1,
all_docs_view_opts/1,
iso8601_timestamp/0,
now/1,
do_recovery/0,
pmap/2,
pmap/3
]).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch_views/include/couch_views.hrl").
revinfo_to_revs(RevInfo) ->
#{
rev_id := {RevPos, Rev},
rev_path := RevPath
} = RevInfo,
{RevPos, [Rev | RevPath]}.
revinfo_to_path(RevInfo) ->
#{
rev_id := {RevPos, Rev},
rev_path := RevPath
} = RevInfo,
Revs = lists:reverse(RevPath, [Rev]),
Path = revinfo_to_path(RevInfo, Revs),
{RevPos - length(Revs) + 1, Path}.
revinfo_to_path(RevInfo, [Rev]) ->
{Rev, RevInfo, []};
revinfo_to_path(RevInfo, [Rev | Rest]) ->
{Rev, ?REV_MISSING, [revinfo_to_path(RevInfo, Rest)]}.
sort_revinfos(RevInfos) ->
CmpFun = fun(A, B) -> rev_sort_key(A) > rev_sort_key(B) end,
lists:sort(CmpFun, RevInfos).
rev_sort_key(#{} = RevInfo) ->
#{
deleted := Deleted,
rev_id := {RevPos, Rev}
} = RevInfo,
{not Deleted, RevPos, Rev}.
rev_size(#doc{} = Doc) ->
#doc{
id = Id,
revs = Revs,
body = Body,
atts = Atts
} = Doc,
{Start, Rev} =
case Revs of
{0, []} -> {0, <<>>};
{N, [RevId | _]} -> {N, RevId}
end,
lists:sum([
size(Id),
size(erlfdb_tuple:pack({Start})),
size(Rev),
% FDB tuple encoding of booleans for deleted flag is 1 byte
1,
couch_ejson_size:encoded_size(Body),
lists:foldl(
fun(Att, Acc) ->
couch_att:external_size(Att) + Acc
end,
0,
Atts
)
]).
ldoc_size(#doc{id = <<"_local/", _/binary>>} = Doc) ->
#doc{
id = Id,
revs = {0, [Rev]},
deleted = Deleted,
body = Body
} = Doc,
StoreRev =
case Rev of
_ when is_integer(Rev) -> integer_to_binary(Rev);
_ when is_binary(Rev) -> Rev
end,
case Deleted of
true ->
0;
false ->
lists:sum([
size(Id),
size(StoreRev),
couch_ejson_size:encoded_size(Body)
])
end.
seq_zero_vs() ->
{versionstamp, 0, 0, 0}.
seq_max_vs() ->
{versionstamp, 18446744073709551615, 65535, 65535}.
user_ctx_to_json(Db) ->
UserCtx = fabric2_db:get_user_ctx(Db),
{[
{<<"db">>, fabric2_db:name(Db)},
{<<"name">>, UserCtx#user_ctx.name},
{<<"roles">>, UserCtx#user_ctx.roles}
]}.
validate_security_object({SecProps}) ->
Admins = get_value(<<"admins">>, SecProps, {[]}),
ok = validate_names_and_roles(Admins),
% we fallback to readers here for backwards compatibility
Readers = get_value(<<"readers">>, SecProps, {[]}),
Members = get_value(<<"members">>, SecProps, Readers),
ok = validate_names_and_roles(Members).
validate_names_and_roles({Props}) when is_list(Props) ->
validate_json_list_of_strings(<<"names">>, Props),
validate_json_list_of_strings(<<"roles">>, Props);
validate_names_and_roles(_) ->
throw("admins or members must be a JSON list of strings").
validate_json_list_of_strings(Member, Props) ->
case get_value(Member, Props, []) of
Values when is_list(Values) ->
NonBinary = lists:filter(fun(V) -> not is_binary(V) end, Values),
if
NonBinary == [] ->
ok;
true ->
MemberStr = binary_to_list(Member),
throw(MemberStr ++ " must be a JSON list of strings")
end;
_ ->
MemberStr = binary_to_list(Member),
throw(MemberStr ++ " must be a JSON list of strings")
end.
hash_atts([]) ->
<<>>;
hash_atts(Atts) ->
SortedAtts = lists:sort(
fun(A, B) ->
couch_att:fetch(name, A) =< couch_att:fetch(name, B)
end,
Atts
),
Md5St = lists:foldl(
fun(Att, Acc) ->
{loc, _Db, _DocId, AttId} = couch_att:fetch(data, Att),
couch_hash:md5_hash_update(Acc, AttId)
end,
couch_hash:md5_hash_init(),
SortedAtts
),
couch_hash:md5_hash_final(Md5St).
dbname_ends_with(#{} = Db, Suffix) ->
dbname_ends_with(fabric2_db:name(Db), Suffix);
dbname_ends_with(DbName, Suffix) when is_binary(DbName), is_binary(Suffix) ->
Suffix == filename:basename(DbName).
get_value(Key, List) ->
get_value(Key, List, undefined).
get_value(Key, List, Default) ->
case lists:keysearch(Key, 1, List) of
{value, {Key, Value}} ->
Value;
false ->
Default
end.
to_hex(Bin) ->
list_to_binary(to_hex_int(Bin)).
to_hex_int(<<>>) ->
[];
to_hex_int(<<Hi:4, Lo:4, Rest/binary>>) ->
[nibble_to_hex(Hi), nibble_to_hex(Lo) | to_hex(Rest)].
nibble_to_hex(I) ->
case I of
0 -> $0;
1 -> $1;
2 -> $2;
3 -> $3;
4 -> $4;
5 -> $5;
6 -> $6;
7 -> $7;
8 -> $8;
9 -> $9;
10 -> $a;
11 -> $b;
12 -> $c;
13 -> $d;
14 -> $e;
15 -> $f
end.
from_hex(Bin) ->
iolist_to_binary(from_hex_int(Bin)).
from_hex_int(<<>>) ->
[];
from_hex_int(<<Hi:8, Lo:8, RestBinary/binary>>) ->
HiNib = hex_to_nibble(Hi),
LoNib = hex_to_nibble(Lo),
[<<HiNib:4, LoNib:4>> | from_hex_int(RestBinary)];
from_hex_int(<<BadHex/binary>>) ->
erlang:error({invalid_hex, BadHex}).
hex_to_nibble(N) ->
case N of
$0 -> 0;
$1 -> 1;
$2 -> 2;
$3 -> 3;
$4 -> 4;
$5 -> 5;
$6 -> 6;
$7 -> 7;
$8 -> 8;
$9 -> 9;
$a -> 10;
$A -> 10;
$b -> 11;
$B -> 11;
$c -> 12;
$C -> 12;
$d -> 13;
$D -> 13;
$e -> 14;
$E -> 14;
$f -> 15;
$F -> 15;
_ -> erlang:error({invalid_hex, N})
end.
uuid() ->
to_hex(crypto:strong_rand_bytes(16)).
encode_all_doc_key(B) when is_binary(B) -> B;
encode_all_doc_key(Term) when Term < <<>> -> <<>>;
encode_all_doc_key(_) -> <<255>>.
all_docs_view_opts(#mrargs{} = Args) ->
NS = couch_util:get_value(namespace, Args#mrargs.extra),
StartKey =
case Args#mrargs.start_key of
undefined -> Args#mrargs.start_key_docid;
SKey -> SKey
end,
EndKey =
case Args#mrargs.end_key of
undefined -> Args#mrargs.end_key_docid;
EKey -> EKey
end,
StartKeyOpts =
case StartKey of
undefined -> [];
_ -> [{start_key, encode_all_doc_key(StartKey)}]
end,
EndKeyOpts =
case {EndKey, Args#mrargs.inclusive_end} of
{undefined, _} -> [];
{_, false} -> [{end_key_gt, encode_all_doc_key(EndKey)}];
{_, true} -> [{end_key, encode_all_doc_key(EndKey)}]
end,
DocOpts =
case Args#mrargs.conflicts of
true -> [conflicts | Args#mrargs.doc_options];
_ -> Args#mrargs.doc_options
end,
[
{dir, Args#mrargs.direction},
{limit, Args#mrargs.limit},
{skip, Args#mrargs.skip},
{update_seq, Args#mrargs.update_seq},
{namespace, NS},
{include_docs, Args#mrargs.include_docs},
{doc_opts, DocOpts}
] ++ StartKeyOpts ++ EndKeyOpts.
iso8601_timestamp() ->
Now = os:timestamp(),
{{Year, Month, Date}, {Hour, Minute, Second}} =
calendar:now_to_datetime(Now),
Format = "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
io_lib:format(Format, [Year, Month, Date, Hour, Minute, Second]).
now(ms) ->
{Mega, Sec, Micro} = os:timestamp(),
(Mega * 1000000 + Sec) * 1000 + round(Micro / 1000);
now(sec) ->
now(ms) div 1000.
do_recovery() ->
config:get_boolean(
"couchdb",
"enable_database_recovery",
false
).
pmap(Fun, Args) ->
pmap(Fun, Args, []).
pmap(Fun, Args, Opts) ->
Refs = lists:map(
fun(Arg) ->
{_, Ref} = spawn_monitor(fun() -> exit(pmap_exec(Fun, Arg)) end),
Ref
end,
Args
),
Timeout = fabric2_util:get_value(timeout, Opts, 5000),
lists:map(
fun(Ref) ->
receive
{'DOWN', Ref, _, _, {'$res', Res}} ->
Res;
{'DOWN', Ref, _, _, {'$err', Tag, Reason, Stack}} ->
erlang:raise(Tag, Reason, Stack)
after Timeout ->
error({pmap_timeout, Timeout})
end
end,
Refs
).
pmap_exec(Fun, Arg) ->
try
{'$res', Fun(Arg)}
catch
Tag:Reason:Stack ->
{'$err', Tag, Reason, Stack}
end.