В этой статье Марк Салливан рассказывает, как с помощью ASP.NET и библиотек Uploadify реализовать мультизагрузку файлов с динамическими индикаторами выполнения. Созданный в процессе класс Http-обработчика, наряду с классом элемента управления, подойдет для любого другого приложения на ASP.NET, что делает этот код, с одной стороны, полезным plug-in-play решением, а с другой – наглядной демонстрацией работы с Http-обработчиком. Он также добавил ряд дополнительных функций на уровень ASP.NET, таких как фильтрация по расширению файла, обратная передача по завершении и безопасность на основе сеансов.



Общая информация


С выходом HTML5 появилась надежда на возможность реализации одновременной загрузки нескольких файлов без использования ActiveX или других плагинов. Но чистый HTML5 не поддерживает динамические индикаторы во время загрузки, к тому же он по-разному обрабатывается в некоторых браузерах. Мне же было необходимо безопасное решение, позволяющее реализовать фильтрацию по расширению файла.

Я открыл для себя Uploadify, когда искал замену предыдущему решению. Эта библиотека изначально написана как бесплатный Flash-плагин, но в 2012 году было также представлено решение для HTML5. Стоимость HTML5-библиотеки варьируется от $5 до $100 в зависимости от того, собираетесь ли вы ее распространять. На мой взгляд, это невысокая цена как для такого качественного продукта, но, конечно, решать вам. В любом случае, я также сделал реализацию Flash-версии, если она вдруг кому-то пригодится. Возможно, мне и самому удастся применить ее для поддержки некоторых своих проектов в более старых браузерах. К сожалению, я пока не встречал в интернете каких-либо наглядных примеров реализации этого решения на ASP.NET.



Я заключил опции в оболочку веб-элемента управления C# и класса C# (для записи напрямую в выходной поток без элементов управления). Классы обрабатывают большинство опций, обрабатываемых библиотекой. Я также добавил поддержку JavaScript для фильтрации загружаемых файлов со стороны сервера и клиента. Что касается безопасности и фильтрации на стороне сервера, для их реализации требуется состояние сессии. Тем не менее они помогают предотвратить нежелательные загрузки. Другие необходимые для проекта опции тоже добавлены в классы или элементы управления C#.

Использование кода


Вступительные замечания

Прежде чем использовать предоставленный код, вам следует выбрать между бесплатной Flash-библиотекой и платной HTML5-библиотекой. Всё содержимое первой нужно поместить в пустую подпапку Uploadify, а второй – в UploadiFive. Flash-версия используется в третьем демо (Demo3.aspx), а HTML5-версия – в первых двух.

Или же вы можете обернуть классы вокруг поля мультизагрузки файлов в HTML5.

Подключение библиотек

Необходимо добавить ссылки к нужной библиотеке jQuery и выбранной библиотеке Uploadify. Стандартный файл CSS поставляется с пакетами, поэтому я добавил в своем коде ссылку и на него. Добавьте следующие строки внутри тега head вашей формы ASPX.

<script src="jquery/jquery-1.10.2.js" type="text/javascript"></script>
<script src="uploadifive/jquery.uploadifive.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="uploadifive/uploadifive.css" />

или так:

<script src="jquery/jquery-1.10.2.js" type="text/javascript"></script>
<script src="uploadify/jquery.uploadify.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="uploadify/uploadify.css" />


Добавление на страницу элемента управления

В веб-формах ASP.NET добавить элемент управления на страницу можно, создав экземпляр кода класса UploadiFiveControl и добавив его в форму или заполнитель.

// Get the save path
string savepath = Context.Server.MapPath("files");

// Create the upload control and add to the form
UploadiFiveControl uploadControl = new UploadiFiveControl
    {
        UploadPath = savepath, 
        RemoveCompleted = true, 
        SubmitWhenQueueCompletes = true, 
        AllowedFileExtensions = ".jpg|.bmp"
    };

form1.Controls.Add(uploadControl);

Добавление элемента управления в MVC происходит аналогичным образом.

Работать с Flash-версией довольно просто, так как она использует те же классы и элемент управления, но флаг версии выставлен на Flash, а не на default. Остальные изменения, включая названия некоторый опций, обрабатываются элементом управления.

