Current section
Files
Jump to
Current section
Files
src/hecate_plugin_files.erl
%%%-------------------------------------------------------------------
%%% @doc File upload/download helper for plugins.
%%%
%%% Handles multipart uploads and chunked downloads via cowboy.
%%% Files are stored under the plugin's data directory.
%%% @end
%%%-------------------------------------------------------------------
-module(hecate_plugin_files).
-export([read_body/1, read_body/2, save_upload/3, stream_file/3]).
%% @doc Read the full request body (up to 8MB default).
-spec read_body(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
read_body(Req) ->
read_body(Req, #{length => 8388608, period => 15000}).
%% @doc Read the full request body with custom options.
-spec read_body(cowboy_req:req(), map()) -> {ok, binary(), cowboy_req:req()}.
read_body(Req, Opts) ->
read_body_acc(Req, Opts, []).
%% @doc Save uploaded binary data to a file in the plugin's data directory.
-spec save_upload(PluginName :: string() | binary(),
RelativePath :: string(),
Data :: binary()) -> ok | {error, term()}.
save_upload(PluginName, RelativePath, Data) ->
FullPath = filename:join(hecate_plugin_paths:base_dir(PluginName), RelativePath),
ok = filelib:ensure_dir(FullPath),
file:write_file(FullPath, Data).
%% @doc Stream a file as the response body (chunked transfer).
-spec stream_file(cowboy_req:req(), file:filename(), binary()) ->
{ok, cowboy_req:req()}.
stream_file(Req0, FilePath, ContentType) ->
{ok, FileInfo} = file:read_file_info(FilePath),
FileSize = element(2, FileInfo),
Req = cowboy_req:stream_reply(200, #{
<<"content-type">> => ContentType,
<<"content-length">> => integer_to_binary(FileSize)
}, Req0),
{ok, Fd} = file:open(FilePath, [read, binary, raw]),
stream_chunks(Fd, Req),
file:close(Fd),
{ok, Req}.
%%--------------------------------------------------------------------
%% Internal
%%--------------------------------------------------------------------
read_body_acc(Req0, Opts, Acc) ->
case cowboy_req:read_body(Req0, Opts) of
{ok, Data, Req} ->
{ok, iolist_to_binary(lists:reverse([Data | Acc])), Req};
{more, Data, Req} ->
read_body_acc(Req, Opts, [Data | Acc])
end.
stream_chunks(Fd, Req) ->
case file:read(Fd, 65536) of
{ok, Data} ->
cowboy_req:stream_body(Data, nofin, Req),
stream_chunks(Fd, Req);
eof ->
cowboy_req:stream_body(<<>>, fin, Req)
end.