Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions documentation/Get-PnPGeoMoveCrossCompatibilityStatus.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ Accept wildcard characters: False

## OUTPUTS

### PnP.PowerShell.Commands.Model.GeoMoveTenantCompatibilityCheck
Returns objects with `SourceDataLocation`, `DestinationDataLocation`, and `GeoMoveTenantCompatibilityResult` properties.
### System.Management.Automation.PSObject
Returns objects with `SourceDataLocation`, `DestinationDataLocation`, and `CompatibilityStatus` properties.

## RELATED LINKS

Expand Down
6 changes: 3 additions & 3 deletions documentation/Get-PnPUserAndContentMoveState.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Get-PnPUserAndContentMoveState -OdbMoveId <Guid> [-Connection <PnPConnection>]
```

## DESCRIPTION
Returns status information for SharePoint Online multi-geo user and OneDrive content move jobs. You can retrieve one move job by user principal name or OneDrive move ID, or retrieve a move report filtered by state, direction, time window, and limit.
Returns status information for SharePoint Online multi-geo user and OneDrive content move jobs. You can retrieve one move job by user principal name or OneDrive move ID, or retrieve a move report filtered by state, direction, time window, and limit. When no move state or direction is specified, all states and directions are returned.

## EXAMPLES

Expand Down Expand Up @@ -150,7 +150,7 @@ Parameter Sets: MoveReport

Required: False
Position: Named
Default value: NotStarted
Default value: All
Accept pipeline input: False
Accept wildcard characters: False
```
Expand All @@ -164,7 +164,7 @@ Parameter Sets: MoveReport

