April 13th, 2021

Microsoft Graph Mailbag – Copy/Move Files and Folders in SharePoint Online

In today’s Microsoft Graph Mailbag post, we cover a customer scenario for copying and moving files between SharePoint Online sites using Microsoft Graph.

Please be sure to follow this blog series using https://aka.ms/MSGraphMailbag or with RSS using https://developer.microsoft.com/graph/blogs/feed/?tag=MSGraphMailbag.

Business Scenario

As a Customer Engineer (CE), I help Microsoft customers by being their trusted advisor to provide best practices, roadmap and guidance for various business scenarios. One such scenario deals with the copying and moving of files and folders between SharePoint Online (SPO) sites. The out of the box “Copy To” and “Move To” capability that SPO provides is great, but the sites that appear as the destination is based on the user signals (e.g., user followed sites, frequently accessed sites). For this customer, they want to control the sites the users see when they attempt to copy/move files.

Background

We considered various approaches including compliance labels, PnP JS, SPO REST API and more.  For SharePoint Online resources, Microsoft generally recommends to first use Microsoft Graph where possible.  If Microsoft Graph does not cover specific functionality or resources, then it is recommended to fall back to SPO CSOM or another existing API.  In this scenario, the customer decided to go with Microsoft Graph and a SharePoint Framework solution after a quick proof of concept.

Solution

Let’s get started.

In this blog, I will share the various Microsoft Graph REST API calls that need to be constructed to copy/move files from one site to another. Let’s take a scenario where I need to copy/move files from source “SiteA” to destination “SiteB”.

Source: https://contoso.sharepoint.com/sites/SiteA/Documents/

Destination: https://contoso.sharepoint.com/sites/SiteB/Documents/

Here is a series of Microsoft Graph API calls. I also provided the corresponding sample response from these calls. Note that few of the responses are truncated for brevity.

Scenario: Copy Files

Get source file:

  1. Get the source site collection id:
GET
https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/SiteA?$select=id
Response:
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites(id)/$entity",
    "id": "contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da"
}

2. Use the Site collection ID in the next query to get the list of document libraries:

GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drives?$select=id,name

Response:
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives(id,name)",
    "value": [
        {
            "id": "b!xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
            "name": "Documents"
        },
        {
            "id": "b!xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9doAJ43YI-NhR6zMPnX1eLzZ",
            "name": "MyDocs1"
        },
        {
            "id": "b!xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9dr4572v55yoQo_RWg6MAlAR",
            "name": "MyDocs2"
        }
    ]
}

NOTE: The /drives endpoint currently (as of 4/10/2021) is not supporting $filter or $search OData parameters.

  1. Now that we have the ID for “Documents” library. Let’s use that to enumerate all the files/folders in this document library. Note how I am using the $select Odata parameter to limit the data in the response.
GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drives/
b!xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-/list/drive/root/children?$select=id,name,webUrl

Response:
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites('contoso.sharepoint.com%2C1af9d2c5-2b3b-47d2-a167-ffd5d0c45205%2Ce748bad1-cb75-432f-8bb1-adff3b9ef5da')/
drives('b%21xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-')/list/drive/root/children(id,name,webUrl)",
    "value": [
        {
            "@odata.etag": "\"{297E7CA7-4C49-440D-8E5F-84844DA865D0},1\"",
            "id": "017LC4LMFHPR7CSSKMBVCI4X4EQRG2QZOQ",
            "name": "Decks",
            "webUrl": "https://contoso.sharepoint.com/sites/SiteA/Shared%20Documents/Decks"
        },
        {
            "@odata.etag": "\"{F3226AA0-414F-4523-8364-9E03AF05801B},1\"",
            "id": "017LC4LMFANIRPGT2BENCYGZE6AOXQLAA3",
            "name": "AuthCodePPEGuide.pdf",
            "webUrl": "https://contoso.sharepoint.com/sites/SiteA/Shared%20Documents/AuthCodePPEGuide.pdf"
        }
    ]
}
  1. I can improve the above query using $filter Odata parameter. Let’s say I want to copy the file ‘AuthCodePPEGuide.pdf’. I can build the Microsoft Graph query as follows:
GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drives/
b!xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-/list/drive/root/children?$filter=startswith(name, 'AuthCodePPEGuide.pdf')&$select=id,name,webUrl

