/// <summary> /// Writes the specified stream which contains the device request to the file storage. /// </summary> /// <param name="deviceId"></param> /// <param name="data"></param> /// <exception cref="InvalidOperationException"> /// Write operation failed due to a conflict while creating the destination blob /// -or- The uploaded blob has been deleted prior to generate the SAS token enabled URL to access the blob. /// </exception> /// <returns></returns> public async Task <string> WriteFileAsync(string deviceId, BlobStreamDecorator data) { var filePath = $"/{m_FileStorageOptions.Value.ServerFileUploadFolder}/{deviceId}/{m_FileStorageOptions.Value.ServerFileUploadSubFolder}/{Guid.NewGuid()}"; try { await m_BlobRepository.WriteBlobAsync(filePath, data); return(await m_BlobRepository.GetDelegatedReadAccessAsync(filePath, ReadAccessExpiryDelay)); } catch (ApiException ex) { //re-throw the exception as a non ApiException which will cause the error to be properly logged string error; switch (ex) { case ConflictException _: //we are the less fortunate people on earth and got a Guid conflict error = $"File '{filePath}' cannot be written"; break; case IdNotFoundException _: //there is a concurrency issue somewhere and someone deleted the blob we just created error = $"File '{filePath}' cannot be found"; break; default: //some case we did not think about yet but we might actually need to handle... throw; } //InvalidOperationException as all those errors are transient and should not happen, retrying should be fine throw new InvalidOperationException(error, ex); } }
/// <summary> /// Creates a device request based on the incoming HTTP request. /// </summary> /// <param name="deviceId"></param> /// <param name="request"></param> /// <returns></returns> public async Task <DeviceRequest> CreateRequestAsync(string deviceId, HttpRequest request) { // discard any body that could have been provided when doing GET or DELETE requests if (HttpMethods.IsGet(request.Method) || HttpMethods.IsDelete(request.Method)) { return(new DeviceInlineRequest(request)); } // determine whether the request body needs to be written to file storage or sent inline to the device. // a more accurate check could be done against the serialized DeviceRequest instead of the current request.ContentLength since additional info is added to the payload later on. // that would still not be perfectly accurate since the SDK adds its own data to the final payload. if (request.HasFormContentType || request.ContentLength.GetValueOrDefault() > m_DeviceCommunicationAdapter.GetMaximumMessageSize()) { var fileData = new BlobStreamDecorator(request.Body) { ContentType = request.ContentType }; var blobSasUrl = string.Empty; await m_TelemetryService.IncrementCounterAsync("blob_upload", new string[] { }); blobSasUrl = await m_TelemetryService.StartTimerAsync(() => m_FileService.WriteFileAsync(deviceId, fileData), "blob_upload", new string[] { }); m_TelemetryService.Dispose(); return(new DeviceFileRequest(request) { BlobUrl = blobSasUrl }); } else { var inlineRequest = new DeviceInlineRequest(request); using (StreamReader reader = new StreamReader(request.Body, inlineRequest.Headers.GetEncoding())) { inlineRequest.Body = await reader.ReadToEndAsync(); } return(inlineRequest); } }