From 2166733628bbab0eb77eeed58bdf204727c48ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Thu, 29 Dec 2016 15:48:06 +0100 Subject: 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). --- src/cowboy_static.erl | 121 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index b21c385..ae4e7c0 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -36,7 +36,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()}. %% Resolve the file that will be sent and get its file information. %% If the handler is configured to manage a directory, check that the @@ -52,13 +53,15 @@ init(Req, Opts) -> init_opts(Req, Opts). init_opts(Req, {priv_file, App, Path, Extra}) -> - init_info(Req, absname(priv_path(App, Path)), Extra); + {PrivPath, HowToAccess} = priv_path(App, Path), + init_info(Req, absname(PrivPath), HowToAccess, Extra); init_opts(Req, {file, Path, Extra}) -> - init_info(Req, absname(Path), Extra); + init_info(Req, absname(Path), direct, Extra); init_opts(Req, {priv_dir, App, Path, Extra}) -> - init_dir(Req, priv_path(App, Path), Extra); + {PrivPath, HowToAccess} = priv_path(App, Path), + init_dir(Req, PrivPath, HowToAccess, Extra); init_opts(Req, {dir, Path, Extra}) -> - init_dir(Req, Path, Extra). + init_dir(Req, Path, direct, Extra). priv_path(App, Path) -> case code:priv_dir(App) of @@ -66,9 +69,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) -> @@ -76,18 +112,18 @@ absname(Path) when is_list(Path) -> absname(Path) when is_binary(Path) -> filename:absname(Path). -init_dir(Req, Path, Extra) when is_list(Path) -> - init_dir(Req, list_to_binary(Path), Extra); -init_dir(Req, Path, Extra) -> +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) -> Dir = fullpath(filename:absname(Path)), PathInfo = cowboy_req:path_info(Req), Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> - init_info(Req, Filepath, Extra); + init_info(Req, Filepath, HowToAccess, Extra); << Dir:Len/binary >> -> - init_info(Req, Filepath, Extra); + init_info(Req, Filepath, HowToAccess, Extra); _ -> {cowboy_rest, Req, error} end. @@ -120,10 +156,49 @@ fullpath([<<"..">>|Tail], [_|Acc]) -> fullpath([Segment|Tail], Acc) -> fullpath(Tail, [Segment|Acc]). -init_info(Req, Path, Extra) -> - Info = file:read_file_info(Path, [{time, universal}]), +init_info(Req, Path, HowToAccess, Extra) -> + Info = read_file_info(Path, HowToAccess), {cowboy_rest, 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 = [ @@ -222,11 +297,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) -> @@ -252,7 +327,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}. @@ -262,7 +337,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 -> @@ -281,7 +356,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. @@ -289,5 +364,9 @@ last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) -> -spec get_file(Req, State) -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State} when State::state(). -get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) -> - {{sendfile, 0, Size, Path}, Req, State}. +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}. -- cgit v1.2.3