Response:
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites('contoso.sharepoint.com%2C1af9d2c5-2b3b-47d2-a167-ffd5d0c45205%2Ce748bad1-cb75-432f-8bb1-adff3b9ef5da')/
drives('b%21xdL5Gjsr0kehZ__V0MRSBdG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-')/list/drive/root/children(id,name,webUrl)",
    "value": [
        {
            "@odata.etag": "\"{F3226AA0-414F-4523-8364-9E03AF05801B},1\"",
            "id": "017LC4LMFANIRPGT2BENCYGZE6AOXQLAA3",
            "name": "AuthCodePPEGuide.pdf",
            "webUrl": "https://contoso.sharepoint.com/sites/SiteA/Shared%20Documents/AuthCodePPEGuide.pdf"
        }
    ]
}
  1. I can now identify that specific file / document using its “id” as follows:
GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drive/items/
017LC4LMFANIRPGT2BENCYGZE6AOXQLAA3

Get destination location details:

  1. Using similar queries shown earlier in steps 1 & 2, I can identify the destination site collection id and the “Documents” library id. Here is an example of what it looks like:
https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drives/
b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-

NOTE: Bold part is the site id for the destination site collection. Italicized part is for the “Documents” document library id.

  1. We need to build a reference notation for our destination location which could be the root of the document library or a specific folder within it. This reference is called “parentReference”.
  2. Run this Microsoft Graph query to enumerate the destination library, select the unique id and the parentReference corresponding to each entry:
GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drives/
b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-/list/drive/root/children?$select=id,name,parentReference

Response:
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites('contoso.sharepoint.com%2C1af9d2c5-2b3b-47d2-a167-ffd5d0c45205%2Ce748bad1-cb75-432f-8bb1-adff3b9ef5da')
/drives('b%21w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-')/list/drive/root/children(id,name,parentReference)",
    "value": [
        {
            "@odata.etag": "\"{F72ACBB9-419C-41FC-965C-F90DDE31F699},1\"",
            "id": "01U7PKJ3NZZMVPPHCB7RAZMXHZBXPDD5UZ",
            "name": "DropFolder",
            "parentReference": {
                "driveId": "b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
                "driveType": "documentLibrary",
                "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ",
                "path": "/drives/b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-/root:"
            }
        },
        {
            "@odata.etag": "\"{08CB141D-4471-445B-B835-776E78A67C23},3\"",
  "id": "01U7PKJ3I5CTFQQ4KELNCLQNLXNZ4KM7BD",
            "name": "NDP462-DevPack-KB3151934-ENU.zip",
            "parentReference": {
                "driveId": "b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
                "driveType": "documentLibrary",
                "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ",
                "path": "/drives/b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-/root:"
            }
        }
    ]
}

NOTE: Bold part shows the unique ID of the entry (in this example, it’s a folder with name ‘DropFolder’) and the Italicized part shows the parentReference corresponding to this document library.

  1. To copy the file to the root of the Document Library, I can use the parentReference from step 8 as-is.
  2. To copy to a specific folder within the library I must use the folders unique ID in the parent reference. From the above example, if you need to copy to ‘DropFolder’, your parentReference notation should look like this:
"parentReference": {
       "driveId": "b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
       "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ"
    }

NOTE: The “id” corresponds to the ID of the DropFolder (see response in Step 8)

Copy the file to destination:

  1. In Step 5 we identified the file to be copied with its ID. In Step 10 we identified the destination reference notation. Now to copy the file to the destination I send a POST request as follows:
//To copy the file to the root of the destination library

POST https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drive/items/
017LC4LMFANIRPGT2BENCYGZE6AOXQLAA3/copy
{
    "parentReference": {
        "driveId": -"
        "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ"
    },
    "name": "AuthCodePPEGuide.pdf"
}
//To copy the file to “DropFolder” folder in the destination library
POST https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drive/items/
017LC4LMFANIRPGT2BENCYGZE6AOXQLAA3/copy
{
    "parentReference": {
       "driveId": "b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
       "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ"
    },
    "name": "AuthCodePPEGuide.pdf"
}
Response:
HTTP 202 Accepted
{
    "cache-control": "no-cache",
    "client-request-id": "0ffe0ea7-85f6-402e-f504-1a8740423832",
    "location": "https://contoso.sharepoint.com/sites/SiteA/_api/v2.1/drive/operations/faa1c015-a4af-4dfb-9abf-03cd22890db3?c=ZU0xOVRzMGc4RFhJSV...",
    "request-id": "3868dff6-cdbd-45d1-bf3d-9ab98ea5491b"
}
  1. As shown above you will get a 202 Accepted response, with a Location header that has a monitoring url. Copy that URL and open in a new tab to see the copy progress and status. Here is sample output:

Sample output on 202 Accepted response

NOTE: The percentageComplete and status attributes in the JSON response.

