PageRenderTime 45ms CodeModel.GetById 17ms app.highlight 23ms RepoModel.GetById 2ms app.codeStats 0ms

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