feat(s3): separate concepts to make app reusable in bridges

This commit is contained in:
Andrew Mayorov 2024-02-08 22:18:12 +01:00
parent a5266f68ec
commit 4ff04ab1f3
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
10 changed files with 213 additions and 160 deletions

View File

@ -69,11 +69,9 @@
-spec start_export(options(), transfer(), filemeta()) -> -spec start_export(options(), transfer(), filemeta()) ->
{ok, export_st()} | {error, term()}. {ok, export_st()} | {error, term()}.
start_export(_Options, Transfer, Filemeta) -> start_export(_Options, Transfer, Filemeta) ->
Options = #{ Key = s3_key(Transfer, Filemeta),
key => s3_key(Transfer, Filemeta), UploadOpts = #{headers => s3_headers(Transfer, Filemeta)},
headers => s3_headers(Transfer, Filemeta) case emqx_s3:start_uploader(?S3_PROFILE_ID, Key, UploadOpts) of
},
case emqx_s3:start_uploader(?S3_PROFILE_ID, Options) of
{ok, Pid} -> {ok, Pid} ->
true = erlang:link(Pid), true = erlang:link(Pid),
{ok, #{filemeta => Filemeta, pid => Pid}}; {ok, #{filemeta => Filemeta, pid => Pid}};
@ -180,22 +178,24 @@ list_pages(Client, Marker, Limit, Acc) ->
ListOptions = [{marker, Marker} || Marker =/= undefined], ListOptions = [{marker, Marker} || Marker =/= undefined],
case list_key_info(Client, [{max_keys, MaxKeys} | ListOptions]) of case list_key_info(Client, [{max_keys, MaxKeys} | ListOptions]) of
{ok, {Exports, NextMarker}} -> {ok, {Exports, NextMarker}} ->
list_accumulate(Client, Limit, NextMarker, [Exports | Acc]); Left = update_limit(Limit, Exports),
NextAcc = [Exports | Acc],
case NextMarker of
undefined ->
{ok, {flatten_pages(NextAcc), undefined}};
_ when Left =< 0 ->
{ok, {flatten_pages(NextAcc), NextMarker}};
_ ->
list_pages(Client, NextMarker, Left, NextAcc)
end;
{error, _Reason} = Error -> {error, _Reason} = Error ->
Error Error
end. end.
list_accumulate(_Client, _Limit, undefined, Acc) -> update_limit(undefined, _Exports) ->
{ok, {flatten_pages(Acc), undefined}}; undefined;
list_accumulate(Client, undefined, Marker, Acc) -> update_limit(Limit, Exports) ->
list_pages(Client, Marker, undefined, Acc); Limit - length(Exports).
list_accumulate(Client, Limit, Marker, Acc = [Exports | _]) ->
case Limit - length(Exports) of
0 ->
{ok, {flatten_pages(Acc), Marker}};
Left ->
list_pages(Client, Marker, Left, Acc)
end.
flatten_pages(Pages) -> flatten_pages(Pages) ->
lists:append(lists:reverse(Pages)). lists:append(lists:reverse(Pages)).

View File

@ -10,7 +10,7 @@
start_profile/2, start_profile/2,
stop_profile/1, stop_profile/1,
update_profile/2, update_profile/2,
start_uploader/2, start_uploader/3,
with_client/2 with_client/2
]). ]).
@ -22,6 +22,7 @@
-export_type([ -export_type([
profile_id/0, profile_id/0,
profile_config/0, profile_config/0,
transport_options/0,
acl/0 acl/0
]). ]).
@ -81,18 +82,18 @@ stop_profile(ProfileId) when ?IS_PROFILE_ID(ProfileId) ->
update_profile(ProfileId, ProfileConfig) when ?IS_PROFILE_ID(ProfileId) -> update_profile(ProfileId, ProfileConfig) when ?IS_PROFILE_ID(ProfileId) ->
emqx_s3_profile_conf:update_config(ProfileId, ProfileConfig). emqx_s3_profile_conf:update_config(ProfileId, ProfileConfig).
-spec start_uploader(profile_id(), emqx_s3_uploader:opts()) -> -spec start_uploader(profile_id(), emqx_s3_client:key(), emqx_s3_client:upload_options()) ->
emqx_types:startlink_ret() | {error, profile_not_found}. emqx_types:startlink_ret() | {error, profile_not_found}.
start_uploader(ProfileId, Opts) when ?IS_PROFILE_ID(ProfileId) -> start_uploader(ProfileId, Key, Props) when ?IS_PROFILE_ID(ProfileId) ->
emqx_s3_profile_uploader_sup:start_uploader(ProfileId, Opts). emqx_s3_profile_uploader_sup:start_uploader(ProfileId, Key, Props).
-spec with_client(profile_id(), fun((emqx_s3_client:client()) -> Result)) -> -spec with_client(profile_id(), fun((emqx_s3_client:client()) -> Result)) ->
{error, profile_not_found} | Result. {error, profile_not_found} | Result.
with_client(ProfileId, Fun) when is_function(Fun, 1) andalso ?IS_PROFILE_ID(ProfileId) -> with_client(ProfileId, Fun) when is_function(Fun, 1) andalso ?IS_PROFILE_ID(ProfileId) ->
case emqx_s3_profile_conf:checkout_config(ProfileId) of case emqx_s3_profile_conf:checkout_config(ProfileId) of
{ok, ClientConfig, _UploadConfig} -> {Bucket, ClientConfig, _UploadOpts, _UploadConfig} ->
try try
Fun(emqx_s3_client:create(ClientConfig)) Fun(emqx_s3_client:create(Bucket, ClientConfig))
after after
emqx_s3_profile_conf:checkin_config(ProfileId) emqx_s3_profile_conf:checkin_config(ProfileId)
end; end;

