Showing posts with label SharePoint. Show all posts
Showing posts with label SharePoint. Show all posts

Tuesday, November 12, 2024

[Video] Stop SharePoint Online from modifying our office files

I don't know how many of us know that SharePoint is modifying the files we upload. It is doing that as part of its indexing process. The problem with that is when we download, we cannot compare the checksum to enforce integrity. This video is about how to disable that setting.

Tuesday, March 12, 2024

Get Graph Id of folder or file in SharePoint Online UI without code

Often when we work with SharePoint Online via its APIs we will encouter situations to know the Graph id of documents or folders. Mainly when we want to debug our automation code or any code written aginst Graph API. The Graph Id is currently not displayed anywhere in the UI. This video shows how can we get the Graph Id of a document from UI or without writing code.

Urls used in the video

https://vermarajneesh.sharepoint.com/sites/DataSite/_api/web/GetFolderByServerRelativeUrl('/sites/DataSite/Important%20Files/')

https://vermarajneesh.sharepoint.com/sites/DataSite/_api/web/GetFileByServerRelativeUrl('/sites/DataSite/Important%20Files/Project01/tracker.xlsx')/ListItemAllFields

https://vermarajneesh.sharepoint.com/sites/DataSite/_api/web/GetFileByServerRelativeUrl('/sites/DataSite/Important%20Files/Project01/tracker.xlsx')/ListItemAllFields/File?$select=VroomItemId

References

VroomId added - https://devblogs.microsoft.com/microsoft365dev/new-sharepoint-csom-version-released-for-sharepoint-online-august-2020/

Tuesday, November 21, 2023

SharePoint Online - Custom Raw Http calls using Graph SDK v5

The Graph SDK is a nuget library to interact with Graph API from .Net applications. Earlier this year they released¹ Graph .Net SDK v5. That highly simplified the coding with fewer lines than v4 but has breaking changes from v4 even in the basic interactions. The good part is that the Graph SDKs simplify the HTTP operations to Graph API but in some cases, we may need to deal with Raw HTTP calls. 

This post discusses such scenarios. A basic idea about Kiota and building API clients using Kiota² is helpful to easily understand the code.

Problem - Custom Url

Support we are dealing with the cutting edge of Graph API, sometimes the API or property in the result may not be available in the Graph SDK as the SDK needs some time to catch up. Unfortunately, no examples can be included due to NDA. In such cases, we need to craft the URL ourselves but we don't need to deal with the authentication. How can we customize the URL in Graph SDK with the result object using the known types in Graph SDK?

Solution

Below is the notebook³ that has the code
Hope the code is self-explanatory. The catch here is the V5 SDK will serialize the result for us. Here we are getting the known Drive object

Problem - Custom URL and Raw JSON results

Sometimes we need to deal with totally custom URLs and the result JSON. In such scenarios, we can only leverage the authentication mechanism of Graph SDK only not even the result object. How can we have a custom HTTP URL and raw JSON result using Graph SDK?

Solution - Using IRequestAdapter

One way is to use the IRequestAdapter and have NativeResponseHandler. The code in a notebook⁴ is below. This uses a dummy object to satisfy the adapter and get the JSON via NativeResponseHander.

Solution - Using native HttpClient

The above does not fully give the native experience. To get the feel of native experience we can have the below approach⁵ that uses familiar HttpClient.

Please have a look at the warning.

References

¹ - https://devblogs.microsoft.com/microsoft365dev/microsoft-graph-net-sdk-v5-is-now-generally-available/

² - https://learn.microsoft.com/en-us/openapi/kiota/quickstarts/dotnet

³ - https://github.com/dotnet-demos/graph-sdk-sharepoint-online-notebooks/blob/main/custom-rawhttp-calls.ipynb

⁴ - https://github.com/dotnet-demos/graph-sdk-sharepoint-online-notebooks/blob/main/custom-rawhttp-IRequestAdapter-json.ipynb

⁵ - https://github.com/dotnet-demos/graph-sdk-sharepoint-online-notebooks/blob/main/custom-rawhttp-HttpClient-json.ipynb

Tuesday, November 14, 2023

SharePoint Online - Which PnP SDK to download large files greater than 2GB