// Create the upload control and add to the form
UploadiFiveControl uploadControl = new UploadiFiveControl
    {
        UploadPath = savepath, 
        RemoveCompleted = true, 
        SubmitWhenQueueCompletes = true, 
        AllowedFileExtensions = ".jpg|.bmp",
        Version = UploadiFive_Version_Enum.Flash
    };

form1.Controls.Add(uploadControl);

Http-обработчик для загруженных файлов

Также необходимо добавить в проект класс Http-обработчика UploadiFiveFileHandler, который обрабатывает файлы по мере их загрузки на сервер. Эта часть реализации меньше всего описана где-либо в справочниках или блогах, и именно поэтому я посвятил ей весь следующий раздел. Чтобы включить этот класс, скопируйте файлы UploadiFiveFileHandler.ashx и UploadiFiveFileHandler.ashx.cs в ваш проект.

Http-обработчик и безопасность

Создать Http-обработчик довольно просто, но гораздо сложнее обеспечить его гибкость и внести некоторые дополнительные функции. Я использовал состояние сессии, чтобы обеспечить безопасность и передачу информации с загружающей страницы в обработчик. Для этого я создал класс UploadiFive_Security_Token. Вот как выглядит его объявление:

/// <summary> Token used to add security, via adding a key to the session
/// state, for uploading documents through this system </summary>
public class UploadiFive_Security_Token
{
  /// <summary> Path where the uploaded files should go </summary>
  public readonly string UploadPath;

  /// <summary> List of file extensions allowed </summary>
  public readonly string AllowedFileExtensions;

  /// <summary> Name of the file object to use in your server-side script</summary>
  public readonly string FileObjName;

  /// <summary> The GUID for this security token </summary>
  public readonly Guid ThisGuid;

  /// <summary> Constructor for a new instance of the UploadiFive_Security_Token class </summary>
  /// <param name="UploadPath" /> Path where the uploaded files should go 
  /// <param name="AllowedFileExtensions" /> List of file extensions allowed 
  /// <param name="FileObjName" /> Name of file object to use in your server-side script 
  public UploadiFive_Security_Token(string UploadPath, string AllowedFileExtensions, string FileObjName )
  {
    this.UploadPath = UploadPath;
    this.AllowedFileExtensions = AllowedFileExtensions;
    this.FileObjName = FileObjName;
    ThisGuid = Guid.NewGuid();
  }
}

В данном классе содержится путь к файлу, допустимые расширения файла и ключ, по которому обработчик будет находить файл. При рендеринге класса UploadiFiveControl создается новый токен безопасности, который хранится в состоянии сессии под GUID. Сам GUID добавляется в словарь FormData, потом передается в библиотеку Uploadify и затем возвращается в обработчик с данными файла. После этого обработчик может отыскать в сессии токен безопасности, основанный на соответствующем GUID.

// Create a new security token with all the configuration info
UploadiFive_Security_Token newToken = 
          new UploadiFive_Security_Token(UploadPath, AllowedFileExtensions, FileObjName);

// Add this token to the current session for the HttpHandler
HttpContext.Current.Session["#UPLOADIFIVE::" + newToken.ThisGuid.ToString()] = newToken;

// Save the token into the formdata so comes to the HttpHandler
FormData["token"] = newToken.ThisGuid.ToString();

Данные формы включены в код JavaScript для создания элемента управления Uploadify. Ниже приведен пример полученного в результате кода HTML без каких-либо ограничений по расширению файла:

<input id="file_upload" name="file_upload" class="file_upload" type="file" />

<script type="text/javascript">
  $(document).ready(function() {
    $('#file_upload').uploadifive({
      'fileObjName': 'Filedata',
      'formData': { 'token' : 'da66e0ad-750b-4d76-a016-72633dea8b53' },
      'onQueueComplete': function (uploads) { $('#file_upload').closest("form").submit(); },
      'uploadScript': 'UploadiFiveFileHandler.ashx'
    });
  });
</script>

Как видно выше, генерируемый токеном безопасности GUID включается в код JavaScript для инициализации библиотеки.

Http-обработчику требуется доступ к состоянию сессии, поэтому ему необходимо реализовать интерфейс IReadOnlySessionState. Вот как выглядит полный код метода обработчика ProcessRequest без каких-либо ограничений по расширению файла:

Context.Response.ContentType = "text/plain";
Context.Response.Expires = -1;

