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}]}].
--------------------------------------------------------------------------------