From b2b9aa0969ac6fd29b464985b8f8eb68c56eb9a8 Mon Sep 17 00:00:00 2001 From: Nishkalank Bezawada <47456098+NishkalankBezawada@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:12:40 +0200 Subject: [PATCH] Feature 3422- New Commandlet- Convert-PnPFileToPdf --- documentation/Convert-PnPFileToPdf.md | 257 ++++++++++++++++++++++ src/Commands/Files/ConvertFile.cs | 185 ++++++++++++++++ src/Commands/Utilities/REST/RestHelper.cs | 27 +++ 3 files changed, 469 insertions(+) create mode 100644 documentation/Convert-PnPFileToPdf.md create mode 100644 src/Commands/Files/ConvertFile.cs diff --git a/documentation/Convert-PnPFileToPdf.md b/documentation/Convert-PnPFileToPdf.md new file mode 100644 index 000000000..2defdf104 --- /dev/null +++ b/documentation/Convert-PnPFileToPdf.md @@ -0,0 +1,257 @@ +--- +Module Name: PnP.PowerShell +schema: 2.0.0 +applicable: SharePoint Online +online version: https://pnp.github.io/powershell/cmdlets/Convert-PnPFileToPdf.html +external help file: PnP.PowerShell.dll-Help.xml +title: Convert-PnPFileToPdf +--- + +# Convert-PnPFileToPdf + +## SYNOPSIS +Converts a file to Pdf + +## SYNTAX + + +### Save to local path +```powershell +Convert-PnPFileToPdf -Url -Path [-Force] +``` + +### Return as memorystream +```powershell +Convert-PnPFileToPdf -Url -AsMemoryStream +``` + +### Save to SharePoint Online (Same SiteCollection) +```powershell +Convert-PnPFileToPdf -Url -DocumentLibrary [-Checkout] [-CheckInComment ] [-CheckinType ] + [-Approve] [-ApproveComment ] [-Publish] [-PublishComment ] +``` + +## DESCRIPTION +Allows converting of a file from SharePoint Online. The file contents can either be directly saved to local disk, or stored in memory for further processing, or Can be uploaded back to SharePoint Online SiteCollection + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Convert-PnPFileToPdf -Url "/sites/project/Shared Documents/Document.docx" -AsMemoryStream +``` + +Retrieves the file and converts to PDF, and outputs its content to the console as a Memory Stream + +### EXAMPLE 2 +```powershell +Convert-PnPFileToPdf -Url "/sites/project/Shared Documents/Document.docx" +``` + +Retrieves the file and converts to PDF, and outputs its content to the console as a Memory Stream + +### EXAMPLE 3 +```powershell +Convert-PnPFileToPdf -Url "/sites/project/Shared Documents/Document.docx" -Path "C:\Temp" +``` + +Retrieves the file and converts to PDF, and save it to the given local path + +### EXAMPLE 4 +```powershell +Convert-PnPFileToPdf -Url "/sites/project/Shared Documents/Document.docx" -Path "C:\Temp" -Force +``` + +Retrieves the file and converts to PDF, and save it to the given local path. Force parameter will override the existing file in the location where the document gets saved. + +### EXAMPLE 5 +```powershell +Convert-PnPFileToPdf -Url "/sites/SampleTeamSite/Shared Documents/Nishkalank's/Book.xlsx.docx" -DocumentLibrary "Archive" +``` + +Retrieves the file and converts to PDF, and save it to the given Document library (Folder) in SharePoint Online (Same SiteCollection). Returns the saved file information in the console. + +### EXAMPLE 6 +```powershell +Convert-PnPFileToPdf -Url "/sites/SampleTeamSite/Shared Documents/Nishkalank's/Presentation.pptx" -DocumentLibrary "Shared Documents" -CheckOut -CheckInType 1 -CheckInComment "Check-in via PowerShell" -Publish -PublishComment "Published via PowerShell" -Approve -ApproveComment "Approved via PowerShell" +``` + +Retrieves the file and converts to PDF, and save it to the given Document library in SharePoint Online (Same SiteCollection). Returns the saved file information in the console. +Note : CheckOut, CheckInType, CheckInComment, Publish, PublishComment, Approve and ApproveComment only works with the document libraries where Versioning is enabled. + + +## PARAMETERS + +### -Url +The URL (server or site relative) to the file + +```yaml +Type: String +Parameter Sets: (All) +Aliases: ServerRelativeUrl, SiteRelativeUrl + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + + +### -AsMemoryStream + +```yaml +Type: SwitchParameter +Parameter Sets: Return as memorystream + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +Local path where the file should be saved + +```yaml +Type: String +Parameter Sets: Save to local path + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + + +### -Force +Overwrites the file if it exists. + +```yaml +Type: SwitchParameter +Parameter Sets: Save to local path + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DocumentLibrary +The destination library in the site + +```yaml +Type: FolderPipeBind +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CheckInComment +The comment added to the checkin + +```yaml +Type: String +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CheckinType +Specifies the type of check-in for a file. + +```yaml +Type: Enum (Microsoft.SharePoint.Client.CheckinType) +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: MinorCheckIn +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Checkout +If versioning is enabled, this will check out the file first if it exists, upload the file, then check it in again + +```yaml +Type: SwitchParameter +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Publish +Will auto publish the file + +```yaml +Type: SwitchParameter +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PublishComment +The comment added to the publish action + +```yaml +Type: String +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Approve +Will auto approve the uploaded file + +```yaml +Type: SwitchParameter +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ApproveComment +The comment added to the approval + +```yaml +Type: String +Parameter Sets: (UPLOADTOSHAREPOINT) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/src/Commands/Files/ConvertFile.cs b/src/Commands/Files/ConvertFile.cs new file mode 100644 index 000000000..9eae84771 --- /dev/null +++ b/src/Commands/Files/ConvertFile.cs @@ -0,0 +1,185 @@ +using Microsoft.SharePoint.Client; +using PnP.Framework.Utilities; +using System.Management.Automation; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Utilities.REST; +using System.IO; +using PnP.PowerShell.Commands.Base.PipeBinds; + +namespace PnP.PowerShell.Commands.Files +{ + [Cmdlet(VerbsData.Convert, "PnPFileToPdf")] + public class ConvertFile : PnPWebCmdlet + { + private const string URLTOPATH = "Save to local path"; + private const string URLASMEMORYSTREAM = "Return as memorystream"; + private const string UPLOADTOSHAREPOINT = "Upload to SharePoint"; + + [Parameter(Mandatory = true, ParameterSetName = URLTOPATH, Position = 0, ValueFromPipeline = true)] + [Parameter(Mandatory = true, ParameterSetName = URLASMEMORYSTREAM, Position = 0, ValueFromPipeline = true)] + [Parameter(Mandatory = true, ParameterSetName = UPLOADTOSHAREPOINT, Position = 0, ValueFromPipeline = true)] + [Alias("ServerRelativeUrl", "SiteRelativeUrl")] + public string Url; + + [Parameter(Mandatory = true, ParameterSetName = URLTOPATH)] + [Alias("Local path")] + public string Path = string.Empty; + + [Parameter(Mandatory = false, ParameterSetName = URLTOPATH)] + public SwitchParameter Force; + + [Parameter(Mandatory = true, ParameterSetName = UPLOADTOSHAREPOINT)] + [ValidateNotNullOrEmpty] + public FolderPipeBind DocumentLibrary; + + [Parameter(Mandatory = false, ParameterSetName = UPLOADTOSHAREPOINT)] + public SwitchParameter Checkout; + + [Parameter(Mandatory = false, ParameterSetName = UPLOADTOSHAREPOINT)] + public string CheckInComment = string.Empty; + + [Parameter(Mandatory = false, ParameterSetName = UPLOADTOSHAREPOINT)] + public CheckinType CheckinType = CheckinType.MinorCheckIn; + + [Parameter(Mandatory = false, ParameterSetName = UPLOADTOSHAREPOINT)] + public SwitchParameter Approve; + + [Parameter(Mandatory = false, ParameterSetName = UPLOADTOSHAREPOINT)] + public string ApproveComment = string.Empty; + + [Parameter(Mandatory = false, ParameterSetName = UPLOADTOSHAREPOINT)] + public SwitchParameter Publish; + + [Parameter(Mandatory = false)] + public string PublishComment = string.Empty; + + [Parameter(Mandatory = false, ParameterSetName = URLASMEMORYSTREAM)] + public SwitchParameter AsMemoryStream; + + + + protected override void ExecuteCmdlet() + { + if (string.IsNullOrEmpty(Path)) + { + Path = SessionState.Path.CurrentFileSystemLocation.Path; + } + else if (!System.IO.Path.IsPathRooted(Path)) + { + Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path); + } + + var graphAccessToken = TokenHandler.GetAccessToken(this, $"https://{Connection.GraphEndPoint}/.default", Connection); + Url = Utilities.UrlUtilities.UrlDecode(Url.Replace("+", "%2B")); + var serverRelativeUrl = GetServerRelativeUrl(Url); + var fileObj = GetFileByServerRelativePath(serverRelativeUrl); + var siteId = PnPContext.Site.Id.ToString(); + var listId = fileObj.ListId.ToString(); + var itemId = fileObj.ListItemAllFields.Id.ToString(); + var sourceFileName = fileObj.Name.ToString().Substring(0, fileObj.Name.ToString().LastIndexOf(".")); + var apiUrl = $"https://{Connection.GraphEndPoint}/v1.0/sites/{siteId}/lists/{listId}/items/{itemId}/driveItem/content?format=pdf"; + byte[] response = RestHelper.GetByteArrayAsync(Connection.HttpClient, apiUrl, graphAccessToken).GetAwaiter().GetResult(); + var fileToDownloadName = !string.IsNullOrEmpty(sourceFileName) ? sourceFileName : "Download"; + + switch (ParameterSetName) + { + case URLTOPATH: + var fileOut = System.IO.Path.Combine(Path, $"{fileToDownloadName}.pdf"); + if (!Directory.Exists(Path)) + { + Directory.CreateDirectory(Path); + } + if (!Force && System.IO.File.Exists(fileOut)) + { + WriteWarning($"File '{fileToDownloadName}' exists already. Use the -Force parameter to overwrite the file."); + } + else + { + System.IO.File.WriteAllBytes(fileOut, response); + WriteObject($"File saved as {fileOut}"); + } + break; + + case URLASMEMORYSTREAM: + var stream = new MemoryStream(response); + WriteObject(stream); + break; + + case UPLOADTOSHAREPOINT: + var targetLibrary = EnsureFolder(); + Stream fileStream = new MemoryStream(response); + var targetFileName = $"{fileToDownloadName}.pdf"; + var uploadedFile = targetLibrary.UploadFileAsync(targetFileName, fileStream, true).GetAwaiter().GetResult(); + if (Checkout || ParameterSpecified(nameof(CheckinType))) + { + CurrentWeb.CheckInFile(uploadedFile.ServerRelativeUrl, CheckinType, CheckInComment); + } + if (Publish) + { + CurrentWeb.PublishFile(uploadedFile.ServerRelativeUrl, PublishComment); + } + if (Approve) + { + CurrentWeb.ApproveFile(uploadedFile.ServerRelativeUrl, ApproveComment); + } + ClientContext.Load(uploadedFile); + try + { + ClientContext.ExecuteQueryRetry(); + } + catch (ServerException) + { + ClientContext.Load(uploadedFile, f => f.Length, f => f.Name, f => f.TimeCreated, f => f.TimeLastModified, f => f.Title); + ClientContext.ExecuteQueryRetry(); + } + WriteObject(uploadedFile); + break; + } + } + + private string GetServerRelativeUrl(string url) + { + var webUrl = CurrentWeb.EnsureProperty(w => w.ServerRelativeUrl); + return url.ToLower().StartsWith(webUrl.ToLower()) ? url : UrlUtility.Combine(webUrl, Url); + } + + private Microsoft.SharePoint.Client.File GetFileByServerRelativePath(string serverRelativeUrl) + { + var fileListItem = CurrentWeb.GetFileByServerRelativePath(ResourcePath.FromDecodedUrl(serverRelativeUrl)); + ClientContext.Load(fileListItem, f => f.Exists, f => f.ListItemAllFields, f => f.ListId, f => f.Name); + ClientContext.ExecuteQueryRetry(); + if (fileListItem.Exists) + { + return fileListItem; + } + else + { + throw new PSArgumentException($"No file found with the provided Url {serverRelativeUrl}", "Url"); + } + } + + + private Folder EnsureFolder() + { + // First try to get the folder if it exists already. This avoids an Access Denied exception if the current user doesn't have Full Control access at Web level + CurrentWeb.EnsureProperty(w => w.ServerRelativeUrl); + + Folder library = null; + try + { + library = DocumentLibrary.GetFolder(CurrentWeb); + library.EnsureProperties(f => f.ServerRelativeUrl); + return library; + } + // Exception will be thrown if the library does not exist yet on SharePoint + catch (ServerException serverEx) when (serverEx.ServerErrorCode == -2147024894) + { + // create the library + CurrentWeb.CreateList(ListTemplateType.DocumentLibrary, DocumentLibrary.ServerRelativeUrl, false, true, "", false, false); + library = DocumentLibrary.GetFolder(CurrentWeb); + library.EnsureProperties(f => f.ServerRelativeUrl); + return library; + } + } + } +} diff --git a/src/Commands/Utilities/REST/RestHelper.cs b/src/Commands/Utilities/REST/RestHelper.cs index b159e78df..c340be8ee 100644 --- a/src/Commands/Utilities/REST/RestHelper.cs +++ b/src/Commands/Utilities/REST/RestHelper.cs @@ -153,6 +153,12 @@ public static async Task GetAsync(HttpClient httpClient, string url, str return await SendMessageAsync(httpClient, message); } + public static async Task GetByteArrayAsync(HttpClient httpClient, string url, string accessToken, string accept = "application/json") + { + var message = GetMessage(url, HttpMethod.Get, accessToken, accept); + return await SendMessageByteArrayAsync(httpClient, message); + } + public static async Task GetAsync(HttpClient httpClient, string url, ClientContext clientContext, string accept = "application/json") { var message = GetMessage(url, HttpMethod.Get, clientContext, accept); @@ -649,6 +655,27 @@ private static async Task SendMessageAsync(HttpClient httpClient, HttpRe } } + private static async Task SendMessageByteArrayAsync(HttpClient httpClient, HttpRequestMessage message) + { + var response = await httpClient.SendAsync(message); + while (response.StatusCode == (HttpStatusCode)429) + { + // throttled + var retryAfter = response.Headers.RetryAfter; + await Task.Delay(retryAfter.Delta.Value.Seconds * 1000); + response = await httpClient.SendAsync(CloneMessage(message)); + } + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadAsByteArrayAsync(); + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new HttpRequestException($"HTTP Error {response.StatusCode}: {errorContent}"); + } + } + private static async Task SendMessageAsyncGetResponseHeader(HttpClient httpClient, HttpRequestMessage message) { var response = await httpClient.SendAsync(message);