// Try to get the security token key
string tokenKey = Context.Request["token"];
if (tokenKey == null)
{
    Context.Response.Write("No token provided with this request");
    Context.Response.StatusCode = 401;
    return;
}

// Try to get the matching token object from the session
UploadiFive_Security_Token tokenObj = 
  Context.Session["#UPLOADIFIVE::" + tokenKey] as UploadiFive_Security_Token;
if (tokenObj == null)
{
    Context.Response.Write("No matching server-side token found for this request");
    Context.Response.StatusCode = 401;
    return;
}

try
{
    // Get the posted file from the appropriate file key
    HttpPostedFile postedFile = Context.Request.Files[ tokenObj.FileObjName ];
    if (postedFile != null)
    {
        // Get the path from the token and ensure it exists
        string path = tokenObj.UploadPath;
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);

        string filename = Path.GetFileName(postedFile.FileName);
        postedFile.SaveAs(path + @"\" + filename);

        // Post a successful status
        Context.Response.Write(filename);
        Context.Response.StatusCode = 200;
    }
}
catch (Exception ex)
{
    Context.Response.Write("Error: " + ex.Message);
    Context.Response.StatusCode = 500;
}

Если токен в сессии не найден, файл не будет допущен к загрузке.

Ограничения по расширению файла

Хорошая практика при разработке загрузчика – добавлять механизм ограничения файлов по их расширению. В этом разделе я покажу, как мне удалось это сделать на клиентской и серверной стороне с учетом рассмотренных выше параметров безопасности.

Проверка на клиентской стороне реализована как событие в библиотеке UploadiFive. Но событие добавления файла onChange может быть использовано и за пределами библиотеки.

Допустимые расширения передаются в элемент управления UploadiFiveControl в виде строки, разделенной запятыми.

uploadControl.AllowedFileExtensions = ".jpg|.bmp";

Если всё задано верно, код JavaScript прикрепляется к событию onAddQueueItem при рендеринге HTML.

<script type="text/javascript">
  $(document).ready(function() {
    $('#file_upload').uploadifive({
      'fileObjName': 'Filedata',
      'formData': { 'token' : '4c893799-fd21-4d85-80c4-e32e6cacc794' },
      'removeCompleted': true,
      'onAddQueueItem' : function(file) {
          var extArray = JSON.parse('[ ".jpg", ".bmp" ]');
          var fileName = file.name;
          var ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
          var isExtValid = false;
          for(var i = 0; i < extArray.length; i++) 
          { 
            if ( ext == extArray[i] ) 
            { 
              isExtValid = true; break; 
            }
          }
          
          if ( !isExtValid )    
          {  
            alert("File types of '<extension>' are not allowed".replace('<extension>', ext));                     
            $('#file_upload').uploadifive('cancel', file);  
          }
        },
      'uploadScript': 'UploadiFiveFileHandler.ashx'
    });
  });
</script>

Этот код берет список расширений, преобразованный элементом управления C# в JSON- массив и сверяет конкретное расширение с данными массива. Если это расширение недопустимо, пользователь получает предупреждение, а файл удаляется из очереди на загрузку.

Код Http-обработчика на серверной стороне выглядит примерно так же. В нем токен безопасности используется для получения списка допустимых расширений:

// Get the filename for the uploaded file
string filename = Path.GetFileName(postedFile.FileName);

// Are there file extension restrictions?
if ( !String.IsNullOrEmpty(tokenObj.AllowedFileExtensions))
{
  string extension = Path.GetExtension(postedFile.FileName).ToLower();
  List<string> allowed = tokenObj.AllowedFileExtensions.Split("|,".ToCharArray()).ToList();
  if (!allowed.Contains(extension))
  {
    Context.Response.Write("Invalid extension");
    Context.Response.StatusCode = 401;
    return;
  }
}