Lessons Learned

Below are a few lessons learned along the way while working out the Microsoft Graph calls:

  1. Microsoft Graph calls to copy/move work asynchronously, which means you must rely on the monitoring url that you get with the 202 Accepted response to check the status.
  2. You may hit issues including ‘bad request’, ‘general error’ or ‘request malformed’ in some scenarios, such as when the file that is being copied/moved is greater than 160MB.
  3. I found that the above issue happens only when the “name” used for the destination file is different from the source file name. Ensure the destination file name is same as the source file name to resolve this issue. This issue is not reproducible with smaller files.

Scenario:  Move Files

  1. To move a file from one library to another, we construct a similar request like we did in Step 5 in the above section. Here is an example for a file named “ProjectYRequirements.docx” that exists in the source document library.
https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,bc0a0284-87ff-4fcc-b039-0b7e02823759,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drive/items/
01HD766IY4YTCVBFQNOBEZUBDSXXYDAQIJ
  1. We also get the parentReference for the destination library/folder. Example:
"parentReference": {
       "driveId": "b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
       "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ"
    }
  1. Then we patch the request to Microsoft Graph as follows:
PATCH https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,bc0a0284-87ff-4fcc-b039-0b7e02823759,e748bad1-cb75-432f-8bb1-adff3b9ef5da/drive/items/
01HD766IY4YTCVBFQNOBEZUBDSXXYDAQIJ

Headers:
Prefer:respond-async
Request body:
{
    "parentReference": {
        "driveId": "b!w8qjV86a-EibM8Lpka5JcNG6SOd1yy9Di7Gt_zue9dq9z2zXDxX4RYKTxY09UkQ-",
        "id": "01U7PKJ3N6Y2GOVW7725BZO354PWSELRRZ"
    },
    "name": "ProjectYRequirements.docx"
}
Response:
{
    "cache-control": "no-cache",
    "client-request-id": "8248a200-00cc-51cc-576c-b48d16109cf4",
    "location": "https://contoso.sharepoint.com/sites/Web03/_api/v2.1/drive/operations/7e75fb87-07ea-4b85-8b40-369d1343ba38?c=T3QzNlIzOG1xdW1VOGR3OGlMZjFQYm5uR21...",
    "request-id": "9e419a03-4421-413a-9753-8d6dec74515d"
}

Lessons Learned

Here are few lessons learned that are specific to moving files between sites and libraries:

  1. If destination name doesn’t match, you will get HTTP 400 – Bad Request error with error message “invalid request”. This is irrespective of the file size.
  2. If you match the file name then you will see HTTP 400 Bad Request but with error code that says: Requested move requires an async response, add ‘Prefer: respond-async’ to allow. Ensure you add this header to the request.
  3. For the move to be successful the source and the destination libraries must have the same schema (Site Columns). This is because the Move action also moves the metadata along with the File.

Copy File using Microsoft Graph SDK

I can use the .NET Core sample code that we introduced in our #30DaysMSGraph series to get started on how to use Microsoft Graph client library. Once I have the GraphServiceClient object instantiated I can use below code snippet to copy a file between SPO sites.

//Create the destination parentReference
var parentReference = new ItemReference
{
    DriveId= "b!w8qjV86a……z2zXDxX4RYKTxY09UkQ-",
    Id= "01U7PKJ3N6Y2GOV……5BZO354PWSELRRZ"
};

//File name to save as
var fileName = "AuthCodePPEGuide.pdf";

//Copy the file over to the destination library
var result = await graphClient.Sites["contoso.sharepoint.com,1af9d2c5-2b3b-47d2-a167-ffd5d0c45205,e748bad1-cb75-432f-8bb1-adff3b9ef5da"]
    .Drives["b!xdL5Gjsr0kehZ__V0MRSBd……dq9z2zXDxX4RYKTxY09UkQ-"]
    .Items["017LC4LMFANIR……CYGZE6AOXQLAA3"].Copy(fileName, parentReference)
    .Request().PostAsync();

Conclusion

We showed the various Microsoft Graph calls required to copy/move files and folders across SPO sites along with the lessons learned. We recommend using one of the Microsoft Graph SDKs rather than making REST API calls to Microsoft Graph. This is because the Microsoft Graph SDKs provide capabilities like token caching, retry calls, strongly typed resources, etc.

 

Today’s post was written by Srinivas Varukala, Sr. Customer Engineer (CE) at Microsoft.  You can follow Srinivas on Twitter at @Svarukala. Join us for our next Microsoft Graph Mailbag post on April 27, 2021.