Скачивание распространённая функциональность интерактивных сайтов. Бесчисленное множество файлов различного содержимого скачивается в интернете каждую секунду. Компьютеры, планшеты, смартфоны с разными операционными системами активно участвуют в скачивании файлов.
Функциональность Download - это возможность скачивания файлов с серверной стороны на клиентскую машину. В результате этой операции копия файла, находящегося на сервере, сохраняется на машине клиента.
Способностью отсылать файлы клиентам обладают и HTML статические страницы и конечно же динамические интерактивные сайты. Download гораздо безопасней своей парной операции Upload, когда пользователи загружают, иногда спамовые и вредные, файлы на сервер.
Существуют множество систем, платформ и языков для динамического интернет-программирования: PHP, Python, ASP.NET, Perl, Go, Ruby и другие. Данная статья описывает некоторые способы обеспечения скачивания файлов c сайтов, созданных на платформе ASP.NET. Active Server Pages .NET бесплатная, кроссплатформенная технология веб-программирования с открытым исходным кодом. Язык C# обеспечивает сайты ASP.NET максимальной динамичностью, позволяет с легкостью создавать разнообразные композиции интерактивности.
Разберём на примере веб-приложений ASP.NET варианты программного кода для скачивания файлов с серверной стороны. Операцию Download будут выполнять приложения, построенные на каркасе Модель-Представление-Контроллер (MVC, распространённая схема создания сайтов) и Pages Razor (облегчённый каркас MVC, где функцию контроллеров выполняют методы классов производных от PageModel. Razor Pages предназначен для быстрого написания динамических веб-страниц). Оба исходника используют среду NET. Core обеспечивающую работу приложений в различных операционных системах.
Отдавать файлы динамические сайты ASP.NET могут как с открытых каталогов, так и закрытых, к которым нет прямого доступа. С открытого каталога пользователь может скачать файл даже введя прямой адрес в браузер. С закрытого каталога прямым доступом скачать файл нельзя, только посредством промежуточного программного кода. Если в адресную строку ввести путь к закрытому каталогу, то результатом будет ошибка 404. Данным способом владелец сайта может ограничивать доступ к определенным файлам.
Отправлять файлы пользователем сайта можно несколькими способами:
Классический способ отправки файлов клиенту обеспечивает чистый HTML. Данным способом можно пользоваться только если файлы хранятся в открытом каталоге сайта.
Можно загрузить файл в оперативную память, при необходимости добавить к контенту файла свой цифровой знак, и уже из памяти отдать поток в консоль.
Считать содержимое файла в массив байтов и далее отправить массив байтов в поток вывода HTTP. Этот способ удобен для отправки файлов, когда их содержимое считывается из базы данных.
Открыть поток с содержимым файла и сразу же отдать его на вывод HTTP. Похож на первый способ, но позволяет скачивать файлы с закрытых каталогов и хранимых в базе данных.
Для отправки файлов значительного размера годятся только классический HTML и способ с открытием потока непосредственно на вывод HTTP. Другие способы тоже работают с большими файлами, но в процессе скачивания, на сервере, будут потребляться значительные объемы оперативной памяти. При нехватке памяти могут происходить сбои в работе веб-страницы, а возможно и сайта.
Для скачивания файлов HTML тег гиперссылки должен содержать атрибут download. Данный атрибут определяет, что целью гиперссылки будет скачивание файла, но не его открытие в браузере. В атрибуте можно определить желаемое имя скачиваемого файла.
Загрузочный код HTML легко реализовать динамически в представлении любого типа сайта на ASP.NET. Программный код контроллера сформирует список файлов и посредством объекта модели данные можно извлечь на веб-странице.
Каркас MVC удобен тем, что один и тоже метод Action может отдавать контент различного типа. Action-метод контроллера может вызвать представление или отправить файл пользователю. Так и реализовано в прикреплённом исходном коде сайта на MVC с функциональностью download files.
Представление получает через свойство модели ListFiles список файлов доступных для загрузки клиентами сайта. Названия файлов, размер и кнопки загрузки образуют пользовательскую таблицу. Каждая кнопка, ответственная за загрузку файла имеет атрибуты asp-route-{value}, добавляющие в строку запроса два параметра: file - имя файла и mode - способ загрузки. В зависимости от значений этих параметров пользователю веб-страницы отправляется выбранный файл указанным способом. Ниже мы увидим, что веб-страницы шаблона Razor Pages и MVC, за исключением небольших различий, имеют практически одинаковый код.
Таблица списка файлов веб-страницы представления MVC
Унифицированный метод SelectiveResult(...) выдаёт соответствующий результат в зависимости от значений аргументов: file - имени запрошенного файла и переменной mode, выбранного режима скачивания. Если имя файла равно null, пользователю показывается веб-страница со списком доступных файлов. Если переменная file имеет значение, метод "переключается" на отправку файлов выбранным способом, в зависимости от значения mode.
Листинг action-методов контроллера
public IActionResult PrivateFiles(string file, string mode)
{
// Путь к закрытому каталогу.
string pathDirectory =
Path.Combine(MyConstants.PrivateDownloadDirectory);
return SelectiveResult(file, mode, pathDirectory);
}
public IActionResult PublicFiles(string file, string mode)
{
// Путь к открытому каталогу.
string pathDirectory =
Path.Combine(_environment.WebRootPath, MyConstants.PublicDownloadDirectory);
return SelectiveResult(file, mode, pathDirectory);
}
// =======
[NonAction]
private IActionResult SelectiveResult(string file, string mode, string pathDirectory)
{
if (file == null)
{
var model = new Download();
// Заполняем список данными файлов для показа на странице.
var di = new DirectoryInfo(pathDirectory);
// При первом извлечении свойств FileInfo вызывает Refresh метод и
// кэширует сведения о файле.
// При последующих вызовах необходимо вызвать, Refresh чтобы
// получить последнюю копию информации.
model.ListFiles = di.GetFiles().ToList();
return View(model);
}
else
{
var fi = new FileInfo(Path.Combine(pathDirectory, file));
// Обновляем информацию о запрошенном файле.
fi.Refresh();
if (fi.Exists == true)
{
// Конструкция оператора switch начиная с C# 8.0
return mode switch
{
"virtual" => VirtualDownload(fi),
"stream" => StreamDownload(fi),
"bytes" => BytesDownload(fi),
_ => new EmptyResult(),
};
}
else
{
// Обработка ситуации отсутствия файла.
// 1. Запись в файл регистрации ошибок
// 2. Отправка оповещения на e-mail
// и другое
return new EmptyResult();
}
}
}
Низкоуровневые NonAction-методы (такие метод не вызываются по URL, а используются как вспомогательные) различных способов отправки в файле контроллера ASP.NET MVC объединены в отдельную группу. Структура кода контроллера получается такой: сначала идёт высокоуровневая логика работы, в конце файла реализация конкретных методов.
В VirtualDownload(FileInfo fi) главным исполнителем отправки файла клиенту выступает метод File(virtualPath, contentType, fi.Name) возвращающий результат в виде экземпляра класса VirtualFileResult. Данный метод по функциональности одинаков с классическим HTML для скачивания файлов. Указывается виртуальный путь относительно хоста и далее вступают в действие серверная логика отправки контента.
#region Способы отдачи файлов пользователю
[NonAction]
private IActionResult VirtualDownload(FileInfo fi)
{
var contentType = MyUtility.ContentTypes(fi.Extension);
// Виртуальный путь относительно адреса хоста,
// например: полный адрес файла http://host.ru/directory/file
// относительный /directory/file
string virtualPath = MyConstants.PublicDownloadDirectory + "//" + fi.Name;
return File(virtualPath, contentType, fi.Name);
}
...
BytesDownload(FileInfo fi) отличает от предыдущего возможностью отправлять файлы из закрытого каталога. Текущий метод для считывания байтов использует полный, физический путь к файлу. Веб-приложение может получить доступ к любым каталогам к которым имеется разрешение у администратора сайта. Если приложение ASP.NET хостится на виртуальном хостинге, то доступны каталог приложения с подкаталогами. Если хост - это виртуальный VDS или физический сервер DDS, то веб-приложение может получить доступ к любым каталогам.
...
[NonAction]
private ActionResult BytesDownload(FileInfo fi)
{
// Получаем байты файла.
var bytes = System.IO.File.ReadAllBytes(fi.FullName);
var contentType = MyUtility.ContentTypes(fi.Extension);
return new FileContentResult(bytes, contentType)
{
FileDownloadName = fi.Name
};
}
...
Отправка файлов определённая в StreamDownload(FileInfo fi) потребляет минимум ресурсов оперативной памяти. Данным способом можно отправлять огромные файлы без нагрузки на ресурсы сервера. Содержимое файла не загружается в память, выходной поток HTTP получает ссылку на открытый файл, а сервер порциями, без перегрузки, экономно отправляет файл клиенту сайта. Данный метод имеет доступ к любым каталогам в пределах разрешенного администратору сайта. Ниже будут показаны два изображения средств диагностики Visual Studio во время процесса скачивания файлов.
...
[NonAction]
private IActionResult StreamDownload(FileInfo fi)
{
// Открываем поток.
var stream = fi.OpenRead(); //System.IO.File.OpenRead(path);
var contentType = MyUtility.ContentTypes(fi.Extension);
// 1 способ
// return File(stream, content_type, file);
// 2 способ
return new FileStreamResult(stream, contentType)
{
FileDownloadName = fi.Name
};
}
#endregion
Каркас Razor Pages представляет собой облегчённый шаблон MVC, где функцию контроллеров выполняют методы экземпляров классов производных от PageModel. Веб-страница Razor Pages состоит из двух файлов: представления и модели. Модель - это класс C# неразрывно связанный с представлением: HTML страницей построенной на эффективном синтаксисе разметки. Razor Pages предназначены для быстрого построения динамических веб-страниц небольших и средних сайтов.
Методика и программный код для отправки файлов клиенту практически не отличается от кода на каркасе ASP.NET MVC. Построив методы download files для сайта на Razor Pages, можно большую часть кода перенести на MVC проект и наоборот. У данных шаблонов различна только логика работы контроллеров.
Интеллектуальный синтаксис Razor гармонично вливается в разметку гипертекста и создаёт пользовательскую таблицу с параметрами для скачивания файлов. Буквально один символ @ выделяет разметку Razor, не нарушая видимую структуру HTML кода. Таблица аналогичная таблицам представлений MVC каркаса, описанного выше. В листинге показан только код формирования списка файлов. В коде разметки также присутствуют asp-атрибуты для однозначного определения выбранного пользователем файла для скачивания.
Post-запросы, инициированные кнопками загрузки отлавливает метод OnPostAsync(string file, string mode) класса модели связанного со страницей представления. Данный метод играет роль контроллера: в зависимости от входных данных выдаёт соответствующий результат. Пользователь отправляет POST-запрос с двумя параметрами, на основании аргументов file, mode OnPostAsync(...) однозначно находит в списке запрошенный файл и вычисляет способ отправки.
Открытие страницы и нажатие кнопки не могут быть одновременно, поэтому при получении данных файла необходимо проверить его наличие на сервере. В случае отсутствия файла желательно об этом оповестить администратора сайта: в блоке else можно отправить письмо на электронную почту, сделать запись в лог ошибок или что-то другое.
Листинг метода OnPostAsync(...)
public async Task OnPostAsync(string file, string mode)
{
FileInfo fi = ListFiles.FirstOrDefault(p => p.Name == file);
// Получаем более актуальную информацию о файле.
fi.Refresh();
if (fi.Exists == true)
{
return mode switch
{
"memory" => await MemorySend(fi),
"stream" => await StreamSend(fi),
"bytes" => await BytesSend(fi),
_ => new EmptyResult(),
};
}
else
{
// Обработка ситуации отсутствия файла.
}
return new EmptyResult();
}
Ещё один способ отправки, применённый в коде сайта Razor Pages, использует буферизацию файла в оперативной памяти. Отображение содержимого файла в памяти позволяет веб-приложению на ходу изменять или создавать новый контент по запрошенному названию. Такой вариант отправки имеет недостаток: при значительных размерах файла использование памяти может быть критичным. Но отправка буферизацией в памяти отлично подходит для небольших файлов: изображений, документов и т.п.
private async Task MemorySend(FileInfo fi)
{
var buffer = new MemoryStream();
using var stream = fi.OpenRead();
await stream.CopyToAsync(buffer);
buffer.Position = 0;
var contentType = MyUtility.ContentTypes(fi.Extension);
// 1 способ
//return File(buffer, contentType, fi.Name);
// 2 способ
return new FileStreamResult(buffer, contentType)
{
FileDownloadName = fi.Name
};
}
Методы отправки BytesDownload(FileInfo fi) и MemorySend(FileInfo fi) используют буферизацию в памяти сервера (см. выше). Данные способы не желательно применять для больших файлов (высказывание относительно и зависит от конфигурации сервера).
Использование память для временного хранения контента сайта может негативно отразиться на работе сайта. На изображении сеанс диагностики среды Visual Studio при скачивании видеофайла размером 1,5 ГБ способами буферизации в памяти. Во время сеанса скачивание потребление оперативки возрастает до 2 ГБ.
Если доверить отправку контента внутренней логике хоста, то скачивание файлов происходит максимально экономно для сервера. Методы VirtualDownload(FileInfo fi) и StreamDownload(FileInfo fi) работают по такому принципу (см. выше).
На скриншоте сеанс диагностики во время отправки видеофайла 1,5 ГБ без буферизации в памяти. Обеспечение скачивания файлов методами VirtualDownload(FileInfo fi) и StreamDownload(FileInfo fi) происходит максимальная экономия аппаратных ресурсов.
Исходные коды сайтов с примерами скачивания файлов построены на каркасах MVC и Razor Pages. Среда программирования MS Visual Studio 2019, ASP.NET Core 3.1