How to upload a small file plus metadata with GraphServiceClient to OneDrive with a single POST request? - microsoft-graph-api

I would like to upload small files with metadata (DriveItem) attached so that the LastModifiedDateTime property is set properly.
First, my current workaround is this:
var graphFileSystemInfo = new Microsoft.Graph.FileSystemInfo()
{
CreatedDateTime = fileSystemInfo.CreationTimeUtc,
LastAccessedDateTime = fileSystemInfo.LastAccessTimeUtc,
LastModifiedDateTime = fileSystemInfo.LastWriteTimeUtc
};
using (var stream = new System.IO.File.OpenRead(localPath))
{
if (fileSystemInfo.Length <= 4 * 1024 * 1024) // file.Length <= 4 MB
{
var driveItem = new DriveItem()
{
File = new File(),
FileSystemInfo = graphFileSystemInfo,
Name = Path.GetFileName(item.Path)
};
try
{
var newDriveItem = await graphClient.Me.Drive.Root.ItemWithPath(item.Path).Content.Request().PutAsync<DriveItem>(stream);
await graphClient.Me.Drive.Items[newDriveItem.Id].Request().UpdateAsync(driveItem);
}
catch (Exception ex)
{
throw;
}
}
else
{
// large file upload
}
}
This code works by first uploading the content via PutAsync and then updating the metadata via UpdateAsync. I tried to do it vice versa (as suggested here) but then I get the error that no file without content can be created. If I then add content to the DriveItem.Content property, the next error is that the stream's ReadTimeout and WriteTimeout properties cannot be read. With a wrapper class for the FileStream, I can overcome this but then I get the next error: A stream property 'content' has a value in the payload. In OData, stream property must not have a value, it must only use property annotations.
By googling, I found that there is another way to upload data, called multipart upload (link). With this description I tried to use the GraphServiceClient to create such a request. But it seems that this is only fully implemented for OneNote items. I took this code as template and created the following function to mimic the OneNote behavior:
public static async Task UploadSmallFile(GraphServiceClient graphClient, DriveItem driveItem, Stream stream)
{
var jsondata = JsonConvert.SerializeObject(driveItem);
// Create the metadata part.
StringContent stringContent = new StringContent(jsondata, Encoding.UTF8, "application/json");
stringContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("related");
stringContent.Headers.ContentDisposition.Name = "Metadata";
stringContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
// Create the data part.
var streamContent = new StreamContent(stream);
streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("related");
streamContent.Headers.ContentDisposition.Name = "Data";
streamContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
// Put the multiparts together
string boundary = "MultiPartBoundary32541";
MultipartContent multiPartContent = new MultipartContent("related", boundary);
multiPartContent.Add(stringContent);
multiPartContent.Add(streamContent);
var requestUrl = graphClient.Me.Drive.Items["F4C4DC6C33B9D421!103"].Children.Request().RequestUrl;
// Create the request message and add the content.
HttpRequestMessage hrm = new HttpRequestMessage(HttpMethod.Post, requestUrl);
hrm.Content = multiPartContent;
// Send the request and get the response.
var response = await graphClient.HttpProvider.SendAsync(hrm);
}
With this code, I get the error Entity only allows writes with a JSON Content-Type header.
What am I doing wrong?

