-
Notifications
You must be signed in to change notification settings - Fork 360
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding upload entire local folder to SharePoint Online into `Copy-PnP…
…Folder` (#3850) * Adding functionality to allow a local folder with all its files and optionally recursed subfolders to be uploaded to SharePoint Online * Added PR reference * Typo fix * Adding verbose parameter * Fixing syntax issue * Added that empty folders will also be removed when providing -RemoveAfterCopy * Updated help text to reflect folders being deleted now as well --------- Co-authored-by: Gautam Sheth <[email protected]>
- v2.12.0
- v2.10.0
- v2.5.0
- 2.99.137-nightly
- 2.99.136-nightly
- 2.99.135-nightly
- 2.99.134-nightly
- 2.99.133-nightly
- 2.99.132-nightly
- 2.99.131-nightly
- 2.99.130-nightly
- 2.99.129-nightly
- 2.99.128-nightly
- 2.99.127-nightly
- 2.99.126-nightly
- 2.99.125-nightly
- 2.99.124-nightly
- 2.99.123-nightly
- 2.99.122-nightly
- 2.99.121-nightly
- 2.99.120-nightly
- 2.99.119-nightly
- 2.99.118-nightly
- 2.99.117-nightly
- 2.99.116-nightly
- 2.99.115-nightly
- 2.99.114-nightly
- 2.99.113-nightly
- 2.99.112-nightly
- 2.99.111-nightly
- 2.99.110-nightly
- 2.99.109-nightly
- 2.99.108-nightly
- 2.99.107-nightly
- 2.99.106-nightly
- 2.99.105-nightly
- 2.99.104-nightly
- 2.99.103-nightly
- 2.99.102-nightly
- 2.99.101-nightly
- 2.99.100-nightly
- 2.99.99-nightly
- 2.99.98-nightly
- 2.99.97-nightly
- 2.99.96-nightly
- 2.99.95-nightly
- 2.99.94-nightly
- 2.99.93-nightly
- 2.99.92-nightly
- 2.99.91-nightly
- 2.99.90-nightly
- 2.99.89-nightly
- 2.99.88-nightly
- 2.99.87-nightly
- 2.99.86-nightly
- 2.99.85-nightly
- 2.99.84-nightly
- 2.99.83-nightly
- 2.99.82-nightly
- 2.99.81-nightly
- 2.99.80-nightly
- 2.99.79-nightly
- 2.99.78-nightly
- 2.99.77-nightly
- 2.99.76-nightly
- 2.99.75-nightly
- 2.99.74-nightly
- 2.99.73-nightly
- 2.99.72-nightly
- 2.99.71-nightly
- 2.99.70-nightly
- 2.99.69-nightly
- 2.99.68-nightly
- 2.99.67-nightly
- 2.99.66-nightly
- 2.99.65-nightly
- 2.99.64-nightly
- 2.99.63-nightly
- 2.99.62-nightly
- 2.99.61-nightly
- 2.99.60-nightly
- 2.99.59-nightly
- 2.99.58-nightly
- 2.99.57-nightly
- 2.99.56-nightly
- 2.99.55-nightly
- 2.99.54-nightly
- 2.99.53-nightly
- 2.99.52-nightly
- 2.99.51-nightly
- 2.99.50-nightly
- 2.99.49-nightly
- 2.99.48-nightly
- 2.99.47-nightly
- 2.99.46-nightly
- 2.99.45-nightly
- 2.99.44-nightly
- 2.99.43-nightly
- 2.99.42-nightly
- 2.99.41-nightly
- 2.99.40-nightly
- 2.99.39-nightly
- 2.99.38-nightly
- 2.99.37-nightly
- 2.99.36-nightly
- 2.99.35-nightly
- 2.99.34-nightly
- 2.99.33-nightly
- 2.99.32-nightly
- 2.99.31-nightly
- 2.99.30-nightly
- 2.99.29-nightly
- 2.99.28-nightly
- 2.99.27-nightly
- 2.99.26-nightly
- 2.99.25-nightly
- 2.99.24-nightly
- 2.99.23-nightly
- 2.99.22-nightly
- 2.99.21-nightly
- 2.99.20-nightly
- 2.99.19-nightly
- 2.99.18-nightly
- 2.99.17-nightly
- 2.99.16-nightly
- 2.99.15-nightly
- 2.99.14-nightly
- 2.99.13-nightly
- 2.99.12-nightly
- 2.99.11-nightly
- 2.99.10-nightly
- 2.99.9-nightly
- 2.99.8-nightly
- 2.99.7-nightly
- 2.99.6-nightly
- 2.99.5-nightly
- 2.99.4-nightly
- 2.99.3-nightly
- 2.99.2-nightly
- 2.99.1-nightly
- 2.12.23-nightly
- 2.12.22-nightly
- 2.12.21-nightly
- 2.12.20-nightly
- 2.12.19-nightly
- 2.12.18-nightly
- 2.12.17-nightly
- 2.12.16-nightly
- 2.12.15-nightly
- 2.12.14-nightly
- 2.12.13-nightly
- 2.12.12-nightly
- 2.12.11-nightly
- 2.12.10-nightly
- 2.12.9-nightly
- 2.12.8-nightly
- 2.12.7-nightly
- 2.12.6-nightly
- 2.12.5-nightly
- 2.12.4-nightly
- 2.12.3-nightly
- 2.12.2-nightly
- 2.12.1-nightly
- 2.11.3-nightly
- 2.11.2-nightly
- 2.11.1-nightly
- 2.10.11-nightly
- 2.10.10-nightly
- 2.10.9-nightly
- 2.10.8-nightly
- 2.10.7-nightly
- 2.10.6-nightly
- 2.10.5-nightly
- 2.10.4-nightly
- 2.10.3-nightly
- 2.10.2-nightly
- 2.10.1-nightly
- 2.9.8-nightly
- 2.9.7-nightly
- 2.9.6-nightly
- 2.9.5-nightly
- 2.9.4-nightly
- 2.9.3-nightly
- 2.9.2-nightly
- 2.9.1-nightly
- 2.8.1-nightly
- 2.7.3-nightly
- 2.7.2-nightly
- 2.7.1-nightly
- 2.5.50-nightly
- 2.5.49-nightly
- 2.5.48-nightly
- 2.5.47-nightly
- 2.5.46-nightly
- 2.5.45-nightly
- 2.5.44-nightly
- 2.5.43-nightly
- 2.5.42-nightly
- 2.5.41-nightly
- 2.5.40-nightly
- 2.5.39-nightly
- 2.5.38-nightly
- 2.5.37-nightly
- 2.5.36-nightly
- 2.5.35-nightly
- 2.5.34-nightly
- 2.5.33-nightly
- 2.5.32-nightly
- 2.5.31-nightly
- 2.5.30-nightly
- 2.5.29-nightly
- 2.5.28-nightly
- 2.5.27-nightly
- 2.5.26-nightly
- 2.5.25-nightly
- 2.5.24-nightly
- 2.5.23-nightly
- 2.5.22-nightly
- 2.5.21-nightly
- 2.5.20-nightly
- 2.5.19-nightly
- 2.5.18-nightly
- 2.5.17-nightly
- 2.5.16-nightly
- 2.5.15-nightly
- 2.5.14-nightly
- 2.5.13-nightly
- 2.5.12-nightly
- 2.5.11-nightly
- 2.5.10-nightly
- 2.5.9-nightly
- 2.5.8-nightly
- 2.5.7-nightly
- 2.5.6-nightly
- 2.5.5-nightly
- 2.5.4-nightly
- 2.5.3-nightly
- 2.5.2-nightly
- 2.5.1-nightly
- 2.4.107-nightly
- 2.4.106-nightly
- 2.4.105-nightly
- 2.4.104-nightly
- 2.4.103-nightly
- 2.4.102-nightly
- 2.4.101-nightly
- 2.4.100-nightly
- 2.4.99-nightly
- 2.4.98-nightly
- 2.4.97-nightly
- 2.4.96-nightly
- 2.4.95-nightly
- 2.4.94-nightly
- 2.4.93-nightly
- 2.4.92-nightly
- 2.4.91-nightly
- 2.4.90-nightly
- 2.4.89-nightly
- 2.4.88-nightly
- 2.4.87-nightly
- 2.4.86-nightly
- 2.4.85-nightly
- 2.4.84-nightly
- 2.4.83-nightly
- 2.4.82-nightly
- 2.4.81-nightly
- 2.4.80-nightly
- 2.4.79-nightly
- 2.4.78-nightly
- 2.4.77-nightly
- 2.4.76-nightly
- 2.4.75-nightly
- 2.4.74-nightly
- 2.4.73-nightly
- 2.4.72-nightly
- 2.4.70-nightly
- 2.4.69-nightly
- 2.4.68-nightly
- 2.4.67-nightly
- 2.4.66-nightly
- 2.4.65-nightly
- 2.4.64-nightly
- 2.4.63-nightly
- 2.4.62-nightly
- 2.4.61-nightly
- 2.4.60-nightly
- 2.4.59-nightly
- 2.4.58-nightly
- 2.4.57-nightly
- 2.4.56-nightly
- 2.4.55-nightly
- 2.4.54-nightly
- 2.4.53-nightly
- 2.4.52-nightly
- 2.4.51-nightly
- 2.4.50-nightly
- 2.4.49-nightly
- 2.4.48-nightly
- 2.4.47-nightly
- 2.4.46-nightly
- 2.4.45-nightly
- 2.4.44-nightly
- 2.4.43-nightly
- 2.4.42-nightly
- 2.4.41-nightly
- 2.4.40-nightly
- 2.4.39-nightly
- 2.4.38-nightly
- 2.4.37-nightly
- 2.4.36-nightly
- 2.4.35-nightly
- 2.4.34-nightly
- 2.4.33-nightly
- 2.4.32-nightly
- 2.4.31-nightly
1 parent
1163653
commit a8542c5
Showing
4 changed files
with
362 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Management.Automation; | ||
using Microsoft.SharePoint.Client; | ||
using Resources = PnP.PowerShell.Commands.Properties.Resources; | ||
using PnP.Framework.Utilities; | ||
using System.Collections.Generic; | ||
|
||
namespace PnP.PowerShell.Commands.Files | ||
{ | ||
[Cmdlet(VerbsCommon.Copy, "PnPFolder", DefaultParameterSetName = ParameterSet_WITHINM365)] | ||
public class CopyFolder : PnPWebCmdlet | ||
{ | ||
private const string ParameterSet_WITHINM365 = "Copy files within Microsoft 365"; | ||
private const string ParameterSet_FROMLOCAL = "Copy files from local to Microsoft 365"; | ||
|
||
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ParameterSetName = ParameterSet_WITHINM365)] | ||
[Alias("ServerRelativeUrl")] | ||
public string SourceUrl = string.Empty; | ||
|
||
[Parameter(Mandatory = true, Position = 0, ParameterSetName = ParameterSet_FROMLOCAL)] | ||
public string LocalPath = string.Empty; | ||
|
||
[Parameter(Mandatory = true, Position = 1, ParameterSetName = ParameterSet_WITHINM365)] | ||
[Parameter(Mandatory = true, Position = 1, ParameterSetName = ParameterSet_FROMLOCAL)] | ||
[Alias("TargetServerRelativeUrl")] | ||
public string TargetUrl = string.Empty; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_WITHINM365)] | ||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_FROMLOCAL)] | ||
[Alias("OverwriteIfAlreadyExists")] | ||
public SwitchParameter Overwrite; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_WITHINM365)] | ||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_FROMLOCAL)] | ||
public SwitchParameter Force; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_WITHINM365)] | ||
public SwitchParameter IgnoreVersionHistory; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_WITHINM365)] | ||
public SwitchParameter AllowSchemaMismatch; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_WITHINM365)] | ||
public SwitchParameter NoWait; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_FROMLOCAL)] | ||
public SwitchParameter Recurse; | ||
|
||
[Parameter(Mandatory = false, ParameterSetName = ParameterSet_FROMLOCAL)] | ||
public SwitchParameter RemoveAfterCopy; | ||
|
||
protected override void ExecuteCmdlet() | ||
{ | ||
var webServerRelativeUrl = CurrentWeb.EnsureProperty(w => w.ServerRelativeUrl); | ||
|
||
switch(ParameterSetName) | ||
{ | ||
case ParameterSet_FROMLOCAL: | ||
// Copy a folder from local to Microsoft 365 | ||
CopyFromLocalToMicrosoft365(); | ||
break; | ||
|
||
case ParameterSet_WITHINM365: | ||
// Copy a folder within Microsoft 365 | ||
CopyWithinMicrosoft365(); | ||
break; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Copies a folder from local to Microsoft 365 | ||
/// </summary> | ||
private void CopyFromLocalToMicrosoft365() | ||
{ | ||
if (TargetUrl.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) | ||
{ | ||
// Remove the server part from the full FQDN making it server relative | ||
TargetUrl = TargetUrl[8..]; | ||
TargetUrl = TargetUrl[TargetUrl.IndexOf('/')..]; | ||
} | ||
|
||
if (!TargetUrl.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase) && TargetUrl.StartsWith(CurrentWeb.ServerRelativeUrl, StringComparison.InvariantCultureIgnoreCase)) | ||
{ | ||
// Remove the server relative path making it web relative | ||
TargetUrl = TargetUrl[CurrentWeb.ServerRelativeUrl.Length..]; | ||
} | ||
|
||
if (!System.IO.Path.IsPathRooted(LocalPath)) | ||
{ | ||
LocalPath = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, LocalPath); | ||
} | ||
|
||
// Normalize the path, taking out relative links and such to make it more readable | ||
LocalPath = System.IO.Path.GetFullPath(new Uri(LocalPath).LocalPath); | ||
|
||
if (!System.IO.Directory.Exists(LocalPath)) | ||
{ | ||
throw new PSArgumentException($"{nameof(LocalPath)} does not exist", nameof(LocalPath)); | ||
} | ||
|
||
WriteVerbose($"Copying folder from local path {LocalPath} to Microsoft 365 location {UrlUtility.Combine(CurrentWeb.ServerRelativeUrl, TargetUrl)}"); | ||
WriteVerbose($"Retrieving local files {(Recurse.ToBool() ? "recursively " : "")}to upload from {LocalPath}"); | ||
|
||
var filesToCopy = System.IO.Directory.GetFiles(LocalPath, string.Empty, Recurse.ToBool() ? System.IO.SearchOption.AllDirectories : System.IO.SearchOption.TopDirectoryOnly); | ||
|
||
WriteVerbose($"Uploading {filesToCopy.Length} file{(filesToCopy.Length != 1 ? "s" : "")}"); | ||
|
||
// Start with the root | ||
string currentRemotePath = null; | ||
Folder folder = null; | ||
var folderPathsToRemove = new List<string>(); | ||
|
||
foreach(var fileToCopy in filesToCopy) | ||
{ | ||
var fileName = System.IO.Path.GetFileName(fileToCopy); | ||
var relativeLocalPathWithFileName = System.IO.Path.GetRelativePath(LocalPath, fileToCopy); | ||
var relativePath = relativeLocalPathWithFileName.Remove(relativeLocalPathWithFileName.Length - fileName.Length); | ||
|
||
// Check if we're dealing with a different subfolder now as we did during the previous cycle | ||
if(relativePath != currentRemotePath) | ||
{ | ||
// Check files should be removed after uploading and if so, if the previous folder is empty and should be removed. Skip the root folder, that should never be removed. | ||
if(RemoveAfterCopy.ToBool() && currentRemotePath != null) | ||
{ | ||
// Add the folder to be examined at the end of the upload session. If we would do it now, we could still have subfolders of which the files have not been uploaded yet, so we cannot delete the folder yet | ||
var localFolder = System.IO.Path.Combine(LocalPath, relativePath); | ||
folderPathsToRemove.Add(localFolder); | ||
} | ||
|
||
// New subfolder, ensure the folder exists remotely as well | ||
currentRemotePath = relativePath; | ||
|
||
var newRemotePath = UrlUtility.Combine(TargetUrl, relativePath); | ||
|
||
WriteVerbose($"* Ensuring remote folder {newRemotePath}"); | ||
|
||
folder = CurrentWeb.EnsureFolderPath(newRemotePath); | ||
} | ||
|
||
// Upload the file from local to remote | ||
WriteVerbose($" * Uploading {fileToCopy} => {relativeLocalPathWithFileName}"); | ||
try | ||
{ | ||
folder.UploadFile(fileName, fileToCopy, Overwrite.ToBool()); | ||
|
||
if(RemoveAfterCopy.ToBool()) | ||
{ | ||
WriteVerbose($" * Removing {fileToCopy}"); | ||
System.IO.File.Delete(fileToCopy); | ||
} | ||
} | ||
catch(Exception ex) | ||
{ | ||
WriteWarning($"* Upload failed: {ex.Message}"); | ||
} | ||
} | ||
|
||
// Check if we should and can clean up folders no longer containing files | ||
if (RemoveAfterCopy.ToBool() && folderPathsToRemove.Count > 0) | ||
{ | ||
WriteVerbose($"Checking if {folderPathsToRemove.Count} folder{(folderPathsToRemove.Count != 1 ? "s" : "")} are empty and can be removed"); | ||
|
||
// Reverse the list so we start with the deepest nested folder first | ||
folderPathsToRemove.Reverse(); | ||
|
||
foreach (var folderPathToRemove in folderPathsToRemove) | ||
{ | ||
if (System.IO.Directory.GetFiles(folderPathToRemove).Length == 0) | ||
{ | ||
WriteVerbose($"* Removing empty folder {folderPathToRemove}"); | ||
try | ||
{ | ||
System.IO.Directory.Delete(folderPathToRemove); | ||
} | ||
catch(Exception ex) | ||
{ | ||
WriteWarning($"* Failed to remove empty folder {folderPathToRemove}: {ex.Message}"); | ||
} | ||
} | ||
else | ||
{ | ||
WriteVerbose($"* Folder {folderPathToRemove} is not empty and thus will not be removed"); | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
/// <summary> | ||
/// Copies a folder within Microsoft 365 | ||
/// </summary> | ||
private void CopyWithinMicrosoft365() | ||
{ | ||
if (!TargetUrl.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase) && !TargetUrl.StartsWith("/")) | ||
{ | ||
TargetUrl = UrlUtility.Combine(CurrentWeb.ServerRelativeUrl, TargetUrl); | ||
} | ||
|
||
if (!SourceUrl.StartsWith("/")) | ||
{ | ||
SourceUrl = UrlUtility.Combine(CurrentWeb.ServerRelativeUrl, SourceUrl); | ||
} | ||
|
||
WriteVerbose($"Copying folder within Microsoft 365 from {SourceUrl} to {TargetUrl}"); | ||
|
||
string sourceFolder = SourceUrl.Substring(0, SourceUrl.LastIndexOf('/')); | ||
string targetFolder = TargetUrl; | ||
if (System.IO.Path.HasExtension(TargetUrl)) | ||
{ | ||
targetFolder = TargetUrl[..TargetUrl.LastIndexOf('/')]; | ||
} | ||
Uri currentContextUri = new(ClientContext.Url); | ||
Uri sourceUri = new(currentContextUri, EncodePath(sourceFolder)); | ||
Uri sourceWebUri = Web.WebUrlFromFolderUrlDirect(ClientContext, sourceUri); | ||
Uri targetUri = new(currentContextUri, EncodePath(targetFolder)); | ||
Uri targetWebUri; | ||
if (TargetUrl.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) | ||
{ | ||
targetUri = new Uri(TargetUrl); | ||
targetWebUri = targetUri; | ||
} | ||
else | ||
{ | ||
targetWebUri = Microsoft.SharePoint.Client.Web.WebUrlFromFolderUrlDirect(ClientContext, targetUri); | ||
} | ||
|
||
if (Force || ShouldContinue(string.Format(Resources.CopyFile0To1, SourceUrl, TargetUrl), Resources.Confirm)) | ||
{ | ||
if (sourceWebUri != targetWebUri) | ||
{ | ||
Copy(currentContextUri, sourceUri, targetUri, SourceUrl, TargetUrl, false, NoWait); | ||
} | ||
else | ||
{ | ||
var isFolder = false; | ||
try | ||
{ | ||
var folder = CurrentWeb.GetFolderByServerRelativePath(ResourcePath.FromDecodedUrl(TargetUrl)); | ||
var folderServerRelativePath = folder.EnsureProperty(f => f.ServerRelativePath); | ||
isFolder = folderServerRelativePath.DecodedUrl == ResourcePath.FromDecodedUrl(TargetUrl).DecodedUrl; | ||
} | ||
catch | ||
{ | ||
} | ||
if (isFolder) | ||
{ | ||
Copy(currentContextUri, sourceUri, targetUri, SourceUrl, TargetUrl, true, NoWait); | ||
} | ||
else | ||
{ | ||
var file = CurrentWeb.GetFileByServerRelativePath(ResourcePath.FromDecodedUrl(SourceUrl)); | ||
file.CopyToUsingPath(ResourcePath.FromDecodedUrl(TargetUrl), Overwrite); | ||
ClientContext.ExecuteQueryRetry(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private string EncodePath(string path) | ||
{ | ||
var parts = path.Split("/"); | ||
return string.Join("/", parts.Select(p => Uri.EscapeDataString(p))); | ||
} | ||
|
||
private void Copy(Uri currentContextUri, Uri source, Uri destination, string sourceUrl, string targetUrl, bool sameWebCopyMoveOptimization, bool noWait) | ||
{ | ||
if (!sourceUrl.StartsWith(source.ToString())) | ||
{ | ||
sourceUrl = $"{source.Scheme}://{source.Host}/{sourceUrl.TrimStart('/')}"; | ||
} | ||
if (!targetUrl.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase) && !targetUrl.StartsWith(destination.ToString())) | ||
{ | ||
targetUrl = $"{destination.Scheme}://{destination.Host}/{targetUrl.TrimStart('/')}"; | ||
} | ||
var results = Utilities.CopyMover.CopyAsync(HttpClient, ClientContext, currentContextUri, sourceUrl, targetUrl, IgnoreVersionHistory, Overwrite, AllowSchemaMismatch, sameWebCopyMoveOptimization, false, noWait).GetAwaiter().GetResult(); | ||
if (NoWait) | ||
{ | ||
WriteObject(results.jobInfo); | ||
} | ||
else | ||
{ | ||
foreach (var log in results.logs) | ||
{ | ||
if (log.Event == "JobError") | ||
{ | ||
WriteObject(log); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |