├── .gitignore ├── Emakefile ├── LICENSE ├── README.txt ├── include └── erlaws.hrl └── src ├── erlaws.erl ├── erlaws_ec2.erl ├── erlaws_s3.erl ├── erlaws_sdb.erl ├── erlaws_sqs.erl └── erlaws_util.erl /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | *~ 3 | -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | {"src/*", [debug_info, {outdir, "ebin"}, {i,"include"}]}. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2008 Sascha Matzke 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 16 | DEVELOPERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 17 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 18 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 19 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 21 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Erlaws provides Erlang interfaces to various Amazon WebService offerings. 2 | 3 | This code is no longer maintained, so feel free to fork it. 4 | 5 | -- original documentation from Google Code wiki -- 6 | 7 | = Description = 8 | Erlaws is a collection of client implementations of Amazon's WebServices offerings. Currently there are clients for S3, SQS and SDB. 9 | 10 | = Build = 11 | 12 | Check out the latest code from svn and issue {{{erl -make}}} to build the sources. 13 | 14 | = Usage = 15 | 16 | All erlaws modules (erlaws_s3, _sdb, _sqs) are now parameterized modules. You can create a new instance of a modules using (example for erlaws_sdb): 17 | 18 | SDB = erlaws_sdb:new(AWS_KEY, AWS_SEC_KEY, (true|false)). 19 | 20 | The last parameter determines whether the connection should made using plain HTTP (false) or HTTPS (true). 21 | 22 | In order to be able to use erlaws the "inets" and "crypto" application must be started. 23 | 24 | = Documentation = 25 | 26 | All available functions are documented in the .erl files for the service clients. 27 | 28 | Here a short overview: 29 | 30 | == erlaws_s3 == 31 | 32 | * list_buckets/0 33 | * create_bucket/1 34 | * create_bucket/2 (for EU buckets) 35 | * delete_bucket/1 36 | * list_contents/1 37 | * list_contents/2 38 | * put_object/5 39 | * get_object/2 40 | * info_object/2 41 | * delete_object/2 42 | 43 | == erlaws_sqs == 44 | 45 | * list_queues/0 46 | * list_queues/1 47 | * get_queue/1 48 | * create_queue/1 49 | * create_queue/2 50 | * get_queue_attr/1 51 | * set_queue_attr/3 52 | * delete_queue/1 53 | * send_message/2 54 | * receive_message/1 55 | * receive_message/2 56 | * receive_message/3 57 | * delete_message/2 58 | 59 | == erlaws_sdb == 60 | 61 | * create_domain/1 62 | * delete_domain/1 63 | * list_domains/0 64 | * list_domains/1 65 | * put_attributes/3 66 | * delete_item/2 67 | * delete_attributes/3 68 | * get_attributes/2 69 | * get_attributes/3 70 | * list_items/1 71 | * list_items/2 72 | * query_items/2 73 | * query_items/3 74 | -------------------------------------------------------------------------------- /include/erlaws.hrl: -------------------------------------------------------------------------------- 1 | %% 2 | %% erlaws record definitions 3 | %% 4 | 5 | -record( s3_list_result, {isTruncated=false, keys=[], prefixes=[]}). 6 | -record( s3_object_info, {key, lastmodified, etag, size} ). 7 | 8 | -record( sqs_queue, {nrOfMessages, visibilityTimeout}). 9 | -record( sqs_message, {messageId, receiptHandle="", contentMD5, body}). -------------------------------------------------------------------------------- /src/erlaws.erl: -------------------------------------------------------------------------------- 1 | -module(erlaws). 2 | 3 | -behaviour(application). 4 | 5 | -export([start/0, start/2, stop/1]). 6 | 7 | start() -> 8 | application:start(sasl), 9 | crypto:start(), 10 | inets:start(). 11 | 12 | start(_Type, _Args) -> 13 | erlaws:start(). 14 | 15 | stop(_State) -> 16 | ok. 17 | -------------------------------------------------------------------------------- /src/erlaws_ec2.erl: -------------------------------------------------------------------------------- 1 | -module(erlaws_ec2). 2 | -------------------------------------------------------------------------------- /src/erlaws_s3.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% File : erlaws_s3.erl 3 | %%% Author : Sascha Matzke 4 | %%% Description : Amazon S3 client library 5 | %%% 6 | %%% Created : 25 Dec 2007 by Sascha Matzke 7 | %%%------------------------------------------------------------------- 8 | 9 | -module(erlaws_s3, [AWS_KEY, AWS_SEC_KEY, SECURE]). 10 | 11 | %% API 12 | -export([list_buckets/0, create_bucket/1, create_bucket/2, delete_bucket/1]). 13 | -export([list_contents/1, list_contents/2, put_object/5, get_object/2]). 14 | -export([info_object/2, delete_object/2]). 15 | 16 | %% include record definitions 17 | -include_lib("xmerl/include/xmerl.hrl"). 18 | -include("../include/erlaws.hrl"). 19 | 20 | %% macro definitions 21 | -define( AWS_S3_HOST, "s3.amazonaws.com"). 22 | -define( NR_OF_RETRIES, 3). 23 | -define( CALL_TIMEOUT, indefinite). 24 | -define( S3_REQ_ID_HEADER, "x-amz-request-id"). 25 | -define( PREFIX_XPATH, "//CommonPrefixes/Prefix/text()"). 26 | 27 | %% Returns a list of all of the buckets owned by the authenticated sender 28 | %% of the request. 29 | %% 30 | %% Spec: list_buckets() -> 31 | %% {ok, Buckets::[Name::string()]} | 32 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 33 | %% 34 | list_buckets() -> 35 | try genericRequest(get, "", "", "", [], "", <<>>) of 36 | {ok, Headers, Body} -> 37 | {XmlDoc, _Rest} = xmerl_scan:string(binary_to_list(Body)), 38 | TextNodes = xmerl_xpath:string("//Bucket/Name/text()", XmlDoc), 39 | BExtr = fun (#xmlText{value=T}) -> T end, 40 | RequestId = case lists:keytake("x-amz-request-id", 1, Headers) of 41 | {value, {_, ReqId}, _} -> ReqId; 42 | _ -> "" end, 43 | {ok, [BExtr(Node) || Node <- TextNodes], {requestId, RequestId}} 44 | catch 45 | throw:{error, Descr} -> 46 | {error, Descr} 47 | end. 48 | 49 | %% Creates a new bucket. Not every string is an acceptable bucket name. 50 | %% See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingBucket.html 51 | %% for information on bucket naming restrictions. 52 | %% 53 | %% Spec: create_bucket(Bucket::string()) -> 54 | %% {ok, Bucket::string()} | 55 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 56 | %% 57 | create_bucket(Bucket) -> 58 | try genericRequest(put, Bucket, "", "", [], "", <<>>) of 59 | {ok, Headers, _Body} -> 60 | RequestId = case lists:keytake("x-amz-request-id", 1, Headers) of 61 | {value, {_, ReqId}, _} -> ReqId; 62 | _ -> "" end, 63 | {ok, Bucket, {requestId, RequestId}} 64 | catch 65 | throw:{error, Descr} -> 66 | {error, Descr} 67 | end. 68 | 69 | %% Creates a new bucket with a location constraint (EU). 70 | %% 71 | %% *** Be aware that Amazon applies a different pricing for EU buckets *** 72 | %% 73 | %% Not every string is an acceptable bucket name. 74 | %% See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingBucket.html 75 | %% for information on bucket naming restrictions. 76 | %% 77 | %% Spec: create_bucket(Bucket::string(), eu) -> 78 | %% {ok, Bucket::string()} | 79 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 80 | %% 81 | create_bucket(Bucket, eu) -> 82 | LCfg = <<" 83 | EU 84 | ">>, 85 | try genericRequest(put, Bucket, "", "", [], "", LCfg) of 86 | {ok, Headers, _Body} -> 87 | RequestId = case lists:keytake("x-amz-request-id", 1, Headers) of 88 | {value, {_, ReqId}, _} -> ReqId; 89 | _ -> "" end, 90 | {ok, Bucket, {requestId, RequestId}} 91 | catch 92 | throw:{error, Descr} -> 93 | {error, Descr} 94 | end. 95 | 96 | %% Deletes a bucket. 97 | %% 98 | %% Spec: delete_bucket(Bucket::string()) -> 99 | %% {ok} | 100 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 101 | %% 102 | delete_bucket(Bucket) -> 103 | try genericRequest(delete, Bucket, "", "", [], "", <<>>) of 104 | {ok, Headers, _Body} -> 105 | RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of 106 | {value, {_, ReqId}, _} -> ReqId; 107 | _ -> "" end, 108 | {ok, {requestId, RequestId}} 109 | catch 110 | throw:{error, Descr} -> 111 | {error, Descr} 112 | end. 113 | 114 | %% Lists the contents of a bucket. 115 | %% 116 | %% Spec: list_contents(Bucket::string()) -> 117 | %% {ok, #s3_list_result{isTruncated::boolean(), 118 | %% keys::[#s3_object_info{}], 119 | %% prefix::[string()]}} | 120 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 121 | %% 122 | list_contents(Bucket) -> 123 | list_contents(Bucket, []). 124 | 125 | %% Lists the contents of a bucket. 126 | %% 127 | %% Spec: list_contents(Bucket::string(), Options::[{atom(), 128 | %% (integer() | string())}]) -> 129 | %% {ok, #s3_list_result{isTruncated::boolean(), 130 | %% keys::[#s3_object_info{}], 131 | %% prefix::[string()]}} | 132 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 133 | %% 134 | %% Options -> [{prefix, string()}, {marker, string()}, 135 | %% {max_keys, integer()}, {delimiter, string()}] 136 | %% 137 | list_contents(Bucket, Options) when is_list(Options) -> 138 | QueryParameters = [makeParam(X) || X <- Options], 139 | try genericRequest(get, Bucket, "", QueryParameters, [], "", <<>>) of 140 | {ok, Headers, Body} -> 141 | {XmlDoc, _Rest} = xmerl_scan:string(binary_to_list(Body)), 142 | [Truncated| _Tail] = xmerl_xpath:string("//IsTruncated/text()", 143 | XmlDoc), 144 | ContentNodes = xmerl_xpath:string("//Contents", XmlDoc), 145 | KeyList = [extractObjectInfo(Node) || Node <- ContentNodes], 146 | PrefixList = [Node#xmlText.value || 147 | Node <- xmerl_xpath:string(?PREFIX_XPATH, XmlDoc)], 148 | RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of 149 | {value, {_, ReqId}, _} -> ReqId; 150 | _ -> "" end, 151 | {ok, #s3_list_result{isTruncated=case Truncated#xmlText.value of 152 | "true" -> true; 153 | _ -> false end, 154 | keys=KeyList, prefixes=PrefixList}, {requestId, RequestId}} 155 | catch 156 | throw:{error, Descr} -> 157 | {error, Descr} 158 | end. 159 | 160 | %% Uploads data for key. 161 | %% 162 | %% Spec: put_object(Bucket::string(), Key::string(), Data::binary(), 163 | %% ContentType::string(), 164 | %% Metadata::[{Key::string(), Value::string()}]) -> 165 | %% {ok, #s3_object_info(key=Key::string(), size=Size::integer())} | 166 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 167 | %% 168 | put_object(Bucket, Key, Data, ContentType, Metadata) -> 169 | try genericRequest(put, Bucket, Key, [], Metadata, ContentType, Data) of 170 | {ok, Headers, _Body} -> 171 | RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of 172 | {value, {_, ReqId}, _} -> ReqId; 173 | _ -> "" end, 174 | {ok, #s3_object_info{key=Key, size=size(Data)}, {requestId, RequestId}} 175 | catch 176 | throw:{error, Descr} -> 177 | {error, Descr} 178 | end. 179 | 180 | %% Retrieves the data associated with the given key. 181 | %% 182 | %% Spec: get_object(Bucket::string(), Key::string()) -> 183 | %% {ok, Data::binary()} | 184 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 185 | %% 186 | get_object(Bucket, Key) -> 187 | try genericRequest(get, Bucket, Key, [], [], "", <<>>) of 188 | {ok, Headers, Body} -> 189 | RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of 190 | {value, {_, ReqId}, _} -> ReqId; 191 | _ -> "" end, 192 | {ok, Body, {requestId, RequestId}} 193 | catch 194 | throw:{error, Descr} -> 195 | {error, Descr} 196 | end. 197 | 198 | %% Returns the metadata associated with the given key. 199 | %% 200 | %% Spec: info_object(Bucket::string(), Key::string()) -> 201 | %% {ok, [{Key::string(), Value::string()},...]} | 202 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 203 | %% 204 | info_object(Bucket, Key) -> 205 | try genericRequest(head, Bucket, Key, [], [], "", <<>>) of 206 | {ok, Headers, _Body} -> 207 | io:format("Headers: ~p~n", [Headers]), 208 | MetadataList = [{string:substr(MKey, 12), Value} || {MKey, Value} <- Headers, string:str(MKey, "x-amz-meta") == 1], 209 | RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of 210 | {value, {_, ReqId}, _} -> ReqId; 211 | _ -> "" end, 212 | {ok, MetadataList, {requestId, RequestId}} 213 | catch 214 | throw:{error, Descr} -> 215 | {error, Descr} 216 | end. 217 | 218 | %% Delete the given key from bucket. 219 | %% 220 | %% Spec: delete_object(Bucket::string(), Key::string()) -> 221 | %% {ok} | 222 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 223 | %% 224 | delete_object(Bucket, Key) -> 225 | try genericRequest(delete, Bucket, Key, [], [], "", <<>>) of 226 | {ok, Headers, _Body} -> 227 | RequestId = case lists:keytake(?S3_REQ_ID_HEADER, 1, Headers) of 228 | {value, {_, ReqId}, _} -> ReqId; 229 | _ -> "" end, 230 | {ok, {requestId, RequestId}} 231 | catch 232 | throw:{error, Descr} -> 233 | {error, Descr} 234 | end. 235 | 236 | %%-------------------------------------------------------------------- 237 | %%% Internal functions 238 | %%-------------------------------------------------------------------- 239 | 240 | isAmzHeader( Header ) -> lists:prefix("x-amz-", Header). 241 | 242 | aggregateValues ({K,V}, [{K,L}|T]) -> [{K,[V|L]}|T]; 243 | aggregateValues ({K,V}, L) -> [{K,[V]}|L]. 244 | 245 | collapse(L) -> 246 | AggrL = lists:foldl( fun aggregateValues/2, [], lists:keysort(1, L) ), 247 | lists:keymap( fun lists:sort/1, 2, lists:reverse(AggrL)). 248 | 249 | 250 | mkHdr ({Key,Values}) -> 251 | Key ++ ":" ++ erlaws_util:mkEnumeration(Values,","). 252 | 253 | canonicalizeAmzHeaders( Headers ) -> 254 | XAmzHeaders = [ {string:to_lower(Key),Value} || {Key,Value} <- Headers, 255 | isAmzHeader(Key) ], 256 | Strings = lists:map( 257 | fun mkHdr/1, 258 | collapse(XAmzHeaders)), 259 | erlaws_util:mkEnumeration( lists:map( fun (String) -> String ++ "\n" end, 260 | Strings), ""). 261 | 262 | canonicalizeResource ( "", "" ) -> "/"; 263 | canonicalizeResource ( Bucket, "" ) -> "/" ++ Bucket ++ "/"; 264 | canonicalizeResource ( "", Path) -> "/" ++ Path; 265 | canonicalizeResource ( Bucket, Path ) -> "/" ++ Bucket ++ "/" ++ Path. 266 | 267 | makeParam(X) -> 268 | case X of 269 | {_, []} -> {}; 270 | {prefix, Prefix} -> 271 | {"prefix", Prefix}; 272 | {marker, Marker} -> 273 | {"marker", Marker}; 274 | {max_keys, MaxKeys} when is_integer(MaxKeys) -> 275 | {"max-keys", integer_to_list(MaxKeys)}; 276 | {delimiter, Delimiter} -> 277 | {"delimiter", Delimiter}; 278 | _ -> {} 279 | end. 280 | 281 | 282 | buildHost("") -> 283 | ?AWS_S3_HOST; 284 | buildHost(Bucket) -> 285 | Bucket ++ "." ++ ?AWS_S3_HOST. 286 | 287 | buildProtocol() -> 288 | case SECURE of 289 | true -> "https://"; 290 | _ -> "http://" end. 291 | 292 | buildUrl("", "", []) -> 293 | buildProtocol() ++ ?AWS_S3_HOST ++ "/"; 294 | buildUrl("", Path, []) -> 295 | buildProtocol() ++ ?AWS_S3_HOST ++ Path; 296 | buildUrl(Bucket,Path,QueryParams) -> 297 | buildProtocol() ++ Bucket ++ "." ++ ?AWS_S3_HOST ++ "/" ++ Path ++ 298 | erlaws_util:queryParams(QueryParams). 299 | 300 | buildContentHeaders( <<>>, _ ) -> []; 301 | buildContentHeaders( Contents, ContentType ) -> 302 | [{"Content-Length", integer_to_list(size(Contents))}, 303 | {"Content-Type", ContentType}]. 304 | 305 | buildMetadataHeaders(Metadata) -> 306 | buildMetadataHeaders(Metadata, []). 307 | 308 | buildMetadataHeaders([], Acc) -> 309 | Acc; 310 | buildMetadataHeaders([{Key, Value}|Tail], Acc) -> 311 | buildMetadataHeaders(Tail, [{string:to_lower("x-amz-meta-"++Key), Value} 312 | | Acc]). 313 | 314 | buildContentMD5Header(ContentMD5) -> 315 | case ContentMD5 of 316 | "" -> []; 317 | _ -> [{"Content-MD5", ContentMD5}] 318 | end. 319 | 320 | stringToSign ( Verb, ContentMD5, ContentType, Date, Bucket, Path, 321 | OriginalHeaders ) -> 322 | Parts = [ Verb, ContentMD5, ContentType, Date, 323 | canonicalizeAmzHeaders(OriginalHeaders)], 324 | erlaws_util:mkEnumeration( Parts, "\n") ++ 325 | canonicalizeResource(Bucket, Path). 326 | 327 | sign (Key,Data) -> 328 | %io:format("StringToSign:~n ~p~n", [Data]), 329 | binary_to_list( base64:encode( crypto:sha_mac(Key,Data) ) ). 330 | 331 | genericRequest( Method, Bucket, Path, QueryParams, Metadata, 332 | ContentType, Body ) -> 333 | genericRequest( Method, Bucket, Path, QueryParams, Metadata, 334 | ContentType, Body, ?NR_OF_RETRIES). 335 | 336 | genericRequest( Method, Bucket, Path, QueryParams, Metadata, 337 | ContentType, Body, NrOfRetries) -> 338 | Date = httpd_util:rfc1123_date(erlang:localtime()), 339 | MethodString = string:to_upper( atom_to_list(Method) ), 340 | Url = buildUrl(Bucket,Path,QueryParams), 341 | 342 | ContentMD5 = case Body of 343 | <<>> -> ""; 344 | _ -> binary_to_list(base64:encode(erlang:md5(Body))) 345 | end, 346 | 347 | Headers = buildContentHeaders( Body, ContentType ) ++ 348 | buildMetadataHeaders(Metadata) ++ 349 | buildContentMD5Header(ContentMD5), 350 | 351 | {AccessKey, SecretAccessKey } = {AWS_KEY, AWS_SEC_KEY}, 352 | 353 | Signature = sign(SecretAccessKey, 354 | stringToSign( MethodString, ContentMD5, ContentType, Date, 355 | Bucket, Path, Headers )), 356 | 357 | FinalHeaders = [ {"Authorization","AWS " ++ AccessKey ++ ":" ++ Signature }, 358 | {"Host", buildHost(Bucket) }, 359 | {"Date", Date }, 360 | {"Expect", "Continue"} 361 | | Headers ], 362 | 363 | Request = case Method of 364 | get -> { Url, FinalHeaders }; 365 | head -> { Url, FinalHeaders }; 366 | put -> { Url, FinalHeaders, ContentType, Body }; 367 | delete -> { Url, FinalHeaders } 368 | end, 369 | 370 | HttpOptions = [{autoredirect, true}], 371 | Options = [ {sync,true}, {headers_as_is,true}, {body_format, binary} ], 372 | 373 | %%io:format("Request:~n ~p~n", [Request]), 374 | 375 | Reply = http:request( Method, Request, HttpOptions, Options ), 376 | 377 | %% {ok, {Status, ReplyHeaders, RBody}} = Reply, 378 | %% io:format("Response:~n ~p~n~p~n~p~n", [Status, ReplyHeaders, 379 | %% binary_to_list(RBody)]), 380 | 381 | case Reply of 382 | {ok, {{_HttpVersion, Code, _ReasonPhrase}, ResponseHeaders, 383 | ResponseBody }} when Code=:=200; Code=:=204 -> 384 | {ok, ResponseHeaders, ResponseBody}; 385 | 386 | {ok, {{_HttpVersion, Code, ReasonPhrase}, ResponseHeaders, 387 | _ResponseBody }} when Code=:=500, NrOfRetries > 0 -> 388 | throw ({error, "500", ReasonPhrase, 389 | proplists:get_value(?S3_REQ_ID_HEADER, ResponseHeaders)}); 390 | 391 | {ok, {{_HttpVersion, Code, _ReasonPhrase}, _ResponseHeaders, 392 | _ResponseBody }} when Code=:=500 -> 393 | timer:sleep((?NR_OF_RETRIES-NrOfRetries)*500), 394 | genericRequest(Method, Bucket, Path, QueryParams, 395 | Metadata, ContentType, Body, NrOfRetries-1); 396 | 397 | {ok, {{_HttpVersion, _HttpCode, _ReasonPhrase}, ResponseHeaders, 398 | ResponseBody }} -> 399 | throw ( mkErr(ResponseBody, ResponseHeaders) ) 400 | end. 401 | 402 | mkErr (Xml, Headers) -> 403 | {XmlDoc, _Rest} = xmerl_scan:string( binary_to_list(Xml) ), 404 | [#xmlText{value=ErrorCode}|_] = 405 | xmerl_xpath:string("/Error/Code/text()", XmlDoc), 406 | [#xmlText{value=ErrorMessage}|_] = 407 | xmerl_xpath:string("/Error/Message/text()", XmlDoc), 408 | {error, {ErrorCode, ErrorMessage, 409 | proplists:get_value(?S3_REQ_ID_HEADER, Headers)}}. 410 | 411 | extractObjectInfo (Node) -> 412 | [Key|_] = xmerl_xpath:string("./Key/text()", Node), 413 | [ETag|_] = xmerl_xpath:string("./ETag/text()", Node), 414 | [LastModified|_] = xmerl_xpath:string("./LastModified/text()", Node), 415 | [Size|_] = xmerl_xpath:string("./Size/text()", Node), 416 | #s3_object_info{key=Key#xmlText.value, lastmodified=LastModified#xmlText.value, 417 | etag=ETag#xmlText.value, size=Size#xmlText.value}. 418 | 419 | 420 | 421 | -------------------------------------------------------------------------------- /src/erlaws_sdb.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @author Sascha Matzke 3 | %% @copyright 2007 Sascha Matzke 4 | %% @doc This is an client implementation for Amazon's SimpleDB WebService 5 | %% @end 6 | %%%------------------------------------------------------------------- 7 | 8 | -module(erlaws_sdb, [AWS_KEY, AWS_SEC_KEY, SECURE]). 9 | 10 | %% exports 11 | -export([create_domain/1, delete_domain/1, list_domains/0, list_domains/1, 12 | put_attributes/3, delete_item/2, delete_attributes/3, 13 | get_attributes/2, get_attributes/3, list_items/1, list_items/2, 14 | query_items/2, query_items/3, storage_size/2]). 15 | 16 | %% include record definitions 17 | -include_lib("xmerl/include/xmerl.hrl"). 18 | 19 | -define(AWS_SDB_HOST, "sdb.amazonaws.com"). 20 | -define(AWS_SDB_VERSION, "2007-11-07"). 21 | 22 | %% This function creates a new SimpleDB domain. The domain name must be unique among the 23 | %% domains associated with your AWS Access Key ID. This function might take 10 24 | %% or more seconds to complete.. 25 | %% 26 | %% Spec: create_domain(Domain::string()) -> 27 | %% {ok, Domain::string()} | 28 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 29 | %% 30 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NumberDomainsExceeded" 31 | %% 32 | create_domain(Domain) -> 33 | try genericRequest("CreateDomain", 34 | Domain, "", [], []) of 35 | {ok, Body} -> 36 | {XmlDoc, _Rest} = xmerl:scan_string(Body), 37 | [#xmlText{value=RequestId}|_] = 38 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 39 | {ok, {requestId, RequestId}} 40 | catch 41 | throw:{error, Descr} -> 42 | {error, Descr} 43 | end. 44 | 45 | %% This function deletes a SimpleDB domain. Any items (and their attributes) in the domain 46 | %% are deleted as well. This function might take 10 or more seconds to complete. 47 | %% 48 | %% Spec: delete_domain(Domain::string()) -> 49 | %% {ok, Domain::string()} | 50 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 51 | %% 52 | %% Code::string() -> "MissingParameter" 53 | %% 54 | delete_domain(Domain) -> 55 | try genericRequest("DeleteDomain", 56 | Domain, "", [], []) of 57 | {ok, Body} -> 58 | {XmlDoc, _Rest} = xmerl:scan_string(Body), 59 | [#xmlText{value=RequestId}|_] = 60 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 61 | {ok, {requestId, RequestId}} 62 | catch 63 | throw:{error, Descr} -> 64 | {error, Descr} 65 | end. 66 | 67 | %% Lists all domains associated with your Access Key ID. 68 | %% 69 | %% Spec: list_domains() -> 70 | %% {ok, DomainNames::[string()], ""} | 71 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 72 | %% 73 | %% See list_domains/1 for a detailed error description 74 | %% 75 | list_domains() -> 76 | list_domains([]). 77 | 78 | %% Lists domains up to the limit set by {max_domains, integer()}. 79 | %% A NextToken is returned if there are more than max_domains domains. 80 | %% Calling list_domains successive times with the NextToken returns up 81 | %% to max_domains more domain names each time. 82 | %% 83 | %% Spec: list_domains(Options::[{atom, (string() | integer())}]) -> 84 | %% {ok, DomainNames::[string()], []} | 85 | %% {ok, DomainNames::[string()], NextToken::string()} | 86 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 87 | %% 88 | %% Options -> [{max_domains, integer()}, {next_token, string()}] 89 | %% 90 | %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | "MissingParameter" 91 | %% 92 | list_domains(Options) -> 93 | try genericRequest("ListDomains", "", "", [], 94 | [makeParam(X) || X <- Options]) of 95 | {ok, Body} -> 96 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 97 | DomainNodes = xmerl_xpath:string("//ListDomainsResult/DomainName/text()", 98 | XmlDoc), 99 | NextToken = case xmerl_xpath:string("//ListDomainsResult/NextToken/text()", 100 | XmlDoc) of 101 | [] -> ""; 102 | [#xmlText{value=NT}|_] -> NT 103 | end, 104 | [#xmlText{value=RequestId}|_] = 105 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 106 | {ok, [Node#xmlText.value || Node <- DomainNodes], NextToken, {requestId, RequestId}} 107 | catch 108 | throw:{error, Descr} -> 109 | {error, Descr} 110 | end. 111 | 112 | %% This function creates or replaces attributes in an item. You specify new 113 | %% attributes using a list of tuples. Attributes are uniquely identified in 114 | %% an item by their name/value combination. For example, a single item can 115 | %% have the attributes { "first_name", "first_value" } and { "first_name", 116 | %% second_value" }. However, it cannot have two attribute instances where 117 | %% both the attribute name and value are the same. 118 | %% 119 | %% Optionally, you can supply the Replace parameter for each individual 120 | %% attribute. Setting this value to true causes the new attribute value 121 | %% to replace the existing attribute value(s). For example, if an item has 122 | %% the attributes { "a", ["1"] }, { "b", ["2","3"]} and you call this function 123 | %% using the attributes { "b", "4", true }, the final attributes of the item 124 | %% are changed to { "a", ["1"] } and { "b", ["4"] }, which replaces the previous 125 | %% values of the "b" attribute with the new value. 126 | %% 127 | %% Using this function to replace attribute values that do not exist will not 128 | %% result in an error. 129 | %% 130 | %% The following limitations are enforced for this operation: 131 | %% - 100 attributes per each call 132 | %% - 256 total attribute name-value pairs per item 133 | %% - 250 million attributes per domain 134 | %% - 10 GB of total user data storage per domain 135 | %% 136 | %% Spec: put_attributes(Domain::string(), Item::string(), 137 | %% Attributes::[{Name::string(), (Value::string() | Values:[string()])}]) | 138 | %% put_attributes(Domain::string(), Item::string(), 139 | %% Attributes::[{Name::string(), (Value::string() | Values:[string()]), 140 | %% Replace -> true}]) -> 141 | %% {ok} | 142 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 143 | %% 144 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" | 145 | %% "NumberItemAttributesExceeded" | "NumberDomainAttributesExceeded" | 146 | %% "NumberDomainBytesExceeded" 147 | %% 148 | put_attributes(Domain, Item, Attributes) when is_list(Domain), 149 | is_list(Item), 150 | is_list(Attributes) -> 151 | try genericRequest("PutAttributes", Domain, Item, 152 | Attributes, []) of 153 | {ok, Body} -> 154 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 155 | [#xmlText{value=RequestId}|_] = 156 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 157 | {ok, {requestId, RequestId}} 158 | catch 159 | throw:{error, Descr} -> 160 | {error, Descr} 161 | end. 162 | 163 | %% Deletes one or more attributes associated with the item. 164 | %% 165 | %% Spec: delete_attributes(Domain::string(), Item::string, Attributes::[string()]) -> 166 | %% {ok} | 167 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 168 | %% 169 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" 170 | %% 171 | delete_attributes(Domain, Item, Attributes) when is_list(Domain), 172 | is_list(Item), 173 | is_list(Attributes) -> 174 | try genericRequest("DeleteAttributes", Domain, Item, 175 | Attributes, []) of 176 | {ok, Body} -> 177 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 178 | [#xmlText{value=RequestId}|_] = 179 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 180 | {ok, {requestId, RequestId}} 181 | catch 182 | throw:{error, Descr} -> 183 | {error, Descr} 184 | end. 185 | 186 | %% Deletes the specified item. 187 | %% 188 | %% Spec: delete_item(Domain::string(), Item::string()) -> 189 | %% {ok} | 190 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 191 | %% 192 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" 193 | %% 194 | delete_item(Domain, Item) when is_list(Domain), 195 | is_list(Item) -> 196 | try delete_attributes(Domain, Item, []) of 197 | {ok, RequestId} -> {ok, RequestId} 198 | catch 199 | throw:{error, Descr} -> 200 | {error, Descr} 201 | end. 202 | 203 | %% Returns all of the attributes associated with the items in the given list. 204 | %% 205 | %% If the item does not exist on the replica that was accessed for this 206 | %% operation, an empty set is returned. The system does not return an 207 | %% error as it cannot guarantee the item does not exist on other replicas. 208 | %% 209 | %% Note: Currently SimpleDB is only capable of returning the attributes for 210 | %% a single item. To work around this limitation, this function starts 211 | %% length(Items) parallel requests to sdb and aggregates the results. 212 | %% 213 | %% Spec: get_attributes(Domain::string(), [Item::string(),..]) -> 214 | %% {ok, Items::[{Item, Attributes::[{Name::string(), Values::[string()]}]}]} | 215 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 216 | %% 217 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" 218 | %% 219 | get_attributes(Domain, Items) when is_list(Domain), 220 | is_list(Items), 221 | is_list(hd(Items)) -> 222 | get_attributes(Domain, Items, ""); 223 | 224 | %% Returns all of the attributes associated with the item. 225 | %% 226 | %% If the item does not exist on the replica that was accessed for this 227 | %% operation, an empty set is returned. The system does not return an 228 | %% error as it cannot guarantee the item does not exist on other replicas. 229 | %% 230 | %% Note: Currently SimpleDB is only capable of returning the attributes for 231 | %% a single item. To be compatible with a possible future this function 232 | %% returns a list of {Item, Attributes::[{Name::string(), Values::[string()]}]} 233 | %% tuples. For the time being this list has exactly one member. 234 | %% 235 | %% Spec: get_attributes(Domain::string(), Item::string()) -> 236 | %% {ok, Items::[{Item, Attributes::[{Name::string(), Values::[string()]}]}]} | 237 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 238 | %% 239 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" 240 | %% 241 | get_attributes(Domain, Item) when is_list(Domain), 242 | is_list(Item) -> 243 | get_attributes(Domain, Item, ""). 244 | 245 | %% Returns the requested attribute for a list of items. 246 | %% 247 | %% See get_attributes/2 for further documentation. 248 | %% 249 | %% Spec: get_attributes(Domain::string(), [Item::string(),...], Attribute::string()) -> 250 | %% {ok, Items::[{Item, Attribute::[{Name::string(), Values::[string()]}]}]} | 251 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 252 | %% 253 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" 254 | %% 255 | get_attributes(Domain, Items, Attribute) when is_list(Domain), 256 | is_list(Items), 257 | is_list(hd(Items)), 258 | is_list(Attribute) -> 259 | Fetch = fun(X) -> 260 | ParentPID = self(), 261 | spawn(fun() -> 262 | case get_attributes(Domain, X, Attribute) of 263 | {ok, [ItemResult]} -> 264 | ParentPID ! { ok, ItemResult }; 265 | {error, Descr} -> 266 | ParentPID ! {error, Descr} 267 | end 268 | end) 269 | end, 270 | Receive= fun(_) -> 271 | receive 272 | { ok, Anything } -> Anything; 273 | { error, Descr } -> {error, Descr } 274 | end 275 | end, 276 | lists:foreach(Fetch, Items), 277 | Results = lists:map(Receive, Items), 278 | case proplists:get_all_values(error, Results) of 279 | [] -> {ok, Results}; 280 | [Error|_Rest] -> {error, Error} 281 | end; 282 | 283 | %% Returns the requested attribute for an item. 284 | %% 285 | %% See get_attributes/2 for further documentation. 286 | %% 287 | %% Spec: get_attributes(Domain::string(), Item::string(), Attribute::string()) -> 288 | %% {ok, Items::[{Item, Attribute::[{Name::string(), Values::[string()]}]}]} | 289 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 290 | %% 291 | %% Code::string() -> "InvalidParameterValue" | "MissingParameter" | "NoSuchDomain" 292 | %% 293 | get_attributes(Domain, Item, Attribute) when is_list(Domain), 294 | is_list(Item), 295 | is_list(Attribute) -> 296 | try genericRequest("GetAttributes", Domain, Item, 297 | Attribute, []) of 298 | {ok, Body} -> 299 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 300 | AttrList = [{KN, VN} || Node <- xmerl_xpath:string("//Attribute", XmlDoc), 301 | begin 302 | [#xmlText{value=KeyRaw}|_] = 303 | xmerl_xpath:string("./Name/text()", Node), 304 | KN = case xmerl_xpath:string("./Name/@encoding", Node) of 305 | [#xmlAttribute{value="base64"}|_] -> base64:decode(KeyRaw); 306 | _ -> KeyRaw end, 307 | ValueRaw = 308 | lists:flatten([ ValueR || #xmlText{value=ValueR} <- xmerl_xpath:string("./Value/text()", Node)]), 309 | VN = case xmerl_xpath:string("./Value/@encoding", Node) of 310 | [#xmlAttribute{value="base64"}|_] -> base64:decode(ValueRaw); 311 | _ -> ValueRaw end, 312 | true 313 | end], 314 | {ok, [{Item, lists:foldr(fun aggregateAttr/2, [], AttrList)}]} 315 | catch 316 | throw:{error, Descr} -> 317 | {error, Descr} 318 | end. 319 | 320 | 321 | 322 | %% Returns a list of all items of a domain - 100 at a time. If your 323 | %% domains contains more then 100 item you must use list_items/2 to 324 | %% retrieve all items. 325 | %% 326 | %% Spec: list_items(Domain::string()) -> 327 | %% {ok, Items::[string()], []} | 328 | %% {ok, Items::[string()], NextToken::string()} | 329 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 330 | %% 331 | %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | 332 | %% "MissingParameter" | "NoSuchDomain" 333 | %% 334 | list_items(Domain) -> 335 | list_items(Domain, []). 336 | 337 | 338 | %% Returns up to max_items -> integer() <= 250 items of a domain. If 339 | %% the total item count exceeds max_items you must call this function 340 | %% again with the NextToken value provided in the return value. 341 | %% 342 | %% Spec: list_items(Domain::string(), Options::[{atom(), (integer() | string())}]) -> 343 | %% {ok, Items::[string()], []} | 344 | %% {ok, Items::[string()], NextToken::string()} | 345 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 346 | %% 347 | %% Options -> [{max_items, integer()}, {next_token, string()}] 348 | %% 349 | %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | 350 | %% "MissingParameter" | "NoSuchDomain" 351 | %% 352 | list_items(Domain, Options) when is_list(Options) -> 353 | try genericRequest("Query", Domain, "", [], 354 | [makeParam(X) || X <- Options]) of 355 | {ok, Body} -> 356 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 357 | ItemNodes = xmerl_xpath:string("//ItemName/text()", XmlDoc), 358 | NextToken = case xmerl_xpath:string("//NextToken/text()", XmlDoc) of 359 | [] -> ""; 360 | [#xmlText{value=NT}|_] -> NT 361 | end, 362 | [#xmlText{value=RequestId}|_] = 363 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 364 | {ok, [Node#xmlText.value || Node <- ItemNodes], NextToken, {requestId, RequestId}} 365 | catch 366 | throw:{error, Descr} -> 367 | {error, Descr} 368 | end. 369 | 370 | %% Executes the given query expression against a domain. The syntax for 371 | %% such a query spec is documented here: 372 | %% http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_Query.html 373 | %% 374 | %% Spec: query_items(Domain::string(), QueryExp::string()]) -> 375 | %% {ok, Items::[string()], []} | 376 | %% {ok, Items::[string()], NextToken::string()} | 377 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 378 | %% 379 | %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | 380 | %% "MissingParameter" | "NoSuchDomain" 381 | %% 382 | query_items(Domain, QueryExp) -> 383 | query_items(Domain, QueryExp, []). 384 | 385 | %% Executes the given query expression against a domain. The syntax for 386 | %% such a query spec is documented here: 387 | %% http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/SDB_API_Query.html 388 | %% 389 | %% Spec: list_items(Domain::string(), QueryExp::string(), 390 | %% Options::[{atom(), (integer() | string())}]) -> 391 | %% {ok, Items::[string()], []} | 392 | %% {ok, Items::[string()], NextToken::string()} | 393 | %% {error, {Code::string(), Msg::string(), ReqId::string()}} 394 | %% 395 | %% Options -> [{max_items, integer()}, {next_token, string()}] 396 | %% 397 | %% Code::string() -> "InvalidParameterValue" | "InvalidNextToken" | 398 | %% "MissingParameter" | "NoSuchDomain" 399 | %% 400 | query_items(Domain, QueryExp, Options) when is_list(Options) -> 401 | {ok, Body} = genericRequest("Query", Domain, "", [], 402 | [{"QueryExpression", QueryExp}| 403 | [makeParam(X) || X <- Options]]), 404 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 405 | ItemNodes = xmerl_xpath:string("//ItemName/text()", XmlDoc), 406 | [#xmlText{value=RequestId}|_] = 407 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 408 | {ok, [Node#xmlText.value || Node <- ItemNodes], {requestId, RequestId}}. 409 | 410 | %% storage cost 411 | 412 | storage_size(Item, Attributes) -> 413 | ItemSize = length(Item) + 45, 414 | {AttrSize, ValueSize} = calcAttrStorageSize(Attributes), 415 | {AttrSize, ItemSize + ValueSize}. 416 | 417 | %% internal functions 418 | 419 | calcAttrStorageSize(Attributes) -> 420 | calcAttrStorageSize(Attributes, {0, 0}). 421 | 422 | calcAttrStorageSize([{Attr, ValueList}|Rest], {AttrSize, ValueSize}) -> 423 | calcAttrStorageSize(Rest, {AttrSize + length(Attr) + 45, 424 | calcValueStorageSize(ValueSize, ValueList)}); 425 | calcAttrStorageSize([], Result) -> 426 | Result. 427 | 428 | calcValueStorageSize(ValueSize, [Value|Rest]) -> 429 | calcValueStorageSize(ValueSize + length(Value) + 45, Rest); 430 | calcValueStorageSize(ValueSize, []) -> 431 | ValueSize. 432 | 433 | sign (Key,Data) -> 434 | %io:format("StringToSign:~n ~p~n", [Data]), 435 | binary_to_list( base64:encode( crypto:sha_mac(Key,Data) ) ). 436 | 437 | genericRequest(Action, Domain, Item, 438 | Attributes, Options) -> 439 | Timestamp = lists:flatten(erlaws_util:get_timestamp()), 440 | ActionQueryParams = getQueryParams(Action, Domain, Item, Attributes, 441 | Options), 442 | SignParams = [{"AWSAccessKeyId", AWS_KEY}, 443 | {"Action", Action}, 444 | {"Version", ?AWS_SDB_VERSION}, 445 | {"SignatureVersion", "1"}, 446 | {"Timestamp", Timestamp}] ++ ActionQueryParams, 447 | StringToSign = erlaws_util:mkEnumeration([Param++Value || {Param, Value} <- lists:sort(fun (A, B) -> 448 | {KeyA, _} = A, 449 | {KeyB, _} = B, 450 | string:to_lower(KeyA) =< string:to_lower(KeyB) end, 451 | SignParams)], ""), 452 | Signature = sign(AWS_SEC_KEY, StringToSign), 453 | FinalQueryParams = SignParams ++ [{"Signature", Signature}], 454 | Result = mkReq(FinalQueryParams), 455 | case Result of 456 | {ok, _Status, Body} -> 457 | {ok, Body}; 458 | {error, {_Proto, Code, Reason}, Body} -> 459 | throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) 460 | end. 461 | 462 | getQueryParams("CreateDomain", Domain, _Item, _Attributes, _Options) -> 463 | [{"DomainName", Domain}]; 464 | getQueryParams("DeleteDomain", Domain, _Item, _Attributes, _Options) -> 465 | [{"DomainName", Domain}]; 466 | getQueryParams("ListDomains", _Domain, _Item, _Attributes, Options) -> 467 | Options; 468 | getQueryParams("PutAttributes", Domain, Item, Attributes, _Options) -> 469 | [{"DomainName", Domain}, {"ItemName", Item}] ++ 470 | buildAttributeParams(Attributes); 471 | getQueryParams("GetAttributes", Domain, Item, Attribute, _Options) -> 472 | [{"DomainName", Domain}, {"ItemName", Item}] ++ 473 | if length(Attribute) > 0 -> 474 | [{"AttributeName", Attribute}]; 475 | true -> [] 476 | end; 477 | getQueryParams("DeleteAttributes", Domain, Item, Attributes, _Options) -> 478 | [{"DomainName", Domain}, {"ItemName", Item}] ++ 479 | if length(Attributes) > 0 -> 480 | buildAttributeParams(Attributes); 481 | true -> [] 482 | end; 483 | getQueryParams("Query", Domain, _Item, _Attributes, Options) -> 484 | [{"DomainName", Domain}] ++ Options. 485 | 486 | getProtocol() -> 487 | case SECURE of 488 | true -> "https://"; 489 | _ -> "http://" end. 490 | 491 | mkReq(QueryParams) -> 492 | %io:format("QueryParams:~n ~p~n", [QueryParams]), 493 | Url = getProtocol() ++ ?AWS_SDB_HOST ++ "/" ++ erlaws_util:queryParams( QueryParams ), 494 | %io:format("RequestUrl:~n ~p~n", [Url]), 495 | Request = {Url, []}, 496 | HttpOptions = [{autoredirect, true}], 497 | Options = [ {sync,true}, {headers_as_is,true}, {body_format, binary} ], 498 | {ok, {Status, _ReplyHeaders, Body}} = 499 | http:request(get, Request, HttpOptions, Options), 500 | %io:format("Response:~n ~p~n", [binary_to_list(Body)]), 501 | case Status of 502 | {_, 200, _} -> {ok, Status, binary_to_list(Body)}; 503 | {_, _, _} -> {error, Status, binary_to_list(Body)} 504 | end. 505 | 506 | buildAttributeParams(Attributes) -> 507 | CAttr = collapse(Attributes), 508 | {_C, L} = lists:foldl(fun flattenParams/2, {0, []}, CAttr), 509 | %io:format("FlattenedList:~n ~p~n", [L]), 510 | lists:reverse(L). 511 | 512 | mkEntryName(Counter, Key) -> 513 | {"Attribute." ++ integer_to_list(Counter) ++ ".Name", Key}. 514 | mkEntryValue(Counter, Value) -> 515 | {"Attribute."++integer_to_list(Counter) ++ ".Value", Value}. 516 | 517 | flattenParams({K, V, R}, {C, L}) -> 518 | PreResult = if R -> 519 | {C, [{"Attribute." ++ integer_to_list(C) 520 | ++ ".Replace", "true"} | L]}; 521 | true -> {C, L} 522 | end, 523 | FlattenVal = fun(Val, {Counter, ResultList}) -> 524 | %io:format("~p -> ~p ~n", [K, Val]), 525 | NextCounter = Counter + 1, 526 | EntryName = mkEntryName(Counter, K), 527 | EntryValue = mkEntryValue(Counter, Val), 528 | {NextCounter, [EntryValue | [EntryName | ResultList]]} 529 | end, 530 | if length(V) > 0 -> 531 | lists:foldl(FlattenVal, PreResult, V); 532 | length(V) =:= 0 -> {C + 1, [mkEntryName(C, K) | L]} 533 | end. 534 | 535 | aggrV({K,V,true}, [{K,L,_OR}|T]) when is_list(V), 536 | is_list(hd(V)) -> 537 | [{K,V ++ L, true}|T]; 538 | aggrV({K,V,true}, [{K,L,_OR}|T]) -> [{K,[V|L], true}|T]; 539 | 540 | aggrV({K,V,false}, [{K, L, OR}|T]) when is_list(V), 541 | is_list(hd(V)) -> 542 | [{K, V ++ L, OR}|T]; 543 | aggrV({K,V,false}, [{K, L, OR}|T]) -> [{K, [V|L], OR}|T]; 544 | 545 | aggrV({K,V}, [{K,L,OR}|T]) when is_list(V), 546 | is_list(hd(V))-> 547 | [{K,V ++ L,OR}|T]; 548 | aggrV({K,V}, [{K,L,OR}|T]) -> [{K,[V|L],OR}|T]; 549 | 550 | aggrV({K,V,R}, L) when is_list(V), 551 | is_list(hd(V)) -> [{K, V, R}|L]; 552 | aggrV({K,V,R}, L) -> [{K,[V], R}|L]; 553 | 554 | aggrV({K,V}, L) when is_list(V), 555 | is_list(hd(V)) -> [{K,V,false}|L]; 556 | aggrV({K,V}, L) -> [{K,[V],false}|L]; 557 | 558 | aggrV(K, L) -> [{K, [], false}|L]. 559 | 560 | collapse(L) -> 561 | AggrL = lists:foldl( fun aggrV/2, [], lists:keysort(1, L) ), 562 | lists:keymap( fun lists:sort/1, 2, lists:reverse(AggrL)). 563 | 564 | makeParam(X) -> 565 | case X of 566 | {_, []} -> {}; 567 | {max_items, MaxItems} when is_integer(MaxItems) -> 568 | {"MaxNumberOfItems", integer_to_list(MaxItems)}; 569 | {max_domains, MaxDomains} when is_integer(MaxDomains) -> 570 | {"MaxNumberOfDomains", integer_to_list(MaxDomains)}; 571 | {next_token, NextToken} -> 572 | {"NextToken", NextToken}; 573 | _ -> {} 574 | end. 575 | 576 | 577 | aggregateAttr ({K,V}, [{K,L}|T]) -> [{K,[V|L]}|T]; 578 | aggregateAttr ({K,V}, L) -> [{K,[V]}|L]. 579 | 580 | mkErr(Xml) -> 581 | {XmlDoc, _Rest} = xmerl_scan:string( Xml ), 582 | [#xmlText{value=ErrorCode}|_] = xmerl_xpath:string("//Error/Code/text()", XmlDoc), 583 | [#xmlText{value=ErrorMessage}|_] = xmerl_xpath:string("//Error/Message/text()", XmlDoc), 584 | [#xmlText{value=RequestId}|_] = xmerl_xpath:string("//RequestID/text()", XmlDoc), 585 | {ErrorCode, ErrorMessage, RequestId}. 586 | -------------------------------------------------------------------------------- /src/erlaws_sqs.erl: -------------------------------------------------------------------------------- 1 | %%------------------------------------------------------------------- 2 | %% @author Sascha Matzke 3 | %% @doc This is an client implementation for Amazon's Simple Queue Service 4 | %% @end 5 | %%%------------------------------------------------------------------- 6 | 7 | -module(erlaws_sqs,[AWS_KEY, AWS_SEC_KEY, SECURE]). 8 | 9 | %% exports 10 | -export([list_queues/0, list_queues/1, get_queue_url/1, create_queue/1, 11 | create_queue/2, get_queue_attr/1, set_queue_attr/3, 12 | delete_queue/1, send_message/2, 13 | receive_message/1, receive_message/2, receive_message/3, 14 | delete_message/2]). 15 | 16 | %% include record definitions 17 | -include_lib("xmerl/include/xmerl.hrl"). 18 | -include("../include/erlaws.hrl"). 19 | 20 | -define(AWS_SQS_HOST, "queue.amazonaws.com"). 21 | -define(AWS_SQS_VERSION, "2008-01-01"). 22 | 23 | %% queues 24 | 25 | %% Creates a new SQS queue with the given name. 26 | %% 27 | %% SQS assigns the queue a queue URL; you must use this URL when 28 | %% performing actions on the queue (for more information, see 29 | %% http://docs.amazonwebservices.com/AWSSimpleQueueService/2007-05-01/SQSDeveloperGuide/QueueURL.html). 30 | %% 31 | %% Spec: create_queue(QueueName::string()) -> 32 | %% {ok, QueueUrl::string(), {requestId, RequestId::string()}} | 33 | %% {error, {Code::string, Msg::string(), ReqId::string()}} 34 | %% 35 | create_queue(QueueName) -> 36 | try query_request("CreateQueue", [{"QueueName", QueueName}]) of 37 | {ok, Body} -> 38 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 39 | [#xmlText{value=QueueUrl}|_] = 40 | xmerl_xpath:string("//QueueUrl/text()", XmlDoc), 41 | [#xmlText{value=RequestId}|_] = 42 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 43 | {ok, QueueUrl, {requestId, RequestId}} 44 | catch 45 | throw:{error, Descr} -> 46 | {error, Descr} 47 | end. 48 | 49 | %% Creates a new SQS queue with the given name and default VisibilityTimeout. 50 | %% 51 | %% Spec: create_queue(QueueName::string(), VisibilityTimeout::integer()) -> 52 | %% {ok, QueueUrl::string(), {requestId, ReqId::string()}} | 53 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 54 | %% 55 | create_queue(QueueName, VisibilityTimeout) when is_integer(VisibilityTimeout) -> 56 | try query_request("CreateQueue", [{"QueueName", QueueName}, 57 | {"DefaultVisibilityTimeout", integer_to_list(VisibilityTimeout)}]) of 58 | {ok, Body} -> 59 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 60 | [#xmlText{value=QueueUrl}|_] = 61 | xmerl_xpath:string("//QueueUrl/text()", XmlDoc), 62 | [#xmlText{value=RequestId}|_] = 63 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 64 | {ok, QueueUrl, {requestId, RequestId}} 65 | catch 66 | throw:{error, Descr} -> 67 | {error, Descr} 68 | end. 69 | 70 | 71 | %% Returns a list of existing queues (QueueUrls). 72 | %% 73 | %% Spec: list_queues() -> 74 | %% {ok, [QueueUrl::string(),...], {requestId, ReqId::string()}} | 75 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 76 | %% 77 | list_queues() -> 78 | try query_request("ListQueues", []) of 79 | {ok, Body} -> 80 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 81 | QueueNodes = xmerl_xpath:string("//QueueUrl/text()", XmlDoc), 82 | %% io:format("QueueNodes: ~p~n", [QueueNodes]), 83 | [#xmlText{value=RequestId}] = 84 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 85 | %% io:format("RequestId: ~p~n", [RequestId]), 86 | {ok, [QueueUrl || #xmlText{value=QueueUrl} <- QueueNodes], {requestId, RequestId}} 87 | catch 88 | throw:{error, Descr} -> 89 | {error, Descr} 90 | end. 91 | 92 | %% Returns a list of existing queues (QueueUrls) whose names start 93 | %% with the given prefix 94 | %% 95 | %% Spec: list_queues(Prefix::string()) -> 96 | %% {ok, [QueueUrl::string(),...], {requestId, ReqId::string()}} | 97 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 98 | %% 99 | list_queues(Prefix) -> 100 | try query_request("ListQueues", [{"QueueNamePrefix", Prefix}]) of 101 | {ok, Body} -> 102 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 103 | QueueNodes = xmerl_xpath:string("//QueueUrl/text()", XmlDoc), 104 | [#xmlText{value=RequestId}|_] = 105 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 106 | {ok, [Queue || #xmlText{value=Queue} <- QueueNodes], {requestId, RequestId}} 107 | catch 108 | throw:{error, Descr} -> 109 | {error, Descr} 110 | end. 111 | 112 | %% Returns the Url for a specific queue-name 113 | %% 114 | %% Spec: get_queue(QueueName::string()) -> 115 | %% {ok, QueueUrl::string(), {requestId, ReqId::string()}} | 116 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 117 | %% 118 | get_queue_url(QueueName) -> 119 | try query_request("ListQueues", [{"QueueNamePrefix", QueueName}]) of 120 | {ok, Body} -> 121 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 122 | QueueNodes = xmerl_xpath:string("//QueueUrl/text()", XmlDoc), 123 | [#xmlText{value=RequestId}|_] = 124 | xmerl_xpath:string("//ResponseMetadata/RequestId/text", XmlDoc), 125 | [QueueUrl|_] = [Queue || #xmlText{value=Queue} <- QueueNodes], 126 | {ok, QueueUrl, {requestId, RequestId}} 127 | catch 128 | throw:{error, Descr} -> 129 | {error, Descr} 130 | end. 131 | 132 | %% Returns the attributes for the given QueueUrl 133 | %% 134 | %% Spec: get_queue_attr(QueueUrl::string()) -> 135 | %% {ok, [{"VisibilityTimeout", Timeout::integer()}, 136 | %% {"ApproximateNumberOfMessages", Number::integer()}], {requestId, ReqId::string()}} | 137 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 138 | %% 139 | get_queue_attr(QueueUrl) -> 140 | try query_request(QueueUrl, "GetQueueAttributes", 141 | [{"AttributeName", "All"}]) of 142 | {ok, Body} -> 143 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 144 | AttributeNodes = xmerl_xpath:string("//Attribute", XmlDoc), 145 | AttrList = [{Key, 146 | list_to_integer(Value)} || Node <- AttributeNodes, 147 | begin 148 | [#xmlText{value=Key}|_] = 149 | xmerl_xpath:string("./Name/text()", Node), 150 | [#xmlText{value=Value}|_] = 151 | xmerl_xpath:string("./Value/text()", Node), 152 | true 153 | end], 154 | [#xmlText{value=RequestId}|_] = 155 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 156 | {ok, AttrList, {requestId, RequestId}} 157 | catch 158 | throw:{error, Descr} -> 159 | {error, Descr} 160 | end. 161 | 162 | %% This function allows you to alter the default VisibilityTimeout for 163 | %% a given QueueUrl 164 | %% 165 | %% Spec: set_queue_attr(visibility_timeout, QueueUrl::string(), 166 | %% Timeout::integer()) -> 167 | %% {ok, {requestId, ReqId::string()}} | 168 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 169 | %% 170 | set_queue_attr(visibility_timeout, QueueUrl, Timeout) 171 | when is_integer(Timeout) -> 172 | try query_request(QueueUrl, "SetQueueAttributes", 173 | [{"Attribute.Name", "VisibilityTimeout"}, 174 | {"Attribute.Value", integer_to_list(Timeout)}]) of 175 | {ok, Body} -> 176 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 177 | [#xmlText{value=RequestId}|_] = 178 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 179 | {ok, {requestId, RequestId}} 180 | catch 181 | throw:{error, Descr} -> 182 | {error, Descr} 183 | end. 184 | 185 | %% Deletes the queue identified by the given QueueUrl. 186 | %% 187 | %% Spec: delete_queue(QueueUrl::string(), Force::boolean()) -> 188 | %% {ok, {requestId, ReqId::string()}} | 189 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 190 | %% 191 | delete_queue(QueueUrl) -> 192 | try query_request(QueueUrl, "DeleteQueue", []) of 193 | {ok, Body} -> 194 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 195 | [#xmlText{value=RequestId}|_] = 196 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 197 | {ok, {requestId, RequestId}} 198 | catch 199 | throw:{error, Descr} -> 200 | {error, Descr} 201 | end. 202 | 203 | %% messages 204 | 205 | %% Sends a message to the given QueueUrl. The message must not be greater 206 | %% that 8 Kb or the call will fail. 207 | %% 208 | %% Spec: send_message(QueueUrl::string(), Message::string()) -> 209 | %% {ok, Message::#sqs_message, {requestId, ReqId::string()}} | 210 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 211 | %% 212 | send_message(QueueUrl, Message) -> 213 | try query_request(QueueUrl, "SendMessage", [{"MessageBody", Message}]) of 214 | {ok, Body} -> 215 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 216 | [#xmlText{value=MessageId}|_] = 217 | xmerl_xpath:string("//MessageId/text()", XmlDoc), 218 | [#xmlText{value=ContentMD5}|_] = 219 | xmerl_xpath:string("//MD5OfMessageBody/text()", XmlDoc), 220 | [#xmlText{value=RequestId}|_] = xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 221 | {ok, #sqs_message{messageId=MessageId, contentMD5=ContentMD5, body=Message}, {requestId, RequestId}} 222 | catch 223 | throw:{error, Descr} -> 224 | {error, Descr} 225 | end. 226 | 227 | 228 | %% Tries to receive a single message from the given queue. 229 | %% 230 | %% Spec: receive_message(QueueUrl::string()) -> 231 | %% {ok, [Message#sqs_message{}], {requestId, ReqId::string()}} | 232 | %% {ok, [], {requestId, ReqId::string()}} 233 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 234 | %% 235 | receive_message(QueueUrl) -> 236 | receive_message(QueueUrl, 1). 237 | 238 | %% Tries to receive the given number of messages (<=10) from the given queue. 239 | %% 240 | %% Spec: receive_message(QueueUrl::string(), NrOfMessages::integer()) -> 241 | %% {ok, [Message#sqs_message{}], {requestId, ReqId::string()}} | 242 | %% {ok, [], {requestId, ReqId::string()}} 243 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 244 | %% 245 | receive_message(QueueUrl, NrOfMessages) -> 246 | receive_message(QueueUrl, NrOfMessages, []). 247 | 248 | %% Tries to receive the given number of messages (<=10) from the given queue, using the given VisibilityTimeout instead 249 | %% of the queues default value. 250 | %% 251 | %% Spec: receive_message(QueueUrl::string(), NrOfMessages::integer(), VisibilityTimeout::integer()) -> 252 | %% {ok, [Message#sqs_message{}], {requestId, ReqId::string()}} | 253 | %% {ok, [], {requestId, ReqId::string()}} 254 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 255 | %% 256 | receive_message(QueueUrl, NrOfMessages, VisibilityTimeout) when is_integer(NrOfMessages) -> 257 | VisibilityTimeoutParam = case VisibilityTimeout of 258 | "" -> []; 259 | _ -> [{"VisibilityTimeout", integer_to_list(VisibilityTimeout)}] end, 260 | try query_request(QueueUrl, "ReceiveMessage", 261 | [{"MaxNumberOfMessages", integer_to_list(NrOfMessages)}] ++ VisibilityTimeoutParam) of 262 | {ok, Body} -> 263 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 264 | [#xmlText{value=RequestId}|_] = 265 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 266 | MessageNodes = xmerl_xpath:string("//Message", XmlDoc), 267 | {ok, [#sqs_message{messageId=MsgId, receiptHandle=ReceiptHandle, contentMD5=ContentMD5, body=MsgBody} || Node <- MessageNodes, 268 | begin 269 | [#xmlText{value=MsgId}|_] = 270 | xmerl_xpath:string("./MessageId/text()", Node), 271 | [#xmlText{value=MsgBody}|_] = 272 | xmerl_xpath:string("./Body/text()", Node), 273 | [#xmlText{value=ReceiptHandle}|_] = 274 | xmerl_xpath:string("./ReceiptHandle/text()", Node), 275 | [#xmlText{value=ContentMD5}|_] = 276 | xmerl_xpath:string("./MD5OfBody/text()", Node), 277 | true 278 | end], {requestId, RequestId}} 279 | catch 280 | throw:{error, Descr} -> 281 | {error, Descr} 282 | end. 283 | 284 | %% Deletes a message from a queue 285 | %% 286 | %% Spec: delete_message(QueueUrl::string(), RequestUrl::string()) -> 287 | %% {ok, {requestId, RequestId::string()}} | 288 | %% {error, {HTTPStatus::string, HTTPReason::string()}, {Code::string(), Message::string(), {requestId, ReqId::string()}}} 289 | %% 290 | delete_message(QueueUrl, ReceiptHandle) -> 291 | try query_request(QueueUrl, "DeleteMessage", 292 | [{"ReceiptHandle", ReceiptHandle}]) of 293 | {ok, Body} -> 294 | {XmlDoc, _Rest} = xmerl_scan:string(Body), 295 | [#xmlText{value=RequestId}|_] = 296 | xmerl_xpath:string("//ResponseMetadata/RequestId/text()", XmlDoc), 297 | {ok, {requestId, RequestId}} 298 | catch 299 | throw:{error, Descr} -> 300 | {error, Descr} 301 | end. 302 | 303 | %% internal methods 304 | 305 | sign (Key,Data) -> 306 | %%%% io:format("Sign:~n ~p~n", [Data]), 307 | binary_to_list( base64:encode( crypto:sha_mac(Key,Data) ) ). 308 | 309 | query_request(Action, Parameters) -> 310 | query_request(case SECURE of 311 | true -> "https://"; 312 | _ -> "http://" end ++ ?AWS_SQS_HOST ++ "/", Action, Parameters). 313 | 314 | query_request(Url, Action, Parameters) -> 315 | %% io:format("query_request: ~p ~p ~p~n", [Url, Action, Parameters]), 316 | Timestamp = lists:flatten(erlaws_util:get_timestamp()), 317 | SignParams = [{"Action", Action}, {"AWSAccessKeyId", AWS_KEY}, {"Timestamp", Timestamp}] ++ 318 | Parameters ++ [{"SignatureVersion", "1"}, {"Version", ?AWS_SQS_VERSION}], 319 | StringToSign = erlaws_util:mkEnumeration([Param++Value || {Param, Value} <- lists:sort(fun (A, B) -> 320 | {KeyA, _} = A, 321 | {KeyB, _} = B, 322 | string:to_lower(KeyA) =< string:to_lower(KeyB) end, 323 | SignParams)], ""), 324 | %% io:format("StringToSign: ~p~n", [StringToSign]), 325 | Signature = sign(AWS_SEC_KEY, StringToSign), 326 | %% io:format("Signature: ~p~n", [Signature]), 327 | FinalQueryParams = SignParams ++ 328 | [{"Signature", Signature}], 329 | Result = mkReq(get, Url, [], FinalQueryParams, "", ""), 330 | case Result of 331 | {ok, _Status, Body} -> 332 | {ok, Body}; 333 | {error, {_Proto, Code, Reason}, Body} -> 334 | throw({error, {integer_to_list(Code), Reason}, mkErr(Body)}) 335 | end. 336 | 337 | mkReq(Method, PreUrl, Headers, QueryParams, ContentType, ReqBody) -> 338 | %%%% io:format("QueryParams:~n ~p~nHeaders:~n ~p~nUrl:~n ~p~n", 339 | %% [QueryParams, Headers, PreUrl]), 340 | Url = PreUrl ++ erlaws_util:queryParams( QueryParams ), 341 | %% io:format("RequestUrl:~n ~p~n", [Url]), 342 | Request = case Method of 343 | get -> { Url, Headers }; 344 | put -> { Url, Headers, ContentType, ReqBody } 345 | end, 346 | 347 | HttpOptions = [{autoredirect, true}], 348 | Options = [ {sync,true}, {headers_as_is,true}, {body_format, binary} ], 349 | {ok, {Status, _ReplyHeaders, Body}} = 350 | http:request(Method, Request, HttpOptions, Options), 351 | %% io:format("Response:~n ~p~n", [binary_to_list(Body)]), 352 | %% io:format("Status: ~p~n", [Status]), 353 | case Status of 354 | {_, 200, _} -> {ok, Status, binary_to_list(Body)}; 355 | {_, _, _} -> {error, Status, binary_to_list(Body)} 356 | end. 357 | 358 | mkErr(Xml) -> 359 | {XmlDoc, _Rest} = xmerl_scan:string( Xml ), 360 | [#xmlText{value=ErrorCode}|_] = xmerl_xpath:string("//Error/Code/text()", 361 | XmlDoc), 362 | ErrorMessage = 363 | case xmerl_xpath:string("//Error/Message/text()", XmlDoc) of 364 | [] -> ""; 365 | [EMsg|_] -> EMsg#xmlText.value 366 | end, 367 | [#xmlText{value=RequestId}|_] = xmerl_xpath:string("//RequestID/text()", 368 | XmlDoc), 369 | {ErrorCode, ErrorMessage, {requestId, RequestId}}. 370 | -------------------------------------------------------------------------------- /src/erlaws_util.erl: -------------------------------------------------------------------------------- 1 | -module(erlaws_util). 2 | 3 | -export([url_encode/1, mkEnumeration/2, queryParams/1, get_timestamp/0]). 4 | 5 | get_timestamp() -> 6 | iso_8601_fmt(erlang:universaltime(), "Z"). 7 | 8 | iso_8601_fmt(DateTime, Zone) -> 9 | {{Year,Month,Day},{Hour,Min,Sec}} = DateTime, 10 | io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B~s", 11 | [Year, Month, Day, Hour, Min, Sec, Zone]). 12 | 13 | mkEnumeration(Values, Separator) -> 14 | lists:flatten(lists:reverse(mkEnumeration(Values, Separator, []))). 15 | 16 | mkEnumeration([], _Separator, Acc) -> 17 | Acc; 18 | mkEnumeration([Head|[]], _Separator, Acc) -> 19 | [Head | Acc]; 20 | mkEnumeration([Head|Tail], Separator, Acc) -> 21 | mkEnumeration(Tail, Separator, [Separator, Head | Acc]). 22 | 23 | queryParams( [] ) -> ""; 24 | queryParams( ParamList ) -> 25 | "?" ++ mkEnumeration([url_encode(Param) ++ "=" ++ url_encode(Value) 26 | || {Param, Value} <- ParamList], "&" ). 27 | 28 | %% The following code is taken from the ibrowse Http client 29 | %% library. 30 | %% 31 | %% Original license: 32 | %% 33 | %% Copyright (c) 2006, Chandrashekhar Mullaparthi 34 | %% 35 | %% Redistribution and use in source and binary forms, with or without 36 | %% modification, are permitted provided that the following conditions are met: 37 | %% 38 | %% * Redistributions of source code must retain the above copyright notice, 39 | %% this list of conditions and the following disclaimer. 40 | %% * Redistributions in binary form must reproduce the above copyright 41 | %% notice, this list of conditions and the following disclaimer in the 42 | %% documentation and/or other materials provided with the distribution. 43 | %% * Neither the name of the T-Mobile nor the names of its contributors 44 | %% may be used to endorse or promote products derived from this software 45 | %% without specific prior written permission. 46 | %% 47 | %% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 48 | %% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 49 | %% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 50 | %% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 51 | %% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 52 | %% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 53 | %% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 54 | %% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 55 | %% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 56 | %% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 57 | %% THE POSSIBILITY OF SUCH DAMAGE. 58 | 59 | 60 | url_encode(Str) when list(Str) -> 61 | url_encode_char(lists:reverse(Str), []). 62 | 63 | url_encode_char([X | T], Acc) when X >= $0, X =< $9 -> 64 | url_encode_char(T, [X | Acc]); 65 | url_encode_char([X | T], Acc) when X >= $a, X =< $z -> 66 | url_encode_char(T, [X | Acc]); 67 | url_encode_char([X | T], Acc) when X >= $A, X =< $Z -> 68 | url_encode_char(T, [X | Acc]); 69 | url_encode_char([X | T], Acc) when X == $-; X == $_; X == $. -> 70 | url_encode_char(T, [X | Acc]); 71 | url_encode_char([32 | T], Acc) -> 72 | url_encode_char(T, [$+ | Acc]); 73 | url_encode_char([X | T], Acc) -> 74 | url_encode_char(T, [$%, d2h(X bsr 4), d2h(X band 16#0f) | Acc]); 75 | url_encode_char([], Acc) -> 76 | Acc. 77 | 78 | d2h(N) when N<10 -> N+$0; 79 | d2h(N) -> N+$a-10. 80 | --------------------------------------------------------------------------------