Skip to content
This repository was archived by the owner on Jan 19, 2021. It is now read-only.

Feature : Add-PnPFile(s)ToProvisioningTemplate extract web part #1652

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
33e38b4
Added SourceUrl parameter to AddFileToProvisioningTemplate
stevebeauge Feb 5, 2018
d2f7151
Improved reliability
stevebeauge Feb 5, 2018
ae97f84
Added `SourceUrl` parameter to `Add-PnPFileToProvisioningTemplate` co…
stevebeauge Jul 12, 2018
e8d7a40
Overwrite any existing file in the template
stevebeauge Jul 12, 2018
115d695
Overwrite any existing file in the template
stevebeauge Jul 12, 2018
afa34dc
Merge branch 'feature/add-pnpfiletoprovisionningtemplate-support-remo…
stevebeauge Jul 12, 2018
87cb166
Support adding a folder to a template from the connected web
stevebeauge Jul 12, 2018
a539fe0
Support for local folder
stevebeauge Jul 12, 2018
1fc8b97
Refactoring to increase code quality and code reuse
stevebeauge Jul 13, 2018
90d2bb3
Implemented the Recurse switch
stevebeauge Jul 13, 2018
4a8e235
Few comments and documentation addition
stevebeauge Jul 13, 2018
a4ed0c8
Fix a property not loaded exception
stevebeauge Jul 13, 2018
c85d848
Add support for webpart extraction
stevebeauge Jul 16, 2018
55aea1e
Minor quality improvements
stevebeauge Jul 16, 2018
53ff68c
Minor quality improvements
stevebeauge Jul 16, 2018
5a87f12
Merge branch 'feature/Add-PnPFile(s)ToProvisioningTemplate-ExtractWeb…
stevebeauge Jul 16, 2018
59b202b
Fix ParameterSetName glitch
stevebeauge Jul 16, 2018
cda6e93
Added log and progression output
stevebeauge Jul 16, 2018
8bde7fb
Add support for extracting file properties when dealing with aspx files
stevebeauge Jul 17, 2018
58c1305
Fix broken loop
stevebeauge Jul 18, 2018
0ef9e26
Merge branch 'feature/Add-PnPFile(s)ToProvisioningTemplate-ExtractWeb…
stevebeauge Jul 18, 2018
64f7bf6
Merge branch 'dev' into feature/Add-PnPFile(s)ToProvisioningTemplate-…
stevebeauge Aug 27, 2018
bf1d134
Feature: Add-PnPFileToProvisionningTemplate allow add a file from an url
stevebeauge Aug 27, 2018
41b9ee1
Feature: new command Add-PnPFilesToProvisioningTemplate
stevebeauge Aug 27, 2018
5ab7d38
Feature : Add-PnPFile(s)ToProvisioningTemplate extract web part
stevebeauge Aug 27, 2018
0aaf227
Merge branch 'dev' into faeture/new-command-add-pnpfilestoprovisionni…
stevebeauge Sep 20, 2018
770fe92
Merge branch 'dev' into feature/add-pnpfiletoprovisionningtemplate-su…
stevebeauge Sep 20, 2018
8cb5bc9
Merge branch 'dev' into feature/Add-PnPFile(s)ToProvisioningTemplate-…
stevebeauge Sep 20, 2018
514842d
Merge branch 'dev' into feature/Add-PnPFile(s)ToProvisioningTemplate-…
stevebeauge Sep 20, 2018
70b8a7a
Merge branch 'dev' into feature/add-pnpfiletoprovisionningtemplate-su…
stevebeauge Nov 5, 2018
bba6754
Merged latest from GH repo
stevebeauge Nov 5, 2018
4bd3ac8
Merged latest from GH repo
stevebeauge Nov 5, 2018
67e8045
Merged latest
stevebeauge Nov 5, 2018
d1327bd
Include latest from GHRepo
stevebeauge Nov 5, 2018
267bfcd
Merge branch 'dev' into feature/add-pnpfiletoprovisionningtemplate-su…
stevebeauge Nov 16, 2018
55d4737
Include latest additions from GH repo
stevebeauge Nov 16, 2018
bae61f7
Merge latest from GH Repo
stevebeauge Nov 16, 2018
5fc0b58
Fix bug in tokenize method
stevebeauge Nov 16, 2018
dde0db8
Include latest from GH Repo
stevebeauge Nov 16, 2018
6c12549
Merge branch 'dev' into feature/add-pnpfiletoprovisionningtemplate-su…
stevebeauge Jan 4, 2019
5ef2756
Included latest additions from GH repo
stevebeauge Jan 4, 2019
23e7382
Included latest additions from GH repo
stevebeauge Jan 4, 2019
305a99a
Merge branch 'dev' into feature/add-pnpfiletoprovisionningtemplate-su…
stevebeauge Feb 18, 2019
31a15d8
Merge branch 'feature/add-pnpfiletoprovisionningtemplate-support-remo…
stevebeauge Feb 18, 2019
21ac508
Merge branch 'feature/Add-PnPFile(s)ToProvisioningTemplate-ExtractWeb…
stevebeauge Feb 18, 2019
edec55e
Fix incorrect tokenization
stevebeauge Feb 18, 2019
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
304 changes: 304 additions & 0 deletions Commands/Base/BaseFileProvisioningCmdlet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.Utilities;
using OfficeDevPnP.Core.Framework.Provisioning.Connectors;
using OfficeDevPnP.Core.Framework.Provisioning.Model;
using OfficeDevPnP.Core.Framework.Provisioning.Providers;
using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml;
using SharePointPnP.PowerShell.Commands.Provisioning;
using SharePointPnP.PowerShell.Commands.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text;
using PnPFileLevel = OfficeDevPnP.Core.Framework.Provisioning.Model.FileLevel;
using SPFile = Microsoft.SharePoint.Client.File;

