Описание
OData-сервис предоставляет возможность загружать какие-либо файлы на сервер, скачивать их, а также осуществлять их привязку к свойствам объектов данных. Клиентская часть, в свою очередь, содержит специальную трансформацию для представления файловых свойств на клиенте, и компонент flexberry-file для работы с ними, далее подробнее.
Быстрый старт
Если вам нужно по быстрому наладить работу с файлами через OData-сервис, не изучая всех подробностей того как в сервисе реализована работа с ними, то краткий алгоритм будет таким:
- Проставьте файловому свойству .NET-класса объекта данных тип
ICSSoft.STORMNET.FileType.File
(если хотите хранить файлы в БД) илиICSSoft.STORMNET.UserDataTypes.WebFile
(если хотите хранить файлы в файловой системе) - Проставьте файловому свойству ember-модели тип
DS.attr('file')
- В БД проставьте этому свойству тип
NVARCHAR(MAX)
(если работаете с MS SQL Server) или типTEXT
(если работаете с PostrgreSQL) - Зарегистрируйте файловый контроллер в OData-сервисе, прописав в классе
App_Start\ODataConfig.cs
в методеConfigure
следующую команду:config.MapODataServiceFileRoute("File", "api/File", HttpContext.Current.Server.MapPath("~/Uploads"), container.Resolve<IDataService>());
- В
hbs
-шаблоне ember-формы “скормите” файловое свойство объекта данных свойствуvalue
компонента flexberry-file, модель, с которой ассоциировано файловое свойство свойствуrelatedModel
компонента, и сконфигурируйте компонент указав ему как минимум URL-для для загрузки файлов:-
Либо прямо в
hbs
-шаблоне:Шаблон:
{{flexberry-file relatedModel=model value=model.myFileProperty uploadUrl="<Адрес узла, на котором развернут OData-сервис>/api/File" }}
-
Либо
value
иrelatedModel
в шаблоне, аuploadUrl
в конфигурационном файле приложения (чтобы не указывать его каждый раз, когда используем компонент):Шаблон:
{{flexberry-file relatedModel=model value=model.myFileProperty }}
Конфигурационный файл приложения (
config/environment.js
):{ ... APP: { ... components: { flexberryFile: { uploadUrl: '<Адрес узла, на котором развернут OData-сервис>/api/File' } } ... } ... }
-
- Успех! Работа с файлами через OData-сервис налажена.
Если хотите знать подробности того как в OData-сервисе реализована работа с файлами, читайте продолжение статьи.
Файловые свойства объектов данных в .NET и в СУБД
Для работы с файлами в привязке к свойствам объектов данных, первое что необходимо сделать, это определиться с типом данных, который будет использоваться файловыми свойствами объектов данных. OData-сервис поддерживает два типа данных для таких свойств:
ICSSoft.STORMNET.FileType.File
- тип данных, который будет хранить файлы прямо в объекте данных base64-строкой, а значит и в базе данных, файлы будут храниться base64-строкой в таблице соответствующей типу объекта данных.- Плюсы использования этого типа данных - файл гарантированно является частью объекта данных, и не может существовать отдельно от него, если удаляется объект данных, то гарантированно будет удален и файл, не оставив никаких следов в системе.
- Минусы использования этого типа данных - большие файлы не получится хранить таким образом.
ICSSoft.STORMNET.UserDataTypes
- сборка, в которой находится .NET-тип данных, её нужно указывать в карте типов в свойствах стадии.NVARCHAR(MAX)
- cоответствующий тип данных для карты типов MS SQL Server-а.TEXT
- cоответствующий тип данных для карты типов PostgreSQL Server-а.
ICSSoft.STORMNET.UserDataTypes.WebFile
- тип данных, который будет хранить файлы в файловой системе сервера, в объекте данных только его метаописание (наименование с расширением, размер, и url, по которому файл можно скачать)- Плюсы использования этого типа данных - таким образом можно хранить в привязке к объектам данных любые файлы каких угодно размеров.
- Минусы использования этого типа данных - файлы в этом случае отделены от объектов данных и целостность связи файла с объектом данных не гарантируется на все 100%. Файл теоретически может быть изменен/перемещен/удален из файловой системы без изменений в метаописании, которое хранится в объекте данных. При удалении объекта данных, ORM не всегда может проверить есть ли где-то в файловой системе какой-нибудь файл ассоциированный с ним, если файловой свойство в объекте данных не загружено, или если объект данных является одним из детейлов удаляемого объекта данных/агрегатора, а детейлы опять же не загружены, тогда объект данных удалится, а ассоциированные с ним файлы так и останутся “жить” в файловой системе.
ICSSoft.STORMNET.UserDataTypes
- сборка, в которой находится .NET-тип данных, её нужно указывать в карте типов в свойствах стадии.NVARCHAR(MAX)
- cоответствующий тип данных для карты типов MS SQL Server-а.TEXT
- cоответствующий тип данных для карты типов PostgreSQL Server-а.
Когда с типом данных определились, нужно указать соответствующий тип у свойства объекта данных, к которому будут привязаны какие-либо файлы, и прописать этот тип в карте типов в свойствах стадии (указать там полный путь к типу данных и сборку, в которой он находится), а также прописать нужный тип в карте типов используемой СУБД (см. рисунок ниже).
Файловые свойства объектов данных в ember
В клиентских моделях ember-а файлы, независимо от выбранного .NET-типа данных, всегда представляются сериализованным JSON-объектом, который содержит метаописание файла. Т.к. метаописание приходит сериализованным, т.е. строкой, то с точки зрения ember-а оно совершенно ничем не отличается от любого другого строкового свойства типа string
, однако в аддоне ember-flexberry-data под него все-таки сделана специальная трансформация file
, для того чтобы файловые свойства можно было отличать от остальных (таким образом, например, компонент flexberyy-groupedit
понимает, что для работы со свойством такого типа нужно встраивать компонент flexberry-file
, а не просто flexberry-textbox
).
Клиентская модель объекта данных с файловым свойством (с изображения выше) будет выглядеть следующим образом:
import DS from 'ember-data';
import BaseModel from 'ember-flexberry/models/base';
import Proj from 'ember-flexberry-projections';
var Model = BaseModel.extend({
order: DS.attr('number'),
file: DS.attr('file'),
suggestion: DS.belongsTo('flexberry-ember-demo-suggestion', { inverse: 'files', async: false })
});
Model.defineProjection('SuggestionFileE', 'flexberry-ember-demo-suggestion-file', {
order: Proj.attr('Order'),
file: Proj.attr('File')
});
export default Model;
Провайдеры файловых свойств в OData-сервисе
Со стороны OData-сервиса работа с файлами ведется единообразно, благодаря провайдерам файловых типов, реализующим общий интерфейс NewPlatform.Flexberry.ORM.ODataService.Files.Providers.IDataObjectFileProvider
из сборки NewPlatform.Flexberry.ORM.ODataService
:
namespace NewPlatform.Flexberry.ORM.ODataService.Files.Providers
{
using System;
using System.Collections.Generic;
using System.IO;
using ICSSoft.STORMNET;
/// <summary>
/// Интерфейс для провайдеров файловых свойств объектов данных.
/// </summary>
public interface IDataObjectFileProvider
{
/// <summary>
/// Получает тип файловых свойств объектов данных, обрабатываемых провайдером.
/// </summary>
Type FilePropertyType { get; }
/// <summary>
/// Получает или задает путь к каталогу, в котором должны храниться файлы, загруженные на сервер при помощи провайдера.
/// </summary>
string UploadsDirectoryPath { get; set; }
/// <summary>
/// Получат или задает базовую часть URL-а для ссылок на скачивание / удаление файлов.
/// </summary>
string FileBaseUrl { get; set; }
/// <summary>
/// Осуществляет получение метаданных с описанием файлового свойства объекта данных.
/// </summary>
/// <param name="fileProperty">
/// Файловое свойство объекта данных, для которого требуется получить метаданные файла.
/// </param>
/// <returns>
/// Метаданные с описанием файлового свойства объекта данных.
/// </returns>
FileDescription GetFileDescription(object fileProperty);
/// <summary>
/// Осуществляет получение метаданных с описанием файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
/// <returns>
/// Метаданные с описанием файлового свойства объекта данных.
/// </returns>
FileDescription GetFileDescription(DataObject dataObject, string dataObjectFilePropertyName);
/// <summary>
/// Осуществляет получение списка метаданных с описанием файловых свойств объекта данных, соответствующих типу <see cref="FilePropertyType"/>.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловые свойства.
/// </param>
/// <returns>
/// Список метаданных с описанием файловых свойств объекта данных, соответствующих типу <see cref="FilePropertyType"/>.
/// </returns>
List<FileDescription> GetFileDescriptions(DataObject dataObject);
/// <summary>
/// Осуществляет получение файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
/// <returns>
/// Значение файлового свойства объекта данных.
/// </returns>
object GetFileProperty(DataObject dataObject, string dataObjectFilePropertyName);
/// <summary>
/// Осуществляет получение файлового свойства из файла, расположенного по заданному пути.
/// </summary>
/// <param name="filePath">
/// Путь к файлу.
/// </param>
/// <returns>
/// Значение файлового свойства объекта данных.
/// </returns>
object GetFileProperty(string filePath);
/// <summary>
/// Осуществляет получение файлового свойства объекта данных, по его метаданным.
/// </summary>
/// <remarks>
/// При необходимости будет вычитан объект данных.
/// </remarks>
/// <param name="fileDescription">
/// Метаданные с описанием файлового свойства объекта данных.
/// </param>
/// <returns>
/// Значение файлового свойства объекта данных.
/// </returns>
object GetFileProperty(FileDescription fileDescription);
/// <summary>
/// Осуществляет получение списка файловых свойств объекта данных, соответствующих типу <see cref="FilePropertyType"/>.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловые свойства.
/// </param>
/// <returns>
/// Список файловых свойств объекта данных, соответствующих типу <see cref="FilePropertyType"/>.
/// </returns>
List<object> GetFileProperties(DataObject dataObject);
/// <summary>
/// Осуществляет получение имени файла для файлового свойства объекта данных.
/// </summary>
/// <param name="fileProperty">
/// Файловое свойство объекта данных, для которого требуется получить имя файла.
/// </param>
/// <returns>
/// Имя файла.
/// </returns>
string GetFileName(object fileProperty);
/// <summary>
/// Осуществляет получение имени файла для файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство, для которого требуется получить имя.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
/// <returns>
/// Имя файла.
/// </returns>
string GetFileName(DataObject dataObject, string dataObjectFilePropertyName);
/// <summary>
/// Осуществляет получение MIME-типа для файлового свойства объекта данных.
/// </summary>
/// <param name="fileProperty">
/// Файловое свойство объекта данных, для которого требуется получить MIME-тип.
/// </param>
/// <returns>
/// MIME-тип файла, соответствующего заданному файловому свойству.
/// </returns>
string GetFileMimeType(object fileProperty);
/// <summary>
/// Осуществляет получение MIME-типа для файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство, для которого требуется получить MIME-тип.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
/// <returns>
/// MIME-тип файла, соответствующего заданному файловому свойству.
/// </returns>
string GetFileMimeType(DataObject dataObject, string dataObjectFilePropertyName);
/// <summary>
/// Осуществляет получение размера файла, связанного с объектом данных, в байтах.
/// </summary>
/// <param name="fileProperty">
/// Файловое свойство объекта данных, для которого требуется получить размер файла.
/// </param>
/// <returns>
/// Размер файла в байтах.
/// </returns>
long GetFileSize(object fileProperty);
/// <summary>
/// Осуществляет получение MIME-типа для файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство, для которого требуется получить MIME-тип.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
/// <returns>
/// MIME-тип файла, соответствующего заданному файловому свойству.
/// </returns>
long GetFileSize(DataObject dataObject, string dataObjectFilePropertyName);
/// <summary>
/// Осуществляет получение потока данных для файлового свойства объекта данных.
/// </summary>
/// <param name="fileProperty">
/// Значение файлового свойства объекта данных, для которого требуется получить поток данных.
/// </param>
/// <returns>
/// Поток данных.
/// </returns>
Stream GetFileStream(object fileProperty);
/// <summary>
/// Осуществляет получение потока данных для файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство, для которого требуется получить поток данных.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
/// <returns>
/// Поток данных.
/// </returns>
Stream GetFileStream(DataObject dataObject, string dataObjectFilePropertyName);
/// <summary>
/// Осуществляет получение потока данных для файлового свойства объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет вычитан объект данных.
/// </remarks>
/// <param name="fileDescription">Метаданные с описанием файлового свойства объекта данных, для которого требуется получить поток данных.</param>
/// <returns>Поток данных.</returns>
Stream GetFileStream(FileDescription fileDescription);
/// <summary>
/// Осуществляет удаление из файловой системы файла, соответствующего файловому свойству объекта данных.
/// </summary>
/// <param name="fileDescription">
/// Метаданные удаляемого файла.
/// </param>
void RemoveFile(FileDescription fileDescription);
/// <summary>
/// Осуществляет удаление из файловой системы файла, соответствующего файловому свойству объекта данных.
/// </summary>
/// <param name="fileProperty">
/// Значение файлового свойства объекта данных, для которого требуется выполнить удаление.
/// </param>
void RemoveFile(object fileProperty);
/// <summary>
/// Осуществляет удаление из файловой системы файла, соответствующего файловому свойству объекта данных.
/// </summary>
/// <remarks>
/// При необходимости будет произведена дочитка объекта данных.
/// </remarks>
/// <param name="dataObject">
/// Объект данных, содержащий файловое свойство.
/// </param>
/// <param name="dataObjectFilePropertyName">
/// Имя файлового свойства в объекте данных.
/// </param>
void RemoveFile(DataObject dataObject, string dataObjectFilePropertyName);
}
}
Этот интерфейс реализуют два провайдера:
NewPlatform.Flexberry.ORM.ODataService.Files.Providers.DataObjectFileProvider
из сборкиNewPlatform.Flexberry.ORM.ODataService
NewPlatform.Flexberry.ORM.ODataService.Files.Providers.DataObjectWebFileProvider
из сборкиNewPlatform.Flexberry.ORM.ODataService
При желании можно реализовать собственный произвольный файловый тип данных, и реализовать для него подобный провайдер.
Каждый из этих провайдеров, по сути, просто stateless-набор утилит для работы с соответствующим файловым типом данных, поэтому OData-сервис инстанцирует их только по одному разу и регистрирует в специальном файловом контроллере.
Файловый контроллер в OData-сервисе
Работа с файлами в OData-сервисе обеспечивается специальным файловым контроллером NewPlatform.Flexberry.ORM.ODataService.Controllers.FileController
из сборки NewPlatform.Flexberry.ORM.ODataService
.
Через него осуществляется доступ к упомянутым выше файловым провайдерам, и с их помощью он обеспечивает загрузку файлов на сервер, и их скачивание.
Для того чтобы через OData-сервис была возможность работать с файловым контроллером, его необходимо зарегистрировать в сервисе и определить маршрут, по которому он будет доступен, для этого в HttpConfiguration
предусмотрен метод расширения:
/// <summary>
/// Осуществляет регистрацию маршрута для загрузки/скачивания файлов.
/// </summary>
/// <param name="httpConfiguration">Используемая конфигурация.</param>
/// <param name="routeName">Имя регистрируемого маршрута.</param>
/// <param name="routeTemplate">Шаблон регистрируемого маршрута.</param>
/// <param name="uploadsDirectoryPath">Пути к каталогу, который предназначен для хранения файлов загружаемых на сервер.</param>
/// <param name="dataService">Сервис данных для операций с БД.</param>
/// <returns>Зарегистрированный маршрут.</returns>
public static IHttpRoute MapODataServiceFileRoute(
this HttpConfiguration httpConfiguration,
string routeName,
string routeTemplate,
string uploadsDirectoryPath,
IDataService dataService)
Его вызов обычно производится в файле конфигурации OData-сервиса после регистрации основного маршрута для доступа к объектам данным, и выглядит следующим образом:
config.MapODataServiceFileRoute("File", "api/File", HttpContext.Current.Server.MapPath("~/Uploads"), container.Resolve<IDataService>());
Этот вызов сопоставит файловый контроллер адресу <Адрес узла, на котором развернут OData-сервис>/api/File
, и зарегистрирует в контроллере файловые провайдеры NewPlatform.Flexberry.ORM.ODataService.Files.Providers.DataObjectFileProvider
и NewPlatform.Flexberry.ORM.ODataService.Files.Providers.DataObjectWebFileProvider
.
Для регистрации файловых провайдеров контроллер содержит статический метод:
/// <summary>
/// Осуществляет регистрацию провайдера файловых свойств для объекта данных.
/// </summary>
/// <param name="dataObjectFileProvider">
/// Провайдер файловых свойств для объекта данных.
/// </param>
public static void RegisterDataObjectFileProvider(IDataObjectFileProvider dataObjectFileProvider)
При необходимости его можно вызвать вручную, и зарегистрировать собственные файловые провайдеры.
Или при регистрации контроллера, сразу вручную указать желаемый набор файловых провайдеров, только для регистрации потребуется обращаться к той перегрузке метода MapODataServiceFileRoute
, которая это позволяет:
/// <summary>
/// Осуществляет регистрацию маршрута для загрузки/скачивания файлов.
/// </summary>
/// <param name="httpConfiguration">Используемая конфигурация.</param>
/// <param name="routeName">Имя регистрируемого маршрута.</param>
/// <param name="routeTemplate">Шаблон регистрируемого маршрута.</param>
/// <param name="uploadsDirectoryPath">Пути к каталогу, который предназначен для хранения файлов загружаемых на сервер.</param>
/// <param name="dataObjectFileProviders">
/// Провайдеры файловых свойств объектов данных, которые будут использоваться для связывания файлов с объектами данных.
/// </param>
/// <param name="dataService">Сервис данных для операций с БД.</param>
/// <returns>Зарегистрированный маршрут.</returns>
public static IHttpRoute MapODataServiceFileRoute(
this HttpConfiguration httpConfiguration,
string routeName,
string routeTemplate,
string uploadsDirectoryPath,
IEnumerable<IDataObjectFileProvider> dataObjectFileProviders,
IDataService dataService)
Помимо метода RegisterDataObjectFileProvider
файловый контроллер содержит еще несколько вспомогательных статических методов, которые используются в основном для тестирования, и скорей всего, в ручную их использовать не потребуются.
Загрузка файла на сервер
Загрузка файлов на сервер осуществляется обработчиком POST-запросов файлового контроллера:
/// <summary>
/// Осуществляет загрузку файлов на сервер.
/// </summary>
/// <remarks>
/// Файлы загружаются в файловую систему, в каталог <see cref="UploadsDirectoryPath"/>/{UploadedFileKey},
/// где UploadedFileGuid - <see cref="Guid"/>, идентифицирующий загруженный файл.
/// </remarks>
/// <returns>
/// Описание загруженного файла.
/// </returns>
[HttpPost]
public Task<FileDescription> Post()
Обработчик действует следующим образом:
- Асинхронно вычитывает загружаемый файл из тела POST-запроса
- В случае, если файл успешно вычитан из тела POST-запроса, “идет” в тот каталог, который при регистрации контроллера был указан в качестве каталога для загружаемых файлов (для наглядности пусть это будет “~/Uploads”), создает там подкаталог, который именует только что сгенерированным GUID-ом (например так “~/Uploads/0d57629c-7d6e-4847-97cb-9e2fc25083fe”) и сохраняет загруженный файл в этом каталоге (если загруженный файл называется image.png, то после того как обработчик закончит работу картина будет такой “~/Uploads/0d57629c-7d6e-4847-97cb-9e2fc25083fe/image.png”). GUID используется из-за того что имена различных файлов в принципе могут и совпадать, и если их складывать прямо в каталог “~/Uploads”, то потенциально они будут друг-друга перетирать.
- Затем обработчик возвращает метаописание загруженного файла, по которому клиент сможет его скачать, либо связать с файловым свойством какого-нибудь объекта данных, для только что загруженного файла это метаописание будет выглядеть следующим образом:
{
// URL для скачивания файла.
"fileUrl":"https://flexberry-ember-dummy.azurewebsites.net/api/File?fileUploadKey=0d57629c-7d6e-4847-97cb-9e2fc25083fe&fileName=image.png",
// URL для скачивания preview (если файл это изображение).
"previewUrl":"https://flexberry-ember-dummy.azurewebsites.net/api/File?fileUploadKey=0d57629c-7d6e-4847-97cb-9e2fc25083fe&fileName=image.png&getPreview=true",
// Наименование файла.
"fileName":"image.png",
// Размер файла в байтах.
"fileSize": 12345,
// MIME-тип файла.
"fileMimeType": "image/png"
}
Также в обработчике предусмотрена возможность, при загрузке очередного файла, удалить ранее загруженный файл, который не пригодился (например пользователь выбрал один файл, загрузил его на сервер, но еще не связывал с объектом данных, а потом передумал и решил загрузить какой-то другой файл) в этом случае можно отправить в теле запроса, в свойстве formData.previousFileDescription
ранее загруженного файла, и он будет удален из файловой системы сервера, после успешной загрузки нового файла (ранее упомянутый компонент flexberry-file так и делает, указывает formData.previousFileDescription
при необходимости).
Привязка файла к свойству объекта данных
Файл просто загруженный в файловую систему сервера, сам по себе не представляет большой ценности, нужно еще связать его с файловым свойством объекта данных. Этим занимается DataObjectController
, обеспечивающий всю работу с объектами данных в OData-сервисе, при обработке создания/обновления объектов данных.
Как это происходит разберем на примере сохранения объекта данных типа Suggestion
с детейлами типа SuggestionFile
, в которых имеется файловое свойство File
типа ICSSoft.STORMNET.FileType.File
(см. диаграмму классов в начале статьи).
Пусть у агрегатора Suggestion
имеется один детейл SuggestionFile
, и у этого детейла в качестве файла выбран описанный в предыдущем разделе файл “image.png”, уже загруженный на сервер по пути “~/Uploads/0d57629c-7d6e-4847-97cb-9e2fc25083fe/image.png” и имеющий метаописание:
{
"fileUploadKey": "0d57629c-7d6e-4847-97cb-9e2fc25083fe", // GUID, который использован в качестве наименования для каталога, в котором хранится файл.
"fileName": "image.png", // Наименование файла.
"fileSize": 12345, // Размер файла в байтах.
"fileMimeType": "image/png" // MIME-тип, соответствующий файлу.
}
Этот агрегатор отправляется на сохранение через OData-сервис, и попадает в обработчик POST-запрсов (в случае сохранения нового объекта) или в обработчик PATCH-запросов (в случае обновления существующего объекта), в DataObjectController
-е.
Объект данных в этот обработчик приходит в виде JSON-объекта, у которого помимо прочих свойств, в свойстве file
содержится приведенное выше метаописание файла “image.png”.
Чтобы осуществить сохранение объекта через ORM, DataObjectController
создает объект данных (и в случае, если обновляется уже существующий объект проставляет ему первичный ключ через вызов метода SetExistObjectPrimaryKey
).
Затем контроллер начинает перебирать свойства полученного JSON-объекета, сопоставляет их свойствам объекта данных, извлекает из объекта данных информацию о типе этих свойств, и наконец когда известен тип, осуществляет приведение типов, и означивает свойства объекта данных значениями полученными из JSON-объекта.
Когда этот разбор свойств доходит до свойства file
, DataObjectController
проверяет зарегистрирован ли в файловом контроллере провайдер для типа ICSSoft.STORMNET.FileType.File
, который имеет это свойство, и если такой провайдер зарегистрирован, DataObjectController
делает вывод что это файловое свойство, извлекает из файлового контроллера ссылку на нужный провайдер, и обращается к нему, чтобы тот на основе имеющегося метаописания сформировал значение нужного типа (в данном случае ICSSoft.STORMNET.FileType.File
), провайдер по метаописанию восстанавливает путь, по которому файл расположен в файловой системе, преобразует его в base64-строку, создает объект типа ICSSoft.STORMNET.FileType.File
, и кладет base64-строку в него, затем полученный объект типа ICSSoft.STORMNET.FileType.File
проставляет в свойство file
объекта данных, а файл находящийся в файловой системе помечает на удаление, и в случае успешного сохранения объекта данных, он будет удален из файловой системы.
Часть логики DataObjectController
-а отвечающая за работу с файловыми свойствами при создании/изменении объектов выглядит следующим образом:
// Если тип свойства относится к одному из зарегистрированных провайдеров файловых свойств,
// значит свойство файловое, и его нужно обработать особым образом.
if (FileController.HasDataObjectFileProvider(dataObjectPropertyType))
{
IDataObjectFileProvider dataObjectFileProvider = FileController.GetDataObjectFileProvider(dataObjectPropertyType);
// Обработка файловых свойств объектов данных.
string serializedFileDescription = value as string;
if (serializedFileDescription == null)
{
// Файловое свойство было сброшено на клиенте.
// Ассоциированный файл должен быть удален, после успешного сохранения изменений.
// Для этого запоминаем метаданные ассоциированного файла, до того как свойство будет сброшено
// (для получения метаданных свойство будет дочитано в объект данных).
// Файловое свойство типа File хранит данные ассоциированного файла прямо в БД,
// соответственно из файловой системы просто нечего удалять,
// поэтому обходим его стороной, чтобы избежать лишных вычиток файлов из БД.
if (dataObjectPropertyType != typeof(File))
{
_removingFileDescriptions.Add(dataObjectFileProvider.GetFileDescription(obj, dataObjectPropName));
}
// Сбрасываем файловое свойство в изменяемом объекте данных.
Information.SetPropValueByName(obj, dataObjectPropName, null);
}
else
{
// Файловое свойство было изменено, но не сброшено.
// Если в метаданных файла присутствует FileUploadKey значит файл был загружен на сервер,
// но еще не был ассоциирован с объектом данных, и это нужно сделать.
FileDescription fileDescription = FileDescription.FromJson(serializedFileDescription);
if (!(string.IsNullOrEmpty(fileDescription.FileUploadKey) || string.IsNullOrEmpty(fileDescription.FileName)))
{
Information.SetPropValueByName(obj, dataObjectPropName, dataObjectFileProvider.GetFileProperty(fileDescription));
// Файловое свойство типа File хранит данные ассоциированного файла прямо в БД,
// поэтому после успешного сохранения объекта данных, оссоциированный с ним файл должен быть удален из файловой системы.
// Для этого запоминаем описание загруженного файла.
if (dataObjectPropertyType == typeof(File))
{
_removingFileDescriptions.Add(fileDescription);
}
}
}
Как видно из приведенной выше части кода DataObjectController
-а, для удаления файла достаточно проставить null
в качестве значения файлового свойства объекта данных (тогда, в случае успешного сохранения изменений, файловое свойство будет сброшено, а ассоциированный файл будет удален).
Если бы свойство file
имело тип ICSSoft.STORMNET.FileType.WebFile
смысл был бы тот же самый, только файл бы не преобразовывался в base64-строку и не удалялся бы потом из файловой системы, а так бы и остался на “постоянном месте жительства” по пути “~/Uploads/0d57629c-7d6e-4847-97cb-9e2fc25083fe/image.png”, а в файловом свойстве объекта данных (и соответственно в БД) сохранилось бы метаописание файла, содержащее URL-адрес файлового контроллера и fileUploadKey (<Адрес узла, на котором развернут OData-сервис>/api/File?fileUploadKey=0d57629c-7d6e-4847-97cb-9e2fc25083fe
).
После успешного сохранения объекта данных, DataObjectController
возвращает его на клиент в виде JSON-объекта, и после того как осуществлено связывание файла с файловым свойством в объекте данных, метаописание файла, которое вернется на клиент в свойстве file
несколько изменится, в нем уже не будет ключа загрузки fileUploadKey
, вместо него будут свойства указывающие на тип объекта данных, его первичный ключ, и имя свойства, в котором хранится файл:
{
// URL для скачивания файла.
"fileUrl":"<Адрес узла, на котором развернут OData-сервис>/api/File?entityTypeName=MyNameSpace.SuggestionFile, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx&entityPrimaryKey=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&entityPropertyName=File&fileName=image.png",
// URL для скачивания preview (если файл это изображение).
"previewUrl":"<Адрес узла, на котором развернут OData-сервис>/api/File?entityTypeName=MyNameSpace.SuggestionFile, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx&entityPrimaryKey=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&entityPropertyName=File&fileName=image.png&getPreview=true",
// Наименование файла.
"fileName":"image.png",
// Размер файла в байтах.
"fileSize": 12345,
// MIME-тип файла.
"fileMimeType": "image/png"
}
Скачивание файла
Скачивание файлов с сервера осуществляется обработчиком GET-запросов файлового контроллера:
/// <summary>
/// Осуществляет скачивание файлов с сервера.
/// В зависимости от значения флага <paramref name="getPreview"/> возвращается либо содержимое файла, либо файл в виде приложения.
/// </summary>
/// <param name="fileDescription">Описание запрашиваемого файла.</param>
/// <param name="getPreview">Параметр, определяющий, требуется ли файл просто для предпросмотра (если значение <c>true</c>), либо требуется его скачать и сохранить.</param>
/// <returns>Описание загруженного файла.</returns>
[HttpGet]
public HttpResponseMessage Get([FromUri] FileDescription fileDescription = null, [FromUri] bool getPreview = false)
В качестве основного параметра обработчик принимает метаописание скачиваемого файла (fileDescription
),
а в качестве опционального параметра принимает флаг определяющий, требуется ли файл просто для предпросмотра, или же его требуется скачать в виде вложения с последующим сохранением на клиентском устройстве (getPreview
), по умолчанию флаг имеет значение false
, а значит по умолчанию запрашиваемые файлы будет скачиваться в виде вложения, но если этот флаг имеет значение true
, то файл будет возвращаться в виде base64-строки представленной через Data URL, в случае изображений такие данные можно подставлять в качестве атрибута src
тега img
(<img src=...></img>
), в ранее упомянутом компоненте flexberry-file так и реализован предпросмотр для файлов изображений.
Получив метаописание файла, обработчик смотрит на состав свойств в нем, и в зависимости от состава действует немного по разному:
- При наличии свойств
entityTypeName
,entityPrimaryKey
,entityPropertyName
в метаописании, обработчик понимает что файл уже был связан с объектом данных, вычитывает его, извлекает из него файловое свойство, и с помощью соответствующего свойству файлового провайдера извлекает поток данных файла (FileStream
). - При наличии свойства
fileUploadKey
в метаописании, обработчик понимает что файл еще не был был связан с объектом данных, а значит хранится в файловой системе, в каталоге именуемом так же какfileUploadKey
, значит не нужно предварительно вычитывать никакой объект данных, а можно сразу получить поток данных файла (FileStream
). А поскольку типICSSoft.STORMNET.FileType.WebFile
как раз хранит файлы в файловой системе по ключуfileUploadKey
, обработчик используетNewPlatform.Flexberry.ORM.ODataService.Files.Providers.DataObjectWebFileProvider
для этих целей.
Часть логики обработчика отвечающая за получения потока данных файла, на основе метаописания выглядит следующим образом:
/// <summary>
/// Осуществляет получение потока данных для запрашиваемого файла (а также имя файла, MIME-тип, и размер в байтах).
/// </summary>
/// <param name="fileDescription">Описание файла.</param>
/// <param name="fileName">Имя файла.</param>
/// <param name="fileMimeType">MIME-тип файла.</param>
/// <param name="fileSize">Размер файла в байтах.</param>
/// <returns>Поток данных для запрашиваемого файла.</returns>
private Stream GetFileStream(
FileDescription fileDescription,
out string fileName,
out string fileMimeType,
out long fileSize)
{
if (fileDescription == null)
{
throw new ArgumentNullException(nameof(fileDescription));
}
Stream fileStream = null;
Type dataObjectType = null;
Type filePropertyType = null;
if (!string.IsNullOrEmpty(fileDescription.EntityPrimaryKey))
{
// Запрашиваемый файл уже был связан с объектом данных, и нужно вычитать из него файловое свойство.
dataObjectType = Type.GetType(fileDescription.EntityTypeName, true);
filePropertyType = Information.GetPropertyType(dataObjectType, fileDescription.EntityPropertyName);
}
else
{
// Запрашиваемый файл еще не был связан с объектом данных, а значит находится в каталоге загрузок,
// в подкаталоге с именем fileDescription.FileUplodKey.
// Получение файлов по ключу загрузки реализовано в DataObjectWebFileProvider.
filePropertyType = typeof(WebFile);
}
if (!HasDataObjectFileProvider(filePropertyType))
{
throw new Exception(string.Format("DataObjectFileProvider for \"{0}\" property type not found.", filePropertyType.AssemblyQualifiedName));
}
IDataObjectFileProvider dataObjectFileProvider = GetDataObjectFileProvider(filePropertyType);
object fileProperty = dataObjectFileProvider.GetFileProperty(fileDescription);
fileStream = dataObjectFileProvider.GetFileStream(fileProperty);
fileName = dataObjectFileProvider.GetFileName(fileProperty);
fileMimeType = dataObjectFileProvider.GetFileMimeType(fileProperty);
fileSize = fileStream.Length;
return fileStream;
}