├── .gitignore ├── LICENSE ├── README.md ├── cferl.png ├── cferl.xcf ├── doc └── overview.edoc ├── include └── cferl.hrl ├── int_tests ├── lib ├── mochijson2.erl └── mochinum.erl ├── rebar.config ├── src ├── cferl.app.src ├── cferl.erl ├── cferl_connection.erl ├── cferl_container.erl ├── cferl_lib.erl └── cferl_object.erl └── test ├── cferl_integration_tests.erl └── elog.config /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.o 3 | *.so 4 | *.a 5 | .eunit 6 | erl_crash.dump 7 | deps 8 | ebin 9 | doc 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is the MIT license. 2 | 3 | Copyright (c) 2010-2013 David Dossot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rackspace Cloud Files Erlang Client 2 | 3 | ## Description 4 | 5 | This is an Erlang interface into the Rackspace Cloud Files service. It has been largely inspired by the existing [Ruby](http://github.com/rackspace/ruby-cloudfiles) API. 6 | 7 | 8 | ## Building 9 | 10 | **cferl** relies on [rebar](http://bitbucket.org/basho/rebar/wiki/Home) for its build and dependency management and targets Erlang/OTP R13B04 or above. 11 | 12 | Simply run: 13 | 14 | rebar get-deps compile eunit 15 | 16 | Optionally, to generate the *cferl* documentation, run: 17 | 18 | rebar skip_deps=true doc 19 | 20 | Optionally, to run the integration tests (and generate the code samples visible below), run: 21 | 22 | ./int_tests 23 | 24 | If you run the integration tests, you'll need your API key and at least one pre-existing container. Note that a test container will be created and some queries could take a while if you have lots of containers. 25 | 26 | 27 | ## Using 28 | 29 | **cferl** requires that the ssl and ibrowse applications be started prior to using it. 30 | 31 | The following, which is output when running the integration tests, demonstrates a typical usage of the API. Refer to the documentation for the complete reference. 32 | 33 | ```erlang 34 | # Connect to Cloud Files (warning: cache/use CloudFiles for a maximum of 24 hours!) 35 | {ok,CloudFiles}=cferl:connect(Username,ApiKey). 36 | 37 | # Retrieve the account information record 38 | {ok,Info}=cferl_connection:get_account_info(CloudFiles). 39 | Info = #cf_account_info{bytes_used=1735871382, container_count=5} 40 | 41 | # Retrieve names of all existing containers (within the limits imposed by Cloud Files server) 42 | {ok,Names}=cferl_connection:get_containers_names(CloudFiles). 43 | 44 | # Retrieve names of a maximum of 3 existing containers 45 | {ok,ThreeNamesMax}=cferl_connection:get_containers_names(CloudFiles,#cf_container_query_args{limit=3}). 46 | 47 | # Retrieve names of all containers currently CDN activated 48 | {ok,CurrentPublicNames}=cferl_connection:get_public_containers_names(CloudFiles,active). 49 | 50 | # Retrieve names of all containers that are currently or have been CDN activated 51 | {ok,AllTimePublicNames}=cferl_connection:get_public_containers_names(CloudFiles,all_time). 52 | 53 | # Retrieve details for all existing containers (within the server limits) 54 | {ok,ContainersDetails}=cferl_connection:get_containers_details(CloudFiles). 55 | 56 | # ContainersDetails is a list of #cf_container_details records 57 | [ContainerDetails|_]=ContainersDetails. 58 | ContainerDetails = #cf_container_details{name=<<".CDN_ACCESS_LOGS">>, bytes=261, count=1} 59 | 60 | # Retrieve details for a maximum of 5 containers whose names start at cf 61 | {ok,CfContainersDetails}=cferl_connection:get_containers_details(CloudFiles,#cf_container_query_args{marker=<<"cf">>,limit=5}). 62 | 63 | # Get a container reference by name 64 | {ok,Container}=cferl_connection:get_container(CloudFiles,ContainerDetails#cf_container_details.name). 65 | 66 | # Get container details from its reference 67 | ContainerName=cferl_container:name(Container). 68 | ContainerBytes=cferl_container:bytes(Container). 69 | ContainerSize=cferl_container:count(Container). 70 | ContainerIsEmpty=cferl_container:is_empty(Container). 71 | 72 | # -> Name: <<".CDN_ACCESS_LOGS">> - Bytes: 261 - Size: 1 - IsEmpty: false 73 | 74 | # Check a container's existence 75 | false=cferl_connection:container_exists(CloudFiles,NewContainerName). 76 | 77 | # Create a new container 78 | {ok,NewContainer}=cferl_connection:create_container(CloudFiles,NewContainerName). 79 | 80 | true=cferl_connection:container_exists(CloudFiles,NewContainerName). 81 | 82 | Check attributes of this newly created container 83 | NewContainerName=cferl_container:name(NewContainer). 84 | 0=cferl_container:bytes(NewContainer). 85 | 0=cferl_container:count(NewContainer). 86 | true=cferl_container:is_empty(NewContainer). 87 | false=cferl_container:is_public(NewContainer). 88 | <<>>=cferl_container:cdn_url(NewContainer). 89 | 0=cferl_container:cdn_ttl(NewContainer). 90 | false=cferl_container:log_retention(NewContainer). 91 | 92 | # Make the container public on the CDN (using the default TTL and ACLs) 93 | ok=cferl_container:make_public(CloudFiles,NewContainer). 94 | 95 | # Activate log retention on the new container 96 | ok=cferl_container:set_log_retention(CloudFiles,NewContainer,true). 97 | 98 | # Refresh an existing container and check its attributes 99 | {ok,RefreshedContainer}=cferl_container:refresh(CloudFiles,NewContainer). 100 | true=cferl_container:is_public(RefreshedContainer). 101 | 102 | io:format("~s~n~n",[cferl_container:cdn_url(RefreshedContainer)]). 103 | http://05f98f987aa9393ccd8c-3d04f8822c5760cb271501bb0c358085.r17.cf1.rackcdn.com 104 | 105 | 86400=cferl_container:cdn_ttl(RefreshedContainer). 106 | true=cferl_container:log_retention(RefreshedContainer). 107 | 108 | ObjectName=<<"test.xml">>. 109 | # Create an object *reference*, nothing is sent to the server yet 110 | {ok,Object}=cferl_container:create_object(CloudFiles,RefreshedContainer,ObjectName). 111 | # As expected, it doesn't exist yet 112 | false=cferl_container:object_exists(CloudFiles,RefreshedContainer,ObjectName). 113 | 114 | # Write data in the object, which creates it on the server 115 | ok=cferl_object:write_data(CloudFiles,Object,<<"">>,<<"application/xml">>). 116 | # Now it exists! 117 | true=cferl_container:object_exists(CloudFiles,RefreshedContainer,ObjectName). 118 | # And trying to re-create it just returns it 119 | {ok,ExistingObject}=cferl_container:create_object(CloudFiles,RefreshedContainer,ObjectName). 120 | 121 | # Set custom meta-data on it 122 | ok=cferl_object:set_metadata(CloudFiles,Object,[{<<"Key123">>,<<"my123Value">>}]). 123 | 124 | # An existing object can be accessed directly from its container 125 | {ok,GotObject}=cferl_container:get_object(CloudFiles,RefreshedContainer,ObjectName). 126 | 127 | # Object names and details can be queried 128 | {ok,[ObjectName]}=cferl_container:get_objects_names(CloudFiles,RefreshedContainer). 129 | {ok,[ObjectName]}=cferl_container:get_objects_names(CloudFiles,RefreshedContainer,#cf_object_query_args{limit=1}). 130 | {ok,[ObjectDetails]}=cferl_container:get_objects_details(CloudFiles,RefreshedContainer). 131 | ObjectDetails = #cf_object_details{name=<<"test.xml">>, bytes=8, last_modified={{2013,3,16},{0,8,47}}, content_type=application/xml, etag=4366c359d1a7b9b248fa262775613699} 132 | 133 | # Read the whole data 134 | {ok,<<"">>}=cferl_object:read_data(CloudFiles,Object). 135 | # Read the data with an offset and a size 136 | {ok,<<"test">>}=cferl_object:read_data(CloudFiles,Object,1,4). 137 | 138 | # Refresh the object so its attributes and metadata are up to date 139 | {ok,RefreshedObject}=cferl_object:refresh(CloudFiles,Object). 140 | 141 | # Get object attributes 142 | ObjectName=cferl_object:name(RefreshedObject). 143 | 8=cferl_object:bytes(RefreshedObject). 144 | {{D,M,Y},{H,Mi,S}}=cferl_object:last_modified(RefreshedObject). 145 | <<"application/xml">>=cferl_object:content_type(RefreshedObject). 146 | Etag=cferl_object:etag(RefreshedObject). 147 | 148 | # Get custom meta-data 149 | [{<<"Key123">>,<<"my123Value">>}]=cferl_object:metadata(RefreshedObject). 150 | 151 | # Delete the object 152 | ok=cferl_object:delete(CloudFiles,RefreshedObject). 153 | 154 | # Data can be streamed to the server from a generating function 155 | {ok,StreamedObject}=cferl_container:create_object(CloudFiles,RefreshedContainer,<<"streamed.txt">>). 156 | cferl_object:write_data_stream(CloudFiles,StreamedObject,WriteDataFun,<<"text/plain">>,1000). 157 | 158 | # Data can be streamed from the server to a receiving function 159 | ok=cferl_object:read_data_stream(CloudFiles,StreamedObject,ReadDataFun). 160 | 161 | # Create all the directory elements for a particular object path 162 | ok=cferl_container:ensure_dir(CloudFiles,RefreshedContainer,<<"photos/plants/fern.jpg">>). 163 | true=cferl_container:object_exists(CloudFiles,RefreshedContainer,<<"photos">>). 164 | true=cferl_container:object_exists(CloudFiles,RefreshedContainer,<<"photos/plants">>). 165 | 166 | # Make the container private 167 | ok=cferl_container:make_private(CloudFiles,RefreshedContainer). 168 | 169 | # Delete an existing container (must be empty) 170 | ok=cferl_container:delete(CloudFiles,RefreshedContainer). 171 | ``` 172 | 173 | ## More information 174 | 175 | Read the Rackspace Cloud Files API specification: 176 | 177 | Contact the author: 178 | 179 | ### Copyright (c) 2010-2013 David Dossot - MIT License 180 | -------------------------------------------------------------------------------- /cferl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddossot/cferl/8bafe3d2a19212bda13bedd5dba4f5d7a7aa6812/cferl.png -------------------------------------------------------------------------------- /cferl.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddossot/cferl/8bafe3d2a19212bda13bedd5dba4f5d7a7aa6812/cferl.xcf -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | @title cferl 2 | @doc


3 |

Rackspace Cloud Files Erlang Client

4 |

Read the project's README file for more information.