As we know there are 2 PnP nuget SDKs to deal with SharePoint Online from .Net client applications. PnP.Framework and PnP.Core to name and this post discuss the approach to download files larger than 2 GB. The 2 GB is an important mark as some methods work only till the MaxValue of the integer¹ data type which is 2147483647.

PnP.Framework to download > 2GB files from SharePoint Online

Below is the code to download the file and save it as a local file.

var file = clientContext.Web.GetFileByUrl(driveItemIdOrUrl);
clientContext.Load(file);
await clientContext.ExecuteQueryAsync();
logger.LogInformation($"File exists in SharePoint. Size:{file.Length:0,#}");
ClientResult<Stream> streamFromSPO = file.OpenBinaryStream();
await clientContext.ExecuteQueryAsync();

var localFilePathToDownload = Path.Combine(localFolderPathToDownload, file.Name);
using (Stream fileStream = new FileStream(localFilePathToDownload, FileMode.Create))
{
    using (streamFromSPO.Value)
    {
        streamFromSPO.Value.CopyTo(fileStream);
    }
}
logger.LogInformation($"Downloaded to {localFilePathToDownload}");

It works fine when the file size is below the max value of the integer. 

Now let us see what happens when the file size is larger.

System.FormatException: 'Invalid MIME content-length header encountered on read.'
There is one more method called OpenBinaryStream with options² that accepts SPOpenBinaryOption' enum. But there is nothing that helps us to get more than 2 GB files.

There is another method called OpenBinaryDirect³ to download files. But it fails with 401 Unauthorized when using JWT for authentication⁴. Not even able to download smaller files.
As we can see in the above image the clientContext object is the same that was used to obtain the file details.

Up on Google, we can get different suggestions such as doing it all ourselves using raw HTTP calls⁵ to avoid it and completely ⁶ avoid and migrate to PnP.Core. 

PnP.Core to download > 2GB files from SharePoint Online

Below is the code to download a file using PnP.Core.

var file = await pnpContext.Web.GetFileByServerRelativeUrlAsync(fileUrl);
logger.LogInformation($"File exists in SharePoint. Size:{file.Length}");
Stream streamFromSPO = await file.GetContentAsync(true);
var localFilePathToDownload = Path.Combine(localFolderPathToDownload, file.Name);
using (Stream fileStream = new FileStream(localFilePathToDownload, FileMode.Create))
{
    using (streamFromSPO)
    {
        streamFromSPO.CopyTo(fileStream);
    }
}
logger.LogInformation($"{nameof(DownloadFileWithReplace)} - Downloaded to {localFilePathToDownload}");

It works fine for files smaller than the max value of the integer.


What happens when the size increases above that limit?

It works!! The Int32.MaxValue didn't cause any problem here. But there are some occasions when we may experience an issue with the stream.CopyTo method. It can even happen regardless the process is 64-bit or not. In such situations, we need to tackle them separately.

One way is to pass the Buffer Size to the stream.CopyTo method.

streamFromSPO.CopyTo(fileStream,BufferSize);

Another way is to chunk ourselves. The code is below for our own chunking.

var file = await pnpContext.Web.GetFileByServerRelativeUrlAsync(fileUrl);
logger.LogInformation($"File exists in SharePoint. Size:{file.Length}");
var buffer = new byte[PnPCoreConstants.BufferSizeToDownloadLargeFile];
           
var localFilePathToDownload = Path.Combine(localFolderPathToDownload, file.Name);
using (Stream streamFromSPO = await file.GetContentAsync(true))
{
    using (Stream fileStream = new FileStream(localFilePathToDownload, FileMode.Create))
    {
        int bytesRead; long totalBytesRead = 0; long streamLength = 0;
        if (streamFromSPO.CanSeek)
        {
            streamLength = streamFromSPO.Length;
            logger.LogInformation($"{nameof(DownloadFileWithReplace)} - File Stream length:{streamLength}");
        }
        else
        {
            streamLength = file.Length;
        }
        while ((bytesRead = await streamFromSPO.ReadAsync(buffer, 0, buffer.Length)) != 0)
        {
            totalBytesRead += bytesRead;
            fileStream.Write(buffer, 0, bytesRead);
            logger.LogInformation($"{nameof(DownloadFileWithReplace)} - Progress - Read {bytesRead:0,#} and wrote.Total {totalBytesRead:0,#} / {streamLength:0,#} to {localFilePathToDownload}");
        }
     }
}
logger.LogInformation($"{nameof(DownloadFileWithReplace)} - Downloaded to {localFilePathToDownload}");

