aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJean-Sébastien Pédron <[email protected]>2016-12-29 15:48:06 +0100
committerLoïc Hoguin <[email protected]>2017-01-31 16:19:45 +0100
commit98c2bc64e5a858db68f260ddd04fed7cee3d80f8 (patch)
tree7e9c8420fca29fca6027a08d310cbd25aa69e45a /src
parenta62cc4260fd071d522d962ea0a302f6f995152aa (diff)
downloadcowboy-98c2bc64e5a858db68f260ddd04fed7cee3d80f8.tar.gz
cowboy-98c2bc64e5a858db68f260ddd04fed7cee3d80f8.tar.bz2
cowboy-98c2bc64e5a858db68f260ddd04fed7cee3d80f8.zip
cowboy_static: Add support for files in EZ archives
If cowboy_static is initialized with `{priv_file, ...}` or `{priv_dir, ...}`, it is now able to read files from Erlang application .ez archives. When serving a file from an archive, the #file_info{} comes from the archive, not the contained file, except for the size and type. The erl_prim_loader module is used to read the latter's #file_info{} and the actual file content (ie. sendfile(2) is not used in this case). (cherry picked from commit 2166733628bbab0eb77eeed58bdf204727c48ec6)
Diffstat (limited to 'src')
-rw-r--r--src/cowboy_static.erl120
1 files changed, 101 insertions, 19 deletions
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index fae4568..dde99e3 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -37,7 +37,8 @@
-include_lib("kernel/include/file.hrl").
--type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}.
+-type state() :: {binary(), {direct | archive, #file_info{}}
+ | {error, atom()}, extra()}.
-spec init(_, _, _) -> {upgrade, protocol, cowboy_rest}.
init(_, _, _) ->
@@ -59,13 +60,15 @@ 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);
+ {PrivPath, HowToAccess} = priv_path(App, Path),
+ rest_init_info(Req, absname(PrivPath), HowToAccess, Extra);
rest_init_opts(Req, {file, Path, Extra}) ->
- rest_init_info(Req, absname(Path), Extra);
+ rest_init_info(Req, absname(Path), direct, Extra);
rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
- rest_init_dir(Req, priv_path(App, Path), Extra);
+ {PrivPath, HowToAccess} = priv_path(App, Path),
+ rest_init_dir(Req, PrivPath, HowToAccess, Extra);
rest_init_opts(Req, {dir, Path, Extra}) ->
- rest_init_dir(Req, Path, Extra).
+ rest_init_dir(Req, Path, direct, Extra).
priv_path(App, Path) ->
case code:priv_dir(App) of
@@ -73,9 +76,42 @@ priv_path(App, Path) ->
error({badarg, "Can't resolve the priv_dir of application "
++ atom_to_list(App)});
PrivDir when is_list(Path) ->
- PrivDir ++ "/" ++ Path;
+ {
+ PrivDir ++ "/" ++ Path,
+ how_to_access_app_priv(PrivDir)
+ };
PrivDir when is_binary(Path) ->
- << (list_to_binary(PrivDir))/binary, $/, Path/binary >>
+ {
+ << (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
end.
absname(Path) when is_list(Path) ->
@@ -83,16 +119,16 @@ absname(Path) when is_list(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) ->
+rest_init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
+ rest_init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
+rest_init_dir(Req, Path, HowToAccess, 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);
+ rest_init_info(Req2, Filepath, HowToAccess, Extra);
_ ->
{ok, Req2, error}
end.
@@ -110,10 +146,49 @@ fullpath([<<"..">>|Tail], [_|Acc]) ->
fullpath([Segment|Tail], Acc) ->
fullpath(Tail, [Segment|Acc]).
-rest_init_info(Req, Path, Extra) ->
- Info = file:read_file_info(Path, [{time, universal}]),
+rest_init_info(Req, Path, HowToAccess, Extra) ->
+ Info = read_file_info(Path, HowToAccess),
{ok, Req, {Path, Info, Extra}}.
+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
+ }.
+
-ifdef(TEST).
fullpath_test_() ->
Tests = [
@@ -212,11 +287,11 @@ malformed_request(Req, State) ->
-spec forbidden(Req, State)
-> {boolean(), Req, State}
when State::state().
-forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) ->
+forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) ->
{true, Req, State};
forbidden(Req, State={_, {error, eacces}, _}) ->
{true, Req, State};
-forbidden(Req, State={_, {ok, #file_info{access=Access}}, _})
+forbidden(Req, State={_, {_, #file_info{access=Access}}, _})
when Access =:= write; Access =:= none ->
{true, Req, State};
forbidden(Req, State) ->
@@ -242,7 +317,7 @@ content_types_provided(Req, State={Path, _, Extra}) ->
-spec resource_exists(Req, State)
-> {boolean(), Req, State}
when State::state().
-resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) ->
+resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) ->
{true, Req, State};
resource_exists(Req, State) ->
{false, Req, State}.
@@ -252,7 +327,7 @@ resource_exists(Req, State) ->
-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}},
+generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
Extra}) ->
case lists:keyfind(etag, 1, Extra) of
false ->
@@ -271,7 +346,7 @@ generate_default_etag(Size, Mtime) ->
-spec last_modified(Req, State)
-> {calendar:datetime(), Req, State}
when State::state().
-last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
+last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
{Modified, Req, State}.
%% Stream the file.
@@ -280,7 +355,7 @@ last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
-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}}, _}) ->
+get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
Sendfile = fun (Socket, Transport) ->
case Transport:sendfile(Socket, Path) of
{ok, _} -> ok;
@@ -288,4 +363,11 @@ get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) ->
{error, etimedout} -> ok
end
end,
+ {{stream, Size, Sendfile}, Req, State};
+get_file(Req, State={Path, {archive, #file_info{size=Size}}, _}) ->
+ Sendfile = fun (Socket, Transport) ->
+ PathS = binary_to_list(Path),
+ {ok, Bin, _} = erl_prim_loader:get_file(PathS),
+ Transport:send(Socket, Bin)
+ end,
{{stream, Size, Sendfile}, Req, State}.