5 | @author David Dossot 6 | @copyright 2010 David Dossot 7 | -------------------------------------------------------------------------------- /include/cferl.hrl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Rackspace Cloud Files Erlang Client 3 | %%% @author David Dossot 4 | %%% @author Tilman Holschuh 5 | %%% 6 | %%% See LICENSE for license information. 7 | %%% Copyright (c) 2010 David Dossot 8 | %%% 9 | 10 | -define(US_API_BASE_URL, "identity.api.rackspacecloud.com"). 11 | -define(UK_API_BASE_URL, "lon.identity.api.rackspacecloud.com"). 12 | -define(VERSION_PATH, "/v1.0"). 13 | 14 | -define(DEFAULT_REQUEST_TIMEOUT, 30000). 15 | -define(OBJECT_META_HEADER_PREFIX, "X-Object-Meta-"). 16 | -define(DIRECTORY_OBJECT_CONTENT_TYPE, <<"application/directory">>). 17 | 18 | -define(IS_CONNECTION(C), is_record(C, cf_connection)). 19 | -define(IS_CONTAINER(C), is_record(C, cf_container)). 20 | -define(IS_OBJECT(O), is_record(O, cf_object)). 21 | 22 | -record(cf_connection, {version :: string(), 23 | auth_token :: string(), 24 | storage_url :: string(), 25 | cdn_management_url :: string() 26 | }). 27 | 28 | -record(cf_account_info, {bytes_used, container_count}). 29 | 30 | -record(cf_container_details, {name, bytes, count}). 31 | -record(cf_container, {container_details :: #cf_container_details{}, 32 | container_path :: string(), 33 | cdn_details :: [{atom(), term()}] 34 | }). 35 | 36 | -record(cf_container_query_args, {marker, limit}). 37 | -record(cf_container_cdn_config, {ttl = 86400, user_agent_acl, referrer_acl}). 38 | 39 | -record(cf_object_details, {name, bytes = 0, last_modified, content_type, etag}). 40 | -record(cf_object, {container :: #cf_container{}, 41 | object_details :: #cf_object_details{}, 42 | object_path :: string(), 43 | http_headers :: [{string(), string()}] 44 | }). 45 | 46 | -record(cf_object_query_args, {marker, limit, prefix, path}). 47 | 48 | -------------------------------------------------------------------------------- /int_tests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rebar get-deps clean compile eunit 3 | erl -boot start_sasl -config test/elog -pa .eunit -pa ebin -pa deps/ibrowse/ebin -s cferl_integration_tests -noshell 4 | 5 | -------------------------------------------------------------------------------- /lib/mochijson2.erl: -------------------------------------------------------------------------------- 1 | %% @author Bob Ippolito 2 | %% @copyright 2007 Mochi Media, Inc. 3 | 4 | %% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works 5 | %% with binaries as strings, arrays as lists (without an {array, _}) 6 | %% wrapper and it only knows how to decode UTF-8 (and ASCII). 7 | 8 | -module(mochijson2). 9 | -author('bob@mochimedia.com'). 10 | -export([encoder/1, encode/1]). 11 | -export([decoder/1, decode/1]). 12 | 13 | % This is a macro to placate syntax highlighters.. 14 | -define(Q, $\"). 15 | -define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset, 16 | column=N+S#decoder.column}). 17 | -define(INC_COL(S), S#decoder{offset=1+S#decoder.offset, 18 | column=1+S#decoder.column}). 19 | -define(INC_LINE(S), S#decoder{offset=1+S#decoder.offset, 20 | column=1, 21 | line=1+S#decoder.line}). 22 | -define(INC_CHAR(S, C), 23 | case C of 24 | $\n -> 25 | S#decoder{column=1, 26 | line=1+S#decoder.line, 27 | offset=1+S#decoder.offset}; 28 | _ -> 29 | S#decoder{column=1+S#decoder.column, 30 | offset=1+S#decoder.offset} 31 | end). 32 | -define(IS_WHITESPACE(C), 33 | (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). 34 | 35 | %% @type iolist() = [char() | binary() | iolist()] 36 | %% @type iodata() = iolist() | binary() 37 | %% @type json_string() = atom | binary() 38 | %% @type json_number() = integer() | float() 39 | %% @type json_array() = [json_term()] 40 | %% @type json_object() = {struct, [{json_string(), json_term()}]} 41 | %% @type json_iolist() = {json, iolist()} 42 | %% @type json_term() = json_string() | json_number() | json_array() | 43 | %% json_object() | json_iolist() 44 | 45 | -record(encoder, {handler=null, 46 | utf8=false}). 47 | 48 | -record(decoder, {object_hook=null, 49 | offset=0, 50 | line=1, 51 | column=1, 52 | state=null}). 53 | 54 | %% @spec encoder([encoder_option()]) -> function() 55 | %% @doc Create an encoder/1 with the given options. 56 | %% @type encoder_option() = handler_option() | utf8_option() 57 | %% @type utf8_option() = boolean(). Emit unicode as utf8 (default - false) 58 | encoder(Options) -> 59 | State = parse_encoder_options(Options, #encoder{}), 60 | fun (O) -> json_encode(O, State) end. 61 | 62 | %% @spec encode(json_term()) -> iolist() 63 | %% @doc Encode the given as JSON to an iolist. 64 | encode(Any) -> 65 | json_encode(Any, #encoder{}). 66 | 67 | %% @spec decoder([decoder_option()]) -> function() 68 | %% @doc Create a decoder/1 with the given options. 69 | decoder(Options) -> 70 | State = parse_decoder_options(Options, #decoder{}), 71 | fun (O) -> json_decode(O, State) end. 72 | 73 | %% @spec decode(iolist()) -> json_term() 74 | %% @doc Decode the given iolist to Erlang terms. 75 | decode(S) -> 76 | json_decode(S, #decoder{}). 77 | 78 | %% Internal API 79 | 80 | parse_encoder_options([], State) -> 81 | State; 82 | parse_encoder_options([{handler, Handler} | Rest], State) -> 83 | parse_encoder_options(Rest, State#encoder{handler=Handler}); 84 | parse_encoder_options([{utf8, Switch} | Rest], State) -> 85 | parse_encoder_options(Rest, State#encoder{utf8=Switch}). 86 | 87 | parse_decoder_options([], State) -> 88 | State; 89 | parse_decoder_options([{object_hook, Hook} | Rest], State) -> 90 | parse_decoder_options(Rest, State#decoder{object_hook=Hook}). 91 | 92 | json_encode(true, _State) -> 93 | <<"true">>; 94 | json_encode(false, _State) -> 95 | <<"false">>; 96 | json_encode(null, _State) -> 97 | <<"null">>; 98 | json_encode(I, _State) when is_integer(I) andalso I >= -2147483648 andalso I =< 2147483647 -> 99 | %% Anything outside of 32-bit integers should be encoded as a float 100 | integer_to_list(I); 101 | json_encode(I, _State) when is_integer(I) -> 102 | mochinum:digits(float(I)); 103 | json_encode(F, _State) when is_float(F) -> 104 | mochinum:digits(F); 105 | json_encode(S, State) when is_binary(S); is_atom(S) -> 106 | json_encode_string(S, State); 107 | json_encode(Array, State) when is_list(Array) -> 108 | json_encode_array(Array, State); 109 | json_encode({struct, Props}, State) when is_list(Props) -> 110 | json_encode_proplist(Props, State); 111 | json_encode({json, IoList}, _State) -> 112 | IoList; 113 | json_encode(Bad, #encoder{handler=null}) -> 114 | exit({json_encode, {bad_term, Bad}}); 115 | json_encode(Bad, State=#encoder{handler=Handler}) -> 116 | json_encode(Handler(Bad), State). 117 | 118 | json_encode_array([], _State) -> 119 | <<"[]">>; 120 | json_encode_array(L, State) -> 121 | F = fun (O, Acc) -> 122 | [$,, json_encode(O, State) | Acc] 123 | end, 124 | [$, | Acc1] = lists:foldl(F, "[", L), 125 | lists:reverse([$\] | Acc1]). 126 | 127 | json_encode_proplist([], _State) -> 128 | <<"{}">>; 129 | json_encode_proplist(Props, State) -> 130 | F = fun ({K, V}, Acc) -> 131 | KS = json_encode_string(K, State), 132 | VS = json_encode(V, State), 133 | [$,, VS, $:, KS | Acc] 134 | end, 135 | [$, | Acc1] = lists:foldl(F, "{", Props), 136 | lists:reverse([$\} | Acc1]). 137 | 138 | json_encode_string(A, State) when is_atom(A) -> 139 | L = atom_to_list(A), 140 | case json_string_is_safe(L) of 141 | true -> 142 | [?Q, L, ?Q]; 143 | false -> 144 | json_encode_string_unicode(xmerl_ucs:from_utf8(L), State, [?Q]) 145 | end; 146 | json_encode_string(B, State) when is_binary(B) -> 147 | case json_bin_is_safe(B) of 148 | true -> 149 | [?Q, B, ?Q]; 150 | false -> 151 | json_encode_string_unicode(xmerl_ucs:from_utf8(B), State, [?Q]) 152 | end; 153 | json_encode_string(I, _State) when is_integer(I) -> 154 | [?Q, integer_to_list(I), ?Q]; 155 | json_encode_string(L, State) when is_list(L) -> 156 | case json_string_is_safe(L) of 157 | true -> 158 | [?Q, L, ?Q]; 159 | false -> 160 | json_encode_string_unicode(L, State, [?Q]) 161 | end. 162 | 163 | json_string_is_safe([]) -> 164 | true; 165 | json_string_is_safe([C | Rest]) -> 166 | case C of 167 | ?Q -> 168 | false; 169 | $\\ -> 170 | false; 171 | $\b -> 172 | false; 173 | $\f -> 174 | false; 175 | $\n -> 176 | false; 177 | $\r -> 178 | false; 179 | $\t -> 180 | false; 181 | C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> 182 | false; 183 | C when C < 16#7f -> 184 | json_string_is_safe(Rest); 185 | _ -> 186 | false 187 | end. 188 | 189 | json_bin_is_safe(<<>>) -> 190 | true; 191 | json_bin_is_safe(<>) -> 192 | case C of 193 | ?Q -> 194 | false; 195 | $\\ -> 196 | false; 197 | $\b -> 198 | false; 199 | $\f -> 200 | false; 201 | $\n -> 202 | false; 203 | $\r -> 204 | false; 205 | $\t -> 206 | false; 207 | C when C >= 0, C < $\s; C >= 16#7f -> 208 | false; 209 | C when C < 16#7f -> 210 | json_bin_is_safe(Rest) 211 | end. 212 | 213 | json_encode_string_unicode([], _State, Acc) -> 214 | lists:reverse([$\" | Acc]); 215 | json_encode_string_unicode([C | Cs], State, Acc) -> 216 | Acc1 = case C of 217 | ?Q -> 218 | [?Q, $\\ | Acc]; 219 | %% Escaping solidus is only useful when trying to protect 220 | %% against "" injection attacks which are only 221 | %% possible when JSON is inserted into a HTML document 222 | %% in-line. mochijson2 does not protect you from this, so 223 | %% if you do insert directly into HTML then you need to 224 | %% uncomment the following case or escape the output of encode. 225 | %% 226 | %% $/ -> 227 | %% [$/, $\\ | Acc]; 228 | %% 229 | $\\ -> 230 | [$\\, $\\ | Acc]; 231 | $\b -> 232 | [$b, $\\ | Acc]; 233 | $\f -> 234 | [$f, $\\ | Acc]; 235 | $\n -> 236 | [$n, $\\ | Acc]; 237 | $\r -> 238 | [$r, $\\ | Acc]; 239 | $\t -> 240 | [$t, $\\ | Acc]; 241 | C when C >= 0, C < $\s -> 242 | [unihex(C) | Acc]; 243 | C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 -> 244 | [xmerl_ucs:to_utf8(C) | Acc]; 245 | C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 -> 246 | [unihex(C) | Acc]; 247 | C when C < 16#7f -> 248 | [C | Acc]; 249 | _ -> 250 | exit({json_encode, {bad_char, C}}) 251 | end, 252 | json_encode_string_unicode(Cs, State, Acc1). 253 | 254 | hexdigit(C) when C >= 0, C =< 9 -> 255 | C + $0; 256 | hexdigit(C) when C =< 15 -> 257 | C + $a - 10. 258 | 259 | unihex(C) when C < 16#10000 -> 260 | <> = <>, 261 | Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]], 262 | [$\\, $u | Digits]; 263 | unihex(C) when C =< 16#10FFFF -> 264 | N = C - 16#10000, 265 | S1 = 16#d800 bor ((N bsr 10) band 16#3ff), 266 | S2 = 16#dc00 bor (N band 16#3ff), 267 | [unihex(S1), unihex(S2)]. 268 | 269 | json_decode(L, S) when is_list(L) -> 270 | json_decode(iolist_to_binary(L), S); 271 | json_decode(B, S) -> 272 | {Res, S1} = decode1(B, S), 273 | {eof, _} = tokenize(B, S1#decoder{state=trim}), 274 | Res. 275 | 276 | decode1(B, S=#decoder{state=null}) -> 277 | case tokenize(B, S#decoder{state=any}) of 278 | {{const, C}, S1} -> 279 | {C, S1}; 280 | {start_array, S1} -> 281 | decode_array(B, S1); 282 | {start_object, S1} -> 283 | decode_object(B, S1) 284 | end. 285 | 286 | make_object(V, #decoder{object_hook=null}) -> 287 | V; 288 | make_object(V, #decoder{object_hook=Hook}) -> 289 | Hook(V). 290 | 291 | decode_object(B, S) -> 292 | decode_object(B, S#decoder{state=key}, []). 293 | 294 | decode_object(B, S=#decoder{state=key}, Acc) -> 295 | case tokenize(B, S) of 296 | {end_object, S1} -> 297 | V = make_object({struct, lists:reverse(Acc)}, S1), 298 | {V, S1#decoder{state=null}}; 299 | {{const, K}, S1} -> 300 | {colon, S2} = tokenize(B, S1), 301 | {V, S3} = decode1(B, S2#decoder{state=null}), 302 | decode_object(B, S3#decoder{state=comma}, [{K, V} | Acc]) 303 | end; 304 | decode_object(B, S=#decoder{state=comma}, Acc) -> 305 | case tokenize(B, S) of 306 | {end_object, S1} -> 307 | V = make_object({struct, lists:reverse(Acc)}, S1), 308 | {V, S1#decoder{state=null}}; 309 | {comma, S1} -> 310 | decode_object(B, S1#decoder{state=key}, Acc) 311 | end. 312 | 313 | decode_array(B, S) -> 314 | decode_array(B, S#decoder{state=any}, []). 315 | 316 | decode_array(B, S=#decoder{state=any}, Acc) -> 317 | case tokenize(B, S) of 318 | {end_array, S1} -> 319 | {lists:reverse(Acc), S1#decoder{state=null}}; 320 | {start_array, S1} -> 321 | {Array, S2} = decode_array(B, S1), 322 | decode_array(B, S2#decoder{state=comma}, [Array | Acc]); 323 | {start_object, S1} -> 324 | {Array, S2} = decode_object(B, S1), 325 | decode_array(B, S2#decoder{state=comma}, [Array | Acc]); 326 | {{const, Const}, S1} -> 327 | decode_array(B, S1#decoder{state=comma}, [Const | Acc]) 328 | end; 329 | decode_array(B, S=#decoder{state=comma}, Acc) -> 330 | case tokenize(B, S) of 331 | {end_array, S1} -> 332 | {lists:reverse(Acc), S1#decoder{state=null}}; 333 | {comma, S1} -> 334 | decode_array(B, S1#decoder{state=any}, Acc) 335 | end. 336 | 337 | tokenize_string(B, S=#decoder{offset=O}) -> 338 | case tokenize_string_fast(B, O) of 339 | {escape, O1} -> 340 | Length = O1 - O, 341 | S1 = ?ADV_COL(S, Length), 342 | <<_:O/binary, Head:Length/binary, _/binary>> = B, 343 | tokenize_string(B, S1, lists:reverse(binary_to_list(Head))); 344 | O1 -> 345 | Length = O1 - O, 346 | <<_:O/binary, String:Length/binary, ?Q, _/binary>> = B, 347 | {{const, String}, ?ADV_COL(S, Length + 1)} 348 | end. 349 | 350 | tokenize_string_fast(B, O) -> 351 | case B of 352 | <<_:O/binary, ?Q, _/binary>> -> 353 | O; 354 | <<_:O/binary, $\\, _/binary>> -> 355 | {escape, O}; 356 | <<_:O/binary, C1, _/binary>> when C1 < 128 -> 357 | tokenize_string_fast(B, 1 + O); 358 | <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, 359 | C2 >= 128, C2 =< 191 -> 360 | tokenize_string_fast(B, 2 + O); 361 | <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, 362 | C2 >= 128, C2 =< 191, 363 | C3 >= 128, C3 =< 191 -> 364 | tokenize_string_fast(B, 3 + O); 365 | <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244, 366 | C2 >= 128, C2 =< 191, 367 | C3 >= 128, C3 =< 191, 368 | C4 >= 128, C4 =< 191 -> 369 | tokenize_string_fast(B, 4 + O); 370 | _ -> 371 | throw(invalid_utf8) 372 | end. 373 | 374 | tokenize_string(B, S=#decoder{offset=O}, Acc) -> 375 | case B of 376 | <<_:O/binary, ?Q, _/binary>> -> 377 | {{const, iolist_to_binary(lists:reverse(Acc))}, ?INC_COL(S)}; 378 | <<_:O/binary, "\\\"", _/binary>> -> 379 | tokenize_string(B, ?ADV_COL(S, 2), [$\" | Acc]); 380 | <<_:O/binary, "\\\\", _/binary>> -> 381 | tokenize_string(B, ?ADV_COL(S, 2), [$\\ | Acc]); 382 | <<_:O/binary, "\\/", _/binary>> -> 383 | tokenize_string(B, ?ADV_COL(S, 2), [$/ | Acc]); 384 | <<_:O/binary, "\\b", _/binary>> -> 385 | tokenize_string(B, ?ADV_COL(S, 2), [$\b | Acc]); 386 | <<_:O/binary, "\\f", _/binary>> -> 387 | tokenize_string(B, ?ADV_COL(S, 2), [$\f | Acc]); 388 | <<_:O/binary, "\\n", _/binary>> -> 389 | tokenize_string(B, ?ADV_COL(S, 2), [$\n | Acc]); 390 | <<_:O/binary, "\\r", _/binary>> -> 391 | tokenize_string(B, ?ADV_COL(S, 2), [$\r | Acc]); 392 | <<_:O/binary, "\\t", _/binary>> -> 393 | tokenize_string(B, ?ADV_COL(S, 2), [$\t | Acc]); 394 | <<_:O/binary, "\\u", C3, C2, C1, C0, Rest/binary>> -> 395 | C = erlang:list_to_integer([C3, C2, C1, C0], 16), 396 | if C > 16#D7FF, C < 16#DC00 -> 397 | %% coalesce UTF-16 surrogate pair 398 | <<"\\u", D3, D2, D1, D0, _/binary>> = Rest, 399 | D = erlang:list_to_integer([D3,D2,D1,D0], 16), 400 | [CodePoint] = xmerl_ucs:from_utf16be(<>), 402 | Acc1 = lists:reverse(xmerl_ucs:to_utf8(CodePoint), Acc), 403 | tokenize_string(B, ?ADV_COL(S, 12), Acc1); 404 | true -> 405 | Acc1 = lists:reverse(xmerl_ucs:to_utf8(C), Acc), 406 | tokenize_string(B, ?ADV_COL(S, 6), Acc1) 407 | end; 408 | <<_:O/binary, C, _/binary>> -> 409 | tokenize_string(B, ?INC_CHAR(S, C), [C | Acc]) 410 | end. 411 | 412 | tokenize_number(B, S) -> 413 | case tokenize_number(B, sign, S, []) of 414 | {{int, Int}, S1} -> 415 | {{const, list_to_integer(Int)}, S1}; 416 | {{float, Float}, S1} -> 417 | {{const, list_to_float(Float)}, S1} 418 | end. 419 | 420 | tokenize_number(B, sign, S=#decoder{offset=O}, []) -> 421 | case B of 422 | <<_:O/binary, $-, _/binary>> -> 423 | tokenize_number(B, int, ?INC_COL(S), [$-]); 424 | _ -> 425 | tokenize_number(B, int, S, []) 426 | end; 427 | tokenize_number(B, int, S=#decoder{offset=O}, Acc) -> 428 | case B of 429 | <<_:O/binary, $0, _/binary>> -> 430 | tokenize_number(B, frac, ?INC_COL(S), [$0 | Acc]); 431 | <<_:O/binary, C, _/binary>> when C >= $1 andalso C =< $9 -> 432 | tokenize_number(B, int1, ?INC_COL(S), [C | Acc]) 433 | end; 434 | tokenize_number(B, int1, S=#decoder{offset=O}, Acc) -> 435 | case B of 436 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 437 | tokenize_number(B, int1, ?INC_COL(S), [C | Acc]); 438 | _ -> 439 | tokenize_number(B, frac, S, Acc) 440 | end; 441 | tokenize_number(B, frac, S=#decoder{offset=O}, Acc) -> 442 | case B of 443 | <<_:O/binary, $., C, _/binary>> when C >= $0, C =< $9 -> 444 | tokenize_number(B, frac1, ?ADV_COL(S, 2), [C, $. | Acc]); 445 | <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> 446 | tokenize_number(B, esign, ?INC_COL(S), [$e, $0, $. | Acc]); 447 | _ -> 448 | {{int, lists:reverse(Acc)}, S} 449 | end; 450 | tokenize_number(B, frac1, S=#decoder{offset=O}, Acc) -> 451 | case B of 452 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 453 | tokenize_number(B, frac1, ?INC_COL(S), [C | Acc]); 454 | <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> 455 | tokenize_number(B, esign, ?INC_COL(S), [$e | Acc]); 456 | _ -> 457 | {{float, lists:reverse(Acc)}, S} 458 | end; 459 | tokenize_number(B, esign, S=#decoder{offset=O}, Acc) -> 460 | case B of 461 | <<_:O/binary, C, _/binary>> when C =:= $- orelse C=:= $+ -> 462 | tokenize_number(B, eint, ?INC_COL(S), [C | Acc]); 463 | _ -> 464 | tokenize_number(B, eint, S, Acc) 465 | end; 466 | tokenize_number(B, eint, S=#decoder{offset=O}, Acc) -> 467 | case B of 468 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 469 | tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]) 470 | end; 471 | tokenize_number(B, eint1, S=#decoder{offset=O}, Acc) -> 472 | case B of 473 | <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> 474 | tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]); 475 | _ -> 476 | {{float, lists:reverse(Acc)}, S} 477 | end. 478 | 479 | tokenize(B, S=#decoder{offset=O}) -> 480 | case B of 481 | <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) -> 482 | tokenize(B, ?INC_CHAR(S, C)); 483 | <<_:O/binary, "{", _/binary>> -> 484 | {start_object, ?INC_COL(S)}; 485 | <<_:O/binary, "}", _/binary>> -> 486 | {end_object, ?INC_COL(S)}; 487 | <<_:O/binary, "[", _/binary>> -> 488 | {start_array, ?INC_COL(S)}; 489 | <<_:O/binary, "]", _/binary>> -> 490 | {end_array, ?INC_COL(S)}; 491 | <<_:O/binary, ",", _/binary>> -> 492 | {comma, ?INC_COL(S)}; 493 | <<_:O/binary, ":", _/binary>> -> 494 | {colon, ?INC_COL(S)}; 495 | <<_:O/binary, "null", _/binary>> -> 496 | {{const, null}, ?ADV_COL(S, 4)}; 497 | <<_:O/binary, "true", _/binary>> -> 498 | {{const, true}, ?ADV_COL(S, 4)}; 499 | <<_:O/binary, "false", _/binary>> -> 500 | {{const, false}, ?ADV_COL(S, 5)}; 501 | <<_:O/binary, "\"", _/binary>> -> 502 | tokenize_string(B, ?INC_COL(S)); 503 | <<_:O/binary, C, _/binary>> when (C >= $0 andalso C =< $9) 504 | orelse C =:= $- -> 505 | tokenize_number(B, S); 506 | <<_:O/binary>> -> 507 | trim = S#decoder.state, 508 | {eof, S} 509 | end. 510 | %% 511 | %% Tests 512 | %% 513 | -include_lib("eunit/include/eunit.hrl"). 514 | -ifdef(TEST). 515 | 516 | 517 | %% testing constructs borrowed from the Yaws JSON implementation. 518 | 519 | %% Create an object from a list of Key/Value pairs. 520 | 521 | obj_new() -> 522 | {struct, []}. 523 | 524 | is_obj({struct, Props}) -> 525 | F = fun ({K, _}) when is_binary(K) -> true end, 526 | lists:all(F, Props). 527 | 528 | obj_from_list(Props) -> 529 | Obj = {struct, Props}, 530 | ?assert(is_obj(Obj)), 531 | Obj. 532 | 533 | %% Test for equivalence of Erlang terms. 534 | %% Due to arbitrary order of construction, equivalent objects might 535 | %% compare unequal as erlang terms, so we need to carefully recurse 536 | %% through aggregates (tuples and objects). 537 | 538 | equiv({struct, Props1}, {struct, Props2}) -> 539 | equiv_object(Props1, Props2); 540 | equiv(L1, L2) when is_list(L1), is_list(L2) -> 541 | equiv_list(L1, L2); 542 | equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; 543 | equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2; 544 | equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true. 545 | 546 | %% Object representation and traversal order is unknown. 547 | %% Use the sledgehammer and sort property lists. 548 | 549 | equiv_object(Props1, Props2) -> 550 | L1 = lists:keysort(1, Props1), 551 | L2 = lists:keysort(1, Props2), 552 | Pairs = lists:zip(L1, L2), 553 | true = lists:all(fun({{K1, V1}, {K2, V2}}) -> 554 | equiv(K1, K2) and equiv(V1, V2) 555 | end, Pairs). 556 | 557 | %% Recursively compare tuple elements for equivalence. 558 | 559 | equiv_list([], []) -> 560 | true; 561 | equiv_list([V1 | L1], [V2 | L2]) -> 562 | equiv(V1, V2) andalso equiv_list(L1, L2). 563 | 564 | decode_test() -> 565 | [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>), 566 | <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]). 567 | 568 | e2j_vec_test() -> 569 | test_one(e2j_test_vec(utf8), 1). 570 | 571 | test_one([], _N) -> 572 | %% io:format("~p tests passed~n", [N-1]), 573 | ok; 574 | test_one([{E, J} | Rest], N) -> 575 | %% io:format("[~p] ~p ~p~n", [N, E, J]), 576 | true = equiv(E, decode(J)), 577 | true = equiv(E, decode(encode(E))), 578 | test_one(Rest, 1+N). 579 | 580 | e2j_test_vec(utf8) -> 581 | [ 582 | {1, "1"}, 583 | {3.1416, "3.14160"}, %% text representation may truncate, trail zeroes 584 | {-1, "-1"}, 585 | {-3.1416, "-3.14160"}, 586 | {12.0e10, "1.20000e+11"}, 587 | {1.234E+10, "1.23400e+10"}, 588 | {-1.234E-10, "-1.23400e-10"}, 589 | {10.0, "1.0e+01"}, 590 | {123.456, "1.23456E+2"}, 591 | {10.0, "1e1"}, 592 | {<<"foo">>, "\"foo\""}, 593 | {<<"foo", 5, "bar">>, "\"foo\\u0005bar\""}, 594 | {<<"">>, "\"\""}, 595 | {<<"\n\n\n">>, "\"\\n\\n\\n\""}, 596 | {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\""}, 597 | {obj_new(), "{}"}, 598 | {obj_from_list([{<<"foo">>, <<"bar">>}]), "{\"foo\":\"bar\"}"}, 599 | {obj_from_list([{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]), 600 | "{\"foo\":\"bar\",\"baz\":123}"}, 601 | {[], "[]"}, 602 | {[[]], "[[]]"}, 603 | {[1, <<"foo">>], "[1,\"foo\"]"}, 604 | 605 | %% json array in a json object 606 | {obj_from_list([{<<"foo">>, [123]}]), 607 | "{\"foo\":[123]}"}, 608 | 609 | %% json object in a json object 610 | {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]), 611 | "{\"foo\":{\"bar\":true}}"}, 612 | 613 | %% fold evaluation order 614 | {obj_from_list([{<<"foo">>, []}, 615 | {<<"bar">>, obj_from_list([{<<"baz">>, true}])}, 616 | {<<"alice">>, <<"bob">>}]), 617 | "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, 618 | 619 | %% json object in a json array 620 | {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null], 621 | "[-123,\"foo\",{\"bar\":[]},null]"} 622 | ]. 623 | 624 | %% test utf8 encoding 625 | encoder_utf8_test() -> 626 | %% safe conversion case (default) 627 | [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] = 628 | encode(<<1,"\321\202\320\265\321\201\321\202">>), 629 | 630 | %% raw utf8 output (optional) 631 | Enc = mochijson2:encoder([{utf8, true}]), 632 | [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] = 633 | Enc(<<1,"\321\202\320\265\321\201\321\202">>). 634 | 635 | input_validation_test() -> 636 | Good = [ 637 | {16#00A3, <>}, %% pound 638 | {16#20AC, <>}, %% euro 639 | {16#10196, <>} %% denarius 640 | ], 641 | lists:foreach(fun({CodePoint, UTF8}) -> 642 | Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)), 643 | Expect = decode(UTF8) 644 | end, Good), 645 | 646 | Bad = [ 647 | %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte 648 | <>, 649 | %% missing continuations, last byte in each should be 80-BF 650 | <>, 651 | <>, 652 | <>, 653 | %% we don't support code points > 10FFFF per RFC 3629 654 | <> 655 | ], 656 | lists:foreach( 657 | fun(X) -> 658 | ok = try decode(X) catch invalid_utf8 -> ok end, 659 | %% could be {ucs,{bad_utf8_character_code}} or 660 | %% {json_encode,{bad_char,_}} 661 | {'EXIT', _} = (catch encode(X)) 662 | end, Bad). 663 | 664 | inline_json_test() -> 665 | ?assertEqual(<<"\"iodata iodata\"">>, 666 | iolist_to_binary( 667 | encode({json, [<<"\"iodata">>, " iodata\""]}))), 668 | ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]}, 669 | decode( 670 | encode({struct, 671 | [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))), 672 | ok. 673 | 674 | big_unicode_test() -> 675 | UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)), 676 | ?assertEqual( 677 | <<"\"\\ud834\\udd20\"">>, 678 | iolist_to_binary(encode(UTF8Seq))), 679 | ?assertEqual( 680 | UTF8Seq, 681 | decode(iolist_to_binary(encode(UTF8Seq)))), 682 | ok. 683 | 684 | custom_decoder_test() -> 685 | ?assertEqual( 686 | {struct, [{<<"key">>, <<"value">>}]}, 687 | (decoder([]))("{\"key\": \"value\"}")), 688 | F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end, 689 | ?assertEqual( 690 | win, 691 | (decoder([{object_hook, F}]))("{\"key\": \"value\"}")), 692 | ok. 693 | 694 | atom_test() -> 695 | %% JSON native atoms 696 | [begin 697 | ?assertEqual(A, decode(atom_to_list(A))), 698 | ?assertEqual(iolist_to_binary(atom_to_list(A)), 699 | iolist_to_binary(encode(A))) 700 | end || A <- [true, false, null]], 701 | %% Atom to string 702 | ?assertEqual( 703 | <<"\"foo\"">>, 704 | iolist_to_binary(encode(foo))), 705 | ?assertEqual( 706 | <<"\"\\ud834\\udd20\"">>, 707 | iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))), 708 | ok. 709 | 710 | key_encode_test() -> 711 | %% Some forms are accepted as keys that would not be strings in other 712 | %% cases 713 | ?assertEqual( 714 | <<"{\"foo\":1}">>, 715 | iolist_to_binary(encode({struct, [{foo, 1}]}))), 716 | ?assertEqual( 717 | <<"{\"foo\":1}">>, 718 | iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))), 719 | ?assertEqual( 720 | <<"{\"foo\":1}">>, 721 | iolist_to_binary(encode({struct, [{"foo", 1}]}))), 722 | ?assertEqual( 723 | <<"{\"\\ud834\\udd20\":1}">>, 724 | iolist_to_binary( 725 | encode({struct, [{[16#0001d120], 1}]}))), 726 | ?assertEqual( 727 | <<"{\"1\":1}">>, 728 | iolist_to_binary(encode({struct, [{1, 1}]}))), 729 | ok. 730 | 731 | unsafe_chars_test() -> 732 | Chars = "\"\\\b\f\n\r\t", 733 | [begin 734 | ?assertEqual(false, json_string_is_safe([C])), 735 | ?assertEqual(false, json_bin_is_safe(<>)), 736 | ?assertEqual(<>, decode(encode(<>))) 737 | end || C <- Chars], 738 | ?assertEqual( 739 | false, 740 | json_string_is_safe([16#0001d120])), 741 | ?assertEqual( 742 | false, 743 | json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))), 744 | ?assertEqual( 745 | [16#0001d120], 746 | xmerl_ucs:from_utf8( 747 | binary_to_list( 748 | decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))), 749 | ?assertEqual( 750 | false, 751 | json_string_is_safe([16#110000])), 752 | ?assertEqual( 753 | false, 754 | json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))), 755 | %% solidus can be escaped but isn't unsafe by default 756 | ?assertEqual( 757 | <<"/">>, 758 | decode(<<"\"\\/\"">>)), 759 | ok. 760 | 761 | int_test() -> 762 | ?assertEqual(0, decode("0")), 763 | ?assertEqual(1, decode("1")), 764 | ?assertEqual(11, decode("11")), 765 | ok. 766 | 767 | float_fallback_test() -> 768 | ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649))), 769 | ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648))), 770 | ok. 771 | 772 | handler_test() -> 773 | ?assertEqual( 774 | {'EXIT',{json_encode,{bad_term,{}}}}, 775 | catch encode({})), 776 | F = fun ({}) -> [] end, 777 | ?assertEqual( 778 | <<"[]">>, 779 | iolist_to_binary((encoder([{handler, F}]))({}))), 780 | ok. 781 | 782 | -endif. 783 | -------------------------------------------------------------------------------- /lib/mochinum.erl: -------------------------------------------------------------------------------- 1 | %% @copyright 2007 Mochi Media, Inc. 2 | %% @author Bob Ippolito 3 | 4 | %% @doc Useful numeric algorithms for floats that cover some deficiencies 5 | %% in the math module. More interesting is digits/1, which implements 6 | %% the algorithm from: 7 | %% http://www.cs.indiana.edu/~burger/fp/index.html 8 | %% See also "Printing Floating-Point Numbers Quickly and Accurately" 9 | %% in Proceedings of the SIGPLAN '96 Conference on Programming Language 10 | %% Design and Implementation. 11 | 12 | -module(mochinum). 13 | -author("Bob Ippolito "). 14 | -export([digits/1, frexp/1, int_pow/2, int_ceil/1]). 15 | 16 | %% IEEE 754 Float exponent bias 17 | -define(FLOAT_BIAS, 1022). 18 | -define(MIN_EXP, -1074). 19 | -define(BIG_POW, 4503599627370496). 20 | 21 | %% External API 22 | 23 | %% @spec digits(number()) -> string() 24 | %% @doc Returns a string that accurately represents the given integer or float 25 | %% using a conservative amount of digits. Great for generating 26 | %% human-readable output, or compact ASCII serializations for floats. 27 | digits(N) when is_integer(N) -> 28 | integer_to_list(N); 29 | digits(0.0) -> 30 | "0.0"; 31 | digits(Float) -> 32 | {Frac, Exp} = frexp(Float), 33 | Exp1 = Exp - 53, 34 | Frac1 = trunc(abs(Frac) * (1 bsl 53)), 35 | [Place | Digits] = digits1(Float, Exp1, Frac1), 36 | R = insert_decimal(Place, [$0 + D || D <- Digits]), 37 | case Float < 0 of 38 | true -> 39 | [$- | R]; 40 | _ -> 41 | R 42 | end. 43 | 44 | %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()} 45 | %% @doc Return the fractional and exponent part of an IEEE 754 double, 46 | %% equivalent to the libc function of the same name. 47 | %% F = Frac * pow(2, Exp). 48 | frexp(F) -> 49 | frexp1(unpack(F)). 50 | 51 | %% @spec int_pow(X::integer(), N::integer()) -> Y::integer() 52 | %% @doc Moderately efficient way to exponentiate integers. 53 | %% int_pow(10, 2) = 100. 54 | int_pow(_X, 0) -> 55 | 1; 56 | int_pow(X, N) when N > 0 -> 57 | int_pow(X, N, 1). 58 | 59 | %% @spec int_ceil(F::float()) -> integer() 60 | %% @doc Return the ceiling of F as an integer. The ceiling is defined as 61 | %% F when F == trunc(F); 62 | %% trunc(F) when F < 0; 63 | %% trunc(F) + 1 when F > 0. 64 | int_ceil(X) -> 65 | T = trunc(X), 66 | case (X - T) of 67 | Neg when Neg < 0 -> T; 68 | Pos when Pos > 0 -> T + 1; 69 | _ -> T 70 | end. 71 | 72 | 73 | %% Internal API 74 | 75 | int_pow(X, N, R) when N < 2 -> 76 | R * X; 77 | int_pow(X, N, R) -> 78 | int_pow(X * X, N bsr 1, case N band 1 of 1 -> R * X; 0 -> R end). 79 | 80 | insert_decimal(0, S) -> 81 | "0." ++ S; 82 | insert_decimal(Place, S) when Place > 0 -> 83 | L = length(S), 84 | case Place - L of 85 | 0 -> 86 | S ++ ".0"; 87 | N when N < 0 -> 88 | {S0, S1} = lists:split(L + N, S), 89 | S0 ++ "." ++ S1; 90 | N when N < 6 -> 91 | %% More places than digits 92 | S ++ lists:duplicate(N, $0) ++ ".0"; 93 | _ -> 94 | insert_decimal_exp(Place, S) 95 | end; 96 | insert_decimal(Place, S) when Place > -6 -> 97 | "0." ++ lists:duplicate(abs(Place), $0) ++ S; 98 | insert_decimal(Place, S) -> 99 | insert_decimal_exp(Place, S). 100 | 101 | insert_decimal_exp(Place, S) -> 102 | [C | S0] = S, 103 | S1 = case S0 of 104 | [] -> 105 | "0"; 106 | _ -> 107 | S0 108 | end, 109 | Exp = case Place < 0 of 110 | true -> 111 | "e-"; 112 | false -> 113 | "e+" 114 | end, 115 | [C] ++ "." ++ S1 ++ Exp ++ integer_to_list(abs(Place - 1)). 116 | 117 | 118 | digits1(Float, Exp, Frac) -> 119 | Round = ((Frac band 1) =:= 0), 120 | case Exp >= 0 of 121 | true -> 122 | BExp = 1 bsl Exp, 123 | case (Frac =/= ?BIG_POW) of 124 | true -> 125 | scale((Frac * BExp * 2), 2, BExp, BExp, 126 | Round, Round, Float); 127 | false -> 128 | scale((Frac * BExp * 4), 4, (BExp * 2), BExp, 129 | Round, Round, Float) 130 | end; 131 | false -> 132 | case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of 133 | true -> 134 | scale((Frac * 2), 1 bsl (1 - Exp), 1, 1, 135 | Round, Round, Float); 136 | false -> 137 | scale((Frac * 4), 1 bsl (2 - Exp), 2, 1, 138 | Round, Round, Float) 139 | end 140 | end. 141 | 142 | scale(R, S, MPlus, MMinus, LowOk, HighOk, Float) -> 143 | Est = int_ceil(math:log10(abs(Float)) - 1.0e-10), 144 | %% Note that the scheme implementation uses a 326 element look-up table 145 | %% for int_pow(10, N) where we do not. 146 | case Est >= 0 of 147 | true -> 148 | fixup(R, S * int_pow(10, Est), MPlus, MMinus, Est, 149 | LowOk, HighOk); 150 | false -> 151 | Scale = int_pow(10, -Est), 152 | fixup(R * Scale, S, MPlus * Scale, MMinus * Scale, Est, 153 | LowOk, HighOk) 154 | end. 155 | 156 | fixup(R, S, MPlus, MMinus, K, LowOk, HighOk) -> 157 | TooLow = case HighOk of 158 | true -> 159 | (R + MPlus) >= S; 160 | false -> 161 | (R + MPlus) > S 162 | end, 163 | case TooLow of 164 | true -> 165 | [(K + 1) | generate(R, S, MPlus, MMinus, LowOk, HighOk)]; 166 | false -> 167 | [K | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)] 168 | end. 169 | 170 | generate(R0, S, MPlus, MMinus, LowOk, HighOk) -> 171 | D = R0 div S, 172 | R = R0 rem S, 173 | TC1 = case LowOk of 174 | true -> 175 | R =< MMinus; 176 | false -> 177 | R < MMinus 178 | end, 179 | TC2 = case HighOk of 180 | true -> 181 | (R + MPlus) >= S; 182 | false -> 183 | (R + MPlus) > S 184 | end, 185 | case TC1 of 186 | false -> 187 | case TC2 of 188 | false -> 189 | [D | generate(R * 10, S, MPlus * 10, MMinus * 10, 190 | LowOk, HighOk)]; 191 | true -> 192 | [D + 1] 193 | end; 194 | true -> 195 | case TC2 of 196 | false -> 197 | [D]; 198 | true -> 199 | case R * 2 < S of 200 | true -> 201 | [D]; 202 | false -> 203 | [D + 1] 204 | end 205 | end 206 | end. 207 | 208 | unpack(Float) -> 209 | <> = <>, 210 | {Sign, Exp, Frac}. 211 | 212 | frexp1({_Sign, 0, 0}) -> 213 | {0.0, 0}; 214 | frexp1({Sign, 0, Frac}) -> 215 | Exp = log2floor(Frac), 216 | <> = <>, 217 | {Frac1, -(?FLOAT_BIAS) - 52 + Exp}; 218 | frexp1({Sign, Exp, Frac}) -> 219 | <> = <>, 220 | {Frac1, Exp - ?FLOAT_BIAS}. 221 | 222 | log2floor(Int) -> 223 | log2floor(Int, 0). 224 | 225 | log2floor(0, N) -> 226 | N; 227 | log2floor(Int, N) -> 228 | log2floor(Int bsr 1, 1 + N). 229 | 230 | 231 | %% 232 | %% Tests 233 | %% 234 | -include_lib("eunit/include/eunit.hrl"). 235 | -ifdef(TEST). 236 | 237 | int_ceil_test() -> 238 | 1 = int_ceil(0.0001), 239 | 0 = int_ceil(0.0), 240 | 1 = int_ceil(0.99), 241 | 1 = int_ceil(1.0), 242 | -1 = int_ceil(-1.5), 243 | -2 = int_ceil(-2.0), 244 | ok. 245 | 246 | int_pow_test() -> 247 | 1 = int_pow(1, 1), 248 | 1 = int_pow(1, 0), 249 | 1 = int_pow(10, 0), 250 | 10 = int_pow(10, 1), 251 | 100 = int_pow(10, 2), 252 | 1000 = int_pow(10, 3), 253 | ok. 254 | 255 | digits_test() -> 256 | ?assertEqual("0", 257 | digits(0)), 258 | ?assertEqual("0.0", 259 | digits(0.0)), 260 | ?assertEqual("1.0", 261 | digits(1.0)), 262 | ?assertEqual("-1.0", 263 | digits(-1.0)), 264 | ?assertEqual("0.1", 265 | digits(0.1)), 266 | ?assertEqual("0.01", 267 | digits(0.01)), 268 | ?assertEqual("0.001", 269 | digits(0.001)), 270 | ?assertEqual("1.0e+6", 271 | digits(1000000.0)), 272 | ?assertEqual("0.5", 273 | digits(0.5)), 274 | ?assertEqual("4503599627370496.0", 275 | digits(4503599627370496.0)), 276 | %% small denormalized number 277 | %% 4.94065645841246544177e-324 278 | <> = <<0,0,0,0,0,0,0,1>>, 279 | ?assertEqual("4.9406564584124654e-324", 280 | digits(SmallDenorm)), 281 | ?assertEqual(SmallDenorm, 282 | list_to_float(digits(SmallDenorm))), 283 | %% large denormalized number 284 | %% 2.22507385850720088902e-308 285 | <> = <<0,15,255,255,255,255,255,255>>, 286 | ?assertEqual("2.225073858507201e-308", 287 | digits(BigDenorm)), 288 | ?assertEqual(BigDenorm, 289 | list_to_float(digits(BigDenorm))), 290 | %% small normalized number 291 | %% 2.22507385850720138309e-308 292 | <> = <<0,16,0,0,0,0,0,0>>, 293 | ?assertEqual("2.2250738585072014e-308", 294 | digits(SmallNorm)), 295 | ?assertEqual(SmallNorm, 296 | list_to_float(digits(SmallNorm))), 297 | %% large normalized number 298 | %% 1.79769313486231570815e+308 299 | <> = <<127,239,255,255,255,255,255,255>>, 300 | ?assertEqual("1.7976931348623157e+308", 301 | digits(LargeNorm)), 302 | ?assertEqual(LargeNorm, 303 | list_to_float(digits(LargeNorm))), 304 | ok. 305 | 306 | frexp_test() -> 307 | %% zero 308 | {0.0, 0} = frexp(0.0), 309 | %% one 310 | {0.5, 1} = frexp(1.0), 311 | %% negative one 312 | {-0.5, 1} = frexp(-1.0), 313 | %% small denormalized number 314 | %% 4.94065645841246544177e-324 315 | <> = <<0,0,0,0,0,0,0,1>>, 316 | {0.5, -1073} = frexp(SmallDenorm), 317 | %% large denormalized number 318 | %% 2.22507385850720088902e-308 319 | <> = <<0,15,255,255,255,255,255,255>>, 320 | {0.99999999999999978, -1022} = frexp(BigDenorm), 321 | %% small normalized number 322 | %% 2.22507385850720138309e-308 323 | <> = <<0,16,0,0,0,0,0,0>>, 324 | {0.5, -1021} = frexp(SmallNorm), 325 | %% large normalized number 326 | %% 1.79769313486231570815e+308 327 | <> = <<127,239,255,255,255,255,255,255>>, 328 | {0.99999999999999989, 1024} = frexp(LargeNorm), 329 | ok. 330 | 331 | -endif. 332 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, {src_dirs, ["src", "lib"]}]}. 2 | 3 | {edoc_opts, [{application, ["cferl"]}]}. 4 | 5 | {deps_dir, ["deps"]}. 6 | 7 | {deps, [{ibrowse, ".*", {git, "git://github.com/cmullaparthi/ibrowse.git", {tag, "v4.0.1"}}}]}. 8 | -------------------------------------------------------------------------------- /src/cferl.app.src: -------------------------------------------------------------------------------- 1 | {application, 2 | cferl, 3 | [{description, "Rackspace Cloud Files client application"}, 4 | {vsn, "2.0.1-SNAPSHOT"}, 5 | {modules, [ 6 | cferl, 7 | cferl_connection, 8 | cferl_container, 9 | cferl_object, 10 | cferl_lib, 11 | mochijson2, 12 | mochinum 13 | ]}, 14 | {registered, []}, 15 | {applications, [kernel,stdlib,sasl,ssl,ibrowse]}, 16 | {env, []} 17 | ]}. 18 | -------------------------------------------------------------------------------- /src/cferl.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Authentication and connection with Rackspace Cloud Files. 3 | %%% @author David Dossot 4 | %%% @author Tilman Holschuh 5 | %%% 6 | %%% See LICENSE for license information. 7 | %%% Copyright (c) 2010 David Dossot 8 | %%% 9 | 10 | -module(cferl). 11 | -author('David Dossot '). 12 | -include("cferl.hrl"). 13 | 14 | -export([connect/2, connect/3]). 15 | -define(APPLICATION, cferl). 16 | 17 | -type(username() :: string() | binary()). 18 | -type(api_key() :: string() | binary()). 19 | -type(auth_service() :: string() | binary() | us | uk). 20 | 21 | %% @doc Authenticate and open connection (US). 22 | -spec connect(username(), api_key()) -> {ok, #cf_connection{}} | cferl_lib:cferl_error(). 23 | connect(Username, ApiKey) when is_binary(Username), is_binary(ApiKey) -> 24 | connect(binary_to_list(Username), 25 | binary_to_list(ApiKey)); 26 | connect(Username, ApiKey) when is_list(Username), is_list(ApiKey) -> 27 | connect(Username, ApiKey, us). 28 | 29 | %% @doc Authenticate and open connection. 30 | -spec connect(username(), api_key(), auth_service()) -> {ok, #cf_connection{}} | cferl_lib:cferl_error(). 31 | connect(Username, ApiKey, us) -> 32 | AuthUrl = "https://" ++ ?US_API_BASE_URL ++ ":443" ++ ?VERSION_PATH, 33 | connect(Username, ApiKey, AuthUrl); 34 | connect(Username, ApiKey, uk) -> 35 | AuthUrl = "https://" ++ ?UK_API_BASE_URL ++ ":443" ++ ?VERSION_PATH, 36 | connect(Username, ApiKey, AuthUrl); 37 | connect(Username, ApiKey, AuthUrl) when is_binary(Username), is_binary(ApiKey), is_binary(AuthUrl) -> 38 | connect(binary_to_list(Username), binary_to_list(ApiKey), binary_to_list(AuthUrl)); 39 | connect(Username, ApiKey, AuthUrl) when is_list(Username), is_list(ApiKey), is_list(AuthUrl) -> 40 | ensure_started(), 41 | 42 | Result = 43 | ibrowse:send_req(AuthUrl, 44 | [{"X-Auth-User", Username}, {"X-Auth-Key", ApiKey}], 45 | get), 46 | 47 | connect_result(Result). 48 | 49 | %% Private functions 50 | 51 | %% @doc Ensure started for the sake of verifying that required applications are running. 52 | ensure_started() -> 53 | ensure_started(?APPLICATION). 54 | 55 | ensure_started(App) -> 56 | ensure_started(App, application:start(App)). 57 | 58 | ensure_started(_App, ok ) -> ok; 59 | ensure_started(_App, {error, {already_started, _App}}) -> ok; 60 | ensure_started(App, {error, {not_started, Dep}}) -> 61 | ok = ensure_started(Dep), 62 | ensure_started(App); 63 | ensure_started(App, {error, Reason}) -> 64 | erlang:error({app_start_failed, App, Reason}). 65 | 66 | connect_result({ok, "204", ResponseHeaders, _ResponseBody}) -> 67 | {ok, Version} = application:get_key(?APPLICATION, vsn), 68 | {ok, cferl_connection:new(Version, 69 | cferl_lib:get_string_header("x-auth-token", ResponseHeaders), 70 | cferl_lib:get_string_header("x-storage-url", ResponseHeaders), 71 | cferl_lib:get_string_header("x-cdn-management-url", ResponseHeaders))}; 72 | connect_result(Other) -> 73 | cferl_lib:error_result(Other). 74 | 75 | -------------------------------------------------------------------------------- /src/cferl_connection.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Management of an account's containers. 3 | %%% @author David Dossot 4 | %%% 5 | %%% See LICENSE for license information. 6 | %%% Copyright (c) 2010 David Dossot 7 | %%% 8 | %%% @type cferl_error() = {error, not_found} | {error, unauthorized} | {error, {unexpected_response, Other}}. 9 | %%% @type cf_account_info() = record(). Record of type cf_account_info. 10 | %%% @type cf_container_query_args() = record(). Record of type cf_container_query_args. 11 | %%% @type cf_container_details() = record(). Record of type cf_container_details. 12 | %%% @type cferl_container() = term(). Reference to the cferl_container parameterized module. 13 | 14 | -module(cferl_connection). 15 | -author('David Dossot '). 16 | -include("cferl.hrl"). 17 | 18 | %% Public API 19 | -export([get_account_info/1, 20 | get_containers_names/1, 21 | get_containers_names/2, 22 | get_containers_details/1, 23 | get_containers_details/2, 24 | container_exists/2, 25 | get_container/2, 26 | create_container/2, 27 | delete_container/2, 28 | get_public_containers_names/2]). 29 | 30 | %% Exposed for internal usage 31 | -export([new/4, 32 | send_storage_request/4, 33 | send_storage_request/5, 34 | send_storage_request/6, 35 | send_cdn_management_request/4, 36 | send_cdn_management_request/5, 37 | async_response_loop/1]). 38 | 39 | 40 | %% @doc Retrieve the account information. 41 | -spec get_account_info(#cf_connection{}) -> {ok, #cf_account_info{}} | cferl_lib:cferl_error(). 42 | get_account_info(Conn) when ?IS_CONNECTION(Conn) -> 43 | Result = send_storage_request(Conn, head, "", raw), 44 | get_account_info_result(Result). 45 | 46 | get_account_info_result({ok, "204", ResponseHeaders, _}) -> 47 | {ok, 48 | #cf_account_info{ 49 | bytes_used = 50 | cferl_lib:get_int_header("x-account-bytes-used", ResponseHeaders), 51 | container_count = 52 | cferl_lib:get_int_header("x-account-container-count", ResponseHeaders) 53 | }}; 54 | get_account_info_result(Other) -> 55 | cferl_lib:error_result(Other). 56 | 57 | %% @doc Retrieve all the containers names (within the limits imposed by Cloud Files server). 58 | -spec get_containers_names(#cf_connection{}) -> {ok, [binary()]} | cferl_lib:cferl_error(). 59 | get_containers_names(Conn) when ?IS_CONNECTION(Conn) -> 60 | Result = send_storage_request(Conn, get, "", raw), 61 | get_containers_names_result(Result). 62 | 63 | %% @doc Retrieve the containers names filtered by the provided query arguments. 64 | %% If you supply the optional limit and marker arguments, the call will return the number of containers specified in limit, starting after the object named in marker. 65 | -spec get_containers_names(#cf_connection{}, #cf_container_query_args{}) -> {ok, [binary()]} | cferl_lib:cferl_error(). 66 | get_containers_names(Conn, QueryArgs) when ?IS_CONNECTION(Conn), is_record(QueryArgs, cf_container_query_args) -> 67 | QueryString = cferl_lib:container_query_args_to_string(QueryArgs), 68 | Result = send_storage_request(Conn, get, QueryString, raw), 69 | get_containers_names_result(Result). 70 | 71 | get_containers_names_result({ok, "204", _, _}) -> 72 | {ok, []}; 73 | get_containers_names_result({ok, "200", _, ResponseBody}) -> 74 | {ok, [list_to_binary(Name) || Name <- string:tokens(binary_to_list(ResponseBody), "\n")]}; 75 | get_containers_names_result(Other) -> 76 | cferl_lib:error_result(Other). 77 | 78 | %% @doc Retrieve all the containers information (within the limits imposed by Cloud Files server). 79 | -spec get_containers_details(#cf_connection{}) -> {ok, [#cf_container_details{}]} | cferl_lib:cferl_error(). 80 | get_containers_details(Conn) when ?IS_CONNECTION(Conn) -> 81 | get_containers_details(Conn, #cf_container_query_args{}). 82 | 83 | %% @doc Retrieve the containers information filtered by the provided query arguments. 84 | -spec get_containers_details(#cf_connection{}, #cf_container_query_args{}) -> 85 | {ok, [#cf_container_details{}]} | cferl_lib:cferl_error(). 86 | get_containers_details(Conn, QueryArgs) when ?IS_CONNECTION(Conn), is_record(QueryArgs, cf_container_query_args) -> 87 | QueryString = cferl_lib:container_query_args_to_string(QueryArgs), 88 | Result = send_storage_request(Conn, get, QueryString, json), 89 | get_containers_details_result(Result). 90 | 91 | get_containers_details_result({ok, "204", _, _}) -> 92 | {ok, []}; 93 | get_containers_details_result({ok, "200", _, ResponseBody}) -> 94 | BuildRecordFun = 95 | fun({struct, Proplist}) -> 96 | #cf_container_details{ 97 | name = proplists:get_value(<<"name">>, Proplist), 98 | bytes = proplists:get_value(<<"bytes">>, Proplist), 99 | count = proplists:get_value(<<"count">>, Proplist) 100 | } 101 | end, 102 | 103 | ContainersInfo = lists:map(BuildRecordFun, 104 | mochijson2:decode(ResponseBody)), 105 | {ok, ContainersInfo}; 106 | get_containers_details_result(Other) -> 107 | cferl_lib:error_result(Other). 108 | 109 | %% @doc Test the existence of a container. 110 | -spec container_exists(#cf_connection{}, Name::binary()) -> true | false. 111 | container_exists(Conn, Name) when ?IS_CONNECTION(Conn), is_binary(Name) -> 112 | Result = send_storage_request(Conn, head, get_container_path(Name), raw), 113 | container_exists_result(Result). 114 | 115 | container_exists_result({ok, "204", _, _}) -> 116 | true; 117 | container_exists_result(_) -> 118 | false. 119 | 120 | %% @doc Get a reference to an existing container. 121 | -spec get_container(#cf_connection{}, Name::binary()) -> {ok, #cf_container{}} | cferl_lib:cferl_error(). 122 | get_container(Conn, Name) when ?IS_CONNECTION(Conn), is_binary(Name) -> 123 | Result = send_storage_request(Conn, head, get_container_path(Name), raw), 124 | get_container_result(Conn, Name, Result). 125 | 126 | get_container_result(Conn, Name, {ok, "204", ResponseHeaders, _}) -> 127 | ContainerDetails = #cf_container_details{ 128 | name = Name, 129 | bytes = cferl_lib:get_int_header("x-container-bytes-used", ResponseHeaders), 130 | count = cferl_lib:get_int_header("x-container-object-count", ResponseHeaders) 131 | }, 132 | {ok, CdnDetails} = get_container_cdn_details(Conn, Name), 133 | {ok, cferl_container:new(ContainerDetails, get_container_path(Name), CdnDetails)}; 134 | get_container_result(_, _, Other) -> 135 | cferl_lib:error_result(Other). 136 | 137 | get_container_cdn_details(Conn, Name) -> 138 | Result = send_cdn_management_request(Conn, head, get_container_path(Name), raw), 139 | get_container_cdn_details_result(Result). 140 | 141 | get_container_cdn_details_result({ok, "204", ResponseHeaders, _}) -> 142 | {ok, build_cdn_details_proplist(ResponseHeaders)}; 143 | get_container_cdn_details_result(_Other) -> 144 | {ok, build_cdn_details_proplist([])}. 145 | 146 | build_cdn_details_proplist(Headers) -> 147 | [ 148 | {cdn_enabled, cferl_lib:get_boolean_header("x-cdn-enabled", Headers)}, 149 | {ttl, cferl_lib:get_int_header("x-ttl", Headers)}, 150 | {cdn_uri, cferl_lib:get_binary_header("x-cdn-uri", Headers)}, 151 | {user_agent_acl, cferl_lib:get_binary_header("x-user-agent-acl", Headers)}, 152 | {referrer_acl, cferl_lib:get_binary_header("x-referrer-acl", Headers)}, 153 | {log_retention, cferl_lib:get_boolean_header("x-log-retention", Headers)} 154 | ]. 155 | 156 | %% @doc Create a new container (name must not be already used). 157 | -spec create_container(#cf_connection{}, Name::binary()) -> 158 | {ok, #cf_container{}} | {error, already_existing} | cferl_lib:cferl_error(). 159 | create_container(Conn, Name) when ?IS_CONNECTION(Conn), is_binary(Name) -> 160 | Result = send_storage_request(Conn, put, get_container_path(Name), raw), 161 | create_container_result(Conn, Name, Result). 162 | 163 | create_container_result(Conn, Name, {ok, "201", _, _}) -> 164 | get_container(Conn, Name); 165 | create_container_result(_, _, {ok, "202", _, _}) -> 166 | {error, already_existing}; 167 | create_container_result(_, _, Other) -> 168 | cferl_lib:error_result(Other). 169 | 170 | %% @doc Delete a container (which must be empty). 171 | -spec delete_container(#cf_connection{}, Name::binary()) -> ok | {error, not_empty} | cferl_lib:cferl_error(). 172 | %% Error = {error, not_empty} | cferl_error() 173 | delete_container(Conn, Name) when ?IS_CONNECTION(Conn), is_binary(Name) -> 174 | Result = send_storage_request(Conn, delete, get_container_path(Name), raw), 175 | delete_container_result(Result). 176 | 177 | delete_container_result({ok, "204", _, _}) -> 178 | ok; 179 | delete_container_result({ok, "409", _, _}) -> 180 | {error, not_empty}; 181 | delete_container_result(Other) -> 182 | cferl_lib:error_result(Other). 183 | 184 | %% @doc Retrieve the names of public (CDN-enabled) containers, whether they are still public (active) or happen to have been exposed in the past(all_time). 185 | -spec get_public_containers_names(#cf_connection{}, TimeFilter::active | all_time) -> {ok, [binary()]} | cferl_lib:cferl_error(). 186 | get_public_containers_names(Conn, active) when ?IS_CONNECTION(Conn) -> 187 | Result = send_cdn_management_request(Conn, get, "?enabled_only=true", raw), 188 | get_public_containers_names_result(Result); 189 | get_public_containers_names(Conn, all_time) when ?IS_CONNECTION(Conn) -> 190 | Result = send_cdn_management_request(Conn, get, "", raw), 191 | get_public_containers_names_result(Result). 192 | 193 | get_public_containers_names_result({ok, "204", _, _}) -> 194 | {ok, []}; 195 | get_public_containers_names_result({ok, "200", _, ResponseBody}) -> 196 | {ok, [list_to_binary(Name) || Name <- string:tokens(binary_to_list(ResponseBody), "\n")]}; 197 | get_public_containers_names_result(Other) -> 198 | cferl_lib:error_result(Other). 199 | 200 | %% Friend functions 201 | %% @hidden 202 | -spec new(Version::string(), 203 | AuthToken :: string(), 204 | StorageUrl :: string(), 205 | CdnManagementUrl :: string()) -> #cf_connection{}. 206 | new(Version, AuthToken, StorageUrl, CdnManagementUrl) -> 207 | #cf_connection{ 208 | version = Version, 209 | auth_token = AuthToken, 210 | storage_url = StorageUrl, 211 | cdn_management_url = CdnManagementUrl }. 212 | 213 | %% @hidden 214 | send_storage_request(Connection, Method, PathAndQuery, Accept) 215 | when is_atom(Method), 216 | is_atom(Accept) or is_function(Accept, 1) -> 217 | 218 | send_storage_request(Connection, Method, PathAndQuery, [], Accept). 219 | 220 | send_storage_request(Connection, Method, PathAndQuery, Headers, Accept) 221 | when is_atom(Method), is_list(Headers), 222 | is_atom(Accept) or is_function(Accept, 1) -> 223 | 224 | send_request(Connection#cf_connection.storage_url, 225 | Method, PathAndQuery, 226 | handle_headers(Connection, Headers), 227 | <<>>, Accept). 228 | 229 | send_storage_request(Connection, Method, PathAndQuery, Headers, Body, Accept) 230 | when is_atom(Method), is_list(Headers), 231 | is_binary(Body) or is_function(Body, 0), 232 | is_atom(Accept) or is_function(Accept, 1) -> 233 | 234 | send_request(Connection#cf_connection.storage_url, 235 | Method, PathAndQuery, 236 | handle_headers(Connection, Headers), 237 | Body, Accept). 238 | 239 | %% @hidden 240 | send_cdn_management_request(Connection, Method, PathAndQuery, Accept) 241 | when is_atom(Method), is_atom(Accept) -> 242 | send_request(Connection#cf_connection.cdn_management_url, 243 | Method, PathAndQuery, 244 | handle_headers(Connection, []), 245 | <<>>, Accept). 246 | 247 | %% @hidden 248 | send_cdn_management_request(Connection, Method, PathAndQuery, Headers, Accept) 249 | when is_atom(Method), is_list(Headers), is_atom(Accept) -> 250 | send_request(Connection#cf_connection.cdn_management_url, 251 | Method, PathAndQuery, 252 | handle_headers(Connection, Headers), 253 | <<>>, Accept). 254 | 255 | %% @hidden 256 | get_container_path(Name) when is_binary(Name) -> 257 | "/" ++ cferl_lib:url_encode(Name). 258 | 259 | %% Private functions 260 | send_request(BaseUrl, Method, PathAndQuery, Headers, Body, Accept) 261 | when is_atom(Method), is_binary(PathAndQuery), is_list(Headers), is_binary(Body), 262 | is_atom(Accept) or is_function(Accept, 1) -> 263 | 264 | send_request(BaseUrl, Method, binary_to_list(PathAndQuery), Headers, Body, Accept); 265 | 266 | send_request(BaseUrl, Method, PathAndQuery, Headers, Body, ResultFun) 267 | when is_atom(Method), is_list(PathAndQuery), is_list(Headers), 268 | is_binary(Body) or is_function(Body, 0), 269 | is_function(ResultFun, 1) -> 270 | 271 | ResultPid = proc_lib:spawn(fun() -> async_response_loop(ResultFun) end), 272 | Options = [{stream_to, {ResultPid, once}}], 273 | do_send_request(BaseUrl, Method, PathAndQuery, Headers, Body, Options); 274 | 275 | send_request(BaseUrl, Method, PathAndQuery, Headers, Body, raw) 276 | when is_atom(Method), is_list(PathAndQuery), is_list(Headers), 277 | is_binary(Body) or is_function(Body, 0) -> 278 | 279 | do_send_request(BaseUrl, Method, PathAndQuery, Headers, Body, []); 280 | 281 | send_request(BaseUrl, Method, PathAndQuery, Headers, Body, json) 282 | when is_atom(Method), is_list(PathAndQuery), is_list(Headers), 283 | is_binary(Body) or is_function(Body, 0) -> 284 | 285 | do_send_request(BaseUrl, 286 | Method, 287 | build_json_query_string(PathAndQuery), 288 | Headers, 289 | Body, 290 | []). 291 | 292 | do_send_request(BaseUrl, Method, PathAndQuery, Headers, Body, Options) 293 | when is_list(BaseUrl), is_list(PathAndQuery), is_list(Headers), is_atom(Method), 294 | is_binary(Body) or is_function(Body, 0), 295 | is_list(Options) -> 296 | 297 | ibrowse:send_req(BaseUrl ++ PathAndQuery, 298 | cferl_lib:binary_headers_to_string(Headers), 299 | Method, 300 | Body, 301 | [{response_format, binary} | Options]). 302 | 303 | handle_headers(Connection, Headers) -> 304 | [{"User-Agent", "cferl (CloudFiles Erlang API) v" ++ Connection#cf_connection.version}, 305 | {"X-Auth-Token", Connection#cf_connection.auth_token} 306 | | Headers]. 307 | 308 | build_json_query_string(PathAndQuery) when is_list(PathAndQuery) -> 309 | PathAndQuery ++ 310 | case lists:member($?, PathAndQuery) of 311 | true -> "&"; 312 | false -> "?" 313 | end ++ 314 | "format=json". 315 | 316 | %% @hidden 317 | async_response_loop(ResultFun) when is_function(ResultFun, 1) -> 318 | receive 319 | {ibrowse_async_headers, Req_id, StatCode, _ResponseHeaders} -> 320 | case StatCode of 321 | [$2|_] -> 322 | stream_next_chunk(ResultFun, Req_id); 323 | 324 | nomatch -> 325 | ResultFun({error, {unexpected_status_code, StatCode}}) 326 | end; 327 | 328 | {ibrowse_async_response, _Req_id, Error = {error, _}} -> 329 | ResultFun(Error); 330 | 331 | {ibrowse_async_response, Req_id, Data} -> 332 | ResultFun({ok, Data}), 333 | stream_next_chunk(ResultFun, Req_id); 334 | 335 | {ibrowse_async_response_end, _Req_id} -> 336 | ResultFun(eof) 337 | 338 | after ?DEFAULT_REQUEST_TIMEOUT -> 339 | ResultFun({error, time_out}) 340 | end. 341 | 342 | stream_next_chunk(ResultFun, Req_id) -> 343 | case ibrowse:stream_next(Req_id) of 344 | ok -> 345 | async_response_loop(ResultFun); 346 | Error -> 347 | ResultFun(Error) 348 | end. 349 | 350 | -------------------------------------------------------------------------------- /src/cferl_container.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Management of a container's storage objects. 3 | %%% @author David Dossot 4 | %%% 5 | %%% See LICENSE for license information. 6 | %%% Copyright (c) 2010 David Dossot 7 | %%% 8 | %%% @type cf_container_cdn_config() = record(). Record of type cf_container_cdn_config. 9 | %%% @type cf_object_query_args() = record(). Record of type cf_object_query_args. 10 | %%% @type cf_object_details() = record(). Record of type cf_object_details. 11 | 12 | -module(cferl_container). 13 | -author('David Dossot '). 14 | -include("cferl.hrl"). 15 | 16 | %% Public API 17 | -export([name/1, 18 | bytes/1, 19 | count/1, 20 | is_empty/1, 21 | is_public/1, 22 | cdn_url/1, 23 | cdn_ttl/1, 24 | log_retention/1, 25 | make_public/2, 26 | make_public/3, 27 | make_private/2, 28 | set_log_retention/3, 29 | refresh/2, 30 | delete/2, 31 | get_objects_names/2, 32 | get_objects_names/3, 33 | get_objects_details/2, 34 | get_objects_details/3, 35 | object_exists/3, 36 | get_object/3, 37 | create_object/3, 38 | delete_object/3, 39 | ensure_dir/3 40 | ]). 41 | 42 | %% Exposed for internal usage 43 | -export([new/3 ]). 44 | 45 | 46 | %% @doc Name of the current container. 47 | -spec name(#cf_container{}) -> binary(). 48 | name(Container) when ?IS_CONTAINER(Container) -> 49 | ContainerDetails = Container#cf_container.container_details, 50 | ContainerDetails#cf_container_details.name. 51 | 52 | %% @doc Size in bytes of the current container. 53 | -spec bytes(#cf_container{}) -> integer(). 54 | bytes(Container) when ?IS_CONTAINER(Container) -> 55 | ContainerDetails = Container#cf_container.container_details, 56 | ContainerDetails#cf_container_details.bytes. 57 | 58 | %% @doc Number of objects in the current container. 59 | -spec count(#cf_container{}) -> integer(). 60 | count(Container) when ?IS_CONTAINER(Container) -> 61 | ContainerDetails = Container#cf_container.container_details, 62 | ContainerDetails#cf_container_details.count. 63 | 64 | %% @doc Determine if the current container is empty. 65 | -spec is_empty(#cf_container{}) -> true | false. 66 | is_empty(Container) -> 67 | count(Container) == 0. 68 | 69 | %% @doc Determine if the current container is public (CDN-enabled). 70 | -spec is_public(#cf_container{}) -> true | false. 71 | is_public(Container) when ?IS_CONTAINER(Container) -> 72 | CdnDetails = Container#cf_container.cdn_details, 73 | proplists:get_value(cdn_enabled, CdnDetails). 74 | 75 | %% @doc CDN of the container URL, if it is public. 76 | -spec cdn_url(#cf_container{}) -> binary(). 77 | cdn_url(Container) when ?IS_CONTAINER(Container) -> 78 | CdnDetails = Container#cf_container.cdn_details, 79 | proplists:get_value(cdn_uri, CdnDetails). 80 | 81 | %% @doc TTL (in seconds) of the container, if it is public. 82 | -spec cdn_ttl(#cf_container{}) -> integer(). 83 | cdn_ttl(Container) when ?IS_CONTAINER(Container) -> 84 | CdnDetails = Container#cf_container.cdn_details, 85 | proplists:get_value(ttl, CdnDetails). 86 | 87 | %% @doc Determine if log retention is enabled on this container (which must be public). 88 | -spec log_retention(#cf_container{}) -> true | false. 89 | log_retention(Container) when ?IS_CONTAINER(Container) -> 90 | CdnDetails = Container#cf_container.cdn_details, 91 | is_public(Container) andalso proplists:get_value(log_retention, CdnDetails). 92 | 93 | %% @doc Make the current container publicly accessible on CDN, using the default configuration (ttl of 1 day and no ACL). 94 | -spec make_public(#cf_connection{}, #cf_container{}) -> ok | cferl_lib:cferl_error(). 95 | make_public(Connection, Container) when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container) -> 96 | make_public(Connection, Container, #cf_container_cdn_config{}). 97 | 98 | %% @doc Make the current container publicly accessible on CDN, using the provided configuration. 99 | %% ttl is in seconds. 100 | %% user_agent_acl and referrer_acl are Perl-compatible regular expression used to limit access to this container. 101 | -spec make_public(#cf_connection{}, #cf_container{}, #cf_container_cdn_config{}) -> ok | cferl_lib:cferl_error(). 102 | make_public(Connection, Container, CdnConfig) 103 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_record(CdnConfig, cf_container_cdn_config) -> 104 | PutResult = cferl_connection:send_cdn_management_request(Connection, put, Container#cf_container.container_path, raw), 105 | make_public_put_result(Connection, Container, CdnConfig, PutResult). 106 | 107 | make_public_put_result(Connection, Container, CdnConfig, {ok, ResponseCode, _, _}) 108 | when ResponseCode =:= "201"; ResponseCode =:= "202" -> 109 | 110 | CdnConfigHeaders = cferl_lib:cdn_config_to_headers(CdnConfig), 111 | Headers = [{"X-CDN-Enabled", "True"} | CdnConfigHeaders], 112 | PostResult = cferl_connection:send_cdn_management_request(Connection, post, Container#cf_container.container_path, Headers, raw), 113 | make_public_post_result(PostResult); 114 | make_public_put_result(_, _, _, Other) -> 115 | cferl_lib:error_result(Other). 116 | 117 | make_public_post_result({ok, ResponseCode, _, _}) 118 | when ResponseCode =:= "201"; ResponseCode =:= "202" -> 119 | ok; 120 | make_public_post_result(Other) -> 121 | cferl_lib:error_result(Other). 122 | 123 | %% @doc Make the current container private. 124 | %% If it was previously public, it will remain accessible on the CDN until its TTL is reached. 125 | -spec make_private(#cf_connection{}, #cf_container{}) -> ok | cferl_lib:cferl_error(). 126 | make_private(Connection, Container) when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container) -> 127 | Headers = [{"X-CDN-Enabled", "False"}], 128 | PostResult = cferl_connection:send_cdn_management_request(Connection, post, Container#cf_container.container_path, Headers, raw), 129 | make_private_result(PostResult). 130 | 131 | make_private_result({ok, ResponseCode, _, _}) 132 | when ResponseCode =:= "201"; ResponseCode =:= "202" -> 133 | ok; 134 | make_private_result(Other) -> 135 | cferl_lib:error_result(Other). 136 | 137 | %% @doc Activate or deactivate log retention for current container. 138 | -spec set_log_retention(#cf_connection{}, #cf_container{}, true | false) -> ok | cferl_lib:cferl_error(). 139 | set_log_retention(Connection, Container, true) when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container) -> 140 | do_set_log_retention(Connection, Container, "True"); 141 | set_log_retention(Connection, Container, false) when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container) -> 142 | do_set_log_retention(Connection, Container, "False"). 143 | 144 | do_set_log_retention(Connection, Container, State) -> 145 | Headers = [{"x-log-retention", State}], 146 | PostResult = cferl_connection:send_cdn_management_request(Connection, post, Container#cf_container.container_path, Headers, raw), 147 | set_log_retention_result(PostResult). 148 | 149 | set_log_retention_result({ok, ResponseCode, _, _}) 150 | when ResponseCode =:= "201"; ResponseCode =:= "202" -> 151 | ok; 152 | set_log_retention_result(Other) -> 153 | cferl_lib:error_result(Other). 154 | 155 | %% @doc Refresh the current container reference. 156 | -spec refresh(#cf_connection{}, #cf_container{}) -> {ok, #cf_container{}} | cferl_lib:cferl_error(). 157 | refresh(Connection, Container) when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container) -> 158 | cferl_connection:get_container(Connection, name(Container)). 159 | 160 | %% @doc Delete the current container (which must be empty). 161 | -spec delete(#cf_connection{}, #cf_container{}) -> ok | {error, not_empty} | cferl_lib:cferl_error(). 162 | delete(Connection, Container) when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container) -> 163 | cferl_connection:delete_container(Connection, name(Container)). 164 | 165 | %% @doc Retrieve all the object names in the current container (within the limits imposed by Cloud Files server). 166 | -spec get_objects_names(#cf_connection{}, #cf_container{}) -> {ok, [binary()]} | cferl_lib:cferl_error(). 167 | get_objects_names(Connection, Container) -> 168 | get_objects_names(Connection, Container, #cf_object_query_args{}). 169 | 170 | %% @doc Retrieve the object names in the current container, filtered by the provided query arguments. 171 | %% If you supply the optional limit, marker, prefix or path arguments, the call will return the number of objects specified in limit, 172 | %% starting at the object index specified in marker, selecting objects whose names start with prefix or search within the pseudo-filesystem 173 | %% path. 174 | -spec get_objects_names(#cf_connection{}, #cf_container{}, #cf_object_query_args{}) -> {ok, [binary()]} | cferl_lib:cferl_error(). 175 | get_objects_names(Connection, Container, QueryArgs) 176 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_record(QueryArgs, cf_object_query_args) -> 177 | QueryString = cferl_lib:object_query_args_to_string(QueryArgs), 178 | Result = cferl_connection:send_storage_request(Connection, get, Container#cf_container.container_path ++ QueryString, raw), 179 | get_objects_names_result(Result). 180 | 181 | get_objects_names_result({ok, "204", _, _}) -> 182 | {ok, []}; 183 | get_objects_names_result({ok, "200", _, ResponseBody}) -> 184 | {ok, [list_to_binary(ObjectName) || ObjectName <- string:tokens(binary_to_list(ResponseBody), "\n")]}; 185 | get_objects_names_result(Other) -> 186 | cferl_lib:error_result(Other). 187 | 188 | %% @doc Retrieve details for all the objects in the current container (within the limits imposed by Cloud Files server). 189 | -spec get_objects_details(#cf_connection{}, #cf_container{}) -> {ok, [#cf_object_details{}]} | cferl_lib:cferl_error(). 190 | %% Error = cferl_error() 191 | get_objects_details(Connection, Container) -> 192 | get_objects_details(Connection, Container, #cf_object_query_args{}). 193 | 194 | %% @doc Retrieve the object details in the current container, filtered by the provided query arguments. 195 | %% If you supply the optional limit, marker, prefix or path arguments, the call will return the number of objects specified in limit, 196 | %% starting at the object index specified in marker, selecting objects whose names start with prefix or search within the pseudo-filesystem 197 | %% path. 198 | -spec get_objects_details(#cf_connection{}, #cf_container{}, #cf_object_query_args{}) -> 199 | {ok, [#cf_object_details{}]} | cferl_lib:cferl_error(). 200 | get_objects_details(Connection, Container, QueryArgs) 201 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_record(QueryArgs, cf_object_query_args) -> 202 | QueryString = cferl_lib:object_query_args_to_string(QueryArgs), 203 | Result = cferl_connection:send_storage_request(Connection, get, Container#cf_container.container_path ++ QueryString, json), 204 | get_objects_details_result(Result). 205 | 206 | get_objects_details_result({ok, "204", _, _}) -> 207 | {ok, []}; 208 | get_objects_details_result({ok, "200", _, ResponseBody}) -> 209 | BuildRecordFun = 210 | fun({struct, Proplist}) -> 211 | LastModifiedBin = proplists:get_value(<<"last_modified">>, Proplist), 212 | <> = LastModifiedBin, 219 | 220 | % drop the microseconds, not supported by RFC 1123 221 | LastModified = {{bin_to_int(Year), bin_to_int(Month), bin_to_int(Day)}, 222 | {bin_to_int(Hour), bin_to_int(Min), bin_to_int(Sec)}}, 223 | 224 | #cf_object_details{ 225 | name = proplists:get_value(<<"name">>, Proplist), 226 | bytes = proplists:get_value(<<"bytes">>, Proplist), 227 | last_modified = LastModified, 228 | content_type = proplists:get_value(<<"content_type">>, Proplist), 229 | etag = proplists:get_value(<<"hash">>, Proplist) 230 | } 231 | end, 232 | 233 | ObjectsInfo = lists:map(BuildRecordFun, 234 | mochijson2:decode(ResponseBody)), 235 | {ok, ObjectsInfo}; 236 | get_objects_details_result(Other) -> 237 | cferl_lib:error_result(Other). 238 | 239 | %% @doc Test the existence of an object in the current container. 240 | -spec object_exists(#cf_connection{}, #cf_container{}, ObjectName::binary()) -> true | false. 241 | object_exists(Connection, Container, ObjectName) when ?IS_CONNECTION(Connection), is_binary(ObjectName) -> 242 | Result = cferl_connection:send_storage_request(Connection, head, get_object_path(Container, ObjectName), raw), 243 | object_exists_result(Result). 244 | 245 | object_exists_result({ok, ResponseCode, _, _}) 246 | when ResponseCode =:= "200"; ResponseCode =:= "204" -> 247 | true; 248 | object_exists_result(_) -> 249 | false. 250 | 251 | %% @doc Get a reference to an existing storage object. 252 | -spec get_object(#cf_connection{}, #cf_container{}, Name::binary()) -> {ok, #cf_object{}} | cferl_lib:cferl_error(). 253 | get_object(Connection, Container, ObjectName) 254 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_binary(ObjectName) -> 255 | Result = cferl_connection:send_storage_request(Connection, head, get_object_path(Container, ObjectName), raw), 256 | get_object_result(ObjectName, Container, Result). 257 | 258 | get_object_result(ObjectName, Container, {ok, ResponseCode, ResponseHeaders, _}) 259 | when ResponseCode =:= "200"; ResponseCode =:= "204" -> 260 | 261 | ObjectDetails = #cf_object_details{ 262 | name = ObjectName, 263 | bytes = cferl_lib:get_int_header("Content-Length", ResponseHeaders), 264 | last_modified = httpd_util:convert_request_date(cferl_lib:get_string_header("Last-Modified", ResponseHeaders)), 265 | content_type = cferl_lib:get_binary_header("Content-Type", ResponseHeaders), 266 | etag = cferl_lib:get_binary_header("Etag", ResponseHeaders) 267 | }, 268 | 269 | {ok, cferl_object:new(Container, ObjectDetails, get_object_path(Container, ObjectName), ResponseHeaders)}; 270 | 271 | get_object_result(_, _, Other) -> 272 | cferl_lib:error_result(Other). 273 | 274 | %% @doc Create a reference to a new storage object. 275 | %% Nothing is actually created until data gets written in the object. 276 | %% If an object with the provided name already exists, a reference to this object is returned. 277 | -spec create_object(#cf_connection{}, #cf_container{}, Name::binary()) -> {ok, #cf_object{}} | cferl_lib:cferl_error(). 278 | %% Error = cferl_error() 279 | create_object(Connection, Container, ObjectName) 280 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_binary(ObjectName) -> 281 | case get_object(Connection, Container, ObjectName) of 282 | {ok, Object} -> 283 | {ok, Object}; 284 | _ -> 285 | ObjectDetails = #cf_object_details{name = ObjectName}, 286 | {ok, cferl_object:new(Container, ObjectDetails, get_object_path(Container, ObjectName), [])} 287 | end. 288 | 289 | %% @doc Delete an existing storage object. 290 | -spec delete_object(#cf_connection{}, #cf_container{}, Name::binary()) -> ok | cferl_lib:cferl_error(). 291 | delete_object(Connection, Container, ObjectName) 292 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_binary(ObjectName) -> 293 | Result = cferl_connection:send_storage_request(Connection, delete, get_object_path(Container, ObjectName), raw), 294 | delete_object_result(Result). 295 | 296 | delete_object_result({ok, "204", _, _}) -> 297 | ok; 298 | delete_object_result(Other) -> 299 | cferl_lib:error_result(Other). 300 | 301 | %% @doc Ensure that all the directories exist in an object path. 302 | %% Passing <<"photos/plants/fern.jpg">>, will ensure that the <<"photos">> and <<"photos/plants">> directories exist. 303 | -spec ensure_dir(#cf_connection{}, #cf_container{}, ObjectPath::binary()) -> ok. 304 | ensure_dir(Connection, Container, ObjectPath) 305 | when ?IS_CONNECTION(Connection), ?IS_CONTAINER(Container), is_binary(ObjectPath) -> 306 | CreateDirectoryFun = 307 | fun(Directory) -> 308 | {ok, DirectoryObject} = create_object(Connection, Container, Directory), 309 | % push the object on the server only if its content-type is not good 310 | case cferl_object:content_type(DirectoryObject) of 311 | ?DIRECTORY_OBJECT_CONTENT_TYPE -> 312 | noop; 313 | _ -> 314 | ok = cferl_object:write_data(Connection, DirectoryObject, <<>>, 315 | ?DIRECTORY_OBJECT_CONTENT_TYPE, 316 | [{<<"Content-Length">>, <<"0">>}]) 317 | end 318 | end, 319 | 320 | lists:foreach(CreateDirectoryFun, cferl_lib:path_to_sub_dirs(ObjectPath)), 321 | ok. 322 | 323 | %% Friend functions 324 | -spec new(#cf_container_details{}, string(), [{atom(), term()}]) -> #cf_container{}. 325 | new(ContainerDetails, Path, CdnDetails) -> 326 | #cf_container{container_details = ContainerDetails, 327 | container_path = Path, 328 | cdn_details = CdnDetails}. 329 | 330 | %% Private functions 331 | get_object_path(Container, ObjectName) when ?IS_CONTAINER(Container), is_binary(ObjectName) -> 332 | Container#cf_container.container_path ++ "/" ++ cferl_lib:url_encode(ObjectName). 333 | 334 | bin_to_int(Bin) when is_binary(Bin) -> 335 | list_to_integer(binary_to_list(Bin)). 336 | 337 | -------------------------------------------------------------------------------- /src/cferl_lib.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Internal utilities 3 | %%% @author David Dossot 4 | %%% @hidden 5 | %%% 6 | %%% See LICENSE for license information. 7 | %%% Copyright (c) 2010 David Dossot 8 | %%% 9 | 10 | -module(cferl_lib). 11 | -author('David Dossot '). 12 | -include("cferl.hrl"). 13 | 14 | -export([error_result/1, 15 | get_int_header/2, get_boolean_header/2, get_binary_header/2, get_string_header/2, 16 | container_query_args_to_string/1, cdn_config_to_headers/1, 17 | object_query_args_to_string/1, 18 | url_encode/1, extract_object_meta_headers/1, binary_headers_to_string/1, 19 | path_to_sub_dirs/1]). 20 | 21 | -define(TEST_HEADERS, [{"int", "123"}, {"bool", "true"}, {"str", "abc"}]). 22 | 23 | 24 | %%% @type cferl_error() = {error, not_found} | {error, unauthorized} | {error, {unexpected_response, Other}}. 25 | -type(cferl_error() :: {error, term()}). 26 | 27 | -export_type([cferl_error/0]). 28 | 29 | -ifdef(TEST). 30 | -include_lib("eunit/include/eunit.hrl"). 31 | -endif. 32 | 33 | %% @doc Authenticate and open connection. 34 | %% @spec error_result(HttpResponse) -> Error 35 | %% HttpResponse = tuple() 36 | %% Error = cferl_error() 37 | error_result({ok, "404", _, _}) -> 38 | {error, not_found}; 39 | error_result({ok, "401", _, _}) -> 40 | {error, unauthorized}; 41 | error_result(Other) -> 42 | {error, {unexpected_response, Other}}. 43 | 44 | %% @doc Get an integer value from a proplist with a case insentive search on key. 45 | %% Return 0 if the key is not found. 46 | %% @spec get_int_header(Key::string(), Proplist::list()) -> integer() 47 | get_int_header(Name, Headers) when is_list(Headers) -> 48 | list_to_int(caseless_get_proplist_value(Name, Headers)). 49 | 50 | %% @doc Get a boolean value from a proplist with a case insentive search on key. 51 | %% Return false if the key is not found. 52 | %% @spec get_boolean_header(Key::string(), Proplist::list()) -> boolean() 53 | get_boolean_header(Name, Headers) when is_list(Headers) -> 54 | list_to_boolean(caseless_get_proplist_value(Name, Headers)). 55 | 56 | %% @doc Get a binary value from a proplist with a case insentive search on key. 57 | %% Return an empty binary if the key is not found. 58 | %% @spec get_binary_header(Key::string(), Proplist::list()) -> binary() 59 | get_binary_header(Name, Headers) when is_list(Headers) -> 60 | list_to_bin(caseless_get_proplist_value(Name, Headers)). 61 | 62 | %% @doc Get a string value from a proplist with a case insentive search on key. 63 | %% Return "" if the key is not found. 64 | %% @spec get_string_header(Key::string(), Proplist::list()) -> string() 65 | get_string_header(Name, Headers) when is_list(Headers) -> 66 | list_to_string(caseless_get_proplist_value(Name, Headers)). 67 | 68 | %% @doc Convert a cf_container_query_args record into an URL encoded query string. 69 | %% @spec container_query_args_to_string(QueryArgs::record()) -> string() 70 | container_query_args_to_string(#cf_container_query_args{marker=Marker, limit=Limit}) -> 71 | QueryElements = 72 | [ 73 | case Marker of 74 | _ when is_binary(Marker) -> "marker=" ++ url_encode(Marker); 75 | _ -> undefined 76 | end, 77 | case Limit of 78 | _ when is_integer(Limit) -> "limit=" ++ integer_to_list(Limit); 79 | _ -> undefined 80 | end 81 | ], 82 | 83 | query_args_to_string(string:join(filter_undefined(QueryElements), "&")). 84 | 85 | %% @doc Convert a cf_container_cdn_config into a list of HTTP headers. 86 | %% @spec cdn_config_to_headers(CdnConfig::record()) -> [{HeaderName, HeaderValue}] 87 | cdn_config_to_headers(#cf_container_cdn_config{ttl=Ttl, user_agent_acl=UaAcl, referrer_acl = RAcl}) -> 88 | CdnConfigHeaders = 89 | [ 90 | case Ttl of 91 | _ when is_integer(Ttl) -> {"X-TTL", integer_to_list(Ttl)}; 92 | _ -> undefined 93 | end, 94 | case UaAcl of 95 | _ when is_binary(UaAcl) -> {"X-User-Agent-ACL", url_encode(UaAcl)}; 96 | _ -> undefined 97 | end, 98 | case RAcl of 99 | _ when is_binary(RAcl) -> {"X-Referrer-ACL", url_encode(RAcl)}; 100 | _ -> undefined 101 | end 102 | ], 103 | 104 | filter_undefined(CdnConfigHeaders). 105 | 106 | %% @doc Convert a cf_object_query_args record into an URL encoded query string. 107 | %% @spec object_query_args_to_string(QueryArgs::record()) -> string() 108 | object_query_args_to_string(#cf_object_query_args{marker=Marker, limit=Limit, prefix=Prefix, path=Path}) -> 109 | QueryElements = 110 | [ 111 | case Marker of 112 | _ when is_integer(Marker) -> "marker=" ++ integer_to_list(Marker); 113 | _ -> undefined 114 | end, 115 | case Limit of 116 | _ when is_integer(Limit) -> "limit=" ++ integer_to_list(Limit); 117 | _ -> undefined 118 | end, 119 | case Prefix of 120 | _ when is_binary(Prefix) -> "prefix=" ++ url_encode(Prefix); 121 | _ -> undefined 122 | end, 123 | case Path of 124 | _ when is_binary(Path) -> "path=" ++ url_encode(Path); 125 | _ -> undefined 126 | end 127 | ], 128 | 129 | query_args_to_string(string:join(filter_undefined(QueryElements), "&")). 130 | 131 | %% @doc Encode a binary URL element into a string. 132 | %% @spec url_encode(Bin::binary()) -> string() 133 | url_encode(Bin) when is_binary(Bin) -> 134 | ibrowse_lib:url_encode(binary_to_list(Bin)). 135 | 136 | %% @doc Extract the HTTP headers that are object metadata, remove their prefix and turn them into binary. 137 | %% @spec extract_object_meta_headers(HttpHeaders::proplist()) -> [{Key::binary(),Value::binary()}] 138 | extract_object_meta_headers(HttpHeaders) when is_list(HttpHeaders) -> 139 | {ok, Re} = re:compile("^" ++ ?OBJECT_META_HEADER_PREFIX, [caseless]), 140 | 141 | MetaHeaders = 142 | lists:filter(fun({Key, _}) -> 143 | re:run(Key, Re) =/= nomatch 144 | end, 145 | HttpHeaders), 146 | [{re:replace(Key, Re, <<>>, [{return, binary}]), list_to_binary(Value)} || {Key, Value} <- MetaHeaders]. 147 | 148 | %% @doc Transform binary keys and values of a proplist into strings. 149 | %% @spec binary_headers_to_string(Headers::proplist()) -> proplist() 150 | binary_headers_to_string(Headers) -> 151 | binary_headers_to_string(Headers, []). 152 | 153 | binary_headers_to_string([], Results) -> 154 | lists:reverse(Results); 155 | binary_headers_to_string([{Key,Value}|Rest], Results) -> 156 | binary_headers_to_string(Rest, [{bin_to_string(Key),bin_to_string(Value)}|Results]). 157 | 158 | %% @doc Breaks a file path into a list of sub-directories. 159 | %% @spec path_to_sub_dirs(Path::path()) -> [Directories::binary()] 160 | %% path() = binary() | string() 161 | path_to_sub_dirs(Path) when is_binary(Path) -> 162 | path_to_sub_dirs(binary_to_list(Path)); 163 | 164 | path_to_sub_dirs(Path) when is_list(Path) -> 165 | PathElements = string:tokens(Path, "/"), 166 | % drop the last element which must be the file name 167 | DirElements = drop_last(PathElements), 168 | dir_elements_to_sub_dirs(DirElements, []). 169 | 170 | dir_elements_to_sub_dirs([], Results) -> 171 | Results; 172 | dir_elements_to_sub_dirs(DirElements, Results) -> 173 | dir_elements_to_sub_dirs(drop_last(DirElements), 174 | [list_to_binary(string:join(DirElements, "/"))|Results]). 175 | 176 | %% Private functions 177 | 178 | caseless_get_proplist_value(Key, Proplist) when is_list(Key), is_list(Proplist) -> 179 | proplists:get_value(string:to_lower(Key), 180 | to_lower_case_keys(Proplist)). 181 | 182 | list_to_int(List) when is_list(List) -> 183 | list_to_integer(List); 184 | list_to_int(_) -> 185 | 0. 186 | 187 | list_to_boolean(List) when is_list(List) -> 188 | string:to_lower(List) == "true"; 189 | list_to_boolean(_) -> 190 | false. 191 | 192 | list_to_bin(List) when is_list(List) -> 193 | list_to_binary(List); 194 | list_to_bin(_) -> 195 | <<>>. 196 | 197 | list_to_string(List) when is_list(List) -> 198 | List; 199 | list_to_string(_) -> 200 | "". 201 | 202 | bin_to_string(Value) when is_binary(Value) -> 203 | binary_to_list(Value); 204 | bin_to_string(Value) when is_list(Value) -> 205 | Value. 206 | 207 | query_args_to_string("") -> 208 | ""; 209 | query_args_to_string(QueryString) -> 210 | "?" ++ QueryString. 211 | 212 | to_lower_case_keys(Proplist) -> 213 | [{string:to_lower(K), V} || {K, V} <- Proplist]. 214 | 215 | filter_undefined(List) when is_list(List) -> 216 | lists:filter(fun(Entry) -> Entry =/= undefined end, List). 217 | 218 | drop_last([]) -> 219 | []; 220 | drop_last(List) when is_list(List) -> 221 | lists:reverse(tl(lists:reverse(List))). 222 | 223 | %% Tests 224 | -ifdef(TEST). 225 | 226 | caseless_get_proplist_value_test() -> 227 | ?assert(undefined == caseless_get_proplist_value("foo", ?TEST_HEADERS)), 228 | ?assert("abc" == caseless_get_proplist_value("STR", ?TEST_HEADERS)), 229 | ok. 230 | 231 | get_int_header_test() -> 232 | ?assert(0 == get_int_header("foo", ?TEST_HEADERS)), 233 | ?assert(123 == get_int_header("INT", ?TEST_HEADERS)), 234 | ok. 235 | 236 | get_boolean_header_test() -> 237 | ?assert(false == get_boolean_header("foo", ?TEST_HEADERS)), 238 | ?assert(true == get_boolean_header("BOOL", ?TEST_HEADERS)), 239 | ok. 240 | 241 | get_binary_header_test() -> 242 | ?assert(<<>> == get_binary_header("foo", ?TEST_HEADERS)), 243 | ?assert(<<"abc">> == get_binary_header("STR", ?TEST_HEADERS)), 244 | ok. 245 | 246 | get_string_header_test() -> 247 | ?assert("" == get_string_header("foo", ?TEST_HEADERS)), 248 | ?assert("abc" == get_string_header("STR", ?TEST_HEADERS)), 249 | ok. 250 | 251 | container_query_args_to_string_test() -> 252 | ?assert("" == container_query_args_to_string(#cf_container_query_args{})), 253 | ?assert("?limit=12" == container_query_args_to_string(#cf_container_query_args{limit=12})), 254 | ?assert("?marker=abc" == container_query_args_to_string(#cf_container_query_args{marker= <<"abc">>})), 255 | ?assert("?marker=def&limit=25" == container_query_args_to_string(#cf_container_query_args{limit=25,marker= <<"def">>})), 256 | ?assert("" == container_query_args_to_string(#cf_container_query_args{marker="bad_value"})), 257 | ?assert("" == container_query_args_to_string(#cf_container_query_args{limit=bad_value})), 258 | ok. 259 | 260 | cdn_config_to_headers_test() -> 261 | ?assert([{"X-TTL", "86400"}] == cdn_config_to_headers(#cf_container_cdn_config{})), 262 | ?assert([{"X-TTL", "3000"}] == cdn_config_to_headers(#cf_container_cdn_config{ttl=3000})), 263 | ?assert([{"X-TTL", "3000"},{"X-User-Agent-ACL", "ua_acl"}] == cdn_config_to_headers(#cf_container_cdn_config{ttl=3000,user_agent_acl= <<"ua_acl">>})), 264 | ?assert([{"X-TTL", "3000"},{"X-User-Agent-ACL", "ua_acl"},{"X-Referrer-ACL","r_acl"}] == cdn_config_to_headers(#cf_container_cdn_config{ttl=3000,user_agent_acl= <<"ua_acl">>,referrer_acl= <<"r_acl">>})), 265 | ?assert([] == cdn_config_to_headers(#cf_container_cdn_config{ttl=bad_value})), 266 | ?assert([{"X-TTL", "86400"}] == cdn_config_to_headers(#cf_container_cdn_config{user_agent_acl=bad_value})), 267 | ?assert([{"X-TTL", "86400"}] == cdn_config_to_headers(#cf_container_cdn_config{referrer_acl=bad_value})), 268 | ok. 269 | 270 | object_query_args_to_string_test() -> 271 | ?assert("" == object_query_args_to_string(#cf_object_query_args{})), 272 | ?assert("?limit=12" == object_query_args_to_string(#cf_object_query_args{limit=12})), 273 | ?assert("?marker=2" == object_query_args_to_string(#cf_object_query_args{marker=2})), 274 | ?assert("?marker=3&limit=25" == object_query_args_to_string(#cf_object_query_args{limit=25,marker=3})), 275 | ?assert("?marker=3&limit=25&prefix=prefoo&path=patbar" == object_query_args_to_string(#cf_object_query_args{prefix= <<"prefoo">>, path= <<"patbar">>, limit=25,marker=3})), 276 | ?assert("" == object_query_args_to_string(#cf_object_query_args{marker="bad_value"})), 277 | ?assert("" == object_query_args_to_string(#cf_object_query_args{limit=bad_value})), 278 | ?assert("" == object_query_args_to_string(#cf_object_query_args{prefix=123})), 279 | ?assert("" == object_query_args_to_string(#cf_object_query_args{path=true})), 280 | ok. 281 | 282 | extract_object_meta_headers_test() -> 283 | TestHeaders = [ 284 | {"Date", "Thu, 07 Jun 2007 20:59:39 GMT"}, 285 | {"Server", "Apache"}, 286 | {"Last-Modified", "Fri, 12 Jun 2007 13:40:18 GMT"}, 287 | {"ETag", "8a964ee2a5e88be344f36c22562a6486"}, 288 | {"Content-Length", "512000"}, 289 | {"Content-Type", "text/plain; charset=UTF-8"}, 290 | {"X-Object-Meta-Meat", "Bacon"}, 291 | {"x-object-meta-fruit", "Orange"}, 292 | {"X-Object-Meta-Veggie", "Turnip"}, 293 | {"x-object-meta-fruit", "Cream"}], 294 | 295 | ExpectedMetas = [ 296 | {<<"Meat">>, <<"Bacon">>}, 297 | {<<"fruit">>, <<"Orange">>}, 298 | {<<"Veggie">>, <<"Turnip">>}, 299 | {<<"fruit">>, <<"Cream">>}], 300 | 301 | ?assert(ExpectedMetas == extract_object_meta_headers(TestHeaders)), 302 | ok. 303 | 304 | binary_headers_to_string_test() -> 305 | ?assert([{"a","1"},{"b","2"}] == binary_headers_to_string([{"a",<<"1">>},{<<"b">>,"2"}])), 306 | ok. 307 | 308 | path_to_sub_dirs_test() -> 309 | ?assert([<<"photo">>,<<"photo/animals">>,<<"photo/animals/dogs">>] 310 | == path_to_sub_dirs("photo/animals/dogs/poodle.jpg")), 311 | ?assert([<<"photo">>,<<"photo/animals">>,<<"photo/animals/dogs">>] 312 | == path_to_sub_dirs(<<"photo/animals/dogs/poodle.jpg">>)), 313 | ?assert([<<"photo">>,<<"photo/animals">>] 314 | == path_to_sub_dirs(<<"photo/animals/dogs">>)), 315 | ?assert([] == path_to_sub_dirs(<<"poodle.jpg">>)), 316 | ?assert([] == path_to_sub_dirs("")), 317 | ?assert([] == path_to_sub_dirs(<<>>)), 318 | ok. 319 | 320 | -endif. 321 | -------------------------------------------------------------------------------- /src/cferl_object.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Handling of a single storage object. 3 | %%% @author David Dossot 4 | %%% 5 | %%% See LICENSE for license information. 6 | %%% Copyright (c) 2010 David Dossot 7 | %%% 8 | 9 | -module(cferl_object). 10 | -author('David Dossot '). 11 | -include("cferl.hrl"). 12 | 13 | %% Public API 14 | -export([name/1, 15 | bytes/1, 16 | last_modified/1, 17 | content_type/1, 18 | etag/1, 19 | metadata/1, 20 | set_metadata/3, 21 | refresh/2, 22 | read_data/2, 23 | read_data/4, 24 | read_data_stream/3, 25 | read_data_stream/5, 26 | write_data/4, 27 | write_data/5, 28 | write_data_stream/5, 29 | write_data_stream/6, 30 | delete/2]). 31 | 32 | %% Exposed for internal usage 33 | -export([new/4]). 34 | 35 | %% @doc Name of the current object. 36 | -spec name(#cf_object{}) -> binary(). 37 | name(Object) when ?IS_OBJECT(Object) -> 38 | ObjectDetails = Object#cf_object.object_details, 39 | ObjectDetails#cf_object_details.name. 40 | 41 | %% @doc Size in bytes of the current object. 42 | -spec bytes(#cf_object{}) -> integer(). 43 | bytes(Object) when ?IS_OBJECT(Object) -> 44 | ObjectDetails = Object#cf_object.object_details, 45 | ObjectDetails#cf_object_details.bytes. 46 | 47 | %% @doc Date and time of the last modification of the current object. 48 | -spec last_modified(#cf_object{}) -> {Date::term(), Time::term()}. 49 | last_modified(Object) when ?IS_OBJECT(Object) -> 50 | ObjectDetails = Object#cf_object.object_details, 51 | ObjectDetails#cf_object_details.last_modified. 52 | 53 | %% @doc Content type of the current object. 54 | -spec content_type(#cf_object{}) -> binary(). 55 | content_type(Object) when ?IS_OBJECT(Object) -> 56 | ObjectDetails = Object#cf_object.object_details, 57 | ObjectDetails#cf_object_details.content_type. 58 | 59 | %% @doc Etag of the current object. 60 | -spec etag(#cf_object{}) -> binary(). 61 | etag(Object) when ?IS_OBJECT(Object) -> 62 | ObjectDetails = Object#cf_object.object_details, 63 | ObjectDetails#cf_object_details.etag. 64 | 65 | %% @doc Meta-data of the current object. 66 | %% The "X-Meta-Object-" prefix is stripped off the underlying HTTP header name. 67 | -spec metadata(#cf_object{}) -> [{Key::binary(),Value::binary()}]. 68 | metadata(Object) when ?IS_OBJECT(Object) -> 69 | HttpHeaders = Object#cf_object.http_headers, 70 | cferl_lib:extract_object_meta_headers(HttpHeaders). 71 | 72 | %% @doc Set meta-data for the current object. All pre-existing meta-data is replaced by the new one. 73 | %% The "X-Meta-Object-" prefix will be automatically prepended to form the HTTP header names. 74 | -spec set_metadata(#cf_connection{}, #cf_object{}, [{Key::binary(),Value::binary()}]) -> ok | cferl_lib:cferl_error(). 75 | set_metadata(Connection, Object, MetaData) 76 | when ?IS_CONNECTION(Connection), ?IS_OBJECT(Object), is_list(MetaData) -> 77 | MetaHttpHeaders = [{<>, Value} || {Key, Value} <- MetaData], 78 | Result = cferl_connection:send_storage_request(Connection, post, Object#cf_object.object_path, MetaHttpHeaders, raw), 79 | set_metadata_result(Result). 80 | 81 | set_metadata_result({ok, "202", _, _}) -> 82 | ok; 83 | set_metadata_result(Other) -> 84 | cferl_lib:error_result(Other). 85 | 86 | %% @doc Refresh the current object reference, including all the meta information. 87 | -spec refresh(#cf_connection{}, #cf_object{}) -> {ok, #cf_object{}} | cferl_lib:cferl_error(). 88 | refresh(Connection, Object) when ?IS_CONNECTION(Connection), ?IS_OBJECT(Object) -> 89 | cferl_container:get_object(Connection, Object#cf_object.container, name(Object)). 90 | 91 | %% @doc Read the data stored for the current object. 92 | -spec read_data(#cf_connection{}, #cf_object{}) -> {ok, Data::binary()} | cferl_lib:cferl_error(). 93 | read_data(Connection, Object) -> 94 | do_read_data(Connection, Object, []). 95 | 96 | %% @doc Read the data stored for the current object, reading 'size' bytes from the 'offset'. 97 | -spec read_data(#cf_connection{}, #cf_object{}, Offset::integer(), Size::integer()) -> {ok, Data::binary()} | cferl_lib:cferl_error(). 98 | read_data(Connection, Object, Offset, Size) when is_integer(Offset), is_integer(Size) -> 99 | do_read_data(Connection, Object, [data_range_header(Offset, Size)]). 100 | 101 | do_read_data(Connection, Object, RequestHeaders) 102 | when ?IS_CONNECTION(Connection), ?IS_OBJECT(Object), is_list(RequestHeaders) -> 103 | Result = cferl_connection:send_storage_request(Connection, get, Object#cf_object.object_path, RequestHeaders, raw), 104 | do_read_data_result(Result). 105 | 106 | do_read_data_result({ok, ResponseCode, _, ResponseBody}) 107 | when ResponseCode =:= "200"; ResponseCode =:= "206" -> 108 | 109 | {ok, ResponseBody}; 110 | 111 | do_read_data_result(Other) -> 112 | cferl_lib:error_result(Other). 113 | 114 | %% @doc Read the data stored for the current object and feed by chunks it into a function. 115 | %% The function of arity 1 will receive: {error, Cause::term()} | {ok, Data:binary()} | eof 116 | -spec read_data_stream(#cf_connection{}, #cf_object{}, DataFun::function()) -> ok | cferl_lib:cferl_error(). 117 | read_data_stream(Connection, Object, DataFun) -> 118 | do_read_data_stream(Connection, Object, DataFun, []). 119 | 120 | %% @doc Read the data stored for the current object, reading 'size' bytes from the 'offset', and feed by chunks it into a function. 121 | %% The function of arity 1 will receive: {error, Cause::term()} | {ok, Data:binary()} | eof 122 | -spec read_data_stream(#cf_connection{}, #cf_object{}, DataFun::function(), Offset::integer(), Size::integer()) -> ok | cferl_lib:cferl_error(). 123 | read_data_stream(Connection, Object, DataFun, Offset, Size) when is_integer(Offset), is_integer(Size) -> 124 | do_read_data_stream(Connection, Object, DataFun, [data_range_header(Offset, Size)]). 125 | 126 | do_read_data_stream(Connection, Object, DataFun, RequestHeaders) 127 | when ?IS_CONNECTION(Connection), ?IS_OBJECT(Object), is_function(DataFun, 1), is_list(RequestHeaders) -> 128 | Result = cferl_connection:send_storage_request(Connection, get, Object#cf_object.object_path, RequestHeaders, DataFun), 129 | do_read_data_stream_result(Result). 130 | 131 | do_read_data_stream_result({ibrowse_req_id, _Req_id}) -> 132 | ok; 133 | do_read_data_stream_result(Other) -> 134 | cferl_lib:error_result(Other). 135 | 136 | data_range_header(Offset, Size) when is_integer(Offset), is_integer(Size) -> 137 | {"Range", io_lib:format("bytes=~B-~B", [Offset, Offset+Size-1])}. 138 | 139 | %% @doc Write data for the current object. 140 | -spec write_data(#cf_connection{}, #cf_object{}, Data::binary(), ContentType::binary()) -> 141 | ok | {error, invalid_content_length} | {error, mismatched_etag} | cferl_lib:cferl_error(). 142 | write_data(Connection, Object, Data, ContentType) when is_binary(Data), is_binary(ContentType) -> 143 | write_data(Connection, Object, Data, ContentType, []). 144 | 145 | %% @doc Write data for the current object. 146 | -spec write_data(#cf_connection{}, #cf_object{}, Data::binary(), ContentType::binary(), [{binary(), binary()}]) -> 147 | ok | {error, invalid_content_length} | {error, mismatched_etag} | cferl_lib:cferl_error(). 148 | write_data(Connection, Object, Data, ContentType, RequestHeaders) 149 | when is_binary(Data), is_binary(ContentType), is_list(RequestHeaders) -> 150 | do_write_data(Connection, Object, Data, ContentType, RequestHeaders). 151 | 152 | %% @doc Write streamed data for the current object. 153 | %% The data generating function must be of arity 0 and return {ok, Data::binary()} | eof. 154 | -spec write_data_stream(#cf_connection{}, #cf_object{}, DataFun::function(), ContentType::binary(), ContentLength::integer()) -> 155 | ok | {error, invalid_content_length} | {error, mismatched_etag} | cferl_lib:cferl_error(). 156 | write_data_stream(Connection, Object, DataFun, ContentType, ContentLength) 157 | when is_function(DataFun, 0), is_binary(ContentType), is_integer(ContentLength) -> 158 | write_data_stream(Connection, Object, DataFun, ContentType, ContentLength, []). 159 | 160 | %% @doc Write streamed data for the current object. 161 | %% The data generating function must be of arity 0 and return {ok, Data::binary()} | eof. 162 | -spec write_data_stream(#cf_connection{}, #cf_object{}, DataFun::function(), 163 | ContentType::binary(), ContentLength::integer(), RequestHeaders::[{Name::binary(), Value::binary()}]) -> 164 | ok | {error, invalid_content_length} | {error, mismatched_etag} | cferl_lib:cferl_error(). 165 | write_data_stream(Connection, Object, DataFun, ContentType, ContentLength, RequestHeaders) 166 | when is_function(DataFun, 0), is_binary(ContentType), is_integer(ContentLength), is_list(RequestHeaders) -> 167 | do_write_data(Connection, Object, DataFun, ContentType, 168 | [{<<"Content-Length">>, list_to_binary(integer_to_list(ContentLength))} | RequestHeaders]). 169 | 170 | do_write_data(Connection, Object, DataSource, ContentType, RequestHeaders) 171 | when ?IS_CONNECTION(Connection), ?IS_OBJECT(Object) -> 172 | Result = cferl_connection:send_storage_request(Connection, 173 | put, 174 | Object#cf_object.object_path, 175 | [{"Content-Type", ContentType}|RequestHeaders], 176 | DataSource, 177 | raw), 178 | do_write_data_result(Result). 179 | 180 | do_write_data_result({ok, "201", _, _}) -> 181 | ok; 182 | do_write_data_result({ok, "412", _, _}) -> 183 | {error, invalid_content_length}; 184 | do_write_data_result({ok, "422", _, _}) -> 185 | {error, mismatched_etag}; 186 | do_write_data_result(Other) -> 187 | cferl_lib:error_result(Other). 188 | 189 | %% @doc Delete the current storage object. 190 | -spec delete(#cf_connection{}, #cf_object{}) -> ok | cferl_lib:cferl_error(). 191 | delete(Connection, Object) when ?IS_CONNECTION(Connection), ?IS_OBJECT(Object) -> 192 | cferl_container:delete_object(Connection, Object#cf_object.container, name(Object)). 193 | 194 | -spec new(#cf_container{}, #cf_object_details{}, string(), [{string(), string()}]) -> #cf_object{}. 195 | new(Container, ObjectDetails, ObjectPath, HttpHeaders) -> 196 | #cf_object{container = Container, 197 | object_details = ObjectDetails, 198 | object_path = ObjectPath, 199 | http_headers = HttpHeaders 200 | }. 201 | -------------------------------------------------------------------------------- /test/cferl_integration_tests.erl: -------------------------------------------------------------------------------- 1 | %%% 2 | %%% @doc Integration tests and demo code generation. 3 | %%% @author David Dossot 4 | %%% @author Tilman Holschuh 5 | %%% 6 | %%% See LICENSE for license information. 7 | %%% Copyright (c) 2010 David Dossot 8 | %%% 9 | 10 | -module(cferl_integration_tests). 11 | -author('David Dossot '). 12 | -include("cferl.hrl"). 13 | 14 | -export([start/0, data_producer_loop/1]). 15 | -define(PRINT_CODE(Code), io:format(" ~s~n", [Code])). 16 | -define(PRTFM_CODE(Format, Data), ?PRINT_CODE(io_lib:format(Format, Data))). 17 | -define(PRINT_CALL(Call), 18 | io:format(" ~s.~n", [re:replace(??Call, " ", "", [global])]), 19 | Call). 20 | 21 | start() -> 22 | application:start(crypto), 23 | application:start(public_key), 24 | application:start(ssl), 25 | application:start(ibrowse), 26 | 27 | {ok, [Username]} = io:fread("Username : ", "~s"), 28 | {ok, [ApiKey]} = io:fread("API Key : ", "~s"), 29 | io:format("~n"), 30 | run_tests(Username, ApiKey), 31 | init:stop(). 32 | 33 | %% Tests 34 | run_tests(Username, ApiKey) -> 35 | CloudFiles = connect_test(Username, ApiKey), 36 | print_account_info(CloudFiles), 37 | container_tests(CloudFiles), 38 | ok. 39 | 40 | connect_test(Username, ApiKey) -> 41 | {error, unauthorized} = cferl:connect("_fake_user_name", "_fake_api_key"), 42 | ?PRINT_CODE("# Connect to Cloud Files (warning: cache/use CloudFiles for a maximum of 24 hours!)"), 43 | ?PRINT_CALL({ok, CloudFiles} = cferl:connect(Username, ApiKey)), 44 | ?PRINT_CODE(""), 45 | CloudFiles. 46 | 47 | print_account_info(CloudFiles) -> 48 | ?PRINT_CODE("# Retrieve the account information record"), 49 | ?PRINT_CALL({ok, Info} = cferl_connection:get_account_info(CloudFiles)), 50 | ?PRTFM_CODE("Info = #cf_account_info{bytes_used=~B, container_count=~B}", 51 | [Info#cf_account_info.bytes_used, Info#cf_account_info.container_count]), 52 | ?PRINT_CODE(""). 53 | 54 | container_tests(CloudFiles) -> 55 | ?PRINT_CODE("# Retrieve names of all existing containers (within the limits imposed by Cloud Files server)"), 56 | ?PRINT_CALL({ok, Names} = cferl_connection:get_containers_names(CloudFiles)), 57 | ?PRINT_CODE(""), 58 | 59 | ?PRINT_CODE("# Retrieve names of a maximum of 3 existing containers"), 60 | ?PRINT_CALL({ok, ThreeNamesMax} = cferl_connection:get_containers_names(CloudFiles, #cf_container_query_args{limit=3})), 61 | ?PRINT_CODE(""), 62 | 63 | % retrieve 0 container 64 | {ok, []} = cferl_connection:get_containers_details(CloudFiles, #cf_container_query_args{limit=0}), 65 | 66 | ?PRINT_CODE("# Retrieve names of all containers currently CDN activated"), 67 | ?PRINT_CALL({ok, CurrentPublicNames} = cferl_connection:get_public_containers_names(CloudFiles, active)), 68 | ?PRINT_CODE(""), 69 | 70 | ?PRINT_CODE("# Retrieve names of all containers that are currently or have been CDN activated"), 71 | ?PRINT_CALL({ok, AllTimePublicNames} = cferl_connection:get_public_containers_names(CloudFiles, all_time)), 72 | ?PRINT_CODE(""), 73 | 74 | ?PRINT_CODE("# Retrieve details for all existing containers (within the server limits)"), 75 | ?PRINT_CALL({ok, ContainersDetails} = cferl_connection:get_containers_details(CloudFiles)), 76 | ?PRINT_CODE(""), 77 | 78 | ?PRINT_CODE("# ContainersDetails is a list of #cf_container_details records"), 79 | ?PRINT_CALL([ContainerDetails|_]=ContainersDetails), 80 | ?PRTFM_CODE("ContainerDetails = #cf_container_details{name=~p, bytes=~B, count=~B}", 81 | [ContainerDetails#cf_container_details.name, 82 | ContainerDetails#cf_container_details.bytes, 83 | ContainerDetails#cf_container_details.count]), 84 | ?PRINT_CODE(""), 85 | 86 | ?PRINT_CODE("# Retrieve details for a maximum of 5 containers whose names start at cf"), 87 | ?PRINT_CALL({ok, CfContainersDetails} = cferl_connection:get_containers_details(CloudFiles, #cf_container_query_args{marker= <<"cf">>, limit=5})), 88 | ?PRINT_CODE(""), 89 | 90 | ?PRINT_CODE("# Get a container reference by name"), 91 | ?PRINT_CALL({ok, Container} = cferl_connection:get_container(CloudFiles, ContainerDetails#cf_container_details.name)), 92 | ?PRINT_CODE(""), 93 | 94 | ?PRINT_CODE("# Get container details from its reference"), 95 | ?PRINT_CALL(ContainerName = cferl_container:name(Container)), 96 | ?PRINT_CALL(ContainerBytes = cferl_container:bytes(Container)), 97 | ?PRINT_CALL(ContainerSize = cferl_container:count(Container)), 98 | ?PRINT_CALL(ContainerIsEmpty = cferl_container:is_empty(Container)), 99 | ?PRINT_CODE(""), 100 | ?PRTFM_CODE("# -> Name: ~p - Bytes: ~p - Size: ~p - IsEmpty: ~p", 101 | [ContainerName, ContainerBytes, ContainerSize, ContainerIsEmpty]), 102 | ?PRINT_CODE(""), 103 | 104 | NewContainerName = make_new_container_name(), 105 | 106 | ?PRINT_CODE("# Check a container's existence"), 107 | ?PRINT_CALL(false = cferl_connection:container_exists(CloudFiles, NewContainerName)), 108 | ?PRINT_CODE(""), 109 | 110 | ?PRINT_CODE("# Create a new container"), 111 | ?PRINT_CALL({ok, NewContainer} = cferl_connection:create_container(CloudFiles, NewContainerName)), 112 | ?PRINT_CODE(""), 113 | ?PRINT_CALL(true = cferl_connection:container_exists(CloudFiles, NewContainerName)), 114 | ?PRINT_CODE(""), 115 | 116 | ?PRINT_CODE("Check attributes of this newly created container"), 117 | ?PRINT_CALL(NewContainerName = cferl_container:name(NewContainer)), 118 | ?PRINT_CALL(0 = cferl_container:bytes(NewContainer)), 119 | ?PRINT_CALL(0 = cferl_container:count(NewContainer)), 120 | ?PRINT_CALL(true = cferl_container:is_empty(NewContainer)), 121 | ?PRINT_CALL(false = cferl_container:is_public(NewContainer)), 122 | ?PRINT_CALL(<<>> = cferl_container:cdn_url(NewContainer)), 123 | ?PRINT_CALL(0 = cferl_container:cdn_ttl(NewContainer)), 124 | ?PRINT_CALL(false = cferl_container:log_retention(NewContainer)), 125 | ?PRINT_CODE(""), 126 | 127 | ?PRINT_CODE("# Make the container public on the CDN (using the default TTL and ACLs)"), 128 | ?PRINT_CALL(ok = cferl_container:make_public(CloudFiles, NewContainer)), 129 | ?PRINT_CODE(""), 130 | 131 | ?PRINT_CODE("# Activate log retention on the new container"), 132 | ?PRINT_CALL(ok = cferl_container:set_log_retention(CloudFiles, NewContainer, true)), 133 | ?PRINT_CODE(""), 134 | 135 | ?PRINT_CODE("# Refresh an existing container and check its attributes"), 136 | ?PRINT_CALL({ok, RefreshedContainer} = cferl_container:refresh(CloudFiles, NewContainer)), 137 | ?PRINT_CALL(true = cferl_container:is_public(RefreshedContainer)), 138 | ?PRINT_CODE(""), 139 | ?PRINT_CALL(io:format(" ~s~n~n", [cferl_container:cdn_url(RefreshedContainer)])), 140 | ?PRINT_CALL(86400 = cferl_container:cdn_ttl(RefreshedContainer)), 141 | ?PRINT_CALL(true = cferl_container:log_retention(RefreshedContainer)), 142 | ?PRINT_CODE(""), 143 | 144 | % ensure container has no object name 145 | {ok, []} = cferl_container:get_objects_names(CloudFiles, RefreshedContainer), 146 | {ok, []} = cferl_container:get_objects_names(CloudFiles, RefreshedContainer, #cf_object_query_args{limit=10}), 147 | 148 | % ensure container has no object details 149 | {ok, []} = cferl_container:get_objects_details(CloudFiles, RefreshedContainer), 150 | {ok, []} = cferl_container:get_objects_details(CloudFiles, RefreshedContainer, #cf_object_query_args{limit=10}), 151 | 152 | ?PRINT_CALL(ObjectName = <<"test.xml">>), 153 | 154 | % ensure new object doesn't exist 155 | false = cferl_container:object_exists(CloudFiles, RefreshedContainer, ObjectName), 156 | 157 | ?PRINT_CODE("# Create an object *reference*, nothing is sent to the server yet"), 158 | ?PRINT_CALL({ok, Object} = cferl_container:create_object(CloudFiles, RefreshedContainer, ObjectName)), 159 | ?PRINT_CODE("# As expected, it doesn't exist yet"), 160 | ?PRINT_CALL(false = cferl_container:object_exists(CloudFiles, RefreshedContainer, ObjectName)), 161 | ?PRINT_CODE(""), 162 | 163 | ?PRINT_CODE("# Write data in the object, which creates it on the server"), 164 | ?PRINT_CALL(ok = cferl_object:write_data(CloudFiles, Object, <<"">>, <<"application/xml">>)), 165 | ?PRINT_CODE("# Now it exists!"), 166 | ?PRINT_CALL(true = cferl_container:object_exists(CloudFiles, RefreshedContainer, ObjectName)), 167 | ?PRINT_CODE("# And trying to re-create it just returns it"), 168 | ?PRINT_CALL({ok, ExistingObject} = cferl_container:create_object(CloudFiles, RefreshedContainer, ObjectName)), 169 | ?PRINT_CODE(""), 170 | 171 | ?PRINT_CODE("# Set custom meta-data on it"), 172 | ?PRINT_CALL(ok = cferl_object:set_metadata(CloudFiles, Object, [{<<"Key123">>, <<"my 123 Value">>}])), 173 | ?PRINT_CODE(""), 174 | 175 | ?PRINT_CODE("# An existing object can be accessed directly from its container"), 176 | ?PRINT_CALL({ok, GotObject} = cferl_container:get_object(CloudFiles, RefreshedContainer, ObjectName)), 177 | ?PRINT_CODE(""), 178 | 179 | ?PRINT_CODE("# Object names and details can be queried"), 180 | ?PRINT_CALL({ok, [ObjectName]} = cferl_container:get_objects_names(CloudFiles, RefreshedContainer)), 181 | ?PRINT_CALL({ok, [ObjectName]} = cferl_container:get_objects_names(CloudFiles, RefreshedContainer, #cf_object_query_args{limit=1})), 182 | ?PRINT_CALL({ok, [ObjectDetails]} = cferl_container:get_objects_details(CloudFiles, RefreshedContainer)), 183 | ?PRTFM_CODE("ObjectDetails = #cf_object_details{name=~p, bytes=~B, last_modified=~1024p, content_type=~s, etag=~s}", 184 | [ObjectDetails#cf_object_details.name, 185 | ObjectDetails#cf_object_details.bytes, 186 | ObjectDetails#cf_object_details.last_modified, 187 | ObjectDetails#cf_object_details.content_type, 188 | ObjectDetails#cf_object_details.etag]), 189 | ?PRINT_CODE(""), 190 | 191 | ?PRINT_CODE("# Read the whole data"), 192 | ?PRINT_CALL({ok, <<"">>} = cferl_object:read_data(CloudFiles, Object)), 193 | ?PRINT_CODE("# Read the data with an offset and a size"), 194 | ?PRINT_CALL({ok, <<"test">>} = cferl_object:read_data(CloudFiles, Object, 1, 4)), 195 | ?PRINT_CODE(""), 196 | 197 | ?PRINT_CODE("# Refresh the object so its attributes and metadata are up to date"), 198 | ?PRINT_CALL({ok, RefreshedObject} = cferl_object:refresh(CloudFiles, Object)), 199 | ?PRINT_CODE(""), 200 | 201 | ?PRINT_CODE("# Get object attributes"), 202 | ?PRINT_CALL(ObjectName = cferl_object:name(RefreshedObject)), 203 | ?PRINT_CALL(8 = cferl_object:bytes(RefreshedObject)), 204 | ?PRINT_CALL({{D,M,Y},{H,Mi,S}} = cferl_object:last_modified(RefreshedObject)), 205 | ?PRINT_CALL(<<"application/xml">> = cferl_object:content_type(RefreshedObject)), 206 | ?PRINT_CALL(Etag = cferl_object:etag(RefreshedObject)), 207 | ?PRINT_CODE(""), 208 | 209 | ?PRINT_CODE("# Get custom meta-data"), 210 | ?PRINT_CALL([{<<"Key123">>, <<"my 123 Value">>}] = cferl_object:metadata(RefreshedObject)), 211 | ?PRINT_CODE(""), 212 | 213 | ?PRINT_CODE("# Delete the object"), 214 | ?PRINT_CALL(ok = cferl_object:delete(CloudFiles, RefreshedObject)), 215 | ?PRINT_CODE(""), 216 | 217 | ?PRINT_CODE("# Data can be streamed to the server from a generating function"), 218 | ?PRINT_CALL({ok, StreamedObject} = cferl_container:create_object(CloudFiles, RefreshedContainer, <<"streamed.txt">>)), 219 | 220 | DataPid = spawn_data_producer(), 221 | WriteDataFun = 222 | fun() -> 223 | DataPid ! {self(), get_data}, 224 | receive 225 | Data -> Data 226 | after 5000 -> eof 227 | end 228 | end, 229 | 230 | ?PRINT_CALL(cferl_object:write_data_stream(CloudFiles, StreamedObject, WriteDataFun, <<"text/plain">>, 1000)), 231 | ?PRINT_CODE(""), 232 | 233 | ?PRINT_CODE("# Data can be streamed from the server to a receiving function"), 234 | ReadDataFun = fun(_Data) -> ok end, 235 | ?PRINT_CALL(ok = cferl_object:read_data_stream(CloudFiles, StreamedObject, ReadDataFun)), 236 | ?PRINT_CODE(""), 237 | 238 | ?PRINT_CODE("# Create all the directory elements for a particular object path"), 239 | ?PRINT_CALL(ok = cferl_container:ensure_dir(CloudFiles, RefreshedContainer, <<"photos/plants/fern.jpg">>)), 240 | ?PRINT_CALL(true = cferl_container:object_exists(CloudFiles, RefreshedContainer, <<"photos">>)), 241 | ?PRINT_CALL(true = cferl_container:object_exists(CloudFiles, RefreshedContainer, <<"photos/plants">>)), 242 | ?PRINT_CODE(""), 243 | 244 | % delete the streamed object 245 | ok = cferl_container:delete_object(CloudFiles, RefreshedContainer, <<"streamed.txt">>), 246 | 247 | % delete the path elements 248 | ok = cferl_container:delete_object(CloudFiles, RefreshedContainer, <<"photos">>), 249 | ok = cferl_container:delete_object(CloudFiles, RefreshedContainer, <<"photos/plants">>), 250 | 251 | % ensure log retention can be stopped 252 | ok = cferl_container:set_log_retention(CloudFiles, RefreshedContainer, false), 253 | 254 | ?PRINT_CODE("# Make the container private"), 255 | ?PRINT_CALL(ok = cferl_container:make_private(CloudFiles, RefreshedContainer)), 256 | ?PRINT_CODE(""), 257 | 258 | ?PRINT_CODE("# Delete an existing container (must be empty)"), 259 | ?PRINT_CALL(ok = cferl_container:delete(CloudFiles, RefreshedContainer)), 260 | ?PRINT_CODE(""), 261 | 262 | % ensure deleting missing container is properly handled 263 | {error, not_found} = cferl_container:delete(CloudFiles, NewContainer), 264 | 265 | ok. 266 | 267 | make_new_container_name() -> 268 | {ok, HostName} = inet:gethostname(), 269 | {M,S,U} = now(), 270 | ContainerName = "cferl_int_test" 271 | ++ integer_to_list(M) 272 | ++ "-" 273 | ++ integer_to_list(S) 274 | ++ "-" 275 | ++ integer_to_list(U) 276 | ++ "-" 277 | ++ HostName, 278 | list_to_binary(ContainerName). 279 | 280 | spawn_data_producer() -> 281 | spawn(?MODULE, data_producer_loop, [0]). 282 | 283 | data_producer_loop(Index) -> 284 | receive 285 | {Pid, get_data} when Index < 10 -> 286 | Pid ! {ok, string:copies(integer_to_list(Index), 100)}, 287 | data_producer_loop(Index + 1); 288 | 289 | {Pid, get_data} -> 290 | Pid ! eof; 291 | 292 | _ -> 293 | ok 294 | end. 295 | 296 | -------------------------------------------------------------------------------- /test/elog.config: -------------------------------------------------------------------------------- 1 | [{sasl, [{sasl_error_logger, false}]}]. --------------------------------------------------------------------------------