<commit_before>%% ===================================================================
%% @author <NAME>
%% @copyright (C): 2013 ACK CYFRONET AGH
%% This software is released under the MIT license
%% cited in 'LICENSE.txt'.
%% @end
%% ===================================================================
%% @doc: This module processes file download and upload requests. After validating 
%% them it conducts streaming to or from a socket or makes nitrogen display a proper error page.
%% @end
%% ===================================================================

-module(file_transfer_handler).
-include("veil_modules/fslogic/fslogic.hrl").
-include("veil_modules/dao/dao_share.hrl").
-include("veil_modules/control_panel/common.hrl").
-include("veil_modules/control_panel/rest_messages.hrl").
-include("err.hrl").

% Buffer size used to send file to a client. Override with control_panel_download_buffer.
-define(DOWNLOAD_BUFFER_SIZE, 1048576). % 1MB

% Buffer size used to stream file from a client. Override with control_panel_upload_buffer.
-define(UPLOAD_BUFFER_SIZE, 1048576). % 1MB

% Size of data read from the socket at a time.
-define(UPLOAD_PART_SIZE, 1024). % 1KB

% Timeout for fetching single part of data from socket
-define(UPLOAD_PART_TIMEOUT, 30000). % 30 seconds

%% ====================================================================
%% API functions
%% ====================================================================
-export([maybe_handle_request/1, handle_upload_request/2, handle_rest_upload/3, get_download_buffer_size/0]).


%% maybe_handle_request/1
%% ====================================================================
%% @doc Intercepts the request if its a user content or shared file download request or
%% doeas nothinf otherwise. Checks its validity and serves the file or redirects to error page. 
%% Should be called on a request coming directly to nitrogen_handler.
%% @end
-spec maybe_handle_request(Req :: req()) -> {boolean(), NewReq :: req()}.
%% ====================================================================
maybe_handle_request(Req) ->
    {Path, _} = cowboy_req:path(Req),
    {Qs, _} = cowboy_req:qs(Req),
    case {Path, Qs} of
        {<<?user_content_download_path>>, <<"f=", _/binary>>} -> handle_user_content_request(Req);
        {<<?shared_files_download_path, _/binary>>, _} -> handle_shared_file_request(Req);
        _ -> {false, Req}
    end.