namespace SharePointPnP.PowerShell.Commands
{
/// <summary>
/// Base class for commands related to adding file to template
/// </summary>
public class BaseFileProvisioningCmdlet : PnPWebCmdlet
{
protected const string PSNAME_LOCAL_SOURCE = "LocalSourceFile";
protected const string PSNAME_REMOTE_SOURCE = "RemoteSourceFile";

[Parameter(Mandatory = true, Position = 0, HelpMessage = "Filename of the .PNP Open XML provisioning template to read from, optionally including full path.")]
public string Path;

[Parameter(Mandatory = false, Position = 3, HelpMessage = "The target Container for the file to add to the in-memory template, optional argument.")]
public string Container;

[Parameter(Mandatory = false, Position = 4, HelpMessage = "The level of the files to add. Defaults to Published")]
public PnPFileLevel FileLevel = PnPFileLevel.Published;

[Parameter(Mandatory = false, Position = 5, HelpMessage = "Set to overwrite in site, Defaults to true")]
public SwitchParameter FileOverwrite = true;

[Parameter(Mandatory = false, Position = 6, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "Include webparts when the file is a page")]
public SwitchParameter ExtractWebParts = true;

[Parameter(Mandatory = false, Position = 7, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "Include webparts when the file is a page")]
public SwitchParameter ExtractFileProperties = true;

[Parameter(Mandatory = false, Position = 8, HelpMessage = "Allows you to specify ITemplateProviderExtension to execute while loading the template.")]
public ITemplateProviderExtension[] TemplateProviderExtensions;

protected readonly ProgressRecord _progressEnumeration = new ProgressRecord(0, "Activity", "Status") { Activity = "Enumerating folder" };
protected readonly ProgressRecord _progressFilesEnumeration = new ProgressRecord(1, "Activity", "Status") { Activity = "Extracting files" };
protected readonly ProgressRecord _progressFileProcessing = new ProgressRecord(2, "Activity", "Status") { Activity = "Extracting file" };

protected override void ProcessRecord()
{
base.ProcessRecord();
var ctx = (ClientContext)SelectedWeb.Context;
ctx.Load(SelectedWeb, web => web.Id, web => web.ServerRelativeUrl, web => web.Url);
if (ExtractWebParts)
{
ctx.Load(ctx.Site, site => site.Id, site => site.ServerRelativeUrl, site => site.Url);
ctx.Load(SelectedWeb.Lists, lists => lists.Include(l => l.Title, l => l.RootFolder.ServerRelativeUrl, l => l.Id));
}
ctx.ExecuteQueryRetry();
}

protected ProvisioningTemplate LoadTemplate()
{
if (!System.IO.Path.IsPathRooted(Path))
{
Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path);
}
// Load the template
var template = ReadProvisioningTemplate
.LoadProvisioningTemplateFromFile(Path,
TemplateProviderExtensions);

if (template == null)
{
throw new ApplicationException("Invalid template file!");
}

return template;
}

/// <summary>
/// Add a file to the template
/// </summary>
/// <param name="template">The provisioning template to add the file to</param>
/// <param name="fs">Stream to read the file content</param>
/// <param name="folder">target folder in the provisioning template</param>
/// <param name="fileName">Name of the file</param>
/// <param name="container">Container path within the template (pnp file) or related to the xml templage</param>
/// <param name="webParts">WebParts to include</param>
/// <param name="properties">Properties of the file</param>
protected void AddFileToTemplate(
ProvisioningTemplate template,
Stream fs,
string folder,
string fileName,
string container,
IEnumerable<WebPart> webParts = null,
IDictionary<string, string> properties = null
)
{
if (template == null) throw new ArgumentNullException(nameof(template));
if (fs == null) throw new ArgumentNullException(nameof(fs));
if (fileName == null) throw new ArgumentNullException(nameof(fileName));

var source = !string.IsNullOrEmpty(container) ? (container + "/" + fileName) : fileName;

template.Connector.SaveFileStream(fileName, container, fs);

if (template.Connector is ICommitableFileConnector)
{
((ICommitableFileConnector)template.Connector).Commit();
}

var existing = template.Files.FirstOrDefault(f => f.Src == $"{container}/{fileName}" && f.Folder == folder);

if (existing != null)
template.Files.Remove(existing);

var newFile = new OfficeDevPnP.Core.Framework.Provisioning.Model.File
{
Src = source,
Folder = folder,
Level = FileLevel,
Overwrite = FileOverwrite
};

if (webParts != null) newFile.WebParts.AddRange(webParts);
if (properties != null)
{
foreach (var property in properties)
{
newFile.Properties.Add(property.Key, property.Value);
}
}
template.Files.Add(newFile);

// Determine the output file name and path
var outFileName = System.IO.Path.GetFileName(Path);
var outPath = new FileInfo(Path).DirectoryName;

var fileSystemConnector = new FileSystemConnector(outPath, "");
var formatter = XMLPnPSchemaFormatter.LatestFormatter;
var extension = new FileInfo(Path).Extension.ToLowerInvariant();
if (extension == ".pnp")
{
var provider = new XMLOpenXMLTemplateProvider(template.Connector as OpenXMLConnector);
var templateFileName = outFileName.Substring(0, outFileName.LastIndexOf(".", StringComparison.Ordinal)) + ".xml";

provider.SaveAs(template, templateFileName, formatter, TemplateProviderExtensions);
}
else
{
XMLTemplateProvider provider = new XMLFileSystemTemplateProvider(Path, "");
provider.SaveAs(template, Path, formatter, TemplateProviderExtensions);
}
}

/// <summary>
/// Adds a remote file to a template
/// </summary>
/// <param name="template">Template to add the file to</param>
/// <param name="file">The SharePoint file to retrieve and add</param>
protected void AddSPFileToTemplate(ProvisioningTemplate template, SPFile file)
{
if (template == null) throw new ArgumentNullException(nameof(template));
if (file == null) throw new ArgumentNullException(nameof(file));

file.EnsureProperties(f => f.Name, f => f.ServerRelativeUrl);

_progressFileProcessing.StatusDescription = $"Extracting file {file.ServerRelativeUrl}";
var folderRelativeUrl = file.ServerRelativeUrl.Substring(0, file.ServerRelativeUrl.Length - file.Name.Length - 1);
var folderWebRelativeUrl = HttpUtility.UrlKeyValueDecode(folderRelativeUrl.Substring(SelectedWeb.ServerRelativeUrl.TrimEnd('/').Length + 1));
if (ClientContext.HasPendingRequest) ClientContext.ExecuteQuery();
try
{
IEnumerable<WebPart> webParts = null;
if (ExtractWebParts)
{
webParts = ExtractSPFileWebParts(file).ToArray();
_progressFileProcessing.PercentComplete = 25;
_progressFileProcessing.StatusDescription = $"Extracting webpart from {file.ServerRelativeUrl} ";
WriteProgress(_progressFileProcessing);
}

using (var fi = SPFile.OpenBinaryDirect(ClientContext, file.ServerRelativeUrl))
using (var ms = new MemoryStream())
{
_progressFileProcessing.PercentComplete = 50;
_progressFileProcessing.StatusDescription = $"Reading file {file.ServerRelativeUrl}";
WriteProgress(_progressFileProcessing);
// We are using a temporary memory stream because the file connector is seeking in the stream
// and the stream provided by OpenBinaryDirect does not allow it
fi.Stream.CopyTo(ms);
ms.Position = 0;
IDictionary<string, string> properties = null;
if (ExtractFileProperties && string.Compare(System.IO.Path.GetExtension(file.Name), ".aspx", true) == 0)
{
_progressFileProcessing.PercentComplete = 35;
_progressFileProcessing.StatusDescription = $"Extracting properties from {file.ServerRelativeUrl}";
properties = XmlPageDataHelper.ExtractProperties(
Encoding.UTF8.GetString(ms.ToArray())
).ToDictionary(p => p.Key, p => Tokenize(p.Value));
}
AddFileToTemplate(template, ms, folderWebRelativeUrl, file.Name, folderWebRelativeUrl, webParts, properties);
_progressFileProcessing.PercentComplete = 100;
_progressFileProcessing.StatusDescription = $"Adding file {file.ServerRelativeUrl} to template";
_progressFileProcessing.RecordType = ProgressRecordType.Completed;
WriteProgress(_progressFileProcessing);
}
}
catch (Exception exc)
{
WriteWarning($"Error trying to add file {file.ServerRelativeUrl} : {exc.Message}");
}
}

private IEnumerable<WebPart> ExtractSPFileWebParts(SPFile file)
{
if (file == null) throw new ArgumentNullException(nameof(file));

if (string.Compare(System.IO.Path.GetExtension(file.Name), ".aspx", true) == 0)
{
foreach (var spwp in SelectedWeb.GetWebParts(file.ServerRelativeUrl))
{
spwp.EnsureProperties(wp => wp.WebPart
#if !SP2016 // Missing ZoneId property in SP2016 version of the CSOM Library
, wp => wp.ZoneId
#endif
);
yield return new WebPart
{
Contents = Tokenize(SelectedWeb.GetWebPartXml(spwp.Id, file.ServerRelativeUrl)),
Order = (uint)spwp.WebPart.ZoneIndex,
Title = spwp.WebPart.Title,
#if !SP2016 // Missing ZoneId property in SP2016 version of the CSOM Library
Zone = spwp.ZoneId
#endif
};
}
}
}

/// <summary>
/// Adds a local file to a template
/// </summary>
/// <param name="template">Template to add the file to</param>
/// <param name="file">Full path to a local file</param>
/// <param name="folder">Destination folder of the added file</param>
protected void AddLocalFileToTemplate(ProvisioningTemplate template, string file, string folder)
{
if (template == null) throw new ArgumentNullException(nameof(template));
if (file == null) throw new ArgumentNullException(nameof(file));
if (folder == null) throw new ArgumentNullException(nameof(folder));

_progressFileProcessing.Activity = $"Extracting file {file}";
_progressFileProcessing.StatusDescription = "Adding file {file}";
_progressFileProcessing.PercentComplete = 0;
WriteProgress(_progressFileProcessing);

try
{
var fileName = System.IO.Path.GetFileName(file);
var container = !string.IsNullOrEmpty(Container) ? Container : folder.Replace("\\", "/");

using (var fs = System.IO.File.OpenRead(file))
{
AddFileToTemplate(template, fs, folder.Replace("\\", "/"), fileName, container);
}
}
catch (Exception exc)
{
WriteWarning($"Error trying to add file {file} : {exc.Message}");
}
_progressFileProcessing.RecordType = ProgressRecordType.Completed;
WriteProgress(_progressFileProcessing);
}

private string Tokenize(string input)
{
if (string.IsNullOrEmpty(input)) return input;

foreach (var list in SelectedWeb.Lists)
{
var webRelativeUrl = list.GetWebRelativeUrl();
if (!webRelativeUrl.StartsWith("_catalogs", StringComparison.Ordinal))
{
input = input
.ReplaceCaseInsensitive(list.Id.ToString("D"), "{listid:" + list.Title + "}")
.ReplaceCaseInsensitive(webRelativeUrl, "{listurl:" + list.Title + "}");
}
}
return input.ReplaceCaseInsensitive(SelectedWeb.Url, "{site}")
.ReplaceCaseInsensitive(SelectedWeb.ServerRelativeUrl, "{site}")
.ReplaceCaseInsensitive(SelectedWeb.Id.ToString(), "{siteid}")
.ReplaceCaseInsensitive(((ClientContext)SelectedWeb.Context).Site.ServerRelativeUrl, "{sitecollection}")
.ReplaceCaseInsensitive(((ClientContext)SelectedWeb.Context).Site.Id.ToString(), "{sitecollectionid}")
.ReplaceCaseInsensitive(((ClientContext)SelectedWeb.Context).Site.Url, "{sitecollection}");
}
}
}
Loading