/modules/mod_base/resources/resource_lib.erl

http://github.com/zotonic/zotonic · Erlang · 285 lines · 223 code · 36 blank · 26 comment · 3 complexity · d37a88e884966c5a380ea2623c3abfea MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2009 Marc Worrell
  3. %% @doc Serve static library files (css and js). Library files can be combined in one path, using z_lib_include:tag/2
  4. %%
  5. %% Serves files like: /lib/some/path
  6. %% Copyright 2009-2011 Marc Worrell, Konstantin Nikiforov
  7. %%
  8. %% Licensed under the Apache License, Version 2.0 (the "License");
  9. %% you may not use this file except in compliance with the License.
  10. %% You may obtain a copy of the License at
  11. %%
  12. %% http://www.apache.org/licenses/LICENSE-2.0
  13. %%
  14. %% Unless required by applicable law or agreed to in writing, software
  15. %% distributed under the License is distributed on an "AS IS" BASIS,
  16. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. %% See the License for the specific language governing permissions and
  18. %% limitations under the License.
  19. -module(resource_lib).
  20. -export([init/1]).
  21. -export([allowed_methods/2,
  22. resource_exists/2,
  23. last_modified/2,
  24. expires/2,
  25. content_types_provided/2,
  26. charsets_provided/2,
  27. encodings_provided/2,
  28. provide_content/2,
  29. finish_request/2
  30. ]).
  31. -include_lib("webmachine_resource.hrl").
  32. -include_lib("zotonic.hrl").
  33. %% These are used for file serving (move to metadata)
  34. -record(state, {
  35. root=undefined,
  36. content_disposition=undefined,
  37. use_cache=false,
  38. encode_data=false,
  39. fullpaths=undefined,
  40. is_cached=false,
  41. path=undefined,
  42. mime=undefined,
  43. last_modified=undefined,
  44. body=undefined
  45. }).
  46. -record(cache, {
  47. path=undefined,
  48. fullpaths=undefined,
  49. mime=undefined,
  50. last_modified=undefined,
  51. body=undefined
  52. }).
  53. -define(MAX_AGE, 315360000).
  54. init(ConfigProps) ->
  55. UseCache = proplists:get_value(use_cache, ConfigProps, false),
  56. Root = proplists:get_value(root, ConfigProps, [lib]),
  57. ContentDisposition = proplists:get_value(content_disposition, ConfigProps),
  58. {ok, #state{root=Root, use_cache=UseCache, content_disposition=ContentDisposition}}.
  59. allowed_methods(ReqData, State) ->
  60. {['HEAD', 'GET'], ReqData, State}.
  61. content_types_provided(ReqData, State) ->
  62. State1 = lookup_path(ReqData, State),
  63. case State1#state.mime of
  64. undefined ->
  65. Path = mochiweb_util:unquote(wrq:disp_path(ReqData)),
  66. CT = z_media_identify:guess_mime(Path),
  67. {[{CT, provide_content}], ReqData, State1#state{mime=CT}};
  68. Mime ->
  69. {[{Mime, provide_content}], ReqData, State1}
  70. end.
  71. encodings_provided(ReqData, State) ->
  72. State1 = lookup_path(ReqData, State),
  73. Encodings = case z_media_identify:is_mime_compressed(State1#state.mime) of
  74. true ->
  75. [{"identity", fun(Data) -> Data end}];
  76. false ->
  77. [{"identity", fun(Data) -> decode_data(identity, Data) end},
  78. {"gzip", fun(Data) -> decode_data(gzip, Data) end}]
  79. end,
  80. EncodeData = length(Encodings) > 1,
  81. {Encodings, ReqData, State1#state{encode_data=EncodeData}}.
  82. resource_exists(ReqData, State) ->
  83. State1 = lookup_path(ReqData, State),
  84. case State1#state.path of
  85. none -> {false, ReqData, State1};
  86. _ -> {true, ReqData, State1}
  87. end.
  88. charsets_provided(ReqData, State) ->
  89. State1 = lookup_path(ReqData, State),
  90. case is_text(State1#state.mime) of
  91. true -> {[{"utf-8", fun(X) -> X end}], ReqData, State1};
  92. _ -> {no_charset, ReqData, State1}
  93. end.
  94. last_modified(ReqData, State) ->
  95. State1 = lookup_path(ReqData, State),
  96. RD1 = wrq:set_resp_header("Cache-Control", "public, max-age="++integer_to_list(?MAX_AGE), ReqData),
  97. case State1#state.last_modified of
  98. undefined ->
  99. LMod = max_last_modified(State1#state.fullpaths, {{1970,1,1},{12,0,0}}),
  100. [LModUTC|_] = calendar:local_time_to_universal_time_dst(LMod),
  101. {LModUTC, RD1, State1#state{last_modified=LModUTC}};
  102. LModUTC ->
  103. {LModUTC, RD1, State1}
  104. end.
  105. %% @doc Find the latest modification time of a list of files.
  106. max_last_modified([], Mod) ->
  107. Mod;
  108. max_last_modified([F|Rest], Mod) ->
  109. LMod = filelib:last_modified(F),
  110. case LMod > Mod of
  111. true -> max_last_modified(Rest, LMod);
  112. false -> max_last_modified(Rest, Mod)
  113. end.
  114. expires(ReqData, State) ->
  115. NowSecs = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
  116. {calendar:gregorian_seconds_to_datetime(NowSecs + ?MAX_AGE), ReqData, State}.
  117. provide_content(ReqData, State) ->
  118. State1 = lookup_path(ReqData, State),
  119. RD1 = case State1#state.content_disposition of
  120. inline -> wrq:set_resp_header("Content-Disposition", "inline", ReqData);
  121. attachment -> wrq:set_resp_header("Content-Disposition", "attachment", ReqData);
  122. undefined -> ReqData
  123. end,
  124. {Content, State2} = case State1#state.body of
  125. undefined ->
  126. Data = [ read_data(F) || F <- State1#state.fullpaths ],
  127. Data1 = case State1#state.mime of
  128. "text/javascript" -> z_utils:combine([$;, $\n], Data);
  129. "application/x-javascript" -> z_utils:combine([$;, $\n], Data);
  130. _ -> z_utils:combine($\n, Data)
  131. end,
  132. Body = case State1#state.encode_data of
  133. true -> encode_data(Data1);
  134. false -> Data1
  135. end,
  136. {Body, State1#state{body=Body}};
  137. Body ->
  138. {Body, State1}
  139. end,
  140. {Content, RD1, State2}.
  141. read_data(F) ->
  142. {ok, Data} = file:read_file(F),
  143. Data.
  144. finish_request(ReqData, State) ->
  145. case State#state.is_cached of
  146. false ->
  147. case State#state.body of
  148. undefined ->
  149. {ok, ReqData, State};
  150. _ ->
  151. case State#state.use_cache andalso State#state.encode_data of
  152. true ->
  153. % Cache the served file in the depcache. Cache it for 3600 secs.
  154. Cache = #cache{
  155. path=State#state.path,
  156. fullpaths=State#state.fullpaths,
  157. mime=State#state.mime,
  158. last_modified=State#state.last_modified,
  159. body=State#state.body
  160. },
  161. Context = z_context:new(ReqData, ?MODULE),
  162. z_depcache:set(cache_key(State#state.path), Cache, Context),
  163. {ok, ReqData, State};
  164. _ ->
  165. % No cache or no gzip'ed version (file system cache is fast enough for image serving)
  166. {ok, ReqData, State}
  167. end
  168. end;
  169. true ->
  170. {ok, ReqData, State}
  171. end.
  172. %%%%%%%%%%%%%% Helper functions %%%%%%%%%%%%%%
  173. cache_key(Path) ->
  174. {resource_file, Path}.
  175. file_exists(_State, [], _Context) ->
  176. false;
  177. file_exists(State, Name, Context) ->
  178. RelName = case hd(Name) of
  179. $/ -> tl(Name);
  180. _ -> Name
  181. end,
  182. case mochiweb_util:safe_relative_path(RelName) of
  183. undefined -> false;
  184. SafePath ->
  185. RelName = case hd(SafePath) of
  186. "/" -> tl(SafePath);
  187. _ -> SafePath
  188. end,
  189. file_exists1(State#state.root, RelName, Context)
  190. end.
  191. file_exists1([], _RelName, _Context) ->
  192. false;
  193. file_exists1([ModuleIndex|T], RelName, Context) when is_atom(ModuleIndex) ->
  194. case z_module_indexer:find(ModuleIndex, RelName, Context) of
  195. {ok, File} -> {true, File};
  196. {error, _} -> file_exists1(T, RelName, Context)
  197. end;
  198. file_exists1([DirName|T], RelName, Context) ->
  199. NamePath = filename:join([DirName,RelName]),
  200. case filelib:is_regular(NamePath) of
  201. true ->
  202. {true, NamePath};
  203. false ->
  204. file_exists1(T, RelName, Context)
  205. end.
  206. %% @spec is_text(Mime) -> bool()
  207. %% @doc Check if a mime type is textual
  208. is_text("text/" ++ _) -> true;
  209. is_text("application/x-javascript") -> true;
  210. is_text("application/xhtml+xml") -> true;
  211. is_text("application/xml") -> true;
  212. is_text(_Mime) -> false.
  213. lookup_path(ReqData, State = #state{path=undefined}) ->
  214. Context = z_context:new(ReqData, ?MODULE),
  215. Path = mochiweb_util:unquote(wrq:disp_path(ReqData)),
  216. Cached = case State#state.use_cache of
  217. true -> z_depcache:get(cache_key(Path), Context);
  218. _ -> undefined
  219. end,
  220. case Cached of
  221. undefined ->
  222. Paths = z_lib_include:uncollapse(Path),
  223. FullPaths = [ file_exists(State, P, Context) || P <- Paths ],
  224. FullPaths1 = [ P || {true, P} <- FullPaths ],
  225. case FullPaths1 of
  226. [] -> State#state{path=none};
  227. _ -> State#state{path=Path, fullpaths=FullPaths1}
  228. end;
  229. {ok, Cache} ->
  230. State#state{
  231. is_cached=true,
  232. path=Cache#cache.path,
  233. fullpaths=Cache#cache.fullpaths,
  234. mime=Cache#cache.mime,
  235. last_modified=Cache#cache.last_modified,
  236. body=Cache#cache.body
  237. }
  238. end;
  239. lookup_path(_ReqData, State) ->
  240. State.
  241. %% Encode the data so that the identity variant comes first and then the gzip'ed variant
  242. encode_data(Data) when is_list(Data) ->
  243. encode_data(iolist_to_binary(Data));
  244. encode_data(Data) when is_binary(Data) ->
  245. {Data, zlib:gzip(Data)}.
  246. decode_data(identity, {Data, _Gzip}) ->
  247. Data;
  248. decode_data(gzip, {_Data, Gzip}) ->
  249. Gzip.