aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_static.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2013-11-02 14:41:46 +0100
committerLoïc Hoguin <[email protected]>2013-11-02 14:41:46 +0100
commit6672ea04155a075e60381413bf9b65b3974b7d57 (patch)
tree8b708354f28335cb8f89fdf222dff0fc676ce22a /src/cowboy_static.erl
parentfaf64524c6758ae1e27404d2ae1383a23538c538 (diff)
downloadcowboy-6672ea04155a075e60381413bf9b65b3974b7d57.tar.gz
cowboy-6672ea04155a075e60381413bf9b65b3974b7d57.tar.bz2
cowboy-6672ea04155a075e60381413bf9b65b3974b7d57.zip
Review, improve and document the static files handler
Changes include: * Much simplified route configuration. * Etag generation is now enabled by default. * Web mimetypes are now detected by default. A bigger list of mimetypes can be detected without any additional library. * Mimetypes can no longer be specified as a list. Copying this list for new connections is too costy. You can easily convert it into a function and pass that function to the handler instead. * You can however specify a single hardcoded mimetype. Mostly useful when serving a single file, like an index.html file, to avoid extra operations. * Specifying a path as a list of tokens is not possible anymore. Use either a binary or a string. * Using a private directory will not work if the application was not started properly. Cowboy will not attempt to find the location of this directory if the VM doesn't know it, as this caused issues in some setups. * Overall the code has been much simplified and clarified, and of course has now been documented.
Diffstat (limited to 'src/cowboy_static.erl')
-rw-r--r--src/cowboy_static.erl582
1 files changed, 164 insertions, 418 deletions
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index d144dd3..476d1b8 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -1,3 +1,4 @@
+%% Copyright (c) 2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Magnus Klaar <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -12,364 +13,90 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-%% @doc Static resource handler.
-%%
-%% This built in HTTP handler provides a simple file serving capability for
-%% cowboy applications. It is provided as a convenience for small or temporary
-%% environments where it is not preferrable to set up a second server just
-%% to serve files. It is recommended to use a CDN instead for efficiently
-%% handling static files, preferrably on a cookie-less domain name.
-%%
-%% If this handler is used the Erlang node running the cowboy application must
-%% be configured to use an async thread pool. This is configured by adding the
-%% `+A $POOL_SIZE' argument to the `erl' command used to start the node. See
-%% <a href="http://erlang.org/pipermail/erlang-bugs/2012-January/002720.html">
-%% this reply</a> from the OTP team to erlang-bugs
-%%
-%% == Base configuration ==
-%%
-%% The handler must be configured with a request path prefix to serve files
-%% under and the path to a directory to read files from. The request path prefix
-%% is defined in the path pattern of the cowboy dispatch rule for the handler.
-%% The request path pattern must end with a `...' token.
-%%
-%% The directory path can be set to either an absolute or relative path in the
-%% form of a list or binary string representation of a file system path. A list
-%% of binary path segments is also a valid directory path.
-%%
-%% The directory path can also be set to a relative path within the `priv/'
-%% directory of an application. This is configured by setting the value of the
-%% directory option to a tuple of the form `{priv_dir, Application, Relpath}'.
-%%
-%% ==== Examples ====
-%% ```
-%% %% Serve files from /var/www/ under http://example.com/static/
-%% {"/static/[...]", cowboy_static,
-%% [{directory, "/var/www"}]}
-%%
-%% %% Serve files from the current working directory under http://example.com/static/
-%% {"/static/[...]", cowboy_static,
-%% [{directory, <<"./">>}]}
-%%
-%% %% Serve files from cowboy/priv/www under http://example.com/
-%% {"/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}}]}
-%% '''
-%%
-%% == Content type configuration ==
-%%
-%% By default the content type of all static resources will be set to
-%% `application/octet-stream'. This can be overriden by supplying a list
-%% of filename extension to mimetypes pairs in the `mimetypes' option.
-%% The filename extension should be a binary string including the leading dot.
-%% The mimetypes must be of a type that the `cowboy_rest' protocol can
-%% handle.
-%%
-%% The <a href="https://github.com/spawngrid/mimetypes">spawngrid/mimetypes</a>
-%% application, or an arbitrary function accepting the path to the file being
-%% served, can also be used to generate the list of content types for a static
-%% file resource. The function used must accept an additional argument after
-%% the file path argument.
-%%
-%% ==== Example ====
-%% ```
-%% %% Use a static list of content types.
-%% {"/static/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, []}},
-%% {mimetypes, [
-%% {<<".css">>, [<<"text/css">>]},
-%% {<<".js">>, [<<"application/javascript">>]}]}]}
-%%
-%% %% Use the default database in the mimetypes application.
-%% {"/static/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, []}},
-%% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}
-%% '''
-%%
-%% == ETag Header Function ==
-%%
-%% The default behaviour of the static file handler is to not generate ETag
-%% headers. This is because generating ETag headers based on file metadata
-%% causes different servers in a cluster to generate different ETag values for
-%% the same file unless the metadata is also synced. Generating strong ETags
-%% based on the contents of a file is currently out of scope for this module.
-%%
-%% The default behaviour can be overridden to generate an ETag header based on
-%% a combination of the file path, file size, inode and mtime values. If the
-%% option value is a non-empty list of attribute names tagged with `attributes'
-%% a hex encoded checksum of each attribute specified is included in the value
-%% of the the ETag header. If the list of attribute names is empty no ETag
-%% header is generated.
-%%
-%% If a strong ETag is required a user defined function for generating the
-%% header value can be supplied. The function must accept a list of key/values
-%% of the file attributes as the first argument and a second argument
-%% containing any additional data that the function requires. The function
-%% must return a term of the type `{weak | strong, binary()}' or `undefined'.
-%%
-%% ==== Examples ====
-%% ```
-%% %% A value of default is equal to not specifying the option.
-%% {"static/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, []}},
-%% {etag, default}]}
-%%
-%% %% Use all avaliable ETag function arguments to generate a header value.
-%% {"static/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, []}},
-%% {etag, {attributes, [filepath, filesize, inode, mtime]}}]}
-%%
-%% %% Use a user defined function to generate a strong ETag header value.
-%% {"static/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, []}},
-%% {etag, {fun generate_strong_etag/2, strong_etag_extra}}]}
-%%
-%% generate_strong_etag(Arguments, strong_etag_extra) ->
-%% {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
-%% {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
-%% {_, _INode} = lists:keyfind(inode, 1, Arguments),
-%% {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
-%% ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
-%% [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
-%% {strong, iolist_to_binary(Checksum)}.
-%% '''
-%%
-%% == File configuration ==
-%%
-%% If the file system path being served does not share a common suffix with
-%% the request path it is possible to override the file path using the `file'
-%% option. The value of this option is expected to be a relative path within
-%% the static file directory specified using the `directory' option.
-%% The path must be in the form of a list or binary string representation of a
-%% file system path. A list of binary path segments, as is used throughout
-%% cowboy, is also a valid.
-%%
-%% When the `file' option is used the same file will be served for all requests
-%% matching the cowboy dispatch fule for the handler. It is not necessary to
-%% end the request path pattern with a `...' token because the request path
-%% will not be used to determine which file to serve from the static directory.
-%%
-%% === Examples ===
-%%
-%% ```
-%% %% Serve cowboy/priv/www/index.html as http://example.com/
-%% {"/", cowboy_static,
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}},
-%% {file, <<"index.html">>}]}
-%%
-%% %% Serve cowboy/priv/www/page.html under http://example.com/*/page
-%% {"/:_/page", cowboy_static,
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}},
-%% {file, <<"page.html">>}]}.
-%%
-%% %% Always serve cowboy/priv/www/other.html under http://example.com/other
-%% {"/other/[...]", cowboy_static,
-%% [{directory, {priv_dir, cowboy, [<<"www">>]}},
-%% {file, "other.html"}]}
-%% '''
-module(cowboy_static).
-%% include files
--include_lib("kernel/include/file.hrl").
-
-%% cowboy_protocol callbacks
-export([init/3]).
-
-%% cowboy_rest callbacks
-export([rest_init/2]).
--export([allowed_methods/2]).
-export([malformed_request/2]).
--export([resource_exists/2]).
-export([forbidden/2]).
+-export([content_types_provided/2]).
+-export([resource_exists/2]).
-export([last_modified/2]).
-export([generate_etag/2]).
--export([content_types_provided/2]).
--export([file_contents/2]).
-
-%% internal
--export([path_to_mimetypes/2]).
-
-%% types
--type dirpath() :: string() | binary() | [binary()].
--type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
--type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
--type etagarg() :: {filepath, binary()} | {mtime, calendar:datetime()}
- | {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
-
-%% handler state
--record(state, {
- filepath :: binary() | error,
- fileinfo :: {ok, #file_info{}} | {error, _} | error,
- mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
- etag_fun :: {fun(([etagarg()], T) ->
- undefined | {strong | weak, binary()}), T}
-}).
-
-%% @private Upgrade from HTTP handler to REST handler.
-init({_Transport, http}, _Req, _Opts) ->
- {upgrade, protocol, cowboy_rest}.
+-export([get_file/2]).
+
+-type extra_etag() :: {etag, module(), function()} | {etag, false}.
+-type extra_mimetypes() :: {mimetypes, module(), function()}
+ | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
+-type extra() :: [extra_etag() | extra_mimetypes()].
+-type opts() :: {file | dir, string() | binary()}
+ | {file | dir, string() | binary(), extra()}
+ | {priv_file | priv_dir, atom(), string() | binary()}
+ | {priv_file | priv_dir, atom(), string() | binary(), extra()}.
+-export_type([opts/0]).
-%% @private Set up initial state of REST handler.
--spec rest_init(Req, list()) -> {ok, Req, #state{}} when Req::cowboy_req:req().
-rest_init(Req, Opts) ->
- {_, DirectoryOpt} = lists:keyfind(directory, 1, Opts),
- Directory = fullpath(filename:absname(directory_path(DirectoryOpt))),
- case lists:keyfind(file, 1, Opts) of
- false ->
- {PathInfo, Req2} = cowboy_req:path_info(Req),
- Filepath = filename:join([Directory|PathInfo]),
- Len = byte_size(Directory),
- case fullpath(Filepath) of
- << Directory:Len/binary, $/, _/binary >> ->
- rest_init(Req2, Opts, Filepath);
- _ ->
- {ok, Req2, #state{filepath=error, fileinfo=error,
- mimetypes=undefined, etag_fun=undefined}}
- end;
- {_, FileOpt} ->
- Filepath = filepath_path(FileOpt),
- Filepath2 = << Directory/binary, $/, Filepath/binary >>,
- rest_init(Req, Opts, Filepath2)
- end.
+-include_lib("kernel/include/file.hrl").
-rest_init(Req, Opts, Filepath) ->
- Fileinfo = file:read_file_info(Filepath, [{time, universal}]),
- Mimetypes = case lists:keyfind(mimetypes, 1, Opts) of
- false -> {fun path_to_mimetypes/2, []};
- {_, {{M, F}, E}} -> {fun M:F/2, E};
- {_, Mtypes} when is_tuple(Mtypes) -> Mtypes;
- {_, Mtypes} when is_list(Mtypes) -> {fun path_to_mimetypes/2, Mtypes}
- end,
- EtagFun = case lists:keyfind(etag, 1, Opts) of
- false -> {fun no_etag_function/2, undefined};
- {_, default} -> {fun no_etag_function/2, undefined};
- {_, {attributes, []}} -> {fun no_etag_function/2, undefined};
- {_, {attributes, Attrs}} -> {fun attr_etag_function/2, Attrs};
- {_, EtagOpt} -> EtagOpt
- end,
- {ok, Req, #state{filepath=Filepath, fileinfo=Fileinfo,
- mimetypes=Mimetypes, etag_fun=EtagFun}}.
-
-%% @private Only allow GET and HEAD requests on files.
--spec allowed_methods(Req, #state{})
- -> {[binary()], Req, #state{}} when Req::cowboy_req:req().
-allowed_methods(Req, State) ->
- {[<<"GET">>, <<"HEAD">>], Req, State}.
-
-%% @private
--spec malformed_request(Req, #state{})
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
-malformed_request(Req, #state{filepath=error}=State) ->
- {true, Req, State};
-malformed_request(Req, State) ->
- {false, Req, State}.
+-type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}.
-%% @private Check if the resource exists under the document root.
--spec resource_exists(Req, #state{})
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
-resource_exists(Req, #state{fileinfo={error, _}}=State) ->
- {false, Req, State};
-resource_exists(Req, #state{fileinfo={ok, Fileinfo}}=State) ->
- {Fileinfo#file_info.type =:= regular, Req, State}.
-
-%% @private
-%% Access to a file resource is forbidden if it exists and the local node does
-%% not have permission to read it. Directory listings are always forbidden.
--spec forbidden(Req, #state{})
- -> {boolean(), Req, #state{}} when Req::cowboy_req:req().
-forbidden(Req, #state{fileinfo={_, #file_info{type=directory}}}=State) ->
- {true, Req, State};
-forbidden(Req, #state{fileinfo={error, eacces}}=State) ->
- {true, Req, State};
-forbidden(Req, #state{fileinfo={error, _}}=State) ->
- {false, Req, State};
-forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
- {not (Access =:= read orelse Access =:= read_write), Req, State}.
-
-%% @private Read the time a file system system object was last modified.
--spec last_modified(Req, #state{})
- -> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req().
-last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
- {Modified, Req, State}.
+init(_, _, _) ->
+ {upgrade, protocol, cowboy_rest}.
-%% @private Generate the ETag header value for this file.
-%% The ETag header value is only generated if the resource is a file that
-%% exists in document root.
--spec generate_etag(Req, #state{})
- -> {undefined | binary(), Req, #state{}} when Req::cowboy_req:req().
-generate_etag(Req, #state{fileinfo={_, #file_info{type=regular, inode=INode,
- mtime=Modified, size=Filesize}}, filepath=Filepath,
- etag_fun={ETagFun, ETagData}}=State) ->
- ETagArgs = [
- {filepath, Filepath}, {filesize, Filesize},
- {inode, INode}, {mtime, Modified}],
- {ETagFun(ETagArgs, ETagData), Req, State};
-generate_etag(Req, State) ->
- {undefined, Req, State}.
-
-%% @private Return the content type of a file.
--spec content_types_provided(cowboy_req:req(), #state{}) -> tuple().
-content_types_provided(Req, #state{filepath=Filepath,
- mimetypes={MimetypesFun, MimetypesData}}=State) ->
- Mimetypes = [{T, file_contents}
- || T <- MimetypesFun(Filepath, MimetypesData)],
- {Mimetypes, Req, State}.
-
-%% @private Return a function that writes a file directly to the socket.
--spec file_contents(cowboy_req:req(), #state{}) -> tuple().
-file_contents(Req, #state{filepath=Filepath,
- fileinfo={ok, #file_info{size=Filesize}}}=State) ->
- Writefile = fun(Socket, Transport) ->
- %% Transport:sendfile/2 may return {error, closed}
- %% if the connection is closed while sending the file.
- case Transport:sendfile(Socket, Filepath) of
- {ok, _} -> ok;
- {error, closed} -> ok;
- {error, etimedout} -> ok
- end
- end,
- {{stream, Filesize, Writefile}, Req, State}.
-
-%% Internal.
-
--spec directory_path(dirspec()) -> dirpath().
-directory_path({priv_dir, App, []}) ->
- priv_dir_path(App);
-directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
- filename:join(priv_dir_path(App), filename:join(Path));
-directory_path({priv_dir, App, Path}) ->
- filename:join(priv_dir_path(App), Path);
-directory_path([H|_]=Path) when is_binary(H) ->
- filename:join(Path);
-directory_path([H|_]=Path) when is_integer(H) ->
- list_to_binary(Path);
-directory_path(Path) when is_binary(Path) ->
- Path.
-
-%% @private Return the path to the priv/ directory of an application.
--spec priv_dir_path(atom()) -> string().
-priv_dir_path(App) ->
+%% @doc Resolve the file that will be sent and get its file information.
+%% If the handler is configured to manage a directory, check that the
+%% requested file is inside the configured directory.
+
+-spec rest_init(Req, opts())
+ -> {ok, Req, error | state()}
+ when Req::cowboy_req:req().
+rest_init(Req, {Name, Path}) ->
+ rest_init_opts(Req, {Name, Path, []});
+rest_init(Req, {Name, App, Path})
+ when Name =:= priv_file; Name =:= priv_dir ->
+ rest_init_opts(Req, {Name, App, Path, []});
+rest_init(Req, Opts) ->
+ rest_init_opts(Req, Opts).
+
+rest_init_opts(Req, {priv_file, App, Path, Extra}) ->
+ rest_init_info(Req, absname(priv_path(App, Path)), Extra);
+rest_init_opts(Req, {file, Path, Extra}) ->
+ rest_init_info(Req, absname(Path), Extra);
+rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
+ rest_init_dir(Req, priv_path(App, Path), Extra);
+rest_init_opts(Req, {dir, Path, Extra}) ->
+ rest_init_dir(Req, Path, Extra).
+
+priv_path(App, Path) ->
case code:priv_dir(App) of
- {error, bad_name} -> priv_dir_mod(App);
- Dir -> list_to_binary(Dir)
+ {error, bad_name} ->
+ error({badarg, "Can't resolve the priv_dir of application "
+ ++ atom_to_list(App)});
+ PrivDir when is_list(Path) ->
+ PrivDir ++ "/" ++ Path;
+ PrivDir when is_binary(Path) ->
+ << (list_to_binary(PrivDir))/binary, $/, Path/binary >>
end.
--spec priv_dir_mod(atom()) -> string().
-priv_dir_mod(Mod) ->
- case code:which(Mod) of
- File when not is_list(File) -> <<"../priv">>;
- File -> filename:join(filename:dirname(File), <<"../priv">>)
+absname(Path) when is_list(Path) ->
+ filename:absname(list_to_binary(Path));
+absname(Path) when is_binary(Path) ->
+ filename:absname(Path).
+
+rest_init_dir(Req, Path, Extra) when is_list(Path) ->
+ rest_init_dir(Req, list_to_binary(Path), Extra);
+rest_init_dir(Req, Path, Extra) ->
+ Dir = fullpath(filename:absname(Path)),
+ {PathInfo, Req2} = cowboy_req:path_info(Req),
+ Filepath = filename:join([Dir|PathInfo]),
+ Len = byte_size(Dir),
+ case fullpath(Filepath) of
+ << Dir:Len/binary, $/, _/binary >> ->
+ rest_init_info(Req2, Filepath, Extra);
+ _ ->
+ {ok, Req2, error}
end.
-%% @private Ensure that a file path is of the same type as a request path.
-filepath_path(Path) when is_binary(Path) ->
- Path;
-filepath_path([H|_]=Path) when is_binary(H) ->
- filename:join(Path);
-filepath_path([H|_]=Path) when is_integer(H) ->
- list_to_binary(Path).
-
-fullpath(Path) when is_binary(Path) ->
+fullpath(Path) ->
fullpath(filename:split(Path), []).
fullpath([], Acc) ->
filename:join(lists:reverse(Acc));
@@ -382,84 +109,11 @@ fullpath([<<"..">>|Tail], [_|Acc]) ->
fullpath([Segment|Tail], Acc) ->
fullpath(Tail, [Segment|Acc]).
-%% @private Use application/octet-stream as the default mimetype.
-%% If a list of extension - mimetype pairs are provided as the mimetypes
-%% an attempt to find the mimetype using the file extension. If no match
-%% is found the default mimetype is returned.
--spec path_to_mimetypes(binary(), [{binary(), [mimedef()]}]) ->
- [mimedef()].
-path_to_mimetypes(Filepath, Extensions) when is_binary(Filepath) ->
- Ext = filename:extension(Filepath),
- case Ext of
- <<>> -> default_mimetype();
- _Ext -> path_to_mimetypes_(Ext, Extensions)
- end.
-
--spec path_to_mimetypes_(binary(), [{binary(), [mimedef()]}]) -> [mimedef()].
-path_to_mimetypes_(Ext, Extensions) ->
- case lists:keyfind(cowboy_bstr:to_lower(Ext), 1, Extensions) of
- {_, MTs} -> MTs;
- _Unknown -> default_mimetype()
- end.
-
--spec default_mimetype() -> [mimedef()].
-default_mimetype() ->
- [{<<"application">>, <<"octet-stream">>, []}].
-
-%% @private Do not send ETag headers in the default configuration.
--spec no_etag_function([etagarg()], undefined) -> undefined.
-no_etag_function(_Args, undefined) ->
- undefined.
-
-%% @private A simple alternative is to send an ETag based on file attributes.
--type fileattr() :: filepath | filesize | mtime | inode.
--spec attr_etag_function([etagarg()], [fileattr()]) -> {strong, binary()}.
-attr_etag_function(Args, Attrs) ->
- [[_|H]|T] = [begin
- {_,Pair} = {_,{_,_}} = {Attr,lists:keyfind(Attr, 1, Args)},
- [$-|integer_to_list(erlang:phash2(Pair, 1 bsl 32), 16)]
- end || Attr <- Attrs],
- {strong, list_to_binary([H|T])}.
+rest_init_info(Req, Path, Extra) ->
+ Info = file:read_file_info(Path, [{time, universal}]),
+ {ok, Req, {Path, Info, Extra}}.
-ifdef(TEST).
-
-directory_path_test_() ->
- PL = fun(D) -> length(filename:split(directory_path(D))) end,
- Base = PL({priv_dir, cowboy, []}),
- LengthTests = [
- Base + 1, {priv_dir, cowboy, "a"},
- Base + 1, {priv_dir, cowboy, <<"a">>},
- Base + 1, {priv_dir, cowboy, [<<"a">>]},
- Base + 2, {priv_dir, cowboy, "a/b"},
- Base + 2, {priv_dir, cowboy, <<"a/b">>},
- Base + 2, {priv_dir, cowboy, [<<"a">>, <<"b">>]}
- ],
- TypeTests = [
- {priv_dir, cowboy, []},
- {priv_dir, cowboy, "a"},
- {priv_dir, cowboy, <<"a">>},
- {priv_dir, cowboy, [<<"a">>]},
- "a",
- <<"a">>,
- [<<"a">>]
- ],
- [{lists:flatten(io_lib:format("~p", [D])),
- fun() -> R = PL(D) end} || {R, D} <- LengthTests]
- ++ [{lists:flatten(io_lib:format("~p", [D])),
- fun() -> is_binary(directory_path(D)) end} || D <- TypeTests].
-
-filepath_path_test_() ->
- Tests = [
- {<<"a">>, "a"},
- {<<"a">>, <<"a">>},
- {<<"a">>, [<<"a">>]},
- {<<"a/b">>, "a/b"},
- {<<"a/b">>, <<"a/b">>},
- {<<"a/b">>, [<<"a">>, <<"b">>]}
- ],
- [{lists:flatten(io_lib:format("~p", [F])),
- fun() -> R = filepath_path(F) end} || {R, F} <- Tests].
-
fullpath_test_() ->
Tests = [
{<<"/home/cowboy">>, <<"/home/cowboy">>},
@@ -541,5 +195,97 @@ bad_path_win32_check_test_() ->
_ -> error
end
end} || P <- Tests].
-
-endif.
+
+%% @doc Reject requests that tried to access a file outside
+%% the target directory.
+
+-spec malformed_request(Req, State)
+ -> {boolean(), Req, State}.
+malformed_request(Req, State) ->
+ {State =:= error, Req, State}.
+
+%% @doc Directories, files that can't be accessed at all and
+%% files with no read flag are forbidden.
+
+-spec forbidden(Req, State)
+ -> {boolean(), Req, State}
+ when State::state().
+forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) ->
+ {true, Req, State};
+forbidden(Req, State={_, {error, eacces}, _}) ->
+ {true, Req, State};
+forbidden(Req, State={_, {ok, #file_info{access=Access}}, _})
+ when Access =:= write; Access =:= none ->
+ {true, Req, State};
+forbidden(Req, State) ->
+ {false, Req, State}.
+
+%% @doc Detect the mimetype of the file.
+
+-spec content_types_provided(Req, State)
+ -> {[{binary(), get_file}], Req, State}
+ when State::state().
+content_types_provided(Req, State={Path, _, Extra}) ->
+ case lists:keyfind(mimetypes, 1, Extra) of
+ false ->
+ {[{cow_mimetypes:web(Path), get_file}], Req, State};
+ {mimetypes, Module, Function} ->
+ {[{Module:Function(Path), get_file}], Req, State};
+ {mimetypes, Type} ->
+ {[{Type, get_file}], Req, State}
+ end.
+
+%% @doc Assume the resource doesn't exist if it's not a regular file.
+
+-spec resource_exists(Req, State)
+ -> {boolean(), Req, State}
+ when State::state().
+resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) ->
+ {true, Req, State};
+resource_exists(Req, State) ->
+ {false, Req, State}.
+
+%% @doc Generate an etag for the file.
+
+-spec generate_etag(Req, State)
+ -> {{strong | weak, binary()}, Req, State}
+ when State::state().
+generate_etag(Req, State={Path, {ok, #file_info{size=Size, mtime=Mtime}},
+ Extra}) ->
+ case lists:keyfind(etag, 1, Extra) of
+ false ->
+ {generate_default_etag(Size, Mtime), Req, State};
+ {etag, Module, Function} ->
+ {Module:Function(Path, Size, Mtime), Req, State};
+ {etag, false} ->
+ {undefined, Req, State}
+ end.
+
+generate_default_etag(Size, Mtime) ->
+ {strong, list_to_binary(integer_to_list(
+ erlang:phash2({Size, Mtime}, 16#ffffffff)))}.
+
+%% @doc Return the time of last modification of the file.
+
+-spec last_modified(Req, State)
+ -> {calendar:datetime(), Req, State}
+ when State::state().
+last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
+ {Modified, Req, State}.
+
+%% @doc Stream the file.
+%% @todo Export cowboy_req:resp_body_fun()?
+
+-spec get_file(Req, State)
+ -> {{stream, non_neg_integer(), fun()}, Req, State}
+ when State::state().
+get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) ->
+ Sendfile = fun (Socket, Transport) ->
+ case Transport:sendfile(Socket, Path) of
+ {ok, _} -> ok;
+ {error, closed} -> ok;
+ {error, etimedout} -> ok
+ end
+ end,
+ {{stream, Size, Sendfile}, Req, State}.