<commit_before>%%%-------------------------------------------------------------------
%%% @author <NAME>
%%% @copyright (C): 2016 ACK CYFRONET AGH
%%% This software is released under the MIT license
%%% cited in 'LICENSE.txt'.
%%% @end
%%%-------------------------------------------------------------------
%%% @doc
%%% This module processes file download requests originating from GUI.
%%% @end
%%%-------------------------------------------------------------------
-module(download_handler).
-author("<NAME>").
-behaviour(cowboy_http_handler).

-include("global_definitions.hrl").
-include("modules/fslogic/lfm_internal.hrl").
-include_lib("ctool/include/logging.hrl").

% Default buffer size used to send file to a client. It is used if env variable
% gui_download_buffer cannot be found.
-define(DEFAULT_DOWNLOAD_BUFFER_SIZE, 4194304). % 4MB

%% Cowboy API
-export([init/3, handle/2, terminate/3]).


%% ====================================================================
%% Cowboy API functions
%% ====================================================================

%%--------------------------------------------------------------------
%% @doc
%% Cowboy handler callback.
%% @end
%%--------------------------------------------------------------------
-spec init({TransportName :: atom(), ProtocolName :: http},
    Req :: cowboy_req:req(), Opts :: any()) ->
    {ok, cowboy_req:req(), []}.
init(_Type, Req, _Opts) ->
    {ok, Req, []}.


%%--------------------------------------------------------------------
%% @doc
%% Handles an upload request.
%% @end
%%--------------------------------------------------------------------
-spec handle(Req :: cowboy_req:req(), term()) -> {ok, cowboy_req:req(), term()}.
handle(Req, State) ->
    {FileId, _} = cowboy_req:binding(id, Req),
    NewReq = handle_http_download(Req, FileId),
    {ok, NewReq, State}.


%%--------------------------------------------------------------------
%% @doc
%% Cowboy handler callback, no cleanup needed
%% @end
%%--------------------------------------------------------------------
-spec terminate(term(), Req :: cowboy_req:req(), term()) -> ok.
terminate(_Reason, _Req, _State) ->
    ok.


%% ====================================================================
%% Internal functions
%% ====================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Asserts the validity of multipart POST request and proceeds with
%% parsing or returns an error. Returns list of parsed filed values and
%% file body.
%% @end
%%--------------------------------------------------------------------
-spec handle_http_download(Req :: cowboy_req:req(),
    FileId :: file_meta:uuid()) -> cowboy_req:req().