Not sure why the provided error occurs, your example appears to be a valid and corresponds to Request body example
But the alternative option could be considered for this matter, since Microsoft Graph supports JSON batching, the folowing example demonstrates how to upload a file and update its metadata within a single request:
POST https://graph.microsoft.com/v1.0/$batch
Accept: application/json
Content-Type: application/json
{
"requests": [
{
"id":"1",
"method":"PUT",
"url":"/me/drive/root:/Sample.docx:/content",
"headers":{
"Content-Type":"application/octet-stream"
},
},
{
"id":"2",
"method":"PATCH",
"url":"/me/drive/root:/Sample.docx:",
"headers":{
"Content-Type":"application/json; charset=utf-8"
},
"body":{
"fileSystemInfo":{
"lastModifiedDateTime":"2019-08-09T00:49:37.7758742+03:00"
}
},
"dependsOn":["1"]
}
]
}
Here is a C# example
var bytes = System.IO.File.ReadAllBytes(path);
var stream = new MemoryStream(bytes);
var batchRequest = new BatchRequest();
//1.1 construct upload file query
var uploadRequest = graphClient.Me
.Drive
.Root
.ItemWithPath(System.IO.Path.GetFileName(path))
.Content
.Request();
batchRequest.AddQuery(uploadRequest, HttpMethod.Put, new StreamContent(stream));
//1.2 construct update driveItem query
var updateRequest = graphClient.Me
.Drive
.Root
.ItemWithPath(System.IO.Path.GetFileName(path))
.Request();
var driveItem = new DriveItem()
{
FileSystemInfo = new FileSystemInfo()
{
LastModifiedDateTime = DateTimeOffset.UtcNow.AddDays(-1)
}
};
var jsonPayload = new StringContent(graphClient.HttpProvider.Serializer.SerializeObject(driveItem), Encoding.UTF8, "application/json");
batchRequest.AddQuery(updateRequest, new HttpMethod("PATCH"), jsonPayload, true, typeof(Microsoft.Graph.DriveItem));
//2. execute Batch request
var result = await graphClient.SendBatchAsync(batchRequest);
var updatedDriveItem = result[1] as DriveItem;
Console.WriteLine(updatedDriveItem.LastModifiedDateTime);
where SendBatchAsync is an extension method which implements JSON Batching support for Microsoft Graph .NET Client Library

Related

Calling POST API in ASP.Net MVC with Payload Body Content

I have a 3rd party API as shown in below image.
I'm trying to call this API in ASP.Net MVC controller as below:
public ActionResult GetQuickCatalogueAsync(string category = "f97b9ec8-5e74-4f20-8582-471d067fc6c4")
{
using (var client = new HttpClient())
{
var data = JsonConvert.SerializeObject(new
{
Content = new { categoryId = category },
Key = "test",
Name = "get-products"
});
StringContent content = new StringContent(data, Encoding.UTF8, "application/json");
var res = client.PostAsync(_baseUrl, content).Result.Content.ReadAsStringAsync().Result;
var response = JsonConvert.DeserializeObject<Product>(res);
return Content("");
}
}
The PostAsync is not getting Payload body (content) properly here.
Generated data object:
{"content":{"categoryId":"f97b9ec8-5e74-4f20-8582-471d067fc6c4"},"key":"test","name":"get-products"}
Expected:
{"Name":"get-products","Key":"test","Content":"{ \u0022categoryId\u0022:\u0022f97b9ec8-5e74-4f20-8582-471d067fc6c4\u0022}"}
And the Error on Post
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"00-d1fb352d7eb19d4cbdb542bc7cee59fb-e62da6de34faee4b-00","errors":{"$.content":["The JSON value could not be converted to System.String. Path: $.content | LineNumber: 0 | BytePositionInLine: 12."]}}

Mirth HTTP POST request with Parameters using Javascript

