diff --git a/mod_http_upload/README.txt b/mod_http_upload/README.txt index dd2c3f5..49d503c 100644 --- a/mod_http_upload/README.txt +++ b/mod_http_upload/README.txt @@ -84,6 +84,13 @@ The configurable mod_http_upload options are: mod_http_upload. Otherwise, a SHA-1 hash of the user's bare JID is included instead. +- thumbnail: (default: 'true') + + This option specifies whether ejabberd should create thumbnails of + uploaded images. If a thumbnail is created, a element that + contains the download and some metadata is returned with the PUT + response. + - file_mode (default: 'undefined') This option defines the permission bits of uploaded files. The bits are diff --git a/mod_http_upload/src/mod_http_upload.erl b/mod_http_upload/src/mod_http_upload.erl index ab76b62..e15e742 100644 --- a/mod_http_upload/src/mod_http_upload.erl +++ b/mod_http_upload/src/mod_http_upload.erl @@ -90,10 +90,17 @@ put_url :: binary(), get_url :: binary(), service_url :: binary() | undefined, + thumbnail :: boolean(), slots = dict:new() :: term()}). % dict:dict() requires Erlang 17. +-record(media_info, + {type :: string(), + height :: integer(), + width :: integer()}). + -type state() :: #state{}. -type slot() :: [binary()]. +-type media_info() :: #media_info{}. %%-------------------------------------------------------------------- %% gen_mod/supervisor callbacks. @@ -188,10 +195,12 @@ mod_opt_type(custom_headers) -> end; mod_opt_type(rm_on_unregister) -> fun(B) when is_boolean(B) -> B end; +mod_opt_type(thumbnail) -> + fun(B) when is_boolean(B) -> B end; mod_opt_type(_) -> [host, name, access, max_size, secret_length, jid_in_url, file_mode, dir_mode, docroot, put_url, get_url, service_url, custom_headers, - rm_on_unregister]. + rm_on_unregister, thumbnail]. %%-------------------------------------------------------------------- %% gen_server callbacks. @@ -242,6 +251,9 @@ init({ServerHost, Opts}) -> fun(<<"http://", _/binary>> = URL) -> URL; (<<"https://", _/binary>> = URL) -> URL end), + Thumbnail = gen_mod:get_opt(thumbnail, Opts, + fun(B) when is_boolean(B) -> B end, + true), case ServiceURL of undefined -> ok; @@ -265,6 +277,7 @@ init({ServerHost, Opts}) -> access = Access, max_size = MaxSize, secret_length = SecretLength, jid_in_url = JIDinURL, file_mode = FileMode, dir_mode = DirMode, + thumbnail = Thumbnail, docroot = expand_home(str:strip(DocRoot, right, $/)), put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), @@ -278,13 +291,16 @@ init({ServerHost, Opts}) -> handle_call({use_slot, Slot}, _From, #state{file_mode = FileMode, dir_mode = DirMode, + get_url = GetPrefix, + thumbnail = Thumbnail, docroot = DocRoot} = State) -> case get_slot(Slot, State) of {ok, {Size, Timer}} -> timer:cancel(Timer), NewState = del_slot(Slot, State), Path = str:join([DocRoot | Slot], <<$/>>), - {reply, {ok, Size, Path, FileMode, DirMode}, NewState}; + {reply, {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail}, + NewState}; error -> {reply, {error, <<"Invalid slot">>}, State} end; @@ -349,16 +365,20 @@ process(LocalPath, #request{method = 'PUT', host = Host, ip = IP, data = Data}) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), case catch gen_server:call(Proc, {use_slot, LocalPath}) of - {ok, Size, Path, FileMode, DirMode} when byte_size(Data) == Size -> + {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail} + when byte_size(Data) == Size -> ?DEBUG("Storing file from ~s for ~s: ~s", [?ADDR_TO_STR(IP), Host, Path]), - case store_file(Path, Data, FileMode, DirMode) of - ok -> - http_response(Host, 201); - {error, Error} -> - ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p", - [Path, ?ADDR_TO_STR(IP), Host, Error]), - http_response(Host, 500) + case store_file(Path, Data, FileMode, DirMode, + GetPrefix, LocalPath, Thumbnail) of + ok -> + http_response(Host, 201); + {ok, Headers, OutData} -> + http_response(Host, 201, Headers, OutData); + {error, Error} -> + ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p", + [Path, ?ADDR_TO_STR(IP), Host, Error]), + http_response(Host, 500) end; {ok, Size, Path} -> ?INFO_MSG("Rejecting file ~s from ~s for ~s: Size is ~B, not ~B", @@ -701,12 +721,38 @@ iq_disco_info(Lang, Name) -> %% HTTP request handling. --spec store_file(file:filename_all(), binary(), - integer() | undefined, - integer() | undefined) +store_file(Path, Data, FileMode, DirMode, GetPrefix, LocalPath, Thumbnail) -> + case do_store_file(Path, Data, FileMode, DirMode) of + ok when Thumbnail -> + case identify(Path) of + {ok, MediaInfo} -> + case convert(Path, MediaInfo) of + pass -> + ok; + {ok, OutPath} -> + [UserDir, RandDir|_] = LocalPath, + FileName = filename:basename(OutPath), + URL = str:join([GetPrefix, UserDir, + RandDir, FileName], <<$/>>), + ThumbEl = thumb_el(OutPath, URL), + {ok, + [{<<"Content-Type">>, + <<"text/xml; charset=utf-8">>}], + xml:element_to_binary(ThumbEl)} + end; + {error, _} -> + ok + end; + ok -> + ok; + Err -> + Err + end. + +-spec do_store_file(file:filename_all(), binary(), integer(), integer()) -> ok | {error, term()}. -store_file(Path, Data, FileMode, DirMode) -> +do_store_file(Path, Data, FileMode, DirMode) -> try ok = filelib:ensure_dir(Path), {ok, Io} = file:open(Path, [write, exclusive, raw]), @@ -786,6 +832,72 @@ code_to_message(413) -> <<"File size doesn't match requested size.">>; code_to_message(500) -> <<"Internal server error.">>; code_to_message(_Code) -> <<"">>. +%%-------------------------------------------------------------------- +%% Image manipulation stuff +%%-------------------------------------------------------------------- +-spec identify(string()) -> {ok, media_info()} | {error, string()}. + +identify(Path) -> + Cmd = lists:flatten(io_lib:fwrite("identify -format \"ok %m %h %w\" ~s", + [Path])), + Res = os:cmd(Cmd), + case string:tokens(Res, " ") of + ["ok", T, H, W] -> + {ok, #media_info{ + type = string:to_lower(T), + height = list_to_integer(H), + width = list_to_integer(W)}}; + _ -> + ?ERROR_MSG("failed to identify type of ~s: ~s", [Path, Res]), + {error, Res} + end. + +-spec convert(string(), media_info()) -> {ok, string()} | pass. + +convert(Path, #media_info{type = T, width = W, height = H}) -> + if W*H >= 25000000 -> + ?DEBUG("the image ~s is more than 25 Mbpx", [Path]), + pass; + (W =< 300) and (H =< 300) -> + {ok, Path}; + T == "gif"; T == "jpeg"; T == "png"; T == "webp" -> + Dir = filename:dirname(Path), + FileName = binary_to_list(randoms:get_string()) ++ "." ++ T, + OutPath = filename:join(Dir, FileName), + Cmd = lists:flatten(io_lib:fwrite("convert -resize 300 ~s ~s", + [Path, OutPath])), + case os:cmd(Cmd) of + "" -> + {ok, OutPath}; + Err -> + ?ERROR_MSG("failed to convert ~s to ~s: ~s", + [Path, OutPath, Err]), + pass + end; + true -> + ?DEBUG("do not call 'convert' for unknown type ~s", [T]), + pass + end. + +-spec thumb_el(string(), binary()) -> xmlel(). + +thumb_el(Path, URI) -> + ContentType = guess_content_type(Path), + case identify(Path) of + {ok, #media_info{height = H, width = W}} -> + #xmlel{name = <<"thumbnail">>, + attrs = [{<<"xmlns">>, ?NS_THUMBS_1}, + {<<"media-type">>, ContentType}, + {<<"uri">>, URI}, + {<<"height">>, jlib:integer_to_binary(H)}, + {<<"width">>, jlib:integer_to_binary(W)}]}; + {error, _} -> + #xmlel{name = <<"thumbnail">>, + attrs = [{<<"xmlns">>, ?NS_THUMBS_1}, + {<<"uri">>, URI}, + {<<"media-type">>, ContentType}]} + end. + %%-------------------------------------------------------------------- %% Remove user. %%--------------------------------------------------------------------