%% Amazon Simple Storage Service (S3) -module(erlcloud_s3). -export([new/2, new/3, new/4, configure/2, configure/3, configure/4, configure/5, create_bucket/1, create_bucket/2, create_bucket/3, create_bucket/4, check_bucket_access/1, check_bucket_access/2, delete_bucket/1, delete_bucket/2, get_bucket_attribute/2, get_bucket_attribute/3, list_buckets/0, list_buckets/1, set_bucket_attribute/3, set_bucket_attribute/4, get_bucket_policy/1, get_bucket_policy/2, put_bucket_policy/2, put_bucket_policy/3, get_bucket_lifecycle/1, get_bucket_lifecycle/2, put_bucket_lifecycle/2, put_bucket_lifecycle/3, delete_bucket_lifecycle/1, delete_bucket_lifecycle/2, list_objects/1, list_objects/2, list_objects/3, list_object_versions/1, list_object_versions/2, list_object_versions/3, copy_object/4, copy_object/5, copy_object/6, delete_objects_batch/2, delete_objects_batch/3, explore_dirstructure/3, explore_dirstructure/4, delete_object/2, delete_object/3, delete_object_version/3, delete_object_version/4, head_object/2, head_object/3, head_object/4, get_object/2, get_object/3, get_object/4, get_object_acl/2, get_object_acl/3, get_object_acl/4, get_object_torrent/2, get_object_torrent/3, get_object_metadata/2, get_object_metadata/3, get_object_metadata/4, put_object/3, put_object/4, put_object/5, put_object/6, set_object_acl/3, set_object_acl/4, make_link/3, make_link/4, make_get_url/3, make_get_url/4, start_multipart/2, start_multipart/5, upload_part/5, upload_part/7, complete_multipart/4, complete_multipart/6, abort_multipart/3, abort_multipart/6, list_multipart_uploads/1, list_multipart_uploads/2, list_multipart_uploads/4, get_object_url/2, get_object_url/3, get_bucket_and_key/1, list_bucket_inventory/1, list_bucket_inventory/2, list_bucket_inventory/3, get_bucket_inventory/2, get_bucket_inventory/3, put_bucket_inventory/2, put_bucket_inventory/3, delete_bucket_inventory/2, delete_bucket_inventory/3, put_bucket_encryption/2, put_bucket_encryption/3, put_bucket_encryption/4, get_bucket_encryption/1, get_bucket_encryption/2, delete_bucket_encryption/1, delete_bucket_encryption/2 ]). -ifdef(TEST). -export([encode_lifecycle/1]). -export([get_bucket_notification/1]). -export([create_notification_xml/1]). -export([create_notification_param_xml/2]). -export([encode_inventory/1]). -endif. -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). -include_lib("xmerl/include/xmerl.hrl"). %%% Note that get_bucket_and_key/1 may be used to obtain the Bucket and Key to pass to various %%% functions here, from a URL such as https://some_bucket.s3.amazonaws.com/path_to_file -spec new(string(), string()) -> aws_config(). new(AccessKeyID, SecretAccessKey) -> #aws_config{ access_key_id=AccessKeyID, secret_access_key=SecretAccessKey }. -spec new(string(), string(), string()) -> aws_config(). new(AccessKeyID, SecretAccessKey, Host) -> #aws_config{ access_key_id=AccessKeyID, secret_access_key=SecretAccessKey, s3_host=Host }. -spec new(string(), string(), string(), non_neg_integer()) -> aws_config(). new(AccessKeyID, SecretAccessKey, Host, Port) -> #aws_config{ access_key_id=AccessKeyID, secret_access_key=SecretAccessKey, s3_host=Host, s3_port=Port }. -spec new(string(), string(), string(), non_neg_integer(), string()) -> aws_config(). new(AccessKeyID, SecretAccessKey, Host, Port, Scheme) -> #aws_config{ access_key_id=AccessKeyID, secret_access_key=SecretAccessKey, s3_host=Host, s3_port=Port, s3_scheme=Scheme }. -spec configure(string(), string()) -> ok. configure(AccessKeyID, SecretAccessKey) -> erlcloud_config:configure(AccessKeyID, SecretAccessKey, fun new/2). -spec configure(string(), string(), string()) -> ok. configure(AccessKeyID, SecretAccessKey, Host) -> erlcloud_config:configure(AccessKeyID, SecretAccessKey, Host, fun new/3). -spec configure(string(), string(), string(), non_neg_integer()) -> ok. configure(AccessKeyID, SecretAccessKey, Host, Port) -> erlcloud_config:configure(AccessKeyID, SecretAccessKey, Host, Port, fun new/4). -spec configure(string(), string(), string(), non_neg_integer(), string()) -> ok. configure(AccessKeyID, SecretAccessKey, Host, Port, Scheme) -> erlcloud_config:configure(AccessKeyID, SecretAccessKey, Host, Port, Scheme, fun new/5). -type s3_bucket_attribute_name() :: acl | location | logging | request_payment | versioning | notification. -type s3_bucket_acl() :: private | public_read | public_read_write | authenticated_read | bucket_owner_read | bucket_owner_full_control. -type s3_location_constraint() :: none | us_west_1 | eu | 'us-east-1' | 'us-east-2' | 'us-west-1' | 'eu-west-1' | 'eu-west-2' | 'eu-central-1' | 'ap-south-1' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-northeast-1' | 'ap-northeast-2' | 'sa-east-1'. -define(XMLNS_S3, "http://doc.s3.amazonaws.com/2006-03-01/"). -define(XMLNS_SCHEMA_INSTANCE, "http://www.w3.org/2001/XMLSchema-instance"). -spec copy_object(string(), string(), string(), string()) -> proplist() | no_return(). copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName) -> copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, []). -spec copy_object(string(), string(), string(), string(), proplist() | aws_config()) -> proplist() | no_return(). copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Config) when is_record(Config, aws_config) -> copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, [], Config); copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options) -> copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options, default_config()). -spec copy_object(string(), string(), string(), string(), proplist(), aws_config()) -> proplist() | no_return(). copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options, Config) -> SrcVersion = case proplists:get_value(version_id, Options) of undefined -> ""; VersionID -> ["?versionId=", VersionID] end, RequestHeaders = [{"x-amz-copy-source", [SrcBucketName, $/, SrcKeyName, SrcVersion]}, {"x-amz-metadata-directive", proplists:get_value(metadata_directive, Options)}, {"x-amz-copy-source-if-match", proplists:get_value(if_match, Options)}, {"x-amz-copy-source-if-none-match", proplists:get_value(if_none_match, Options)}, {"x-amz-copy-source-if-unmodified-since", proplists:get_value(if_unmodified_since, Options)}, {"x-amz-copy-source-if-modified-since", proplists:get_value(if_modified_since, Options)}, {"x-amz-acl", encode_acl(proplists:get_value(acl, Options))}], {Headers, _Body} = s3_request(Config, put, DestBucketName, [$/|DestKeyName], "", [], <<>>, RequestHeaders), [{copy_source_version_id, proplists:get_value("x-amz-copy-source-version-id", Headers, "false")}, {version_id, proplists:get_value("x-amz-version-id", Headers, "null")}]. -spec create_bucket(string()) -> ok | no_return(). create_bucket(BucketName) -> create_bucket(BucketName, private). -spec create_bucket(string(), s3_bucket_acl() | aws_config()) -> ok | no_return(). create_bucket(BucketName, Config) when is_record(Config, aws_config) -> create_bucket(BucketName, private, Config); create_bucket(BucketName, ACL) -> create_bucket(BucketName, ACL, none). -spec create_bucket(string(), s3_bucket_acl(), s3_location_constraint() | aws_config()) -> ok | no_return(). create_bucket(BucketName, ACL, Config) when is_record(Config, aws_config) -> create_bucket(BucketName, ACL, none, Config); create_bucket(BucketName, ACL, LocationConstraint) -> create_bucket(BucketName, ACL, LocationConstraint, default_config()). -spec create_bucket(string(), s3_bucket_acl(), s3_location_constraint(), aws_config()) -> ok | no_return(). create_bucket(BucketName, ACL, LocationConstraint, Config) when is_list(BucketName), is_atom(ACL), is_atom(LocationConstraint) -> Headers = case ACL of private -> []; %% private is the default _ -> [{"x-amz-acl", encode_acl(ACL)}] end, POSTData = case encode_location_constraint(LocationConstraint) of undefined -> <<>>; LocationName -> XML = {'CreateBucketConfiguration', [{xmlns, ?XMLNS_S3}], [{'LocationConstraint', [LocationName]}]}, list_to_binary(xmerl:export_simple([XML], xmerl_xml)) end, s3_simple_request(Config, put, BucketName, "/", "", [], POSTData, Headers). encode_location_constraint(eu) -> "EU"; encode_location_constraint(us_west_1) -> "us-west-1"; encode_location_constraint('us-east-1') -> undefined; encode_location_constraint('us-east-2') -> "us-east-2"; encode_location_constraint('us-west-1') -> "us-west-1"; encode_location_constraint('us-west-2') -> "us-west-2"; encode_location_constraint('eu-west-1') -> "eu-west-1"; encode_location_constraint('eu-west-2') -> "eu-west-2"; encode_location_constraint('eu-central-1') -> "eu-central-1"; encode_location_constraint('ap-south-1') -> "ap-south-1"; encode_location_constraint('ap-southeast-1') -> "ap-southeast-1"; encode_location_constraint('ap-southeast-2') -> "ap-southeast-2"; encode_location_constraint('ap-northeast-1') -> "ap-northeast-1"; encode_location_constraint('ap-northeast-2') -> "ap-northeast-2"; encode_location_constraint('sa-east-1') -> "sa-east-1"; encode_location_constraint(_) -> undefined. encode_acl(undefined) -> undefined; encode_acl(private) -> "private"; encode_acl(public_read) -> "public-read"; encode_acl(public_read_write) -> "public-read-write"; encode_acl(authenticated_read) -> "authenticated-read"; encode_acl(bucket_owner_read) -> "bucket-owner-read"; encode_acl(bucket_owner_full_control) -> "bucket-owner-full-control". -spec delete_bucket(string()) -> ok | no_return(). delete_bucket(BucketName) -> delete_bucket(BucketName, default_config()). -spec delete_bucket(string(), aws_config()) -> ok | no_return(). delete_bucket(BucketName, Config) when is_list(BucketName) -> s3_simple_request(Config, delete, BucketName, "/", "", [], <<>>, []). -spec check_bucket_access(string()) -> ok | {error, any()}. check_bucket_access(BucketName) when is_list(BucketName) -> check_bucket_access(BucketName, default_config()). -spec check_bucket_access(string(), aws_config()) -> ok | {error, any()}. check_bucket_access(BucketName, Config) when is_list(BucketName), is_record(Config, aws_config) -> case s3_request2(Config, head, BucketName, "/", "", [], <<>>, []) of {ok, {_Headers, _Body}} -> ok; Error -> Error end. -spec delete_objects_batch(string(), list()) -> proplist() | no_return(). delete_objects_batch(BucketName, KeyList) -> delete_objects_batch(BucketName, KeyList, default_config()). -spec delete_objects_batch(string(), list(), aws_config()) -> proplist() | no_return(). delete_objects_batch(BucketName, KeyList, Config) when is_list(BucketName), is_list(KeyList) -> Data = lists:map(fun(Item) -> lists:concat(["", Item, ""]) end, KeyList), Payload = unicode:characters_to_list( "" ++ Data ++ "", utf8), Headers = [{"content-md5", base64:encode(erlcloud_util:md5(Payload))}, {"content-length", integer_to_list(string:len(Payload))}, {"content-type", "application/xml"}], Doc = s3_xml_request(Config, post, BucketName, [$/], "delete", [], Payload, Headers), Attributes = [{deleted, "Deleted", fun extract_delete_objects_batch_key_contents/1}, {error, "Error", fun extract_delete_objects_batch_err_contents/1}], erlcloud_xml:decode(Attributes, Doc). extract_delete_objects_batch_key_contents(Nodes) -> Attributes = [{key, "Key", text}], [Key || X <- [erlcloud_xml:decode(Attributes, Node) || Node <- Nodes], {_,Key} <- X]. extract_delete_objects_batch_err_contents(Nodes) -> Attributes = [ {key, "Key", text}, {code, "Code", text}, {message, "Message", text}], [to_flat_format(erlcloud_xml:decode(Attributes, Node)) || Node <- Nodes]. to_flat_format([{key,Key},{code,Code},{message,Message}]) -> {Key,Code,Message}. %% returns paths list from AWS S3 root directory, used as input to %% delete_objects_batch %% example : %% 25> rp(erlcloud_s3:explore_dirstructure("xmppfiledev", %% ["sailfish/deleteme"], [])). %% ["sailfish/deleteme/deep/deep1/deep4/ZZZ_1.txt", %% "sailfish/deleteme/deep/deep1/deep4/ZZZ_0.txt", %% "sailfish/deleteme/deep/deep1/ZZZ_0.txt", %% "sailfish/deleteme/deep/ZZZ_0.txt"] %% ok %% -spec explore_dirstructure(string(), list(), list()) -> list() | no_return(). explore_dirstructure(Bucketname, Branches, Accum) -> explore_dirstructure(Bucketname, Branches, Accum, default_config()). -spec explore_dirstructure(string(), list(), list(), aws_config()) -> list() | no_return(). explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config) -> explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config, []). explore_dirstructure(_, [], Result, _Config, _Marker) -> lists:append(Result); explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config, Marker) when is_record(Config, aws_config) -> ProcessContent = fun(Data)-> Content = proplists:get_value(contents, Data), lists:foldl(fun(I,Acc)-> R = proplists:get_value(key, I), [R|Acc] end, [], Content) end, Data = list_objects(Bucketname, [{prefix, Branch}, {delimiter, "/"}, {marker, Marker}], Config), Files = ProcessContent(Data), Sub = proplists:get_value(common_prefixes, Data), SubDirs = lists:foldl(fun(I,Acc)-> R = proplists:get_value(prefix, I), [R|Acc] end, [], Sub), SubFiles = explore_dirstructure(Bucketname, SubDirs, [], Config, []), TruncFiles = case proplists:get_value(is_truncated, Data) of false -> []; true -> NextMarker = proplists:get_value(next_marker, Data), explore_dirstructure(Bucketname, [Branch], [], Config, NextMarker) end, explore_dirstructure(Bucketname, Tail, [SubFiles, TruncFiles, Files|Accum], Config, []). -spec delete_object(string(), string()) -> proplist() | no_return(). delete_object(BucketName, Key) -> delete_object(BucketName, Key, default_config()). -spec delete_object(string(), string(), aws_config()) -> proplist() | no_return(). delete_object(BucketName, Key, Config) when is_list(BucketName), is_list(Key) -> {Headers, _Body} = s3_request(Config, delete, BucketName, [$/|Key], "", [], <<>>, []), Marker = proplists:get_value("x-amz-delete-marker", Headers, "false"), Id = proplists:get_value("x-amz-version-id", Headers, "null"), [{delete_marker, list_to_existing_atom(Marker)}, {version_id, Id}]. -spec delete_object_version(string(), string(), string()) -> proplist() | no_return(). delete_object_version(BucketName, Key, Version) -> delete_object_version(BucketName, Key, Version, default_config()). -spec delete_object_version(string(), string(), string(), aws_config()) -> proplist() | no_return(). delete_object_version(BucketName, Key, Version, Config) when is_list(BucketName), is_list(Key), is_list(Version)-> {Headers, _Body} = s3_request(Config, delete, BucketName, [$/|Key], ["versionId=", Version], [], <<>>, []), Marker = proplists:get_value("x-amz-delete-marker", Headers, "false"), Id = proplists:get_value("x-amz-version-id", Headers, "null"), [{delete_marker, list_to_existing_atom(Marker)}, {version_id, Id}]. -spec list_buckets() -> proplist() | no_return(). list_buckets() -> list_buckets(default_config()). -spec list_buckets(aws_config()) -> proplist() | no_return(). list_buckets(Config) -> Doc = s3_xml_request(Config, get, "", "/", "", [], <<>>, []), Owner = extract_user(xmerl_xpath:string("/*/Owner", Doc)), Buckets = [extract_bucket(Node) || Node <- xmerl_xpath:string("/*/Buckets/Bucket", Doc)], [{owner, Owner}, {buckets, Buckets}]. % % @doc Get S3 bucket policy JSON object % API Document: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETacl.html % -spec get_bucket_policy(BucketName::string()) -> ok | {error, Reason::term()}. get_bucket_policy(BucketName) -> get_bucket_policy(BucketName, default_config()). % % Example request: erlcloud_s3:get_bucket_policy("bucket1234", Config). % Example success repsonse: {ok, "{\"Version\":\"2012-10-17\",\"Statement\": ..........} % Example error response: {error,{http_error,404,"Not Found", % "\n % % NoSuchBucket % The specified bucket does not exist % bucket1234 % DC1EA9456B266EF5 % DRtkAB80cAeom+4ffSGU3PFCxS7QvtiW+wxLnPF0dM2nxoaRqQk1SK/z62ZJVHAD % "}} -spec get_bucket_policy(BucketName::string(), Config::aws_config()) -> {ok, Policy::string()} | {error, Reason::term()}. get_bucket_policy(BucketName, Config) when is_record(Config, aws_config) -> case s3_request2(Config, get, BucketName, "/", "policy", [], <<>>, []) of {ok, {_Headers, Body}} -> {ok, binary_to_list(Body)}; Error -> Error end. -spec put_bucket_policy(string(), binary()) -> ok | no_return(). put_bucket_policy(BucketName, Policy) -> put_bucket_policy(BucketName, Policy, default_config()). -spec put_bucket_policy(string(), binary(), aws_config()) -> ok | no_return(). put_bucket_policy(BucketName, Policy, Config) when is_list(BucketName), is_binary(Policy), is_record(Config, aws_config) -> s3_simple_request(Config, put, BucketName, "/", "policy", [], Policy, []). -spec get_bucket_lifecycle(BucketName::string()) -> ok | {error, Reason::term()}. get_bucket_lifecycle(BucketName) -> get_bucket_lifecycle(BucketName, default_config()). -spec get_bucket_lifecycle(BucketName::string(), Config::aws_config()) -> {ok, Policy::string()} | {error, Reason::term()}. get_bucket_lifecycle(BucketName, Config) when is_record(Config, aws_config) -> case s3_request2(Config, get, BucketName, "/", "lifecycle", [], <<>>, []) of {ok, {_Headers, Body}} -> {ok, parse_lifecycle(element(1, xmerl_scan:string(binary_to_list(Body))))}; Error -> Error end. -spec put_bucket_lifecycle(string(), list() | binary()) -> ok | {error, Reason::term()} | no_return(). put_bucket_lifecycle(BucketName, Policy) -> put_bucket_lifecycle(BucketName, Policy, default_config()). -spec put_bucket_lifecycle(string(), list() | binary(), aws_config()) -> ok | {error, Reason::term()} | no_return(). put_bucket_lifecycle(BucketName, Policy, Config) when is_list(BucketName), is_list(Policy), is_record(Config, aws_config) -> XmlPolicy = encode_lifecycle(Policy), put_bucket_lifecycle(BucketName, list_to_binary(XmlPolicy), Config); put_bucket_lifecycle(BucketName, XmlPolicy, Config) when is_list(BucketName), is_binary(XmlPolicy), is_record(Config, aws_config) -> Md5 = base64:encode(crypto:hash(md5, XmlPolicy)), s3_simple_request(Config, put, BucketName, "/", "lifecycle", [], XmlPolicy, [{"Content-MD5", Md5}]). -spec delete_bucket_lifecycle(string()) -> ok | {error, Reason::term()} | no_return(). delete_bucket_lifecycle(BucketName) -> delete_bucket_lifecycle(BucketName, default_config()). -spec delete_bucket_lifecycle(string(), #aws_config{}) -> ok | {error, Reason::term()} | no_return(). delete_bucket_lifecycle(BucketName, AwsConfig) -> s3_simple_request(AwsConfig, delete, BucketName, "/", "lifecycle", [], <<>>, []). -spec list_objects(string()) -> proplist() | no_return(). list_objects(BucketName) -> list_objects(BucketName, []). -spec list_objects(string(), proplist() | aws_config()) -> proplist() | no_return(). list_objects(BucketName, Config) when is_record(Config, aws_config) -> list_objects(BucketName, [], Config); list_objects(BucketName, Options) -> list_objects(BucketName, Options, default_config()). -spec list_objects(string(), proplist(), aws_config()) -> proplist() | no_return(). list_objects(BucketName, Options, Config) when is_list(BucketName), is_list(Options) -> Params = [{"delimiter", proplists:get_value(delimiter, Options)}, {"marker", proplists:get_value(marker, Options)}, {"max-keys", proplists:get_value(max_keys, Options)}, {"prefix", proplists:get_value(prefix, Options)}], Doc = s3_xml_request(Config, get, BucketName, "/", "", Params, <<>>, []), Attributes = [{name, "Name", text}, {prefix, "Prefix", text}, {marker, "Marker", text}, {next_marker, "NextMarker", text}, {delimiter, "Delimiter", text}, {max_keys, "MaxKeys", integer}, {is_truncated, "IsTruncated", boolean}, {common_prefixes, "CommonPrefixes", fun extract_prefixes/1}, {contents, "Contents", fun extract_contents/1}], erlcloud_xml:decode(Attributes, Doc). extract_prefixes(Nodes) -> Attributes = [{prefix, "Prefix", text}], [erlcloud_xml:decode(Attributes, Node) || Node <- Nodes]. extract_contents(Nodes) -> Attributes = [{key, "Key", text}, {last_modified, "LastModified", time}, {etag, "ETag", text}, {size, "Size", integer}, {storage_class, "StorageClass", text}, {owner, "Owner", fun extract_user/1}], [erlcloud_xml:decode(Attributes, Node) || Node <- Nodes]. extract_user([]) -> []; extract_user([Node]) -> Attributes = [{id, "ID", optional_text}, {display_name, "DisplayName", optional_text}, {uri, "URI", optional_text} ], erlcloud_xml:decode(Attributes, Node). -spec get_bucket_attribute(string(), s3_bucket_attribute_name()) -> term() | no_return(). get_bucket_attribute(BucketName, AttributeName) -> get_bucket_attribute(BucketName, AttributeName, default_config()). -spec get_bucket_attribute(string(), s3_bucket_attribute_name(), aws_config()) -> term() | no_return(). get_bucket_attribute(BucketName, AttributeName, Config) when is_list(BucketName), is_atom(AttributeName) -> Attr = case AttributeName of acl -> "acl"; location -> "location"; logging -> "logging"; request_payment -> "requestPayment"; versioning -> "versioning"; notification -> "notification" end, Doc = s3_xml_request(Config, get, BucketName, "/", Attr, [], <<>>, []), case AttributeName of acl -> Attributes = [{owner, "Owner", fun extract_user/1}, {access_control_list, "AccessControlList/Grant", fun extract_acl/1}], erlcloud_xml:decode(Attributes, Doc); location -> case erlcloud_xml:get_text("/LocationConstraint", Doc) of %% logic according to http://s3tools.org/s3cmd %% s3cmd-1.5.2/S3/S3.py : line 342 (function get_bucket_location) "" -> "us-east-1"; "US" -> "us-east-1"; "EU" -> "eu-west-1"; Loc -> Loc end; logging -> case xmerl_xpath:string("/BucketLoggingStatus/LoggingEnabled", Doc) of [] -> {enabled, false}; [LoggingEnabled] -> Attributes = [{target_bucket, "TargetBucket", text}, {target_prefix, "TargetPrefix", text}, {target_trants, "TargetGrants/Grant", fun extract_acl/1}], [{enabled, true}|erlcloud_xml:decode(Attributes, LoggingEnabled)] end; request_payment -> case erlcloud_xml:get_text("/RequestPaymentConfiguration/Payer", Doc) of "Requester" -> requester; _ -> bucket_owner end; versioning -> case erlcloud_xml:get_text("/VersioningConfiguration/Status", Doc) of "Enabled" -> enabled; "Suspended" -> suspended; _ -> disabled end; notification -> get_bucket_notification(Doc) end. %% gets the notifications configuration of an S3 bucket. %% for an example of the returned data, see tests. -spec get_bucket_notification(#xmlElement{}) -> [proplist()]. get_bucket_notification(Doc) -> SNSNotifications = get_notifications_config(topic_configuration, topic, "Topic", "TopicConfiguration", Doc), SQSNotifications = get_notifications_config(queue_configuration, queue, "Queue", "QueueConfiguration", Doc), LambdaNotifications = get_notifications_config(cloud_function_configuration, cloud_function, "CloudFunction", "CloudFunctionConfiguration", Doc), SNSNotifications ++ SQSNotifications ++ LambdaNotifications. get_notifications_config(ConfType, AttributeName, Attr, Path, Doc) -> case xmerl_xpath:string("/NotificationConfiguration/" ++ Path, Doc) of [] -> []; Configs when is_list(Configs) -> [decode_notification_config(ConfType, AttributeName, Attr, Path, Config) || Config <- Configs] end. decode_notification_config(ConfType, AttributeName, Attr, Path, Config) -> Attributes = s3_notification_attrs(AttributeName, Attr), Configurations = [get_notification_filter(Path, Config) | erlcloud_xml:decode(Attributes, Config)], [{ConfType, Configurations}]. get_notification_filter(Path, Config) -> get_notification_filter_do(xmerl_xpath:string("/" ++ Path ++ "/Filter/S3Key", Config)). get_notification_filter_do([]) -> []; get_notification_filter_do(S3Key) -> #xmlElement{content = Content} = hd(S3Key), GV = fun(Key, PL) -> proplists:get_value(Key, PL) end, Filter0 = [erlcloud_xml:decode([{name, "Name", text}, {value, "Value", text}], C) || C <- Content], Filter = [{list_to_atom(string:to_lower(GV(name, E))), GV(value, E)} || E <- Filter0], {filter, Filter}. s3_notification_attrs(AttributeName, Attr) -> [ {AttributeName, Attr, text} | [{id, "Id", optional_text}, {event, "Event", list}]]. parse_lifecycle(Xml) -> Rules = xmerl_xpath:string("/LifecycleConfiguration/Rule", Xml), [extract_rule(X) || X <- Rules]. extract_rule(Xml) -> erlcloud_xml:decode( [ {expiration, "Expiration", {single, fun extract_expiration/1}}, {id, "ID", text}, {noncurrent_version_expiration, "NoncurrentVersionExpiration", {optional_map, fun extract_noncurrent_version_expiration/1}}, {noncurrent_version_transition, "NoncurrentVersionTransition", {optional_map, fun extract_noncurrent_version_transition/1}}, {prefix, "Prefix", text}, {status, "Status", text}, {transition, "Transition", {optional_map, fun extract_transition/1}} ], Xml). encode_lifecycle(Lifecycle) -> lists:flatten(xmerl:export_simple( [{'LifecycleConfiguration', lists:map(fun(Rule) -> {'Rule', encode_rule(Rule)} end, Lifecycle)}], xmerl_xml)). encode_rule(Rule) -> lists:flatten(lists:map( fun({Key, [{_, _} | _] = Proplist}) -> {key_to_name(Key), encode_subtype(Proplist)}; ({Key, [[{_, _} | _], _] = ListOfProplist}) -> lists:map(fun(List) -> {key_to_name(Key), encode_subtype(List)} end, ListOfProplist); ({Key, [[{_, _}, _] = List]}) -> {key_to_name(Key), encode_subtype(List)}; ({Key, Value}) -> {key_to_name(Key), encode_subtype(Value)} end, Rule)). encode_subtype(List) -> case List of [{_, _} | _] -> lists:map(fun({Key, Value}) -> {key_to_name(Key), [erlcloud_util:to_string(Value)]} end, List); Value -> [erlcloud_util:to_string(Value)] end. extract_transition(Xml) -> erlcloud_xml:decode( [ {date, "Date", optional_text}, {days, "Days", optional_integer}, {storage_class, "StorageClass", text} ], Xml). extract_noncurrent_version_transition(Xml) -> erlcloud_xml:decode( [{noncurrent_days, "NoncurrentDays", integer}, {storage_class, "StorageClass", text}], Xml). extract_noncurrent_version_expiration(Xml) -> erlcloud_xml:decode([{noncurrent_days, "NoncurrentDays", integer}], Xml). extract_expiration(Xml) -> erlcloud_xml:decode( [{date, "Date", optional_text}, {days, "Days", optional_integer}], Xml). key_to_name(expiration) -> 'Expiration'; key_to_name(id) -> 'ID'; key_to_name(prefix) -> 'Prefix'; key_to_name(status) -> 'Status'; key_to_name(transition) -> 'Transition'; key_to_name(date) -> 'Date'; key_to_name(days) -> 'Days'; key_to_name('noncurrent_version_expiration') -> 'NoncurrentVersionExpiration'; key_to_name('noncurrent_version_transition') -> 'NoncurrentVersionTransition'; key_to_name('storage_class') -> 'StorageClass'; key_to_name('noncurrent_days') -> 'NoncurrentDays'. extract_acl(ACL) -> [extract_grant(Item) || Item <- ACL]. extract_grant(GrantNode) -> [GranteeNode] = xmerl_xpath:string("Grantee", GrantNode), [{grantee, extract_grantee(GranteeNode)}, {permission, decode_permission(erlcloud_xml:get_text("Permission", GrantNode))}]. extract_grantee(Node) -> erlcloud_xml:decode([ {type, {".", "xsi:type"}, text}, {id, "ID", optional_text}, {display_name, "DisplayName", optional_text}, {uri, "URI", optional_text} ], Node). encode_permission(full_control) -> "FULL_CONTROL"; encode_permission(write) -> "WRITE"; encode_permission(write_acp) -> "WRITE_ACP"; encode_permission(read) -> "READ"; encode_permission(read_acp) -> "READ_ACP". decode_permission("FULL_CONTROL") -> full_control; decode_permission("WRITE") -> write; decode_permission("WRITE_ACP") -> write_acp; decode_permission("READ") -> read; decode_permission("READ_ACP") -> read_acp. -spec head_object(string(), string()) -> proplist() | no_return(). head_object(BucketName, Key) -> head_object(BucketName, Key, []). -spec head_object(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). head_object(BucketName, Key, Config) when is_record(Config, aws_config) -> head_object(BucketName, Key, [], Config); head_object(BucketName, Key, Options) -> head_object(BucketName, Key, Options, default_config()). -spec head_object(string(), string(), proplist(), aws_config()) -> proplist() | no_return(). head_object(BucketName, Key, Options, Config) -> get_or_head(head, BucketName, Key, Options, Config). -spec get_object(string(), string()) -> proplist() | no_return(). get_object(BucketName, Key) -> get_object(BucketName, Key, []). -spec get_object(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). get_object(BucketName, Key, Config) when is_record(Config, aws_config) -> get_object(BucketName, Key, [], Config); get_object(BucketName, Key, Options) -> get_object(BucketName, Key, Options, default_config()). -spec get_object(string(), string(), proplist(), aws_config()) -> proplist() | no_return(). get_object(BucketName, Key, Options, Config) -> get_or_head(get, BucketName, Key, Options, Config). get_or_head(Method, BucketName, Key, Options, Config) -> RequestHeaders = [{"Range", proplists:get_value(range, Options)}, {"If-Modified-Since", proplists:get_value(if_modified_since, Options)}, {"If-Unmodified-Since", proplists:get_value(if_unmodified_since, Options)}, {"If-Match", proplists:get_value(if_match, Options)}, {"If-None-Match", proplists:get_value(if_none_match, Options)}, {"x-amz-server-side-encryption-customer-algorithm", proplists:get_value(server_side_encryption_customer_algorithm, Options)}, {"x-amz-server-side-encryption-customer-key", proplists:get_value(server_side_encryption_customer_key, Options)}, {"x-amz-server-side-encryption-customer-key-md5", proplists:get_value(server_side_encryption_customer_key_md5, Options)}], Subresource = case proplists:get_value(version_id, Options) of undefined -> ""; Version -> ["versionId=", Version] end, {Headers, Body} = s3_request(Config, Method, BucketName, [$/|Key], Subresource, [], <<>>, RequestHeaders), [{last_modified, proplists:get_value("last-modified", Headers)}, {etag, proplists:get_value("etag", Headers)}, {content_length, proplists:get_value("content-length", Headers)}, {content_type, proplists:get_value("content-type", Headers)}, {content_encoding, proplists:get_value("content-encoding", Headers)}, {delete_marker, list_to_existing_atom(proplists:get_value("x-amz-delete-marker", Headers, "false"))}, {version_id, proplists:get_value("x-amz-version-id", Headers, "null")}, {content, Body}| extract_metadata(Headers)]. -spec get_object_acl(string(), string()) -> proplist() | no_return(). get_object_acl(BucketName, Key) -> get_object_acl(BucketName, Key, default_config()). -spec get_object_acl(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). get_object_acl(BucketName, Key, Config) when is_record(Config, aws_config) -> get_object_acl(BucketName, Key, [], Config); get_object_acl(BucketName, Key, Options) -> get_object_acl(BucketName, Key, Options, default_config()). -spec get_object_acl(string(), string(), proplist(), aws_config()) -> proplist() | no_return(). get_object_acl(BucketName, Key, Options, Config) when is_list(BucketName), is_list(Key), is_list(Options) -> Subresource = case proplists:get_value(version_id, Options) of undefined -> ""; Version -> ["&versionId=", Version] end, Doc = s3_xml_request(Config, get, BucketName, [$/|Key], "acl" ++ Subresource, [], <<>>, []), Attributes = [{owner, "Owner", fun extract_user/1}, {access_control_list, "AccessControlList/Grant", fun extract_acl/1}], erlcloud_xml:decode(Attributes, Doc). -spec get_object_metadata(string(), string()) -> proplist() | no_return(). get_object_metadata(BucketName, Key) -> get_object_metadata(BucketName, Key, []). -spec get_object_metadata(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). get_object_metadata(BucketName, Key, Config) when is_record(Config, aws_config) -> get_object_metadata(BucketName, Key, [], Config); get_object_metadata(BucketName, Key, Options) -> get_object_metadata(BucketName, Key, Options, default_config()). -spec get_object_metadata(string(), string(), proplist(), proplist() | aws_config()) -> proplist() | no_return(). get_object_metadata(BucketName, Key, Options, Config) -> RequestHeaders = [{"If-Modified-Since", proplists:get_value(if_modified_since, Options)}, {"If-Unmodified-Since", proplists:get_value(if_unmodified_since, Options)}, {"If-Match", proplists:get_value(if_match, Options)}, {"If-None-Match", proplists:get_value(if_none_match, Options)}], Subresource = case proplists:get_value(version_id, Options) of undefined -> ""; Version -> ["versionId=", Version] end, {Headers, _Body} = s3_request(Config, head, BucketName, [$/|Key], Subresource, [], <<>>, RequestHeaders), [{last_modified, proplists:get_value("last-modified", Headers)}, {etag, proplists:get_value("etag", Headers)}, {content_length, proplists:get_value("content-length", Headers)}, {content_type, proplists:get_value("content-type", Headers)}, {content_encoding, proplists:get_value("content-encoding", Headers)}, {delete_marker, list_to_existing_atom(proplists:get_value("x-amz-delete-marker", Headers, "false"))}, {replication_status, decode_replication_status(proplists:get_value("x-amz-replication-status", Headers))}, {version_id, proplists:get_value("x-amz-version-id", Headers, "false")}|extract_metadata(Headers)]. extract_metadata(Headers) -> [{Key, Value} || {Key = "x-amz-meta-" ++ _, Value} <- Headers]. decode_replication_status(undefined) -> undefined; decode_replication_status("PENDING") -> pending; decode_replication_status("COMPLETED") -> completed; decode_replication_status("FAILED") -> failed; decode_replication_status("REPLICA") -> replica. -spec get_object_torrent(string(), string()) -> proplist() | no_return(). get_object_torrent(BucketName, Key) -> get_object_torrent(BucketName, Key, default_config()). -spec get_object_torrent(string(), string(), aws_config()) -> proplist() | no_return(). get_object_torrent(BucketName, Key, Config) -> {Headers, Body} = s3_request(Config, get, BucketName, [$/|Key], "torrent", [], <<>>, []), [{delete_marker, list_to_existing_atom(proplists:get_value("x-amz-delete-marker", Headers, "false"))}, {version_id, proplists:get_value("x-amz-version-id", Headers, "null")}, {torrent, Body}]. -spec list_object_versions(string()) -> proplist() | no_return(). list_object_versions(BucketName) -> list_object_versions(BucketName, []). -spec list_object_versions(string(), proplist() | aws_config()) -> proplist() | no_return(). list_object_versions(BucketName, Config) when is_record(Config, aws_config) -> list_object_versions(BucketName, [], Config); list_object_versions(BucketName, Options) -> list_object_versions(BucketName, Options, default_config()). -spec list_object_versions(string(), proplist(), aws_config()) -> proplist() | no_return(). list_object_versions(BucketName, Options, Config) when is_list(BucketName), is_list(Options) -> Params = [{"delimiter", proplists:get_value(delimiter, Options)}, {"key-marker", proplists:get_value(key_marker, Options)}, {"max-keys", proplists:get_value(max_keys, Options)}, {"prefix", proplists:get_value(prefix, Options)}, {"version-id-marker", proplists:get_value(version_id_marker, Options)}], Doc = s3_xml_request(Config, get, BucketName, "/", "versions", Params, <<>>, []), Attributes = [{name, "Name", text}, {prefix, "Prefix", text}, {key_marker, "KeyMarker", text}, {next_key_marker, "NextKeyMarker", optional_text}, {version_id_marker, "VersionIdMarker", text}, {next_version_id_marker, "NextVersionIdMarker", optional_text}, {max_keys, "MaxKeys", integer}, {is_truncated, "Istruncated", boolean}, {versions, "Version", fun extract_versions/1}, {delete_markers, "DeleteMarker", fun extract_delete_markers/1}], erlcloud_xml:decode(Attributes, Doc). extract_versions(Nodes) -> [extract_version(Node) || Node <- Nodes]. extract_version(Node) -> Attributes = [{key, "Key", text}, {version_id, "VersionId", text}, {is_latest, "IsLatest", boolean}, {etag, "ETag", text}, {size, "Size", integer}, {owner, "Owner", fun extract_user/1}, {storage_class, "StorageClass", text}, {last_modified, "LastModified", time}], erlcloud_xml:decode(Attributes, Node). extract_delete_markers(Nodes) -> [extract_delete_marker(Node) || Node <- Nodes]. extract_delete_marker(Node) -> Attributes = [{key, "Key", text}, {version_id, "VersionId", text}, {is_latest, "IsLatest", boolean}, {owner, "Owner", fun extract_user/1}], erlcloud_xml:decode(Attributes, Node). extract_bucket(Node) -> erlcloud_xml:decode([{name, "Name", text}, {creation_date, "CreationDate", time}], Node). -spec put_object(string(), string(), iodata()) -> proplist() | no_return(). put_object(BucketName, Key, Value) -> put_object(BucketName, Key, Value, []). -spec put_object(string(), string(), iodata(), proplist() | aws_config()) -> proplist() | no_return(). put_object(BucketName, Key, Value, Config) when is_record(Config, aws_config) -> put_object(BucketName, Key, Value, [], Config); put_object(BucketName, Key, Value, Options) -> put_object(BucketName, Key, Value, Options, default_config()). -spec put_object(string(), string(), iodata(), proplist(), [{string(), string()}] | aws_config()) -> proplist() | no_return(). put_object(BucketName, Key, Value, Options, Config) when is_record(Config, aws_config) -> put_object(BucketName, Key, Value, Options, [], Config); put_object(BucketName, Key, Value, Options, HTTPHeaders) -> put_object(BucketName, Key, Value, Options, HTTPHeaders, default_config()). -spec put_object(string(), string(), iodata(), proplist(), [{string(), string()}], aws_config()) -> proplist() | no_return(). put_object(BucketName, Key, Value, Options, HTTPHeaders, Config) when is_list(BucketName), is_list(Key), is_list(Value) orelse is_binary(Value), is_list(Options) -> RequestHeaders = [{"x-amz-acl", encode_acl(proplists:get_value(acl, Options))}|HTTPHeaders] ++ [{"x-amz-meta-" ++ string:to_lower(MKey), MValue} || {MKey, MValue} <- proplists:get_value(meta, Options, [])], POSTData = iolist_to_binary(Value), {Headers, _Body} = s3_request(Config, put, BucketName, [$/|Key], "", [], POSTData, RequestHeaders), [{version_id, proplists:get_value("x-amz-version-id", Headers, "null")} | Headers]. -spec set_object_acl(string(), string(), proplist()) -> ok | no_return(). set_object_acl(BucketName, Key, ACL) -> set_object_acl(BucketName, Key, ACL, default_config()). -spec set_object_acl(string(), string(), proplist(), aws_config()) -> ok | no_return(). set_object_acl(BucketName, Key, ACL, Config) when is_list(BucketName), is_list(Key), is_list(ACL) -> Id = proplists:get_value(id, proplists:get_value(owner, ACL)), DisplayName = proplists:get_value(display_name, proplists:get_value(owner, ACL)), ACL1 = proplists:get_value(access_control_list, ACL), XML = {'AccessControlPolicy', [{'xmlns', ?XMLNS_S3}], [{'Owner', [{'ID', [Id]}, {'DisplayName', [DisplayName]}]}, {'AccessControlList', encode_grants(ACL1)}]}, XMLText = list_to_binary(xmerl:export_simple([XML], xmerl_xml, [{prolog, ""}])), s3_simple_request(Config, put, BucketName, [$/|Key], "acl", [], XMLText, [{"content-type", "application/xml"}]). -spec sign_get(integer(), string(), string(), aws_config()) -> {binary(), string()}. sign_get(Expire_time, BucketName, Key, Config) when is_integer(Expire_time), is_list(BucketName), is_list(Key) -> {Mega, Sec, _Micro} = os:timestamp(), Datetime = (Mega * 1000000) + Sec, Expires = integer_to_list(Expire_time + Datetime), SecurityTokenToSign = case Config#aws_config.security_token of undefined -> ""; SecurityToken -> "x-amz-security-token:" ++ SecurityToken ++ "\n" end, To_sign = lists:flatten(["GET\n\n\n", Expires, "\n", SecurityTokenToSign, "/", BucketName, "/", Key]), Sig = base64:encode(erlcloud_util:sha_mac(Config#aws_config.secret_access_key, To_sign)), {Sig, Expires}. -spec make_link(integer(), string(), string()) -> {integer(), string(), string()}. make_link(Expire_time, BucketName, Key) -> make_link(Expire_time, BucketName, Key, default_config()). -spec make_link(integer(), string(), string(), aws_config()) -> {integer(), string(), string()}. make_link(Expire_time, BucketName, Key, Config) -> EncodedKey = erlcloud_http:url_encode_loose(Key), {Sig, Expires} = sign_get(Expire_time, BucketName, EncodedKey, Config), Host = lists:flatten([Config#aws_config.s3_scheme, BucketName, ".", Config#aws_config.s3_host, port_spec(Config)]), SecurityTokenQS = case Config#aws_config.security_token of undefined -> ""; SecurityToken -> "&x-amz-security-token=" ++ erlcloud_http:url_encode(SecurityToken) end, URI = lists:flatten(["/", EncodedKey, "?AWSAccessKeyId=", erlcloud_http:url_encode(Config#aws_config.access_key_id), "&Signature=", erlcloud_http:url_encode(Sig), "&Expires=", Expires, SecurityTokenQS]), {list_to_integer(Expires), binary_to_list(erlang:iolist_to_binary(Host)), binary_to_list(erlang:iolist_to_binary(URI))}. -spec get_object_url(string(), string()) -> string(). get_object_url(BucketName, Key) -> get_object_url(BucketName, Key, default_config()). -spec get_object_url(string(), string(), aws_config()) -> string(). get_object_url(BucketName, Key, Config) -> case Config#aws_config.s3_bucket_after_host of false -> lists:flatten([Config#aws_config.s3_scheme, BucketName, ".", Config#aws_config.s3_host, port_spec(Config), "/", Key]); true -> lists:flatten([Config#aws_config.s3_scheme, Config#aws_config.s3_host, port_spec(Config), "/", BucketName, "/", Key]) end. -spec make_get_url(integer(), string(), string()) -> iolist(). make_get_url(Expire_time, BucketName, Key) -> make_get_url(Expire_time, BucketName, Key, default_config()). -spec make_get_url(integer(), string(), string(), aws_config()) -> iolist(). make_get_url(Expire_time, BucketName, Key, Config) -> {Sig, Expires} = sign_get(Expire_time, BucketName, erlcloud_http:url_encode_loose(Key), Config), SecurityTokenQS = case Config#aws_config.security_token of undefined -> ""; SecurityToken -> "&x-amz-security-token=" ++ erlcloud_http:url_encode(SecurityToken) end, lists:flatten([get_object_url(BucketName, Key, Config), "?AWSAccessKeyId=", erlcloud_http:url_encode(Config#aws_config.access_key_id), "&Signature=", erlcloud_http:url_encode(Sig), "&Expires=", Expires, SecurityTokenQS]). -spec start_multipart(string(), string()) -> {ok, proplist()} | {error, any()}. start_multipart(BucketName, Key) when is_list(BucketName), is_list(Key) -> start_multipart(BucketName, Key, [], [], default_config()). -spec start_multipart(string(), string(), proplist(), [{string(), string()}], aws_config()) -> {ok, proplist()} | {error, any()}. start_multipart(BucketName, Key, Options, HTTPHeaders, Config) when is_list(BucketName), is_list(Key), is_list(Options), is_list(HTTPHeaders), is_record(Config, aws_config) -> RequestHeaders = [{"x-amz-acl", encode_acl(proplists:get_value(acl, Options))}|HTTPHeaders] ++ [{"x-amz-meta-" ++ string:to_lower(MKey), MValue} || {MKey, MValue} <- proplists:get_value(meta, Options, [])], POSTData = <<>>, case s3_xml_request2(Config, post, BucketName, [$/|Key], "uploads", [], POSTData, RequestHeaders) of {ok, Doc} -> Attributes = [{uploadId, "UploadId", text}], {ok, erlcloud_xml:decode(Attributes, Doc)}; Error -> Error end. -spec upload_part(string(), string(), string(), integer(), iodata()) -> {ok, proplist()} | {error, any()}. upload_part(BucketName, Key, UploadId, PartNumber, Value) -> upload_part(BucketName, Key, UploadId, PartNumber, Value, [], default_config()). -spec upload_part(string(), string(), string(), integer(), iodata(), [{string(), string()}], aws_config()) -> {ok, proplist()} | {error, any()}. upload_part(BucketName, Key, UploadId, PartNumber, Value, HTTPHeaders, Config) when is_list(BucketName), is_list(Key), is_list(UploadId), is_integer(PartNumber), is_list(Value) orelse is_binary(Value), is_list(HTTPHeaders), is_record(Config, aws_config) -> POSTData = iolist_to_binary(Value), case s3_request2(Config, put, BucketName, [$/|Key], [], [{"uploadId", UploadId}, {"partNumber", integer_to_list(PartNumber)}], POSTData, HTTPHeaders) of {ok, {Headers, _Body}} -> {ok, [{etag, proplists:get_value("etag", Headers)}]}; Error -> Error end. -spec complete_multipart(string(), string(), string(), [{integer(), string()}]) -> ok | {error, any()}. complete_multipart(BucketName, Key, UploadId, ETags) when is_list(BucketName), is_list(Key), is_list(UploadId), is_list(ETags) -> complete_multipart(BucketName, Key, UploadId, ETags, [], default_config()). -spec complete_multipart(string(), string(), string(), [{integer(), string()}], [{string(), string()}], aws_config()) -> ok | {error, any()}. complete_multipart(BucketName, Key, UploadId, ETags, HTTPHeaders, Config) when is_list(BucketName), is_list(Key), is_list(UploadId), is_list(ETags), is_list(HTTPHeaders), is_record(Config, aws_config) -> POSTData = list_to_binary(xmerl:export_simple([{'CompleteMultipartUpload', [{'Part', [{'PartNumber', [integer_to_list(Num)]}, {'ETag', [ETag]}] } || {Num, ETag} <- ETags]}], xmerl_xml)), case s3_request2(Config, post, BucketName, [$/|Key], [], [{"uploadId", UploadId}], POSTData, HTTPHeaders) of {ok, {_Headers, _Body}} -> ok; Error -> Error end. -spec abort_multipart(string(), string(), string()) -> ok | {error, any()}. abort_multipart(BucketName, Key, UploadId) when is_list(BucketName), is_list(Key), is_list(UploadId) -> abort_multipart(BucketName, Key, UploadId, [], [], default_config()). -spec abort_multipart(string(), string(), string(), proplist(), [{string(), string()}], aws_config()) -> ok | {error, any()}. abort_multipart(BucketName, Key, UploadId, Options, HTTPHeaders, Config) when is_list(BucketName), is_list(Key), is_list(UploadId), is_list(Options), is_list(HTTPHeaders), is_record(Config, aws_config) -> case s3_request2(Config, delete, BucketName, [$/|Key], [], [{"uploadId", UploadId}], <<>>, HTTPHeaders) of {ok, _} -> ok; Error -> Error end. -spec list_multipart_uploads(string()) -> {ok, proplist()} | {error, any()}. list_multipart_uploads(BucketName) when is_list(BucketName) -> list_multipart_uploads(BucketName, [], [], default_config()). -spec list_multipart_uploads(string(), proplist()) -> {ok, proplist()} | {error, any()}. list_multipart_uploads(BucketName, Options) when is_list(BucketName), is_list(Options) -> list_multipart_uploads(BucketName, Options, [], default_config()). -spec list_multipart_uploads(string(), proplist(), [{string(), string()}], aws_config()) -> {ok, proplist()} | {error, any()}. list_multipart_uploads(BucketName, Options, HTTPHeaders, Config) when is_list(BucketName), is_list(Options), is_list(HTTPHeaders), is_record(Config, aws_config) -> Params = [ {"uploads", ""}, {"delimiter", proplists:get_value(delimiter, Options)}, {"prefix", proplists:get_value(prefix, Options)}, {"max-uploads", proplists:get_value(max_uploads, Options)}, {"key-marker", proplists:get_value(key_marker, Options)}, {"upload-id-marker", proplists:get_value(upload_id_marker, Options)} ], case s3_xml_request2(Config, get, BucketName, "/", "", Params, <<>>, HTTPHeaders) of {ok, Xml} -> Uploads = [erlcloud_xml:decode([{key, "Key", text}, {uploadId, "UploadId", text}], Node) || Node <- xmerl_xpath:string("/ListMultipartUploadsResult/Upload", Xml)], CommonPrefixes = [erlcloud_xml:get_text("Prefix", Node) || Node <- xmerl_xpath:string("/ListMultipartUploadsResult/CommonPrefixes", Xml)], {ok, [{uploads, Uploads}, {common_prefixes, CommonPrefixes}]}; Error -> Error end. -spec set_bucket_attribute(string(), atom(), term()) -> ok | no_return(). set_bucket_attribute(BucketName, AttributeName, Value) -> set_bucket_attribute(BucketName, AttributeName, Value, default_config()). -spec set_bucket_attribute(string(), atom(), term(), aws_config()) -> ok | no_return(). set_bucket_attribute(BucketName, AttributeName, Value, Config) when is_list(BucketName) -> {Subresource, XML} = case AttributeName of acl -> ACLXML = {'AccessControlPolicy', [{'Owner', [{'ID', [proplists:get_value(id, proplists:get_value(owner, Value))]}, {'DisplayName', [proplists:get_value(display_name, proplists:get_value(owner, Value))]}]}, {'AccessControlList', encode_grants(proplists:get_value(access_control_list, Value))}]}, {"acl", ACLXML}; logging -> LoggingXML = {'BucketLoggingStatus', [{'xmlns:xsi', ?XMLNS_S3}], case proplists:get_bool(enabled, Value) of true -> [{'LoggingEnabled', [ {'TargetBucket', [proplists:get_value(target_bucket, Value)]}, {'TargetPrefix', [proplists:get_value(target_prefix, Value)]}, {'TargetGrants', encode_grants(proplists:get_value(target_grants, Value, []))} ] }]; false -> [] end}, {"logging", LoggingXML}; request_payment -> PayerName = case Value of requester -> "Requester"; bucket_owner -> "BucketOwner" end, RPXML = {'RequestPaymentConfiguration', [{'xmlns:xsi', ?XMLNS_S3}], [ {'Payer', [PayerName]} ] }, {"requestPayment", RPXML}; versioning -> Status = case proplists:get_value(status, Value) of suspended -> "Suspended"; enabled -> "Enabled" end, MFADelete = case proplists:get_value(mfa_delete, Value, disabled) of enabled -> "Enabled"; disabled -> "Disabled" end, VersioningXML = {'VersioningConfiguration', [{'xmlns:xsi', ?XMLNS_S3}], [{'Status', [Status]}, {'MfaDelete', [MFADelete]}]}, {"versioning", VersioningXML}; notification -> {"notification", create_notification_xml(Value)} end, POSTData = list_to_binary(xmerl:export_simple([XML], xmerl_xml)), Headers = [{"content-type", "application/xml"}], s3_simple_request(Config, put, BucketName, "/", Subresource, [], POSTData, Headers). -spec list_bucket_inventory(string()) -> {ok, Result:: list(term())} | {error, Reason::term()}. list_bucket_inventory(BucketName) when is_list(BucketName) -> list_bucket_inventory(BucketName, default_config()). -spec list_bucket_inventory(string(), string() | aws_config()) -> {ok, Result:: list(term())} | {error, Reason::term()}. list_bucket_inventory(BucketName, #aws_config{} = Config) when is_list(BucketName) -> list_bucket_inventory(BucketName, undefined, Config); list_bucket_inventory(BucketName, Token) when is_list(BucketName), is_list(Token) -> list_bucket_inventory(BucketName, Token, default_config()). -spec list_bucket_inventory(string(), undefined | string(), aws_config()) -> {ok, Result:: list(term())} | {error, Reason::term()}. list_bucket_inventory(BucketName, Token, #aws_config{} = Config) when is_list(BucketName) -> Params = [{"continuation-token", Token}], case s3_request2(Config, get, BucketName, "/", "inventory", Params, <<>>, []) of {ok, {_Headers, Body}} -> list_inventory_result(element(1, xmerl_scan:string(binary_to_list(Body)))); Error -> Error end. list_inventory_result(Response) -> ParsedResponse = extract_list_inventory_result(Response), Truncated = proplists:get_value(is_truncated, ParsedResponse, false), ContinuationToken = proplists:get_value(continuation_token, ParsedResponse, []), NextContinuationToken = proplists:get_value(next_continuation_token, ParsedResponse, []), UpdatedResponse = lists:keydelete( is_truncated, 1, lists:keydelete( continuation_token, 1, lists:keydelete(next_continuation_token, 1, ParsedResponse) ) ), make_list_inventory_result( Truncated, ContinuationToken, NextContinuationToken, UpdatedResponse ). make_list_inventory_result(false, _, _, Result)-> {ok, Result}; make_list_inventory_result(_, [], NextContinuationToken, Result)-> {ok, Result, NextContinuationToken}; make_list_inventory_result(_, ContinuationToken, [], Result)-> {ok, Result, ContinuationToken}. extract_list_inventory_result(Result) -> [Content] = xmerl_xpath:string("/ListInventoryConfigurationsResult", Result), extract_list_inventory_result_content(Content). extract_list_inventory_result_content(Content) -> Attributes = [ { inventory_configuration, "InventoryConfiguration", fun extract_inventory_configuration_content/1 }, {is_truncated, "IsTruncated", boolean}, {continuation_token, "ContinuationToken", text}, {next_continuation_token, "NextContinuationToken", text} ], erlcloud_xml:decode(Attributes, Content). extract_inventory_configuration(Configuration) -> [Content] = xmerl_xpath:string("/InventoryConfiguration", Configuration), extract_inventory_configuration_content(Content). extract_inventory_configuration_content(Contents) when is_list(Contents)-> [extract_inventory_configuration_content(Content) || Content <- Contents]; extract_inventory_configuration_content(Content) -> Attributes = [ {id, "Id", text}, {is_enabled, "IsEnabled", text}, {filter, "Filter", fun extract_filter/1}, {destination, "Destination", fun extract_destination/1}, {schedule, "Schedule", fun extract_schedule/1}, {included_object_versions, "IncludedObjectVersions", text}, {optional_fields, "OptionalFields", fun extract_optional_fields/1} ], erlcloud_xml:decode(Attributes, Content). extract_destination([Destination]) -> Attributes = [{s3_bucket_destination, "S3BucketDestination", fun extract_s3_bucket_destination/1}], erlcloud_xml:decode(Attributes, Destination). extract_s3_bucket_destination([S3BucketDestination]) -> Attributes = [ {format, "Format", text}, {account_id, "AccountId", text}, {bucket, "Bucket", text}, {prefix, "Prefix", text} ], erlcloud_xml:decode(Attributes, S3BucketDestination). extract_schedule([Schedule]) -> Attributes = [{frequency, "Frequency", text}], erlcloud_xml:decode(Attributes, Schedule). extract_filter([]) -> [{prefix, ""}]; extract_filter([Filter]) -> Attributes = [{prefix, "Prefix", text}], erlcloud_xml:decode(Attributes, Filter). extract_optional_fields([OptionalFields]) -> Attributes = [{field, "Field", list}], erlcloud_xml:decode(Attributes, OptionalFields). -spec get_bucket_inventory(string(), string()) -> {ok, Result:: list(term())} | {error, Reason::term()}. get_bucket_inventory(BucketName, InventoryId) when is_list(BucketName), is_list(InventoryId) -> get_bucket_inventory(BucketName, InventoryId, default_config()). -spec get_bucket_inventory(string(), string(), aws_config()) -> {ok, Result:: list(term())} | {error, Reason::term()}. get_bucket_inventory(BucketName, InventoryId, #aws_config{} = Config) when is_list(BucketName), is_list(InventoryId) -> Params = [{"id", InventoryId}], case s3_request2(Config, get, BucketName, "/", "inventory", Params, <<>>, []) of {ok, {_Headers, Body}} -> {ok, extract_inventory_configuration(element(1, xmerl_scan:string(binary_to_list(Body))))}; Error -> Error end. -spec put_bucket_inventory(string(), list()) -> ok | {error, Reason::term()} | no_return(). put_bucket_inventory(BucketName, Inventory) when is_list(BucketName), is_list(Inventory) -> put_bucket_inventory(BucketName, Inventory, default_config()). -spec put_bucket_inventory(string(), list(), aws_config()) -> ok | {error, Reason::term()} | no_return(). put_bucket_inventory(BucketName, Inventory, #aws_config{} = Config) when is_list(BucketName), is_list(Inventory) -> InventoryId = proplists:get_value(id, Inventory), XmlInventory = encode_inventory(Inventory), put_bucket_inventory(BucketName, InventoryId, list_to_binary(XmlInventory), Config). -spec put_bucket_inventory(string(), string(), binary(), aws_config()) -> ok | {error, Reason::term()} | no_return(). put_bucket_inventory(BucketName, InventoryId, XmlInventory, #aws_config{} = Config) when is_list(BucketName), is_list(InventoryId), is_binary(XmlInventory) -> Md5 = base64:encode(crypto:hash(md5, XmlInventory)), Params = [{"id", InventoryId}], Headers = [{"Content-MD5", Md5}, {"content-type", "application/xml"}], s3_simple_request(Config, put, BucketName, "/", "inventory", Params, XmlInventory, Headers). -spec delete_bucket_inventory(string(), string()) -> ok | {error, Reason::term()} | no_return(). delete_bucket_inventory(BucketName, InventoryId) when is_list(BucketName), is_list(InventoryId) -> delete_bucket_inventory(BucketName, InventoryId, default_config()). -spec delete_bucket_inventory(string(), string(), aws_config()) -> ok | {error, Reason::term()} | no_return(). delete_bucket_inventory(BucketName, InventoryId, #aws_config{} = Config) when is_list(BucketName), is_list(InventoryId) -> Params = [{"id", InventoryId}], s3_simple_request(Config, delete, BucketName, "/", "inventory", Params, <<>>, []). encode_inventory(Inventory) -> lists:flatten( xmerl:export_simple( [{ 'InventoryConfiguration', [{xmlns, "http://doc.s3.amazonaws.com/2006-03-01/"}], inv_encode_subtype(Inventory) }], xmerl_xml ) ). inv_encode_subtype([{_, _} | _] = Proplist) -> lists:flatten(lists:map( fun(Elem) -> inv_encode_subtype(Elem) end, Proplist )); inv_encode_subtype({Key, [{_, _} | _] = Proplist}) -> {inv_key_to_name(Key), inv_encode_subtype(Proplist)}; inv_encode_subtype({Key, [[{_, _} | _], _] = ListOfProplist}) -> lists:map( fun(List) -> {inv_key_to_name(Key), inv_encode_subtype(List)} end, ListOfProplist ); inv_encode_subtype({Key, [Val | _] = ListOfValues}) when is_list(Val) -> lists:map( fun(Value) -> {inv_key_to_name(Key), inv_encode_subtype(Value)} end, ListOfValues ); inv_encode_subtype({Key, Value}) -> {inv_key_to_name(Key), inv_encode_subtype(Value)}; inv_encode_subtype(Value) -> [erlcloud_util:to_string(Value)]. inv_key_to_name(inventory_configuration) -> 'InventoryConfiguration'; inv_key_to_name(id) -> 'Id'; inv_key_to_name(is_enabled) -> 'IsEnabled'; inv_key_to_name(included_object_versions) -> 'IncludedObjectVersions'; inv_key_to_name(destination) -> 'Destination'; inv_key_to_name(schedule) -> 'Schedule'; inv_key_to_name(frequency) -> 'Frequency'; inv_key_to_name(filter) -> 'Filter'; inv_key_to_name(optional_fields) -> 'OptionalFields'; inv_key_to_name(s3_bucket_destination) -> 'S3BucketDestination'; inv_key_to_name(format) -> 'Format'; inv_key_to_name(account_id) -> 'AccountId'; inv_key_to_name(bucket) -> 'Bucket'; inv_key_to_name(prefix) -> 'Prefix'; inv_key_to_name(field) -> 'Field'. %% @doc %% S3 API: %% https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTencryption.html %% %% ` %% ok = erlcloud_s3:put_bucket_encryption("bucket-name", %% "aws:kms", %% "arn:aws:kms:us-east-1:1234/example"). %% ' %% -spec put_bucket_encryption(string(), string()) -> ok | {error, any()}. put_bucket_encryption(BucketName, SSEAlgorithm) -> put_bucket_encryption(BucketName, SSEAlgorithm, undefined, default_config()). -spec put_bucket_encryption(string(), string(), string() | aws_config()) -> ok | {error, any()}. put_bucket_encryption(BucketName, SSEAlgorithm, KMSMasterKeyId) when is_list(KMSMasterKeyId) -> Config = default_config(), put_bucket_encryption(BucketName, SSEAlgorithm, KMSMasterKeyId, Config); put_bucket_encryption(BucketName, SSEAlgorithm, Config) when is_record(Config, aws_config) -> put_bucket_encryption(BucketName, SSEAlgorithm, undefined, Config). -spec put_bucket_encryption(string(), string(), string() | undefined, aws_config()) -> ok | {error, any()}. put_bucket_encryption(BucketName, SSEAlgorithm, KMSMasterKeyId, Config) -> ApplySSEOPts = build_apply_sse_opts(SSEAlgorithm, KMSMasterKeyId), XML = { 'ApplyServerSideEncryptionByDefault', [{'xmlns', ?XMLNS_S3}], [{'Rule', [ {'ApplyServerSideEncryptionByDefault', ApplySSEOPts} ]}] }, Attrs = [{prolog, ""}], Data = list_to_binary(xmerl:export_simple([XML], xmerl_xml, Attrs)), s3_xml_request2(Config, put, BucketName, "/", "encryption", [], Data, []). build_apply_sse_opts(SSEAlgorithm, undefined) -> [{'SSEAlgorithm', [SSEAlgorithm]}]; build_apply_sse_opts(SSEAlgorithm, KMSMasterKeyId) -> [{'SSEAlgorithm', [SSEAlgorithm]}, {'KMSMasterKeyID', [KMSMasterKeyId]}]. %% @doc %% S3 API: %% https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETencryption.html %% %% ` %% {ok, [{sse_algorithm, "AES256"}, %% {kms_master_key_id, undefined}]} = erlcloud_s3:get_bucket_encryption("bucket-name"). %% ' %% -spec get_bucket_encryption(string()) -> {ok, proplists:proplist()} | {error, any()}. get_bucket_encryption(BucketName) -> get_bucket_encryption(BucketName, default_config()). -spec get_bucket_encryption(string(), aws_config()) -> {ok, proplists:proplist()} | {error, any()}. get_bucket_encryption(BucketName, Config) -> case s3_xml_request2(Config, get, BucketName, "/", "encryption", [], <<>>, []) of {ok, XML} -> XPath = "/ServerSideEncryptionConfiguration" "/Rule" "/ApplyServerSideEncryptionByDefault", Algorithm = XPath ++ "/SSEAlgorithm", KMSKey = XPath ++ "/KMSMasterKeyID", {ok, [ {sse_algorithm, erlcloud_xml:get_text(Algorithm, XML)}, {kms_master_key_id, erlcloud_xml:get_text(KMSKey, XML, undefined)} ]}; Error -> Error end. %% @doc %% S3 API: %% https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketDELETEencryption.html %% %% ` %% ok = erlcloud_s3:delete_bucket_encryption("bucket-name"). %% ' %% -spec delete_bucket_encryption(string()) -> ok | {error, any()}. delete_bucket_encryption(BucketName) -> delete_bucket_encryption(BucketName, default_config()). -spec delete_bucket_encryption(string(), aws_config()) -> ok | {error, any()}. delete_bucket_encryption(BucketName, Config) -> s3_xml_request2(Config, delete, BucketName, "/", "encryption", [], <<>>, []). %% takes an S3 bucket notification configuration and creates an xmerl simple %% form out of it. %% for the examples of input / output of this function, see tests. -spec create_notification_xml(proplist()) -> tuple(). create_notification_xml(Confs) -> {'NotificationConfiguration', [create_notification_xml(ConfName, Params) || [{ConfName, Params}] <- Confs]}. -spec create_notification_xml(atom(), proplist()) -> tuple(). create_notification_xml(ConfName, Params) -> {conf_name_in_xml(ConfName), lists:foldr(fun create_notification_param_xml/2, [], Params)}. conf_name_in_xml(queue_configuration) -> 'QueueConfiguration'; conf_name_in_xml(topic_configuration) -> 'TopicConfiguration'; conf_name_in_xml(cloud_function_configuration) -> 'CloudFunctionConfiguration'. filter_rule_tuple(Name, Value) -> {'FilterRule', [{'Name', [Name]}, {'Value', [Value]}]}. filter_rule({prefix, Value}) -> filter_rule_tuple("Prefix", Value); filter_rule({suffix, Value}) -> filter_rule_tuple("Suffix", Value). -spec create_notification_param_xml({atom(), term()}, proplist()) -> proplist(). %% Filter example: [{prefix, "images/"}, {suffix, "jpg"}] %% both prefix and suffix are optional create_notification_param_xml({filter, Filter}, Acc) -> FilterRules = [filter_rule({Name, Value}) || {Name, Value} <- Filter], [{'Filter', [{'S3Key', FilterRules}]} | Acc]; create_notification_param_xml({event, Events}, Acc) -> [{'Event', [Event]} || Event <- Events] ++ Acc; create_notification_param_xml({queue, Queue}, Acc) -> [{'Queue', [Queue]} | Acc]; create_notification_param_xml({topic, Topic}, Acc) -> [{'Topic', [Topic]} | Acc]; create_notification_param_xml({id, Id}, Acc) -> [{'Id', [Id]} | Acc]; create_notification_param_xml({cloud_function, CF}, Acc) -> [{'CloudFunction', [CF]} | Acc]. %%% See http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html and %%% http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html for info on %%% addressing -spec get_bucket_and_key(string()) -> {string(), string()}. get_bucket_and_key(Uri) -> #{host := Host, path := Path} = uri_string:parse(Uri), extract_location_fields(Host, Path). extract_location_fields(Host, Path) -> HostTokens = string:tokens(Host, "."), extract_bucket_and_key(HostTokens, Path). extract_bucket_and_key([Bucket, _S3, _AmazonAWS, _Com], [$/ | Key]) -> %% Virtual-hosted-style URL %% For example: bucket_name.s3.amazonaws.com/path/to/key {Bucket, Key}; extract_bucket_and_key([_S3, _AmazonAWS, _Com], [$/ | BucketAndKey]) -> %% Path-style URL %% For example: bucket_name.s3.amazonaws.com/path/to/key [Bucket, Key] = re:split(BucketAndKey, "/", [{return, list}, {parts, 2}]), {Bucket, Key}. encode_grants(Grants) -> [encode_grant(Grant) || Grant <- Grants]. encode_grant(Grant) -> Grantee = proplists:get_value(grantee, Grant), {'Grant', [encode_grantee(Grantee), {'Permission', [encode_permission(proplists:get_value(permission, Grant))]}]}. encode_grantee(Grantee) -> case proplists:get_value(id, Grantee) of undefined -> {'Grantee', [{'xmlns:xsi', ?XMLNS_SCHEMA_INSTANCE}, {'xsi:type', "Group"}], [{'URI', [proplists:get_value(uri, Grantee)]}]}; Id -> {'Grantee', [{'xmlns:xsi', ?XMLNS_SCHEMA_INSTANCE}, {'xsi:type', "CanonicalUser"}], [{'ID', [Id]}, {'DisplayName', [proplists:get_value(display_name, Grantee)]}]} end. s3_simple_request(Config, Method, Host, Path, Subresource, Params, POSTData, Headers) -> case s3_request(Config, Method, Host, Path, Subresource, Params, POSTData, Headers) of {_Headers, <<>>} -> ok; {_Headers, Body} -> XML = element(1,xmerl_scan:string(binary_to_list(Body))), case XML of #xmlElement{name='Error'} -> ErrCode = erlcloud_xml:get_text("/Error/Code", XML), ErrMsg = erlcloud_xml:get_text("/Error/Message", XML), erlang:error({s3_error, ErrCode, ErrMsg}); _ -> ok end end. s3_xml_request(Config, Method, Host, Path, Subresource, Params, POSTData, Headers) -> {_Headers, Body} = s3_request(Config, Method, Host, Path, Subresource, Params, POSTData, Headers), XML = element(1,xmerl_scan:string(binary_to_list(Body))), case XML of #xmlElement{name='Error'} -> ErrCode = erlcloud_xml:get_text("/Error/Code", XML), ErrMsg = erlcloud_xml:get_text("/Error/Message", XML), erlang:error({s3_error, ErrCode, ErrMsg}); _ -> XML end. s3_request(Config, Method, Host, Path, Subreasource, Params, POSTData, Headers) -> case s3_request2(Config, Method, Host, Path, Subreasource, Params, POSTData, Headers) of {ok, Result} -> Result; {error, Reason} -> erlang:error({aws_error, Reason}) end. %% s3_request2 returns {ok, Body} or {error, Reason} instead of throwing as s3_request does %% This is the preferred pattern for new APIs s3_request2(Config, Method, Bucket, Path, Subresource, Params, POSTData, Headers) -> case erlcloud_aws:update_config(Config) of {ok, Config1} -> case s3_request4_no_update(Config1, Method, Bucket, Path, Subresource, Params, POSTData, Headers) of {error, {http_error, StatusCode, _, _, _}} = RedirectResponse when StatusCode >= 301 andalso StatusCode < 400 -> s3_follow_redirect(RedirectResponse, Config1, Method, Bucket, Path, Subresource, Params, POSTData, Headers); {error, {http_error, StatusCode, StatusLine, Body, _Headers}} -> {error, {http_error, StatusCode, StatusLine, Body}}; Response -> Response end; {error, Reason} -> {error, Reason} end. s3_xml_request2(Config, Method, Host, Path, Subresource, Params, POSTData, Headers) -> case s3_request2(Config, Method, Host, Path, Subresource, Params, POSTData, Headers) of {ok, {_Headers, <<>>}} -> ok; {ok, {_Headers, Body}} -> XML = element(1,xmerl_scan:string(binary_to_list(Body))), case XML of #xmlElement{name='Error'} -> ErrCode = erlcloud_xml:get_text("/Error/Code", XML), ErrMsg = erlcloud_xml:get_text("/Error/Message", XML), {error, {s3_error, ErrCode, ErrMsg}}; _ -> {ok, XML} end; Error -> Error end. %% http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#create-bucket-intro %% AccessMethod can be either 'vhost' - virtual-hosted–style or %% 'path' - older path-style URLs to access a bucket. s3_request4_no_update(Config, Method, Bucket, Path, Subresource, Params, Body, Headers) -> ContentType = proplists:get_value("content-type", Headers, ""), FParams = [Param || {_, Value} = Param <- Params, Value =/= undefined], FHeaders = [Header || {_, Val} = Header <- Headers, Val =/= undefined], QueryParams = case Subresource of "" -> FParams; _ -> [{Subresource, ""} | FParams] end, S3Host = Config#aws_config.s3_host, AccessMethod = case Config#aws_config.s3_bucket_access_method of auto -> case erlcloud_util:is_dns_compliant_name(Bucket) orelse Bucket == [] of true -> vhost; _ -> path end; ManualMethod -> ManualMethod end, {EscapedPath, HostName} = case AccessMethod of vhost -> %% Add bucket name to the front of hostname, %% i.e. https://bucket.name.s3.amazonaws.com/ VHostPath = erlcloud_http:url_encode_loose(Path), VHostName = lists:flatten( [case Bucket of "" -> ""; _ -> [Bucket, $.] end, S3Host]), {VHostPath, VHostName}; path -> %% Add bucket name into a URL path %% i.e. https://bucket.s3.amazonaws.com/ PathStyleUrl = erlcloud_http:url_encode_loose( lists:flatten( [case Bucket of "" -> ""; _ -> ["/", Bucket] end, Path])), {PathStyleUrl, S3Host} end, RequestHeaders = erlcloud_aws:sign_v4( Method, EscapedPath, Config, [{"host", HostName} | FHeaders ], Body, aws_region_from_host(S3Host), "s3", QueryParams), RequestURI = lists:flatten([ Config#aws_config.s3_scheme, S3Host, port_spec(Config), EscapedPath, case Subresource of "" -> ""; _ -> [$?, Subresource] end, if FParams =:= [] -> ""; Subresource =:= "" -> [$?, erlcloud_http:make_query_string(FParams, no_assignment)]; true -> [$&, erlcloud_http:make_query_string(FParams, no_assignment)] end]), {RequestHeaders2, RequestBody} = case Method of M when M =:= get orelse M =:= head orelse M =:= delete -> {RequestHeaders, <<>>}; _ -> Headers2 = case lists:keyfind("content-type", 1, RequestHeaders) of false -> [{"content-type", ContentType} | RequestHeaders]; _ -> RequestHeaders end, {Headers2, Body} end, Request = #aws_request{service = s3, uri = RequestURI, method = Method, request_headers = RequestHeaders2, request_body = RequestBody}, Request2 = erlcloud_retry:request(Config, Request, fun s3_result_fun/1), erlcloud_aws:request_to_return(Request2). s3_result_fun(#aws_request{response_type = ok} = Request) -> Request; s3_result_fun(#aws_request{response_type = error, error_type = aws, response_status = Status} = Request) when %% Retry conflicting operations 409,Conflict and 500s %% including 503, SlowDown, Reduce your request rate. Status =:= 409; Status >= 500 -> Request#aws_request{should_retry = true}; s3_result_fun(#aws_request{response_type = error, error_type = aws} = Request) -> Request#aws_request{should_retry = false}. default_config() -> erlcloud_aws:default_config(). port_spec(#aws_config{s3_port=80}) -> ""; port_spec(#aws_config{s3_port=Port}) -> [":", erlang:integer_to_list(Port)]. %% Extract region form s3 endpoint names. %% http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region aws_region_from_host(Host) -> case string:tokens(Host, ".") of %% s3.cn-north-1.amazonaws.com.cn ["s3", Value, _, _, _] -> Value; %% s3.eu-central-1.amazonaws.com ["s3", Value, _, _] -> Value; %% s3.amazonaws.com ["s3", _, _] -> "us-east-1"; %% s3-external-1.amazonaws.com ["s3-external-1", _, _] -> "us-east-1"; %% For example: s3-us-east-1.amazonaws.com [Value, _, _] -> %% Skip "s3-" prefix string:substr(Value, 4); _ -> "us-east-1" end. %% %% http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html %% http://docs.aws.amazon.com/AmazonS3/latest/dev/Redirects.html %% http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html %% Note: Redirects are sequentially handled '#aws_config.s3_follow_redirect_count' times. %% This is needed for attempts to access a bucket in non-defaut region using %% the path-style method. Such request is redirected to virtual-hosted bucket %% endpointand then - to region specific one. %% For example: trying to get acl of "bucket-frankfurt" in eu-central-1 %% region using path-style access method. %% %% The 1st request ("https://bucket-frankfurt.s3.amazonaws.com/?acl) is %% redirected to "bucket-frankfurt.s3.amazonaws.com" endpoint. %% %% The 2nd ("https://s3.amazonaws.com/?acl" with %% {"host","bucket-frankfurt.s3.amazonaws.com"} header) is redirected %% to "bucket-frankfurt.s3.eu-central-1.amazonaws.com". %% %% And finally the 3rd request succeeds - %% ("https://s3.eu-central-1.amazonaws.com/?acl" with %% {"host","bucket-frankfurt.s3.eu-central-1.amazonaws.com""} header) s3_follow_redirect( {error, {http_error, StatusCode, StatusLine, ErrBody, _ErrHeaders}} = Response, Config, Method, Bucket, Path, Subresource, Params, POSTData, Headers) -> case Config#aws_config.s3_follow_redirect of true -> s3_follow_redirect_impl(Response, Config, Method, Bucket, Path, Subresource, Params, POSTData, Headers, Config#aws_config.s3_follow_redirect_count); _ -> {error, {http_error, StatusCode, StatusLine, ErrBody}} end. s3_follow_redirect_impl( {error, {http_error, StatusCode, StatusLine, ErrBody, _ErrHeaders}} = _Response, _Config, _Method, _Bucket, _Path, _Subresource, _Params, _POSTData, _Headers, 0) -> {error, {http_error, StatusCode, StatusLine, ErrBody}}; s3_follow_redirect_impl(Response, Config, Method, Bucket, Path, Subresource, Params, POSTData, Headers, RedirectCount) -> {S3RegionEndpoint, AccessMethod} = s3_endpoint_from_response(Config, Bucket, Response), case s3_request4_no_update( Config#aws_config{s3_host = S3RegionEndpoint, s3_bucket_access_method = AccessMethod}, Method, Bucket, Path, Subresource, Params, POSTData, Headers) of {error, {http_error, RedirectCode, _, _, _}} = RedirectResponse when RedirectCode >= 301 andalso RedirectCode < 400 -> s3_follow_redirect_impl(RedirectResponse, Config, Method, Bucket, Path, Subresource, Params, POSTData, Headers, RedirectCount - 1); {error, {http_error, ErrorCode, ErrorLine, ErrorBody, _ErrorHeaders}} -> {error, {http_error, ErrorCode, ErrorLine, ErrorBody}}; FinalResponse -> FinalResponse end. s3_endpoint_from_response(Config, Bucket, {error, {http_error, _Code, _Msg, ErrBody, ErrHeaders}} = _Response) -> case {proplists:get_value("x-amz-bucket-region", ErrHeaders), proplists:get_value("location", ErrHeaders)} of {undefined, undefined} -> %% Try to get redirect location from error message. XML = element(1,xmerl_scan:string(binary_to_list(ErrBody))), case erlcloud_xml:get_text("/Error/Endpoint", XML) of [] -> {Config#aws_config.s3_host, Config#aws_config.s3_bucket_access_method}; Name -> s3_endpoint_from_hostname(Name, Bucket) end; {undefined, RedirectUrl} -> %% Use "location" header value if there is no "x-amz-bucket-region" one. [_Scheme, HostName | _] = string:tokens(RedirectUrl, "/"), s3_endpoint_from_hostname(HostName, Bucket); {BucketRegion, _} -> %% Use "x-amz-bucket-region" header value if present. {s3_endpoint_for_region(BucketRegion), Config#aws_config.s3_bucket_access_method} end. %% If bucket name is a part of the input hostname then virtual hosted-style access %% should be used to access this bucket and bucket name should be subtracted %% from the hostname. Otherwise send requests to provided endpoint as is %% using path-style method. %% Examples: %% s3_endpoint_from_hostname( %% "test.bucket.s3.eu-central-1.amazonaws.com", %% "test.bucket") -> {"s3.eu-central-1.amazonaws.com", vhost} %% s3_endpoint_from_hostname( %% "s3.amazonaws.com", %% "test.bucket") -> {"s3.amazonaws.com", path} s3_endpoint_from_hostname(HostName, Bucket) -> case lists:prefix(Bucket, HostName) of true -> {HostName -- lists:flatten([Bucket, $.]), vhost}; false -> {HostName, path} end. s3_endpoint_for_region(RegionName) -> case RegionName of "us-east-1" -> "s3-external-1.amazonaws.com"; _ -> lists:flatten(["s3-", RegionName, ".amazonaws.com"]) end.