View File

@ -9,12 +9,11 @@
-include_lib("erlcloud/include/erlcloud_aws.hrl"). -include_lib("erlcloud/include/erlcloud_aws.hrl").
-export([ -export([
create/1, create/2,
put_object/3, put_object/3,
put_object/4, put_object/4,
start_multipart/2,
start_multipart/3, start_multipart/3,
upload_part/5, upload_part/5,
complete_multipart/4, complete_multipart/4,
@ -26,10 +25,15 @@
format_request/1 format_request/1
]). ]).
%% For connectors
-export([aws_config/1]).
-export_type([ -export_type([
client/0, client/0,
headers/0, headers/0,
bucket/0,
key/0, key/0,
upload_options/0,
upload_id/0, upload_id/0,
etag/0, etag/0,
part_number/0, part_number/0,
@ -39,18 +43,17 @@
-type headers() :: #{binary() | string() => iodata()}. -type headers() :: #{binary() | string() => iodata()}.
-type erlcloud_headers() :: list({string(), iodata()}). -type erlcloud_headers() :: list({string(), iodata()}).
-type bucket() :: string().
-type key() :: string(). -type key() :: string().
-type part_number() :: non_neg_integer(). -type part_number() :: non_neg_integer().
-type upload_id() :: string(). -type upload_id() :: string().
-type etag() :: string(). -type etag() :: string().
-type http_pool() :: ehttpc:pool_name(). -type http_pool() :: ehttpc:pool_name().
-type pool_type() :: random | hash. -type pool_type() :: random | hash.
-type upload_options() :: list({acl, emqx_s3:acl()}).
-opaque client() :: #{ -opaque client() :: #{
aws_config := aws_config(), aws_config := aws_config(),
upload_options := upload_options(), bucket := bucket(),
bucket := string(),
headers := erlcloud_headers(), headers := erlcloud_headers(),
url_expire_time := non_neg_integer(), url_expire_time := non_neg_integer(),
pool_type := pool_type() pool_type := pool_type()
@ -60,9 +63,7 @@
scheme := string(), scheme := string(),
host := string(), host := string(),
port := part_number(), port := part_number(),
bucket := string(),
headers := headers(), headers := headers(),
acl := emqx_s3:acl() | undefined,
url_expire_time := pos_integer(), url_expire_time := pos_integer(),
access_key_id := string() | undefined, access_key_id := string() | undefined,
secret_access_key := emqx_secret:t(string()) | undefined, secret_access_key := emqx_secret:t(string()) | undefined,
@ -72,6 +73,11 @@
max_retries := non_neg_integer() | undefined max_retries := non_neg_integer() | undefined
}. }.
-type upload_options() :: #{
acl => emqx_s3:acl() | undefined,
headers => headers()
}.
-type s3_options() :: proplists:proplist(). -type s3_options() :: proplists:proplist().
-define(DEFAULT_REQUEST_TIMEOUT, 30000). -define(DEFAULT_REQUEST_TIMEOUT, 30000).
@ -81,12 +87,11 @@
%% API %% API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec create(config()) -> client(). -spec create(bucket(), config()) -> client().
create(Config) -> create(Bucket, Config) ->
#{ #{
aws_config => aws_config(Config), aws_config => aws_config(Config),
upload_options => upload_options(Config), bucket => Bucket,
bucket => maps:get(bucket, Config),
url_expire_time => maps:get(url_expire_time, Config), url_expire_time => maps:get(url_expire_time, Config),
headers => headers(Config), headers => headers(Config),
pool_type => maps:get(pool_type, Config) pool_type => maps:get(pool_type, Config)
@ -94,17 +99,19 @@ create(Config) ->
-spec put_object(client(), key(), iodata()) -> ok_or_error(term()). -spec put_object(client(), key(), iodata()) -> ok_or_error(term()).
put_object(Client, Key, Value) -> put_object(Client, Key, Value) ->
put_object(Client, #{}, Key, Value). put_object(Client, Key, #{}, Value).
-spec put_object(client(), headers(), key(), iodata()) -> ok_or_error(term()). -spec put_object(client(), key(), upload_options(), iodata()) -> ok_or_error(term()).
put_object( put_object(
#{bucket := Bucket, upload_options := Options, headers := Headers, aws_config := AwsConfig}, #{bucket := Bucket, headers := BaseHeaders, aws_config := AwsConfig},
SpecialHeaders,
Key, Key,
Value UploadOpts,
Content
) -> ) ->
AllHeaders = join_headers(Headers, SpecialHeaders), ECKey = erlcloud_key(Key),
try erlcloud_s3:put_object(Bucket, erlcloud_key(Key), Value, Options, AllHeaders, AwsConfig) of ECOpts = erlcloud_upload_options(UploadOpts),
Headers = join_headers(BaseHeaders, maps:get(headers, UploadOpts, undefined)),
try erlcloud_s3:put_object(Bucket, ECKey, Content, ECOpts, Headers, AwsConfig) of
Props when is_list(Props) -> Props when is_list(Props) ->
ok ok
catch catch
@ -113,18 +120,16 @@ put_object(
{error, Reason} {error, Reason}
end. end.
-spec start_multipart(client(), key()) -> ok_or_error(upload_id(), term()). -spec start_multipart(client(), key(), upload_options()) -> ok_or_error(upload_id(), term()).
start_multipart(Client, Key) ->
start_multipart(Client, #{}, Key).
-spec start_multipart(client(), headers(), key()) -> ok_or_error(upload_id(), term()).
start_multipart( start_multipart(
#{bucket := Bucket, upload_options := Options, headers := Headers, aws_config := AwsConfig}, #{bucket := Bucket, headers := BaseHeaders, aws_config := AwsConfig},
SpecialHeaders, Key,
Key UploadOpts
) -> ) ->
AllHeaders = join_headers(Headers, SpecialHeaders), ECKey = erlcloud_key(Key),
case erlcloud_s3:start_multipart(Bucket, erlcloud_key(Key), Options, AllHeaders, AwsConfig) of ECOpts = erlcloud_upload_options(UploadOpts),
Headers = join_headers(BaseHeaders, maps:get(headers, UploadOpts, undefined)),
case erlcloud_s3:start_multipart(Bucket, ECKey, ECOpts, Headers, AwsConfig) of
{ok, Props} -> {ok, Props} ->
{ok, response_property('uploadId', Props)}; {ok, response_property('uploadId', Props)};
{error, Reason} -> {error, Reason} ->
@ -204,11 +209,11 @@ format(#{aws_config := AwsConfig} = Client) ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
upload_options(#{acl := Acl}) when Acl =/= undefined -> erlcloud_upload_options(#{acl := Acl}) when Acl =/= undefined ->
[ [
{acl, Acl} {acl, Acl}
]; ];
upload_options(#{}) -> erlcloud_upload_options(#{}) ->
[]. [].
headers(#{headers := Headers}) -> headers(#{headers := Headers}) ->
@ -401,6 +406,8 @@ headers_ehttpc_to_erlcloud_response(EhttpcHeaders) ->
headers_erlcloud_request_to_ehttpc(ErlcloudHeaders) -> headers_erlcloud_request_to_ehttpc(ErlcloudHeaders) ->
[{to_binary(K), V} || {K, V} <- ErlcloudHeaders]. [{to_binary(K), V} || {K, V} <- ErlcloudHeaders].
join_headers(ErlcloudHeaders, undefined) ->
ErlcloudHeaders;
join_headers(ErlcloudHeaders, UserSpecialHeaders) -> join_headers(ErlcloudHeaders, UserSpecialHeaders) ->
ErlcloudHeaders ++ headers_user_to_erlcloud_request(UserSpecialHeaders). ErlcloudHeaders ++ headers_user_to_erlcloud_request(UserSpecialHeaders).

View File

@ -37,13 +37,25 @@
code_change/3 code_change/3
]). ]).
%% For test purposes %% For connectors
-export([ -export([
client_config/2, client_config/2,
http_config/1
]).
%% For test purposes
-export([
start_http_pool/2, start_http_pool/2,
id/1 id/1
]). ]).
-type config_checkout() :: {
emqx_s3_client:bucket(),
emqx_s3_client:config(),
emqx_s3_client:upload_options(),
emqx_s3_uploader:config()
}.
-define(DEFAULT_CALL_TIMEOUT, 5000). -define(DEFAULT_CALL_TIMEOUT, 5000).
-define(DEFAULT_HTTP_POOL_TIMEOUT, 60000). -define(DEFAULT_HTTP_POOL_TIMEOUT, 60000).
@ -78,12 +90,12 @@ update_config(ProfileId, ProfileConfig, Timeout) ->
?SAFE_CALL_VIA_GPROC(ProfileId, {update_config, ProfileConfig}, Timeout). ?SAFE_CALL_VIA_GPROC(ProfileId, {update_config, ProfileConfig}, Timeout).
-spec checkout_config(emqx_s3:profile_id()) -> -spec checkout_config(emqx_s3:profile_id()) ->
{ok, emqx_s3_client:config(), emqx_s3_uploader:config()} | {error, profile_not_found}. config_checkout() | {error, profile_not_found}.
checkout_config(ProfileId) -> checkout_config(ProfileId) ->
checkout_config(ProfileId, ?DEFAULT_CALL_TIMEOUT). checkout_config(ProfileId, ?DEFAULT_CALL_TIMEOUT).
-spec checkout_config(emqx_s3:profile_id(), timeout()) -> -spec checkout_config(emqx_s3:profile_id(), timeout()) ->
{ok, emqx_s3_client:config(), emqx_s3_uploader:config()} | {error, profile_not_found}. config_checkout() | {error, profile_not_found}.
checkout_config(ProfileId, Timeout) -> checkout_config(ProfileId, Timeout) ->
?SAFE_CALL_VIA_GPROC(ProfileId, {checkout_config, self()}, Timeout). ?SAFE_CALL_VIA_GPROC(ProfileId, {checkout_config, self()}, Timeout).
@ -108,6 +120,8 @@ init([ProfileId, ProfileConfig]) ->
{ok, #{ {ok, #{
profile_id => ProfileId, profile_id => ProfileId,
profile_config => ProfileConfig, profile_config => ProfileConfig,
bucket => bucket(ProfileConfig),
upload_options => upload_options(ProfileConfig),
client_config => client_config(ProfileConfig, PoolName), client_config => client_config(ProfileConfig, PoolName),
uploader_config => uploader_config(ProfileConfig), uploader_config => uploader_config(ProfileConfig),
pool_name => PoolName, pool_name => PoolName,
@ -128,12 +142,14 @@ handle_call(
{checkout_config, Pid}, {checkout_config, Pid},
_From, _From,
#{ #{
bucket := Bucket,
upload_options := Options,
client_config := ClientConfig, client_config := ClientConfig,
uploader_config := UploaderConfig uploader_config := UploaderConfig
} = State } = State
) -> ) ->
ok = register_client(Pid, State), ok = register_client(Pid, State),
{reply, {ok, ClientConfig, UploaderConfig}, State}; {reply, {Bucket, ClientConfig, Options, UploaderConfig}, State};
handle_call({checkin_config, Pid}, _From, State) -> handle_call({checkin_config, Pid}, _From, State) ->
ok = unregister_client(Pid, State), ok = unregister_client(Pid, State),
{reply, ok, State}; {reply, ok, State};
@ -146,6 +162,8 @@ handle_call(
{ok, PoolName} -> {ok, PoolName} ->
NewState = State#{ NewState = State#{
profile_config => NewProfileConfig, profile_config => NewProfileConfig,
bucket => bucket(NewProfileConfig),
upload_options => upload_options(NewProfileConfig),
client_config => client_config(NewProfileConfig, PoolName), client_config => client_config(NewProfileConfig, PoolName),
uploader_config => uploader_config(NewProfileConfig), uploader_config => uploader_config(NewProfileConfig),
http_pool_timeout => http_pool_timeout(NewProfileConfig), http_pool_timeout => http_pool_timeout(NewProfileConfig),
@ -198,8 +216,6 @@ client_config(ProfileConfig, PoolName) ->
port => maps:get(port, ProfileConfig), port => maps:get(port, ProfileConfig),
url_expire_time => maps:get(url_expire_time, ProfileConfig), url_expire_time => maps:get(url_expire_time, ProfileConfig),
headers => maps:get(headers, HTTPOpts, #{}), headers => maps:get(headers, HTTPOpts, #{}),
acl => maps:get(acl, ProfileConfig, undefined),
bucket => maps:get(bucket, ProfileConfig),
access_key_id => maps:get(access_key_id, ProfileConfig, undefined), access_key_id => maps:get(access_key_id, ProfileConfig, undefined),
secret_access_key => maps:get(secret_access_key, ProfileConfig, undefined), secret_access_key => maps:get(secret_access_key, ProfileConfig, undefined),
request_timeout => maps:get(request_timeout, HTTPOpts, undefined), request_timeout => maps:get(request_timeout, HTTPOpts, undefined),
@ -214,6 +230,12 @@ uploader_config(#{max_part_size := MaxPartSize, min_part_size := MinPartSize} =
max_part_size => MaxPartSize max_part_size => MaxPartSize
}. }.
bucket(ProfileConfig) ->
maps:get(bucket, ProfileConfig).
upload_options(ProfileConfig) ->
#{acl => maps:get(acl, ProfileConfig, undefined)}.
scheme(#{ssl := #{enable := true}}) -> "https://"; scheme(#{ssl := #{enable := true}}) -> "https://";
scheme(_TransportOpts) -> "http://". scheme(_TransportOpts) -> "http://".

View File

@ -15,7 +15,7 @@
start_link/1, start_link/1,
child_spec/1, child_spec/1,
id/1, id/1,
start_uploader/2 start_uploader/3
]). ]).
-export([init/1]). -export([init/1]).
@ -43,10 +43,10 @@ child_spec(ProfileId) ->
id(ProfileId) -> id(ProfileId) ->
{?MODULE, ProfileId}. {?MODULE, ProfileId}.
-spec start_uploader(emqx_s3:profile_id(), emqx_s3_uploader:opts()) -> -spec start_uploader(emqx_s3:profile_id(), emqx_s3_client:key(), emqx_s3_client:upload_options()) ->
emqx_types:startlink_ret() | {error, profile_not_found}. emqx_types:startlink_ret() | {error, profile_not_found}.
start_uploader(ProfileId, Opts) -> start_uploader(ProfileId, Key, UploadOpts) ->
try supervisor:start_child(?VIA_GPROC(id(ProfileId)), [Opts]) of try supervisor:start_child(?VIA_GPROC(id(ProfileId)), [Key, UploadOpts]) of
Result -> Result Result -> Result
catch catch
exit:{noproc, _} -> {error, profile_not_found} exit:{noproc, _} -> {error, profile_not_found}

View File

@ -23,6 +23,13 @@ tags() ->
[<<"S3">>]. [<<"S3">>].
fields(s3) -> fields(s3) ->
lists:append([
fields(s3_client),
fields(s3_uploader),
fields(s3_url_options),
props_with([bucket, acl], fields(s3_upload))
]);
fields(s3_client) ->
[ [
{access_key_id, {access_key_id,
mk( mk(
@ -38,14 +45,6 @@ fields(s3) ->
desc => ?DESC("secret_access_key") desc => ?DESC("secret_access_key")
} }
)}, )},
{bucket,
mk(
string(),
#{
desc => ?DESC("bucket"),
required => true
}
)},
{host, {host,
mk( mk(
string(), string(),
@ -62,16 +61,51 @@ fields(s3) ->
required => true required => true
} }
)}, )},
{url_expire_time, {transport_options,
mk( mk(
%% not used in a `receive ... after' block, just timestamp comparison ref(?MODULE, transport_options),
emqx_schema:duration_s(),
#{ #{
default => <<"1h">>, desc => ?DESC("transport_options"),
desc => ?DESC("url_expire_time"),
required => false required => false
} }
)}
];
fields(s3_upload) ->
[
{bucket,
mk(
string(),
#{
desc => ?DESC("bucket"),
required => true
}
)}, )},
{key,
mk(
string(),
#{
desc => ?DESC("key"),
required => true
}
)},
{acl,
mk(
hoconsc:enum([
private,
public_read,
public_read_write,
authenticated_read,
bucket_owner_read,
bucket_owner_full_control
]),
#{
desc => ?DESC("acl"),
required => false
}
)}
];
fields(s3_uploader) ->
[
{min_part_size, {min_part_size,
mk( mk(
emqx_schema:bytesize(), emqx_schema:bytesize(),
@ -91,27 +125,17 @@ fields(s3) ->
required => true, required => true,
validator => fun part_size_validator/1 validator => fun part_size_validator/1
} }
)}, )}
{acl, ];
fields(s3_url_options) ->
[
{url_expire_time,
mk( mk(
hoconsc:enum([ %% not used in a `receive ... after' block, just timestamp comparison
private, emqx_schema:duration_s(),
public_read,
public_read_write,
authenticated_read,
bucket_owner_read,
bucket_owner_full_control
]),
#{ #{
desc => ?DESC("acl"), default => <<"1h">>,
required => false desc => ?DESC("url_expire_time"),
}
)},
{transport_options,
mk(
ref(?MODULE, transport_options),
#{
desc => ?DESC("transport_options"),
required => false required => false
} }
)} )}
@ -138,6 +162,10 @@ fields(transport_options) ->
desc(s3) -> desc(s3) ->
"S3 connection options"; "S3 connection options";
desc(s3_client) ->
"S3 connection options";
desc(s3_upload) ->
"S3 upload options";
desc(transport_options) -> desc(transport_options) ->
"Options for the HTTP transport layer used by the S3 client". "Options for the HTTP transport layer used by the S3 client".

