Загрузка файлов на сервер
Загрузка файлов на сервер - это одна из сложных проблем web-программирования. Несмотря на множество существующих решений этой проблемы программисты до сих пор не пришли к единому мнению относительно данного вопроса. Это, собственно, и подтверждается большим количеством решений. На сегодняшний день нет какого-либо общепризнанного, удобного, универсального способа решения данной задачи.
Введение в проблему
Корень проблемы заключается не в том, чтобы загрузить файл на сервер. С этим-то как раз проблем нет. Проблему составляет загрузка больших файлов на сервер. Больших, конечно, понятие растяжимое. Мы будем считать большими файлы больше 50 мегабайт.
Для загрузки файлов на сервер существует специальный механизм обзначенный в спецификациях HTTP и HTML. Данный механизм поддерживается всеми современными браузерами. Таким образом, клиентская часть системы вопросов у нас не вызывает. А вот на серверной стороне не все так просто. Мы не будем говорить обо всех web-серверах, и платформах программирования для серверной части web-систем (в некоторых из них, надо отметить, проблема давно решена). Мы будем говорить только об IIS и ASP.NET.
Так вот. Касательно обозначенных систем: загрузка файла на сервер происходит очень странно. Странно с точки зрения оптимальности работы. Разработчики ASP.NET впринципе не рассматривают вопрос загрузки больших файлов на сервер. Поэтму загружаемые файлы на сервере целиком попадают в оперативную память приложения. С одной стороны, конечно, пустяк. Ну что там 300 мегабайт, когда на сервере их 16 гигабайт. Однако, не все так просто как хочется. Каждому приложению IIS выделяет пространство ровно в 2 гигабайта. А сама система ASP.NET по умолчанию имеет ограничение на размер загружаемого файла.
Множество опытов с настроками показало, что при разрешении загрузки больших файлов и увеличении таймаута выполнения запроса большие файлы удается загрузить. Однако, при возникновении ошибок, при увеличении числа загружаемых файлов система неминуемо падает с ошибкой "Переполнение памяти". Следовательно, проблема заключается в том, что файлы попадают в память.
Существующие решения проблемы
Единственным очевидным принципом решения проблемы является перехват запроса на ранней стадии обработки, сохранение файла на диск и дальнейшая передача запроса обработчикам сохраненного файла.
В системах с использованием ASP применялись спецальные ISAPI-модули. Эти модули работают на достаточно низком уровне, в области памяти IIS. Задача таких модулей перехват всех запросов и их обработка. Обработка может также осуществляться несколькими ISAPI-модулями последовательно.
Данный подход получил наибольшее распространение для систем написанных с использованием ASP. Только использование ISAPI позволяет сохранить файл на диск избежав при этом размещения файла целиком в памяти.
К сожалению, для систем ASP.NET такое решение не очень подходит. ASP.NET просто не предназначена для общения со старыми ISAPI-компонентами. Да и не очень хочется увеличивать гетерогенность системы, вводя в неё допольнительные сторонние компоненты. Кроме того, для ASP.NET существует класс решений связанных с использованием HttpModule.
В ASP.NET существует специальный механизм обработки запросов, основанный на перехвате каждого запроса специальным классом - наследником HttpModule.
Построение решения в нашей системе
В нашей системе используется как раз последний обозначенный подход. В Internet можно найти множетсво коммерческих и свободных готовых модулей для обработки входящих файлов. Мы перепробовали практически все. Выводом было то, что нужно писать собственный модуль. Поскольку существующие либо плохо интегрировались в нашу систему, либо стоили дорого, либо имели неясные ошибки, исправить которые было сложнее чем написать модуль попроще, но работающий.
Если кому-то интересно как выглядит модуль изнутри - можете посмотреть в библиотеке классов, он называется FilesHttpModule. Далее мы опишем как его использовать в своих компонентах.
Как всегда система загрузки файлов делится на 2 части - серверную и клиентскую.

Файлы отправляемые на сервер должны быть отосланы на определенный URL. Модуль FilesHttpModule обнаружив запрос содержащий файл перехватывает его обработку. Он создает реализацию сразу двух классов: MultipartRequestProcessor и GenericUploadProcessor. В зависимости от URL на который приходит файл UploadProcessor может быть другим. После чего обработка запроса передается в объект MultipartRequestProcessor.
Объект MultipartRequestProcessor разбирает структуру пришедшего запроса (в спецификации HTTP). Как только он доходит до начала файла, он порциями отправляет данные обработчику UploadProcessor. Задача последнего сохранить уже файл во временный каталог, а по окончанию произвести над файлом определенные операции (копирование в файловую базу, создание описания в базе данных и т.п.)
Очевидно, что для правильной обработки файла на сервере одного только файла недостаточно. Файл должен сопровождаться набором метаинформации. Передача дополнительной информации была реализована не сразу, это повлекло за собой несколько последовательных переделок всей системы. Но тем не менее, теперь данные передавать можно и достаточно просто.
Допольнительные данные передаются посредством скрытых полей HTML-формы. MultipartRequestProcessor выделяет их из запроса и отправляет в UploadProcessor посредством метода AddMetaValue. После завершения разбора запроса обработчик файла имеет не только сам файл сохраненный во временном каталоге, но и набор данных, на основании которого он производит те или иные операци с файлом.
Вся эта кухна подробно описана в комментариях к членам означенных классов.
Теперь перейдем к клиентской стороне, наиболее интересной. Клиентская сторона, по давней традиции получает описание интерфейса в виде HTML с серверной стороны. Очевидно, задача серверной строны сформировать такое описание, реализация какового с успехом бы могла загрузить файл на сервер.
Для отдачи клиенту HTML структур загружающих файл на сервер используйте контрол FileUploader. Данный контрол генерирует собственно инпут, для отправки файла. Кроме того, он динамически показывает список уже загруженных файлов.
Файлы на сервере хранятся достаточно упорядоченно. В таблице FILES существует описание каждого файла и там же идет сквозная нумерация файлов. Для привязки файла к каждой конкретной записи в базе используется специальная таблица-расширение "_FILES", связывающая по внешнему ключу главную таблицу и таблицу FILES. Таким образом, каждый файл может быть привязан к нескольким записям, а к одной записи может быть привязано несколько файлов. Для контроля за количеством ссылок на файл используется специальное поле в таблице FILES, как только количество ссылок становится равным 0 - файл удаляется физически.
Все записи в базе данных производятся в одной транзакции классом UploadProcessor. Таким образом, для отправки файла на сервер и успешной его обработки, UploadProcessor должен знать к какой таблице привязывать файл и к какой записи его привязывать. Эти данные передаются на сервер в скрытом поле. Для удобства эти данные указываются в FileUploader в полях RecordID и TableName.
Итак, для того, чтобы устроить загрузку файлов на сервер необходимо выполнить следующие действия:
- Создать таблицу-расширение "_FILES" в ней должно быть всего 2 поля: внешний ключ главной таблицы и FILES_ID
- Создать реализацию класса FileUploader и проинициализировать значения RecordID и TableName.
- Обеспечить переход на отдельную страницу содержащую только этот контрол.
Пример использования данного контрола можно посмотреть в веб-модуле ArticleChanel.
И напоследок: поле CanWrite указывает сможет ли пользователь загружать файлы на сервер или управлять уже загруженными файлами. Если установить это поле в false, то вместо интерфейса загрузки файлов пользователь увидит только список файлов привязанных к записи.