diff --git a/Commands/Base/BaseFileProvisioningCmdlet.cs b/Commands/Base/BaseFileProvisioningCmdlet.cs
new file mode 100644
index 000000000..96cb46f9a
--- /dev/null
+++ b/Commands/Base/BaseFileProvisioningCmdlet.cs
@@ -0,0 +1,160 @@
+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 System;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using System.Net;
+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, HelpMessage = "Allows you to specify ITemplateProviderExtension to execute while loading the template.")]
+ public ITemplateProviderExtension[] TemplateProviderExtensions;
+
+ 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
+ protected void AddFileToTemplate(ProvisioningTemplate template, Stream fs, string folder, string fileName, string container)
+ {
+ 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,
+ };
+
+ 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)
+ {
+ file.EnsureProperties(f => f.Name, f => f.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
+ {
+ using (var fi = SPFile.OpenBinaryDirect(ClientContext, file.ServerRelativeUrl))
+ using (var ms = new MemoryStream())
+ {
+ // 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;
+ AddFileToTemplate(template, ms, folderWebRelativeUrl, file.Name, folderWebRelativeUrl);
+ }
+ }
+ catch (WebException exc)
+ {
+ WriteWarning($"Can't add file from url {file.ServerRelativeUrl} : {exc}");
+ }
+ }
+
+ ///
+ /// 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)
+ {
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs b/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
index ad972c19c..375bcfe81 100644
--- a/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
+++ b/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
@@ -1,15 +1,7 @@
-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
{
@@ -32,94 +24,55 @@ namespace SharePointPnP.PowerShell.Commands.Provisioning.Site
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",
SortOrder = 4)]
-
- public class AddFileToProvisioningTemplate : PSCmdlet
+ [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 = 4)]
+ public class AddFileToProvisioningTemplate : 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 = 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 = "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 = 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))
- {
- Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source);
- }
- // Load the template
- var template = ReadProvisioningTemplate
- .LoadProvisioningTemplateFromFile(Path,
- TemplateProviderExtensions);
-
- if (template == null)
- {
- throw new ApplicationException("Invalid template file!");
- }
-
- // Load the file and add it to the .PNP file
- using (var fs = new FileStream(Source, FileMode.Open, FileAccess.Read, FileShare.Read))
+ var template = LoadTemplate();
+ if (this.ParameterSetName == PSNAME_REMOTE_SOURCE)
{
- Folder = Folder.Replace("\\", "/");
+ SelectedWeb.EnsureProperty(w => w.ServerRelativeUrl);
+ var sourceUri = new Uri(SourceUrl, UriKind.RelativeOrAbsolute);
- 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;
+ // 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)
- 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;
-
- 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";
+ // Load the file and add it to the .PNP file
+ Folder = Folder.Replace("\\", "/");
- 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/AddFilesToProvisioningTemplate.cs b/Commands/Provisioning/Site/AddFilesToProvisioningTemplate.cs
new file mode 100644
index 000000000..c060eb7d2
--- /dev/null
+++ b/Commands/Provisioning/Site/AddFilesToProvisioningTemplate.cs
@@ -0,0 +1,123 @@
+using Microsoft.SharePoint.Client;
+using OfficeDevPnP.Core.Framework.Provisioning.Model;
+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, "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)]
+ 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 = true, Position = 7, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")]
+ public SwitchParameter Recurse = false;
+
+ protected override void ProcessRecord()
+ {
+ var template = LoadTemplate();
+ if (this.ParameterSetName == PSNAME_REMOTE_SOURCE)
+ {
+ SelectedWeb.EnsureProperty(w => w.ServerRelativeUrl);
+ 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);
+ foreach (var file in files)
+ {
+ AddSPFileToTemplate(template, file);
+ }
+ }
+ else
+ {
+ if (!System.IO.Path.IsPathRooted(SourceFolder))
+ {
+ SourceFolder = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, SourceFolder);
+ }
+
+ var files = System.IO.Directory.GetFiles(SourceFolder, "*", Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).OrderBy(f => f);
+
+ foreach (var file in files)
+ {
+ 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);
+ }
+ }
+ }
+
+ private IEnumerable EnumRemoteFiles(Microsoft.SharePoint.Client.Folder folder, bool recurse)
+ {
+ var ctx = folder.Context;
+
+ ctx.Load(folder.Files, files => files.Include(f => f.ServerRelativeUrl, f => f.Name));
+ ctx.ExecuteQueryRetry();
+
+ 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..272604440 100644
--- a/Commands/SharePointPnP.PowerShell.Commands.csproj
+++ b/Commands/SharePointPnP.PowerShell.Commands.csproj
@@ -570,6 +570,7 @@
+
@@ -635,6 +636,7 @@
+