src/sb_file_upload_handler.erl

%%%-------------------------------------------------------------------
%%% @author Mehmet Emin Tok
%%% @copyright (c) 2013, Synlay Technologies
%%% See MIT-LICENSE for licensing information.
%%%
%%% @doc Handler for file uploads
%%%
%%% @end
%%% Created : 19.09.13
%%%-------------------------------------------------------------------
-module(sb_file_upload_handler).

%% API
-export([
    new_file/1
    ,new_file/2
    ,get_tempfile/1
    ,get_data/1
    ,complete_file/1
    ,receive_data/2
    ,memory_file_handler/0
    ,temporary_file_handler/0
]).


% Override with config variable {scratch_dir, Directory}
-define (SCRATCH_DIR, "./scratch").

% Override with config var {max_file_in_memory_size, SizeInMB}
% for backwards compatibility default is 0
-define (MAX_MEMORY_SIZE, 0).

% Override with config var {max_file_size, SizeInMB}
-define (MAX_FILE_SIZE, 100).


%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc Starting a new file
%%
%% Starts a new file with an optional File upload Handler
%% and a FileName.
%% When no File upload handler is given the memory file
%% handler is set as default.
%%
%% @end
%%--------------------------------------------------------------------
new_file(FileName) ->
    Result = new_file(memory_file_handler(), FileName),
    Result.
new_file(FileUploadHandler={_Id, Funs}, FileName) ->
    NewFileFun = proplists:get_value(new_file, Funs),
    Result = NewFileFun(FileUploadHandler, FileName),
    Result.

%%--------------------------------------------------------------------
%% @doc Completes the File
%%
%% Completes the File and returns the new File upload handler state.
%%
%% @end
%%--------------------------------------------------------------------
complete_file(FileUploadHandlerState={_Id, _State, Funs}) ->
    CompleteFileFun = proplists:get_value(complete_file, Funs),
    CompleteFileFun(FileUploadHandlerState).

%%--------------------------------------------------------------------
%% @doc Receiving new appending data
%%
%% Receives a "chunk" of data from the file upload and returns
%% the new File upload handler state.
%%
%% @end
%%--------------------------------------------------------------------
receive_data(FileUploadHandlerState={_Id, _State, Funs}, Data) ->
    ReceiveDataFun = proplists:get_value(receive_data, Funs),
    ReceiveDataFun(FileUploadHandlerState, Data).

%%--------------------------------------------------------------------
%% @doc Getting the temporary file name
%%
%% This is empty for "memory" handler and set by the
%% "temporary_file" handler.
%%
%% @end
%%--------------------------------------------------------------------
get_tempfile(FileUploadHandlerState={_Id, _State, Funs}) ->
    TempFileFun = proplists:get_value(get_tempfile, Funs),
    TempFileFun(FileUploadHandlerState).

%%--------------------------------------------------------------------
%% @doc Getting the File data
%%
%% This is empty for "temporary_file" handler and set by the
%% "memory" handler.
%%
%% @end
%%--------------------------------------------------------------------
get_data(FileUploadHandlerState={_Id, _State, Funs}) ->
    DataFun = proplists:get_value(get_data, Funs),
    DataFun(FileUploadHandlerState).


%%--------------------------------------------------------------------
%% @doc File Upload Handler "temporary_file"
%%
%% the temporary_file file upload handler is writing the uploaded
%% files to a temporary file on the hard disk.
%%
%% @end
%%--------------------------------------------------------------------
temporary_file_handler() ->
    {temporary_file, [
        {new_file,      fun handle_new_file/2}
        ,{receive_data,  fun handle_receive_data/2}
        ,{complete_file, fun handle_complete_file/1}
        ,{get_tempfile,  fun handle_get_tempfile/1}
        ,{get_data,      fun handle_get_data/1}
    ]}.



%%--------------------------------------------------------------------
%% @doc File Upload Handler "memory"
%%
%% the memory file upload handler is keeping the file in the
%% memory while the file is smaller than `get_max_memory_size()'.
%% When file is bigger this handler transforms to "temporary_file"
%% handler.
%%
%% @end
%%--------------------------------------------------------------------
memory_file_handler() ->
    {memory, [
        {new_file,      fun handle_new_file/2}
        ,{receive_data,  fun handle_receive_data/2}
        ,{complete_file, fun handle_complete_file/1}
        ,{get_tempfile,  fun handle_get_tempfile/1}
        ,{get_data,      fun handle_get_data/1}
    ]}.


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

handle_new_file({temporary_file, Funs}, FileName) ->
    {temporary_file, {FileName, get_tempfilename(), 0}, Funs};
handle_new_file({memory, Funs}, FileName) ->
    {memory, {FileName, <<>>, 0}, Funs}.

handle_receive_data({memory, {FileName, DataState, CurrentSize}, Funs}, Data) ->
    NewSize = CurrentSize + size(Data),
    case NewSize > get_max_memory_size() of
        true ->
            %% Transform current MemoryUploadHandler to FileUploadHandler
            %% when Data is too big
            NewFileHandler = new_file(temporary_file_handler(), FileName),
            receive_data(NewFileHandler, <<DataState/binary, Data/binary>>);
        false ->
            {memory, {FileName, <<DataState/binary, Data/binary>>, NewSize}, Funs}
    end;
handle_receive_data({temporary_file, {FileName, TempFile, CurrentSize}, Funs}, Data) ->
    NewSize = CurrentSize + size(Data),

    % Throw exception if the uploaded file is getting too big.
    case NewSize > get_max_file_size() of
        true ->
            file:delete(TempFile),
            throw({file_too_big, FileName});
        false ->
            continue
    end,

    ok = filelib:ensure_dir(TempFile),
    {ok, FD} = file:open(TempFile, [append, raw]),
    ok = file:write(FD, Data),
    ok = file:close(FD),
    {temporary_file, {FileName, TempFile, NewSize}, Funs}.

handle_get_tempfile({temporary_file, {_FileName, TempFile, _CurrentSize}, _}) -> TempFile;
handle_get_tempfile({memory, _, _}) -> undefined.

handle_get_data({temporary_file, _, _}) -> undefined;
handle_get_data({memory, {_FileName, DataState, _CurrentSize}, _}) -> DataState.

handle_complete_file(State) -> State.

get_tempfilename() ->
	Dir = simple_bridge_util:get_scratch_dir(?SCRATCH_DIR),
    Parts = [integer_to_list(X) || X <- binary_to_list(erlang:md5(term_to_binary(os:timestamp())))],
    filename:join([Dir, string:join(Parts, "-")]).


get_max_file_size() ->
	simple_bridge_util:get_max_file_size(?MAX_FILE_SIZE).

get_max_memory_size() ->
	simple_bridge_util:get_max_file_in_memory_size(?MAX_MEMORY_SIZE).