The following code by #Nick Rupley works well, but, I need also to pass parameters as POST. How do we pass POST parameters?
from java.net.URL
var url = new java.net.URL('http://localhost/myphpscript.php');
var conn = url.openConnection();
var is = conn.getInputStream();
try {
var result = org.apache.commons.io.IOUtils.toString(is, 'UTF-8');
} finally {
is.close();
}
2 Parameters to pass: firstname="John" and lastname="Smith"
Thanks
This will POST with MIME type application/x-www-form-urlencoded. It is using apache httpclient, which is already included with mirth, as it is used internally by the HTTP Sender connector, as well as some other functionality. Other solutions may require you to download jars and add library resources.
Closer is part of Google Guava, which is also already included with mirth.
Check comments where Rhino javascript allows for simplified code compared to direct Java conversion.
It wouldn't be a bad idea to wrap all of this up in a code template function.
var result;
// Using block level Java class imports
with (JavaImporter(
org.apache.commons.io.IOUtils,
org.apache.http.client.methods.HttpPost,
org.apache.http.client.entity.UrlEncodedFormEntity,
org.apache.http.impl.client.HttpClients,
org.apache.http.message.BasicNameValuePair,
com.google.common.io.Closer))
{
var closer = Closer.create();
try {
var httpclient = closer.register(HttpClients.createDefault());
var httpPost = new HttpPost('http://localhost:9919/myphpscript.php');
// javascript array as java List
var postParameters = [
new BasicNameValuePair("firstname", "John"),
new BasicNameValuePair("lastname", "Smith")
];
// Rhino JavaBean access to set property
// Same as httpPost.setEntity(new UrlEncodedFormEntity(postParameters, "UTF-8"));
httpPost.entity = new UrlEncodedFormEntity(postParameters, "UTF-8");
var response = closer.register(httpclient.execute(httpPost));
// Rhino JavaBean access to get properties
// Same as var is = response.getEntity().getContent();
var is = closer.register(response.entity.content);
result = IOUtils.toString(is, 'UTF-8');
} finally {
closer.close();
}
}
logger.info(result);
Following is a complete working HTTP POST request solution tested in Mirth 3.9.1
importPackage(Packages.org.apache.http.client);
importPackage(Packages.org.apache.http.client.methods);
importPackage(Packages.org.apache.http.impl.client);
importPackage(Packages.org.apache.http.message);
importPackage(Packages.org.apache.http.client.entity);
importPackage(Packages.org.apache.http.entity);
importPackage(Packages.org.apache.http.util);
var httpclient = HttpClients.createDefault();
var httpPost = new HttpPost("http://localhost/test/");
var httpGet = new HttpGet("http://httpbin.org/get");
// FIll in each of the fields below by entering your values between the ""'s
var authJSON = {
"userName": "username",
"password": "password",
};
var contentStr =JSON.stringify(authJSON);
//logger.info("JSON String: "+contentStr);
httpPost.setEntity(new StringEntity(contentStr,ContentType.APPLICATION_JSON,"UTF-8"));
httpPost.setHeader('Content-Type', 'application/json');
httpPost.setHeader("Accept", "application/json");
// Execute the HTTP POST
var resp;
try {
// Get the response
resp = httpclient.execute(httpPost);
var statusCode = resp.getStatusLine().getStatusCode();
var entity = resp.getEntity();
var responseString = EntityUtils.toString(entity, "UTF-8");
var authHeader = resp.getFirstHeader("Authorization");
// logger.info("Key : " + authHeader.getName()+" ,Value : " + authHeader.getValue());
// Save off the response and status code to Channel Maps for any potential troubleshooting
channelMap.put("responseString", responseString);
channelMap.put("statusCode", statusCode);
// Parse the JSON response
var responseJson = JSON.parse(responseString);
// If an error is returned, manually throw an exception
// Else save the token to a channel map for use later in the processing
if (statusCode >= 300) {
throw(responseString);
} else {
logger.info("Token: "+ authHeader.getValue());
channelMap.put("token", authHeader.getValue());
}
} catch (err) {
logger.debug(err)
throw(err);
} finally {
resp.close();
}
This linke + above answers helped me to come up with a solution
https://help.datica.com/hc/en-us/articles/115005322946-Advanced-Mirth-Functionality
There are plenty of libraries that can help you with URI building in Java. You can find them below. But if you want to stay in Javascript just add your parameters manually than create it.
function addParam(uri, appendQuery) {
if (appendQuery != null) {
uri += "?" + appendQuery;
}
return uri;
}
var newUri = addParam('http://localhost/myphpscript.php', 'firstname="John"');
var url = new java.net.URL(newUri);
Java EE 7
import javax.ws.rs.core.UriBuilder;
...
return UriBuilder.fromUri(url).queryParam(key, value).build();
org.apache.httpcomponents:httpclient:4.5.2
import org.apache.http.client.utils.URIBuilder;
...
return new URIBuilder(url).addParameter(key, value).build();
org.springframework:spring-web:4.2.5.RELEASE
import org.springframework.web.util.UriComponentsBuilder;
...
return UriComponentsBuilder.fromUriString(url).queryParam(key, value).build().toUri();
There are multiple ways to provide http client connection with java. Since your question is specific to java.net.URL I will stick to that.
Basically you can pass parameters as POST, GET, PUT, DELETE using .setRequestMethod this will be used along with new java.net.URL(ur-destination-url).openConnection();
Here is the complete code I've using javascript in Mirth using the same java.net.URL use this it will be helpful. It worked well for me.
do {
try {
// Assuming your writing this in the destination Javascript writer
var data = connectorMessage.getEncodedData();
//Destination URL
destURL = “https://Your-api-that-needs-to-be-connected.com”;
//URL
var url = new java.net.URL(destURL);
var conn = url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
enter code here
conn.setRequestProperty (“Authorization”, globalMap.get(‘UniversalToken’));
conn.setRequestMethod(“DELETE”); // this can be post or put or get or patch
conn.setRequestProperty(“Content-length”, data.length());
conn.setRequestProperty(“Content-type”, “application/json”);
var outStream = conn.getOutputStream();
var outWriter = new java.io.OutputStreamWriter(outStream);
outWriter.write(data);
outWriter.close();
// Get response Code (200, 500 etc.)
var respCode = conn.getResponseCode();
if (respCode != 200) {
// Write error to error folder
var stringData = response.toString() + “\n”;
FileUtil.write(“C:/Outbox/Errors/” + $(“originalFilename”) + “.ERROR_RESPONSE”, false, stringData);
// Return Error to Mirth to move the file to the error folder
return ERROR;
}
errorCond = “false”;
break;
}
catch(err) {
channelMap.put(“RESPONSE”, err);
responseMap.put(“WEBSVC”, ResponseFactory.getErrorResponse(err))
throw(err);
// Can return ERROR, QUEUED, SENT
// This re-queues the message on a fatal error. I”m doing this since any fatal message may be
// caused by HTTPS connect errors etc. The message will be re-queued
return QUEUED; // Re-queue the message
java.lang.Thread.sleep(6000); // 6 seconds * 10
errorCond = “true”;
}
}
while (errorCond == “true”);