Required: False
Position: Named
Default value: MoveOut
Default value: All
Accept pipeline input: False
Accept wildcard characters: False
```
Expand Down
17 changes: 15 additions & 2 deletions src/Commands/Admin/GetGeoMoveCrossCompatibilityStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@ namespace PnP.PowerShell.Commands.Admin
[Cmdlet(VerbsCommon.Get, "PnPGeoMoveCrossCompatibilityStatus")]
[RequiredApiApplicationPermissions("sharepoint/Sites.FullControl.All")]
[RequiredApiDelegatedPermissions("sharepoint/AllSites.FullControl")]
[OutputType(typeof(GeoMoveTenantCompatibilityCheck))]
[OutputType(typeof(PSObject))]
public class GetGeoMoveCrossCompatibilityStatus : PnPSharePointOnlineAdminCmdlet
{
protected override void ExecuteCmdlet()
{
var multiGeoRestApiClient = new MultiGeoRestApiClient(AdminContext);
WriteObject(multiGeoRestApiClient.GetGeoMoveCompatibilityChecks(), true);
foreach (var compatibilityCheck in multiGeoRestApiClient.GetGeoMoveCompatibilityChecks())
{
WriteObject(ConvertToPSObject(compatibilityCheck));
}

}

private static PSObject ConvertToPSObject(GeoMoveTenantCompatibilityCheck compatibilityCheck)
{
var result = new PSObject();
result.Properties.Add(new PSNoteProperty("SourceDataLocation", compatibilityCheck.SourceDataLocation));
result.Properties.Add(new PSNoteProperty("DestinationDataLocation", compatibilityCheck.DestinationDataLocation));
result.Properties.Add(new PSNoteProperty("CompatibilityStatus", compatibilityCheck.GeoMoveTenantCompatibilityResult));
return result;
}
}
}
4 changes: 2 additions & 2 deletions src/Commands/Admin/GetUserAndContentMoveState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ public class GetUserAndContentMoveState : PnPSharePointOnlineAdminCmdlet
public DateTime MoveEndTime { get; set; }

[Parameter(Mandatory = false, ParameterSetName = ParameterSetMoveReport)]
public MoveState MoveState { get; set; }
public MoveState MoveState { get; set; } = MoveState.All;

[Parameter(Mandatory = false, ParameterSetName = ParameterSetMoveReport)]
public MoveDirection MoveDirection { get; set; }
public MoveDirection MoveDirection { get; set; } = MoveDirection.All;

protected override void ExecuteCmdlet()
{
Expand Down
152 changes: 144 additions & 8 deletions src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
using PnP.Framework.Http;
using PnP.PowerShell.Commands.Model;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
Expand All @@ -24,18 +26,71 @@ internal class MultiGeoRestApiClient
private const string TenantRenameJobsPathToGetStatus = "TenantRenameJobs/Get";
private const string TenantRenameJobsPathToGetStatusV2 = "TenantRenameJobs/GetV2";
private const string TenantRenameJobsPathToCancelAJob = "TenantRenameJobs/Cancel";
private const string GeoMoveCompatibilityChecksApiVersion = "1.3.6";
private const string GeoMoveCompatibilityChecksMinimumApiVersion = "1.3.6";
private const string GeoMoveCompatibilityChecksPath = "GeoMoveCompatibilityChecks";
private const string AllowedDataLocationsApiVersion = "1.3.11";
private const string AllowedDataLocationsPath = "AllowedDataLocations";
private const string UserMoveJobsApiVersion = "1.0";
private const string UserMoveJobsByMoveIdApiVersion = "1.2.2";
private const string UserMoveJobsReportApiVersion = "1.3.2";
private const string MultiGeoApiVersionsPath = "MultiGeoApiVersions";
private const string UserMoveJobsMinimumApiVersion = "1.0";
private const string UserMoveJobsByMoveIdMinimumApiVersion = "1.2.2";
private const string UserMoveJobsReportMinimumApiVersion = "1.3.2";
private const string UserMoveJobPathByUpn = "UserMoveJobs(upn='{0}')";
private const string UserMoveJobPathByMoveId = "UserMoveJobs/GetByMoveId(odbMoveId='{0}')";
private const string UserMoveJobsPathForMoveReport = "UserMoveJobs/GetMoveReport(moveState={0},moveDirection={1},startTime='{2:u}',endTime='{3:u}',limit='{4}')";
private const int MaximumPagination = 10;
private const int ApiVersionCacheValidTimeInHours = 1;
private static readonly TimeSpan CreateTenantRenameJobTimeout = TimeSpan.FromSeconds(300);
private static readonly string[] ClientSupportedApiVersions =
[
"1.6.0",
"1.5.20",
"1.5.19",
"1.5.18",
"1.5.17",
"1.5.16",
"1.5.15",
"1.5.14",
"1.5.13",
"1.5.12",
"1.5.11",
"1.5.10",
"1.5.9",
"1.5.8",
"1.5.7",
"1.5.6",
"1.5.5",
"1.5.4",
"1.5.3",
"1.5.2",
"1.5.1",
"1.5.0",
"1.4.7",
"1.4.6",
"1.4.5",
"1.4.4",
"1.4.3",
"1.4.2",
"1.4.1",
"1.4.0",
"1.3.11",
"1.3.10",
"1.3.9",
"1.3.8",
"1.3.7",
"1.3.6",
"1.3.5",
"1.3.4",
"1.3.3-beta",
"1.3.2",
"1.3.1",
"1.3.0",
"1.2.2",
"1.2.1-beta",
"1.2-beta",
"1.1",
"1.0"
];
private static readonly ConcurrentDictionary<string, CachedApiVersion> ApiVersionCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true
Expand Down Expand Up @@ -72,7 +127,7 @@ internal IEnumerable<string> GetTenantRenameWarningMessages()

internal IEnumerable<GeoMoveTenantCompatibilityCheck> GetGeoMoveCompatibilityChecks()
{
return Get<GeoMoveCompatibilityChecks>(GeoMoveCompatibilityChecksPath, GeoMoveCompatibilityChecksApiVersion)?.GeoMoveTenantCompatibilityChecks ?? Array.Empty<GeoMoveTenantCompatibilityCheck>();
return GetFeed<GeoMoveTenantCompatibilityCheck>(GeoMoveCompatibilityChecksPath, GetCurrentApiVersion(GeoMoveCompatibilityChecksMinimumApiVersion));
}

internal IEnumerable<MultiGeoCompanyAllowedDataLocation> GetAllowedDataLocations()
Expand All @@ -82,20 +137,23 @@ internal IEnumerable<MultiGeoCompanyAllowedDataLocation> GetAllowedDataLocations

internal UserAndContentMoveState GetUserAndContentMoveState(string userPrincipalName)
{
var apiVersion = GetCurrentApiVersion(UserMoveJobsMinimumApiVersion);
var path = string.Format(CultureInfo.InvariantCulture, UserMoveJobPathByUpn, ProcessSpecialChars(userPrincipalName));
return Get<UserAndContentMoveState>(path, UserMoveJobsApiVersion);
return Get<UserAndContentMoveState>(path, apiVersion);
}

internal UserAndContentMoveState GetUserAndContentMoveState(Guid odbMoveId)
{
var apiVersion = GetCurrentApiVersion(UserMoveJobsByMoveIdMinimumApiVersion);
var path = string.Format(CultureInfo.InvariantCulture, UserMoveJobPathByMoveId, odbMoveId);
return Get<UserAndContentMoveState>(path, UserMoveJobsByMoveIdApiVersion);
return Get<UserAndContentMoveState>(path, apiVersion);
}

internal IEnumerable<UserAndContentMoveState> GetUserAndContentMoveStates(MoveState moveState, MoveDirection moveDirection, DateTime startTime, DateTime endTime, uint limit)
{
var apiVersion = GetCurrentApiVersion(UserMoveJobsReportMinimumApiVersion);
var path = string.Format(CultureInfo.InvariantCulture, UserMoveJobsPathForMoveReport, (int)moveState, (int)moveDirection, startTime, endTime, limit);
return GetFeed<UserAndContentMoveState>(path, UserMoveJobsReportApiVersion);
return GetFeed<UserAndContentMoveState>(path, apiVersion);
}

internal void CancelTenantRenameJob()
Expand All @@ -109,6 +167,12 @@ private T Get<T>(string path, string apiVersion = TenantRenameApiVersion)
return DeserializeResponse<T>(responseText);
}

private T GetWithoutApiVersion<T>(string path)
{
var responseText = Send(() => CreateRequest(HttpMethod.Get, CreateApiUri(path)), timeout: null, allowRetries: true);
return DeserializeResponse<T>(responseText);
}

private IEnumerable<T> GetFeed<T>(string path, string apiVersion)
{
var results = new List<T>();
Expand Down Expand Up @@ -186,6 +250,66 @@ private Uri CreateApiUri(string path, string apiVersion)
return new Uri($"{adminContext.Url.TrimEnd('/')}/_api/{normalizedPath}{separator}api-version={apiVersion}");
}

private Uri CreateApiUri(string path)
{
var normalizedPath = path.TrimStart('/');
return new Uri($"{adminContext.Url.TrimEnd('/')}/_api/{normalizedPath}");
}

private string GetCurrentApiVersion(string minimumApiVersion)
{
var apiVersion = GetCurrentApiVersion();
if (!IsSupportedApiVersion(apiVersion, minimumApiVersion))
{
throw new NotSupportedException($"SharePoint Online MultiGeo API version {apiVersion} does not support this operation. Minimum required version is {minimumApiVersion}.");
}

return apiVersion;
}

private string GetCurrentApiVersion()
{
var cacheKey = adminContext.Url.TrimEnd('/');
if (ApiVersionCache.TryGetValue(cacheKey, out var cachedApiVersion) && cachedApiVersion.ExpiresOnUtc > DateTime.UtcNow)
{
return cachedApiVersion.Identity;
}

var supportedVersions = GetWithoutApiVersion<ApiVersions>(MultiGeoApiVersionsPath)?.SupportedVersions;
var currentApiVersion = GetLatestClientSupportedApiVersion(supportedVersions);
ApiVersionCache[cacheKey] = new CachedApiVersion
{
Identity = currentApiVersion,
ExpiresOnUtc = DateTime.UtcNow.AddHours(ApiVersionCacheValidTimeInHours)
};

return currentApiVersion;
}

private static string GetLatestClientSupportedApiVersion(IEnumerable<string> supportedVersions)
{
if (supportedVersions == null)
{
throw new InvalidOperationException("SharePoint Online REST API did not return any supported MultiGeo API versions.");
}

var supportedVersionSet = new HashSet<string>(supportedVersions, StringComparer.OrdinalIgnoreCase);
var apiVersion = ClientSupportedApiVersions.FirstOrDefault(supportedVersionSet.Contains);
if (apiVersion == null)
{
throw new InvalidOperationException("SharePoint Online REST API did not return a supported MultiGeo API version.");
}

return apiVersion;
}

private static bool IsSupportedApiVersion(string apiVersion, string minimumApiVersion)
{
var apiVersionIndex = Array.IndexOf(ClientSupportedApiVersions, apiVersion);
var minimumApiVersionIndex = Array.IndexOf(ClientSupportedApiVersions, minimumApiVersion);
return apiVersionIndex >= 0 && minimumApiVersionIndex >= 0 && apiVersionIndex <= minimumApiVersionIndex;
}

private string Send(Func<HttpRequestMessage> requestFactory, TimeSpan? timeout, bool allowRetries)
{
var retryAttempt = 0;
Expand Down Expand Up @@ -388,5 +512,17 @@ private sealed class ODataFeed<T>

public string NextLink { get; set; }
}

private sealed class ApiVersions
{
public string[] SupportedVersions { get; set; }
}

private sealed class CachedApiVersion
{
public string Identity { get; set; }

public DateTime ExpiresOnUtc { get; set; }
}
}
}
Loading