handle_http_download(Req, FileId) ->
    % Try to retrieve user's session
    InitSession =
        try
            g_ctx:init(Req, false)
        catch _:_ ->
            % Logging is done inside g_ctx:init
            error
        end,

    case InitSession of
        error ->
            g_ctx:reply(500, [], <<"">>);
        ok ->
            try
                SessionId = g_session:get_session_id(),
                {ok, FileHandle} = logical_file_manager:open(
                    SessionId, {guid, FileId}, read),
                try
                    {ok, #file_attr{size = Size, name = FileName}} =
                        logical_file_manager:stat(SessionId, {guid, FileId}),
                    StreamFun = cowboy_file_stream_fun(FileHandle, Size),
                    Headers = attachment_headers(FileName),
                    % Reply with attachment headers and a streaming function
                    g_ctx:reply(200, Headers, {Size, StreamFun})
                catch
                    T2:M2 ->
                        ?error_stacktrace("Error while processing file download "
                        "for user ~p - ~p:~p", [g_session:get_user_id(), T2, M2]),
                        logical_file_manager:release(FileHandle), % release if possible
                        g_ctx:reply(500, [], <<"">>)
                end
            catch
                T:M ->
                    ?error_stacktrace("Error while processing file download "
                    "for user ~p - ~p:~p", [g_session:get_user_id(), T, M]),
                    g_ctx:reply(500, [], <<"">>)
            end
    end,
    g_ctx:finish().


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Returns a cowboy-compliant streaming function, that will be evaluated
%% by cowboy to send data (file content) to receiving socket.
%% @end
%%--------------------------------------------------------------------
-spec cowboy_file_stream_fun(FileHandle :: #lfm_handle{}, Size :: integer()) ->
    fun((any(), module()) -> ok).
cowboy_file_stream_fun(FileHandle, Size) ->
    fun(Socket, Transport) ->
        try
            BufSize = get_download_buffer_size(),
            stream_file(Socket, Transport, FileHandle, Size, BufSize)
        catch T:M ->
            % Any exceptions that occur during file streaming must be caught
            % here for cowboy to close the connection cleanly.
            ?error_stacktrace("Error while streaming file '~p' - ~p:~p",
                [FileHandle#lfm_handle.file_guid, T, M]),
            ok
        end
    end.


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Function that will be evaluated by cowboy to stream a file to client.
%% NOTE! Filename must be a unicode string (not utf8)
%% @end
%%--------------------------------------------------------------------
-spec stream_file(Socket :: term(), Transport :: atom(),
    FileHandle :: #lfm_handle{}, Size :: integer(), BufSize :: integer()) -> ok.
stream_file(Socket, Transport, FileHandle, Size, BufSize) ->
    stream_file(Socket, Transport, FileHandle, Size, 0, BufSize, 0).


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Function that will be evaluated by cowboy to stream a file to client.
%% NOTE! Filename must be a unicode string (not utf8)
%% @end
%%--------------------------------------------------------------------
-spec stream_file(Socket :: term(), Transport :: atom(),
    FileHandle :: #lfm_handle{}, Size :: integer(),
    Sent :: integer(), BufSize :: integer(), EmptyReads :: integer()) -> ok.
stream_file(_, _, FileHandle, _, _, _, 3) ->
    ok = logical_file_manager:release(FileHandle);
stream_file(_, _, FileHandle, Size, BytesSent, _, _) when BytesSent >= Size ->
    ok = logical_file_manager:release(FileHandle);
stream_file(Socket, Transport, FileHandle, Size, BytesSent, BufSize, EmptyReads) ->
    {ok, NewHandle, BytesRead} = logical_file_manager:read(
        FileHandle, BytesSent, min(Size - BytesSent, BufSize)),
    ok = Transport:send(Socket, BytesRead),
    NewSent = BytesSent + size(BytesRead),
    case size(BytesRead) of
        0 ->
            stream_file(Socket, Transport, NewHandle, Size, NewSent, BufSize, EmptyReads + 1);
        _ ->
            stream_file(Socket, Transport, NewHandle, Size, NewSent, BufSize, 0)
    end.


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Returns buffer size for file downloads, as specified in config,<commit_msg>VFS-2292, Minor download handler update<commit_after>%%%-------------------------------------------------------------------
%%% @author <NAME>
%%% @copyright (C): 2016 ACK CYFRONET AGH
%%% This software is released under the MIT license
%%% cited in 'LICENSE.txt'.
%%% @end
%%%-------------------------------------------------------------------
%%% @doc
%%% This module processes file download requests originating from GUI.
%%% @end
%%%-------------------------------------------------------------------
-module(download_handler).
-author("<NAME>").
-behaviour(cowboy_http_handler).

-include("global_definitions.hrl").
-include("modules/fslogic/lfm_internal.hrl").
-include_lib("ctool/include/logging.hrl").

% Default buffer size used to send file to a client. It is used if env variable
% gui_download_buffer cannot be found.
-define(DEFAULT_DOWNLOAD_BUFFER_SIZE, 4194304). % 4MB

%% Cowboy API
-export([init/3, handle/2, terminate/3]).


%% ====================================================================
%% Cowboy API functions
%% ====================================================================

%%--------------------------------------------------------------------
%% @doc
%% Cowboy handler callback.
%% @end
%%--------------------------------------------------------------------
-spec init({TransportName :: atom(), ProtocolName :: http},
    Req :: cowboy_req:req(), Opts :: any()) ->
    {ok, cowboy_req:req(), []}.
init(_Type, Req, _Opts) ->
    {ok, Req, []}.


%%--------------------------------------------------------------------
%% @doc
%% Handles an upload request.
%% @end
%%--------------------------------------------------------------------
-spec handle(Req :: cowboy_req:req(), term()) -> {ok, cowboy_req:req(), term()}.
handle(Req, State) ->
    {FileId, _} = cowboy_req:binding(id, Req),
    NewReq = handle_http_download(Req, FileId),
    {ok, NewReq, State}.


%%--------------------------------------------------------------------
%% @doc
%% Cowboy handler callback, no cleanup needed
%% @end
%%--------------------------------------------------------------------
-spec terminate(term(), Req :: cowboy_req:req(), term()) -> ok.
terminate(_Reason, _Req, _State) ->
    ok.


%% ====================================================================
%% Internal functions
%% ====================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Asserts the validity of multipart POST request and proceeds with
%% parsing or returns an error. Returns list of parsed filed values and
%% file body.
%% @end
%%--------------------------------------------------------------------
-spec handle_http_download(Req :: cowboy_req:req(),
    FileId :: file_meta:uuid()) -> cowboy_req:req().
handle_http_download(Req, FileId) ->
    % Try to retrieve user's session
    InitSession =
        try
            g_ctx:init(Req, false)
        catch _:_ ->
            % Logging is done inside g_ctx:init
            error
        end,

    case InitSession of
        error ->
            g_ctx:reply(500, [], <<"">>);
        ok ->
            try
                SessionId = g_session:get_session_id(),
                {ok, FileHandle} = logical_file_manager:open(
                    SessionId, {guid, FileId}, read),
                try
                    {ok, #file_attr{size = Size, name = FileName}} =
                        logical_file_manager:stat(SessionId, {guid, FileId}),
                    StreamFun = cowboy_file_stream_fun(FileHandle, Size),
                    Headers = attachment_headers(FileName),
                    % Reply with attachment headers and a streaming function
                    g_ctx:reply(200, Headers, {Size, StreamFun})
                catch
                    T2:M2 ->
                        ?error_stacktrace("Error while processing file download "
                        "for user ~p - ~p:~p", [g_session:get_user_id(), T2, M2]),
                        logical_file_manager:release(FileHandle), % release if possible
                        g_ctx:reply(500, [], <<"">>)
                end
            catch
                T:M ->
                    ?error_stacktrace("Error while processing file download "
                    "for user ~p - ~p:~p", [g_session:get_user_id(), T, M]),
                    g_ctx:reply(500, [], <<"">>)
            end
    end,
    g_ctx:finish().


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Returns a cowboy-compliant streaming function, that will be evaluated
%% by cowboy to send data (file content) to receiving socket.
%% @end
%%--------------------------------------------------------------------
-spec cowboy_file_stream_fun(FileHandle :: #lfm_handle{}, Size :: integer()) ->
    fun((any(), module()) -> ok).
cowboy_file_stream_fun(FileHandle, Size) ->
    fun(Socket, Transport) ->
        try
            BufSize = get_download_buffer_size(),
            stream_file(Socket, Transport, FileHandle, Size, BufSize)
        catch T:M ->
            % Any exceptions that occur during file streaming must be caught
            % here for cowboy to close the connection cleanly.
            ?error_stacktrace("Error while streaming file '~p' - ~p:~p",
                [FileHandle#lfm_handle.file_guid, T, M]),
            ok
        end
    end.


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Function that will be evaluated by cowboy to stream a file to client.
%% NOTE! Filename must be a unicode string (not utf8)
%% @end
%%--------------------------------------------------------------------
-spec stream_file(Socket :: term(), Transport :: atom(),
    FileHandle :: #lfm_handle{}, Size :: integer(), BufSize :: integer()) -> ok.
stream_file(Socket, Transport, FileHandle, Size, BufSize) ->
    stream_file(Socket, Transport, FileHandle, Size, 0, BufSize).


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Function that will be evaluated by cowboy to stream a file to client.
%% NOTE! Filename must be a unicode string (not utf8)
%% @end
%%--------------------------------------------------------------------
-spec stream_file(Socket :: term(), Transport :: atom(),
    FileHandle :: #lfm_handle{}, Size :: integer(),
    Sent :: integer(), BufSize :: integer()) -> ok.
stream_file(_, _, FileHandle, Size, BytesSent, _) when BytesSent >= Size ->
    ok = logical_file_manager:release(FileHandle);
stream_file(Socket, Transport, FileHandle, Size, BytesSent, BufSize) ->
    {ok, NewHandle, BytesRead} = logical_file_manager:read(
        FileHandle, BytesSent, min(Size - BytesSent, BufSize)),
    ok = Transport:send(Socket, BytesRead),
    NewSent = BytesSent + size(BytesRead),
    case size(BytesRead) of
        0 ->
            ok = logical_file_manager:release(FileHandle);
        _ ->
            stream_file(Socket, Transport, NewHandle, Size, NewSent, BufSize)
    end.


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Returns buffer size for file downloads, as specified in config,
{'fraction_non_alphanumeric': 0.3180817393697537, 'fraction_numerical': 0.005366726296958855, 'mean_word_length': 1.5978552278820375, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 72, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 21, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6256349', 'n_tokens_mistral': 3690, 'n_tokens_neox': 3493, 'n_words': 985}