diff --git a/Commands/Base/BaseFileProvisioningCmdlet.cs b/Commands/Base/BaseFileProvisioningCmdlet.cs
new file mode 100644
index 000000000..b2b01daa4
--- /dev/null
+++ b/Commands/Base/BaseFileProvisioningCmdlet.cs
@@ -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
+{
+ ///
+ /// Base class for commands related to adding file to template
+ ///
+ 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;
+ }
+
+ ///
+ /// Add a file to the template
+ ///
+ /// The provisioning template to add the file to
+ /// Stream to read the file content
+ /// target folder in the provisioning template
+ /// Name of the file
+ /// Container path within the template (pnp file) or related to the xml templage
+ /// WebParts to include
+ /// Properties of the file
+ protected void AddFileToTemplate(
+ ProvisioningTemplate template,
+ Stream fs,
+ string folder,
+ string fileName,
+ string container,
+ IEnumerable webParts = null,
+ IDictionary 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);
+ }
+ }
+
+ ///
+ /// Adds a remote file to a template
+ ///
+ /// Template to add the file to
+ /// The SharePoint file to retrieve and add
+ 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 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 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 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
+ };
+ }
+ }
+ }
+
+ ///
+ /// Adds a local file to a template
+ ///
+ /// Template to add the file to
+ /// Full path to a local file
+ /// Destination folder of the added file
+ 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}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Commands/Extensions/SPFileExtensions.cs b/Commands/Extensions/SPFileExtensions.cs
new file mode 100644
index 000000000..54b379ba9
--- /dev/null
+++ b/Commands/Extensions/SPFileExtensions.cs
@@ -0,0 +1,151 @@
+using Microsoft.SharePoint.Client;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+using System.Web;
+
+namespace SharePointPnP.PowerShell.Commands.Extensions
+{
+ public static class SPFileExtensions
+ {
+ public static IList GetPropertyBag(this File file)
+ {
+ var ctx = (ClientContext)file.Context;
+ var web = ctx.Web;
+ var webRelativeFileUrl = file.ServerRelativeUrl.Replace(web.ServerRelativeUrl.TrimEnd('/') + '/', "");
+
+ using (var wc = new WebClientEx())
+ {
+ if (file.Context.Credentials != null)
+ {
+ wc.Credentials = file.Context.Credentials;
+ }
+ else
+ {
+ wc.UseDefaultCredentials = true;
+ }
+
+ var requestUrl = web.Url.TrimEnd('/') + "/_vti_bin/_vti_aut/author.dll";
+
+ wc.Headers.Add(HttpRequestHeader.ContentType, "application/x-www-form-urlencoded");
+ wc.Headers.Add("X-Vermeer-Content-Type", "application/x-www-form-urlencoded");
+
+ var query = HttpUtility.ParseQueryString(string.Empty);
+ query.Add("method", "getDocsMetaInfo");
+ query.Add("url_list", $"[{webRelativeFileUrl}]");
+
+ var rpcResult = Encoding.UTF8.GetString(
+ wc.UploadData(requestUrl, "POST", Encoding.UTF8.GetBytes(query.ToString()))
+ );
+
+ return ParseRpcResult(rpcResult);
+ }
+ }
+
+ public struct RpcProperty
+ {
+ private readonly string _key;
+ private readonly object _value;
+ private readonly bool _writable;
+
+ public RpcProperty(string key, object value, bool writable)
+ {
+ this._key = key;
+ this._value = value;
+ this._writable = writable;
+ }
+
+ public string Key => _key;
+
+ public object Value => _value;
+
+ public bool Writable => _writable;
+
+ public override string ToString()
+ {
+ return new { Key, Value, Writable }.ToString();
+ }
+ }
+
+ private static IList ParseRpcResult(string rpcResult)
+ {
+ var result = new List();
+
+ using (var sr = new System.IO.StringReader(rpcResult))
+ {
+ string currentLine;
+ var hasReachedMetadata = false;
+ while ((currentLine = sr.ReadLine()) != null)
+ {
+ if (!hasReachedMetadata)
+ {
+ if (currentLine == "meta_info=")
+ {
+ // SKip the next ul line
+ sr.ReadLine();
+ hasReachedMetadata = true;
+ }
+ // else nothing to do
+ }
+ else
+ {
+ if (currentLine == "")
+ {
+ break; // end of data has been reached
+ }
+ else
+ {
+ var key = currentLine.Substring(4);
+ var rawValue = sr.ReadLine().Substring(4);
+ var typeInfo = rawValue.Substring(0, 1);
+ var writable = rawValue.Substring(1, 1) == "W";
+ var strValue = HttpUtility.HtmlDecode(rawValue.Substring(3));
+ object value = null;
+ switch (typeInfo)
+ {
+ case "B":
+ value = Convert.ToBoolean(strValue);
+ break;
+
+ case "I":
+ value = Convert.ToInt32(strValue);
+ break;
+
+ case "F":
+ case "T":
+ value = DateTime.Parse(strValue);
+ break;
+
+ case "S":
+ case "V":
+ value = strValue;
+ break;
+
+ default:
+ throw new InvalidOperationException("Unknown RPC type");
+ }
+ var prop = new RpcProperty(key, value, writable);
+ result.Add(prop);
+ }
+ }
+ }
+ return result;
+ }
+ }
+
+ // See https://stackoverflow.com/a/43172235/588868
+ protected class WebClientEx : WebClient
+ {
+ protected override System.Net.WebRequest GetWebRequest(Uri address)
+ {
+ var request = base.GetWebRequest(address) as HttpWebRequest;
+ if (request != null)
+ {
+ request.ServicePoint.Expect100Continue = false;
+ }
+ return request;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Commands/Provisioning/AddFileToProvisioningTemplate.cs b/Commands/Provisioning/AddFileToProvisioningTemplate.cs
new file mode 100644
index 000000000..3438a0aef
--- /dev/null
+++ b/Commands/Provisioning/AddFileToProvisioningTemplate.cs
@@ -0,0 +1,91 @@
+using Microsoft.SharePoint.Client;
+using SharePointPnP.PowerShell.CmdletHelpAttributes;
+using System;
+using System.Management.Automation;
+
+namespace SharePointPnP.PowerShell.Commands.Provisioning
+{
+ [Cmdlet(VerbsCommon.Add, "PnPFileToProvisioningTemplate")]
+ [CmdletHelp("Adds a file to a PnP Provisioning Template",
+ Category = CmdletHelpCategory.Provisioning)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder",
+ Remarks = "Adds a file to a PnP Provisioning Template",
+ SortOrder = 1)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.xml -Source $sourceFilePath -Folder $targetFolder",
+ Remarks = "Adds a file reference to a PnP Provisioning XML Template",
+ SortOrder = 2)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source ""./myfile.png"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false",
+ Remarks = "Adds a file to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the file if it exists in the site.",
+ SortOrder = 3)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder -Container $container",
+ Remarks = "Adds a file to a PnP Provisioning Template with a custom container for the file",
+ SortOrder = 4)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceUrl $urlOfFile",
+ Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.",
+ SortOrder = 5)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceUrl $urlOfFile -ExtractWebParts:$false",
+ Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.",
+ SortOrder = 6)]
+ public class AddFileToProvisioningTemplate : BaseFileProvisioningCmdlet
+ {
+ /*
+* Path, FileLevel, FileOverwrite and TemplateProviderExtensions fields are in the base class
+* */
+
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The file to add to the in-memory template, optionally including full path.")]
+ public string Source;
+
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The file to add to the in-memory template, specifying its url in the current connected Web.")]
+ public string SourceUrl;
+
+ [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the file to add to the in-memory template.")]
+ public string Folder;
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+ var template = LoadTemplate();
+ if (this.ParameterSetName == PSNAME_REMOTE_SOURCE)
+ {
+ var sourceUri = new Uri(SourceUrl, UriKind.RelativeOrAbsolute);
+
+ // Get the server relative url of the file, whatever the input url is (absolute, server relative or web relative form)
+ var serverRelativeUrl =
+ sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file)
+ SourceUrl.StartsWith("/", StringComparison.Ordinal) ? SourceUrl : // The url is server relative. Take it as is (/sites/web/folder/file)
+ SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceUrl; // The url is web relative, prepend by the web url (folder/file)
+
+ _progressFileProcessing.PercentComplete = 0;
+ _progressFileProcessing.RecordType = ProgressRecordType.Processing;
+ _progressFileProcessing.StatusDescription = $"Getting file info {serverRelativeUrl}";
+
+ var file = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl);
+
+ AddSPFileToTemplate(template, file);
+ }
+ else
+ {
+ if (!System.IO.Path.IsPathRooted(Source))
+ {
+ Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source);
+ }
+
+ // Load the file and add it to the .PNP file
+ Folder = Folder.Replace("\\", "/");
+
+ _progressFileProcessing.PercentComplete = 0;
+ _progressFileProcessing.RecordType = ProgressRecordType.Processing;
+ _progressFileProcessing.StatusDescription = $"Getting file info {Source}";
+ WriteProgress(_progressFileProcessing);
+
+ AddLocalFileToTemplate(template, Source, Folder);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Commands/Provisioning/AddFilesToProvisioningTemplate.cs b/Commands/Provisioning/AddFilesToProvisioningTemplate.cs
new file mode 100644
index 000000000..533a6dd1e
--- /dev/null
+++ b/Commands/Provisioning/AddFilesToProvisioningTemplate.cs
@@ -0,0 +1,148 @@
+using Microsoft.SharePoint.Client;
+using SharePointPnP.PowerShell.CmdletHelpAttributes;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using SPFile = Microsoft.SharePoint.Client.File;
+
+namespace SharePointPnP.PowerShell.Commands.Provisioning
+{
+ [Cmdlet(VerbsCommon.Add, "PnPFilesToProvisioningTemplate")]
+ [CmdletHelp("Adds files to a PnP Provisioning Template",
+ Category = CmdletHelpCategory.Provisioning)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFilesToProvisioningTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder",
+ Remarks = "Adds files to a PnP Provisioning Template from a local folder",
+ SortOrder = 1)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.xml -SourceFolder $sourceFolder -Folder $targetFolder",
+ Remarks = "Adds files reference to a PnP Provisioning XML Template",
+ SortOrder = 2)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false",
+ Remarks = "Adds files to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the files if it exists in the site.",
+ SortOrder = 3)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Recurse",
+ Remarks = "Adds files to a PnP Provisioning Template from a local folder recursively.",
+ SortOrder = 4)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder -Container $container",
+ Remarks = "Adds files to a PnP Provisioning Template with a custom container for the files",
+ SortOrder = 5)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder",
+ Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.",
+ SortOrder = 6)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder -ExtractWebParts:$false",
+ Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.",
+ SortOrder = 7)]
+ public class AddFilesToProvisioningTemplate : BaseFileProvisioningCmdlet
+ {
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The source folder to add to the in-memory template, optionally including full path.")]
+ public string SourceFolder;
+
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The source folder to add to the in-memory template, specifying its url in the current connected Web.")]
+ public string SourceFolderUrl;
+
+ [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")]
+ public string Folder;
+
+ [Parameter(Mandatory = false, Position = 8, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")]
+ public SwitchParameter Recurse = false;
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+ var template = LoadTemplate();
+ if (this.ParameterSetName == PSNAME_REMOTE_SOURCE)
+ {
+ var sourceUri = new Uri(SourceFolderUrl, UriKind.RelativeOrAbsolute);
+ // Get the server relative url of the folder, whatever the input url is (absolute, server relative or web relative form)
+ var serverRelativeUrl =
+ sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file)
+ SourceFolderUrl.StartsWith("/", StringComparison.Ordinal) ? SourceFolderUrl : // The url is server relative. Take it as is (/sites/web/folder/file)
+ SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceFolderUrl; // The url is web relative, prepend by the web url (folder/file)
+
+ var folder = SelectedWeb.GetFolderByServerRelativeUrl(serverRelativeUrl);
+
+ var files = EnumRemoteFiles(folder, Recurse).OrderBy(f => f.ServerRelativeUrl).ToArray();
+ _progressEnumeration.RecordType = ProgressRecordType.Completed;
+ WriteProgress(_progressEnumeration);
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ var file = files[0];
+
+ _progressFilesEnumeration.PercentComplete = 100 * i / files.Length;
+ _progressFilesEnumeration.StatusDescription = $"Processing file {file.ServerRelativeUrl}";
+ WriteProgress(_progressFilesEnumeration);
+ AddSPFileToTemplate(template, file);
+ }
+ _progressFilesEnumeration.RecordType = ProgressRecordType.Completed;
+ WriteProgress(_progressFilesEnumeration);
+ }
+ else
+ {
+ if (!System.IO.Path.IsPathRooted(SourceFolder))
+ {
+ SourceFolder = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, SourceFolder);
+ }
+
+ var files = Directory.GetFiles(SourceFolder, "*", Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).OrderBy(f => f).ToArray();
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ var file = files[i];
+
+ _progressFilesEnumeration.PercentComplete = 100 * i / files.Length;
+
+ WriteProgress(_progressFilesEnumeration);
+
+ var localFileFolder = System.IO.Path.GetDirectoryName(file);
+ // relative folder of the leaf file within the directory structure, under the source folder
+ var relativeFolder = Folder + localFileFolder.Substring(SourceFolder.Length);
+ // Load the file and add it to the .PNP file
+ AddLocalFileToTemplate(template, file, relativeFolder);
+ }
+ _progressFilesEnumeration.RecordType = ProgressRecordType.Completed;
+ WriteProgress(_progressFilesEnumeration);
+ }
+ }
+
+ private IEnumerable EnumRemoteFiles(Folder folder, bool recurse)
+ {
+ if (folder == null) throw new ArgumentNullException(nameof(folder));
+
+ var ctx = folder.Context;
+ ctx.Load(folder, fld => fld.ServerRelativeUrl);
+ ctx.Load(folder.Files, files => files.Include(f => f.ServerRelativeUrl, f => f.Name));
+ ctx.ExecuteQueryRetry();
+
+ _progressEnumeration.RecordType = ProgressRecordType.Processing;
+ _progressEnumeration.StatusDescription = $"Enumerating files in folder {folder.ServerRelativeUrl}";
+ WriteProgress(_progressEnumeration);
+ foreach (var file in folder.Files)
+ {
+ yield return file;
+ }
+
+ if (recurse)
+ {
+ ctx.Load(folder.Folders);
+ ctx.ExecuteQueryRetry();
+
+ foreach (var subFolder in folder.Folders)
+ {
+ foreach (var file in EnumRemoteFiles(subFolder, recurse))
+ {
+ yield return file;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs b/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
index ad972c19c..ccddf765e 100644
--- a/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
+++ b/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
@@ -1,125 +1,91 @@
-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 Microsoft.SharePoint.Client;
using SharePointPnP.PowerShell.CmdletHelpAttributes;
using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
using System.Management.Automation;
-using System.Text;
-using System.Threading.Tasks;
-namespace SharePointPnP.PowerShell.Commands.Provisioning.Site
+namespace SharePointPnP.PowerShell.Commands.Provisioning
{
- [Cmdlet(VerbsCommon.Add, "PnPFileToProvisioningTemplate")]
+ [Cmdlet(VerbsCommon.Add, "PnPFileToSiteTemplate")]
[CmdletHelp("Adds a file to a PnP Provisioning Template",
Category = CmdletHelpCategory.Provisioning)]
[CmdletExample(
- Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder",
- Remarks = "Adds a file to a PnP Site Template",
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder",
+ Remarks = "Adds a file to a PnP Provisioning Template",
SortOrder = 1)]
[CmdletExample(
- Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.xml -Source $sourceFilePath -Folder $targetFolder",
- Remarks = "Adds a file reference to a PnP Site XML Template",
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.xml -Source $sourceFilePath -Folder $targetFolder",
+ Remarks = "Adds a file reference to a PnP Provisioning XML Template",
SortOrder = 2)]
[CmdletExample(
- Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source ""./myfile.png"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false",
- Remarks = "Adds a file to a PnP Site Template, specifies the level as Published and defines to not overwrite the file if it exists in the site.",
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -Source ""./myfile.png"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false",
+ Remarks = "Adds a file to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the file if it exists in the site.",
SortOrder = 3)]
[CmdletExample(
- Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder -Container $container",
- Remarks = "Adds a file to a PnP Site Template with a custom container for the file",
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder -Container $container",
+ Remarks = "Adds a file to a PnP Provisioning Template with a custom container for the file",
SortOrder = 4)]
-
- public class AddFileToProvisioningTemplate : PSCmdlet
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceUrl $urlOfFile",
+ Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.",
+ SortOrder = 5)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceUrl $urlOfFile -ExtractWebParts:$false",
+ Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.",
+ SortOrder = 6)]
+ public class AddFileToSiteTemplate : BaseFileProvisioningCmdlet
{
- [Parameter(Mandatory = true, Position = 0, HelpMessage = "Filename of the .PNP Open XML site template to read from, optionally including full path.")]
- public string Path;
+ /*
+* Path, FileLevel, FileOverwrite and TemplateProviderExtensions fields are in the base class
+* */
- [Parameter(Mandatory = true, Position = 1, HelpMessage = "The file to add to the in-memory template, optionally including full path.")]
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The file to add to the in-memory template, optionally including full path.")]
public string Source;
- [Parameter(Mandatory = true, Position = 2, HelpMessage = "The target Folder for the file to add to the in-memory template.")]
- public string Folder;
-
- [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 FileLevel FileLevel = FileLevel.Published;
-
- [Parameter(Mandatory = false, Position = 5, HelpMessage = "Set to overwrite in site, Defaults to true")]
- public SwitchParameter FileOverwrite = true;
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The file to add to the in-memory template, specifying its url in the current connected Web.")]
+ public string SourceUrl;
- [Parameter(Mandatory = false, Position = 4, HelpMessage = "Allows you to specify ITemplateProviderExtension to execute while loading the template.")]
- public ITemplateProviderExtension[] TemplateProviderExtensions;
+ [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the file to add to the in-memory template.")]
+ public string Folder;
protected override void ProcessRecord()
{
- if (!System.IO.Path.IsPathRooted(Path))
- {
- Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path);
- }
- if(!System.IO.Path.IsPathRooted(Source))
+ base.ProcessRecord();
+ var template = LoadTemplate();
+ if (this.ParameterSetName == PSNAME_REMOTE_SOURCE)
{
- Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source);
- }
- // Load the template
- var template = ReadProvisioningTemplate
- .LoadProvisioningTemplateFromFile(Path,
- TemplateProviderExtensions);
+ var sourceUri = new Uri(SourceUrl, UriKind.RelativeOrAbsolute);
- if (template == null)
- {
- throw new ApplicationException("Invalid template file!");
- }
+ // Get the server relative url of the file, whatever the input url is (absolute, server relative or web relative form)
+ var serverRelativeUrl =
+ sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file)
+ SourceUrl.StartsWith("/", StringComparison.Ordinal) ? SourceUrl : // The url is server relative. Take it as is (/sites/web/folder/file)
+ SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceUrl; // The url is web relative, prepend by the web url (folder/file)
- // Load the file and add it to the .PNP file
- using (var fs = new FileStream(Source, FileMode.Open, FileAccess.Read, FileShare.Read))
- {
- Folder = Folder.Replace("\\", "/");
-
- var fileName = Source.IndexOf("\\") > 0 ? Source.Substring(Source.LastIndexOf("\\") + 1) : Source;
- var container = !string.IsNullOrEmpty(Container) ? Container : string.Empty;
- var source = !string.IsNullOrEmpty(container) ? (container + "/" + fileName) : fileName;
+ _progressFileProcessing.PercentComplete = 0;
+ _progressFileProcessing.RecordType = ProgressRecordType.Processing;
+ _progressFileProcessing.StatusDescription = $"Getting file info {serverRelativeUrl}";
- template.Connector.SaveFileStream(fileName, container, fs);
+ var file = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl);
- if (template.Connector is ICommitableFileConnector)
+ AddSPFileToTemplate(template, file);
+ }
+ else
+ {
+ if (!System.IO.Path.IsPathRooted(Source))
{
- ((ICommitableFileConnector)template.Connector).Commit();
+ Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source);
}
- template.Files.Add(new OfficeDevPnP.Core.Framework.Provisioning.Model.File
- {
- Src = source,
- Folder = Folder,
- Level = FileLevel,
- Overwrite = FileOverwrite,
- });
-
- // Determine the output file name and path
- var outFileName = System.IO.Path.GetFileName(Path);
- var outPath = new FileInfo(Path).DirectoryName;
+ // Load the file and add it to the .PNP file
+ Folder = Folder.Replace("\\", "/");
- 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";
+ _progressFileProcessing.PercentComplete = 0;
+ _progressFileProcessing.RecordType = ProgressRecordType.Processing;
+ _progressFileProcessing.StatusDescription = $"Getting file info {Source}";
+ WriteProgress(_progressFileProcessing);
- provider.SaveAs(template, templateFileName, formatter, TemplateProviderExtensions);
- }
- else
- {
- XMLTemplateProvider provider = new XMLFileSystemTemplateProvider(Path, "");
- provider.SaveAs(template, Path, formatter, TemplateProviderExtensions);
- }
+ AddLocalFileToTemplate(template, Source, Folder);
}
}
}
-}
+}
\ No newline at end of file
diff --git a/Commands/Provisioning/Site/AddFilesToSiteTemplate.cs b/Commands/Provisioning/Site/AddFilesToSiteTemplate.cs
new file mode 100644
index 000000000..700194ab3
--- /dev/null
+++ b/Commands/Provisioning/Site/AddFilesToSiteTemplate.cs
@@ -0,0 +1,148 @@
+using Microsoft.SharePoint.Client;
+using SharePointPnP.PowerShell.CmdletHelpAttributes;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using SPFile = Microsoft.SharePoint.Client.File;
+
+namespace SharePointPnP.PowerShell.Commands.Provisioning.Site
+{
+ [Cmdlet(VerbsCommon.Add, "PnPFilesToSiteTemplate")]
+ [CmdletHelp("Adds files to a PnP Provisioning Template",
+ Category = CmdletHelpCategory.Provisioning)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFilesToSiteTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder",
+ Remarks = "Adds files to a PnP Provisioning Template from a local folder",
+ SortOrder = 1)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.xml -SourceFolder $sourceFolder -Folder $targetFolder",
+ Remarks = "Adds files reference to a PnP Provisioning XML Template",
+ SortOrder = 2)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false",
+ Remarks = "Adds files to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the files if it exists in the site.",
+ SortOrder = 3)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Recurse",
+ Remarks = "Adds files to a PnP Provisioning Template from a local folder recursively.",
+ SortOrder = 4)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder -Container $container",
+ Remarks = "Adds files to a PnP Provisioning Template with a custom container for the files",
+ SortOrder = 5)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder",
+ Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.",
+ SortOrder = 6)]
+ [CmdletExample(
+ Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder -ExtractWebParts:$false",
+ Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.",
+ SortOrder = 7)]
+ public class AddFilesToSiteTemplate : BaseFileProvisioningCmdlet
+ {
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The source folder to add to the in-memory template, optionally including full path.")]
+ public string SourceFolder;
+
+ [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The source folder to add to the in-memory template, specifying its url in the current connected Web.")]
+ public string SourceFolderUrl;
+
+ [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")]
+ public string Folder;
+
+ [Parameter(Mandatory = false, Position = 8, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")]
+ public SwitchParameter Recurse = false;
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+ var template = LoadTemplate();
+ if (this.ParameterSetName == PSNAME_REMOTE_SOURCE)
+ {
+ var sourceUri = new Uri(SourceFolderUrl, UriKind.RelativeOrAbsolute);
+ // Get the server relative url of the folder, whatever the input url is (absolute, server relative or web relative form)
+ var serverRelativeUrl =
+ sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file)
+ SourceFolderUrl.StartsWith("/", StringComparison.Ordinal) ? SourceFolderUrl : // The url is server relative. Take it as is (/sites/web/folder/file)
+ SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceFolderUrl; // The url is web relative, prepend by the web url (folder/file)
+
+ var folder = SelectedWeb.GetFolderByServerRelativeUrl(serverRelativeUrl);
+
+ var files = EnumRemoteFiles(folder, Recurse).OrderBy(f => f.ServerRelativeUrl).ToArray();
+ _progressEnumeration.RecordType = ProgressRecordType.Completed;
+ WriteProgress(_progressEnumeration);
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ var file = files[i];
+
+ _progressFilesEnumeration.PercentComplete = 100 * i / files.Length;
+ _progressFilesEnumeration.StatusDescription = $"Processing file {file.ServerRelativeUrl}";
+ WriteProgress(_progressFilesEnumeration);
+ AddSPFileToTemplate(template, file);
+ }
+ _progressFilesEnumeration.RecordType = ProgressRecordType.Completed;
+ WriteProgress(_progressFilesEnumeration);
+ }
+ else
+ {
+ if (!System.IO.Path.IsPathRooted(SourceFolder))
+ {
+ SourceFolder = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, SourceFolder);
+ }
+
+ var files = Directory.GetFiles(SourceFolder, "*", Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).OrderBy(f => f).ToArray();
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ var file = files[i];
+
+ _progressFilesEnumeration.PercentComplete = 100 * i / files.Length;
+
+ WriteProgress(_progressFilesEnumeration);
+
+ var localFileFolder = System.IO.Path.GetDirectoryName(file);
+ // relative folder of the leaf file within the directory structure, under the source folder
+ var relativeFolder = Folder + localFileFolder.Substring(SourceFolder.Length);
+ // Load the file and add it to the .PNP file
+ AddLocalFileToTemplate(template, file, relativeFolder);
+ }
+ _progressFilesEnumeration.RecordType = ProgressRecordType.Completed;
+ WriteProgress(_progressFilesEnumeration);
+ }
+ }
+
+ private IEnumerable EnumRemoteFiles(Folder folder, bool recurse)
+ {
+ if (folder == null) throw new ArgumentNullException(nameof(folder));
+
+ var ctx = folder.Context;
+ ctx.Load(folder, fld => fld.ServerRelativeUrl);
+ ctx.Load(folder.Files, files => files.Include(f => f.ServerRelativeUrl, f => f.Name));
+ ctx.ExecuteQueryRetry();
+
+ _progressEnumeration.RecordType = ProgressRecordType.Processing;
+ _progressEnumeration.StatusDescription = $"Enumerating files in folder {folder.ServerRelativeUrl}";
+ WriteProgress(_progressEnumeration);
+ foreach (var file in folder.Files)
+ {
+ yield return file;
+ }
+
+ if (recurse)
+ {
+ ctx.Load(folder.Folders);
+ ctx.ExecuteQueryRetry();
+
+ foreach (var subFolder in folder.Folders)
+ {
+ foreach (var file in EnumRemoteFiles(subFolder, recurse))
+ {
+ yield return file;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Commands/SharePointPnP.PowerShell.Commands.csproj b/Commands/SharePointPnP.PowerShell.Commands.csproj
index e953d2835..4b2a10a41 100644
--- a/Commands/SharePointPnP.PowerShell.Commands.csproj
+++ b/Commands/SharePointPnP.PowerShell.Commands.csproj
@@ -570,6 +570,7 @@
+
@@ -635,6 +636,7 @@
+
@@ -796,6 +798,7 @@
+
@@ -1084,7 +1087,9 @@
-
+
+
+
diff --git a/Commands/Utilities/XmlPageDataHelper.cs b/Commands/Utilities/XmlPageDataHelper.cs
new file mode 100644
index 000000000..521a3b9bd
--- /dev/null
+++ b/Commands/Utilities/XmlPageDataHelper.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace SharePointPnP.PowerShell.Commands.Utilities
+{
+ public static class XmlPageDataHelper
+ {
+ ///
+ /// Extract properties from Xml data hidden in aspx file
+ ///
+ /// Full page content including xml data
+ /// Properties extracted from the page.
+ public static IDictionary ExtractProperties(string documentContent)
+ {
+ // Seek for the Xml data within the page
+ var match = Regex.Match(documentContent, "(.*)<\\/xml>.*<\\/SharePoint:CTFieldRefs>", RegexOptions.Singleline);
+ if (match.Success)
+ {
+ // Wrap the actual data to be xml compliant
+ var xmlDataStr = $"{match.Groups[1].Value}";
+
+ var xmlData = XElement.Parse(xmlDataStr);
+
+ return xmlData
+ .Elements("{mso}CustomDocumentProperties").Elements()
+ .Where(el => !string.IsNullOrEmpty(el.Value))
+ .ToDictionary(el => el.Name.LocalName, el => el.Value);
+ }
+ else
+ {
+ throw new ApplicationException("Invalid documentContent. Is is an .aspx file?");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/SharePointPnP.PowerShell.Tests.csproj b/Tests/SharePointPnP.PowerShell.Tests.csproj
index 72cda4e62..9baa7658d 100644
--- a/Tests/SharePointPnP.PowerShell.Tests.csproj
+++ b/Tests/SharePointPnP.PowerShell.Tests.csproj
@@ -354,6 +354,7 @@
+
diff --git a/Tests/Utilities/XmlPageDataHelperTests.cs b/Tests/Utilities/XmlPageDataHelperTests.cs
new file mode 100644
index 000000000..83cc7963d
--- /dev/null
+++ b/Tests/Utilities/XmlPageDataHelperTests.cs
@@ -0,0 +1,60 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SharePointPnP.PowerShell.Commands.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SharePointPnP.PowerShell.Commands.Utilities.Tests
+{
+ [TestClass()]
+ public class XmlPageDataHelperTests
+ {
+ [TestMethod()]
+ public void ExtractPropertiesTest()
+ {
+ var sampleContent = @"<%@ Page Inherits=""Microsoft.SharePoint.Publishing.TemplateRedirectionPage,Microsoft.SharePoint.Publishing,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c"" %> <%@ Reference VirtualPath=""~TemplatePageUrl"" %> <%@ Reference VirtualPath=""~masterurl/custom.master"" %>
+<%@ Register Tagprefix=""SharePoint"" Namespace=""Microsoft.SharePoint.WebControls"" Assembly=""Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"" %>
+
+
+Recherche globale";
+
+ var result = XmlPageDataHelper.ExtractProperties(sampleContent);
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.ContainsKey("PublishingPageLayout"));
+ Assert.AreEqual(result["PublishingPageLayout"], "/sites/somesite/_catalogs/masterpage/BlankWebPartPage.aspx, Page de composant WebPart vierge");
+
+ }
+ }
+}
\ No newline at end of file