Некоторое время назад я в блоге показывал фрагменты экспериментов с парсером значений HTTP-полей в RESTinio (вот, например, последний из этих постов на данный момент). Все это было нужно не само по себе, а как часть более общей задачи. В частности, как часть предоставления пользователям RESTinio простого механизма обработки загружаемых на сервер файлов (т.н. file uploading). Ниже приведены примеры того, с чего поддержка file upload начиналась месяц назад и к чему удалось прийти на данный момент. Кому интересно, милости прошу под кат.
Итак, началось все с попытки добавить в набор штатных примеров RESTinio еще один пример под названием file_upload, в котором бы показывалось, как можно отослать клиенту простую форму с элементом типа "file" и затем обработать multi-part POST, в котором на сервер придет содержимое какого-то файла.
Соответственно, получился довольно объемный (и не очень корректный) код, который вряд ли можно было бы использовать в качестве примера того, как RESTinio следует использовать. Непосредственно за работу с multi-part телом запроса и обработку той части тела, которая содержит файл, отвечал вот этот фрагмент:
bool starts_with( const restinio::string_view_t & where, const restinio::string_view_t & what ) noexcept { return where.size() >= what.size() && 0 == where.compare(0u, what.size(), what); } bool ends_with( const restinio::string_view_t & where, const restinio::string_view_t & what ) noexcept { return where.size() >= what.size() && 0 == where.compare( where.size() - what.size(), what.size(), what); } std::string to_string( const restinio::string_view_t & what ) { return { what.data(), what.size() }; } std::string get_boundary( const restinio::request_handle_t & req ) { // There should be content-type field. const auto content_type = req->header().value_of( restinio::http_field::content_type ); std::string boundary; if( !restinio::http_field_parser::try_parse_field_value( content_type, ';', restinio::http_field_parser::expect( "multipart/form-data" ), restinio::http_field_parser::name_value( "boundary", boundary ) ) ) throw std::runtime_error( "unable to parse content-type field: " + to_string( content_type ) ); if( boundary.empty() ) throw std::runtime_error( "empty 'boundary' in content-type field: " + to_string( content_type ) ); std::string result; result.reserve( 2u + boundary.size() ); result.append( "--" ); result.append( boundary ); return result; } struct line_from_buffer_t { restinio::string_view_t m_line; restinio::string_view_t m_remaining_buffer; }; line_from_buffer_t get_line_from_buffer( const restinio::string_view_t & buffer ) { const restinio::string_view_t eol{ "\r\n" }; const auto pos = buffer.find( eol ); if( restinio::string_view_t::npos == pos ) throw std::runtime_error( "no lines with the correct EOL in the buffer" ); return { buffer.substr( 0u, pos ), buffer.substr( pos + eol.size() ) }; } void store_file_to_disk( const app_args_t & args, const std::string & file_name, restinio::string_view_t raw_content ) { const restinio::string_view_t content_terminator{ "\r\n" }; if( ends_with( raw_content, content_terminator ) ) raw_content = raw_content.substr( 0u, raw_content.size() - content_terminator.size() ); std::ofstream dest_file; dest_file.exceptions( std::ofstream::failbit ); dest_file.open( args.m_dest_folder + "/" + file_name, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary ); dest_file.write( raw_content.data(), raw_content.size() ); } bool try_handle_body_fragment( const app_args_t & args, restinio::string_view_t fragment ) { // Process fields at the beginning of the fragment. restinio::optional_t< std::string > file_name; auto line = get_line_from_buffer( fragment ); for(; !line.m_line.empty(); line = get_line_from_buffer( line.m_remaining_buffer ) ) { std::string name_value; std::string filename_value; if( restinio::http_field_parser::try_parse_whole_field( line.m_line, "content-disposition", ';', restinio::http_field_parser::expect( "form-data" ), restinio::http_field_parser::name_value( "name", name_value ), restinio::http_field_parser::name_value( "filename", filename_value ) ) ) { if( name_value == "file" ) { file_name = filename_value; } } } if( file_name ) { store_file_to_disk( args, to_string( *file_name ), line.m_remaining_buffer ); return true; } return false; } void save_file( const app_args_t & args, const restinio::request_handle_t & req ) { const auto boundary = get_boundary( req ); const restinio::string_view_t eol{ "\r\n" }; const restinio::string_view_t last_separator{ "--\r\n" }; restinio::string_view_t body_view = [&boundary]( restinio::string_view_t body ) { auto start = body.find( boundary ); if( restinio::string_view_t::npos == start ) throw std::runtime_error( "the first separator " "isn't found in request body, boundary is: " + boundary ); return body.substr( start + boundary.size() ); }( req->body() ); while( body_view.size() > boundary.size() && !starts_with( body_view, last_separator ) ) { if( starts_with( body_view, eol ) ) body_view = body_view.substr( eol.size() ); else throw std::runtime_error( "EOL is not found after a fragment-openning " "boundary" ); const auto end = body_view.find( boundary ); if( restinio::string_view_t::npos == end ) throw std::runtime_error( "the next separator " "isn't found in request body, boundary is: " + boundary ); if( try_handle_body_fragment( args, body_view.substr( 0u, end ) ) ) break; body_view = body_view.substr( end + boundary.size() ); } } |
Поскольку в стандартной библиотеке C++14 нет таких полезных вещей, как starts_with и ends_with, то их так же приходится делать самостоятельно, что добавляет объема коду.
Но все-таки основная часть приведенного выше фрагмента -- это прямая, незамысловатая и неправильная попытка разобраться с multi-part телом запроса, что называется, "в лоб", методом грубой силы.
После того, как этот код появился, стало понятно, что если мы позиционируем RESTinio как, прежде всего, удобный в использовании инструмент, то нам нужно во что бы то ни стало огранить своих пользователей от написания подобных простыней. Тем более, что для написания еще и потребуется штудирование нескольких RFC.
Поэтому последовало несколько итераций над RESTinio и теперь пользователю потребуется написать вот столько кода:
void store_file_to_disk( const app_args_t & args, restinio::string_view_t file_name, restinio::string_view_t raw_content ) { std::ofstream dest_file; dest_file.exceptions( std::ofstream::failbit ); dest_file.open( fmt::format( "{}/{}", args.m_dest_folder, file_name ), std::ios_base::out | std::ios_base::trunc | std::ios_base::binary ); dest_file.write( raw_content.data(), raw_content.size() ); } void save_file( const app_args_t & args, const restinio::request_handle_t & req ) { using namespace restinio::file_upload; const auto enumeration_result = enumerate_parts_with_files( *req, [&args]( const part_description_t & part ) { if( "file" == part.name_parameter() ) { restinio::string_view_t file_name = part.filename_star_parameter() ? *(part.filename_star_parameter()) : *(part.filename_parameter()); store_file_to_disk( args, file_name, part.body() ); // There is no need to handle other parts. return handling_result_t::stop_enumeration; } // We expect only one part with name 'file'. // So if that part is not found yet there is some error // and there is no need to continue. return handling_result_t::terminate_enumeration; } ); if( !enumeration_result || 1u != *enumeration_result ) throw std::runtime_error( "file content not found!" ); } |
При этом функция store_file_to_disk изменилась, разве что, косметически.
Вся рутина теперь перешла во вспомогательную функцию enumerate_parts_with_files из нового пространства имен restinio::file_upload. Эта функция берет весь входящий запрос, анализирует значение его заголовка Content-Type и, если там лежит multipart/form-data с параметром boundary, то пытается разбить тело полученного запроса на отдельные части. Если разделить удалось, то для каждой части происходит дополнительный разбор на набор HTTP-заголовков (полей) и body. Если это удалось, то для каждой части проверяется наличие и содержимое Content-Disposition. Если для очередной части Content-Disposition содержит значение form-data и параметры name + filename/filename*, то для такой части вызывается заданный пользователем обработчик, в котором уже можно делать содержимым этой части что угодно. В том числе и записывать на диск.
В общем, удалось взять на себя весь геморрой, связанный с multipart запросами и дать пользователю одну большую кнопку "Сделать пи простую функцию.
Хотя есть ощущение, что неправильно ограничиваться одной enumerate_parts_with_files вот в таком вот виде. Подозреваю, что использовать ее будет удобно разве что если пришел запрос, в котором нас интересуют только загружаемые на сервер файлы. А вот если в POST-запросе лежит еще какая-то дополнительная информация, например, значения других полей из отправленной ранее клиенту формы, то тут нужно иметь еще что-то в дополнение к enumerate_parts_with_files...
В общем, есть еще надобность в дополнительных итерациях. Но направление движения и промежуточные результаты лично мне пока нравятся :)
Кстати говоря, если кто-то что-то хочет увидеть в RESTinio, то можно сказать нам об этом. Хоть здесь, хоть в Issues на github-е, хоть в Google-группе.
Комментариев нет:
Отправить комментарий