cowboy_http_static.erl 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. %% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. %% @doc Static resource handler.
  15. %%
  16. %% This built in HTTP handler provides a simple file serving capability for
  17. %% cowboy applications. It should be considered an experimental feature because
  18. %% of it's dependency on the experimental REST handler. It's recommended to be
  19. %% used for small or temporary environments where it is not preferrable to set
  20. %% up a second server just to serve files.
  21. %%
  22. %% If this handler is used the Erlang node running the cowboy application must
  23. %% be configured to use an async thread pool. This is configured by adding the
  24. %% `+A $POOL_SIZE' argument to the `erl' command used to start the node. See
  25. %% <a href="http://erlang.org/pipermail/erlang-bugs/2012-January/002720.html">
  26. %% this reply</a> from the OTP team to erlang-bugs
  27. %%
  28. %% == Base configuration ==
  29. %%
  30. %% The handler must be configured with a request path prefix to serve files
  31. %% under and the path to a directory to read files from. The request path prefix
  32. %% is defined in the path pattern of the cowboy dispatch rule for the handler.
  33. %% The request path pattern must end with a `...' token.
  34. %% The directory path can be set to either an absolute or relative path in the
  35. %% form of a list or binary string representation of a file system path. A list
  36. %% of binary path segments, as is used throughout cowboy, is also a valid
  37. %% directory path.
  38. %%
  39. %% The directory path can also be set to a relative path within the `priv/'
  40. %% directory of an application. This is configured by setting the value of the
  41. %% directory option to a tuple of the form `{priv_dir, Application, Relpath}'.
  42. %%
  43. %% ==== Examples ====
  44. %% ```
  45. %% %% Serve files from /var/www/ under http://example.com/static/
  46. %% {[<<"static">>, '...'], cowboy_http_static,
  47. %% [{directory, "/var/www"}]}
  48. %%
  49. %% %% Serve files from the current working directory under http://example.com/static/
  50. %% {[<<"static">>, '...'], cowboy_http_static,
  51. %% [{directory, <<"./">>}]}
  52. %%
  53. %% %% Serve files from cowboy/priv/www under http://example.com/
  54. %% {['...'], cowboy_http_static,
  55. %% [{directory, {priv_dir, cowboy, [<<"www">>]}}]}
  56. %% '''
  57. %%
  58. %% == Content type configuration ==
  59. %%
  60. %% By default the content type of all static resources will be set to
  61. %% `application/octet-stream'. This can be overriden by supplying a list
  62. %% of filename extension to mimetypes pairs in the `mimetypes' option.
  63. %% The filename extension should be a binary string including the leading dot.
  64. %% The mimetypes must be of a type that the `cowboy_http_rest' protocol can
  65. %% handle.
  66. %%
  67. %% The <a href="https://github.com/spawngrid/mimetypes">spawngrid/mimetypes</a>
  68. %% application, or an arbitrary function accepting the path to the file being
  69. %% served, can also be used to generate the list of content types for a static
  70. %% file resource. The function used must accept an additional argument after
  71. %% the file path argument.
  72. %%
  73. %% ==== Example ====
  74. %% ```
  75. %% %% Use a static list of content types.
  76. %% {[<<"static">>, '...'], cowboy_http_static,
  77. %% [{directory, {priv_dir, cowboy, []}},
  78. %% {mimetypes, [
  79. %% {<<".css">>, [<<"text/css">>]},
  80. %% {<<".js">>, [<<"application/javascript">>]}]}]}
  81. %%
  82. %% %% Use the default database in the mimetypes application.
  83. %% {[<<"static">>, '...', cowboy_http_static,
  84. %% [{directory, {priv_dir, cowboy, []}},
  85. %% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]]}
  86. %% '''
  87. %%
  88. %% == ETag Header Function ==
  89. %%
  90. %% The default behaviour of the static file handler is to not generate ETag
  91. %% headers. This is because generating ETag headers based on file metadata
  92. %% causes different servers in a cluster to generate different ETag values for
  93. %% the same file unless the metadata is also synced. Generating strong ETags
  94. %% based on the contents of a file is currently out of scope for this module.
  95. %%
  96. %% The default behaviour can be overridden to generate an ETag header based on
  97. %% a combination of the file path, file size, inode and mtime values. If the
  98. %% option value is a non-empty list of attribute names tagged with `attributes'
  99. %% a hex encoded checksum of each attribute specified is included in the value
  100. %% of the the ETag header. If the list of attribute names is empty no ETag
  101. %% header is generated.
  102. %%
  103. %% If a strong ETag is required a user defined function for generating the
  104. %% header value can be supplied. The function must accept a proplist of the
  105. %% file attributes as the first argument and a second argument containing any
  106. %% additional data that the function requires. The function must return a term
  107. %% of the type `{weak | strong, binary()}' or `undefined'.
  108. %%
  109. %% ==== Examples ====
  110. %% ```
  111. %% %% A value of default is equal to not specifying the option.
  112. %% {[<<"static">>, '...', cowboy_http_static,
  113. %% [{directory, {priv_dir, cowboy, []}},
  114. %% {etag, default}]]}
  115. %%
  116. %% %% Use all avaliable ETag function arguments to generate a header value.
  117. %% {[<<"static">>, '...', cowboy_http_static,
  118. %% [{directory, {priv_dir, cowboy, []}},
  119. %% {etag, {attributes, [filepath, filesize, inode, mtime]}}]]}
  120. %%
  121. %% %% Use a user defined function to generate a strong ETag header value.
  122. %% {[<<"static">>, '...', cowboy_http_static,
  123. %% [{directory, {priv_dir, cowboy, []}},
  124. %% {etag, {fun generate_strong_etag/2, strong_etag_extra}}]]}
  125. %%
  126. %% generate_strong_etag(Arguments, strong_etag_extra) ->
  127. %% {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
  128. %% {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
  129. %% {_, _INode} = lists:keyfind(inode, 1, Arguments),
  130. %% {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
  131. %% ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
  132. %% [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
  133. %% {strong, iolist_to_binary(Checksum)}.
  134. %% '''
  135. %%
  136. %% == File configuration ==
  137. %%
  138. %% If the file system path being served does not share a common suffix with
  139. %% the request path it is possible to override the file path using the `file'
  140. %% option. The value of this option is expected to be a relative path within
  141. %% the static file directory specified using the `directory' option.
  142. %% The path must be in the form of a list or binary string representation of a
  143. %% file system path. A list of binary path segments, as is used throughout
  144. %% cowboy, is also a valid.
  145. %%
  146. %% When the `file' option is used the same file will be served for all requests
  147. %% matching the cowboy dispatch fule for the handler. It is not necessary to
  148. %% end the request path pattern with a `...' token because the request path
  149. %% will not be used to determine which file to serve from the static directory.
  150. %%
  151. %% === Examples ===
  152. %%
  153. %% ```
  154. %% %% Serve cowboy/priv/www/index.html as http://example.com/
  155. %% {[], cowboy_http_static,
  156. %% [{directory, {priv_dir, cowboy, [<<"www">>]}}
  157. %% {file, <<"index.html">>}]}
  158. %%
  159. %% %% Serve cowboy/priv/www/page.html under http://example.com/*/page
  160. %% {['*', <<"page">>], cowboy_http_static,
  161. %% [{directory, {priv_dir, cowboy, [<<"www">>]}}
  162. %% {file, <<"page.html">>}]}.
  163. %%
  164. %% %% Always serve cowboy/priv/www/other.html under http://example.com/other
  165. %% {[<<"other">>, '...'], cowboy_http_static,
  166. %% [{directory, {priv_dir, cowboy, [<<"www">>]}}
  167. %% {file, "other.html"}]}
  168. %% '''
  169. -module(cowboy_http_static).
  170. %% include files
  171. -include("http.hrl").
  172. -include_lib("kernel/include/file.hrl").
  173. %% cowboy_http_protocol callbacks
  174. -export([init/3]).
  175. %% cowboy_http_rest callbacks
  176. -export([rest_init/2, allowed_methods/2, malformed_request/2,
  177. resource_exists/2, forbidden/2, last_modified/2, generate_etag/2,
  178. content_types_provided/2, file_contents/2]).
  179. %% internal
  180. -export([path_to_mimetypes/2]).
  181. %% types
  182. -type dirpath() :: string() | binary() | [binary()].
  183. -type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
  184. -type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
  185. -type etagarg() :: {filepath, binary()} | {mtime, calendar:datetime()}
  186. | {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
  187. %% handler state
  188. -record(state, {
  189. filepath :: binary() | error,
  190. fileinfo :: {ok, #file_info{}} | {error, _} | error,
  191. mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
  192. etag_fun :: {fun(([etagarg()], T) ->
  193. undefined | {strong | weak, binary()}), T}}).
  194. %% @private Upgrade from HTTP handler to REST handler.
  195. init({_Transport, http}, _Req, _Opts) ->
  196. {upgrade, protocol, cowboy_http_rest}.
  197. %% @private Set up initial state of REST handler.
  198. -spec rest_init(#http_req{}, list()) -> {ok, #http_req{}, #state{}}.
  199. rest_init(Req, Opts) ->
  200. Directory = proplists:get_value(directory, Opts),
  201. Directory1 = directory_path(Directory),
  202. Mimetypes = proplists:get_value(mimetypes, Opts, []),
  203. Mimetypes1 = case Mimetypes of
  204. {_, _} -> Mimetypes;
  205. [] -> {fun path_to_mimetypes/2, []};
  206. [_|_] -> {fun path_to_mimetypes/2, Mimetypes}
  207. end,
  208. ETagFunction = case proplists:get_value(etag, Opts) of
  209. default -> {fun no_etag_function/2, undefined};
  210. undefined -> {fun no_etag_function/2, undefined};
  211. {attributes, []} -> {fun no_etag_function/2, undefined};
  212. {attributes, Attrs} -> {fun attr_etag_function/2, Attrs};
  213. {_, _}=ETagFunction1 -> ETagFunction1
  214. end,
  215. {Filepath, Req1} = case lists:keyfind(file, 1, Opts) of
  216. {_, Filepath2} -> {filepath_path(Filepath2), Req};
  217. false -> cowboy_http_req:path_info(Req)
  218. end,
  219. State = case check_path(Filepath) of
  220. error ->
  221. #state{filepath=error, fileinfo=error, mimetypes=undefined,
  222. etag_fun=ETagFunction};
  223. ok ->
  224. Filepath1 = join_paths(Directory1, Filepath),
  225. Fileinfo = file:read_file_info(Filepath1),
  226. #state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1,
  227. etag_fun=ETagFunction}
  228. end,
  229. {ok, Req1, State}.
  230. %% @private Only allow GET and HEAD requests on files.
  231. -spec allowed_methods(#http_req{}, #state{}) ->
  232. {[atom()], #http_req{}, #state{}}.
  233. allowed_methods(Req, State) ->
  234. {['GET', 'HEAD'], Req, State}.
  235. %% @private
  236. -spec malformed_request(#http_req{}, #state{}) ->
  237. {boolean(), #http_req{}, #state{}}.
  238. malformed_request(Req, #state{filepath=error}=State) ->
  239. {true, Req, State};
  240. malformed_request(Req, State) ->
  241. {false, Req, State}.
  242. %% @private Check if the resource exists under the document root.
  243. -spec resource_exists(#http_req{}, #state{}) ->
  244. {boolean(), #http_req{}, #state{}}.
  245. resource_exists(Req, #state{fileinfo={error, _}}=State) ->
  246. {false, Req, State};
  247. resource_exists(Req, #state{fileinfo={ok, Fileinfo}}=State) ->
  248. {Fileinfo#file_info.type =:= regular, Req, State}.
  249. %% @private
  250. %% Access to a file resource is forbidden if it exists and the local node does
  251. %% not have permission to read it. Directory listings are always forbidden.
  252. -spec forbidden(#http_req{}, #state{}) -> {boolean(), #http_req{}, #state{}}.
  253. forbidden(Req, #state{fileinfo={_, #file_info{type=directory}}}=State) ->
  254. {true, Req, State};
  255. forbidden(Req, #state{fileinfo={error, eacces}}=State) ->
  256. {true, Req, State};
  257. forbidden(Req, #state{fileinfo={error, _}}=State) ->
  258. {false, Req, State};
  259. forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
  260. {not (Access =:= read orelse Access =:= read_write), Req, State}.
  261. %% @private Read the time a file system system object was last modified.
  262. -spec last_modified(#http_req{}, #state{}) ->
  263. {calendar:datetime(), #http_req{}, #state{}}.
  264. last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
  265. {Modified, Req, State}.
  266. %% @private Generate the ETag header value for this file.
  267. %% The ETag header value is only generated if the resource is a file that
  268. %% exists in document root.
  269. -spec generate_etag(#http_req{}, #state{}) ->
  270. {undefined | binary(), #http_req{}, #state{}}.
  271. generate_etag(Req, #state{fileinfo={_, #file_info{type=regular, inode=INode,
  272. mtime=Modified, size=Filesize}}, filepath=Filepath,
  273. etag_fun={ETagFun, ETagData}}=State) ->
  274. ETagArgs = [
  275. {filepath, Filepath}, {filesize, Filesize},
  276. {inode, INode}, {mtime, Modified}],
  277. {ETagFun(ETagArgs, ETagData), Req, State};
  278. generate_etag(Req, State) ->
  279. {undefined, Req, State}.
  280. %% @private Return the content type of a file.
  281. -spec content_types_provided(#http_req{}, #state{}) -> tuple().
  282. content_types_provided(Req, #state{filepath=Filepath,
  283. mimetypes={MimetypesFun, MimetypesData}}=State) ->
  284. Mimetypes = [{T, file_contents}
  285. || T <- MimetypesFun(Filepath, MimetypesData)],
  286. {Mimetypes, Req, State}.
  287. %% @private Return a function that writes a file directly to the socket.
  288. -spec file_contents(#http_req{}, #state{}) -> tuple().
  289. file_contents(Req, #state{filepath=Filepath,
  290. fileinfo={ok, #file_info{size=Filesize}}}=State) ->
  291. {ok, Transport, Socket} = cowboy_http_req:transport(Req),
  292. Writefile = content_function(Transport, Socket, Filepath),
  293. {{stream, Filesize, Writefile}, Req, State}.
  294. %% @private Return a function writing the contents of a file to a socket.
  295. %% The function returns the number of bytes written to the socket to enable
  296. %% the calling function to determine if the expected number of bytes were
  297. %% written to the socket.
  298. -spec content_function(module(), inet:socket(), binary()) ->
  299. fun(() -> {sent, non_neg_integer()}).
  300. content_function(Transport, Socket, Filepath) ->
  301. %% `file:sendfile/2' will only work with the `cowboy_tcp_transport'
  302. %% transport module. SSL or future SPDY transports that require the
  303. %% content to be encrypted or framed as the content is sent.
  304. case erlang:function_exported(file, sendfile, 2) of
  305. false ->
  306. fun() -> sfallback(Transport, Socket, Filepath) end;
  307. _ when Transport =/= cowboy_tcp_transport ->
  308. fun() -> sfallback(Transport, Socket, Filepath) end;
  309. true ->
  310. fun() -> sendfile(Socket, Filepath) end
  311. end.
  312. %% @private Sendfile fallback function.
  313. -spec sfallback(module(), inet:socket(), binary()) -> {sent, non_neg_integer()}.
  314. sfallback(Transport, Socket, Filepath) ->
  315. {ok, File} = file:open(Filepath, [read,binary,raw]),
  316. sfallback(Transport, Socket, File, 0).
  317. -spec sfallback(module(), inet:socket(), file:io_device(),
  318. non_neg_integer()) -> {sent, non_neg_integer()}.
  319. sfallback(Transport, Socket, File, Sent) ->
  320. case file:read(File, 16#1FFF) of
  321. eof ->
  322. ok = file:close(File),
  323. {sent, Sent};
  324. {ok, Bin} ->
  325. case Transport:send(Socket, Bin) of
  326. ok -> sfallback(Transport, Socket, File, Sent + byte_size(Bin));
  327. {error, closed} -> {sent, Sent}
  328. end
  329. end.
  330. %% @private Wrapper for sendfile function.
  331. -spec sendfile(inet:socket(), binary()) -> {sent, non_neg_integer()}.
  332. sendfile(Socket, Filepath) ->
  333. {ok, Sent} = file:sendfile(Filepath, Socket),
  334. {sent, Sent}.
  335. -spec directory_path(dirspec()) -> dirpath().
  336. directory_path({priv_dir, App, []}) ->
  337. priv_dir_path(App);
  338. directory_path({priv_dir, App, [H|_]=Path}) when is_integer(H) ->
  339. filename:join(priv_dir_path(App), Path);
  340. directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
  341. filename:join(filename:split(priv_dir_path(App)) ++ Path);
  342. directory_path({priv_dir, App, Path}) when is_binary(Path) ->
  343. filename:join(priv_dir_path(App), Path);
  344. directory_path(Path) ->
  345. Path.
  346. %% @private Ensure that a file path is of the same type as a request path.
  347. -spec filepath_path(dirpath()) -> Path::[binary()].
  348. filepath_path([H|_]=Path) when is_integer(H) ->
  349. filename:split(list_to_binary(Path));
  350. filepath_path(Path) when is_binary(Path) ->
  351. filename:split(Path);
  352. filepath_path([H|_]=Path) when is_binary(H) ->
  353. Path.
  354. %% @private Validate a request path for unsafe characters.
  355. %% There is no way to escape special characters in a filesystem path.
  356. -spec check_path(Path::[binary()]) -> ok | error.
  357. check_path([]) -> ok;
  358. check_path([<<"">>|_T]) -> error;
  359. check_path([<<".">>|_T]) -> error;
  360. check_path([<<"..">>|_T]) -> error;
  361. check_path([H|T]) ->
  362. case binary:match(H, <<"/">>) of
  363. {_, _} -> error;
  364. nomatch -> check_path(T)
  365. end.
  366. %% @private Join the the directory and request paths.
  367. -spec join_paths(dirpath(), [binary()]) -> binary().
  368. join_paths([H|_]=Dirpath, Filepath) when is_integer(H) ->
  369. filename:join(filename:split(Dirpath) ++ Filepath);
  370. join_paths([H|_]=Dirpath, Filepath) when is_binary(H) ->
  371. filename:join(Dirpath ++ Filepath);
  372. join_paths(Dirpath, Filepath) when is_binary(Dirpath) ->
  373. filename:join([Dirpath] ++ Filepath);
  374. join_paths([], Filepath) ->
  375. filename:join(Filepath).
  376. %% @private Return the path to the priv/ directory of an application.
  377. -spec priv_dir_path(atom()) -> string().
  378. priv_dir_path(App) ->
  379. case code:priv_dir(App) of
  380. {error, bad_name} -> priv_dir_mod(App);
  381. Dir -> Dir
  382. end.
  383. -spec priv_dir_mod(atom()) -> string().
  384. priv_dir_mod(Mod) ->
  385. case code:which(Mod) of
  386. File when not is_list(File) -> "../priv";
  387. File -> filename:join([filename:dirname(File),"../priv"])
  388. end.
  389. %% @private Use application/octet-stream as the default mimetype.
  390. %% If a list of extension - mimetype pairs are provided as the mimetypes
  391. %% an attempt to find the mimetype using the file extension. If no match
  392. %% is found the default mimetype is returned.
  393. -spec path_to_mimetypes(binary(), [{binary(), [mimedef()]}]) ->
  394. [mimedef()].
  395. path_to_mimetypes(Filepath, Extensions) when is_binary(Filepath) ->
  396. Ext = filename:extension(Filepath),
  397. case Ext of
  398. <<>> -> default_mimetype();
  399. _Ext -> path_to_mimetypes_(Ext, Extensions)
  400. end.
  401. -spec path_to_mimetypes_(binary(), [{binary(), [mimedef()]}]) -> [mimedef()].
  402. path_to_mimetypes_(Ext, Extensions) ->
  403. case lists:keyfind(Ext, 1, Extensions) of
  404. {_, MTs} -> MTs;
  405. _Unknown -> default_mimetype()
  406. end.
  407. -spec default_mimetype() -> [mimedef()].
  408. default_mimetype() ->
  409. [{<<"application">>, <<"octet-stream">>, []}].
  410. %% @private Do not send ETag headers in the default configuration.
  411. -spec no_etag_function([etagarg()], undefined) -> undefined.
  412. no_etag_function(_Args, undefined) ->
  413. undefined.
  414. %% @private A simple alternative is to send an ETag based on file attributes.
  415. -type fileattr() :: filepath | filesize | mtime | inode.
  416. -spec attr_etag_function([etagarg()], [fileattr()]) -> {strong, binary()}.
  417. attr_etag_function(Args, Attrs) ->
  418. [[_|H]|T] = [begin
  419. {_,Pair} = {_,{_,_}} = {Attr,lists:keyfind(Attr, 1, Args)},
  420. [$-|integer_to_list(erlang:phash2(Pair, 1 bsl 32), 16)]
  421. end || Attr <- Attrs],
  422. {strong, list_to_binary([H|T])}.
  423. -ifdef(TEST).
  424. -include_lib("eunit/include/eunit.hrl").
  425. -define(_eq(E, I), ?_assertEqual(E, I)).
  426. check_path_test_() ->
  427. C = fun check_path/1,
  428. [?_eq(error, C([<<>>])),
  429. ?_eq(ok, C([<<"abc">>])),
  430. ?_eq(error, C([<<".">>])),
  431. ?_eq(error, C([<<"..">>])),
  432. ?_eq(error, C([<<"/">>]))
  433. ].
  434. join_paths_test_() ->
  435. P = fun join_paths/2,
  436. [?_eq(<<"a">>, P([], [<<"a">>])),
  437. ?_eq(<<"a/b/c">>, P(<<"a/b">>, [<<"c">>])),
  438. ?_eq(<<"a/b/c">>, P("a/b", [<<"c">>])),
  439. ?_eq(<<"a/b/c">>, P([<<"a">>, <<"b">>], [<<"c">>]))
  440. ].
  441. directory_path_test_() ->
  442. P = fun directory_path/1,
  443. PL = fun(I) -> length(filename:split(P(I))) end,
  444. Base = PL({priv_dir, cowboy, []}),
  445. [?_eq(Base + 1, PL({priv_dir, cowboy, "a"})),
  446. ?_eq(Base + 1, PL({priv_dir, cowboy, <<"a">>})),
  447. ?_eq(Base + 1, PL({priv_dir, cowboy, [<<"a">>]})),
  448. ?_eq(Base + 2, PL({priv_dir, cowboy, "a/b"})),
  449. ?_eq(Base + 2, PL({priv_dir, cowboy, <<"a/b">>})),
  450. ?_eq(Base + 2, PL({priv_dir, cowboy, [<<"a">>, <<"b">>]})),
  451. ?_eq("a/b", P("a/b"))
  452. ].
  453. filepath_path_test_() ->
  454. P = fun filepath_path/1,
  455. [?_eq([<<"a">>], P("a")),
  456. ?_eq([<<"a">>], P(<<"a">>)),
  457. ?_eq([<<"a">>], P([<<"a">>])),
  458. ?_eq([<<"a">>, <<"b">>], P("a/b")),
  459. ?_eq([<<"a">>, <<"b">>], P(<<"a/b">>)),
  460. ?_eq([<<"a">>, <<"b">>], P([<<"a">>, <<"b">>]))
  461. ].
  462. -endif.