PageRenderTime 51ms CodeModel.GetById 20ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/mod_base/resources/resource_file_readonly.erl

http://github.com/zotonic/zotonic
Erlang | 452 lines | 356 code | 48 blank | 48 comment | 0 complexity | df367c9d5e160ae9d5f95557f8a22668 MD5 | raw file
  1%% @author Marc Worrell <marc@worrell.nl>
  2%% @copyright 2009-2010 Marc Worrell
  3%%
  4%% @doc Serve static (image) files from a configured list of directories or template lookup keys.  Caches files in the local depcache.
  5%% Is also able to generate previews (if configured to do so).
  6
  7%% Copyright 2009-2010 Marc Worrell
  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%% Serves files like:
 22%% 
 23%% /image/2007/03/31/wedding.jpg(300x300)(crop-center)(709a-a3ab6605e5c8ce801ac77eb76289ac12).jpg
 24%% /media/inline/<filepath>
 25%% /media/attachment/<filepath>
 26
 27-module(resource_file_readonly).
 28-export([
 29    init/1,
 30    service_available/2,
 31    allowed_methods/2,
 32    resource_exists/2,
 33    forbidden/2,
 34    last_modified/2,
 35    expires/2,
 36    content_types_provided/2,
 37    charsets_provided/2,
 38    encodings_provided/2,
 39    provide_content/2,
 40    finish_request/2
 41]).
 42
 43-include_lib("webmachine_resource.hrl").
 44-include_lib("zotonic.hrl").
 45
 46-record(cache, {path, fullpath, mime, last_modified, body}).
 47
 48-define(MAX_AGE, 315360000).
 49-define(CHUNKED_CONTENT_LENGTH, 1048576).
 50-define(CHUNK_LENGTH, 65536).
 51
 52init(ConfigProps) ->
 53    {ok, ConfigProps}.
 54
 55
 56%% @doc Initialize the context for the request. Continue session when available.
 57service_available(ReqData, ConfigProps) ->
 58    Context = z_context:set(ConfigProps, z_context:new(ReqData)),
 59    Context1 = z_context:ensure_qs(z_context:continue_session(Context)),
 60    
 61    try ensure_file_info(ReqData, Context1) of
 62        {_, ContextFile} ->
 63            % Use chunks for large files
 64            case z_context:get(fullpath, ContextFile) of
 65                undefined -> 
 66                    ?WM_REPLY(true, ContextFile);
 67                FullPath ->
 68                    case catch filelib:file_size(FullPath) of
 69                        N when is_integer(N) ->
 70                            case N > ?CHUNKED_CONTENT_LENGTH of
 71                                true -> 
 72                                    ContextChunked = z_context:set([{chunked, true}, {file_size, N}], ContextFile), 
 73                                    ?WM_REPLY(true, ContextChunked);
 74                                false ->
 75                                    ContextSize = z_context:set([{file_size, N}], ContextFile), 
 76                                    ?WM_REPLY(true, ContextSize)
 77                            end;
 78                        _ ->
 79                            ?WM_REPLY(true, ContextFile)
 80                    end
 81            end
 82    catch 
 83        _:checksum_invalid ->
 84            %% Not a nice solution, but since 'resource_exists'
 85            %% are checked much later in the wm flow, we would otherwise 
 86            %% have to break the logical flow, and introduce some ugly
 87            %% condition checking in the intermediate callback functions.            
 88            ?WM_REPLY(false, Context1)
 89    end.
 90
 91
 92allowed_methods(ReqData, Context) ->
 93    {['HEAD', 'GET'], ReqData, Context}.
 94
 95content_types_provided(ReqData, Context) ->
 96    {[{z_context:get(mime, Context), provide_content}], ReqData, Context}.
 97
 98%% @doc Simple access control for rsc based files
 99forbidden(ReqData, Context) ->
100    case z_context:get(id, Context) of
101        undefined ->
102            case z_context:get(root, Context) of
103                [{module, Module}] ->
104                    {Module:file_forbidden(z_context:get(fullpath, Context), Context), ReqData, Context};
105                _ ->
106                    {false, ReqData, Context}
107            end;
108        RscId when is_integer(RscId) ->
109            {not z_acl:rsc_visible(RscId, Context), ReqData, Context}
110    end.
111
112
113encodings_provided(ReqData, Context) ->
114    Encodings = case z_context:get(chunked, Context) of
115                    true ->
116                        [{"identity", fun(Data) -> Data end}];
117                    _ ->
118                        case z_context:get(mime, Context) of
119                            "image/" ++ _ ->  [{"identity", fun(Data) -> Data end}];
120                            "video/" ++ _ ->  [{"identity", fun(Data) -> Data end}];
121                            "audio/" ++ _ ->  [{"identity", fun(Data) -> Data end}];
122                            "application/x-gzip" ++ _ -> [{"identity", fun(Data) -> Data end}];
123                            "application/zip" ++ _ -> [{"identity", fun(Data) -> Data end}];
124                            _ -> 
125                                [{"identity", fun(Data) -> decode_data(identity, Data) end},
126                                 {"gzip",     fun(Data) -> decode_data(gzip, Data) end}]
127                        end
128                end,
129    {Encodings, ReqData, z_context:set(encode_data, length(Encodings) > 1, Context)}.
130
131resource_exists(ReqData, Context) ->
132    {z_context:get(fullpath, Context) =/= undefined, ReqData, Context}.
133
134charsets_provided(ReqData, Context) ->
135    case is_text(z_context:get(mime, Context)) of
136        true -> {[{"utf-8", fun(X) -> X end}], ReqData, Context};
137        _ -> {no_charset, ReqData, Context}
138    end.
139    
140last_modified(ReqData, Context) ->
141    RD1 = case z_context:get(id, Context) of
142        undefined ->
143            wrq:set_resp_header("Cache-Control", "public, max-age="++integer_to_list(?MAX_AGE), ReqData);
144        RscId when is_integer(RscId) ->
145            case is_public(RscId, Context) of
146                true ->
147                    % Public
148                    wrq:set_resp_header("Cache-Control", "public, max-age="++integer_to_list(?MAX_AGE), ReqData);
149                false ->
150                    % Not public
151                    wrq:set_resp_header("Cache-Control", "private, max-age=0, must-revalidate, post-check=0, pre-check=0", ReqData)
152            end
153    end,
154    case z_context:get(last_modified, Context) of
155        undefined -> 
156            LMod = filelib:last_modified(z_context:get(fullpath, Context)),
157            [LModUTC|_] = calendar:local_time_to_universal_time_dst(LMod),
158            {LModUTC, RD1, z_context:set(last_modified, LModUTC, Context)};
159        LModUTC ->
160            {LModUTC, RD1, Context}
161    end.
162
163expires(ReqData, Context) ->
164    NowSecs = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
165    {calendar:gregorian_seconds_to_datetime(NowSecs + ?MAX_AGE), ReqData, Context}.
166
167provide_content(ReqData, Context) ->
168    RD1 = case z_context:get(content_disposition, Context) of
169              inline ->     wrq:set_resp_header("Content-Disposition", "inline", ReqData);
170              attachment -> wrq:set_resp_header("Content-Disposition", "attachment", ReqData);
171              undefined ->  ReqData
172          end,
173    case z_context:get(body, Context) of
174        undefined ->
175            case z_context:get(chunked, Context) of
176                true ->
177                    {ok, Device} = file:open(z_context:get(fullpath, Context), [read,raw,binary]),
178                    FileSize = z_context:get(file_size, Context),
179                    {   {stream, read_chunk(0, FileSize, Device)}, 
180                     wrq:set_resp_header("Content-Length", integer_to_list(FileSize), RD1),
181                     z_context:set(use_cache, false, Context) };
182                _ ->
183                    {ok, Data} = file:read_file(z_context:get(fullpath, Context)),
184                    Body = case z_context:get(encode_data, Context, false) of 
185                               true -> encode_data(Data);
186                               false -> Data
187                           end,
188                    {Body, RD1, z_context:set(body, Body, Context)}
189            end;
190        Body -> 
191            {Body, RD1, Context}
192    end.
193    
194    
195read_chunk(Offset, Size, Device) when Offset =:= Size ->
196    file:close(Device),
197    {<<>>, done};
198read_chunk(Offset, Size, Device) when Size - Offset =< ?CHUNK_LENGTH ->
199    {ok, Data} = file:read(Device, Size - Offset),
200    file:close(Device),
201    {Data, done};
202read_chunk(Offset, Size, Device) ->
203    {ok, Data} = file:read(Device, ?CHUNK_LENGTH),
204    {Data, fun() -> read_chunk(Offset+?CHUNK_LENGTH, Size, Device) end}.
205
206
207finish_request(ReqData, Context) ->
208    case z_context:get(is_cached, Context) of
209        false ->
210            case z_context:get(body, Context) of
211                undefined ->  
212                    {ok, ReqData, Context};
213                Body ->
214                    case z_context:get(use_cache, Context, false) andalso z_context:get(encode_data, Context, false) of
215                        true ->
216                            % Cache the served file in the depcache.  Cache it for 3600 secs.
217                            Path = z_context:get(path, Context),
218                            Cache = #cache{
219                                path=Path,
220                                fullpath=z_context:get(fullpath, Context),
221                                mime=z_context:get(mime, Context),
222                                last_modified=z_context:get(last_modified, Context),
223                                body=Body
224                            },
225                            z_depcache:set(cache_key(Path), Cache, Context),
226                            {ok, ReqData, Context};
227                        _ ->
228                            % No cache or no gzip'ed version (file system cache is fast enough for image serving)
229                            {ok, ReqData, Context}
230                    end
231            end;
232        true ->
233            {ok, ReqData, Context}
234    end.
235
236
237%%%%%%%%%%%%%% Helper functions %%%%%%%%%%%%%%
238
239%% @doc Find the file referred to by the reqdata or the preconfigured path
240ensure_file_info(ReqData, Context) ->
241    {Path, ContextPath} = case z_context:get(path, Context) of
242                             undefined ->
243                                 FilePath = mochiweb_util:safe_relative_path(mochiweb_util:unquote(wrq:disp_path(ReqData))),
244                                 rsc_media_check(FilePath, Context);
245                             id ->
246                                 RscId = m_rsc:rid(z_context:get_q("id", Context), Context),
247                                 ContextRsc = z_context:set(id, RscId, Context),
248                                 case m_media:get(RscId, ContextRsc) of
249                                     undefined ->
250                                         {undefined, ContextRsc};
251                                     Media ->
252                                         {z_convert:to_list(proplists:get_value(filename, Media)),
253                                          z_context:set(mime, z_convert:to_list(proplists:get_value(mime, Media)), ContextRsc)}
254                                 end;
255                             ConfiguredPath ->
256                                 {ConfiguredPath, Context}
257                         end,
258
259    Cached = case z_context:get(use_cache, ContextPath) of
260                 true -> z_depcache:get(cache_key(Path), ContextPath);
261                 _    -> undefined
262             end,
263    case Cached of
264        undefined ->
265            ContextMime = case z_context:get(mime, ContextPath) of
266                              undefined -> z_context:set(mime, z_media_identify:guess_mime(Path), ContextPath);
267                              _Mime -> ContextPath
268                          end,
269            case file_exists(Path, ContextMime) of 
270                {true, FullPath} ->
271                    {true, z_context:set([ {path, Path}, {fullpath, FullPath} ], ContextMime)};
272                _ -> 
273                    %% We might be able to generate a new preview
274                    case z_context:get(is_media_preview, ContextMime, false) of
275                        true ->
276                            % Generate a preview, recurse on success
277                            ensure_preview(Path, ContextMime);
278                        false ->
279                            {false, ContextMime}
280                    end
281            end;
282        {ok, Cache} ->
283            {true, z_context:set([ {is_cached, true},
284                                            {path, Cache#cache.path},
285                                            {fullpath, Cache#cache.fullpath},
286                                            {mime, Cache#cache.mime},
287                                            {last_modified, Cache#cache.last_modified},
288                                            {body, Cache#cache.body}
289                                          ],
290                                          ContextPath)}
291    end.
292
293
294rsc_media_check(undefined, Context) ->
295    {undefined, Context};
296rsc_media_check(File, Context) ->
297    {BaseFile, IsResized, Context1} = case lists:member($(, File) of
298                            true ->
299                                {File1, Proplists, Check, Prop} = z_media_tag:url2props(File, Context),
300                                {File1, true, z_context:set(media_tag_url2props, {File1, Proplists, Check, Prop}, Context)};
301                            false ->
302                                {File, false, Context}
303                          end,
304    case m_media:get_by_filename(BaseFile, Context1) of
305        undefined ->
306            {File, Context1};
307        Media ->
308            MimeOriginal = z_convert:to_list(proplists:get_value(mime, Media)),
309            Props = [
310                {id, proplists:get_value(id, Media)},
311                {mime_original, MimeOriginal}
312            ],
313            Props1 = case IsResized of 
314                        true -> [ {mime, z_media_identify:guess_mime(File)} | Props ];
315                        false -> [ {mime, MimeOriginal} | Props ]
316                     end,
317            {File, z_context:set(Props1, Context1)}
318    end.
319
320
321
322cache_key(Path) ->
323    {resource_file, Path}.
324
325file_exists(undefined, _Context) ->
326    false;
327file_exists([], _Context) ->
328    false;
329file_exists(Name, Context) ->
330    RelName = case hd(Name) of
331                  $/ -> tl(Name);
332                  _ -> Name
333              end,
334    case mochiweb_util:safe_relative_path(RelName) of
335        undefined -> false;
336        SafePath ->
337            RelName = case hd(SafePath) of
338                          "/" -> tl(SafePath);
339                          _ -> SafePath
340                      end,
341            Root = case z_context:get(root, Context) of
342                       undefined -> 
343                           case z_context:get(is_media_preview, Context, false) of
344                               true  -> [z_path:media_preview(Context)];
345                               false -> [z_path:media_archive(Context)]
346                           end;
347                       ConfRoot -> ConfRoot
348                   end,
349            file_exists1(Root, RelName, Context)
350    end.
351
352file_exists1([], _RelName, _Context) ->
353    false;
354file_exists1([ModuleIndex|T], RelName, Context) when is_atom(ModuleIndex) ->
355    case z_module_indexer:find(ModuleIndex, RelName, Context) of
356        {ok, File} -> {true, File};
357        {error, _} -> file_exists1(T, RelName, Context)
358    end;
359file_exists1([{module, Module}|T], RelName, Context) ->
360    case Module:file_exists(RelName, Context) of
361        false -> file_exists1(T, RelName, Context);
362        Result -> Result
363    end;
364file_exists1([DirName|T], RelName, Context) ->
365    NamePath = filename:join([DirName,RelName]),
366    case filelib:is_regular(NamePath) of 
367	true ->
368	    {true, NamePath};
369	false ->
370	    file_exists1(T, RelName, Context)
371    end.
372
373
374%% @spec is_text(Mime) -> bool()
375%% @doc Check if a mime type is textual
376is_text("text/" ++ _) -> true;
377is_text("application/x-javascript") -> true;
378is_text("application/xhtml+xml") -> true;
379is_text("application/xml") -> true;
380is_text(_Mime) -> false.
381
382
383%% @spec ensure_preview(Path, Context) -> {Boolean, NewContext}
384%% @doc Generate the file on the path from an archived media file.
385%% The path is like: 2007/03/31/wedding.jpg(300x300)(crop-center)(709a-a3ab6605e5c8ce801ac77eb76289ac12).jpg
386%% The original media should be in State#media_path (or z_path:media_archive)
387%% The generated image should be created in State#root (or z_path:media_preview)
388ensure_preview(Path, Context) ->
389    UrlProps = case z_context:get(media_tag_url2props,Context) of
390                    undefined -> z_media_tag:url2props(Path, Context);
391                    MediaInfo -> MediaInfo
392                end,
393    case UrlProps of
394        error -> 
395            {false, Context};
396        {Filepath, PreviewPropList, _Checksum, _ChecksumBaseString} ->
397            case mochiweb_util:safe_relative_path(Filepath) of
398                undefined ->
399                    {false, Context};
400                Safepath  ->
401                    MediaPath = case z_context:get(media_path, Context) of
402                                    undefined -> z_path:media_archive(Context);
403                                    ConfMediaPath -> ConfMediaPath
404                                end,
405            
406                    MediaFile = case Safepath of 
407                                    "lib/" ++ LibPath ->  
408                                        case z_module_indexer:find(lib, LibPath, Context) of 
409                                            {ok, ModuleFilename} -> ModuleFilename; 
410                                            {error, _} -> filename:join(MediaPath, Safepath) 
411                                        end; 
412                                    _ -> 
413                                        filename:join(MediaPath, Safepath) 
414                                end,
415                    case filelib:is_regular(MediaFile) of
416                        true ->
417                            % Media file exists, perform the resize
418                            Root = case z_context:get(root, Context) of
419                                       [ConfRoot|_] -> ConfRoot;
420                                       _ -> z_path:media_preview(Context)
421                                   end,
422                            PreviewFile = filename:join(Root, Path),
423                            case z_media_preview:convert(MediaFile, PreviewFile, PreviewPropList, Context) of
424                                ok -> {true, z_context:set(fullpath, PreviewFile, Context)};
425                                {error, Reason} -> throw(Reason)
426                            end;
427                        false ->
428                            {false, Context}
429                    end
430            end
431    end.
432
433
434%% Encode the data so that the identity variant comes first and then the gzip'ed variant
435encode_data(Data) when is_binary(Data) ->
436    {Data, zlib:gzip(Data)}.
437
438decode_data(gzip, Data) when is_binary(Data) ->
439    zlib:gzip(Data);
440decode_data(identity, Data) when is_binary(Data) ->
441    Data;
442decode_data(identity, {Data, _Gzip}) ->
443    Data;
444decode_data(gzip, {_Data, Gzip}) ->
445    Gzip.
446
447
448%% @doc Check if a resource is publicly viewable
449is_public(RscId, Context) ->
450    z_acl:rsc_visible(RscId, z_context:new(Context)).
451
452