The interesting thing is that even if we provide a 2 MB buffer size, it reads 16,384 bytes only. 

Conclusion

Use the PnP.Core to download large files.

Reference

¹ - https://learn.microsoft.com/en-us/dotnet/api/system.int32.maxvalue?view=net-7.0

² - SPOpenBinaryOptions - https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/bb802697(v=office.14)

³ - https://learn.microsoft.com/en-us/previous-versions/office/sharepoint-csom/ee537083(v=office.15)

⁴ - https://stackoverflow.com/questions/45674435/401-unauthorized-exception-while-downloading-file-from-sharepoint

⁵ - https://piyushksingh.com/2016/08/15/download-large-files-from-sharepoint-online/

⁶ - Use PnP.Core https://github.com/pnp/powershell/pull/1239

Tuesday, October 31, 2023

SharePoint Online - Graph API to get all files in a drive recursively even if it has more than 5000 items

Problem

As part of the validation, we must dump all the file details inside a SharePoint drive. Some more points below

  • The input is a drive graph id.
  • It has to be recursive
  • There can be 10,000+ files in the drive and the export needs to be done in a reasonable amount of time.
  • The expected output is CSV with the below schema.
  • FolderName, FileGraphId, FileName
  • FileGraphId is the unique id of the file that Graph API understands. It is not Guid or integer instead it uses a 34-character wide format like '01XXXXA4QGNCJMT7XXXVDKCNXXXV6LCC4E'

    Approaches

    There are 3 APIs to interact with SharePoint Online. The first Graph has no direct method to list all the files recursively. We have to go by individual Graph API calls to the list folders and make subsequent  Graph calls to get the folders and files inside recursively. It works but is very slow and high chance of getting throttled. There is a search API but that has no guarantee of returning all the files as the indexing will take some time. 

    If we use the PnP.Core or PnP.Frameworks, there is no way to get the Graph IDs.

    Solution

    So lastly we have to go via Graph API but get the List items (not drive) then get the driveItem of list item to retrieve the Graph Id. The code is as follows.

    var graphClient = new GraphServiceClient(tokenProvider);

    //Url for below looks like /v1.0/sites/{siteGraphid}}/lists/{listGuid}/items?$select=contentType&$expand=driveItem HTTP/1.1
    var listItems = await graphClient
                            .Sites[siteGraphId]
                            .Lists[listGuid] // Need to get the ListId from DriveId
                            .Items
                            .GetAsync(req=> {
                                                req.QueryParameters.Expand = new []{"driveItem"};
                                                req.QueryParameters.Select = new []{"contentType"};
                                            });
    listItems
    .Value
    .Where(item=>item.ContentType.Name == "Document")
    .ToList()
    .ForEach(item=>Console.WriteLine($"FolderName:{item.DriveItem.ParentReference.Name},File Graph Id:{item.DriveItem.Id}, File Name: {item.DriveItem.Name}"));

    The drive Id needs to be translated to ListId to get this working. The QueryParameters.Select is used to limit the data to mainly 2 elements: driveItem and contentType.

    Full code

    The above code gives the idea how the required data can be pulled. Below goes full code that can be run inside Polyglot notebook.
    #r "nuget:Microsoft.Graph"
    #r "nuget:Azure.Identity"
    #r "nuget:CsvHelper"
    using Microsoft.DotNet.Interactive;
    using Microsoft.Graph;
    using Microsoft.Graph.Models;
    using Azure.Core;
    using Azure.Identity;
    using System.Collections.Generic;
    using CsvHelper;
    using CsvHelper.Configuration;
    using System.Globalization;
    public class FooMap : ClassMap<(string  FolderName,string FileGraphId,string FileName)>
    {
        public FooMap()
        {
            Map(m => m.FolderName).Name("FolderName");
            Map(m => m.FileGraphId).Name("FileGraphId");
            Map(m => m.FileName).Name("FileName");
        }
    }
    TokenCredential tokenprovider = new UsernamePasswordCredential(
                                "{userName@tenant.onmicrosoft.com}",
                                "{password}",
                                "{AAD Tenant Id}",
                                "{AAD App registration id}");
    var siteGraphId="{3 part graph Id of site}";
    var listGuid = "{Guid of List}";// Get this using Drive.
    var graphClient = new GraphServiceClient(tokenprovider);
    //Url for below looks like /v1.0/sites/{siteGraphid}}/lists/{listGuid}/items?$select=contentType&$expand=driveItem HTTP/1.1
    ListItemCollectionResponse listItems = await graphClient
                            .Sites[siteGraphId]
                            .Lists[listGuid]
                            .Items
                            .GetAsync(req=> {
                                                req.QueryParameters.Expand = new []{"driveItem"};
                                                req.QueryParameters.Select = new []{"contentType"};
                                            });
    var finalItems = new List<(string FolderName,string FileGraphId,string FileName)>();
    var pageIterator = PageIterator<ListItem,ListItemCollectionResponse>
    .CreatePageIterator(graphClient,listItems,(item)=>{
        if(item.ContentType.Name == "Document")
        {
            finalItems.Add((FolderName:item.DriveItem.ParentReference.Name,FileGraphId:item.DriveItem.Id,FileName:item.DriveItem.Name));
        }
        return true;
    });
    await pageIterator.IterateAsync();
    using(var outputFileWriter= new StreamWriter(@"c:\temp\export.csv"))
    {
        using (var csv=new CsvWriter(outputFileWriter,CultureInfo.InvariantCulture))
        {
            csv.Context.RegisterClassMap<FooMap>();
            csv.WriteRecords(finalItems);
        }
    }
    finalItems.ForEach((item)=>Console.WriteLine($"FolderName:{item.FolderName},File Graph Id:{item.FileGraphId}, File Name: {item.FileName}"))
    Each(item=>Console.WriteLine($"FolderName:{item.DriveItem.ParentReference.Name},File Graph Id:{item.DriveItem.Id}, File Name: {item.DriveItem.Name}"));

    The authentication used here is the ROPC username password. It needs to be changed based on best practices. It uses a nuget package for writing a list as a CSV file.

    References

    Wednesday, October 18, 2023

    [Video] SharePoint Online - Prefer PnP.Core over PnP.Framework

    This is a transcript of a video published on the blog's YouTube channel except for one section. That section was omitted to keep the video under 5 minutes. This may be helpful for fellow developers who cannot view videos during their work.


    Before discussing about PnP SDKs, please note that the first preference to interact with SharePoint Online should always be Graph SDK. That is the official stand from Microsoft. In case Graph API doesn't have the feature, go for SharePoint APIs or CSOM APIs. There are 2 different SDKs or nuget packages to interact with CSOM API from .Net applications. PnP.Framework and PnP.Core to name the nugets. Today we are talking about these 2 PnP SDKs what SDK we should choose and why one over the other?

    As per the documentation the PnP.Core is the successor of PnP.Framework. In that sense PnP.Core should be our clear preference. One benefit they are advertising is that it can seamlessly switch between the modern Graph API and old SharePoint CSOM APIs. That will hide the API complexities from the developer and the developer needs to learn only PnP.Core. But there are more reasons than that to choose PnP.Core.

    Number of lines

    First of all PnP.Core requires less number of lines to achieve a task. Take the example of Getting the title of the web.

    The left side is PnP.Framework code and the right side show PnP.Core. We can clearly see that the PnP.Core works with one less line. Compare this in an enterprise application with tens of thousands of lines.

    Ease of use

    Now let us look at the ease of use in achieving a use case. The example we are taking is the renaming of a folder in the document library. PnP.Core has a direct Rename method to do that. But PnP.Framework uses the move method with the new name.

    Support for Dependency injection

    The PnP.Core SDK has out-of-the-box dependency injection support. If we are using PnP.Framework, we need to write extra code to get the .net core model dependency injection.

    Size of the request payload

    The PnP.Framework uses one particular URL almost always. It is _vti_bin/client.svc/ProcessQuery.  This is the Fiddler screenshot of PnP.Framework making HTTP call to get a folder by relative Url. It uses HTTP POST to get data instead of HTTP GET. The request payload is XML format.Luckily response is in JSON. The XML requires more bytes than JSON for sending the same information. The different operations are achieved by changing the method attribute in the request payload. 


    Now let us see how the same use case is done by PnP.Core. As we can see it uses HTTP GET with user friendly URL. No request payload at all. The response payload is relatively smaller than the PnP.Framework

    Here is the side-by-side comparison between the two when querying for a folder by its relative Url. Left is PnP.Framework and right side is the PnP.Core.


    Logging

    Now coming to the logging aspects, PnP.Core is already following the .Net programming model and honoring the ILogger API. By adding the required configuration in the appsettings.json file we can get the logs into the required destination.

    Observability

    Observability is important to run a production workload. Tools such as Azure Application Insights normally log the URLs and their HTTP verbs. That will help us to understand the system behavior and also to troubleshoot issues. We have already seen that the Url is used by the PnP.Framework is mostly the same. Adding to that, it always uses HTTP POST with parameters to distinguish the operation which makes. We cannot distinguish the operation type by the HTTP verbs.

    Error handling

    In a normal web system, when an error happens it will return respective HTTP status codes. Never Http 200. Look a the fiddler trace when querying with non existing folder path from PnP.Framework. it always returns HTTP 200. In case it errors out, the error message will be inside. Production observability systems can never detect errors like this and proactively alert the dev team. 

    Otherside, PnP.Core error out with proper HTTP status code. In case the folder URL is not present, we get HTTP 404 back, and will be handled properly by the production monitoring systems.

    Contribution to Library

    The PnP libraries are open-source and maintained by the community. Microsoft is not giving any warranty or support for these libraries. In case we end up facing an issue and need to get fixed quickly we need to fix ourselves and give a pull request.

    Look at the source code of PnP.Core and PnP.Framework. The PnP.Core is relatively simple to understand. Meaning easy for us to fix the bugs in that library. It simply prepares the Url uses an internal method to execute that Url and returns the result.

    Can we completely avoid PnP.Framework?

    Not really. The PnP.Core is not a feature-complete SDK. There are features that are not available in PnP.Core.. In such situations, we need to use PnP.Framework. One place they are telling PnP.Core will eventually replace PnP.Framework. However, when a feature request was created to bridge the gap in CreateCopyJobs-related tasks, they informed PnP.Core is never going to implement that feature. In such situations we need PnP.Framework forever.
    Thanks for reading. Comments are welcome.

    Tuesday, September 26, 2023

    SharePoint Online - PnP.Core sample to check CopyJobProgress via Azure Storage Queue SDK for .Net

    This is the continuation of the last post in this blog related to checking the status of CopyJobs in SharePoint Online. The last post has code snippets using PnP.Framework. It is not a complete compilable and working sample. This post is to introduce a functional example. The only difference is that this sample is using PnP.Core SDK.

    If anyone wondering what is PnP.Framework and PnP.Core, please have a look at the dilemma of choosing .Net SDK to interact with SharePoint Online.

    Sample located at https://github.com/dotnet-demos/sharepoint-createcopyjob-azure-queue-tracking.

    Points of interest

    Though the code has a lot of comments, below are the areas that may bring some questions.

    Getting the job status

    There is no way to get the Copy Job status in PnP.Core SDK. Even there is no plan to implement that feature. Feature request #1277 was closed.

    So either we need to switch to PnP.Framework SDK or make a direct HTTP call. In the sample, we can see it is making a direct HTTP call.

    Determining the JSON request body for direct HTTP calls

    If we refer to any documentation related to SharePoint migration API, it is referring to the SDK which is nothing but .Net classes and methods. No HTTP request-response level documentation is available. The PnP.Framework SDK uses XML to craft the request payload and the response is JSON. That request XML is the hardest thing in the world to understand. Luckily the PnP.Core SDK uses JSON for request and response. Though there is a tutorial on basic operations to SharePoint REST endpoints, it is difficult to craft the JSON request to do a simple operation such as GetMigrationJobStatus(guide jobId).

    Raw HTTP call to GetMigrationJobStatus(guide jobId).

    The official documentation is there in 2 places. But in both places, it is talking about .Net SDK usage, not the HTTP-level request response.

    First, we have to decide what should be the HTTP method. Common sense tells us this should be GET as its read operation. Url should be something like _api/site/GetMigrationJobStatus/{JobId}. 
    We are totally wrong. It should be HTTP POST.

    Now how should we build the POST body to include the jobId? Just the id in the POST body or we should have an object as below?
    {
    "jobId":"{jobId}"
    }

    Nothing is gonna work except the below one.

    {
    "id":"{jobId}"
    }

    Refer to the GetJobStatus() in the example.

    Have fun with SharePoint. Also please give PR or create an issue in case of any improvements to be made.

    Tuesday, September 19, 2023

    SharePoint Online - Check CopyJobProgress via Azure Storage Queue SDK for .Net

    This post assumes some knowledge about SharePoint Online, it's file storage model via DocumentLibrary ie Drive ie List and CreateCopyJobs API used to bulk move drive items. Also Azure storage queue and how to interact with it via .Net SDK

    Scenario

    We must copy all the folders of one SharePoint drive sitting in one SharePoint Online site to drive sitting in another site. The destination site already has the drive created. It needs to be done efficiently i.e. without downloading the files from one SharePoint drive and upload to another.

    There is no drive copy mechanism available in either Graph API as well as legacy SharePoint API (CSOM). Please comment if one exists. 
    One of the promising ways is to use the CreateCopyJobs API present in SharePoint API via PnP.Framework SDK for .Net. This API accepts a list of drive item URLs, and a destination URL then returns one or more Jobs. This job is technically de-queued by Microsoft worker machines and does the actual copy job. They may have backdoors to copy the files efficiently. The returned value is of type CopyMigrationInfo. This has a JobId of type Guid. An Url to random Azure Storage Queue with SAS token, a key to decrypt messages, and the IDs of drive items it is working on.
    Note that the Azure Storage Queue is not to store the work messages. It has the log messages that are generated as part of processing. 
    Each Job can be polled via site.GetMigrationJobStatus(jobId) to check the high-level status of Queued, Processing, and None. completed. As per direct Microsoft meetings, None means Completed. Nowhere it is documented.
    None ie Completed means it is a success. Failed also will be considered completed. In order to get a detailed status of whether it failed or not we need to use another API site.GetCopyJobProgress(jobInfo). This returns CopyJobProgress object. Using this GetCopyJobProgress API is complex. It emits Logs property sometimes. 

    So we can look at the CopyMigrationInfo object that we got after the Job creation and its URL to dequeue log messages from Azure Storage Queue.

    This post is about how to check the status of CopyJobs via the Azure Storage Queue. The second warning here. Without the familiarity of these APIs, it would be difficult to understand the rest.

    Problem

    99.99% of how this thing is working is not documented. No working sample from Microsoft to use this  CreateCopyJobs API. The PnP.Framework SDK doesn't support dequeue messages from the Azure Storage Queue. Even there is no class to deserialize the output log entry into.

    Solution

    Let us start with code as the high-level steps are to de-queue messages and decrypt using the key provided. The encryption algorithm is AES256 with CBC. The IV is base64 encoded and available with the message.

            private IEnumerable<CopyJobLog> GetCopyJobLogsFromAzureStorageQueue(CopyMigrationInfo info)
            {
                Response<QueueMessage[]> messages;
                List<CopyJobLog> result = new();
                do
                {
                    QueueClient client = new(new Uri(info.JobQueueUri));
                    messages = client.ReceiveMessages(maxMessages: 25);
                    IEnumerable<CopyJobLog> logs = DecryptAndDeserializeMessages(messages, info.EncryptionKey);
                    result.AddRange(logs);
                } while (messages.Value.Length != 0);
                return result;
            }

    Now the code for DecryptAndDeserializeMessages()

            private IEnumerable<CopyJobLog> DecryptAndDeserializeMessages(Response<QueueMessage[]> messages, byte[] encryptionKey)
            {
                foreach (Azure.Storage.Queues.Models.QueueMessage msg in messages.Value)
                {
                    var base64DecodedArray = Convert.FromBase64String(msg.Body.ToString());
                    var jsonBody = Encoding.UTF8.GetString(base64DecodedArray);
                    AzureJobProgress progress = JsonConvert.DeserializeObject<AzureJobProgress>(jsonBody);
                    string progressString = progress.Decrypt(encryptionKey);
                    yield return JsonConvert.DeserializeObject<CopyJobLog>(progressString);
                }
            }

    Now we need the code for 2 entity classes and a couple of enums. They are below

       class AzureJobProgress
        {
            public string Label { get; set; }
            public string JobId { get; set; }
            public string IV { get; set; }
            public string Content { get; set; }

            public string Decrypt(byte[] key)
            {
                using (Aes aes = Aes.Create())
                {
                    aes.Key = key;
                    aes.IV = Convert.FromBase64String(IV);
                    aes.Mode = CipherMode.CBC;
                    using (ICryptoTransform decipher = aes.CreateDecryptor(aes.Key, aes.IV))
                    {
                        using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(Content)))
                        {
                            using (CryptoStream cs = new CryptoStream(ms, decipher, CryptoStreamMode.Read))
                            {
                                using (StreamReader sr = new StreamReader(cs))
                                {
                                    return sr.ReadToEnd();
                                }
                            }
                        }
                    }
                }
            }
        }

    The last one has more properties and enums.

    enum CopyJobLogMigrationDirection
        {
            Export,
            Import
        }
        enum CopyJobLogEvent
        {
            JobQueued,
            JobStart,
            JobLogFileCreate,
            FinishManifestFileUpload,
            JobProgress,
            JobEnd,
            JobFinishedObjectInfo,
            JobWarning,
        }
        enum CopyJobLogMigrationType
        {
            Copy,
        }
        class CopyJobLog
        {
            public CopyJobLogEvent Event { get; set; }
            public int TotalRetryCount { get; set; }//Set for Events:JobProgress,
            public DateTime Time { get; set; }
            public Guid JobId { get; set; }
            public Guid SiteId { get; set; }
            public Guid DbId { get; set; }
            public CopyJobLogMigrationType MigrationType { get; set; }
            public CopyJobLogMigrationDirection MigrationDirection { get; set; }
            public string Url { get; set; }
            public int FilesCreated { get; set; }//Set for Events:JobProgress,
            public int BytesProcessed { get; set; }//Set for Events:JobProgress,
            public int TotalExpectedBytes { get; set; }
            public int ObjectsProcessed { get; set; }//Set for Events:JobProgress,
            public int TotalExpectedSPObjects { get; set; }//Set for Events:JobProgress,
            public int TotalErrors { get; set; }//Set for Events:JobProgress,
            public int TotalWarnings { get; set; }//Set for Events:JobProgress,
            public double TotalDurationInMs { get; set; }//Set for Events:JobProgress,
            public int CpuDurationInMs { get; set; }//Set for Events:JobProgress,
            public int SqlDurationInMs { get; set; }//Set for Events:JobProgress,
            public int SqlQueryCount { get; set; }//Set for Events:JobProgress,
            public int WaitTimeOnSqlThrottlingMilliseconds { get; set; }
            public string FileName { get; set; }
            public bool IsShallowCopy { get; set; }//Set for Events:JobProgress,
            public Guid CorrelationId { get; set; }
            public string SourceObjectFullUrl { get; set; }
            public string TargetServerUrl { get; set; }
            public string TargetListId { get; set; }
            public string TargetObjectSiteRelativeUrl { get; set; }
            public string TargetObjectUniqueId { get; set; }
            public string TargetSiteName { get; set; }
            // For JobStart
            public Guid FarmId { get; set; }
            public Guid SubscriptionId { get; set; }
            // For JobWarning
            public string ObjectType { get; set; }
            public Guid Id { get; set; }
            public int SourceListItemIntId { get; set; }
            public int TargetListItemIntId { get; set; }
            public string Message { get; set; }
            public string ManifestFileName { get; set; }//Set for Events:FinishManifestFileUpload
        }

    Hope this is somewhat explanatory with good prior information about SharePoint CreateCopyJobs API. Planning to wrap all these into a working sample in GitHub. Hope that can be done soon.

    Comments are welcome. Use this API at your risk as it lacks documentation and samples.

    References