ASP.NET MVC 3 для начинающих: загрузка файлов на сервер

    image

    В ходе обучения новым технологиям перед новичками часто возникают типовые задачи, которые не так просто решить. В цикле статей MVC3 для начинающих будут представлены решения таких задач.

    Недавно, на конференции для разработчиков в Екатеринбурге, где я рассказывал про ASP.NET мне задали вопрос о том, как с помощью MVC3 и Razor организовать загрузку одного или нескольких файлов со стороны клиента на сервер. Вполне типовая задача, которая очень легко и элегантно решается в ASP.NET MVC3.

    Ниже представлено полное решение с исходными кодами.

    Разметка


    Прежде всего сформируем разметку для страницы Index.cshtml контроллера Home:

    <h2>Загрузка одного файла</h2>
    <p>
    @using (Html.BeginForm("", "home", FormMethod.Post, new {enctype="multipart/form-data"}))  {       
        <input type="file" name="fileUpload" /><br />      
        <input type="submit" name="Submit" id="SubmitSingle" value="Upload" />
    }
    </p>
    <h2>Загрузка нескольких файлов</h2>
    <p>
    @using (Html.BeginForm("", "home", FormMethod.Post, new {enctype="multipart/form-data"}))  {      
        <input type="file" name="fileUpload[0]" /><br />      
        <input type="file" name="fileUpload[1]" /><br />      
        <input type="file" name="fileUpload[2]" /><br />      
        <input type="submit" name="Submit" id="SubmitMultiply" value="Upload" />
    }
    </p>

    Здесь есть несколько важный моментов, которые необходимо разобрать:
    • для передачи больших файлов используется MIME-тип данных multipart/form-data;
    • при формировании формы с несколькими элементами загрузки файлов необходимо формировать имена элементов (атрибуты name) с индексаторами [№]. Это соглашение ASP.NET MVC3, которое позволяет автоматически получить массив элементов при работе с действиями контроллеров.
    Как можно убедиться, код разметки тривиален.

    Код действия в контроллере


    Добавим контроллер Home действие Index с атрибутом Post и некоторыми параметрами:

    [HttpPost]
    public ActionResult Index(IEnumerable<HttpPostedFileBase> fileUpload)
    {     
        foreach (var file in fileUpload)     
        {          
            if (file == null) continue;                        
            string path = AppDomain.CurrentDomain.BaseDirectory + "UploadedFiles/";
            string filename = Path.GetFileName(file.FileName);
            if (filename != null) file.SaveAs(Path.Combine(path, filename));
        }     
    
        return RedirectToAction("Index");
    }

    Здесь есть несколько важных моментов:
    • с помощью атрибута HttpPost мы определяем действие, которое будет обрабатывать только POST-запросы;
    • параметр IEnumerable<HttpPostedFileBase> fileUpload представляет перечисление файлов, которые были отправлены пользователем. Обратите внимание, что имя fileUpload совпадает со значением атрибута name элементов формы, в том числе, с теми которые содержат индексаторы [№]. Именно наличие индексаторов позволяет MVC3 автоматически присваивать в параметр перечисление.
    Как можно убедиться еще раз, код действия не менее тривиален, чем код разметки.

    Ограничение на размер запроса и web.config


    В ASP.NET существует ограничение на размер запроса, который может быть передан на сервер. Это ограничение введено, в том числе, из соображений безопасности и легкого предотвращения атак на сервер путем формирования тяжелых запросов. Тем не менее, при создании функционала по загрузке на сервер файлов, особенно когда их несколько и они могут быть большого размера следует предварительно настроить параметр ограничения на нужное значение.

    Параметр, который управляет максимальным размером запроса находится в web.config:

    <system.web>
        <!--ограничение на размер запроса-->
        <httpRuntime maxRequestLength="10000" />

    Параметр maxRequestLength указывает максимально допустимый размер в килобайтах, который может иметь запрос. Таким образом в коде выше установлен лимит на запрос в ~10 мегабайт. То есть пользователь может передать в одном запросе файл или файлы размером около десяти мегабайт. Учтите только, что запрос состоит не только из данных, но и некоторой обвязки, которая тоже формирует данные запроса. Эта обвязка имеет небольшой размер, но ее следует учитывать при расчете необходимого лимита на размер запроса.

    Исходный код проекта


    Вы можете загрузить исходный код работающего проекта для Visual Studio 2010 по этой ссылке.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 24

      +10
      А почему AppDomain.CurrentDomain.BaseDirectory + «UploadedFiles/», а не Server.MapPath("~/UploadedFiles")?
        0
        Лично у меня были ситуации, когда определенные задания запускались по таймеру (к примеру — массовая рассылка писем, и подгрузка аттачей/шаблона тела письма с файла на сервере). В эти моменты System.Web.HttpContext.Current равен null, а значит и Server недоступен.

        К примеру Server.MapPath заменялось на System.Web.Hosting.HostingEnvironment.MapPath в моих задачах.

        Вероятно AppDomain.CurrentDomain.BaseDirectory тут используется по той же причине
          0
          Задание запускалось в рамках web-приложения?
        +2
        Присоединяюсь к предыдущему вопросу и дополнил бы что

        string path = AppDomain.CurrentDomain.BaseDirectory + «UploadedFiles/»;

        лучше бы вынести за пределы цикла. Конечно в данном случае это микрооптимизация, но лучше на такие вещи обращать внимание, иначе можно как то запихнуть в цикл что то более ресурсоемкое и не заметить.
          0
          Ну, возможно, в топике так с целью простоты примера, так то в контроллерах вообще надо избегать таких dependency как AppDomain.
            0
            Ну тогда уж Path.Combine(...) =)
            0
            Ну это уж совсем, конечно, для чайников.

            Но есть пара замечаний:

            при формировании формы с несколькими элементами загрузки файлов необходимо формировать имена элементов (атрибуты name) с индексаторами [№].


            достаточно просто name=«someField[]»

            про Server.MapPath уже сказали, добалю, что лично я использую также не конкатенацию строк, а String.Format, получается более изящно и гибко

            ну и public ActionResult Index(IEnumerable fileUpload) избыточно, на мой взгляд. доступ к файлам можно получить из Request.Files
              0
              ой, простите, во-первых «добавлю», а во-вторых, забыл закрыть тег.
                0
                Не согласен насчёт избыточности, тестировать гораздо проще.
                  0
                  В этом плане согласен.
                  +1
                  «public ActionResult Index(IEnumerable fileUpload)»

                  Хех, мало того, на практике еще и

                  public object UploadFile(FileUploadModel model)

                  Никакого доступа к Request.Files — для этого есть ModelBinder, и это не должно быть в контроллере.
                  0
                  А мне интересно, как такой код можно протестировать?
                    0
                    код сохранения переданных на вход файлов?
                    можно протестировать их наличием на диске и совпадением CRC после выполнения метода
                      0
                      Ммм тогда будет тестироваться SaveAs, плюс это будет даже не юнит-тест.
                      +1
                      Такой? Да никак :) Чего стоит могучий AppDomain в методе контроллера.
                        0
                        Ну SaveAs абстрагируем (тем более что хранилище в любом случае стоит абстрагировать) — и ура.
                        0
                        Вообще сама задача загрузки файлов не настолько тривиальна, практически всегда требуется сделать одновременную загрузку нескольких файлов произвольного размера и progress bar.
                        <зануда>Не назвал бы приведенный подход «элегатным»<зануда/>
                          0
                          А вот вопрос, вы делаете file.SaveAs(Path.Combine(path, filename)), т.е. имя файла берется из загружаемого файла и если такой файл на сервере уже есть, он будет перезаписан? или MVC3 обеспечивает уникальные имена?
                            0
                            будет перезаписан
                              0
                              тогда может стоит отметить этот факт раз статья для начинающих? в данном виде код не безопасный
                            +1
                            Еще можно добавить, что для IIS 7 при конфигурировании максимального размера для загружаемых файлов надо настроить не только maxRequestLength, но и
                            <system.webServer>
                               <security>
                                  <requestFiltering>
                                     <!--ограничение на размер запроса в байтах-->
                                        <requestLimits maxAllowedContentLength="10000000"&gh;</requestLimits>


                            А также, при сохранении файла надо еще пару тривиальных проверок сделать:
                            1. На тип файла. Не хотите же, чтобы на сервер загружали .aspx файлы :).
                            2. На разрешенные символы в имени файла. И заменить недопустимые.

                            И спасибо за [№], не знал, что так можно.
                              0
                              1. А может мы хотим, чтобы .aspx файлы загружали? Что такого-то… главное, права на исполнение убрать для папки закачек. )
                                0
                                Тогда права на чтение надо убирать :)
                                .aspx файл не исполняется сам. Его асп.нет рантайм исполняет, ему надо только прочитать содержимое файла. А без прав на чтение нельзя будет и обыный текстовый файл или картинку, например, считать.
                                +1
                                вы правы насчет maxAllowedContentLength, однако, по умолчанию значение атрибута равно 30000000, то есть ~28 мегабайт, следовательно ваш пример с 10 мегабайтами излишен :-)

                                но спасибо за дополнение, это действительно важно и полезно…

                                * да, комментарии на Хабре не менее важны, чем сама статья

                              Only users with full accounts can post comments. Log in, please.