Skip to content

Commit

Permalink
Merge pull request #3674 from esl/graphql/http_upload
Browse files Browse the repository at this point in the history
Graphql/http upload
  • Loading branch information
chrzaszcz authored Jun 15, 2022
2 parents 66f476b + 15b9cf6 commit 9fea697
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 58 deletions.
1 change: 1 addition & 0 deletions big_tests/default.spec
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
{suites, "tests", graphql_session_SUITE}.
{suites, "tests", graphql_stanza_SUITE}.
{suites, "tests", graphql_vcard_SUITE}.
{suites, "tests", graphql_http_upload_SUITE}.
{suites, "tests", inbox_SUITE}.
{suites, "tests", inbox_extensions_SUITE}.
{suites, "tests", jingle_SUITE}.
Expand Down
1 change: 1 addition & 0 deletions big_tests/dynamic_domains.spec
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
{suites, "tests", graphql_session_SUITE}.
{suites, "tests", graphql_stanza_SUITE}.
{suites, "tests", graphql_vcard_SUITE}.
{suites, "tests", graphql_http_upload_SUITE}.

{suites, "tests", inbox_SUITE}.

Expand Down
236 changes: 236 additions & 0 deletions big_tests/tests/graphql_http_upload_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
-module(graphql_http_upload_SUITE).

-compile([export_all, nowarn_export_all]).

-import(distributed_helper, [require_rpc_nodes/1]).
-import(domain_helper, [host_type/0, domain/0]).
-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1]).

-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("exml/include/exml.hrl").
-include_lib("escalus/include/escalus.hrl").
-include("../../include/mod_roster.hrl").

-define(S3_HOSTNAME, <<"http://bucket.s3-eu-east-25.example.com">>).

suite() ->
require_rpc_nodes([mim]) ++ escalus:suite().

all() ->
[{group, user_http_upload},
{group, user_http_upload_not_configured},
{group, admin_http_upload},
{group, admin_http_upload_not_configured}].

groups() ->
[{user_http_upload, [], user_http_upload_handler()},
{user_http_upload_not_configured, [], user_http_upload_not_configured_handler()},
{admin_http_upload, [], admin_http_upload_handler()},
{admin_http_upload_not_configured, [], admin_http_upload_not_configured_handler()}].

user_http_upload_handler() ->
[user_get_url_test,
user_get_url_zero_size,
user_get_url_too_large_size,
user_get_url_zero_timeout].

user_http_upload_not_configured_handler() ->
[user_http_upload_not_configured].

admin_http_upload_handler() ->
[admin_get_url_test,
admin_get_url_zero_size,
admin_get_url_too_large_size,
admin_get_url_zero_timeout,
admin_get_url_no_domain].

admin_http_upload_not_configured_handler() ->
[admin_http_upload_not_configured].

init_per_suite(Config) ->
Config1 = dynamic_modules:save_modules(host_type(), Config),
escalus:init_per_suite(Config1).

end_per_suite(Config) ->
dynamic_modules:restore_modules(Config),
escalus:end_per_suite(Config).

init_per_group(user_http_upload, Config) ->
dynamic_modules:ensure_modules(host_type(),
[{mod_http_upload, create_opts(?S3_HOSTNAME, true)}]),
[{schema_endpoint, user} | Config];
init_per_group(user_http_upload_not_configured, Config) ->
dynamic_modules:ensure_modules(host_type(), [{mod_http_upload, stopped}]),
[{schema_endpoint, user} | Config];
init_per_group(admin_http_upload, Config) ->
dynamic_modules:ensure_modules(host_type(),
[{mod_http_upload, create_opts(?S3_HOSTNAME, true)}]),
graphql_helper:init_admin_handler(Config);
init_per_group(admin_http_upload_not_configured, Config) ->
dynamic_modules:ensure_modules(host_type(), [{mod_http_upload, stopped}]),
graphql_helper:init_admin_handler(Config).

end_per_group(_, _Config) ->
escalus_fresh:clean().

