Uploading Large Files using Microsoft Graph API

This post is a contribution from Adam Burns, an engineer with the SharePoint Developer Support team

A while back, I wrote an article explaining that you must use File Chunking if you want to upload files larger than 250 MB to SharePoint or OneDrive for Business, using either the SharePoint REST API or CSOM.  This is because the file upload limit is set in the service and cannot be changed.  Using the Microsoft Graph API is even more restrictive.  The file size limit for uploading files is so small (4 MB) that you really should always use the chunking method, but the implementation is a little different than it is with the SharePoint REST API.  Also, to get an access token for Graph you will always need to use an Azure AD authorization endpoint.

At the bottom of this post you find links to two examples of how to upload larger file using Graph – one that uses plain JavaScript and jQuery and the other using a simple Angular 4 framework.  These examples both use Azure v2.0 authentication protocols documented at: /en-us/azure/active-directory/develop/active-directory-v2-protocols.  This means you can use the same code to access resources with either your organizational credentials (such as for SharePoint Online or OneDrive for Business document libraries) or your Microsoft account credentials (such as OneDrive Personal document or contacts).

 

The Basics

The main operation you need is /createUploadSession. The details of the API are clearly documented here: /en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession#create-an-upload-session

You’ll need to write more code than you may expect, to do this correctly.

Here are the main steps you need to perform to upload a file using the /createUploadSession approach:

  1. Use /createUploadSession to retrieve an upload URL which contains the session identifier. The response will look like this:

     {
      "uploadUrl": "https://sn3302.up.1drv.com/up/fe6987415ace7X4e1eF866337",
      "expirationDateTime": "2015-01-29T09:21:55.523Z",
      "nextExpectedRanges": ["0-"]
    }
    
  2. Use PUT to upload chunks to the uploadUrl. Each chunk should start with a starting range represented by the first number in the “nextExpectedRanges” value in the JSON response from the last PUT. As shown above, the first range will always start with zero. Don’t rely on the end of the range in the “nextExpectedRanges” value. For one thing, this is an array and you may occasionally see multiple ranges. You will usually just pick the starting number of the 0th member of the array. As noted in the documentation, you should use a chunk size which is divisible by 320 KiB (327,680 bytes). So, the headers of your PUT request will look like this:

     PUT https:// {tenant}-my.sharepoint.com/personal/adambu_{tenant}-_onmicrosoft_com/_api/v2.0/drive/items/013JB6FTV6Y2GOVW7725BZO354PWSELRRZ/uploadSession?guid=%277734613e-7e6b-433a-840a-a77a93496928%27&path=%27Code%20Example%20Tagging%20Tool%20Deck.pptx%27&overwrite=True&rename=False&tempauth=eyJ0eXAiOiJKV1Q{....} HTTP/1.1
    Host: {tenant}-my.sharepoint.com
    Connection: keep-alive
    Content-Length: 327680
    Content-Range: bytes 0-327679/1048558
    Accept: */*
    Origin: ------snip------
    

 

Both the linked examples below use 4 main functions to do all this work:

  1. getUploadSession() which makes the initial POST request that retrieves the uploadUrl

  2. uploadChunks() which has the logic to loop based on the response to each PUT call. It in turn calls:

  3. readFragmentAsync() which actually slices up the byte array using FileReader and it’s onloadend event. Here is and example of that method:

     // Reads in the chunk and returns a promise.
        function readFragmentAsync(file, startByte, stopByte) {
            var frag = ""; 
            const reader = new FileReader(); 
            console.log("startByte :" + startByte + " stopByte :" + stopByte); 
            var blob = file.slice(startByte, stopByte); 
            reader.readAsArrayBuffer(blob); 
            return new Promise((resolve, reject) =>  {
                reader.onloadend = (event) =>  {
                    console.log("onloadend called  " + reader.result.byteLength); 
                    if (reader.readyState === reader.DONE) {
                        frag = reader.result; 
                        resolve(frag); 
                    }
                }; 
            }); 
        }
    
  4. uploadChunk() . This method is also called by uploadChunks to actually execute the ajax PUT call.

All this results in some pretty lengthy code. My examples may be more verbose because I put in a lot of logging to illustrate the process. You may figure out a way to make the code more efficient and terse, but for a production scenario, you will have to add additional code to handle unexpected situations. These scenarios are explained at: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem\_createuploadsession\#create-an-upload-session. My purpose with this blog post was to expose some real-world code examples, because, at the time of this writing, there is almost nothing out there for this API. Once the basic approach is understood, it should not be hard to create tests and guard code to handle unexpected conditions.

 

Special Notes
I want to point out two things which are not mentioned in the above-referenced article.

  1. The articles and examples show the following request body for the initial call to create the upload session:

         const body = {
                "item": {
                    "@microsoft.graph.conflictBehavior":"rename"
                }
            }; 
    

    As you would expect, conflictBehavior is an enum that would provide different behaviors on the server side when there is a name conflict for the uploaded file. The definition of the enum, describes the following three values: Fail, Replace, Rename.
    When you use “rename,” as the conflict behavior, the file gets an incremented integer as a string appended to the file name, so for “myFile.docx” you would get a new file uploaded called “myFile 1.docx.”
    When you use “replace,” you end up replacing the file and all the metadata is updated. When you use “fail,” as the conflict behavior, the initial POST receives a HTTP 409 response ("The specified item name already exists.") and your code will have to handle the exception.

  2. In most cases, when you upload the final chunk of the file, you will receive a response with the HTTP status code of 201. If you specify a conflict behavior of “replace,” you will receive a HTTP 200 response instead. Therefore, you must handle the response with code with something like:

                if (res.status === 201 || res.status === 200) { 
           console.log("Reached last chunk of file.  Status code is: " + res.status);
                     continueRead = false; 
               }  
    

 

Sample Code
1.       https://github.com/adambu/createuploadsession-graph-jquery

2.      https://github.com/adambu/createuploadsession-graph-angular