April 8, 2020

Copying Teams Files tab content using MoveCopyUtil

Series

This is series of blog articles showing how to clone Teams Channels and Tabs without using the Clone REST endpoint or direct REST queries. Except we use the Microsoft.Graph (3.1.0). First post discusses things in general, details about different tab types are separated to individual articles.

  1. Cloning Teams Channels and Tabs without /clone REST endpoint
  2. Cloning Planner Tab without /clone REST endpoint
  3. Cloning OneNote Tab without /clone REST endpoint
  4. Cloning Web Tab without /clone REST endpoint (TBD)
  5. Copying Teams Files tab content using MoveCopyUtil <<YOU ARE HERE>>

Task

As part of Teams provisioning, content of Files tab needed to be copied from source Team to the destination Team. As you might know, in practice it just means copying folders and files across SharePoint site collections.

Solution

Microsoft.SharePoint.Client.MoveCopyUtil will do the trick UNLESS a folder name contains special characters, see here for example. This has been an issue for long in CSOM, and if at all possible, I recommend not having special characters in folder names, and not in file names either for that matter.

If you must support special character scenarios, it is doable, but will require carefully splitting and constructing the query parameters and effectively means you need to loop and create/copy folders and files one by one. I’ve spent some time creating SPFx solution that manages to do this, and I recommend thinking not twice, but ten times before agreeing to support special characters in this case. Good thing is that in this Teams provisioning scenario you will have control over the source Team and should be able to work around any needs for special characters.

Explanation of the code

What happens in this code can probably be read quite well by looking at the LogInformation entries, but the idea is that we get hold of the Drive item of the source and destination Group. Drive.WebUrl is then absolute URL to the document library where Teams File tab contents are in their own sub-folders.

As a reminder, each File tab in a Team Channel maps to one root level folder (Channel name = folder name) in that document library you find at Drive.WebUrl.

Another nice thing about Group.Drive is that whenever we get hold of that, and it is not returning HTTP 404, we know Group provisioning (the process that runs in the background and creates all back-end dependencies, such as the SharePoint site) has been completed.

retry object is Polly’s AsyncRetryPolicy and ensures retry logic in case provisioning would not be yet ready at this point. It will retry the call as configured, in my case every 5 seconds, until the .GetAsync() completes successfully.

Next up there is some ugly parsing of the source document library path, namely we’re removing the document library part from the URL, leaving us with URL of the SPWeb. There are many ways of doing this, and I admit this one is quite verbose way of doing it but doesn’t at least require additional HTTP query.

Then we get to the actual point, initializing SharePoint context using the source SPWeb, and credentials stored in Azure Key Vault.

After getting all the root level folders (each corresponding a Channel, yes, good), we loop through each folder item and use MoveCopyUtil.CopyFolder to copy it to destination document library. Just remember to set the MoveCopyOptions to KeepBoth, indicating this is a Copy operation and not Move operation.

I felt more comfortable doing .ExecuteQuery for each folder, although it means few more HTTP calls, but allows improved error handling in case of those special characters, or some other problems that might occur during the copy operation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public static async Task CopyDocuments(string srcGroupId, string dstGroupId, AsyncRetryPolicy retry, GraphServiceClient graphClient, KeyVault keyvault, ILogger logger)
{
    var sourceDrive = await retry.ExecuteAsync(async () =>
    {
        return await graphClient.Groups[srcGroupId].Drive.Request().GetAsync();
    });

    var destDrive = await retry.ExecuteAsync(async () =>
    {
        return await graphClient.Groups[dstGroupId].Drive.Request().GetAsync();
    });

    try
    {
        logger?.LogInformation($"Copying SPO files, source drive: {sourceDrive.WebUrl}");
        logger?.LogInformation($"Copying SPO files, destination drive: {destDrive.WebUrl}");

        var sourceUri = new Uri(sourceDrive.WebUrl);
        string sourceSiteUrlWithoutLastSegment = sourceUri.AbsoluteUri.Remove(sourceUri.AbsoluteUri.Length - sourceUri.Segments.Last().Length);

        using (var srcContext = new OfficeDevPnP.Core.AuthenticationManager()
            .GetSharePointOnlineAuthenticatedContextTenant(sourceSiteUrlWithoutLastSegment, keyvault.GetSecret("teamsClone--SPOUser"), keyvault.GetSecret("teamsClone--SPOPassword")))
        {
            logger?.LogInformation($"Copying SPO files, getting root folders");

            // get folders at root                   
            var targetList = srcContext.Web.Lists.GetByTitle(sourceDrive.Name);

            // This method only gets the folders which are on top level of the list/library
            FolderCollection oFolderCollection = targetList.RootFolder.Folders;

            // Load folder collection
            srcContext.Load(oFolderCollection);
            srcContext.ExecuteQuery();

            MoveCopyOptions option = new MoveCopyOptions
            {
                KeepBoth = true
            };

            logger?.LogInformation($"Copying SPO files, looping folders");

            foreach (var folder in oFolderCollection)
            {
                if (folder.Name.Equals("Forms"))
                {
                    continue;
                }

                try
                {
                    logger?.LogInformation($"Copying SPO files, copying folder '{sourceUri.GetLeftPart(UriPartial.Authority) + folder.ServerRelativeUrl}' -> '{destDrive.WebUrl}/{folder.Name}'");

                    MoveCopyUtil.CopyFolder(
                        srcContext,
                        sourceUri.GetLeftPart(UriPartial.Authority) + folder.ServerRelativeUrl,
                        $"{destDrive.WebUrl}/{folder.Name}",
                        option);

                    srcContext.ExecuteQuery();

                    logger?.LogInformation("Copying done");
                }
                catch (Exception ex)
                {
                    logger?.LogError($"Error copying SPO folder {folder.Name}. {ex.Message}");
                }
            }
        }
    }
    catch (Exception ex)
    {
        logger?.LogError($"Error copying SPO content. {ex.Message}");
    }
}

No comments:

Post a Comment