init_per_testcase(CaseName, Config) ->
escalus:init_per_testcase(CaseName, Config).

end_per_testcase(CaseName, Config) ->
escalus:end_per_testcase(CaseName, Config).

create_opts(Host, AddAcl) ->
config_parser_helper:mod_config(mod_http_upload,
#{
max_file_size => 1234,
s3 => #{
bucket_url => Host,
add_acl => AddAcl,
region => <<"eu-east-25">>,
access_key_id => <<"AKIAIAOAONIULXQGMOUA">>,
secret_access_key => <<"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">>
}
}).

% User test cases

user_get_url_test(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun user_get_url_test/2).

user_get_url_test(Config, Alice) ->
Vars = #{<<"filename">> => <<"test">>, <<"size">> => 123,
<<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = user_send_request(Config, Vars, Alice),
ParsedResult = ok_result(<<"httpUpload">>, <<"getUrl">>, GraphQlRequest),
#{<<"PutUrl">> := PutURL, <<"GetUrl">> := GetURL, <<"Header">> := _Headers} = ParsedResult,
?assertMatch({_, _}, binary:match(PutURL, [?S3_HOSTNAME])),
?assertMatch({_, _}, binary:match(GetURL, [?S3_HOSTNAME])).

user_get_url_zero_size(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun user_get_url_zero_size/2).

user_get_url_zero_size(Config, Alice) ->
Vars = #{<<"filename">> => <<"test">>, <<"size">> => 0,
<<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = user_send_request(Config, Vars, Alice),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"size_error">>, ParsedResult).

user_get_url_too_large_size(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun user_get_url_too_large_size/2).

user_get_url_too_large_size(Config, Alice) ->
Vars = #{<<"filename">> => <<"test">>, <<"size">> => 100000,
<<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = user_send_request(Config, Vars, Alice),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"file_too_large_error">>, ParsedResult).

user_get_url_zero_timeout(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun user_get_url_zero_timeout/2).

user_get_url_zero_timeout(Config, Alice) ->
Vars = #{<<"filename">> => <<"test">>, <<"size">> => 123,
<<"contentType">> => <<"Test">>, <<"timeout">> => 0},
GraphQlRequest = user_send_request(Config, Vars, Alice),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"timeout_error">>, ParsedResult).

user_http_upload_not_configured(Config) ->
escalus:fresh_story_with_config(Config, [{alice, 1}],
fun user_http_upload_not_configured/2).

user_http_upload_not_configured(Config, Alice) ->
Vars = #{<<"filename">> => <<"test">>, <<"size">> => 123,
<<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = user_send_request(Config, Vars, Alice),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"module_not_loaded_error">>, ParsedResult).

% Admin test cases

admin_get_url_test(Config) ->
Vars = #{<<"domain">> => domain(), <<"filename">> => <<"test">>,
<<"size">> => 123, <<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = admin_send_request(Config, Vars),
ParsedResult = ok_result(<<"httpUpload">>, <<"getUrl">>, GraphQlRequest),
#{<<"PutUrl">> := PutURL, <<"GetUrl">> := GetURL, <<"Header">> := _Headers} = ParsedResult,
?assertMatch({_, _}, binary:match(PutURL, [?S3_HOSTNAME])),
?assertMatch({_, _}, binary:match(GetURL, [?S3_HOSTNAME])).

admin_get_url_zero_size(Config) ->
Vars = #{<<"domain">> => domain(), <<"filename">> => <<"test">>,
<<"size">> => 0, <<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = admin_send_request(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"size_error">>, ParsedResult).

admin_get_url_too_large_size(Config) ->
Vars = #{<<"domain">> => domain(), <<"filename">> => <<"test">>,
<<"size">> => 1000000, <<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = admin_send_request(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"file_too_large_error">>, ParsedResult).

admin_get_url_zero_timeout(Config) ->
Vars = #{<<"domain">> => domain(), <<"filename">> => <<"test">>,
<<"size">> => 123, <<"contentType">> => <<"Test">>, <<"timeout">> => 0},
GraphQlRequest = admin_send_request(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"timeout_error">>, ParsedResult).

