Skip to content

Instantly share code, notes, and snippets.

@egobrain
Created May 30, 2012 06:47
Show Gist options
  • Save egobrain/2834137 to your computer and use it in GitHub Desktop.
Save egobrain/2834137 to your computer and use it in GitHub Desktop.
Cowboy files uploader.
%%%-------------------------------------------------------------------
%%% @author egobrain <egobrain@linux-ympb>
%%% @copyright (C) 2012, egobrain
%%% @doc
%%% Function for uploading files and properties,which were sent as a
%%% multipart. Files are stored in tmp_folder with random name,
%%% generated by tmp_filename function.
%%% @end
%%% Created : 25 Mar 2012 by egobrain <egobrain@linux-ympb>
%%%-------------------------------------------------------------------
-module(cowboy_multipart_uploader).
-export([files_qs/1,
files_qs/2]).
-include("log.hrl").
-define(MAX_FILE_SIZE,5000).
-define(MAX_FILES,unlimited).
-define(TMP_PATH,"/tmp").
-define(TMP_FILE_NAME,fun tmp_filename/1).
-record(state,{
headers = [],
max_file_size = ?MAX_FILE_SIZE :: non_neg_integer(),
max_files = ?MAX_FILES :: non_neg_integer() | unlimited,
tmp_folder = ?TMP_PATH :: string(),
tmp_filename = ?TMP_FILE_NAME :: function(),
files_cnt = 0
}).
-record(datafile,{name = <<"">> :: binary(),
size = 0 :: non_neg_integer(),
'Content-Type' :: binary(),
'Original-Name' :: binary()
}).
%% ===================================================================
%%% Types
%% ===================================================================
-type datafile() :: #datafile{}.
-type headers() :: [datafile() | {cowboy_http:header(), iodata()}].
-type option() :: {max_file_size, Size :: non_neg_integer()} |
{tmp_filename,Fun :: function()} |
{tmp_folder, Path :: string}.
-type options() :: [option()].
-type error() :: {error, file_too_big | max_files_limit}.
-export_type([datafile/0,headers/0,options/0]).
%% ===================================================================
%%% Api
%% ===================================================================
-spec files_qs(Req :: cowboy_http:req()) -> {headers(), Req :: cowboy_http:req() } | error().
files_qs(Req) ->
files_qs(Req, []).
-spec files_qs(Req :: cowboy_http:req() , Options :: options()) -> {headers(), Req :: cowboy_http:req()} | error().
files_qs(Req, Options) ->
State = #state{headers=[],
files_cnt=0,
max_file_size= proplists:get_value(max_file_size,Options, ?MAX_FILE_SIZE),
max_files = proplists:get_value(max_files, Options, ?MAX_FILES),
tmp_folder = proplists:get_value(tmp_folder, Options, ?TMP_PATH),
tmp_filename = proplists:get_value(tmp_filename, Options, ?TMP_FILE_NAME)
},
acc(Req, State).
%% ===================================================================
%%% Internal functions
%% ===================================================================
acc(Req,State) ->
{Result, Req2} = cowboy_http_req:multipart_data(Req),
acc(Result,Req2,State).
acc({headers,NewHeaders},Req,#state{headers=Headers,
tmp_folder=TmpFolder,
tmp_filename=TmpFilenameFun,
max_file_size=MaxFileSize,
max_files=MaxFiles,
files_cnt=FilesCnt
} = State) ->
case parse_headers(NewHeaders) of
{error,_Reason} = Err -> Err;
{Name, undefined} ->
{Value,Req2} = acc_property(Req),
acc(Req2,State#state{headers=[{Name,Value}|Headers]});
{Name, Filename} ->
case MaxFiles =:= unlimited orelse FilesCnt < MaxFiles of
true ->
TempFilename = filename:join([TmpFolder,TmpFilenameFun(Filename)]),
{ok, File} = file:open(TempFilename, [raw, write]),
Res = acc_file(Req,File,MaxFileSize),
file:close(File),
case Res of
{error,file_too_big} = Err ->
file:delete(TempFilename),
delete_files(Headers),
Err;
{FileSize,Req2} ->
FileProp = #datafile{name=TempFilename,
size=FileSize,
'Original-Name'=Filename,
'Content-Type'=proplists:get_value('Content-Type',NewHeaders)},
acc(Req2,State#state{headers=[FileProp|Headers],files_cnt=FilesCnt+1})
end;
false ->
delete_files(Headers),
{error,max_files_limit}
end
end;
acc(eof,Req,#state{headers=Headers}) -> {Headers,Req}.
parse_headers(Headers) ->
case proplists:get_value(<<"Content-Disposition">>,Headers) of
undefined ->
{error,"No content disposition in header"};
Value ->
{_FormData,Props} = cowboy_multipart:content_disposition(Value),
Name = proplists:get_value(<<"name">>,Props),
Filename = proplists:get_value(<<"filename">>,Props),
{Name,Filename}
end.
-spec acc_file(Req :: cowboy_http:req() , File :: file:io_device() ,MaxFileSize :: non_neg_integer()) ->
{FileSize :: non_neg_integer(), Req2 :: cowboy_http:req()} |
{error,file_too_big}.
acc_file(Req,File,MaxFileSize) -> acc_file(Req,File,0,MaxFileSize).
acc_file(Req,File,FileSize,MaxFileSize) ->
{Result, Req2} = cowboy_http_req:multipart_data(Req),
case Result of
end_of_part -> {FileSize,Req2};
{body,Data} ->
NewFileSize = FileSize + byte_size(Data),
case NewFileSize > MaxFileSize of
true ->
{error,file_too_big};
false ->
ok = file:write(File,Data),
acc_file(Req2,File,NewFileSize,MaxFileSize)
end
end.
-spec acc_property(_Req) -> {Data :: binary(),_Req2}.
acc_property(Req) ->
acc_property(Req,<<>>).
acc_property(Req,Buff) ->
{Result, Req2} = cowboy_http_req:multipart_data(Req),
case Result of
end_of_part -> {Buff,Req2};
{body,Data} -> acc_property(Req2,<<Buff/binary,Data/binary>>)
end.
%% ===================================================================
%%% Default Functions
%% ===================================================================
-spec tmp_filename(OriginalName :: binary()) -> string() | [string()].
tmp_filename(_OriginalName) ->
atom_to_list(?MODULE) ++ integer_to_list(erlang:phash2(make_ref())).
delete_files(Headers) ->
lists:foreach(fun(#datafile{name=FName}) ->
file:delete(FName);
(_) -> ok
end,Headers).
@Kozlov-V
Copy link

hi, Egobrain :-)
Thanks - its good!
%%is this supports resumable transfers upload files?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment