mod_http_upload: Support thumbnail generation
Import thumbnail support from the mod_http_upload version shipped with ejabberd.
This commit is contained in:
parent
0d2fa84c8a
commit
f6b66cd130
|
@ -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
|
mod_http_upload. Otherwise, a SHA-1 hash of the user's bare JID is
|
||||||
included instead.
|
included instead.
|
||||||
|
|
||||||
|
- thumbnail: (default: 'true')
|
||||||
|
|
||||||
|
This option specifies whether ejabberd should create thumbnails of
|
||||||
|
uploaded images. If a thumbnail is created, a <thumbnail/> element that
|
||||||
|
contains the download <uri/> and some metadata is returned with the PUT
|
||||||
|
response.
|
||||||
|
|
||||||
- file_mode (default: 'undefined')
|
- file_mode (default: 'undefined')
|
||||||
|
|
||||||
This option defines the permission bits of uploaded files. The bits are
|
This option defines the permission bits of uploaded files. The bits are
|
||||||
|
|
|
@ -90,10 +90,17 @@
|
||||||
put_url :: binary(),
|
put_url :: binary(),
|
||||||
get_url :: binary(),
|
get_url :: binary(),
|
||||||
service_url :: binary() | undefined,
|
service_url :: binary() | undefined,
|
||||||
|
thumbnail :: boolean(),
|
||||||
slots = dict:new() :: term()}). % dict:dict() requires Erlang 17.
|
slots = dict:new() :: term()}). % dict:dict() requires Erlang 17.
|
||||||
|
|
||||||
|
-record(media_info,
|
||||||
|
{type :: string(),
|
||||||
|
height :: integer(),
|
||||||
|
width :: integer()}).
|
||||||
|
|
||||||
-type state() :: #state{}.
|
-type state() :: #state{}.
|
||||||
-type slot() :: [binary()].
|
-type slot() :: [binary()].
|
||||||
|
-type media_info() :: #media_info{}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% gen_mod/supervisor callbacks.
|
%% gen_mod/supervisor callbacks.
|
||||||
|
@ -188,10 +195,12 @@ mod_opt_type(custom_headers) ->
|
||||||
end;
|
end;
|
||||||
mod_opt_type(rm_on_unregister) ->
|
mod_opt_type(rm_on_unregister) ->
|
||||||
fun(B) when is_boolean(B) -> B end;
|
fun(B) when is_boolean(B) -> B end;
|
||||||
|
mod_opt_type(thumbnail) ->
|
||||||
|
fun(B) when is_boolean(B) -> B end;
|
||||||
mod_opt_type(_) ->
|
mod_opt_type(_) ->
|
||||||
[host, name, access, max_size, secret_length, jid_in_url, file_mode,
|
[host, name, access, max_size, secret_length, jid_in_url, file_mode,
|
||||||
dir_mode, docroot, put_url, get_url, service_url, custom_headers,
|
dir_mode, docroot, put_url, get_url, service_url, custom_headers,
|
||||||
rm_on_unregister].
|
rm_on_unregister, thumbnail].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% gen_server callbacks.
|
%% gen_server callbacks.
|
||||||
|
@ -242,6 +251,9 @@ init({ServerHost, Opts}) ->
|
||||||
fun(<<"http://", _/binary>> = URL) -> URL;
|
fun(<<"http://", _/binary>> = URL) -> URL;
|
||||||
(<<"https://", _/binary>> = URL) -> URL
|
(<<"https://", _/binary>> = URL) -> URL
|
||||||
end),
|
end),
|
||||||
|
Thumbnail = gen_mod:get_opt(thumbnail, Opts,
|
||||||
|
fun(B) when is_boolean(B) -> B end,
|
||||||
|
true),
|
||||||
case ServiceURL of
|
case ServiceURL of
|
||||||
undefined ->
|
undefined ->
|
||||||
ok;
|
ok;
|
||||||
|
@ -265,6 +277,7 @@ init({ServerHost, Opts}) ->
|
||||||
access = Access, max_size = MaxSize,
|
access = Access, max_size = MaxSize,
|
||||||
secret_length = SecretLength, jid_in_url = JIDinURL,
|
secret_length = SecretLength, jid_in_url = JIDinURL,
|
||||||
file_mode = FileMode, dir_mode = DirMode,
|
file_mode = FileMode, dir_mode = DirMode,
|
||||||
|
thumbnail = Thumbnail,
|
||||||
docroot = expand_home(str:strip(DocRoot, right, $/)),
|
docroot = expand_home(str:strip(DocRoot, right, $/)),
|
||||||
put_url = expand_host(str:strip(PutURL, right, $/), ServerHost),
|
put_url = expand_host(str:strip(PutURL, right, $/), ServerHost),
|
||||||
get_url = expand_host(str:strip(GetURL, 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,
|
handle_call({use_slot, Slot}, _From, #state{file_mode = FileMode,
|
||||||
dir_mode = DirMode,
|
dir_mode = DirMode,
|
||||||
|
get_url = GetPrefix,
|
||||||
|
thumbnail = Thumbnail,
|
||||||
docroot = DocRoot} = State) ->
|
docroot = DocRoot} = State) ->
|
||||||
case get_slot(Slot, State) of
|
case get_slot(Slot, State) of
|
||||||
{ok, {Size, Timer}} ->
|
{ok, {Size, Timer}} ->
|
||||||
timer:cancel(Timer),
|
timer:cancel(Timer),
|
||||||
NewState = del_slot(Slot, State),
|
NewState = del_slot(Slot, State),
|
||||||
Path = str:join([DocRoot | Slot], <<$/>>),
|
Path = str:join([DocRoot | Slot], <<$/>>),
|
||||||
{reply, {ok, Size, Path, FileMode, DirMode}, NewState};
|
{reply, {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail},
|
||||||
|
NewState};
|
||||||
error ->
|
error ->
|
||||||
{reply, {error, <<"Invalid slot">>}, State}
|
{reply, {error, <<"Invalid slot">>}, State}
|
||||||
end;
|
end;
|
||||||
|
@ -349,16 +365,20 @@ process(LocalPath, #request{method = 'PUT', host = Host, ip = IP,
|
||||||
data = Data}) ->
|
data = Data}) ->
|
||||||
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
Proc = gen_mod:get_module_proc(Host, ?PROCNAME),
|
||||||
case catch gen_server:call(Proc, {use_slot, LocalPath}) of
|
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",
|
?DEBUG("Storing file from ~s for ~s: ~s",
|
||||||
[?ADDR_TO_STR(IP), Host, Path]),
|
[?ADDR_TO_STR(IP), Host, Path]),
|
||||||
case store_file(Path, Data, FileMode, DirMode) of
|
case store_file(Path, Data, FileMode, DirMode,
|
||||||
ok ->
|
GetPrefix, LocalPath, Thumbnail) of
|
||||||
http_response(Host, 201);
|
ok ->
|
||||||
{error, Error} ->
|
http_response(Host, 201);
|
||||||
?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p",
|
{ok, Headers, OutData} ->
|
||||||
[Path, ?ADDR_TO_STR(IP), Host, Error]),
|
http_response(Host, 201, Headers, OutData);
|
||||||
http_response(Host, 500)
|
{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;
|
end;
|
||||||
{ok, Size, Path} ->
|
{ok, Size, Path} ->
|
||||||
?INFO_MSG("Rejecting file ~s from ~s for ~s: Size is ~B, not ~B",
|
?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.
|
%% HTTP request handling.
|
||||||
|
|
||||||
-spec store_file(file:filename_all(), binary(),
|
store_file(Path, Data, FileMode, DirMode, GetPrefix, LocalPath, Thumbnail) ->
|
||||||
integer() | undefined,
|
case do_store_file(Path, Data, FileMode, DirMode) of
|
||||||
integer() | undefined)
|
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()}.
|
-> ok | {error, term()}.
|
||||||
|
|
||||||
store_file(Path, Data, FileMode, DirMode) ->
|
do_store_file(Path, Data, FileMode, DirMode) ->
|
||||||
try
|
try
|
||||||
ok = filelib:ensure_dir(Path),
|
ok = filelib:ensure_dir(Path),
|
||||||
{ok, Io} = file:open(Path, [write, exclusive, raw]),
|
{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(500) -> <<"Internal server error.">>;
|
||||||
code_to_message(_Code) -> <<"">>.
|
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.
|
%% Remove user.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in New Issue