/// <summary> /// Осуществляет загрузку файла на сервер в подкаталог с заданным именем внутри каталога <see cref="UploadsDirectoryPath"/>. /// </summary> /// <param name="fileUploadKey"> /// Ключ загрузки файла (используется как имя подкаталога). /// </param> /// <returns> /// Асинхронная операция, которая в случае успешного выполнения вернет метаданные загруженного файла (<see cref="FileDescription"/>). /// </returns> private Task <FileDescription> UploadFile(string fileUploadKey) { // Создаём каталог для загружаемого файла, и провайдер для его вычитки и сохранения. string fileUploadPath = CreateFileUploadDirectory(fileUploadKey); FileUploadStreamProvider fileUploadProvider = new FileUploadStreamProvider(fileUploadPath); // Вычитываем загружаемый файл из запроса, и сохраняем в созданном каталоге. Task <FileDescription> uploadTask = Request.Content.ReadAsMultipartAsync(fileUploadProvider).ContinueWith((readRequestTask) => { // Обрабатываем сбой/отмену при обработке файла. if (readRequestTask.IsFaulted || readRequestTask.IsCanceled) { Exception exception = readRequestTask.Exception ?? new Exception("Read MIME multipart content task was faulted or cancelled."); throw exception; } // Возвращаем описание загруженного файла. return(new FileDescription(BaseUrl, fileUploadProvider.FileData.First().LocalFileName)); }).ContinueWith((fileDescriptionTask) => { // Если загрузка файла прошла успешно, нужно удалить ранее загруженнй файл, // который еще не ассоциированн с объектом данных. if (!(fileDescriptionTask.IsFaulted || fileDescriptionTask.IsCanceled)) { FileDescription previousFileDescription = FileDescription.FromJson(fileUploadProvider.FormData.Get("previousFileDescription")); if (!string.IsNullOrEmpty(previousFileDescription?.FileUploadKey)) { RemoveFileUploadDirectory(previousFileDescription.FileUploadKey); } } return(fileDescriptionTask.Result); }); return(uploadTask); }
/// <summary> /// Осуществляет проверку того, что файлы корректно загружаются на сервер. /// </summary> //[Test] public void TestUploadSuccess() { string fileBaseUrl = "http://localhost/api/File"; // TODO: По какой-то причине этот тест виснет при асинхронном запросе к серверу если создаются временные БД. // Скорее всего проблема в контексте синхронизации ASP.NET и асинхронном методе контроллера. //using (DataServiceWrapper dataServiceWrapper = new DataServiceWrapper()) //using (ShimsContext.Create()) { // Подменяем HttpContext. //ShimHttpContext.CurrentGet = () => { //return new HttpContext(new HttpRequest(null, "http://localhost", null), new HttpResponse(null)); }; //foreach (IDataService dataService in dataServiceWrapper.AllowedDataServices) { foreach (string srcFilePath in new List <string> { _srcImageFilePath, _srcTextFilePath }) { FileInfo srcFileInfo = new FileInfo(srcFilePath); // Ключ загрузки файла (этим ключем будет именоваться подкаталог в каталоге UploadsDirectoryPath). Guid fileUploadKeyGuid = Guid.NewGuid(); string fileUploadKey = fileUploadKeyGuid.ToString("D"); string fileName = srcFileInfo.Name; string fileMimeType = MimeMapping.GetMimeMapping(fileName); long fileSize = srcFileInfo.Length; string uploadedFilePath = $"{_uploadsDirectoryPath}\\{fileUploadKey}\\{srcFileInfo.Name}"; using (var config = new HttpConfiguration()) { config.EnableCors(new EnableCorsAttribute("*", "*", "*")); config.MapODataServiceFileRoute("File", "api/File", _uploadsDirectoryPath, new MSSQLDataService()); using (var server = new HttpServer(config)) { using (var client = new HttpClient(server, false)) { using (var uploadingImageFileContent = new StreamContent(srcFileInfo.Open(FileMode.Open, FileAccess.Read))) { uploadingImageFileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { FileName = srcFileInfo.Name }; uploadingImageFileContent.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(srcFileInfo.Name)); var formDataContent = new MultipartFormDataContent { uploadingImageFileContent }; // Подменяем NewGuid, чтобы имя подкаталога, в который будет загружен файл, было заранее известно. //ShimGuid.NewGuid = () => fileUploadKeyGuid; using (HttpResponseMessage response = client.PostAsync(fileBaseUrl, formDataContent).Result) { // Проверяем, что запрос завершился успешно. //Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); // Проверяем, что загруженный на сервер файл существует, и совпадает с тем файлом, из которого производилась загрузка. //Assert.AreEqual(true, System.IO.File.Exists(uploadedFilePath)); //Assert.AreEqual(true, FilesComparer.FilesAreEqual(srcFilePath, uploadedFilePath)); // Получаем описание загруженного файла, из ответа, полученного от сервера. FileDescription receivedFileDescription = FileDescription.FromJson(response.Content.ReadAsStringAsync().Result); Uri receivedFileUri = new Uri(receivedFileDescription.FileUrl); Uri receivedPreviewUri = new Uri(receivedFileDescription.PreviewUrl); string receivedFileBaseUrl = receivedFileUri.GetLeftPart(UriPartial.Path); string receivedPreviewBaseUrl = receivedPreviewUri.GetLeftPart(UriPartial.Path); NameValueCollection receivedFileQueryParameters = HttpUtility.ParseQueryString(receivedFileUri.Query); NameValueCollection receivedPreviewQueryParameters = HttpUtility.ParseQueryString(receivedPreviewUri.Query); // Проверяем, что полученное описание загруженного файла совпадает с ожидаемым. /* * Assert.AreEqual(fileUploadKey, receivedFileDescription.FileUploadKey); * Assert.AreEqual(fileName, receivedFileDescription.FileName); * Assert.AreEqual(fileSize, receivedFileDescription.FileSize); * Assert.AreEqual(fileMimeType, receivedFileDescription.FileMimeType); * Assert.AreEqual(null, receivedFileDescription.EntityTypeName); * Assert.AreEqual(null, receivedFileDescription.EntityPropertyName); * Assert.AreEqual(null, receivedFileDescription.EntityPrimaryKey); * * Assert.AreEqual(fileBaseUrl, receivedFileBaseUrl); * Assert.AreEqual(fileBaseUrl, receivedPreviewBaseUrl); * * Assert.AreEqual(2, receivedFileQueryParameters.Count); * Assert.AreEqual(fileUploadKey, receivedFileQueryParameters[nameof(FileDescription.FileUploadKey)]); * Assert.AreEqual(fileName, receivedFileQueryParameters[nameof(FileDescription.FileName)]); * * Assert.AreEqual(3, receivedPreviewQueryParameters.Count); * Assert.AreEqual(fileUploadKey, receivedPreviewQueryParameters[nameof(FileDescription.FileUploadKey)]); * Assert.AreEqual(fileName, receivedPreviewQueryParameters[nameof(FileDescription.FileName)]); * Assert.AreEqual(true, bool.Parse(receivedPreviewQueryParameters["GetPreview"])); */ } } } } } } } } }
/// <summary> /// Осуществляет проверку того, что <see cref="FileDescription"/> с заданными свойствами, описывающими объект данных, /// корректно десериализуется из JSON-строки. /// </summary> //[Test] public void TestDeserializationFromJsonByEntityProperties() { string fileBaseUrl = "http://localhost/api/File"; // Имя, размер и тип файла. string fileName = "readme.txt"; long fileSize = 1024; string fileMimeType = MimeMapping.GetMimeMapping(fileName); // Свойства достаточные для описания файла, который привязан к объекту данных // (тип и первичный ключ объекта данных, а так же имя файлового свойства в объекте данных). КлассСМножествомТипов entity = new КлассСМножествомТипов(); string entityTypeName = entity.GetType().AssemblyQualifiedName; string entityPropertyName = nameof(entity.PropertyStormnetFile); string entityPrimaryKey = entity.__PrimaryKey.ToString(); // Описание файла с избыточным набором свойств. FileDescription fileDescription = new FileDescription(fileBaseUrl) { FileName = fileName, FileSize = fileSize, FileMimeType = fileMimeType, EntityTypeName = entityTypeName, EntityPropertyName = entityPropertyName, EntityPrimaryKey = entityPrimaryKey }; string serializedFileDescription = fileDescription.ToJson(); // Получаем десериализованные описания файла, полученные различными способами. List <FileDescription> deserializedFileDescriptions = new List <FileDescription> { FileDescription.FromJson(serializedFileDescription) }; using (HttpConfiguration config = new HttpConfiguration()) { config.EnableCors(new EnableCorsAttribute("*", "*", "*")); config.Routes.MapHttpRoute("File", "api/File", new { controller = "FileDescriptionTest" }); using (HttpServer server = new HttpServer(config)) { using (HttpClient client = new HttpClient(server, false)) { // Получаем десериализованное описание файла из тела POST-запроса. using (HttpResponseMessage response = client.PostAsJsonStringAsync("http://localhost/api/File", serializedFileDescription).Result) { // Убедимся, что запрос завершился успешно. //Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); deserializedFileDescriptions.Add(FileDescriptionTestController.FileDescriptionPost); } // Получаем десериализованное описание файла из URL PUT-запроса. string putUrl = string.Format( "http://localhost/api/File?{0}={1}&{2}={3}&{4}={5}&{6}={7}&{8}={9}&{10}={11}", nameof(FileDescription.FileName), fileDescription.FileName, nameof(FileDescription.FileSize), fileDescription.FileSize, nameof(FileDescription.FileMimeType), fileDescription.FileMimeType, nameof(FileDescription.EntityTypeName), fileDescription.EntityTypeName, nameof(FileDescription.EntityPropertyName), fileDescription.EntityPropertyName, nameof(FileDescription.EntityPrimaryKey), fileDescription.EntityPrimaryKey); using (HttpResponseMessage response = client.SendAsync(new HttpRequestMessage(HttpMethod.Put, putUrl)).Result) { // Убедимся, что запрос завершился успешно. //Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); deserializedFileDescriptions.Add(FileDescriptionTestController.FileDescriptionPut); } } } } // Проверяем, что результаты десериализации, полученные разными способами - одинаковы и корректны. foreach (FileDescription deserializedFileDescription in deserializedFileDescriptions) { /* * Assert.AreEqual(fileName, deserializedFileDescription.FileName); * Assert.AreEqual(fileSize, deserializedFileDescription.FileSize); * Assert.AreEqual(fileMimeType, deserializedFileDescription.FileMimeType); * Assert.AreEqual(entityTypeName, deserializedFileDescription.EntityTypeName); * Assert.AreEqual(entityPropertyName, deserializedFileDescription.EntityPropertyName); * Assert.AreEqual(entityPrimaryKey, deserializedFileDescription.EntityPrimaryKey); * Assert.AreEqual(fileDescription.FileUrl, deserializedFileDescription.FileUrl); * Assert.AreEqual(fileDescription.PreviewUrl, deserializedFileDescription.PreviewUrl); * Assert.AreEqual(null, deserializedFileDescription.FileUploadKey); */ } }
/// <summary> /// Построение объекта данных по сущности OData. /// </summary> /// <param name="edmEntity"> Сущность OData. </param> /// <param name="key"> Значение ключевого поля сущности. </param> /// <param name="dObjs"> Список объектов для обновления. </param> /// <param name="endObject"> Признак, что объект добавляется в конец списка обновления. </param> /// <returns> Объект данных. </returns> private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object key, List <DataObject> dObjs, bool endObject = false) { if (edmEntity == null) { return(null); } IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType; Type objType = _model.GetDataObjectType(_model.GetEdmEntitySet(entityType).Name); // Значение свойства. object value; // Получим значение ключа. var keyProperty = entityType.Properties().FirstOrDefault(prop => prop.Name == _model.KeyPropertyName); if (key != null) { value = key; } else { edmEntity.TryGetPropertyValue(keyProperty.Name, out value); } // Загрузим объект из хранилища, если он там есть (используем представление по умолчанию), или создадим, если нет, но только для POST. // Тем самым гарантируем загруженность свойств при необходимости обновления и установку нужного статуса. DataObject obj = ReturnDataObject(objType, value); // Добавляем объект в список для обновления, если там ещё нет объекта с таким ключом. var objInList = dObjs.FirstOrDefault(o => o.__PrimaryKey.ToString() == obj.__PrimaryKey.ToString()); if (objInList == null) { if (!endObject) { // Добавляем объект в начало списка. dObjs.Insert(0, obj); } else { // Добавляем в конец списка. dObjs.Add(obj); } } // Все свойства объекта данных означим из пришедшей сущности, если они были там установлены(изменены). foreach (var prop in entityType.Properties()) { string dataObjectPropName = _model.GetDataObjectProperty(entityType.FullTypeName(), prop.Name).Name; if (edmEntity.GetChangedPropertyNames().Contains(prop.Name)) { // Обработка мастеров и детейлов. if (prop is EdmNavigationProperty) { EdmNavigationProperty navProp = (EdmNavigationProperty)prop; edmEntity.TryGetPropertyValue(prop.Name, out value); EdmMultiplicity edmMultiplicity = navProp.TargetMultiplicity(); // var aggregator = Information.GetAgregatePropertyName(objType); // Обработка мастеров. if (edmMultiplicity == EdmMultiplicity.One || edmMultiplicity == EdmMultiplicity.ZeroOrOne) { if (value != null && value is EdmEntityObject) { EdmEntityObject edmMaster = (EdmEntityObject)value; DataObject master = GetDataObjectByEdmEntity(edmMaster, null, dObjs); Information.SetPropValueByName(obj, dataObjectPropName, master); } else { Information.SetPropValueByName(obj, dataObjectPropName, null); } } // Обработка детейлов. if (edmMultiplicity == EdmMultiplicity.Many) { Type detType = Information.GetPropertyType(objType, dataObjectPropName); DetailArray detarr = (DetailArray)Information.GetPropValueByName(obj, dataObjectPropName); if (value != null && value is EdmEntityObjectCollection) { EdmEntityObjectCollection coll = (EdmEntityObjectCollection)value; if (coll != null && coll.Count > 0) { foreach (var edmEnt in coll) { DataObject det = GetDataObjectByEdmEntity( (EdmEntityObject)edmEnt, null, dObjs, true); if (det.__PrimaryKey == null) { detarr.AddObject(det); } else { detarr.SetByKey(det.__PrimaryKey, det); } } } } else { detarr.Clear(); } } } else { // Обработка собственных свойств объекта (неключевых, т.к. ключ устанавливаем при начальной инициализации объекта obj). if (prop.Name != keyProperty.Name) { Type dataObjectPropertyType = Information.GetPropertyType(objType, dataObjectPropName); edmEntity.TryGetPropertyValue(prop.Name, out value); // Если тип свойства относится к одному из зарегистрированных провайдеров файловых свойств, // значит свойство файловое, и его нужно обработать особым образом. 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); } } } } else { // Преобразование типов для примитивных свойств. if (value is DateTimeOffset) { value = ((DateTimeOffset)value).UtcDateTime; } if (value is EdmEnumObject) { value = ((EdmEnumObject)value).Value; } Information.SetPropValueByName(obj, dataObjectPropName, value); } } } } } return(obj); }
/// <summary> /// Осуществляет проверку того, что <see cref="FileDescription"/> с заданными ключом загрузки и именем файла /// корректно десериализуется из JSON-строки. /// </summary> //[Test] public void TestDeserializationFromJsonByUploadKeyAndFileNameProperties() { string fileBaseUrl = "http://localhost/api/File"; // Свойства достаточные для описания файла, который не привязан к объекту данных (ключ загрузки и имя файла). string fileUploadKey = Guid.NewGuid().ToString("D"); string fileName = "readme.txt"; // Размер и тип файла. long fileSize = 1024; string fileMimeType = MimeMapping.GetMimeMapping(fileName); FileDescription fileDescription = new FileDescription(fileBaseUrl) { FileUploadKey = fileUploadKey, FileName = fileName, FileSize = fileSize, FileMimeType = fileMimeType }; string serializedFileDescription = fileDescription.ToJson(); // Получаем десериализованные описания файла, полученные различными способами. List <FileDescription> deserializedFileDescriptions = new List <FileDescription> { FileDescription.FromJson(serializedFileDescription) }; using (HttpConfiguration config = new HttpConfiguration()) { config.EnableCors(new EnableCorsAttribute("*", "*", "*")); config.Routes.MapHttpRoute("File", "api/File", new { controller = "FileDescriptionTest" }); using (HttpServer server = new HttpServer(config)) { using (HttpClient client = new HttpClient(server, false)) { // Получаем десериализованное описание файла из тела POST-запроса. using (HttpResponseMessage response = client.PostAsJsonStringAsync("http://localhost/api/File", serializedFileDescription).Result) { // Убедимся, что запрос завершился успешно. //Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); deserializedFileDescriptions.Add(FileDescriptionTestController.FileDescriptionPost); } // Получаем десериализованное описание файла из URL PUT-запроса. string putUrl = string.Format( "http://localhost/api/File?{0}={1}&{2}={3}&{4}={5}&{6}={7}", nameof(FileDescription.FileUploadKey), fileDescription.FileUploadKey, nameof(FileDescription.FileName), fileDescription.FileName, nameof(FileDescription.FileSize), fileDescription.FileSize, nameof(FileDescription.FileMimeType), fileDescription.FileMimeType); using (HttpResponseMessage response = client.SendAsync(new HttpRequestMessage(HttpMethod.Put, putUrl)).Result) { // Убедимся, что запрос завершился успешно. //Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); deserializedFileDescriptions.Add(FileDescriptionTestController.FileDescriptionPut); } } } } // Проверяем, что результаты десериализации, полученные разными способами - одинаковы и корректны. foreach (FileDescription deserializedFileDescription in deserializedFileDescriptions) { /* * Assert.AreEqual(fileUploadKey, deserializedFileDescription.FileUploadKey); * Assert.AreEqual(fileName, deserializedFileDescription.FileName); * Assert.AreEqual(fileSize, deserializedFileDescription.FileSize); * Assert.AreEqual(fileMimeType, deserializedFileDescription.FileMimeType); * Assert.AreEqual(fileDescription.FileUrl, deserializedFileDescription.FileUrl); * Assert.AreEqual(fileDescription.PreviewUrl, deserializedFileDescription.PreviewUrl); * Assert.AreEqual(null, deserializedFileDescription.EntityTypeName); * Assert.AreEqual(null, deserializedFileDescription.EntityPropertyName); * Assert.AreEqual(null, deserializedFileDescription.EntityPrimaryKey); */ } }