Object reference not set to an object while file upload in OneDrive

I am using Microsoft Graph SDK to upload file in chunks in OneDrive. I am using below code to upload the file:
try
{
GraphServiceClient graphClient = this.GetGraphServiceClient(accessToken);
string fileName = Path.GetFileName(srcFilePath);
using (var fileContentStream = System.IO.File.Open(srcFilePath, System.IO.FileMode.Open))
{
var uploadSession = await graphClient.Me.Drive.Root.ItemWithPath(fileName).CreateUploadSession().Request().PostAsync();
var maxChunkSize = 5 * 1024 * 1024;
var provider = new ChunkedUploadProvider(uploadSession, graphClient, fileContentStream, maxChunkSize);
var chunkRequests = provider.GetUploadChunkRequests();
var readBuffer = new byte[maxChunkSize];
var trackedExceptions = new List<Exception>();
Microsoft.Graph.DriveItem itemResult = null;
foreach (var request in chunkRequests)
{
var result = await provider.GetChunkRequestResponseAsync(request, readBuffer, trackedExceptions);
if (result.UploadSucceeded)
{
itemResult = result.ItemResponse;
}
}
}
}
catch (Microsoft.Graph.ServiceException e)
{
}
catch (Exception ex)
{
}
The above code works fine with normal file names. However, when I am trying to upload a file with name as Test#123.pdf, "Object reference not set to an object" exception is thrown at line var provider = new ChunkedUploadProvider(uploadSession, graphClient, fileContentStream, maxChunkSize); Please see below screenshot:
Is this a limitation of OneDrive SDK, or am I not passing the parameters correctly?
The # sign has a special meaning in a URL. Before you can use it, you'll need to URL Encode the file name: Test%23123.pdf.

.NET Graph SDK - OneDrive File Upload Fails with "Unsupported segment type"

Trying to upload file using the .NET SDK for Microsoft Graph. Here is the code:
DriveItem file = new DriveItem()
{
File = new Microsoft.Graph.File(),
Name = filename,
ParentReference = new ItemReference()
{
DriveId = parent.ParentReference.DriveId,
Path = path + "/" + filename
}
};
var freq = _client
.Me
.Drive
.Items[parent.Id]
.Children
.Request();
// add the drive item
file = await freq.AddAsync(file);
DriveItem uploadedFile = null;
using (MemoryStream stream = new MemoryStream(data))
{
var req = _client
.Me
.ItemWithPath(path + "/" + file.Name)
.Content
.Request();
stream.Position = 0;
// upload the content to the driveitem just created
try
{
uploadedFile = await req.PutAsync<DriveItem>(stream);
}
catch(Exception ex)
{
Debug.WriteLine("File Put Error"); <<< FAILS HERE
}
}
return uploadedFile;
An exception is thrown on the req.PutAsync method to upload the byte array containing the file contents. I am just testing with a simple text file, less than 100 bytes in size. The exception contains Bad Request and Unsupported segment type.
The file is created in OneDrive, but contains 0 bytes.
Me.ItemWithPath() requires the full path after /me. For example, _client.Me.ItemWithPath("/drives/driveId/items/itemId:/file/path"). This method is so the Path returned on the ItemReference returned via the API can be passed into the ItemWithPath method without any processing.
What you'll want to use is:
var req = _client
.Me
.Drive
.ItemWithPath(path + "/" + file.Name)
.Content
.Request();
or:
var req = _client
.Me
.ItemWithPath(file.ParentReference.Path + "/" + file.Name)
.Content
.Request();
I have found that it is sometimes easier to skip the path in lee of setting the containing folder id in the SDK statement... works in OneDrive and unified groups..
var createdFile = await graphClient.Me.Drive
.Items[currentDriveFolder.id]
.ItemWithPath(fileName)
.Content.Request()
.PutAsync<DriveItem>(stream);
I would really like to be able to just set drive id and folder id like this:
var createdFile = await graphClient
.Drives[driveId]
.Items[folderId]
.ItemWithPath(fileName)
.Content
.Request()
.PutAsync<DriveItem>(stream);

