Skip to content

Added -Stream parameter to Invoke-PnPSiteTemplate which allows an in memory .pnp site template to be applied to a site #4845

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

Merged
merged 2 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added `Get-PnPBrandCenterFontPackage` to retrieve the available font packages from the various Brand Centers [#4830](https://github.com/pnp/powershell/pull/4830)
- Added `Use-PnPBrandCenterFontPackage` to apply the specified font package from the Brand Center to the current site [#4830](https://github.com/pnp/powershell/pull/4830)
- Added `Add-PnPBrandCenterFont` to upload a font to the tenant Brand Center [#4830](https://github.com/pnp/powershell/pull/4830)
- Added `-Stream` parameter to `Invoke-PnPSiteTemplate` which allows an in memory .pnp site template to be applied to a site [#4845](https://github.com/pnp/powershell/pull/4845)

### Changed

Expand Down
38 changes: 35 additions & 3 deletions documentation/Invoke-PnPSiteTemplate.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Applies a site template to a web

### Path
```powershell
Invoke-PnPSiteTemplate [-Path] <String> [-TemplateId <String>] [-ResourceFolder <String>]
Invoke-PnPSiteTemplate -Path <String> [-TemplateId <String>] [-ResourceFolder <String>]
[-OverwriteSystemPropertyBagValues] [-IgnoreDuplicateDataRowErrors] [-ProvisionContentTypesToSubWebs]
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
Expand All @@ -26,11 +26,21 @@ Invoke-PnPSiteTemplate [-Path] <String> [-TemplateId <String>] [-ResourceFolder

### Instance
```powershell
Invoke-PnPSiteTemplate [-TemplateId <String>] [-ResourceFolder <String>]
Invoke-PnPSiteTemplate -InputInstance <SiteTemplate> [-TemplateId <String>] [-ResourceFolder <String>]
[-OverwriteSystemPropertyBagValues] [-IgnoreDuplicateDataRowErrors] [-ProvisionContentTypesToSubWebs]
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
[-TemplateProviderExtensions <ITemplateProviderExtension[]>] [-InputInstance <SiteTemplate>]
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
[-Connection <PnPConnection>]
```

### Stream
```powershell
Invoke-PnPSiteTemplate -Stream <Stream> [-TemplateId <String>] [-ResourceFolder <String>]
[-OverwriteSystemPropertyBagValues] [-IgnoreDuplicateDataRowErrors] [-ProvisionContentTypesToSubWebs]
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
[-Connection <PnPConnection>]
```

Expand Down Expand Up @@ -107,6 +117,14 @@ Invoke-PnPSiteTemplate -Path .\template.xml -TemplateId "MyTemplate"

Applies the SiteTemplate with the ID "MyTemplate" located in the template definition file template.xml.

### EXAMPLE 10
```powershell
$stream = Get-PnPFile -Url https://tenant.sharepoint.com/sites/TemplateGallery/Shared%20Documents/ProjectSite.pnp -AsMemoryStream
Invoke-PnPSiteTemplate -Stream $stream
```

Downloads the ProjectSite.pnp template from the TemplateGallery document library and applies it to the currently connected to site.

## PARAMETERS

### -ClearNavigation
Expand Down Expand Up @@ -293,6 +311,20 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -Stream
Allows a stream to be passed in. This is useful when you want to apply a template that is stored in a stream, for example when you download it from SharePoint Online so you can keep it in memory and don't (temporarily) need to store it anywhere. It only supports .pnp files, not .xml files. It also supports having additional files stored in the .pnp file.

```yaml
Type: Stream
Parameter Sets: Stream

Required: True
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```

### -TemplateId
ID of the template to use from the xml file containing the provisioning template. If not specified and multiple SiteTemplate elements exist, the last one will be used.

Expand Down
83 changes: 69 additions & 14 deletions src/Commands/Provisioning/Site/InvokeSiteTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public class InvokeSiteTemplate : PnPWebCmdlet

[Parameter(Mandatory = false, ParameterSetName = "Instance")]
public ProvisioningTemplate InputInstance;

[Parameter(Mandatory = false, ParameterSetName = "Stream")]
public MemoryStream Stream { get; set; }

protected override void ExecuteCmdlet()
{
Expand Down Expand Up @@ -173,29 +176,75 @@ protected override void ExecuteCmdlet()
}
else
{
provisioningTemplate = InputInstance;

if (ResourceFolder != null)
if (InputInstance == null && Stream != null)
{
var fileSystemConnector = new FileSystemConnector(ResourceFolder, "");
provisioningTemplate.Connector = fileSystemConnector;
LogDebug("Determining if template from provided stream is a .pnp package file");
Stream.Position = 0;

var isOpenOfficeFile = FileUtilities.IsOpenOfficeFile(Stream);
if (isOpenOfficeFile)
{
LogDebug("Package is a .pnp package file, loading template from provided stream");
var openXmlConnector = new OpenXMLConnector(Stream);
var provider = new XMLOpenXMLTemplateProvider(openXmlConnector);

var templates = ProvisioningHelper.LoadSiteTemplatesFromStream(Stream, TemplateProviderExtensions, LogError);

LogDebug($"Package stream contains {templates.Count} template{(templates.Count != 1 ? "s" : "")}");

if (ParameterSpecified(nameof(TemplateId)))
{
// If we have the -TemplateId parameter, we look for the template with that ID in the package stream
LogDebug($"Looking for template with ID {TemplateId} in package stream");
provisioningTemplate = templates.FirstOrDefault(t => t.Id == TemplateId);
}
else
{
// If we don't have the -TemplateId parameter, we use the last template in the package stream
LogDebug($"No template ID specified, using the last template in package stream");
provisioningTemplate = templates.LastOrDefault();
}
if (provisioningTemplate == null)
{
// If we don't have a template, raise an error and exit
LogError("Unable to find template in provided stream. Please check the template ID or the content of the stream.");
return;
}
provisioningTemplate.Connector = provider.Connector;
}
else
{
// XML template files have not been implemented to be used from a stream, only .pnp files
throw new NotSupportedException("Only .pnp package files are supported in a stream");
}
}
else
{
if (Path != null)
provisioningTemplate = InputInstance;

if (ResourceFolder != null)
{
if (!System.IO.Path.IsPathRooted(Path))
{
Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path);
}
var fileSystemConnector = new FileSystemConnector(ResourceFolder, "");
provisioningTemplate.Connector = fileSystemConnector;
}
else
{
Path = SessionState.Path.CurrentFileSystemLocation.Path;
if (Path != null)
{
if (!System.IO.Path.IsPathRooted(Path))
{
Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path);
}

var fileInfo = new FileInfo(Path);
fileConnector = new FileSystemConnector(System.IO.Path.IsPathRooted(fileInfo.FullName) ? fileInfo.FullName : fileInfo.DirectoryName, "");
provisioningTemplate.Connector = fileConnector;
}
else
{
Path = SessionState.Path.CurrentFileSystemLocation.Path;
}
}
var fileInfo = new FileInfo(Path);
fileConnector = new FileSystemConnector(System.IO.Path.IsPathRooted(fileInfo.FullName) ? fileInfo.FullName : fileInfo.DirectoryName, "");
provisioningTemplate.Connector = fileConnector;
}
}

Expand Down Expand Up @@ -327,6 +376,12 @@ protected override void ExecuteCmdlet()
}

WriteProgress(new ProgressRecord(0, $"Applying template to {CurrentWeb.Url}", " ") { RecordType = ProgressRecordType.Completed });

if(Stream != null)
{
// Reset the stream position to 0 so it can be used again if needed
Stream.Position = 0;
}
}
}
}
Loading