View File

@ -9,7 +9,7 @@
-behaviour(gen_statem). -behaviour(gen_statem).
-export([ -export([
start_link/2, start_link/3,
write/2, write/2,
write/3, write/3,
@ -33,30 +33,25 @@
format_status/2 format_status/2
]). ]).
-export_type([opts/0, config/0]). -export_type([config/0]).
-type config() :: #{ -type config() :: #{
min_part_size => pos_integer(), min_part_size => pos_integer(),
max_part_size => pos_integer() max_part_size => pos_integer()
}. }.
-type opts() :: #{
key := string(),
headers => emqx_s3_client:headers()
}.
-type data() :: #{ -type data() :: #{
profile_id := emqx_s3:profile_id(), profile_id => emqx_s3:profile_id(),
client := emqx_s3_client:client(), client := emqx_s3_client:client(),
key := emqx_s3_client:key(), key := emqx_s3_client:key(),
upload_opts := emqx_s3_client:upload_options(),
buffer := iodata(), buffer := iodata(),
buffer_size := non_neg_integer(), buffer_size := non_neg_integer(),
min_part_size := pos_integer(), min_part_size := pos_integer(),
max_part_size := pos_integer(), max_part_size := pos_integer(),
upload_id := undefined | emqx_s3_client:upload_id(), upload_id := undefined | emqx_s3_client:upload_id(),
etags := [emqx_s3_client:etag()], etags := [emqx_s3_client:etag()],
part_number := emqx_s3_client:part_number(), part_number := emqx_s3_client:part_number()
headers := emqx_s3_client:headers()
}. }.
%% 5MB %% 5MB
@ -66,9 +61,10 @@
-define(DEFAULT_TIMEOUT, 30000). -define(DEFAULT_TIMEOUT, 30000).
-spec start_link(emqx_s3:profile_id(), opts()) -> gen_statem:start_ret(). -spec start_link(emqx_s3:profile_id(), emqx_s3_client:key(), emqx_s3_client:upload_options()) ->
start_link(ProfileId, #{key := Key} = Opts) when is_list(Key) -> gen_statem:start_ret().
gen_statem:start_link(?MODULE, [ProfileId, Opts], []). start_link(ProfileId, Key, UploadOpts) when is_list(Key) ->
gen_statem:start_link(?MODULE, {profile, ProfileId, Key, UploadOpts}, []).
-spec write(pid(), iodata()) -> ok_or_error(term()). -spec write(pid(), iodata()) -> ok_or_error(term()).
write(Pid, WriteData) -> write(Pid, WriteData) ->
@ -105,19 +101,23 @@ shutdown(Pid) ->
callback_mode() -> handle_event_function. callback_mode() -> handle_event_function.
init([ProfileId, #{key := Key} = Opts]) -> init({profile, ProfileId, Key, UploadOpts}) ->
process_flag(trap_exit, true), {Bucket, ClientConfig, BaseOpts, UploaderConfig} =
{ok, ClientConfig, UploaderConfig} = emqx_s3_profile_conf:checkout_config(ProfileId), emqx_s3_profile_conf:checkout_config(ProfileId),
Client = client(ClientConfig), Upload = #{
{ok, upload_not_started, #{
profile_id => ProfileId, profile_id => ProfileId,
client => Client, client => client(Bucket, ClientConfig),
headers => maps:get(headers, Opts, #{}),
key => Key, key => Key,
upload_opts => maps:merge(BaseOpts, UploadOpts)
},
init({upload, UploaderConfig, Upload});
init({upload, Config, Upload}) ->
process_flag(trap_exit, true),
{ok, upload_not_started, Upload#{
buffer => [], buffer => [],
buffer_size => 0, buffer_size => 0,
min_part_size => maps:get(min_part_size, UploaderConfig, ?DEFAULT_MIN_PART_SIZE), min_part_size => maps:get(min_part_size, Config, ?DEFAULT_MIN_PART_SIZE),
max_part_size => maps:get(max_part_size, UploaderConfig, ?DEFAULT_MAX_PART_SIZE), max_part_size => maps:get(max_part_size, Config, ?DEFAULT_MAX_PART_SIZE),
upload_id => undefined, upload_id => undefined,
etags => [], etags => [],
part_number => 1 part_number => 1
@ -221,8 +221,8 @@ maybe_start_upload(#{buffer_size := BufferSize, min_part_size := MinPartSize} =
end. end.
-spec start_upload(data()) -> {started, data()} | {error, term()}. -spec start_upload(data()) -> {started, data()} | {error, term()}.
start_upload(#{client := Client, key := Key, headers := Headers} = Data) -> start_upload(#{client := Client, key := Key, upload_opts := UploadOpts} = Data) ->
case emqx_s3_client:start_multipart(Client, Headers, Key) of case emqx_s3_client:start_multipart(Client, Key, UploadOpts) of
{ok, UploadId} -> {ok, UploadId} ->
NewData = Data#{upload_id => UploadId}, NewData = Data#{upload_id => UploadId},
{started, NewData}; {started, NewData};
@ -274,12 +274,9 @@ complete_upload(
} = Data0 } = Data0
) -> ) ->
case upload_part(Data0) of case upload_part(Data0) of
{ok, #{etags := ETags} = Data1} -> {ok, #{etags := ETagsRev} = Data1} ->
case ETags = lists:reverse(ETagsRev),
emqx_s3_client:complete_multipart( case emqx_s3_client:complete_multipart(Client, Key, UploadId, ETags) of
Client, Key, UploadId, lists:reverse(ETags)
)
of
ok -> ok ->
{ok, Data1}; {ok, Data1};
{error, _} = Error -> {error, _} = Error ->
@ -309,11 +306,11 @@ put_object(
#{ #{
client := Client, client := Client,
key := Key, key := Key,
buffer := Buffer, upload_opts := UploadOpts,
headers := Headers buffer := Buffer
} }
) -> ) ->
case emqx_s3_client:put_object(Client, Headers, Key, Buffer) of case emqx_s3_client:put_object(Client, Key, UploadOpts, Buffer) of
ok -> ok ->
ok; ok;
{error, _} = Error -> {error, _} = Error ->
@ -337,5 +334,5 @@ unwrap(WrappedData) ->
is_valid_part(WriteData, #{max_part_size := MaxPartSize, buffer_size := BufferSize}) -> is_valid_part(WriteData, #{max_part_size := MaxPartSize, buffer_size := BufferSize}) ->
BufferSize + iolist_size(WriteData) =< MaxPartSize. BufferSize + iolist_size(WriteData) =< MaxPartSize.
client(Config) -> client(Bucket, Config) ->
emqx_s3_client:create(Config). emqx_s3_client:create(Bucket, Config).

View File

@ -79,7 +79,7 @@ t_multipart_upload(Config) ->
Client = client(Config), Client = client(Config),
{ok, UploadId} = emqx_s3_client:start_multipart(Client, Key), {ok, UploadId} = emqx_s3_client:start_multipart(Client, Key, #{}),
Data = data(6_000_000), Data = data(6_000_000),
@ -97,7 +97,7 @@ t_simple_put(Config) ->
Data = data(6_000_000), Data = data(6_000_000),
ok = emqx_s3_client:put_object(Client, Key, Data). ok = emqx_s3_client:put_object(Client, Key, #{acl => private}, Data).
t_list(Config) -> t_list(Config) ->
Key = ?config(key, Config), Key = ?config(key, Config),
@ -123,7 +123,7 @@ t_url(Config) ->
Key = ?config(key, Config), Key = ?config(key, Config),
Client = client(Config), Client = client(Config),
ok = emqx_s3_client:put_object(Client, Key, <<"data">>), ok = emqx_s3_client:put_object(Client, Key, #{acl => public_read}, <<"data">>),
Url = emqx_s3_client:uri(Client, Key), Url = emqx_s3_client:uri(Client, Key),
@ -135,20 +135,18 @@ t_url(Config) ->
t_no_acl(Config) -> t_no_acl(Config) ->
Key = ?config(key, Config), Key = ?config(key, Config),
ClientConfig = emqx_s3_profile_conf:client_config( Client = client(Config),
profile_config(Config), ?config(ehttpc_pool_name, Config)
),
Client = emqx_s3_client:create(maps:without([acl], ClientConfig)),
ok = emqx_s3_client:put_object(Client, Key, <<"data">>). ok = emqx_s3_client:put_object(Client, Key, #{}, <<"data">>).
t_extra_headers(Config0) -> t_extra_headers(Config0) ->
Config = [{extra_headers, #{'Content-Type' => <<"application/json">>}} | Config0], Config = [{extra_headers, #{'Content-Type' => <<"application/json">>}} | Config0],
Key = ?config(key, Config), Key = ?config(key, Config),
Client = client(Config), Client = client(Config),
Opts = #{acl => public_read},
Data = #{foo => bar}, Data = #{foo => bar},
ok = emqx_s3_client:put_object(Client, Key, emqx_utils_json:encode(Data)), ok = emqx_s3_client:put_object(Client, Key, Opts, emqx_utils_json:encode(Data)),
Url = emqx_s3_client:uri(Client, Key), Url = emqx_s3_client:uri(Client, Key),
@ -164,10 +162,11 @@ t_extra_headers(Config0) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
client(Config) -> client(Config) ->
Bucket = ?config(bucket, Config),
ClientConfig = emqx_s3_profile_conf:client_config( ClientConfig = emqx_s3_profile_conf:client_config(
profile_config(Config), ?config(ehttpc_pool_name, Config) profile_config(Config), ?config(ehttpc_pool_name, Config)
), ),
emqx_s3_client:create(ClientConfig). emqx_s3_client:create(Bucket, ClientConfig).
profile_config(Config) -> profile_config(Config) ->
ProfileConfig0 = emqx_s3_test_helpers:base_config(?config(conn_type, Config)), ProfileConfig0 = emqx_s3_test_helpers:base_config(?config(conn_type, Config)),
@ -175,7 +174,6 @@ profile_config(Config) ->
fun inject_config/3, fun inject_config/3,
ProfileConfig0, ProfileConfig0,
#{ #{
bucket => ?config(bucket, Config),
[transport_options, pool_type] => ?config(pool_type, Config), [transport_options, pool_type] => ?config(pool_type, Config),
[transport_options, headers] => ?config(extra_headers, Config) [transport_options, headers] => ?config(extra_headers, Config)
} }

View File

@ -46,7 +46,7 @@ end_per_testcase(_TestCase, _Config) ->
t_regular_outdated_pool_cleanup(Config) -> t_regular_outdated_pool_cleanup(Config) ->
_ = process_flag(trap_exit, true), _ = process_flag(trap_exit, true),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
[OldPool] = emqx_s3_profile_http_pools:all(profile_id()), [OldPool] = emqx_s3_profile_http_pools:all(profile_id()),
@ -94,7 +94,7 @@ t_timeout_pool_cleanup(Config) ->
%% Start uploader %% Start uploader
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
ok = emqx_s3_uploader:write(Pid, <<"data">>), ok = emqx_s3_uploader:write(Pid, <<"data">>),
[OldPool] = emqx_s3_profile_http_pools:all(profile_id()), [OldPool] = emqx_s3_profile_http_pools:all(profile_id()),

View File

@ -133,7 +133,7 @@ end_per_testcase(_TestCase, _Config) ->
t_happy_path_simple_put(Config) -> t_happy_path_simple_put(Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -165,7 +165,7 @@ t_happy_path_simple_put(Config) ->
t_happy_path_multi(Config) -> t_happy_path_multi(Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -233,7 +233,7 @@ t_signed_nonascii_url_download(_Config) ->
t_abort_multi(Config) -> t_abort_multi(Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -260,7 +260,7 @@ t_abort_multi(Config) ->
t_abort_simple_put(_Config) -> t_abort_simple_put(_Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -278,7 +278,7 @@ t_abort_simple_put(_Config) ->
t_config_switch(Config) -> t_config_switch(Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
OldBucket = ?config(bucket, Config), OldBucket = ?config(bucket, Config),
{ok, Pid0} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid0} = emqx_s3:start_uploader(profile_id(), Key, #{}),
[Data0, Data1] = data($a, 6 * 1024 * 1024, 2), [Data0, Data1] = data($a, 6 * 1024 * 1024, 2),
@ -304,7 +304,7 @@ t_config_switch(Config) ->
), ),
%% Now check that new uploader uses new config %% Now check that new uploader uses new config
{ok, Pid1} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid1} = emqx_s3:start_uploader(profile_id(), Key, #{}),
ok = emqx_s3_uploader:write(Pid1, Data0), ok = emqx_s3_uploader:write(Pid1, Data0),
ok = emqx_s3_uploader:complete(Pid1), ok = emqx_s3_uploader:complete(Pid1),
@ -318,7 +318,7 @@ t_config_switch(Config) ->
t_config_switch_http_settings(Config) -> t_config_switch_http_settings(Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
OldBucket = ?config(bucket, Config), OldBucket = ?config(bucket, Config),
{ok, Pid0} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid0} = emqx_s3:start_uploader(profile_id(), Key, #{}),
[Data0, Data1] = data($a, 6 * 1024 * 1024, 2), [Data0, Data1] = data($a, 6 * 1024 * 1024, 2),
@ -345,7 +345,7 @@ t_config_switch_http_settings(Config) ->
), ),
%% Now check that new uploader uses new config %% Now check that new uploader uses new config
{ok, Pid1} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid1} = emqx_s3:start_uploader(profile_id(), Key, #{}),
ok = emqx_s3_uploader:write(Pid1, Data0), ok = emqx_s3_uploader:write(Pid1, Data0),
ok = emqx_s3_uploader:complete(Pid1), ok = emqx_s3_uploader:complete(Pid1),
@ -360,7 +360,7 @@ t_start_multipart_error(Config) ->
_ = process_flag(trap_exit, true), _ = process_flag(trap_exit, true),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -386,7 +386,7 @@ t_upload_part_error(Config) ->
_ = process_flag(trap_exit, true), _ = process_flag(trap_exit, true),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -414,7 +414,7 @@ t_abort_multipart_error(Config) ->
_ = process_flag(trap_exit, true), _ = process_flag(trap_exit, true),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -442,7 +442,7 @@ t_complete_multipart_error(Config) ->
_ = process_flag(trap_exit, true), _ = process_flag(trap_exit, true),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -470,7 +470,7 @@ t_put_object_error(Config) ->
_ = process_flag(trap_exit, true), _ = process_flag(trap_exit, true),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -496,7 +496,7 @@ t_put_object_error(Config) ->
t_too_large(Config) -> t_too_large(Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -533,7 +533,7 @@ t_tls_error(Config) ->
), ),
ok = emqx_s3:update_profile(profile_id(), ProfileConfig), ok = emqx_s3:update_profile(profile_id(), ProfileConfig),
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),
@ -553,7 +553,7 @@ t_no_profile(_Config) ->
Key = emqx_s3_test_helpers:unique_key(), Key = emqx_s3_test_helpers:unique_key(),
?assertMatch( ?assertMatch(
{error, profile_not_found}, {error, profile_not_found},
emqx_s3:start_uploader(<<"no-profile">>, #{key => Key}) emqx_s3:start_uploader(<<"no-profile">>, Key, #{})
). ).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -572,7 +572,7 @@ list_objects(Config) ->
proplists:get_value(contents, Props). proplists:get_value(contents, Props).
upload(Key, ChunkSize, ChunkCount) -> upload(Key, ChunkSize, ChunkCount) ->
{ok, Pid} = emqx_s3:start_uploader(profile_id(), #{key => Key}), {ok, Pid} = emqx_s3:start_uploader(profile_id(), Key, #{}),
_ = erlang:monitor(process, Pid), _ = erlang:monitor(process, Pid),