Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Command- Convert-PnPFileToPdf #3435

Merged
merged 8 commits into from
Dec 22, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added `Get-PnPFlowOwner` cmdlet which allows retrieving the owners of a Power Automate flow [#3314](https://github.com/pnp/powershell/pull/3314)
- Added `-AvailableForTagging` to `Set-PnPTerm` which allows the available for tagging property on a Term to be set [#3321](https://github.com/pnp/powershell/pull/3321)
- Added `Get-PnPPowerPlatformConnector` cmdlet which allows for all custom connectors to be retrieved [#3309](https://github.com/pnp/powershell/pull/3309)
- Added `Convert-PnPFileToPdf` cmdlet which allows for a file to be converted to PDF [#3435](https://github.com/pnp/powershell/pull/3435)
- Added `Set-PnPSearchExternalItem` cmdlet which allows ingesting external items into the Microsoft Search index for custom connectors. [#3420](https://github.com/pnp/powershell/pull/3420)
- Added `Get-PnPTenantInfo` which allows retrieving tenant information by its Id or domain name [#3414](https://github.com/pnp/powershell/pull/3414)
- Added option to create a Microsoft 365 Group with dynamic membership by passing in `-DynamicMembershipRule` [#3426](https://github.com/pnp/powershell/pull/3426)
Expand All @@ -65,6 +66,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added the ability to set Manage and FullControl permissions directly when using Sites.Selected with `Grant-PnPAzureADAppSitePermission` [#3617](https://github.com/pnp/powershell/pull/3617)
- Added `Remove-PnPMicrosoft365GroupPhoto` cmdlet which allows removal of profile picture of M365 Group. [#3607](https://github.com/pnp/powershell/pull/3607)


### Fixed

- Fixed `Add-PnPContentTypeToList` cmdlet to better handle piped lists. [#3244](https://github.com/pnp/powershell/pull/3244)
Expand Down
151 changes: 151 additions & 0 deletions documentation/Convert-PnPFileToPdf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
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 <String> -Path <String> [-Force]
```

### Return as memorystream
```powershell
Convert-PnPFileToPDF -Url <String> -AsMemoryStream
```

### Save to SharePoint Online (Same SiteCollection)
```powershell
Convert-PnPFileToPDF -Url <String> -Folder <String>
```

## 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" -Folder "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.



## 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
```

### -Folder
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
```

## RELATED LINKS

[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp)
153 changes: 153 additions & 0 deletions src/Commands/Files/ConvertFileToPdf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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 ConvertFileToPDF : 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)]
public string Path = string.Empty;

[Parameter(Mandatory = false, ParameterSetName = URLTOPATH)]
public SwitchParameter Force;

[Parameter(Mandatory = true, ParameterSetName = UPLOADTOSHAREPOINT)]
[ValidateNotNullOrEmpty]
public FolderPipeBind Folder;

[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);
}

Url = Utilities.UrlUtilities.UrlDecode(Url.Replace("+", "%2B"));
var serverRelativeUrl = GetServerRelativeUrl(Url);
var fileObj = GetFileByServerRelativePath(serverRelativeUrl);
var sourceFileName = fileObj.Name.ToString().Substring(0, fileObj.Name.ToString().LastIndexOf("."));
var apiUrl = GeneratePdfApiUrl(Url, sourceFileName, fileObj);
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))
{
throw new PSArgumentException("Path does not exists");
}
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();
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 string GeneratePdfApiUrl(string url, string sourceFileName, Microsoft.SharePoint.Client.File fileObj)
{
var siteId = PnPContext.Site.Id.ToString();
var listId = fileObj.ListId.ToString();
var itemId = fileObj.ListItemAllFields.Id.ToString();
return $"https://{Connection.GraphEndPoint}/v1.0/sites/{siteId}/lists/{listId}/items/{itemId}/driveItem/content?format=pdf";
}


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 = Folder.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, Folder.ServerRelativeUrl, false, true, "", false, false);
library = Folder.GetFolder(CurrentWeb);
library.EnsureProperties(f => f.ServerRelativeUrl);
return library;
}
}
}
}
27 changes: 27 additions & 0 deletions src/Commands/Utilities/REST/RestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ public static async Task<string> GetAsync(HttpClient httpClient, string url, str
return await SendMessageAsync(httpClient, message);
}

public static async Task<byte[]> 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<string> GetAsync(HttpClient httpClient, string url, ClientContext clientContext, string accept = "application/json")
{
var message = GetMessage(url, HttpMethod.Get, clientContext, accept);
Expand Down Expand Up @@ -649,6 +655,27 @@ private static async Task<string> SendMessageAsync(HttpClient httpClient, HttpRe
}
}

private static async Task<byte[]> 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<HttpResponseHeaders> SendMessageAsyncGetResponseHeader(HttpClient httpClient, HttpRequestMessage message)
{
var response = await httpClient.SendAsync(message);
Expand Down
Loading