%% handle_upload_request/2
%% ====================================================================
%% @doc Asserts the validity of mutlipart POST request and proceeds with
%% parsing or returns an error. Returns list of parsed filed values and
%% file body.
%% @end
-spec handle_upload_request(Req :: req(), record()) ->
    {ok, Params :: [tuple()], Files :: [#uploaded_file{}]} |
    {error, incorrect_session}.
%% ====================================================================
handle_upload_request(Req, UserDoc) ->
    case UserDoc of
        undefined ->
            {error, incorrect_session};
        Doc ->
            case user_logic:get_dn_list(Doc) of
                [] ->
                    {error, incorrect_session};
                undefined ->
                    {error, incorrect_session};
                List when is_list(List) ->
                    % This will cause logical_files_manager to create the file for proper user
                    put(user_id, lists:nth(1, List)),
                    {ok, _Params, _Files} = parse_multipart(Req, [], [])
            end
    end.

%% handle_rest_upload/3
%% ====================================================================
%% @doc Asserts the validity of mutlipart POST request and proceeds with
%% parsing and writing its data to a file at specified path. Returns
%% values conforming to rest_module_behaviour requirements.
%% @end
handle_rest_upload(Req, Path, Overwrite) ->
    case cowboy_req:parse_header(<<"content-length">>, Req) of
        {ok, Length, NewReq} when is_integer(Length) ->
            case try_to_create_file(binary_to_list(Path), Overwrite) of
                ok ->
                    case parse_rest_upload(NewReq, binary_to_list(Path)) of
                        {true, NewReq2} ->
                            {{body, rest_utils:success_reply(?success_file_uploaded)}, NewReq2};
                        {false, NewReq2} ->
                            ErrorRec = ?report_error(?error_upload_unprocessable),
                            {{error, rest_utils:error_reply(ErrorRec)}, NewReq2}
                    end;
                {error, _Error} ->
                    ErrorRec = ?report_error(?error_upload_cannot_create),
                    {{error, rest_utils:error_reply(ErrorRec)}, NewReq}
            end;
        _ ->
            ErrorRec = ?report_error(?error_upload_unprocessable),
            {{error, rest_utils:error_reply(ErrorRec)}, Req}
    end.

%% ====================================================================
%% INTERNAL FUNCTIONS
%% ====================================================================

%% ====================================================================
%% File download (download requests)
%% ====================================================================

% Checks if user content download request is valid and serves a file
handle_user_content_request(Req) ->
    try
        try_or_throw(
            fun() ->
                RequestBridge = simple_bridge:make_request(cowboy_request_bridge, {Req, ""}),
                wf_context:init_context(RequestBridge, undefined),
                wf_handler:call(session_handler, init),
                UserID = wf:session(user_doc),
                true = (UserID /= undefined),
                put(user_id, lists:nth(1, user_logic:get_dn_list(UserID)))
            end, not_logged_in),

        {Filepath, Size} = try_or_throw(
            fun() ->
                {FilepathBin, _} = cowboy_req:qs_val(<<"f">>, Req),
                TryFilepath = binary_to_list(FilepathBin),
                {ok, Fileattr} = logical_files_manager:getfileattr(TryFilepath),
                "REG" = Fileattr#fileattributes.type,
                TrySize = Fileattr#fileattributes.size,
                {TryFilepath, TrySize}
            end, file_not_found),

        {ok, NewReq} = try_or_throw(
            fun() ->
                send_file_by_path(Req, Filepath, Size)
            end, {sending_failed, Filepath}),
        {true, NewReq}

    catch Type:Message ->
        case Message of
            {sending_failed, Path} ->
                lager:error("Error while sending file ~p to user ~p - ~p:~p~n~p",
                    [Path, user_logic:get_login(get(user_id)),
                        Type, Message, erlang:get_stacktrace()]);
            _ -> skip
        end,
        {true, _FinReq} = cowboy_req:reply(500, Req#http_req{connection = close})
    end.


% Checks if shared file download request is valid and serves a file
handle_shared_file_request(Req) ->
    try
        {FileID, FileName, Size} = try_or_throw(
            fun() ->
                put(user_id, undefined),
                {Path, _} = cowboy_req:path(Req),
                <<"/share/", ShareID/binary>> = Path,
                true = (ShareID /= <<"">>),

                {ok, #veil_document{record = #share_desc{file = FileID}}} =
                    logical_files_manager:get_share({uuid, binary_to_list(ShareID)}),
                {ok, FileName} = logical_files_manager:get_file_name_by_uuid(FileID),
                {ok, Fileattr} = logical_files_manager:getfileattr({uuid, FileID}),
                "REG" = Fileattr#fileattributes.type,
                TrySize = Fileattr#fileattributes.size,
                {FileID, FileName, TrySize}
            end, file_not_found),

        {ok, NewReq} = try_or_throw(
            fun() ->
                send_file_by_uuid(Req, FileID, FileName, Size)
            end, {sending_failed, FileName}),
        {true, NewReq}

    catch Type:Message ->
        case Message of
            {sending_failed, Path} ->
                lager:error("Error while sending shared file ~p - ~p:~p~n~p",
                    [Path, Type, Message, erlang:get_stacktrace()]);
            _ -> skip
        end,
        {true, _FinReq} = cowboy_req:reply(500, Req#http_req{connection = close})
    end.

% Sends file as a http response, file is given by uuid
send_file_by_uuid(Req, FileID, FileName, Size) ->
    [Mimetype] = mimetypes:path_to_mimes(FileName),
    Headers = simple_bridge_util:ensure_header([], {"Content-Type", Mimetype}),
    Headers2 = simple_bridge_util:ensure_header(Headers,
        {"Content-Disposition", "attachment;" ++
            % Replace spaces with underscores
            " filename=" ++ re:replace(FileName, " ", "_", [global, {return, list}]) ++
            % Offer safely-encoded UTF-8 filename for browsers supporting it
            "; filename*=UTF-8''" ++ http_uri:encode(FileName)
        }),

    StreamFun = fun(Socket, Transport) ->
        stream_file(Socket, Transport, {uuid, FileID}, Size, get_download_buffer_size())
    end,

    Req2 = prepare_headers(Req, Headers2),
    Req3 = cowboy_req:set_resp_body_fun(Size, StreamFun, Req2),<commit_msg>fix connection not being closed during download error<commit_after>%% ===================================================================
%% @author <NAME>
%% @copyright (C): 2013 ACK CYFRONET AGH
%% This software is released under the MIT license
%% cited in 'LICENSE.txt'.
%% @end
%% ===================================================================
%% @doc: This module processes file download and upload requests. After validating 
%% them it conducts streaming to or from a socket or makes nitrogen display a proper error page.
%% @end
%% ===================================================================

-module(file_transfer_handler).
-include("veil_modules/fslogic/fslogic.hrl").
-include("veil_modules/dao/dao_share.hrl").
-include("veil_modules/control_panel/common.hrl").
-include("veil_modules/control_panel/rest_messages.hrl").
-include("err.hrl").

% Buffer size used to send file to a client. Override with control_panel_download_buffer.
-define(DOWNLOAD_BUFFER_SIZE, 1048576). % 1MB

% Buffer size used to stream file from a client. Override with control_panel_upload_buffer.
-define(UPLOAD_BUFFER_SIZE, 1048576). % 1MB

% Size of data read from the socket at a time.
-define(UPLOAD_PART_SIZE, 1024). % 1KB

% Timeout for fetching single part of data from socket
-define(UPLOAD_PART_TIMEOUT, 30000). % 30 seconds

%% ====================================================================
%% API functions
%% ====================================================================
-export([maybe_handle_request/1, handle_upload_request/2, handle_rest_upload/3, get_download_buffer_size/0]).


%% maybe_handle_request/1
%% ====================================================================
%% @doc Intercepts the request if its a user content or shared file download request or
%% doeas nothinf otherwise. Checks its validity and serves the file or redirects to error page. 
%% Should be called on a request coming directly to nitrogen_handler.
%% @end
-spec maybe_handle_request(Req :: req()) -> {boolean(), NewReq :: req()}.
%% ====================================================================
maybe_handle_request(Req) ->
    {Path, _} = cowboy_req:path(Req),
    {Qs, _} = cowboy_req:qs(Req),
    case {Path, Qs} of
        {<<?user_content_download_path>>, <<"f=", _/binary>>} -> handle_user_content_request(Req);
        {<<?shared_files_download_path, _/binary>>, _} -> handle_shared_file_request(Req);
        _ -> {false, Req}
    end.


%% handle_upload_request/2
%% ====================================================================
%% @doc Asserts the validity of mutlipart POST request and proceeds with
%% parsing or returns an error. Returns list of parsed filed values and
%% file body.
%% @end
-spec handle_upload_request(Req :: req(), record()) ->
    {ok, Params :: [tuple()], Files :: [#uploaded_file{}]} |
    {error, incorrect_session}.
%% ====================================================================
handle_upload_request(Req, UserDoc) ->
    case UserDoc of
        undefined ->
            {error, incorrect_session};
        Doc ->
            case user_logic:get_dn_list(Doc) of
                [] ->
                    {error, incorrect_session};
                undefined ->
                    {error, incorrect_session};
                List when is_list(List) ->
                    % This will cause logical_files_manager to create the file for proper user
                    put(user_id, lists:nth(1, List)),
                    {ok, _Params, _Files} = parse_multipart(Req, [], [])
            end
    end.

%% handle_rest_upload/3
%% ====================================================================
%% @doc Asserts the validity of mutlipart POST request and proceeds with
%% parsing and writing its data to a file at specified path. Returns
%% values conforming to rest_module_behaviour requirements.
%% @end
handle_rest_upload(Req, Path, Overwrite) ->
    case cowboy_req:parse_header(<<"content-length">>, Req) of
        {ok, Length, NewReq} when is_integer(Length) ->
            case try_to_create_file(binary_to_list(Path), Overwrite) of
                ok ->
                    case parse_rest_upload(NewReq, binary_to_list(Path)) of
                        {true, NewReq2} ->
                            {{body, rest_utils:success_reply(?success_file_uploaded)}, NewReq2};
                        {false, NewReq2} ->
                            ErrorRec = ?report_error(?error_upload_unprocessable),
                            {{error, rest_utils:error_reply(ErrorRec)}, NewReq2}
                    end;
                {error, _Error} ->
                    ErrorRec = ?report_error(?error_upload_cannot_create),
                    {{error, rest_utils:error_reply(ErrorRec)}, NewReq}
            end;
        _ ->
            ErrorRec = ?report_error(?error_upload_unprocessable),
            {{error, rest_utils:error_reply(ErrorRec)}, Req}
    end.

%% ====================================================================
%% INTERNAL FUNCTIONS
%% ====================================================================

%% ====================================================================
%% File download (download requests)
%% ====================================================================

% Checks if user content download request is valid and serves a file
handle_user_content_request(Req) ->
    try
        try_or_throw(
            fun() ->
                RequestBridge = simple_bridge:make_request(cowboy_request_bridge, {Req, ""}),
                wf_context:init_context(RequestBridge, undefined),
                wf_handler:call(session_handler, init),
                UserID = wf:session(user_doc),
                true = (UserID /= undefined),
                put(user_id, lists:nth(1, user_logic:get_dn_list(UserID)))
            end, not_logged_in),

        {Filepath, Size} = try_or_throw(
            fun() ->
                {FilepathBin, _} = cowboy_req:qs_val(<<"f">>, Req),
                TryFilepath = binary_to_list(FilepathBin),
                {ok, Fileattr} = logical_files_manager:getfileattr(TryFilepath),
                "REG" = Fileattr#fileattributes.type,
                TrySize = Fileattr#fileattributes.size,
                {TryFilepath, TrySize}
            end, file_not_found),

        {ok, NewReq} = try_or_throw(
            fun() ->
                send_file_by_path(Req, Filepath, Size)
            end, {sending_failed, Filepath}),
        {true, NewReq}

    catch Type:Message ->
        case Message of
            {sending_failed, Path} ->
                lager:error("Error while sending file ~p to user ~p - ~p:~p~n~p",
                    [Path, user_logic:get_login(get(user_id)),
                        Type, Message, erlang:get_stacktrace()]);
            _ -> skip
        end,
        {ok, FinReq} = cowboy_req:reply(500, Req#http_req{connection = close}),
        {true, FinReq}
    end.


% Checks if shared file download request is valid and serves a file
handle_shared_file_request(Req) ->
    try
        {FileID, FileName, Size} = try_or_throw(
            fun() ->
                put(user_id, undefined),
                {Path, _} = cowboy_req:path(Req),
                <<"/share/", ShareID/binary>> = Path,
                true = (ShareID /= <<"">>),

                {ok, #veil_document{record = #share_desc{file = FileID}}} =
                    logical_files_manager:get_share({uuid, binary_to_list(ShareID)}),
                {ok, FileName} = logical_files_manager:get_file_name_by_uuid(FileID),
                {ok, Fileattr} = logical_files_manager:getfileattr({uuid, FileID}),
                "REG" = Fileattr#fileattributes.type,
                TrySize = Fileattr#fileattributes.size,
                {FileID, FileName, TrySize}
            end, file_not_found),

        {ok, NewReq} = try_or_throw(
            fun() ->
                send_file_by_uuid(Req, FileID, FileName, Size)
            end, {sending_failed, FileName}),
        {true, NewReq}

    catch Type:Message ->
        case Message of
            {sending_failed, Path} ->
                lager:error("Error while sending shared file ~p - ~p:~p~n~p",
                    [Path, Type, Message, erlang:get_stacktrace()]);
            _ -> skip
        end,
        {ok, FinReq} = cowboy_req:reply(500, Req#http_req{connection = close}),
        {true, FinReq}
    end.

% Sends file as a http response, file is given by uuid
send_file_by_uuid(Req, FileID, FileName, Size) ->
    [Mimetype] = mimetypes:path_to_mimes(FileName),
    Headers = simple_bridge_util:ensure_header([], {"Content-Type", Mimetype}),
    Headers2 = simple_bridge_util:ensure_header(Headers,
        {"Content-Disposition", "attachment;" ++
            % Replace spaces with underscores
            " filename=" ++ re:replace(FileName, " ", "_", [global, {return, list}]) ++
            % Offer safely-encoded UTF-8 filename for browsers supporting it
            "; filename*=UTF-8''" ++ http_uri:encode(FileName)
        }),

    StreamFun = fun(Socket, Transport) ->
        stream_file(Socket, Transport, {uuid, FileID}, Size, get_download_buffer_size())
    end,

    Req2 = prepare_headers(Req, Headers2),
    Req3 = cowboy_req:set_resp_body_fun(Size, StreamFun, Req2),
{'fraction_non_alphanumeric': 0.20863075924724206, 'fraction_numerical': 0.00627298291152931, 'mean_word_length': 2.86559364548495, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 101, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 23, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5044181', 'n_tokens_mistral': 5308, 'n_tokens_neox': 4959, 'n_words': 1192}