Apollo3zehn
Apollo3zehn

Reputation: 552

How to upload a small file plus metadata with GraphServiceClient to OneDrive with a single POST request?

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?

Upvotes: 1

Views: 2888

Answers (1)

Vadim Gremyachev
Vadim Gremyachev

Reputation: 59338

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

Upvotes: 3

Related Questions