Receive, send file over Web Api

I'm trying to write a WebApi service that receives a file, does a trivial manipulation, and sends the file back. I'm having issues on sending and/or receiving the file from the service.
The issue I'm having is that the file returned from the service is ~1.5x larger than the manipulated file, e.g. when the file is returned it's like 300kb instead of the 200kb it should be.
I assume its being wrapped and or manipulated somehow, and I'm unsure of how to receive it properly. The code for the WebAPI service and the method that calls the web service are included below
In, the WebApi service, when I hit the line return Ok(bufferResult), the file is a byte[253312]
In the method that calls the web service, after the file is manipulated and returned, following the line var content = stream.Result;, the stream has a length of 337754 bytes.
Web API service code
public ConversionController: APIController{
public async Task<IHttpActionResult> TransformImage()
{
if (!Request.Content.IsMimeMultipartContent())
throw new Exception();
var provider = new MultipartMemoryStreamProvider();
await Request.Content.ReadAsMultipartAsync(provider);
var file = provider.Contents.First();
var filename = file.Headers.ContentDisposition.FileName.Trim('\"');
var buffer = await file.ReadAsByteArrayAsync();
var stream = new MemoryStream(buffer);
// [file manipulations omitted;]
// [the result is populated into a MemoryStream named response ]
//debug : save memory stream to disk to make sure tranformation is successfull
/*response.Position = 0;
path = #"C:\temp\file.ext";
using (var fileStream = System.IO.File.Create(path))
{
saveStream.CopyTo(fileStream);
}*/
var bufferResult = response.GetBuffer();
return Ok(bufferResult);
}
}
Method Calling the Service
public async Task<ActionResult> AsyncConvert()
{
var url = "http://localhost:49246/api/conversion/transformImage";
var filepath = "drive/file/path.ext";
HttpContent fileContent = new ByteArrayContent(System.IO.File.ReadAllBytes(filepath));
using (var client = new HttpClient())
{
using (var formData = new MultipartFormDataContent())
{
formData.Add(fileContent, "file", "fileName");
//call service
var response = client.PostAsync(url, formData).Result;
if (!response.IsSuccessStatusCode)
{
throw new Exception();
}
else
{
if (response.Content.GetType() != typeof(System.Net.Http.StreamContent))
throw new Exception();
var stream = response.Content.ReadAsStreamAsync();
var content = stream.Result;
var path = #"drive\completed\name.ext";
using (var fileStream = System.IO.File.Create(path))
{
content.CopyTo(fileStream);
}
}
}
}
return null;
}
I'm still new to streams and WebApi, so I may be missing something quite obvious. Why are the file streams different sizes? (eg. is it wrapped and how do I unwrap and/or receive the stream)
okay, to receive the file correctly, I needed to replace the line
var stream = response.Content.ReadAsStreamAsync();
with
var contents = await response.Content.ReadAsAsync<Byte[]>();
to provide the correct type for the binding
so, the later part of the methods that calls the service looks something like
var content = await response.Content.ReadAsAsync<Byte[]>();
var saveStream = new MemoryStream(content);
saveStream.Position = 0;
//Debug: save converted file to disk
/*
var path = #"drive\completed\name.ext";
using (var fileStream = System.IO.File.Create(path))
{
saveStream.CopyTo(fileStream);
}*/

Resources