diff --git a/rebar b/rebar new file mode 100755 index 0000000..29bdd59 Binary files /dev/null and b/rebar differ diff --git a/src/erlaws.app.src b/src/erlaws.app.src new file mode 100644 index 0000000..6478c07 --- /dev/null +++ b/src/erlaws.app.src @@ -0,0 +1,8 @@ +%% -*- mode: erlang; -*- +{application, erlaws, + [{description, "Erlang interface to Amazon Web Service"}, + {vsn, "0.0.1"}, + {modules, []}, + {registered, []}, + {applications, [kernel, stdlib, inets, ssl, crypto]}, + {env, []}]}. diff --git a/src/erlaws.erl b/src/erlaws.erl index c1f3fa7..c4b4b9e 100644 --- a/src/erlaws.erl +++ b/src/erlaws.erl @@ -6,7 +6,7 @@ start() -> application:start(sasl), - crypto:start(), + ssl:start(), inets:start(). start(_Type, _Args) -> diff --git a/src/erlaws_ec2.erl b/src/erlaws_ec2.erl index e9191b2..31916cd 100644 --- a/src/erlaws_ec2.erl +++ b/src/erlaws_ec2.erl @@ -1,21 +1,24 @@ --module(erlaws_ec2, [AWS_KEY, AWS_SEC_KEY, SECURE]). +-module(erlaws_ec2). -author (dieu). -include_lib("xmerl/include/xmerl.hrl"). --export ([start_instances/1, run_instances/13, stop_instances/2, terminate_instances/1, describe_instances/0, describe_instances/1]). +-export ([new/3, start_instances/2, run_instances/14, stop_instances/3, terminate_instances/2, describe_instances/1, describe_instances/2]). -define (AWS_EC2_HOST, "ec2.amazonaws.com"). -define (AWS_EC2_VERSION, "2009-11-30"). - + +new(AWS_KEY, AWS_SEC_KEY, SECURE) -> + {?MODULE, [AWS_KEY, AWS_SEC_KEY, SECURE]}. + % API -describe_instances() -> - describe_instances([]). +describe_instances({?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + describe_instances([], THIS). -describe_instances(InstanceIds) -> +describe_instances(InstanceIds, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> Fun = fun(AIM, [Inc, List]) -> [Inc + 1, lists:append(List, [{lists:flatten(io_lib:format("InstanceId.~p", [Inc])), AIM}])] end, [_, Params] = lists:foldl(Fun, [1, []], InstanceIds), - try query_request("DescribeInstances", Params) of + try query_request("DescribeInstances", Params, THIS) of {ok, Body} -> {ResponseXML, _Rest} = xmerl_scan:string(Body), Response = lists:foldl( @@ -40,8 +43,8 @@ describe_instances(InstanceIds) -> {error, Descr} end. -terminate_instances(InstanceId) -> - try query_request("TerminateInstances", [{"InstanceId", InstanceId}]) of +terminate_instances(InstanceId, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request("TerminateInstances", [{"InstanceId", InstanceId}], THIS) of {ok, Body} -> {ResponseXML, _Rest} = xmerl_scan:string(Body), Response = lists:foldl( @@ -58,8 +61,8 @@ terminate_instances(InstanceId) -> {error, Descr} end. -stop_instances(InstanceId, Force) -> - try query_request("StopInstances", [{"InstanceId", InstanceId}, {"Force", Force}]) of +stop_instances(InstanceId, Force, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request("StopInstances", [{"InstanceId", InstanceId}, {"Force", Force}], THIS) of {ok, Body} -> {ResponseXML, _Rest} = xmerl_scan:string(Body), Response = lists:foldl( @@ -76,10 +79,10 @@ stop_instances(InstanceId, Force) -> {error, Descr} end. -start_instances(InstanceIds) -> +start_instances(InstanceIds, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> Fun = fun(AIM, [Inc, List]) -> [Inc + 1, lists:append(List, [{lists:flatten(io_lib:format("InstanceId.~p", [Inc])), AIM}])] end, [_, Params] = lists:foldl(Fun, [1, []], InstanceIds), - try query_request("StartInstances", Params) of + try query_request("StartInstances", Params, THIS) of {ok, Body} -> {ResponseXML, _Rest} = xmerl_scan:string(Body), Response = lists:foldl( @@ -98,12 +101,14 @@ start_instances(InstanceIds) -> %InstanceType Valid Values: m1.small | m1.large | m1.xlarge | c1.medium | c1.xlarge | m2.xlarge | m2.2xlarge | m2.4xlarge run_instances(ImageId, MinCount, MaxCount, KeyName, SecurityGroups, AddressingType, InstanceType, - KernelId, RamdiskId, Monitoring, SubnetId, DisableApiTermination, InstanceInitiatedShutdownBehavior) -> + KernelId, RamdiskId, Monitoring, SubnetId, DisableApiTermination, InstanceInitiatedShutdownBehavior, + {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> Fun = fun(Group, [Inc, List]) -> [Inc + 1, lists:append(List, [{lists:flatten(io_lib:format("SecurityGroup.~p", [Inc])), Group}])] end, [_, SecGroups] = lists:foldl(Fun, [1, []], SecurityGroups), try query_request("RunInstances", [{"ImageId", ImageId}, {"MinCount", lists:flatten(io_lib:format("~p", [MinCount]))}, {"KeyName", KeyName}] ++ SecGroups ++ [{"AddressingType", AddressingType}, {"InstanceType", InstanceType}, {"KernelId", KernelId}, {"RamdiskId", RamdiskId}, {"Monitoring", Monitoring}, - {"SubnetId", SubnetId}, {"DisableApiTermination", DisableApiTermination}, {"InstanceInitiatedShutdownBehavior", InstanceInitiatedShutdownBehavior}, {"MaxCount", lists:flatten(io_lib:format("~p", [MaxCount]))}]) of + {"SubnetId", SubnetId}, {"DisableApiTermination", DisableApiTermination}, {"InstanceInitiatedShutdownBehavior", InstanceInitiatedShutdownBehavior}, {"MaxCount", lists:flatten(io_lib:format("~p", [MaxCount]))}], + THIS) of {ok, Body} -> {ResponseXML, _Rest} = xmerl_scan:string(Body), Response = lists:foldl( @@ -121,17 +126,17 @@ run_instances(ImageId, MinCount, MaxCount, KeyName, SecurityGroups, AddressingTy {error, Descr} end. -sign (Key,Data) -> +sign (Key, Data, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}) -> binary_to_list( base64:encode( crypto:sha_mac(Key,Data) ) ). -query_request(Action, Parameters) -> +query_request(Action, Parameters, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, SECURE]}=THIS) -> case SECURE of true -> Prefix = "https://"; _ -> Prefix = "http://" end, - query_request(Prefix ++ ?AWS_EC2_HOST ++ "/", Action, Parameters). + query_request(Prefix ++ ?AWS_EC2_HOST ++ "/", Action, Parameters, THIS). -query_request(Url, Action, Parameters) -> +query_request(Url, Action, Parameters, {?MODULE, [AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> Timestamp = lists:flatten(erlaws_util:get_timestamp()), SignParams = [{"Action", Action}, {"AWSAccessKeyId", AWS_KEY}, {"Timestamp", Timestamp}] ++ Parameters ++ [{"SignatureVersion", "1"}, {"Version", ?AWS_EC2_VERSION}], @@ -140,18 +145,18 @@ query_request(Url, Action, Parameters) -> {KeyB, _} = B, string:to_lower(KeyA) =< string:to_lower(KeyB) end, SignParams)], ""), - Signature = sign(AWS_SEC_KEY, StringToSign), + Signature = sign(AWS_SEC_KEY, StringToSign, THIS), FinalQueryParams = SignParams ++ [{"Signature", Signature}], - Result = mkReq(get, Url, [], FinalQueryParams, "", ""), + Result = mkReq(get, Url, [], FinalQueryParams, "", "", THIS), case Result of {ok, _Status, Body} -> {ok, Body}; {error, {_Proto, Code, Reason}, Body} -> - throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) + throw({error, {integer_to_list(Code), Reason}, mkErr(Body, THIS)}) end. -mkReq(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody) -> +mkReq(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}) -> Url = PreUrl ++ erlaws_util:queryParams( QueryParams ), Request = case Method of get -> { Url, Headers }; @@ -160,13 +165,13 @@ mkReq(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody) -> HttpOptions = [{autoredirect, true}], Options = [ {sync,true}, {headers_as_is,true}, {body_format, binary} ], - {ok, {Status, _ReplyHeaders, Body}} = http:request(Method, Request, HttpOptions, Options), + {ok, {Status, _ReplyHeaders, Body}} = httpc:request(Method, Request, HttpOptions, Options), case Status of {_, 200, _} -> {ok, Status, binary_to_list(Body)}; {_, _, _} -> {error, Status, binary_to_list(Body)} end. -mkErr(Xml) -> +mkErr(Xml, _THIS) -> {XmlDoc, _Rest} = xmerl_scan:string( Xml ), [#xmlText{value=ErrorCode}|_] = xmerl_xpath:string("//Error/Code/text()", XmlDoc), ErrorMessage = diff --git a/src/erlaws_s3.erl b/src/erlaws_s3.erl index 9411566..07807b2 100644 --- a/src/erlaws_s3.erl +++ b/src/erlaws_s3.erl @@ -6,13 +6,14 @@ %%% Created : 25 Dec 2007 by Sascha Matzke %%%------------------------------------------------------------------- --module(erlaws_s3, [AWS_KEY, AWS_SEC_KEY, SECURE]). +-module(erlaws_s3). %% API --export([list_buckets/0, create_bucket/1, create_bucket/2, delete_bucket/1]). --export([list_contents/1, list_contents/2, put_object/5, put_file/5, get_object/2]). --export([info_object/2, delete_object/2]). --export([initiate_mp_upload/4, complete_mp_upload/4, abort_mp_upload/3, upload_part/6]). +-export([new/3]). +-export([list_buckets/1, create_bucket/2, create_bucket/3, delete_bucket/2]). +-export([list_contents/2, list_contents/3, put_object/6, put_file/6, get_object/3]). +-export([info_object/3, delete_object/3]). +-export([initiate_mp_upload/5, complete_mp_upload/5, abort_mp_upload/4, upload_part/7]). %% include record definitions -include_lib("xmerl/include/xmerl.hrl"). @@ -27,15 +28,18 @@ -define(PREFIX_XPATH, "//CommonPrefixes/Prefix/text()"). -define(CHUNK_SIZE, 8 * 1024). +new(AWS_KEY, AWS_SEC_KEY, SECURE) -> + {?MODULE, [AWS_KEY, AWS_SEC_KEY, SECURE]}. + %% Returns a list of all of the buckets owned by the authenticated sender %% of the request. %% %% Spec: list_buckets() -> -%% {ok, Buckets::[Name::string()]} | +%% {ok, Buckets::[Name::string()], ReqId::string()} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -list_buckets() -> - try genericRequest(get, "", "", [], [], [], <<>>) of +list_buckets({?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(get, "", "", [], [], [], <<>>, THIS) of {ok, Headers, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(binary_to_list(Body)), TextNodes = xmerl_xpath:string("//Bucket/Name/text()", XmlDoc), @@ -54,11 +58,11 @@ list_buckets() -> %% for information on bucket naming restrictions. %% %% Spec: create_bucket(Bucket::string()) -> -%% {ok, Bucket::string()} | +%% {ok, Bucket::string(), ReqId::string()} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -create_bucket(Bucket) -> - try genericRequest(put, Bucket, "", [], [], [], <<>>) of +create_bucket(Bucket, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(put, Bucket, "", [], [], [], <<>>, THIS) of {ok, Headers, _Body} -> RequestId = case lists:keytake("x-amz-request-id", 1, Headers) of {value, {_, ReqId}, _} -> ReqId; @@ -81,20 +85,20 @@ create_bucket(Bucket) -> %% {ok, Bucket::string()} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -create_bucket(Bucket, eu) -> - create_bucket(Bucket, 'EU'); +create_bucket(Bucket, eu, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + create_bucket(Bucket, 'EU', THIS); %% Creates a new bucket with a location constraint. %% ex) create_bucket("bucket", 'ap-southeast-1') %% %% Spec: create_bucket(Bucket::string(), Region::atom()) -> %% {ok, Bucket::string()} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} -create_bucket(Bucket, Region) -> +create_bucket(Bucket, Region, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> LCfgStr = io_lib:format(" ~s ", [Region]), LCfg = list_to_binary(LCfgStr), - try genericRequest(put, Bucket, "", [], [], [], LCfg) of + try genericRequest(put, Bucket, "", [], [], [], LCfg, THIS) of {ok, Headers, _Body} -> RequestId = case lists:keytake("x-amz-request-id", 1, Headers) of {value, {_, ReqId}, _} -> ReqId; @@ -108,11 +112,11 @@ create_bucket(Bucket, Region) -> %% Deletes a bucket. %% %% Spec: delete_bucket(Bucket::string()) -> -%% {ok} | +%% {ok, {requestId, RequestId}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -delete_bucket(Bucket) -> - try genericRequest(delete, Bucket, "", [], [], [], <<>>) of +delete_bucket(Bucket, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(delete, Bucket, "", [], [], [], <<>>, THIS) of {ok, Headers, _Body} -> RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of {value, {_, ReqId}, _} -> ReqId; @@ -131,8 +135,8 @@ delete_bucket(Bucket) -> %% prefix::[string()]}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -list_contents(Bucket) -> - list_contents(Bucket, []). +list_contents(Bucket, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + list_contents(Bucket, [], THIS). %% Lists the contents of a bucket. %% @@ -146,15 +150,15 @@ list_contents(Bucket) -> %% Options -> [{prefix, string()}, {marker, string()}, %% {max_keys, integer()}, {delimiter, string()}] %% -list_contents(Bucket, Options) when is_list(Options) -> - QueryParameters = [makeParam(X) || X <- Options], - try genericRequest(get, Bucket, "", QueryParameters, [], [], <<>>) of +list_contents(Bucket, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Options) -> + QueryParameters = [makeParam(X, THIS) || X <- Options], + try genericRequest(get, Bucket, "", QueryParameters, [], [], <<>>, THIS) of {ok, Headers, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(binary_to_list(Body)), [Truncated| _Tail] = xmerl_xpath:string("//IsTruncated/text()", XmlDoc), ContentNodes = xmerl_xpath:string("//Contents", XmlDoc), - KeyList = [extractObjectInfo(Node) || Node <- ContentNodes], + KeyList = [extractObjectInfo(Node, THIS) || Node <- ContentNodes], PrefixList = [Node#xmlText.value || Node <- xmerl_xpath:string(?PREFIX_XPATH, XmlDoc)], RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of @@ -177,8 +181,8 @@ list_contents(Bucket, Options) when is_list(Options) -> %% {ok, #s3_object_info(key=Key::string(), size=Size::integer())} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -put_object(Bucket, Key, Data, ContentType, Metadata) when is_integer(hd(ContentType)) -> - put_object(Bucket, Key, Data, [{"Content-Type", ContentType}], Metadata); +put_object(Bucket, Key, Data, ContentType, Metadata, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_integer(hd(ContentType)) -> + put_object(Bucket, Key, Data, [{"Content-Type", ContentType}], Metadata, THIS); %% Uploads data for key. More general version. %% @@ -193,8 +197,8 @@ put_object(Bucket, Key, Data, ContentType, Metadata) when is_integer(hd(ContentT %% S3:put_object("someBucket", "filename.js", <<"...">>, [{"Content-Type", "application/x-javascript; charset=\"utf-8\""},{"Cache-Control", "max-age=86400"},{"x-amz-acl", "public-read"}], [{"name", "metavalue"}]). %% S3:put_object("someBucket", "filename.mp4", <<"...">>, [{"Content-Type", "video/mp4"}, {"x-amz-storage-class", "REDUCED_REDUNDANCY"}], []). %% -put_object(Bucket, Key, Data, HTTPHeaders, Metadata) -> - try genericRequest(put, Bucket, Key, [], Metadata, HTTPHeaders, Data) of +put_object(Bucket, Key, Data, HTTPHeaders, Metadata, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(put, Bucket, Key, [], Metadata, HTTPHeaders, Data, THIS) of {ok, Headers, _Body} -> {ok, #s3_object_info{key=Key, size=iolist_size(Data), @@ -208,13 +212,13 @@ put_object(Bucket, Key, Data, HTTPHeaders, Metadata) -> %% @doc %% Initiates multipart upload. %% @end --spec initiate_mp_upload(Bucket::string(), Key::string(), - HTTPHeaders::[{string(), string()}], - Metadata::[{string(), string()}]) -> - {ok, UploadId::string(), ReqId::string()}. -initiate_mp_upload(Bucket, Key, HTTPHeaders, Metadata) -> +%% -spec initiate_mp_upload(Bucket::string(), Key::string(), +%% HTTPHeaders::[{string(), string()}], +%% Metadata::[{string(), string()}]) -> +%% {ok, UploadId::string(), ReqId::string()}. +initiate_mp_upload(Bucket, Key, HTTPHeaders, Metadata, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try genericRequest(post, Bucket, Key, [{"uploads", ""}], - Metadata, HTTPHeaders, <<>>) of + Metadata, HTTPHeaders, <<>>, THIS) of {ok, Headers, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(binary_to_list(Body)), [_XBucket|_] = xmerl_xpath:string("/InitiateMultipartUploadResult/Bucket/text()", XmlDoc), @@ -229,9 +233,9 @@ initiate_mp_upload(Bucket, Key, HTTPHeaders, Metadata) -> %% @doc %% Completes multipart upload. %% @end --spec complete_mp_upload(Bucket::string(), Key::string(), UploadId::string(), - [{PartNum::integer(), ETag::string()}]) -> ok. -complete_mp_upload(Bucket, Key, UploadId, Parts) -> +%% -spec complete_mp_upload(Bucket::string(), Key::string(), UploadId::string(), +%% [{PartNum::integer(), ETag::string()}]) -> ok. +complete_mp_upload(Bucket, Key, UploadId, Parts, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> F = fun({PartNum, ETag}) -> {'Part', [], [ @@ -243,7 +247,7 @@ complete_mp_upload(Bucket, Key, UploadId, Parts) -> Req = {'CompleteMultipartUpload', [], [F(X) || X <- Parts]}, XMLReqBody = xmerl:export_simple([Req], xmerl_xml), try genericRequest(post, Bucket, Key, - [{"uploadId", UploadId}], [], [], XMLReqBody) of + [{"uploadId", UploadId}], [], [], XMLReqBody, THIS) of {ok, _Headers, _Body} -> ok catch throw:{error, Descr} -> @@ -253,11 +257,11 @@ complete_mp_upload(Bucket, Key, UploadId, Parts) -> %% @doc %% Aborts multipart upload. %% @end --spec abort_mp_upload(Bucket::string(), Key::string(), UploadId::string()) -> - ok. -abort_mp_upload(Bucket, Key, UploadId) -> +%% -spec abort_mp_upload(Bucket::string(), Key::string(), UploadId::string()) -> +%% ok. +abort_mp_upload(Bucket, Key, UploadId, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try genericRequest(delete, Bucket, Key, - [{"uploadId", UploadId}], [], [], <<>>) of + [{"uploadId", UploadId}], [], [], <<>>, THIS) of {ok, _Headers, _Body} -> ok catch throw:{error, Descr} -> @@ -267,14 +271,14 @@ abort_mp_upload(Bucket, Key, UploadId) -> %% @doc %% Uploads a part in multipart upload. %% @end --spec upload_part(Bucket::string(), Key::string(), PartNum::integer(), - UploadId::string(), Data::binary(), - HTTPHeaders::[{string(), string()}]) -> ok. -upload_part(Bucket, Key, PartNum, UploadId, Data, HTTPHeaders) -> +%% -spec upload_part(Bucket::string(), Key::string(), PartNum::integer(), +%% UploadId::string(), Data::binary(), +%% HTTPHeaders::[{string(), string()}]) -> ok. +upload_part(Bucket, Key, PartNum, UploadId, Data, HTTPHeaders, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try genericRequest(put, Bucket, Key, [{"partNumber", integer_to_list(PartNum)}, {"uploadId", UploadId}], - [], HTTPHeaders, Data) of + [], HTTPHeaders, Data, THIS) of {ok, Headers, _Body} -> {ok, #s3_object_info{key=Key, size=iolist_size(Data), @@ -285,15 +289,15 @@ upload_part(Bucket, Key, PartNum, UploadId, Data, HTTPHeaders) -> {error, Descr} end. -put_file(Bucket, Key, FileName, ContentType, Metadata) -> +put_file(Bucket, Key, FileName, ContentType, Metadata, {?MODULE, [AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> Date = httpd_util:rfc1123_date(erlang:localtime()), - {FileSize, File} = openAndGetFileSize(FileName), + {FileSize, File} = openAndGetFileSize(FileName, THIS), Headers = - buildContentHeaders(FileSize) ++ - buildMetadataHeaders(Metadata), + buildContentHeaders(FileSize, THIS) ++ + buildMetadataHeaders(Metadata, THIS), Signature = sign(AWS_SEC_KEY, stringToSign("PUT", "", ContentType, Date, - Bucket, Key, Headers)), + Bucket, Key, Headers, THIS), THIS), FinalHeaders = [ {"Authorization", "AWS " ++ AWS_KEY ++ ":" ++ Signature }, {"Host", buildHost(Bucket) }, {"Date", Date }, @@ -308,7 +312,7 @@ put_file(Bucket, Key, FileName, ContentType, Metadata) -> {ok, Socket} = gen_tcp:connect(?AWS_S3_HOST, 80, [binary, {active, false}, {packet, 0}]), gen_tcp:send(Socket, list_to_binary(Payload)), - sendData(Socket, File), + sendData(Socket, File, THIS), gen_tcp:close(Socket), file:close(File). @@ -318,8 +322,8 @@ put_file(Bucket, Key, FileName, ContentType, Metadata) -> %% {ok, Data::binary()} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -get_object(Bucket, Key) -> - try genericRequest(get, Bucket, Key, [], [], [], <<>>) of +get_object(Bucket, Key, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(get, Bucket, Key, [], [], [], <<>>, THIS) of {ok, Headers, Body} -> RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of {value, {_, ReqId}, _} -> ReqId; @@ -336,10 +340,9 @@ get_object(Bucket, Key) -> %% {ok, [{Key::string(), Value::string()},...], {requestId, ReqId::string()}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -info_object(Bucket, Key) -> - try genericRequest(head, Bucket, Key, [], [], [], <<>>) of +info_object(Bucket, Key, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(head, Bucket, Key, [], [], [], <<>>, THIS) of {ok, Headers, _Body} -> - io:format("Headers: ~p~n", [Headers]), MetadataList = [{string:substr(MKey, 12), Value} || {MKey, Value} <- Headers, string:str(MKey, "x-amz-meta") == 1], RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of {value, {_, ReqId}, _} -> ReqId; @@ -353,11 +356,11 @@ info_object(Bucket, Key) -> %% Delete the given key from bucket. %% %% Spec: delete_object(Bucket::string(), Key::string()) -> -%% {ok} | +%% {ok, {requestId, ReqId::string()}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% -delete_object(Bucket, Key) -> - try genericRequest(delete, Bucket, Key, [], [], [], <<>>) of +delete_object(Bucket, Key, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try genericRequest(delete, Bucket, Key, [], [], [], <<>>, THIS) of {ok, Headers, _Body} -> RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of {value, {_, ReqId}, _} -> ReqId; @@ -393,12 +396,12 @@ canonicalizeAmzHeaders( Headers ) -> collapse(XAmzHeaders)), erlaws_util:mkEnumeration( [[String, "\n"] || String <- Strings], ""). -canonicalizeResource ( "", "" ) -> "/"; -canonicalizeResource ( Bucket, "" ) -> "/" ++ Bucket ++ "/"; -canonicalizeResource ( "", Path) -> "/" ++ Path; -canonicalizeResource ( Bucket, Path ) -> "/" ++ Bucket ++ "/" ++ Path. +canonicalizeResource ( "", "", _THIS ) -> "/"; +canonicalizeResource ( Bucket, "", _THIS ) -> "/" ++ Bucket ++ "/"; +canonicalizeResource ( "", Path, _THIS) -> "/" ++ Path; +canonicalizeResource ( Bucket, Path, _THIS ) -> "/" ++ Bucket ++ "/" ++ Path. -makeParam(X) -> +makeParam(X, _THIS) -> case X of {_, []} -> {}; {prefix, Prefix} -> @@ -418,59 +421,59 @@ buildHost("") -> buildHost(Bucket) -> Bucket ++ "." ++ ?AWS_S3_HOST. -buildProtocol() -> +buildProtocol({?MODULE, [_AWS_KEY, _AWS_SEC_KEY, SECURE]}) -> case SECURE of true -> "https://"; _ -> "http://" end. -buildUrl("", "", []) -> - buildProtocol() ++ ?AWS_S3_HOST ++ "/"; -buildUrl("", Path, []) -> - buildProtocol() ++ ?AWS_S3_HOST ++ Path; -buildUrl(Bucket,Path,QueryParams) -> - buildProtocol() ++ Bucket ++ "." ++ ?AWS_S3_HOST ++ "/" ++ Path ++ +buildUrl("", "", [], THIS) -> + buildProtocol(THIS) ++ ?AWS_S3_HOST ++ "/"; +buildUrl("", Path, [], THIS) -> + buildProtocol(THIS) ++ ?AWS_S3_HOST ++ Path; +buildUrl(Bucket,Path,QueryParams,THIS) -> + buildProtocol(THIS) ++ Bucket ++ "." ++ ?AWS_S3_HOST ++ "/" ++ Path ++ erlaws_util:queryParams(QueryParams). -buildContentHeaders(Contents) when is_integer(Contents) -> +buildContentHeaders(Contents, _THIS) when is_integer(Contents) -> [{"Content-Length", integer_to_list(Contents)}]; % Detect gzip header and put appropriate Content-Encoding. Questionable?.. -buildContentHeaders(<<16#1f, 16#8b, _/binary>> = Contents) -> +buildContentHeaders(<<16#1f, 16#8b, _/binary>> = Contents, _THIS) -> [{"Content-Length", integer_to_list(iolist_size(Contents))}, {"Content-Encoding", "gzip"}]; -buildContentHeaders(Contents) -> +buildContentHeaders(Contents, _THIS) -> [{"Content-Length", integer_to_list(iolist_size(Contents))}]. -buildMetadataHeaders(Metadata) -> +buildMetadataHeaders(Metadata, _THIS) -> lists:foldl(fun({Key, Value}, Acc) -> [{string:to_lower("x-amz-meta-"++Key), Value} | Acc] end, [], Metadata). -buildContentMD5Header(ContentMD5) -> +buildContentMD5Header(ContentMD5, _THIS) -> case ContentMD5 of "" -> []; _ -> [{"Content-MD5", ContentMD5}] end. stringToSign ( Verb, ContentMD5, ContentType, Date, Bucket, Path, - OriginalHeaders ) -> + OriginalHeaders, THIS ) -> Parts = [ Verb, ContentMD5, ContentType, Date, canonicalizeAmzHeaders(OriginalHeaders)], erlaws_util:mkEnumeration( Parts, "\n") ++ - canonicalizeResource(Bucket, Path). + canonicalizeResource(Bucket, Path, THIS). -sign (Key,Data) -> +sign (Key,Data,_THIS) -> binary_to_list( base64:encode( crypto:sha_mac(Key,Data) ) ). genericRequest( Method, Bucket, Path, QueryParams, Metadata, - HTTPHeaders, Body ) -> + HTTPHeaders, Body, THIS ) -> genericRequest( Method, Bucket, Path, QueryParams, Metadata, - HTTPHeaders, Body, ?NR_OF_RETRIES). + HTTPHeaders, Body, ?NR_OF_RETRIES, THIS). genericRequest( Method, Bucket, Path, QueryParams, Metadata, - HTTPHeaders, Body, NrOfRetries) -> + HTTPHeaders, Body, NrOfRetries, {?MODULE, [AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> Date = httpd_util:rfc1123_date(erlang:localtime()), MethodString = string:to_upper( atom_to_list(Method) ), - Url = buildUrl(Bucket,Path,QueryParams), + Url = lists:flatten(buildUrl(Bucket,Path,QueryParams,THIS)), ContentMD5 = case iolist_size(Body) of 0 -> ""; @@ -478,9 +481,9 @@ genericRequest( Method, Bucket, Path, QueryParams, Metadata, end, Headers = - buildContentHeaders(Body) ++ - buildMetadataHeaders(Metadata) ++ - buildContentMD5Header(ContentMD5) ++ + buildContentHeaders(Body, THIS) ++ + buildMetadataHeaders(Metadata, THIS) ++ + buildContentMD5Header(ContentMD5, THIS) ++ HTTPHeaders, ContentType = case [Value || {"Content-Type", Value} <- HTTPHeaders] of @@ -494,7 +497,7 @@ genericRequest( Method, Bucket, Path, QueryParams, Metadata, stringToSign(MethodString, ContentMD5, ContentType, Date, Bucket, Path ++ erlaws_util:queryParams(QueryParams), - Headers )), + Headers, THIS), THIS), FinalHeaders = [ {"Authorization","AWS " ++ AccessKey ++ ":" ++ Signature }, {"Host", buildHost(Bucket) }, @@ -513,7 +516,7 @@ genericRequest( Method, Bucket, Path, QueryParams, Metadata, HttpOptions = [{autoredirect, true}], Options = [ {sync,true}, {headers_as_is,true}, {body_format, binary} ], - Reply = http:request( Method, Request, HttpOptions, Options ), + Reply = httpc:request( Method, Request, HttpOptions, Options ), %% {ok, {Status, ReplyHeaders, RBody}} = Reply, %% io:format("Response:~n ~p~n~p~n~p~n", [Status, ReplyHeaders, @@ -533,11 +536,11 @@ genericRequest( Method, Bucket, Path, QueryParams, Metadata, _ResponseBody }} when Code=:=500 -> timer:sleep((?NR_OF_RETRIES-NrOfRetries)*500), genericRequest(Method, Bucket, Path, QueryParams, - Metadata, HTTPHeaders, Body, NrOfRetries-1); + Metadata, HTTPHeaders, Body, NrOfRetries-1, THIS); {ok, {{_HttpVersion, HttpCode, ReasonPhrase}, ResponseHeaders, ResponseBody }} -> - throw (try mkErr(ResponseBody, ResponseHeaders) of + throw (try mkErr(ResponseBody, ResponseHeaders, THIS) of {error, Reason} -> {error, Reason} catch exit:_Error -> @@ -546,7 +549,7 @@ genericRequest( Method, Bucket, Path, QueryParams, Metadata, end) end. -mkErr (Xml, Headers) -> +mkErr (Xml, Headers, _THIS) -> {XmlDoc, _Rest} = xmerl_scan:string( binary_to_list(Xml) ), [#xmlText{value=ErrorCode}|_] = xmerl_xpath:string("/Error/Code/text()", XmlDoc), @@ -555,7 +558,7 @@ mkErr (Xml, Headers) -> {error, {ErrorCode, ErrorMessage, proplists:get_value(?S3_REQ_ID_HEADER, Headers)}}. -extractObjectInfo (Node) -> +extractObjectInfo (Node, _THIS) -> [Key|_] = xmerl_xpath:string("./Key/text()", Node), [ETag|_] = xmerl_xpath:string("./ETag/text()", Node), [LastModified|_] = xmerl_xpath:string("./LastModified/text()", Node), @@ -563,7 +566,7 @@ extractObjectInfo (Node) -> #s3_object_info{key=Key#xmlText.value, lastmodified=LastModified#xmlText.value, etag=ETag#xmlText.value, size=Size#xmlText.value}. -openAndGetFileSize(FileName) -> +openAndGetFileSize(FileName, _THIS) -> case file:open(FileName, [read, binary]) of {ok, File} -> {ok, #file_info{size=Size}} = file:read_file_info(FileName), @@ -572,11 +575,11 @@ openAndGetFileSize(FileName) -> {error, no_file} end. -sendData(Socket, File) -> +sendData(Socket, File, _THIS) -> case file:read(File, ?CHUNK_SIZE) of {ok, Data} -> gen_tcp:send(Socket, Data), - sendData(Socket, File); + sendData(Socket, File, _THIS); eof -> ok end. diff --git a/src/erlaws_sdb.erl b/src/erlaws_sdb.erl index f2653a3..90c2298 100644 --- a/src/erlaws_sdb.erl +++ b/src/erlaws_sdb.erl @@ -6,15 +6,16 @@ %% @end %%%------------------------------------------------------------------- --module(erlaws_sdb, [AWS_KEY, AWS_SEC_KEY, SECURE]). +-module(erlaws_sdb). %% exports --export([create_domain/1, delete_domain/1, list_domains/0, list_domains/1, - put_attributes/3, put_attributes/4, batch_put_attributes/2, - delete_item/2, delete_attributes/3, delete_attributes/4, - get_attributes/2, get_attributes/3, get_attributes/4, - list_items/1, list_items/2, - query_items/2, query_items/3, select/1, select/2, storage_size/2]). +-export([new/3]). +-export([create_domain/2, delete_domain/2, list_domains/1, list_domains/2, + put_attributes/4, put_attributes/5, batch_put_attributes/3, + delete_item/3, delete_attributes/4, delete_attributes/5, + get_attributes/3, get_attributes/4, get_attributes/5, + list_items/2, list_items/3, + query_items/3, query_items/4, select/2, select/3, storage_size/3]). %% include record definitions -include_lib("xmerl/include/xmerl.hrl"). @@ -24,6 +25,9 @@ -define(AWS_SDB_VERSION, "2009-04-15"). -define(USE_SIGNATURE_V1, false). +new(AWS_KEY, AWS_SEC_KEY, SECURE) -> + {?MODULE, [AWS_KEY, AWS_SEC_KEY, SECURE]}. + %% This function creates a new SimpleDB domain. The domain name must be unique among the %% domains associated with your AWS Access Key ID. This function might take 10 %% or more seconds to complete.. @@ -34,9 +38,9 @@ %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NumberDomainsExceeded" %% -create_domain(Domain) -> +create_domain(Domain, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try genericRequest("CreateDomain", - Domain, "", [], []) of + Domain, "", [], [], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -51,14 +55,14 @@ create_domain(Domain) -> %% are deleted as well. This function might take 10 or more seconds to complete. %% %% Spec: delete_domain(Domain::string()) -> -%% {ok, Domain::string()} | +%% {ok, {requestId, RequestId:string()}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% %% Code::string() -> "MissingParameter" %% -delete_domain(Domain) -> +delete_domain(Domain, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try genericRequest("DeleteDomain", - Domain, "", [], []) of + Domain, "", [], [], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -77,8 +81,8 @@ delete_domain(Domain) -> %% %% See list_domains/1 for a detailed error description %% -list_domains() -> - list_domains([]). +list_domains({?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + list_domains([], THIS). %% Lists domains up to the limit set by {max_domains, integer()}. %% A NextToken is returned if there are more than max_domains domains. @@ -86,17 +90,17 @@ list_domains() -> %% to max_domains more domain names each time. %% %% Spec: list_domains(Options::[{atom, (string() | integer())}]) -> -%% {ok, DomainNames::[string()], []} | -%% {ok, DomainNames::[string()], NextToken::string()} | +%% {ok, DomainNames::[string()], [], {requestId, ReqId::string()}} | +%% {ok, DomainNames::[string()], NextToken::string(), {requestId, ReqId::string()}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% %% Options -> [{max_domains, integer()}, {next_token, string()}] %% %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | "MissingParameter" %% -list_domains(Options) -> +list_domains(Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try genericRequest("ListDomains", "", "", [], - [makeParam(X) || X <- Options]) of + [makeParam(X) || X <- Options], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), DomainNodes = xmerl_xpath:string("//ListDomainsResult/DomainName/text()", @@ -151,10 +155,11 @@ list_domains(Options) -> %% "NumberItemAttributesExceeded" | "NumberDomainAttributesExceeded" | %% "NumberDomainBytesExceeded" %% -put_attributes(Domain, Item, Attributes) when is_list(Domain), - is_list(Item), - is_list(Attributes) -> - put_attributes(Domain, Item, Attributes, []). +put_attributes(Domain, Item, Attributes, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) + when is_list(Domain), + is_list(Item), + is_list(Attributes) -> + put_attributes(Domain, Item, Attributes, [], THIS). %% You may request "conditional put" by providing the 4th argument to %% put_attributes/4. @@ -164,12 +169,12 @@ put_attributes(Domain, Item, Attributes) when is_list(Domain), %% [{expected, AttributeName, false}]. %% %% See: http://developer.amazonwebservices.com/connect/entry.jspa?externalID=3572 -put_attributes(Domain, Item, Attributes, Options) when is_list(Domain), +put_attributes(Domain, Item, Attributes, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Item), is_list(Attributes), is_list(Options) -> try genericRequest("PutAttributes", Domain, Item, - Attributes, [makeParam(X) || X <- Options]) of + Attributes, [makeParam(X) || X <- Options], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -183,15 +188,15 @@ put_attributes(Domain, Item, Attributes, Options) when is_list(Domain), %% Deletes one or more attributes associated with the item. %% %% Spec: delete_attributes(Domain::string(), Item::string, Attributes::[string()]) -> -%% {ok} | +%% {ok, {requestId, ReqId::string()}} | %% {error, {Code::string(), Msg::string(), ReqId::string()}} %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" %% -delete_attributes(Domain, Item, Attributes) when is_list(Domain), +delete_attributes(Domain, Item, Attributes, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Item), is_list(Attributes) -> - delete_attributes(Domain, Item, Attributes, []). + delete_attributes(Domain, Item, Attributes, [], THIS). %% You may request "conditional delete" by providing the 4th argument to %% put_attributes/4. @@ -201,12 +206,19 @@ delete_attributes(Domain, Item, Attributes) when is_list(Domain), %% [{expected, AttributeName, false}]. %% %% See: http://developer.amazonwebservices.com/connect/entry.jspa?externalID=3572 -delete_attributes(Domain, Item, Attributes, Options) when is_list(Domain), +delete_attributes(Domain, Item, Attributes, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Item), is_list(Attributes), is_list(Options) -> + Attributes0 = + case length(Attributes) > 0 andalso not (is_tuple(hd(Attributes))) of + true -> %% [ "attr1", "attr2", "attr3" ] + lists:map(fun(X) -> {X, dummy} end, Attributes); + false -> %% [ {"attr1", 1}, {"attr2", 2}, {"attr3", 3} ] + Attributes + end, try genericRequest("DeleteAttributes", Domain, Item, - Attributes, [makeParam(X) || X <- Options]) of + Attributes0, [makeParam(X) || X <- Options], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -247,10 +259,10 @@ delete_attributes(Domain, Item, Attributes, Options) when is_list(Domain), %% "NumberDomainAttributesExceeded" | "NumberDomainBytesExceeded" | %% "NumberSubmittedItemsExceeded" | "NumberSubmittedAttrubutesExceeded" %% -batch_put_attributes(Domain, ItemAttributes) when is_list(Domain), +batch_put_attributes(Domain, ItemAttributes, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(ItemAttributes) -> try genericRequest("BatchPutAttributes", Domain, "", - ItemAttributes, []) of + ItemAttributes, [], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -269,9 +281,9 @@ batch_put_attributes(Domain, ItemAttributes) when is_list(Domain), %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" %% -delete_item(Domain, Item) when is_list(Domain), +delete_item(Domain, Item, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Item) -> - try delete_attributes(Domain, Item, []) of + try delete_attributes(Domain, Item, [], THIS) of {ok, RequestId} -> {ok, RequestId} catch throw:{error, Descr} -> @@ -294,10 +306,10 @@ delete_item(Domain, Item) when is_list(Domain), %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" %% -get_attributes(Domain, Items) when is_list(Domain), +get_attributes(Domain, Items, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Items), is_list(hd(Items)) -> - get_attributes(Domain, Items, ""); + get_attributes(Domain, Items, "", THIS); %% Returns all of the attributes associated with the item. %% @@ -316,9 +328,9 @@ get_attributes(Domain, Items) when is_list(Domain), %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" %% -get_attributes(Domain, Item) when is_list(Domain), +get_attributes(Domain, Item, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Item) -> - get_attributes(Domain, Item, ""). + get_attributes(Domain, Item, "", THIS). %% Returns the requested attribute for a list of items. %% @@ -330,12 +342,12 @@ get_attributes(Domain, Item) when is_list(Domain), %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" %% -get_attributes(Domain, Items, Attribute) when is_list(Domain), +get_attributes(Domain, Items, Attribute, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Items), is_list(Attribute) -> - get_attributes(Domain, Items, Attribute, []). + get_attributes(Domain, Items, Attribute, [], THIS). -get_attributes(Domain, Items, Attribute, Options) when is_list(Domain), +get_attributes(Domain, Items, Attribute, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Items), is_list(hd(Items)), is_list(Attribute), @@ -343,7 +355,7 @@ get_attributes(Domain, Items, Attribute, Options) when is_list(Domain), Fetch = fun(X) -> ParentPID = self(), spawn(fun() -> - case get_attributes(Domain, X, Attribute, Options) of + case get_attributes(Domain, X, Attribute, Options, THIS) of {ok, [ItemResult]} -> ParentPID ! { ok, ItemResult }; {error, Descr} -> @@ -374,12 +386,12 @@ get_attributes(Domain, Items, Attribute, Options) when is_list(Domain), %% %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" %% -get_attributes(Domain, Item, Attribute, Options) when is_list(Domain), +get_attributes(Domain, Item, Attribute, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Domain), is_list(Item), is_list(Attribute), is_list(Options) -> try genericRequest("GetAttributes", Domain, Item, - Attribute, [makeParam(X) || X <- Options]) of + Attribute, [makeParam(X) || X <- Options], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), AttrList = [{KN, VN} || Node <- xmerl_xpath:string("//Attribute", XmlDoc), @@ -418,8 +430,8 @@ get_attributes(Domain, Item, Attribute, Options) when is_list(Domain), %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | %% "MissingParameter" | "NoSuchDomain" %% -list_items(Domain) -> - list_items(Domain, []). +list_items(Domain, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + list_items(Domain, [], THIS). %% Returns up to max_items -> integer() <= 250 items of a domain. If @@ -439,9 +451,9 @@ list_items(Domain) -> %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | %% "MissingParameter" | "NoSuchDomain" %% -list_items(Domain, Options) when is_list(Options) -> +list_items(Domain, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Options) -> try genericRequest("Query", Domain, "", [], - [makeParam(X) || X <- [{version, ?OLD_AWS_SDB_VERSION}|Options]]) of + [makeParam(X) || X <- [{version, ?OLD_AWS_SDB_VERSION}|Options]], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), ItemNodes = xmerl_xpath:string("//ItemName/text()", XmlDoc), @@ -472,13 +484,13 @@ list_items(Domain, Options) when is_list(Options) -> %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | "InvalidNumberPredicates" %% | "InvalidNumberValueTests" | "InvalidQueryExpression" | "InvalidSortExpression" %% | "MissingParameter" | "NoSuchDomain" | "RequestTimeout" | "TooManyRequestAttributes" -select(SelectExp) -> - select(SelectExp, []). +select(SelectExp, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + select(SelectExp, [], THIS). -select(SelectExp, Options) when is_list(Options) -> +select(SelectExp, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Options) -> try genericRequest("Select", "", "", [], [{"SelectExpression", SelectExp}| - [makeParam(X) || X <- Options]]) of + [makeParam(X) || X <- Options]], THIS) of {ok, Body} -> {XmlDoc_, _Rest} = xmerl_scan:string(Body), ItemNodes = xmerl_xpath:string("//Item", XmlDoc_), @@ -526,8 +538,8 @@ select(SelectExp, Options) when is_list(Options) -> %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | %% "MissingParameter" | "NoSuchDomain" %% -query_items(Domain, QueryExp) -> - query_items(Domain, QueryExp, []). +query_items(Domain, QueryExp, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + query_items(Domain, QueryExp, [], THIS). %% Executes the given query expression against a domain. The syntax for %% such a query spec is documented here: @@ -547,10 +559,10 @@ query_items(Domain, QueryExp) -> %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | %% "MissingParameter" | "NoSuchDomain" %% -query_items(Domain, QueryExp, Options) when is_list(Options) -> +query_items(Domain, QueryExp, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_list(Options) -> {ok, Body} = genericRequest("Query", Domain, "", [], [{"QueryExpression", QueryExp}| - [makeParam(X) || X <- [{version, ?OLD_AWS_SDB_VERSION}|Options]]]), + [makeParam(X) || X <- [{version, ?OLD_AWS_SDB_VERSION}|Options]]], THIS), {XmlDoc, _Rest} = xmerl_scan:string(Body), ItemNodes = xmerl_xpath:string("//ItemName/text()", XmlDoc), [#xmlText{value=RequestId}|_] = @@ -560,40 +572,39 @@ query_items(Domain, QueryExp, Options) when is_list(Options) -> %% storage cost -storage_size(Item, Attributes) -> +storage_size(Item, Attributes, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> ItemSize = length(Item) + 45, - {AttrSize, ValueSize} = calcAttrStorageSize(Attributes), + {AttrSize, ValueSize} = calcAttrStorageSize(Attributes, THIS), {AttrSize, ItemSize + ValueSize}. %% internal functions -calcAttrStorageSize(Attributes) -> - calcAttrStorageSize(Attributes, {0, 0}). +calcAttrStorageSize(Attributes, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + calcAttrStorageSize(Attributes, {0, 0}, THIS). -calcAttrStorageSize([{Attr, ValueList}|Rest], {AttrSize, ValueSize}) -> +calcAttrStorageSize([{Attr, ValueList}|Rest], {AttrSize, ValueSize}, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> calcAttrStorageSize(Rest, {AttrSize + length(Attr) + 45, - calcValueStorageSize(ValueSize, ValueList)}); -calcAttrStorageSize([], Result) -> + calcValueStorageSize(ValueSize, ValueList, THIS)}, THIS); +calcAttrStorageSize([], Result, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}) -> Result. -calcValueStorageSize(ValueSize, [Value|Rest]) -> - calcValueStorageSize(ValueSize + length(Value) + 45, Rest); -calcValueStorageSize(ValueSize, []) -> +calcValueStorageSize(ValueSize, [Value|Rest], {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + calcValueStorageSize(ValueSize + length(Value) + 45, Rest, THIS); +calcValueStorageSize(ValueSize, [], {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}) -> ValueSize. -sign (Key,Data) -> +sign (Key,Data, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}) -> %io:format("StringToSign:~n ~p~n", [Data]), binary_to_list( base64:encode( crypto:sha_mac(Key,Data) ) ). genericRequest(Action, Domain, Item, - Attributes, Options) when ?USE_SIGNATURE_V1 -> - genericRequestV1(Action, Domain, Item, Attributes, Options); + Attributes, Options, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when ?USE_SIGNATURE_V1 -> + genericRequestV1(Action, Domain, Item, Attributes, Options, THIS); genericRequest(Action, Domain, Item, - Attributes, Options) -> - io:format("Options: ~p~n", [Options]), + Attributes, Options, {?MODULE, [AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> Timestamp = lists:flatten(erlaws_util:get_timestamp()), ActionQueryParams = getQueryParams(Action, Domain, Item, Attributes, - lists:flatten(Options)), + lists:flatten(Options), THIS), Params = [{"AWSAccessKeyId", AWS_KEY}, {"Action", Action}, {"Timestamp", Timestamp} @@ -603,60 +614,59 @@ genericRequest(Action, Domain, Item, _ -> ActionQueryParams end, - Result = mkReq(Params), + Result = mkReq(Params, THIS), case Result of {ok, _Status, Body} -> {ok, Body}; {error, {_Proto, _Code, _Reason}, Body} -> %throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) - throw({error, mkErr(Body)}) + throw({error, mkErr(Body, THIS)}) end. -getQueryParams("CreateDomain", Domain, _Item, _Attributes, _Options) -> +getQueryParams("CreateDomain", Domain, _Item, _Attributes, _Options, _THIS) -> [{"DomainName", Domain}]; -getQueryParams("DeleteDomain", Domain, _Item, _Attributes, _Options) -> +getQueryParams("DeleteDomain", Domain, _Item, _Attributes, _Options, _THIS) -> [{"DomainName", Domain}]; -getQueryParams("ListDomains", _Domain, _Item, _Attributes, Options) -> +getQueryParams("ListDomains", _Domain, _Item, _Attributes, Options, _THIS) -> Options; -getQueryParams("PutAttributes", Domain, Item, Attributes, Options) -> +getQueryParams("PutAttributes", Domain, Item, Attributes, Options, THIS) -> Options ++ [{"DomainName", Domain}, {"ItemName", Item}] ++ - buildAttributeParams(Attributes); -getQueryParams("BatchPutAttributes", Domain, _Item, ItemAttributes, _Options) -> + buildAttributeParams(Attributes, THIS); +getQueryParams("BatchPutAttributes", Domain, _Item, ItemAttributes, _Options, THIS) -> [{"DomainName", Domain}| - buildBatchAttributeParams(ItemAttributes)]; -getQueryParams("GetAttributes", Domain, Item, Attribute, Options) -> + buildBatchAttributeParams(ItemAttributes, THIS)]; +getQueryParams("GetAttributes", Domain, Item, Attribute, Options, _THIS) -> Options ++ [{"DomainName", Domain}, {"ItemName", Item}] ++ if length(Attribute) > 0 -> [{"AttributeName", Attribute}]; true -> [] end; -getQueryParams("DeleteAttributes", Domain, Item, Attributes, Options) -> +getQueryParams("DeleteAttributes", Domain, Item, Attributes, Options, THIS) -> Options ++ [{"DomainName", Domain}, {"ItemName", Item}] ++ if length(Attributes) > 0 -> - buildAttributeParams(Attributes); + buildAttributeParams(Attributes, THIS); true -> [] end; -getQueryParams("Select", _Domain, _Item, _Attributes, Options) -> +getQueryParams("Select", _Domain, _Item, _Attributes, Options, _THIS) -> Options; -getQueryParams("Query", Domain, _Item, _Attributes, Options) -> +getQueryParams("Query", Domain, _Item, _Attributes, Options, _THIS) -> [{"DomainName", Domain}] ++ Options. -getProtocol() -> +getProtocol({?MODULE, [_AWS_KEY, _AWS_SEC_KEY, SECURE]}) -> case SECURE of true -> "https://"; _ -> "http://" end. -mkReq(Params) -> +mkReq(Params, {?MODULE, [_AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> QueryParams = [{"SignatureVersion", "2"}|[{"SignatureMethod", "HmacSHA1"}|Params]], - io:format("~p~n", [QueryParams]), ParamsString = erlaws_util:mkEnumeration([ erlaws_util:url_encode(Key) ++ "=" ++ erlaws_util:url_encode(Value) || {Key, Value} <- lists:keysort(1, QueryParams)], "&"), StringToSign = "POST\n" ++ string:to_lower(?AWS_SDB_HOST) ++ "\n" ++ "/" ++ "\n" ++ ParamsString, - Signature = sign(AWS_SEC_KEY, StringToSign), + Signature = sign(AWS_SEC_KEY, StringToSign, THIS), SignatureString = "&Signature=" ++ erlaws_util:url_encode(Signature), - Url = getProtocol() ++ ?AWS_SDB_HOST ++ "/", + Url = getProtocol(THIS) ++ ?AWS_SDB_HOST ++ "/", PostData = ParamsString ++ SignatureString, %io:format("~s~n~s~n", [Url, PostData]), Request = {Url, [], "application/x-www-form-urlencoded", PostData}, @@ -669,13 +679,13 @@ mkReq(Params) -> {_, _, _} -> {error, Status, binary_to_list(Body)} end. -buildAttributeParams(Attributes) -> - CAttr = collapse(Attributes), +buildAttributeParams(Attributes, THIS) -> + CAttr = collapse(Attributes, THIS), {_C, L} = lists:foldl(fun flattenParams/2, {0, []}, CAttr), %io:format("FlattenedList:~n ~p~n", [L]), lists:reverse(L). -buildBatchAttributeParams(ItemAttributes) -> +buildBatchAttributeParams(ItemAttributes, THIS) -> BuildItemHdrFun = fun(F, IAs, ItemNum, Acc) -> ItemNumL = integer_to_list(ItemNum), @@ -688,7 +698,7 @@ buildBatchAttributeParams(ItemAttributes) -> [{Lead ++ X, Y}|AccIn] end, [], - buildAttributeParams(Attributes)) + buildAttributeParams(Attributes, THIS)) ++ [{Lead ++ "ItemName", Item}|Acc]) end end, @@ -709,8 +719,13 @@ flattenParams({K, V, R}, {C, L}) -> %io:format("~p -> ~p ~n", [K, Val]), NextCounter = Counter + 1, EntryName = mkEntryName(Counter, K), - EntryValue = mkEntryValue(Counter, Val), - {NextCounter, [EntryValue | [EntryName | ResultList]]} + case is_atom(Val) of + true -> + {NextCounter, [EntryName | ResultList]}; + false -> + EntryValue = mkEntryValue(Counter, Val), + {NextCounter, [EntryValue | [EntryName | ResultList]]} + end end, if length(V) > 0 -> lists:foldl(FlattenVal, PreResult, V); @@ -742,8 +757,8 @@ aggrV({K,V}, L) -> [{K,[V],false}|L]; aggrV(K, L) -> [{K, [], false}|L]. -collapse(L) -> - AggrL = lists:foldl( fun aggrV/2, [], lists:keysort(1, L) ), +collapse(L, _THIS) -> + AggrL = lists:foldl( fun aggrV/2, [], lists:keysort(1, L)), lists:keymap( fun lists:sort/1, 2, lists:reverse(AggrL)). makeParam(X) -> @@ -772,7 +787,7 @@ makeParam(X) -> aggregateAttr ({K,V}, [{K,L}|T]) -> [{K,[V|L]}|T]; aggregateAttr ({K,V}, L) -> [{K,[V]}|L]. -mkErr(Xml) -> +mkErr(Xml, _THIS) -> {XmlDoc, _Rest} = xmerl_scan:string( Xml ), [#xmlText{value=ErrorCode}|_] = xmerl_xpath:string("//Error/Code/text()", XmlDoc), [#xmlText{value=ErrorMessage}|_] = xmerl_xpath:string("//Error/Message/text()", XmlDoc), @@ -785,10 +800,10 @@ mkErr(Xml) -> %%% left below for compatibility. genericRequestV1(Action, Domain, Item, - Attributes, Options) -> + Attributes, Options, {?MODULE, [AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> Timestamp = lists:flatten(erlaws_util:get_timestamp()), ActionQueryParams = getQueryParams(Action, Domain, Item, Attributes, - lists:flatten(Options)), + lists:flatten(Options), THIS), SignParams = [{"AWSAccessKeyId", AWS_KEY}, {"Action", Action}, {"SignatureVersion", "1"}, @@ -805,20 +820,20 @@ genericRequestV1(Action, Domain, Item, string:to_lower(KeyA) =< string:to_lower(KeyB) end, SignParams)], ""), - Signature = sign(AWS_SEC_KEY, StringToSign), + Signature = sign(AWS_SEC_KEY, StringToSign, THIS), FinalQueryParams = SignParams ++ [{"Signature", Signature}], - Result = mkReqV1(FinalQueryParams), + Result = mkReqV1(FinalQueryParams, THIS), case Result of {ok, _Status, Body} -> {ok, Body}; {error, {_Proto, _Code, _Reason}, Body} -> %throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) - throw({error, mkErr(Body)}) + throw({error, mkErr(Body, THIS)}) end. -mkReqV1(QueryParams) -> +mkReqV1(QueryParams, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> %io:format("QueryParams:~n ~p~n", [QueryParams]), - Url = getProtocol() ++ ?AWS_SDB_HOST ++ "/" ++ erlaws_util:queryParams( QueryParams ), + Url = getProtocol(THIS) ++ ?AWS_SDB_HOST ++ "/" ++ erlaws_util:queryParams( QueryParams ), %io:format("RequestUrl:~n ~p~n", [Url]), Request = {Url, []}, HttpOptions = [{autoredirect, true}], diff --git a/src/erlaws_sqs.erl b/src/erlaws_sqs.erl index 8ac9e71..ce38521 100644 --- a/src/erlaws_sqs.erl +++ b/src/erlaws_sqs.erl @@ -5,14 +5,15 @@ %% @end %%%------------------------------------------------------------------- --module(erlaws_sqs,[AWS_KEY, AWS_SEC_KEY, SECURE]). +-module(erlaws_sqs). %% exports --export([list_queues/0, list_queues/1, get_queue_url/1, create_queue/1, - create_queue/2, get_queue_attr/1, set_queue_attr/3, - delete_queue/1, send_message/2, - receive_message/1, receive_message/2, receive_message/3, - delete_message/2]). +-export([new/3]). +-export([list_queues/1, list_queues/2, get_queue_url/2, create_queue/2, + create_queue/3, get_queue_attr/2, set_queue_attr/4, + delete_queue/2, send_message/3, + receive_message/2, receive_message/3, receive_message/4, + delete_message/3]). %% include record definitions -include_lib("xmerl/include/xmerl.hrl"). @@ -22,6 +23,9 @@ -define(AWS_SQS_VERSION, "2008-01-01"). -define(USE_SIGNATURE_V1, false). +new(AWS_KEY, AWS_SEC_KEY, SECURE) -> + {?MODULE, [AWS_KEY, AWS_SEC_KEY, SECURE]}. + %% queues %% Creates a new SQS queue with the given name. @@ -34,8 +38,8 @@ %% {ok, QueueUrl::string(), {requestId, RequestId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -create_queue(QueueName) -> - try query_request("CreateQueue", [{"QueueName", QueueName}]) of +create_queue(QueueName, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request("CreateQueue", [{"QueueName", QueueName}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=QueueUrl}|_] = @@ -54,9 +58,9 @@ create_queue(QueueName) -> %% {ok, QueueUrl::string(), {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -create_queue(QueueName, VisibilityTimeout) when is_integer(VisibilityTimeout) -> +create_queue(QueueName, VisibilityTimeout, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_integer(VisibilityTimeout) -> try query_request("CreateQueue", [{"QueueName", QueueName}, - {"DefaultVisibilityTimeout", integer_to_list(VisibilityTimeout)}]) of + {"DefaultVisibilityTimeout", integer_to_list(VisibilityTimeout)}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=QueueUrl}|_] = @@ -76,8 +80,8 @@ create_queue(QueueName, VisibilityTimeout) when is_integer(VisibilityTimeout) -> %% {ok, [QueueUrl::string(),...], {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -list_queues() -> - try query_request("ListQueues", []) of +list_queues({?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request("ListQueues", [], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), QueueNodes = xmerl_xpath:string("//QueueUrl/text()", XmlDoc), @@ -98,8 +102,8 @@ list_queues() -> %% {ok, [QueueUrl::string(),...], {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -list_queues(Prefix) -> - try query_request("ListQueues", [{"QueueNamePrefix", Prefix}]) of +list_queues(Prefix, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request("ListQueues", [{"QueueNamePrefix", Prefix}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), QueueNodes = xmerl_xpath:string("//QueueUrl/text()", XmlDoc), @@ -118,8 +122,8 @@ list_queues(Prefix) -> %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} | %% {error, no_match} %% -get_queue_url(QueueName) -> - try query_request("ListQueues", [{"QueueNamePrefix", QueueName}]) of +get_queue_url(QueueName, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request("ListQueues", [{"QueueNamePrefix", QueueName}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), QueueNodes = xmerl_xpath:string("//QueueUrl/text()", XmlDoc), @@ -146,9 +150,9 @@ get_queue_url(QueueName) -> %% {"ApproximateNumberOfMessages", Number::integer()}], {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -get_queue_attr(QueueUrl) -> +get_queue_attr(QueueUrl, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try query_request(QueueUrl, "GetQueueAttributes", - [{"AttributeName", "All"}]) of + [{"AttributeName", "All"}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), AttributeNodes = xmerl_xpath:string("//Attribute", XmlDoc), @@ -177,11 +181,11 @@ get_queue_attr(QueueUrl) -> %% {ok, {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -set_queue_attr(visibility_timeout, QueueUrl, Timeout) +set_queue_attr(visibility_timeout, QueueUrl, Timeout, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_integer(Timeout) -> try query_request(QueueUrl, "SetQueueAttributes", [{"Attribute.Name", "VisibilityTimeout"}, - {"Attribute.Value", integer_to_list(Timeout)}]) of + {"Attribute.Value", integer_to_list(Timeout)}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -198,8 +202,8 @@ set_queue_attr(visibility_timeout, QueueUrl, Timeout) %% {ok, {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -delete_queue(QueueUrl) -> - try query_request(QueueUrl, "DeleteQueue", []) of +delete_queue(QueueUrl, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request(QueueUrl, "DeleteQueue", [], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -219,8 +223,8 @@ delete_queue(QueueUrl) -> %% {ok, Message::#sqs_message, {requestId, ReqId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -send_message(QueueUrl, Message) -> - try query_request(QueueUrl, "SendMessage", [{"MessageBody", Message}]) of +send_message(QueueUrl, Message, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + try query_request(QueueUrl, "SendMessage", [{"MessageBody", Message}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=MessageId}|_] = @@ -242,8 +246,8 @@ send_message(QueueUrl, Message) -> %% {ok, [], {requestId, ReqId::string()}} %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -receive_message(QueueUrl) -> - receive_message(QueueUrl, 1). +receive_message(QueueUrl, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + receive_message(QueueUrl, 1, THIS). %% Tries to receive the given number of messages (<=10) from the given queue. %% @@ -252,8 +256,8 @@ receive_message(QueueUrl) -> %% {ok, [], {requestId, ReqId::string()}} %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -receive_message(QueueUrl, NrOfMessages) -> - receive_message(QueueUrl, NrOfMessages, []). +receive_message(QueueUrl, NrOfMessages, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> + receive_message(QueueUrl, NrOfMessages, [], THIS). %% Tries to receive the given number of messages (<=10) from the given queue, using the given VisibilityTimeout instead %% of the queues default value. @@ -263,12 +267,12 @@ receive_message(QueueUrl, NrOfMessages) -> %% {ok, [], {requestId, ReqId::string()}} %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -receive_message(QueueUrl, NrOfMessages, VisibilityTimeout) when is_integer(NrOfMessages) -> +receive_message(QueueUrl, NrOfMessages, VisibilityTimeout, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when is_integer(NrOfMessages) -> VisibilityTimeoutParam = case VisibilityTimeout of "" -> []; _ -> [{"VisibilityTimeout", integer_to_list(VisibilityTimeout)}] end, try query_request(QueueUrl, "ReceiveMessage", - [{"MaxNumberOfMessages", integer_to_list(NrOfMessages)}] ++ VisibilityTimeoutParam) of + [{"MaxNumberOfMessages", integer_to_list(NrOfMessages)}] ++ VisibilityTimeoutParam, THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -297,9 +301,9 @@ receive_message(QueueUrl, NrOfMessages, VisibilityTimeout) when is_integer(NrOfM %% {ok, {requestId, RequestId::string()}} | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} %% -delete_message(QueueUrl, ReceiptHandle) -> +delete_message(QueueUrl, ReceiptHandle, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> try query_request(QueueUrl, "DeleteMessage", - [{"ReceiptHandle", ReceiptHandle}]) of + [{"ReceiptHandle", ReceiptHandle}], THIS) of {ok, Body} -> {XmlDoc, _Rest} = xmerl_scan:string(Body), [#xmlText{value=RequestId}|_] = @@ -312,40 +316,47 @@ delete_message(QueueUrl, ReceiptHandle) -> %% internal methods -sign(Key,Data) -> +sign(Key,Data,_THIS) -> %%%% io:format("Sign:~n ~p~n", [Data]), binary_to_list(base64:encode(crypto:sha_mac(Key,Data))). -query_request(Action, Parameters) -> - if - SECURE -> Prefix = "https://"; - true -> Prefix = "http://" - end, - - query_request(Prefix ++ ?AWS_SQS_HOST ++ "/", Action, Parameters). +query_request(Action, Parameters, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, SECURE]}=THIS) -> + Prefix = case SECURE of + true -> "https://"; + _ -> "http://" + end, + query_request(Prefix ++ ?AWS_SQS_HOST ++ "/", Action, Parameters, THIS). -query_request(Url, Action, Parameters) when ?USE_SIGNATURE_V1 -> - query_requestV1(Url, Action, Parameters); -query_request(Url, Action, Parameters) -> +query_request(Url, Action, Parameters, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) when ?USE_SIGNATURE_V1 -> + query_requestV1(Url, Action, Parameters, THIS); +query_request(Url, Action, Parameters, {?MODULE, [AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> Timestamp = lists:flatten(erlaws_util:get_timestamp()), Params = [{"Action", Action}, {"AWSAccessKeyId", AWS_KEY}, {"Timestamp", Timestamp}] ++Parameters ++ [{"Version", ?AWS_SQS_VERSION}], - Result = mkReq(Url, Params), + Result = mkReq(Url, Params, THIS), case Result of {ok, _Status, Body} -> {ok, Body}; {error, {_Proto, Code, Reason}, Body} -> - throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) + throw({error, {integer_to_list(Code), Reason}, mkErr(Body, THIS)}) end. -mkReq(Url, Params) -> +mkReq(Url, Params, {?MODULE, [_AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> QueryParams = [{"SignatureVersion", "2"}|[{"SignatureMethod", "HmacSHA1"}|Params]], ParamsString = erlaws_util:mkEnumeration([ erlaws_util:url_encode(Key) ++ "=" ++ erlaws_util:url_encode(Value) || {Key, Value} <- lists:keysort(1, QueryParams)], "&"), - {_, _, Host, _, Path, _} = http_uri:parse(Url), + + {Host, Path} = + case http_uri:parse(Url) of + {ok, {_, _, Host0, _, Path0, _}} -> %% R15B + {Host0, Path0}; + {_, _, Host1, _, Path1, _} -> + {Host1, Path1} + end, + StringToSign = "POST\n" ++ string:to_lower(Host) ++ "\n" ++ Path ++ "\n" ++ ParamsString, - Signature = sign(AWS_SEC_KEY, StringToSign), + Signature = sign(AWS_SEC_KEY, StringToSign, THIS), SignatureString = "&Signature=" ++ erlaws_util:url_encode(Signature), PostData = ParamsString ++ SignatureString, %io:format("~p~n~s~n~s~n", [Params, Url, PostData]), @@ -359,7 +370,7 @@ mkReq(Url, Params) -> {_, _, _} -> {error, Status, binary_to_list(Body)} end. -mkErr(Xml) -> +mkErr(Xml, _THIS) -> {XmlDoc, _Rest} = xmerl_scan:string( Xml ), [#xmlText{value=ErrorCode}|_] = xmerl_xpath:string("//Error/Code/text()", XmlDoc), @@ -383,7 +394,7 @@ mkErr(Xml) -> %%% implemented above (plain requests work fine). Signature ver.1 based code are %%% left below for compatibility. -query_requestV1(Url, Action, Parameters) -> +query_requestV1(Url, Action, Parameters, {?MODULE, [AWS_KEY, AWS_SEC_KEY, _SECURE]}=THIS) -> %% io:format("query_request: ~p ~p ~p~n", [Url, Action, Parameters]), Timestamp = lists:flatten(erlaws_util:get_timestamp()), SignParams = [{"Action", Action}, {"AWSAccessKeyId", AWS_KEY}, {"Timestamp", Timestamp}] ++ @@ -394,19 +405,19 @@ query_requestV1(Url, Action, Parameters) -> string:to_lower(KeyA) =< string:to_lower(KeyB) end, SignParams)], ""), %% io:format("StringToSign: ~p~n", [StringToSign]), - Signature = sign(AWS_SEC_KEY, StringToSign), + Signature = sign(AWS_SEC_KEY, StringToSign, THIS), %% io:format("Signature: ~p~n", [Signature]), FinalQueryParams = SignParams ++ [{"Signature", Signature}], - Result = mkReqV1(get, Url, [], FinalQueryParams, "", ""), + Result = mkReqV1(get, Url, [], FinalQueryParams, "", "", THIS), case Result of {ok, _Status, Body} -> {ok, Body}; {error, {_Proto, Code, Reason}, Body} -> - throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) + throw({error, {integer_to_list(Code), Reason}, mkErr(Body, THIS)}) end. -mkReqV1(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody) -> +mkReqV1(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody, {?MODULE, [_AWS_KEY, _AWS_SEC_KEY, _SECURE]}=THIS) -> %%%% io:format("QueryParams:~n ~p~nHeaders:~n ~p~nUrl:~n ~p~n", %% [QueryParams, Headers, PreUrl]), Url = PreUrl ++ erlaws_util:queryParams( QueryParams ), @@ -419,7 +430,7 @@ mkReqV1(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody) -> HttpOptions = [{autoredirect, true}], Options = [ {sync,true}, {headers_as_is,true}, {body_format, binary} ], {ok, {Status, _ReplyHeaders, Body}} = - http:request(Method, Request, HttpOptions, Options), + http:request(Method, Request, HttpOptions, Options, THIS), %% io:format("Response:~n ~p~n", [binary_to_list(Body)]), %% io:format("Status: ~p~n", [Status]), case Status of diff --git a/src/erlaws_util.erl b/src/erlaws_util.erl index e8808cb..168c45e 100644 --- a/src/erlaws_util.erl +++ b/src/erlaws_util.erl @@ -75,8 +75,8 @@ url_encode_char([X | T], Acc) when X == $-; X == $_; X == $. -> % url_encode_char(T, [$+ | Acc]); url_encode_char([X | T], Acc) -> url_encode_char(T, [$%, d2h(X bsr 4), d2h(X band 16#0f) | Acc]); - url_encode_char([], Acc) -> - Acc. +url_encode_char([], Acc) -> + Acc. d2h(N) when N<10 -> N+$0; % Commented out to strictly comply with AWS requirement -- goura diff --git a/test/erlaws_s3_SUITE.erl b/test/erlaws_s3_SUITE.erl new file mode 100644 index 0000000..3f03571 --- /dev/null +++ b/test/erlaws_s3_SUITE.erl @@ -0,0 +1,253 @@ +%% common_test suite for erlaws_s3 + +-module(erlaws_s3_SUITE). +-include_lib("common_test/include/ct.hrl"). + +-compile(export_all). + +%%-------------------------------------------------------------------- +%% Function: suite() -> Info +%% +%% Info = [tuple()] +%% List of key/value pairs. +%% +%% Description: Returns list of tuples to set default properties +%% for the suite. +%% +%% Note: The suite/0 function is only meant to be used to return +%% default data values, not perform any other operations. +%%-------------------------------------------------------------------- +suite() -> [{timetrap, {seconds, 20}}]. + +%%-------------------------------------------------------------------- +%% Function: groups() -> [Group] +%% +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% The name of the group. +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% Group properties that may be combined. +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% The name of a test case. +%% Shuffle = shuffle | {shuffle,Seed} +%% To get cases executed in random order. +%% Seed = {integer(),integer(),integer()} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% To get execution of cases repeated. +%% N = integer() | forever +%% +%% Description: Returns a list of test case group definitions. +%%-------------------------------------------------------------------- +groups() -> []. + +%%-------------------------------------------------------------------- +%% Function: all() -> GroupsAndTestCases +%% +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% Name of a test case group. +%% TestCase = atom() +%% Name of a test case. +%% +%% Description: Returns the list of groups and test cases that +%% are to be executed. +%% +%% NB: By default, we export all 1-arity user defined functions +%%-------------------------------------------------------------------- +all() -> + [ {exports, Functions} | _ ] = ?MODULE:module_info(), + [ FName || {FName, _} <- lists:filter( + fun ({module_info,_}) -> false; + ({all,_}) -> false; + ({init_per_suite,1}) -> false; + ({end_per_suite,1}) -> false; + ({_,1}) -> true; + ({_,_}) -> false + end, Functions)]. + +%%-------------------------------------------------------------------- +%% Function: init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the suite. +%% +%% Description: Initialization before the suite. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + erlaws:start(), + + AwsKey = os:getenv("ERLAWS_TEST_AWS_KEY"), + AwsSecKey = os:getenv("ERLAWS_TEST_AWS_SEC_KEY"), + try + case (AwsKey =/= false) andalso (AwsSecKey =/= false) of + false -> + throw({skip, "Environment variable ERLAWS_TEST_AWS_KEY and ERLAWS_TEST_AWS_SEC_KEY is not set"}); + _ -> ok + end, + AwsSecure = + case os:getenv("ERLAWS_TEST_AWS_SECURE") of + false -> true; + "1" -> true; + "true" -> true; + _ -> false + end, + [{aws_key, AwsKey}, {aws_sec_key, AwsSecKey}, {secure, AwsSecure} | Config] + catch + {skip, _} = E0 -> + ct:print(E0), + E0 + end. + + +%%-------------------------------------------------------------------- +%% Function: end_per_suite(Config0) -> void() | {save_config,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% +%% Description: Cleanup after the suite. +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% Reason = term() +%% The reason for skipping all test cases and subgroups in the group. +%% +%% Description: Initialization before each test case group. +%%-------------------------------------------------------------------- +init_per_group(_group, Config) -> + Config. + + +%%-------------------------------------------------------------------- +%% Function: end_per_group(GroupName, Config0) -> +%% void() | {save_config,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% +%% Description: Cleanup after each test case group. +%%-------------------------------------------------------------------- +end_per_group(_group, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Function: init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% TestCase = atom() +%% Name of the test case that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the test case. +%% +%% Description: Initialization before each test case. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_testcase(TestCase, Config) -> + AwsKey = proplists:get_value(aws_key, Config), + AwsSecKey = proplists:get_value(aws_sec_key, Config), + Secure = proplists:get_value(secure, Config), + S3 = erlaws_s3:new(AwsKey, AwsSecKey, Secure), + [{s3, S3}|Config]. + +%%-------------------------------------------------------------------- +%% Function: end_per_testcase(TestCase, Config0) -> +%% void() | {save_config,Config1} | {fail,Reason} +%% +%% TestCase = atom() +%% Name of the test case that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for failing the test case. +%% +%% Description: Cleanup after each test case. +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, Config) -> + Config. + +test_erlaws_s3() -> + [{userdata,[{doc,"Testing the erlaws_s3 module"}]}]. + +%%test_erlaws_s3(_Config) -> +%% {skip,"Not implemented."}. + +test_connection_1_basic(Config) -> + C = proplists:get_value(s3, Config), + ct:print("C: ~p", [C]), + + %% Create a new, empty bucket + BucketName = lists:flatten(io_lib:format("test-~w-~w-~w", tuple_to_list(now()))), + ct:print("BucketName: ~s", [BucketName]), + {ok, BucketName, _} = C:create_bucket(BucketName), + + %% Try list_buckets and see if it's really there + {ok, BucketNames, _} = C:list_buckets(), + true = lists:member(BucketName, BucketNames), + + %% Upload 01 + Data01 = <<"This is a test of file upload and download">>, + {ok, _, _} = + C:put_object(BucketName, "foobar", Data01, "text/plain", []), + + %% Upload 02 + Data02 = <<"Another test data">>, + {ok, _, _} = + C:put_object(BucketName, "barbaz", Data02, "text/plain", [{"name", "metavalue"}]), + + %% Download 01 + {ok, Data01, _} = + C:get_object(BucketName, "foobar"), + + %% Download 02 + {ok, Data02, _} = + C:get_object(BucketName, "barbaz"), + + %% Try to download not existing key + {error, {"NoSuchKey", _, _}} = + C:get_object(BucketName, "bogus"), + + %% Info 01 + {ok, [], _} = C:info_object(BucketName, "foobar"), + + %% Info 02 + {ok, [{"name", "metavalue"}], _} = C:info_object(BucketName, "barbaz"), + + %% Try to delete before emptying + {error, _} = C:delete_bucket(BucketName), + + %% Delete 01 + {ok, _} = C:delete_object(BucketName, "foobar"), + + %% Delete 02 + {ok, _} = C:delete_object(BucketName, "barbaz"), + + %% Now delete bucket + {ok, _} = C:delete_bucket(BucketName), + + %% Try list_buckets and ensure it's not there + {ok, BucketNames99, _} =C:list_buckets(), + false = lists:member(BucketName, BucketNames99). + diff --git a/test/erlaws_sdb_SUITE.erl b/test/erlaws_sdb_SUITE.erl new file mode 100644 index 0000000..b013af7 --- /dev/null +++ b/test/erlaws_sdb_SUITE.erl @@ -0,0 +1,291 @@ +%% common_test suite for erlaws_sdb + +-module(erlaws_sdb_SUITE). +-include_lib("common_test/include/ct.hrl"). +-include_lib("../include/erlaws.hrl"). + +-compile(export_all). + +%%-------------------------------------------------------------------- +%% Function: suite() -> Info +%% +%% Info = [tuple()] +%% List of key/value pairs. +%% +%% Description: Returns list of tuples to set default properties +%% for the suite. +%% +%% Note: The suite/0 function is only meant to be used to return +%% default data values, not perform any other operations. +%%-------------------------------------------------------------------- +suite() -> [{timetrap, {seconds, 20}}]. + +%%-------------------------------------------------------------------- +%% Function: groups() -> [Group] +%% +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% The name of the group. +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% Group properties that may be combined. +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% The name of a test case. +%% Shuffle = shuffle | {shuffle,Seed} +%% To get cases executed in random order. +%% Seed = {integer(),integer(),integer()} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% To get execution of cases repeated. +%% N = integer() | forever +%% +%% Description: Returns a list of test case group definitions. +%%-------------------------------------------------------------------- +groups() -> []. + +%%-------------------------------------------------------------------- +%% Function: all() -> GroupsAndTestCases +%% +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% Name of a test case group. +%% TestCase = atom() +%% Name of a test case. +%% +%% Description: Returns the list of groups and test cases that +%% are to be executed. +%% +%% NB: By default, we export all 1-arity user defined functions +%%-------------------------------------------------------------------- +all() -> + [ {exports, Functions} | _ ] = ?MODULE:module_info(), + [ FName || {FName, _} <- lists:filter( + fun ({module_info,_}) -> false; + ({all,_}) -> false; + ({init_per_suite,1}) -> false; + ({end_per_suite,1}) -> false; + ({_,1}) -> true; + ({_,_}) -> false + end, Functions)]. + +%%-------------------------------------------------------------------- +%% Function: init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the suite. +%% +%% Description: Initialization before the suite. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + erlaws:start(), + + AwsKey = os:getenv("ERLAWS_TEST_AWS_KEY"), + AwsSecKey = os:getenv("ERLAWS_TEST_AWS_SEC_KEY"), + try + case (AwsKey =/= false) andalso (AwsSecKey =/= false) of + false -> + throw({skip, "Environment variable ERLAWS_TEST_AWS_KEY and ERLAWS_TEST_AWS_SEC_KEY is not set"}); + _ -> ok + end, + AwsSecure = + case os:getenv("ERLAWS_TEST_AWS_SECURE") of + false -> true; + "1" -> true; + "true" -> true; + _ -> false + end, + [{aws_key, AwsKey}, {aws_sec_key, AwsSecKey}, {secure, AwsSecure} | Config] + catch + {skip, _} = E0 -> + ct:print(E0), + E0 + end. + + +%%-------------------------------------------------------------------- +%% Function: end_per_suite(Config0) -> void() | {save_config,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% +%% Description: Cleanup after the suite. +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% Reason = term() +%% The reason for skipping all test cases and subgroups in the group. +%% +%% Description: Initialization before each test case group. +%%-------------------------------------------------------------------- +init_per_group(_group, Config) -> + Config. + + +%%-------------------------------------------------------------------- +%% Function: end_per_group(GroupName, Config0) -> +%% void() | {save_config,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% +%% Description: Cleanup after each test case group. +%%-------------------------------------------------------------------- +end_per_group(_group, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Function: init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% TestCase = atom() +%% Name of the test case that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the test case. +%% +%% Description: Initialization before each test case. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_testcase(TestCase, Config) -> + AwsKey = proplists:get_value(aws_key, Config), + AwsSecKey = proplists:get_value(aws_sec_key, Config), + Secure = proplists:get_value(secure, Config), + SDB = erlaws_sdb:new(AwsKey, AwsSecKey, Secure), + [{sdb, SDB}|Config]. + +%%-------------------------------------------------------------------- +%% Function: end_per_testcase(TestCase, Config0) -> +%% void() | {save_config,Config1} | {fail,Reason} +%% +%% TestCase = atom() +%% Name of the test case that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for failing the test case. +%% +%% Description: Cleanup after each test case. +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, Config) -> + Config. + +test_erlaws_sdb() -> + [{userdata,[{doc,"Testing the erlaws_sdb module"}]}]. + +test_1_basic(Config) -> + C = proplists:get_value(sdb, Config), + ct:print("C: ~p", [C]), + + %% Try illegal queue name + {error, {"InvalidParameterValue", _, _}} = C:create_domain("bad:domain:name"), + + %% Create a new domain + DomainName = lists:flatten(io_lib:format("test_~w_~w_~w", tuple_to_list(now()))), + + ct:print("DomainName: ~s", [DomainName]), + {ok, _} = C:create_domain(DomainName), + + try + %% Put items and attributes + SameValue = "same_value", + {ok, _} = + C:put_attributes(DomainName, "item1", + [{"name1", SameValue}, {"name2", "diff_value_1"}]), + {ok, _} = + C:put_attributes(DomainName, "item2", + [{"name1", SameValue}, {"name2", "diff_value_2"}]), + + %% Try to get the attributes and see if the match + {ok, Results0} = C:get_attributes(DomainName, "item1", "", [{consistent_read, true}]), + GotItem1 = proplists:get_value("item1", Results0), + [SameValue] = proplists:get_value("name1", GotItem1), + ["diff_value_1"] = proplists:get_value("name2", GotItem1), + + %% Try a search of two + Query1 = lists:flatten( + io_lib:format("select * from ~s where name1=\"~s\"", [DomainName, SameValue])), + {ok, Results1, _} = C:select(Query1, [{consistent_read, true}]), + 2 = length(Results1), + + Query2 = lists:flatten( + io_lib:format("select * from ~s where name2=\"diff_value_2\"", [DomainName])), + {ok, Results2, _} = C:select(Query2, [{consistent_read, true}]), + 1 = length(Results2), + + %% Delete all attributes associated with item1 (pattern #1) + {ok, _} = C:delete_attributes(DomainName, "item1", [{"name1", true}, {"name2", true}]), + + {ok, Results1a} = C:get_attributes(DomainName, "item1", "", [{consistent_read, true}]), + GotItem1a = proplists:get_value("item1", Results1a), + ct:print("~p~n", [GotItem1a]), + 0 = length(GotItem1a), + + %% Delete all attributes associated with item2 (pattern #2) + {ok, _} = C:delete_attributes(DomainName, "item2", []), + + {ok, Results2a} = C:get_attributes(DomainName, "item2", "", [{consistent_read, true}]), + GotItem2a = proplists:get_value("item2", Results2a), + 0 = length(GotItem2a), + + %% Try a batch put operation on the domain + Item3 = [{"name3_1", "value3_1"}, + {"name3_2", "value3_2"}, + {"name3_3", ["value3_3_1", "value3_3_2"]}], + Item4 = [{"name4_1", "value4_1"}, + {"name4_2", ["value4_2_1", "value4_2_2"]}, + {"name4_3", "value4_3"}], + {ok, _} = C:batch_put_attributes(DomainName, [{"item3", Item3}, {"item4", Item4}]), + + {ok, Results3} = C:get_attributes(DomainName, "item3", "", [{consistent_read, true}]), + GotItem3 = proplists:get_value("item3", Results3), + ["value3_2"] = proplists:get_value("name3_2", GotItem3), + + %% Delete item3 + {ok, _} = C:delete_item(DomainName, "item3"), + + {ok, Results4} = C:get_attributes(DomainName, "item3", "", [{consistent_read, true}]), + GotItem3a = proplists:get_value("item3", Results4), + 0 = length(GotItem3a), + + %% Delete some attributes associated with item4 (pattern #3) + {ok, _} = C:delete_attributes(DomainName, "item4", ["name4_1", "name4_3"]), + + {ok, Results4a} = C:get_attributes(DomainName, "item4", "", [{consistent_read, true}]), + GotItem4a = proplists:get_value("item4", Results4a), + 1 = length(GotItem4a), + + %% Delete some attributes associated with item4 (pattern #4) + {ok, _} = C:delete_attributes(DomainName, "item4", [{"name4_2_1", "value4_2_2"}]), + + {ok, Results4b} = C:get_attributes(DomainName, "item4", "", [{consistent_read, true}]), + GotItem4b = proplists:get_value("item4", Results4b), + 1 = length(GotItem4b) + + after + %% Delete the domain + {ok, _} = C:delete_domain(DomainName), + + %% Try a list_queues and ensure it's not there + {ok, DomainNames99, _, _} =C:list_domains(), + false = lists:member(DomainName, DomainNames99) + end. diff --git a/test/erlaws_sqs_SUITE.erl b/test/erlaws_sqs_SUITE.erl new file mode 100644 index 0000000..7bc44e9 --- /dev/null +++ b/test/erlaws_sqs_SUITE.erl @@ -0,0 +1,269 @@ +%% common_test suite for erlaws_sqs + +-module(erlaws_sqs_SUITE). +-include_lib("common_test/include/ct.hrl"). +-include_lib("../include/erlaws.hrl"). + +-compile(export_all). + +%%-------------------------------------------------------------------- +%% Function: suite() -> Info +%% +%% Info = [tuple()] +%% List of key/value pairs. +%% +%% Description: Returns list of tuples to set default properties +%% for the suite. +%% +%% Note: The suite/0 function is only meant to be used to return +%% default data values, not perform any other operations. +%%-------------------------------------------------------------------- +suite() -> [{timetrap, {seconds, 20}}]. + +%%-------------------------------------------------------------------- +%% Function: groups() -> [Group] +%% +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% The name of the group. +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% Group properties that may be combined. +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% The name of a test case. +%% Shuffle = shuffle | {shuffle,Seed} +%% To get cases executed in random order. +%% Seed = {integer(),integer(),integer()} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% To get execution of cases repeated. +%% N = integer() | forever +%% +%% Description: Returns a list of test case group definitions. +%%-------------------------------------------------------------------- +groups() -> []. + +%%-------------------------------------------------------------------- +%% Function: all() -> GroupsAndTestCases +%% +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% Name of a test case group. +%% TestCase = atom() +%% Name of a test case. +%% +%% Description: Returns the list of groups and test cases that +%% are to be executed. +%% +%% NB: By default, we export all 1-arity user defined functions +%%-------------------------------------------------------------------- +all() -> + [ {exports, Functions} | _ ] = ?MODULE:module_info(), + [ FName || {FName, _} <- lists:filter( + fun ({module_info,_}) -> false; + ({all,_}) -> false; + ({init_per_suite,1}) -> false; + ({end_per_suite,1}) -> false; + ({_,1}) -> true; + ({_,_}) -> false + end, Functions)]. + +%%-------------------------------------------------------------------- +%% Function: init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the suite. +%% +%% Description: Initialization before the suite. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + erlaws:start(), + + AwsKey = os:getenv("ERLAWS_TEST_AWS_KEY"), + AwsSecKey = os:getenv("ERLAWS_TEST_AWS_SEC_KEY"), + try + case (AwsKey =/= false) andalso (AwsSecKey =/= false) of + false -> + throw({skip, "Environment variable ERLAWS_TEST_AWS_KEY and ERLAWS_TEST_AWS_SEC_KEY is not set"}); + _ -> ok + end, + AwsSecure = + case os:getenv("ERLAWS_TEST_AWS_SECURE") of + false -> true; + "1" -> true; + "true" -> true; + _ -> false + end, + [{aws_key, AwsKey}, {aws_sec_key, AwsSecKey}, {secure, AwsSecure} | Config] + catch + {skip, _} = E0 -> + ct:print(E0), + E0 + end. + + +%%-------------------------------------------------------------------- +%% Function: end_per_suite(Config0) -> void() | {save_config,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% +%% Description: Cleanup after the suite. +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% Reason = term() +%% The reason for skipping all test cases and subgroups in the group. +%% +%% Description: Initialization before each test case group. +%%-------------------------------------------------------------------- +init_per_group(_group, Config) -> + Config. + + +%%-------------------------------------------------------------------- +%% Function: end_per_group(GroupName, Config0) -> +%% void() | {save_config,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% +%% Description: Cleanup after each test case group. +%%-------------------------------------------------------------------- +end_per_group(_group, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Function: init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% TestCase = atom() +%% Name of the test case that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the test case. +%% +%% Description: Initialization before each test case. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_testcase(TestCase, Config) -> + AwsKey = proplists:get_value(aws_key, Config), + AwsSecKey = proplists:get_value(aws_sec_key, Config), + Secure = proplists:get_value(secure, Config), + SQS = erlaws_sqs:new(AwsKey, AwsSecKey, Secure), + [{sqs, SQS}|Config]. + +%%-------------------------------------------------------------------- +%% Function: end_per_testcase(TestCase, Config0) -> +%% void() | {save_config,Config1} | {fail,Reason} +%% +%% TestCase = atom() +%% Name of the test case that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for failing the test case. +%% +%% Description: Cleanup after each test case. +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, Config) -> + Config. + +test_erlaws_sqs() -> + [{userdata,[{doc,"Testing the erlaws_sqs module"}]}]. + +test_1_basic(Config) -> + C = proplists:get_value(sqs, Config), + ct:print("C: ~p", [C]), + + %% Try illegal queue name + {error, {"400", _}, _} = C:create_queue("bad*queue*name"), + + %% Create a new queue + QueueName1 = lists:flatten(io_lib:format("test-~w-~w-~w-1", tuple_to_list(now()))), + QueueName2 = lists:flatten(io_lib:format("test-~w-~w-~w-2", tuple_to_list(now()))), + + ct:print("QueueName: ~s ~s", [QueueName1, QueueName2]), + {ok, QueueUrl1, _} = C:create_queue(QueueName1, 55), %% Set 55 for QueueName1 + {ok, QueueUrl2, _} = C:create_queue(QueueName2), %% Default for QueueName2 + ct:print("QueueUrl: ~s ~s", [QueueUrl1, QueueUrl2]), + + %% Test get_queue_url + {ok, QueueUrl1, _} = C:get_queue_url(QueueName1), + {ok, QueueUrl2, _} = C:get_queue_url(QueueName2), + + %% Try a list_queues and see if it's really there + {ok, QueueUrls1, _} = C:list_queues(QueueName1), + ct:print("QueueUrls1: ~p", [QueueUrls1]), + true = lists:member(QueueUrl1, QueueUrls1), + + {ok, QueueUrls2, _} = C:list_queues(QueueName2), + ct:print("QueueUrls1: ~p", [QueueUrls2]), + true = lists:member(QueueUrl2, QueueUrls2), + + %% Check visibility timeout + {ok, AttrList1, _} = C:get_queue_attr(QueueUrl1), + VisibilityTimeout1 = proplists:get_value("VisibilityTimeout", AttrList1), + ct:print("AttrList: ~p", [AttrList1]), + ct:print("VisibilityTimeout1: ~p", [VisibilityTimeout1]), + + {ok, AttrList2, _} = C:get_queue_attr(QueueUrl2), + VisibilityTimeout2 = proplists:get_value("VisibilityTimeout", AttrList2), + ct:print("AttrList: ~p", [AttrList2]), + ct:print("VisibilityTimeout2: ~p", [VisibilityTimeout2]), + + %% Check visibility timeout of QueueName1 + {ok, AttrList1a, _} = C:get_queue_attr(QueueUrl1), + VisibilityTimeout1a = proplists:get_value("VisibilityTimeout", AttrList1a), + VisibilityTimeout1a = 55, + + %% Set and check visibility timeout of QueueName2 + {ok, _} = C:set_queue_attr(visibility_timeout, QueueUrl2, 45), + {ok, AttrList2a, _} = C:get_queue_attr(QueueUrl2), + VisibilityTimeout2a = proplists:get_value("VisibilityTimeout", AttrList2a), + VisibilityTimeout2a = 45, + + %% Add a message + MsgBody1 = "This is a test.\n", + {ok, _Msg1, _} = C:send_message(QueueUrl1, MsgBody1), + + %% Read a message + {ok, GotMsgs1, _} = C:receive_message(QueueUrl1), + [GotMsg1] = GotMsgs1, + MsgBody1 = GotMsg1#sqs_message.body, + + %% Another read, shouldn't find anything + {ok, [], _} = C:receive_message(QueueUrl1), + + %% Delete the message + {ok, _} = C:delete_message(QueueUrl1, GotMsg1#sqs_message.receiptHandle), + + %% Delete queues + {ok, _} = C:delete_queue(QueueUrl1), + {ok, _} = C:delete_queue(QueueUrl2), + + %% Try a list_queues and ensure it's not there + {ok, QueueUrls99, _} =C:list_queues(), + false = lists:member(QueueUrl1, QueueUrls99), + false = lists:member(QueueUrl2, QueueUrls99). +