2017-01-02 19:36:36 +01:00
|
|
|
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
|
2011-12-28 18:02:32 +01:00
|
|
|
%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
|
|
|
|
%%
|
|
|
|
%% Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
%% purpose with or without fee is hereby granted, provided that the above
|
|
|
|
%% copyright notice and this permission notice appear in all copies.
|
|
|
|
%%
|
|
|
|
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
|
|
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
|
|
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
|
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
|
2012-08-27 13:53:27 +02:00
|
|
|
-module(cowboy_static).
|
2011-12-28 18:02:32 +01:00
|
|
|
|
2014-09-26 15:58:44 +03:00
|
|
|
-export([init/2]).
|
2012-08-27 12:16:07 +02:00
|
|
|
-export([malformed_request/2]).
|
|
|
|
-export([forbidden/2]).
|
2013-11-02 14:41:46 +01:00
|
|
|
-export([content_types_provided/2]).
|
2018-11-02 15:31:54 +01:00
|
|
|
-export([charsets_provided/2]).
|
2018-11-11 16:25:45 +01:00
|
|
|
-export([ranges_provided/2]).
|
2013-11-02 14:41:46 +01:00
|
|
|
-export([resource_exists/2]).
|
2012-08-27 12:16:07 +02:00
|
|
|
-export([last_modified/2]).
|
|
|
|
-export([generate_etag/2]).
|
2013-11-02 14:41:46 +01:00
|
|
|
-export([get_file/2]).
|
|
|
|
|
2018-11-02 15:31:54 +01:00
|
|
|
-type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
|
2013-11-02 14:41:46 +01:00
|
|
|
-type extra_etag() :: {etag, module(), function()} | {etag, false}.
|
|
|
|
-type extra_mimetypes() :: {mimetypes, module(), function()}
|
|
|
|
| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
|
2018-11-05 17:33:00 +01:00
|
|
|
-type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()].
|
2013-11-02 14:41:46 +01:00
|
|
|
-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]).
|
2011-12-28 18:02:32 +01:00
|
|
|
|
2013-11-02 14:41:46 +01:00
|
|
|
-include_lib("kernel/include/file.hrl").
|
2013-02-27 23:20:55 +01:00
|
|
|
|
2016-12-29 15:48:06 +01:00
|
|
|
-type state() :: {binary(), {direct | archive, #file_info{}}
|
|
|
|
| {error, atom()}, extra()}.
|
2011-12-28 18:02:32 +01:00
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Resolve the file that will be sent and get its file information.
|
2013-11-02 14:41:46 +01:00
|
|
|
%% If the handler is configured to manage a directory, check that the
|
|
|
|
%% requested file is inside the configured directory.
|
|
|
|
|
2014-09-30 20:12:13 +03:00
|
|
|
-spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req().
|
2014-09-26 15:58:44 +03:00
|
|
|
init(Req, {Name, Path}) ->
|
|
|
|
init_opts(Req, {Name, Path, []});
|
|
|
|
init(Req, {Name, App, Path})
|
2013-11-02 14:41:46 +01:00
|
|
|
when Name =:= priv_file; Name =:= priv_dir ->
|
2014-09-26 15:58:44 +03:00
|
|
|
init_opts(Req, {Name, App, Path, []});
|
|
|
|
init(Req, Opts) ->
|
|
|
|
init_opts(Req, Opts).
|
|
|
|
|
|
|
|
init_opts(Req, {priv_file, App, Path, Extra}) ->
|
2016-12-29 15:48:06 +01:00
|
|
|
{PrivPath, HowToAccess} = priv_path(App, Path),
|
|
|
|
init_info(Req, absname(PrivPath), HowToAccess, Extra);
|
2014-09-26 15:58:44 +03:00
|
|
|
init_opts(Req, {file, Path, Extra}) ->
|
2016-12-29 15:48:06 +01:00
|
|
|
init_info(Req, absname(Path), direct, Extra);
|
2014-09-26 15:58:44 +03:00
|
|
|
init_opts(Req, {priv_dir, App, Path, Extra}) ->
|
2016-12-29 15:48:06 +01:00
|
|
|
{PrivPath, HowToAccess} = priv_path(App, Path),
|
|
|
|
init_dir(Req, PrivPath, HowToAccess, Extra);
|
2014-09-26 15:58:44 +03:00
|
|
|
init_opts(Req, {dir, Path, Extra}) ->
|
2016-12-29 15:48:06 +01:00
|
|
|
init_dir(Req, Path, direct, Extra).
|
2013-11-02 14:41:46 +01:00
|
|
|
|
|
|
|
priv_path(App, Path) ->
|
2011-12-28 18:02:32 +01:00
|
|
|
case code:priv_dir(App) of
|
2013-11-02 14:41:46 +01:00
|
|
|
{error, bad_name} ->
|
|
|
|
error({badarg, "Can't resolve the priv_dir of application "
|
|
|
|
++ atom_to_list(App)});
|
|
|
|
PrivDir when is_list(Path) ->
|
2016-12-29 15:48:06 +01:00
|
|
|
{
|
|
|
|
PrivDir ++ "/" ++ Path,
|
|
|
|
how_to_access_app_priv(PrivDir)
|
|
|
|
};
|
2013-11-02 14:41:46 +01:00
|
|
|
PrivDir when is_binary(Path) ->
|
2016-12-29 15:48:06 +01:00
|
|
|
{
|
|
|
|
<< (list_to_binary(PrivDir))/binary, $/, Path/binary >>,
|
|
|
|
how_to_access_app_priv(PrivDir)
|
|
|
|
}
|
|
|
|
end.
|
|
|
|
|
|
|
|
how_to_access_app_priv(PrivDir) ->
|
|
|
|
%% If the priv directory is not a directory, it must be
|
|
|
|
%% inside an Erlang application .ez archive. We call
|
|
|
|
%% how_to_access_app_priv1() to find the corresponding archive.
|
|
|
|
case filelib:is_dir(PrivDir) of
|
|
|
|
true -> direct;
|
|
|
|
false -> how_to_access_app_priv1(PrivDir)
|
|
|
|
end.
|
|
|
|
|
|
|
|
how_to_access_app_priv1(Dir) ->
|
|
|
|
%% We go "up" by one path component at a time and look for a
|
|
|
|
%% regular file.
|
|
|
|
Archive = filename:dirname(Dir),
|
|
|
|
case Archive of
|
|
|
|
Dir ->
|
|
|
|
%% filename:dirname() returned its argument:
|
|
|
|
%% we reach the root directory. We found no
|
|
|
|
%% archive so we return 'direct': the given priv
|
|
|
|
%% directory doesn't exist.
|
|
|
|
direct;
|
|
|
|
_ ->
|
|
|
|
case filelib:is_regular(Archive) of
|
|
|
|
true -> {archive, Archive};
|
|
|
|
false -> how_to_access_app_priv1(Archive)
|
|
|
|
end
|
2011-12-28 18:02:32 +01:00
|
|
|
end.
|
|
|
|
|
2013-11-02 14:41:46 +01:00
|
|
|
absname(Path) when is_list(Path) ->
|
|
|
|
filename:absname(list_to_binary(Path));
|
|
|
|
absname(Path) when is_binary(Path) ->
|
|
|
|
filename:absname(Path).
|
|
|
|
|
2016-12-29 15:48:06 +01:00
|
|
|
init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
|
|
|
|
init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
|
|
|
|
init_dir(Req, Path, HowToAccess, Extra) ->
|
2013-11-02 14:41:46 +01:00
|
|
|
Dir = fullpath(filename:absname(Path)),
|
2014-09-23 16:43:29 +03:00
|
|
|
PathInfo = cowboy_req:path_info(Req),
|
2018-11-19 12:15:40 +01:00
|
|
|
Filepath = filename:join([Dir|escape_reserved(PathInfo)]),
|
2013-11-02 14:41:46 +01:00
|
|
|
Len = byte_size(Dir),
|
|
|
|
case fullpath(Filepath) of
|
|
|
|
<< Dir:Len/binary, $/, _/binary >> ->
|
2016-12-29 15:48:06 +01:00
|
|
|
init_info(Req, Filepath, HowToAccess, Extra);
|
2016-06-06 17:37:52 +02:00
|
|
|
<< Dir:Len/binary >> ->
|
2016-12-29 15:48:06 +01:00
|
|
|
init_info(Req, Filepath, HowToAccess, Extra);
|
2013-11-02 14:41:46 +01:00
|
|
|
_ ->
|
2014-09-30 20:12:13 +03:00
|
|
|
{cowboy_rest, Req, error}
|
2011-12-28 18:02:32 +01:00
|
|
|
end.
|
|
|
|
|
2018-11-19 12:15:40 +01:00
|
|
|
escape_reserved([]) -> [];
|
|
|
|
escape_reserved([P|Tail]) -> [escape_reserved(P, <<>>)|escape_reserved(Tail)].
|
|
|
|
|
2016-06-06 17:39:06 +02:00
|
|
|
%% We escape the slash found in path segments because
|
|
|
|
%% a segment corresponds to a directory entry, and
|
|
|
|
%% therefore those slashes are expected to be part of
|
|
|
|
%% the directory name.
|
|
|
|
%%
|
|
|
|
%% Note that on most systems the slash is prohibited
|
|
|
|
%% and cannot appear in filenames, which means the
|
|
|
|
%% requested file will end up being not found.
|
|
|
|
escape_reserved(<<>>, Acc) ->
|
|
|
|
Acc;
|
|
|
|
escape_reserved(<< $/, Rest/bits >>, Acc) ->
|
|
|
|
escape_reserved(Rest, << Acc/binary, $\\, $/ >>);
|
|
|
|
escape_reserved(<< C, Rest/bits >>, Acc) ->
|
|
|
|
escape_reserved(Rest, << Acc/binary, C >>).
|
|
|
|
|
2013-11-02 14:41:46 +01:00
|
|
|
fullpath(Path) ->
|
2013-02-28 17:31:56 +01:00
|
|
|
fullpath(filename:split(Path), []).
|
|
|
|
fullpath([], Acc) ->
|
|
|
|
filename:join(lists:reverse(Acc));
|
|
|
|
fullpath([<<".">>|Tail], Acc) ->
|
|
|
|
fullpath(Tail, Acc);
|
|
|
|
fullpath([<<"..">>|Tail], Acc=[_]) ->
|
|
|
|
fullpath(Tail, Acc);
|
|
|
|
fullpath([<<"..">>|Tail], [_|Acc]) ->
|
|
|
|
fullpath(Tail, Acc);
|
|
|
|
fullpath([Segment|Tail], Acc) ->
|
|
|
|
fullpath(Tail, [Segment|Acc]).
|
|
|
|
|
2016-12-29 15:48:06 +01:00
|
|
|
init_info(Req, Path, HowToAccess, Extra) ->
|
|
|
|
Info = read_file_info(Path, HowToAccess),
|
2014-09-30 20:12:13 +03:00
|
|
|
{cowboy_rest, Req, {Path, Info, Extra}}.
|
2012-01-07 23:07:45 +01:00
|
|
|
|
2016-12-29 15:48:06 +01:00
|
|
|
read_file_info(Path, direct) ->
|
|
|
|
case file:read_file_info(Path, [{time, universal}]) of
|
|
|
|
{ok, Info} -> {direct, Info};
|
|
|
|
Error -> Error
|
|
|
|
end;
|
|
|
|
read_file_info(Path, {archive, Archive}) ->
|
|
|
|
case file:read_file_info(Archive, [{time, universal}]) of
|
|
|
|
{ok, ArchiveInfo} ->
|
|
|
|
%% The Erlang application archive is fine.
|
|
|
|
%% Now check if the requested file is in that
|
|
|
|
%% archive. We also need the file_info to merge
|
|
|
|
%% them with the archive's one.
|
|
|
|
PathS = binary_to_list(Path),
|
|
|
|
case erl_prim_loader:read_file_info(PathS) of
|
|
|
|
{ok, ContainedFileInfo} ->
|
|
|
|
Info = fix_archived_file_info(
|
|
|
|
ArchiveInfo,
|
|
|
|
ContainedFileInfo),
|
|
|
|
{archive, Info};
|
|
|
|
error ->
|
|
|
|
{error, enoent}
|
|
|
|
end;
|
|
|
|
Error ->
|
|
|
|
Error
|
|
|
|
end.
|
|
|
|
|
|
|
|
fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
|
|
|
|
%% We merge the archive and content #file_info because we are
|
|
|
|
%% interested by the timestamps of the archive, but the type and
|
|
|
|
%% size of the contained file/directory.
|
|
|
|
%%
|
|
|
|
%% We reset the access to 'read', because we won't rewrite the
|
|
|
|
%% archive.
|
|
|
|
ArchiveInfo#file_info{
|
|
|
|
size = ContainedFileInfo#file_info.size,
|
|
|
|
type = ContainedFileInfo#file_info.type,
|
|
|
|
access = read
|
|
|
|
}.
|
|
|
|
|
2011-12-28 18:02:32 +01:00
|
|
|
-ifdef(TEST).
|
2013-02-28 17:31:56 +01:00
|
|
|
fullpath_test_() ->
|
|
|
|
Tests = [
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy">>},
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy/">>},
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy/./">>},
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
|
|
|
|
{<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
|
|
|
|
{<<"/">>, <<"/home/cowboy/../../../../../..">>},
|
|
|
|
{<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
|
|
|
|
],
|
|
|
|
[{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
|
|
|
|
|
|
|
|
good_path_check_test_() ->
|
|
|
|
Tests = [
|
|
|
|
<<"/home/cowboy/file">>,
|
|
|
|
<<"/home/cowboy/file/">>,
|
|
|
|
<<"/home/cowboy/./file">>,
|
|
|
|
<<"/home/cowboy/././././././file">>,
|
|
|
|
<<"/home/cowboy/abc/../file">>,
|
|
|
|
<<"/home/cowboy/abc/../file">>,
|
|
|
|
<<"/home/cowboy/abc/./.././file">>
|
|
|
|
],
|
|
|
|
[{P, fun() ->
|
|
|
|
case fullpath(P) of
|
2014-10-03 18:25:29 +03:00
|
|
|
<< "/home/cowboy/", _/bits >> -> ok
|
2013-02-28 17:31:56 +01:00
|
|
|
end
|
|
|
|
end} || P <- Tests].
|
|
|
|
|
|
|
|
bad_path_check_test_() ->
|
|
|
|
Tests = [
|
|
|
|
<<"/home/cowboy/../../../../../../file">>,
|
|
|
|
<<"/home/cowboy/../../etc/passwd">>
|
|
|
|
],
|
|
|
|
[{P, fun() ->
|
|
|
|
error = case fullpath(P) of
|
2014-10-03 18:25:29 +03:00
|
|
|
<< "/home/cowboy/", _/bits >> -> ok;
|
2013-02-28 17:31:56 +01:00
|
|
|
_ -> error
|
|
|
|
end
|
|
|
|
end} || P <- Tests].
|
|
|
|
|
|
|
|
good_path_win32_check_test_() ->
|
|
|
|
Tests = case os:type() of
|
|
|
|
{unix, _} ->
|
|
|
|
[];
|
|
|
|
{win32, _} ->
|
|
|
|
[
|
|
|
|
<<"c:/home/cowboy/file">>,
|
|
|
|
<<"c:/home/cowboy/file/">>,
|
|
|
|
<<"c:/home/cowboy/./file">>,
|
|
|
|
<<"c:/home/cowboy/././././././file">>,
|
|
|
|
<<"c:/home/cowboy/abc/../file">>,
|
|
|
|
<<"c:/home/cowboy/abc/../file">>,
|
|
|
|
<<"c:/home/cowboy/abc/./.././file">>
|
|
|
|
]
|
|
|
|
end,
|
|
|
|
[{P, fun() ->
|
|
|
|
case fullpath(P) of
|
2014-10-03 18:25:29 +03:00
|
|
|
<< "c:/home/cowboy/", _/bits >> -> ok
|
2013-02-28 17:31:56 +01:00
|
|
|
end
|
|
|
|
end} || P <- Tests].
|
|
|
|
|
|
|
|
bad_path_win32_check_test_() ->
|
|
|
|
Tests = case os:type() of
|
|
|
|
{unix, _} ->
|
|
|
|
[];
|
|
|
|
{win32, _} ->
|
|
|
|
[
|
|
|
|
<<"c:/home/cowboy/../../secretfile.bat">>,
|
|
|
|
<<"c:/home/cowboy/c:/secretfile.bat">>,
|
|
|
|
<<"c:/home/cowboy/..\\..\\secretfile.bat">>,
|
|
|
|
<<"c:/home/cowboy/c:\\secretfile.bat">>
|
|
|
|
]
|
|
|
|
end,
|
|
|
|
[{P, fun() ->
|
|
|
|
error = case fullpath(P) of
|
2014-10-03 18:25:29 +03:00
|
|
|
<< "c:/home/cowboy/", _/bits >> -> ok;
|
2013-02-28 17:31:56 +01:00
|
|
|
_ -> error
|
|
|
|
end
|
|
|
|
end} || P <- Tests].
|
2011-12-28 18:02:32 +01:00
|
|
|
-endif.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Reject requests that tried to access a file outside
|
2013-11-02 14:41:46 +01:00
|
|
|
%% the target directory.
|
|
|
|
|
|
|
|
-spec malformed_request(Req, State)
|
|
|
|
-> {boolean(), Req, State}.
|
|
|
|
malformed_request(Req, State) ->
|
|
|
|
{State =:= error, Req, State}.
|
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Directories, files that can't be accessed at all and
|
2013-11-02 14:41:46 +01:00
|
|
|
%% files with no read flag are forbidden.
|
|
|
|
|
|
|
|
-spec forbidden(Req, State)
|
|
|
|
-> {boolean(), Req, State}
|
|
|
|
when State::state().
|
2016-12-29 15:48:06 +01:00
|
|
|
forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
|
2013-11-02 14:41:46 +01:00
|
|
|
{true, Req, State};
|
|
|
|
forbidden(Req, State={_, {error, eacces}, _}) ->
|
|
|
|
{true, Req, State};
|
2016-12-29 15:48:06 +01:00
|
|
|
forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
|
2013-11-02 14:41:46 +01:00
|
|
|
when Access =:= write; Access =:= none ->
|
|
|
|
{true, Req, State};
|
|
|
|
forbidden(Req, State) ->
|
|
|
|
{false, Req, State}.
|
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Detect the mimetype of the file.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
|
|
|
-spec content_types_provided(Req, State)
|
|
|
|
-> {[{binary(), get_file}], Req, State}
|
|
|
|
when State::state().
|
2018-11-19 12:15:40 +01:00
|
|
|
content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) ->
|
2013-11-02 14:41:46 +01:00
|
|
|
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.
|
|
|
|
|
2018-11-02 15:31:54 +01:00
|
|
|
%% Detect the charset of the file.
|
|
|
|
|
|
|
|
-spec charsets_provided(Req, State)
|
|
|
|
-> {[binary()], Req, State}
|
|
|
|
when State::state().
|
|
|
|
charsets_provided(Req, State={Path, _, Extra}) ->
|
|
|
|
case lists:keyfind(charset, 1, Extra) of
|
|
|
|
%% We simulate the callback not being exported.
|
|
|
|
false ->
|
|
|
|
no_call;
|
|
|
|
{charset, Module, Function} ->
|
|
|
|
{[Module:Function(Path)], Req, State};
|
|
|
|
{charset, Charset} ->
|
|
|
|
{[Charset], Req, State}
|
|
|
|
end.
|
|
|
|
|
2018-11-11 16:25:45 +01:00
|
|
|
%% Enable support for range requests.
|
|
|
|
|
|
|
|
-spec ranges_provided(Req, State)
|
|
|
|
-> {[{binary(), auto}], Req, State}
|
|
|
|
when State::state().
|
|
|
|
ranges_provided(Req, State) ->
|
|
|
|
{[{<<"bytes">>, auto}], Req, State}.
|
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Assume the resource doesn't exist if it's not a regular file.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
|
|
|
-spec resource_exists(Req, State)
|
|
|
|
-> {boolean(), Req, State}
|
|
|
|
when State::state().
|
2016-12-29 15:48:06 +01:00
|
|
|
resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
|
2013-11-02 14:41:46 +01:00
|
|
|
{true, Req, State};
|
|
|
|
resource_exists(Req, State) ->
|
|
|
|
{false, Req, State}.
|
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Generate an etag for the file.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
|
|
|
-spec generate_etag(Req, State)
|
|
|
|
-> {{strong | weak, binary()}, Req, State}
|
|
|
|
when State::state().
|
2016-12-29 15:48:06 +01:00
|
|
|
generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
|
2013-11-02 14:41:46 +01:00
|
|
|
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) ->
|
2014-07-12 14:19:29 +02:00
|
|
|
{strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Return the time of last modification of the file.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
|
|
|
-spec last_modified(Req, State)
|
|
|
|
-> {calendar:datetime(), Req, State}
|
|
|
|
when State::state().
|
2016-12-29 15:48:06 +01:00
|
|
|
last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
|
2013-11-02 14:41:46 +01:00
|
|
|
{Modified, Req, State}.
|
|
|
|
|
2014-03-26 19:05:59 +01:00
|
|
|
%% Stream the file.
|
2013-11-02 14:41:46 +01:00
|
|
|
|
|
|
|
-spec get_file(Req, State)
|
2017-01-02 16:47:16 +01:00
|
|
|
-> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
|
2013-11-02 14:41:46 +01:00
|
|
|
when State::state().
|
2016-12-29 15:48:06 +01:00
|
|
|
get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
|
|
|
|
{{sendfile, 0, Size, Path}, Req, State};
|
|
|
|
get_file(Req, State={Path, {archive, _}, _}) ->
|
|
|
|
PathS = binary_to_list(Path),
|
|
|
|
{ok, Bin, _} = erl_prim_loader:get_file(PathS),
|
|
|
|
{Bin, Req, State}.
|