// Save this file locally
postedFile.SaveAs(path + @"\" + filename);

Теперь ограничение недопустимых форматов на клиентской и серверной стороне можно считать реализованным.

Ограничения по размеру файла

Существует также ряд способов ограничить загрузку файлов по их размеру. У сервера IIS имеются такие ограничения, и об этом следует помнить при работе. К тому же в Uploadify тоже есть способ добиться ограничения размеров загружаемых файлов, о чем я рассказывал.

В файле web.config можно задать максимальный размер для допустимого типа контента, что будет накладывать ограничения на загружаемые файлы. За это отвечает тег requestLimits, принимающий значения в количестве байтов. К примеру, фрагмент кода ниже ограничивает размер загружаемого файла примерно до 200 Мб. Этот фрагмент тоже был включен в код в файле web.config.

<configuration>
    <system.webServer>
        <security>
            <requestLimits maxAllowedContentLength="209715200" />
        </security>
    </system.webServer>
</configuration>    

Помимо этого, библиотека Uploadify использует свойство JavaScript fileSizeLimit в качестве FileSizeLimit в классах C#. Это выглядит как строка вроде “100KB” или “200MB”.

Flash как резервный вариант и вопросы совместимости

Некоторые популярные браузеры, в особенности IE9, не получают необходимых обновлений для того, чтобы поддерживать HTML5. Потому, если в нашем случае не позаботиться о резервном варианте, пользователь старого браузера увидит только стандартное для HTML поле загрузки файла без кнопки загрузки. При этом, конечно же, ничего не будет работать.

К счастью, мы можем взять в качестве резервного варианта Flash-версию, которая очень похожа на HTML5-версию. Для этого используется событие onFallback. Оно было дополнительно реализовано в классах C#, доступных для загрузки здесь. Если добавить эту функцию в текущий код с помощью элемента управления UploadiFiveControl, получится примерно такой код:

// Create the upload control and add to the form
UploadiFiveControl uploadControl = new UploadiFiveControl
{
    UploadPath = savepath,
    RemoveCompleted = true,
    Version = UploadiFive_Version_Enum.HTML5,
    RevertToFlashVersion = true,
    NoHtml5OrFlashMessage = "Please try a more current browser"
};

В коде ниже мы указываем классу добавить резервный вариант Flash, выставив свойство RevertToFlashVersion на true. Если же браузер не поддерживает ни HTML5, ни Flash, можно вывести окно с предупреждением, используя свойство NoHtml5OrFlashMessage.

Пример выше генерирует следующий код JavaScript, который будет включен в код HTML вашей aspx-страницы:

<script type="text/javascript">
 $(document).ready(function() {
    $('#file_upload').uploadifive({
      'fileObjName': 'Filedata',
      'formData': { 'token' : 'f8916a9f-9dda-441f-a58b-13948e61f7e7' },
      'removeCompleted': true,
      'uploadScript': 'UploadiFiveFileHandler.ashx',
      'onFallback': function() {
                           // Revert to flash version if no HTML5
                           $('#file_upload').uploadify({
                             'fileObjName': 'Filedata',
                             'formData': { 'token' : 'f8916a9f-9dda-441f-a58b-13948e61f7e7' },
                             'removeCompleted': true,
                             'swf': 'uploadify/uploadify.swf',
                             'uploader': 'UploadiFiveFileHandler.ashx',
                             'onFallback': function() { alert('Please try a more current browser'); }
                           });

                    }
    });
  });

</script>

Теперь у нас есть рабочий резервный вариант для старых браузеров, который практически идентичен основной версии. Для срабатывания резервного механизма необходима Flash-версия, но только в качества второстепенного варианта, как в моем примере. Конечно же, вам нужно будет скачать обе версии и привязать ссылки на соответствующие им CSS- и JS-файлы внутри тега head.

Ссылка на код:

Весь код и четыре демо-страницы aspx доступны для скачивания по ссылке.

Комментарии (6)


  1. lair
    31.10.2017 15:14

    HTTP-хэндлеры? В век asp.net core, OWIN и несчастного WebAPI? Спасибо, но нет.


    (оригинальной статье все-таки три с половиной года, устарело уже)


    1. ZanziPanzi
      31.10.2017 16:12
      +2

      Но интересно как ретроспектива.


    1. Evengard
      31.10.2017 17:23
      +1

      Почему WebAPI несчастен?


      1. lair
        31.10.2017 17:37

        В этом конкретном списке он просто (на мой вкус) наименее подходит для загрузки файлов, особенно если остальное приложение его не использует.


  1. MRomaV
    01.11.2017 00:23

    уже немного не актуальна статья на сегодняшнее время


    1. MRomaV
      01.11.2017 18:33

      Вот более актуальна статья www.hishambinateya.com/uploading-files-in-razor-pages