admin_get_url_no_domain(Config) ->
Vars = #{<<"domain">> => <<"AAAAA">>, <<"filename">> => <<"test">>,
<<"size">> => 123, <<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = admin_send_request(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"domain_not_found">>, ParsedResult).

admin_http_upload_not_configured(Config) ->
Vars = #{<<"domain">> => domain(), <<"filename">> => <<"test">>,
<<"size">> => 123, <<"contentType">> => <<"Test">>, <<"timeout">> => 123},
GraphQlRequest = admin_send_request(Config, Vars),
ParsedResult = error_result(<<"extensions">>, <<"code">>, GraphQlRequest),
?assertEqual(<<"module_not_loaded_error">>, ParsedResult).

% Helpers

user_get_url() ->
<<"mutation M1($filename: String!, $size: Int!,
$contentType: String!, $timeout: Int!)",
"{httpUpload{getUrl(filename: $filename, size: $size,
contentType: $contentType, timeout: $timeout)
{PutUrl, GetUrl, Header}}}">>.

admin_get_url() ->
<<"mutation M1($domain: String!, $filename: String!, $size: Int!,
$contentType: String!, $timeout: Int!)",
"{httpUpload{getUrl(domain: $domain, filename: $filename,
size: $size, contentType: $contentType, timeout: $timeout)
{PutUrl, GetUrl, Header}}}">>.

user_send_request(Config, Vars, User) ->
Body = #{query => user_get_url(), operationName => <<"M1">>, variables => Vars},
execute_user(Body, User, Config).

admin_send_request(Config, Vars) ->
Body = #{query => admin_get_url(), operationName => <<"M1">>, variables => Vars},
execute_auth(Body, Config).

error_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"errors">> := [Data]}}) ->
maps:get(What2, maps:get(What1, Data)).

ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) ->
maps:get(What2, maps:get(What1, Data)).

url_contains(UrlType, Filename, Result) ->
Url = extract_url(Result, UrlType),
binary:match(Url, Filename) =/= nomatch.

extract_url(Result, UrlType) ->
exml_query:path(Result, [{element, <<"slot">>}, {element, UrlType}, {attr, <<"url">>}]).
2 changes: 2 additions & 0 deletions priv/graphql/schemas/admin/admin_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ type AdminMutation @protected{
vcard: VcardAdminMutation
"Private storage management"
private: PrivateAdminMutation
"Http upload"
httpUpload: HttpUploadAdminMutation
}
8 changes: 8 additions & 0 deletions priv/graphql/schemas/admin/http_upload.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
Allow admin to generate upload/download URL for a file on user's behalf".
"""
type HttpUploadAdminMutation @protected{
"Allow admin to generate upload/download URLs for a file on user's behalf"
getUrl(domain: String!, filename: String!, size: Int!, contentType: String!, timeout: Int!): FileUrls
@protected(type: DOMAIN, args: ["domain"])
}
11 changes: 11 additions & 0 deletions priv/graphql/schemas/global/http_upload.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Type containing put url and get url for the file
"""
type FileUrls {
"Url to put the file"
PutUrl: String
"Url to get the file"
GetUrl: String
"Http headers"
Header: String
}
7 changes: 7 additions & 0 deletions priv/graphql/schemas/user/http_upload.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Allow user to generate upload/download URL for a file".
"""
type HttpUploadUserMutation @protected{
"Allow user to generate upload/download URLs for a file"
getUrl(filename: String!, size: Int!, contentType: String!, timeout: Int!): FileUrls
}
2 changes: 2 additions & 0 deletions priv/graphql/schemas/user/user_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ type UserMutation @protected{
vcard: VcardUserMutation
"User's private storage management"
private: PrivateUserMutation
"Http upload"
httpUpload: HttpUploadUserMutation
}
41 changes: 3 additions & 38 deletions src/admin_extra/service_admin_extra_upload.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

-include("ejabberd_commands.hrl").

-export([commands/0, get_urls/5]).
-export([commands/0]).

-ignore_xref([commands/0, get_urls/5]).

Expand All @@ -14,45 +14,10 @@ commands() -> [
desc = "Generate upload/download URLs for the file",
longdesc = "Returns upload/download URLs generated by mod_http_upload. Example:\n"
" mongooseimctl http_upload localhost tmp.txt 5 '' 60",
module = ?MODULE,
function = get_urls,
module = mod_http_upload_api,
function = get_urls_mongooseimctl,
args = [{domain, binary}, {file_name, binary}, {size, integer},
{content_type, binary}, {timeout, integer}],
result = {res, restuple}
}
].

-spec get_urls(Domain :: jid:lserver(), Filename :: binary(), Size :: pos_integer(),
ContentType :: binary() | undefined, Timeout :: pos_integer()) ->
{ok, string()} | {error, string()}.
get_urls(_Domain, _Filename, Size, _ContentType, _Timeout) when Size =< 0->
{error, "size must be positive integer"};
get_urls(_Domain, _Filename, _Size, _ContentType, Timeout) when Timeout =< 0->
{error, "timeout must be positive integer"};
get_urls(Domain, Filename, Size, <<"">>, Timeout) ->
get_urls(Domain, Filename, Size, undefined, Timeout);
get_urls(Domain, Filename, Size, ContentType, Timeout) ->
{ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain),
case gen_mod:is_loaded(HostType, mod_http_upload) of
true ->
{PutURL, GetURL, Header} =
mod_http_upload:get_urls(HostType, Filename, Size, ContentType, Timeout),
{ok, generate_output_message(PutURL, GetURL, Header)};
false ->
{error, "mod_http_upload is not loaded for this host"}
end.

-spec generate_output_message(PutURL :: binary(), GetURL :: binary(),
Headers :: #{binary() => binary()}) -> string().
generate_output_message(PutURL, GetURL, Header) ->
PutURLOutput = url_output("PutURL:", PutURL),
GetURLOutput = url_output("GetURL:", GetURL),
HeaderOutput = header_output(Header),
lists:flatten([PutURLOutput, GetURLOutput, HeaderOutput]).

url_output(Name, Url) ->
io_lib:format("~s ~s~n", [Name, Url]).

header_output(Header) when Header =:= #{} -> [];
header_output(Header) ->
io_lib:format("Header: ~p~n", [maps:to_list(Header)]).
2 changes: 2 additions & 0 deletions src/graphql/admin/mongoose_graphql_admin_mutation.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ execute(_Ctx, _Obj, <<"last">>, _Args) ->
{ok, last};
execute(_Ctx, _Obj, <<"muc">>, _Args) ->
{ok, muc};
execute(_Ctx, _Obj, <<"httpUpload">>, _Args) ->
{ok, httpUpload};
execute(_Ctx, _Obj, <<"muc_light">>, _Args) ->
{ok, muc_light};
execute(_Ctx, _Obj, <<"private">>, _Args) ->
Expand Down
23 changes: 23 additions & 0 deletions src/graphql/admin/mongoose_graphql_http_upload_admin_mutation.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-module(mongoose_graphql_http_upload_admin_mutation).
-behaviour(mongoose_graphql).

-export([execute/4]).

-import(mongoose_graphql_helper, [make_error/2]).

-ignore_xref([execute/4]).

-include("../mongoose_graphql_types.hrl").
-include("mongoose.hrl").
-include("jlib.hrl").

execute(_Ctx, httpUpload, <<"getUrl">>, #{<<"domain">> := Domain,
<<"filename">> := FileName,
<<"size">> := FileSize,
<<"contentType">> := ContentType,
<<"timeout">> := Timeout} = Data) ->
case mod_http_upload_api:get_urls(Domain, FileName, FileSize, ContentType, Timeout) of
{ok, _} = Result -> Result;
Error ->
make_error(Error, Data)
end.
Loading

0 comments on commit 9fea697

Please sign in to comment.