Skip to content

Commit 45db45a

Browse files
authored
Merge pull request #433 from microsoftgraph/po/StreamSupport
Stream Support Improvements
2 parents 0586d7d + fb50ec5 commit 45db45a

File tree

8 files changed

+222
-6
lines changed

8 files changed

+222
-6
lines changed

config/ModulesMapping.jsonc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"Files": "^drives\\.|^shares\\.|^users.drive$|^groups.drive$",
1414
"Financials": "^financials\\.",
1515
"Groups": "^groups.group$|^groups.directoryObject$|^groups.conversation$|^groups.endpoint$|^groups.extension$|^groups.resourceSpecificPermissionGrant$|^groups.profilePhoto$|^groups.conversationThread$|^groupLifecyclePolicies\\.|^users.group$|^groups.directorySetting$|^groups.Actions$|^groups.Functions$",
16-
"Identity.DirectoryManagement": "^administrativeUnits\\.|^contacts\\.|^devices\\.|^domains\\.|^directoryRoles\\.|^directoryRoleTemplates\\.|^directorySettingTemplates\\.|^settings\\.|^subscribedSkus\\.|^contracts\\.|^directory\\.|^users.scopedRoleMembership$|^organization.organization$|^organization.organizationalBranding$|^organization.organizationSettings $|^organization.Actions$|^organization.extension$",
16+
"Identity.DirectoryManagement": "^administrativeUnits\\.|^contacts\\.|^devices\\.|^domains\\.|^directoryRoles\\.|^directoryRoleTemplates\\.|^directorySettingTemplates\\.|^settings\\.|^subscribedSkus\\.|^contracts\\.|^directory\\.|^users.scopedRoleMembership$|^organization.organization$|^organization.organizationalBranding$|^organization.organizationSettings$|^organization.Actions$|^organization.extension$",
1717
"Identity.Governance": "^accessReviews\\.|^businessFlowTemplates\\.|^programs\\.|^programControls\\.|^programControlTypes\\.|^privilegedRoles\\.|^privilegedRoleAssignments\\.|^privilegedRoleAssignmentRequests\\.|^privilegedApproval\\.|^privilegedOperationEvents\\.|^privilegedAccess\\.|^agreements\\.|^users.agreementAcceptance$|^identityGovernance.entitlementManagement$|^identityGovernance.Functions$|^identityGovernance.Actions$",
1818
"Identity.SignIns": "^organization.certificateBasedAuthConfiguration$|^invitations\\.|^identityProviders\\.|^oauth2PermissionGrants\\.|^riskDetections\\.|^riskyUsers\\.|^dataPolicyOperations\\.|^identity.identityUserFlow$|^trustFramework\\.|^informationProtection\\.|^policies\\.|^users.authentication$|^users.informationProtection$|^identity.conditionalAccessRoot$",
1919
"Mail": "^users.inferenceClassification$|^users.mailFolder$|^users.message$",
@@ -26,7 +26,7 @@
2626
"Search": "^search\\.|^external\\.",
2727
"Security": "^Security\\.",
2828
"Sites": "^sites.site$|^sites.itemAnalytics$|^sites.columnDefinition$|^sites.contentType$|^sites.drive$|^sites.list$|^sites.sitePage$|^users.site$|^groups.site$|^sites.Functions$|^sites.Actions$",
29-
"Teams": "^teams\\.|^chats\\.|^users.chat$|^appCatalogs$|^users.userTeamwork$|^teamwork\\.|^users.team$|^users.userTeamwork$|^groups.team$",
29+
"Teams": "^teams\\.|^chats\\.|^users.chat$|^appCatalogs.teamsApp$|^users.userTeamwork$|^teamwork\\.|^users.team$|^groups.team$",
3030
"Users": "^users.user$|^users.directoryObject$|^users.licenseDetails$|^users.notification$|^users.outlookUser$|^users.profilePhoto$|^users.userSettings$|^users.extension$|^users.oAuth2PermissionGrant$|^users.todo$",
3131
"Users.Actions": "^users.Actions$",
3232
"Users.Functions": "^users.Functions$"

src/Applications/Applications/readme.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ subject-prefix: ''
3838
3939
``` yaml
4040
directive:
41+
# Remove invalid paths.
42+
- remove-path-by-operation: onPremisesPublishingProfiles\.(connectors\.memberOf_.*|connectors_GetMemberOf|connectorGroups\.members_.*|connectorGroups_(Get|Create|Update|Delete)Members)
43+
4144
# Remove cmdlets
4245
- where:
4346
verb: Test
@@ -69,6 +72,11 @@ directive:
6972
subject: ^OnPremis(PublishingProfile.*)$
7073
set:
7174
subject: OnPremise$1
75+
# Fix cmdlet name
76+
- where:
77+
subject: (^OnPremisePublishingProfileConnectorMember$)
78+
set:
79+
subject: $1Of
7280
```
7381
### Versioning
7482

src/Authentication/Authentication/Common/DiskDataStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.Graph.PowerShell.Authentication.Common
1111
/// <summary>
1212
/// Disk data store based on System.IO APIs.
1313
/// </summary>
14-
internal class DiskDataStore : IDataStore
14+
public class DiskDataStore : IDataStore
1515
{
1616
/// <summary>
1717
/// Writes the given contents to the specified file.

src/Authentication/Authentication/Common/ProtectedFileProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public enum FileProtection
2222
/// The file can be accessed in ReadOnly or ReadWrite mode.
2323
/// This class MUST be disposed by the caller.
2424
/// </summary>
25-
internal abstract class ProtectedFileProvider : IFileProvider, IDisposable
25+
public abstract class ProtectedFileProvider : IFileProvider, IDisposable
2626
{
2727
protected Stream _stream;
2828
public const int MaxTries = 30;

src/Files/Files/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ subject-prefix: ''
3838
3939
``` yaml
4040
directive:
41-
- remove-path-by-operation: .*_(Create|Get|Update|Set|Delete)Activities$|.*\.activities.*$
41+
- remove-path-by-operation: .*_(Create|Get|Update|Set|Delete)Activities$|.*\.activities.*$|shares\..*_createLink
4242
```
4343
### Versioning
4444

src/readme.graph.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
azure: false
77
powershell: true
88
version: latest
9-
use: "@autorest/powershell@latest"
9+
use: "@autorest/powershell@2.1.401"
1010
metadata:
1111
authors: Microsoft Corporation
1212
owners: Microsoft Corporation
@@ -555,6 +555,49 @@ directive:
555555
return $;
556556
}
557557
558+
# Modify generated .cs file download cmdlets.
559+
- from: source-file-csharp
560+
where: $
561+
transform: >
562+
if (!$documentPath.match(/generated%2Fcmdlets%2FGet\w*\d*.cs/gm))
563+
{
564+
return $;
565+
} else {
566+
let outFileParameterRegex = /(^\s*)public\s*global::System\.String\s*OutFile\s*/gmi
567+
let streamResponseRegex = /global::System\.Threading\.Tasks\.Task<global::System\.IO\.Stream>\s*response/gmi
568+
if($.match(outFileParameterRegex) && $.match(streamResponseRegex)) {
569+
// Handle file download.
570+
let overrideOnOkCallRegex = /(^\s*)(overrideOnOk\(\s*responseMessage\s*,\s*response\s*,\s*ref\s*_returnNow\s*\);)/gmi
571+
$ = $.replace(overrideOnOkCallRegex, '$1$2\n$1using(var stream = await response){ this.WriteToFile(responseMessage, stream, this.GetProviderPath(OutFile, false), _cancellationTokenSource.Token); _returnNow = global::System.Threading.Tasks.Task<bool>.FromResult(true);}\n$1');
572+
}
573+
return $;
574+
}
575+
576+
# Modify generated .cs file upload cmdlets.
577+
- from: source-file-csharp
578+
where: $
579+
transform: >
580+
if (!$documentPath.match(/generated%2Fcmdlets%2FSet\w*\d*.cs/gm))
581+
{
582+
return $;
583+
} else {
584+
let streamBodyParameterRegex = /(^\s*)public\s*global::System.IO.Stream\s*BodyParameter\s*/gmi
585+
if($.match(streamBodyParameterRegex)) {
586+
// Replace base class with FileUploadCmdlet.
587+
let psBaseClassImplementationRegex = /(\s*:\s*)(global::System.Management.Automation.PSCmdlet)/gmi
588+
$ = $.replace(psBaseClassImplementationRegex, '$1Microsoft.Graph.PowerShell.Cmdlets.Custom.FileUploadCmdlet');
589+
590+
// Set bodyParameter to required to false.
591+
let streamBodyParameterAnnotation = /(global::System\.IO\.Stream _bodyParameter;\s*\[global::System\.Management\.Automation\.Parameter\(Mandatory\s*=\s*)(true)/gmi
592+
$ = $.replace(streamBodyParameterAnnotation, '$1false');
593+
594+
// Handle file upload.
595+
let processRecordCallRegex = /(^\s*)(asyncCommandRuntime\.Wait\(\s*ProcessRecordAsync\s*\(\))/gmi
596+
$ = $.replace(processRecordCallRegex, '$1if (!MyInvocation.BoundParameters.ContainsKey(nameof(BodyParameter))){BodyParameter = GetFileAsStream() ?? BodyParameter;}\n$1$2');
597+
}
598+
return $;
599+
}
600+
558601
# Modify generated runtime TypeConverterExtensions class.
559602
- from: source-file-csharp
560603
where: $

tools/Custom/FileUploadCmdlet.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
namespace Microsoft.Graph.PowerShell.Cmdlets.Custom
5+
{
6+
using Microsoft.Graph.PowerShell.Authentication.Common;
7+
using System.IO;
8+
using System.Management.Automation;
9+
10+
public partial class FileUploadCmdlet : PSCmdlet
11+
{
12+
/// <summary>Backing field for <see cref="InFile" /> property.</summary>
13+
private string _inFile;
14+
15+
/// <summary>The path to the file to upload. This SHOULD include the file name and extension.</summary>
16+
[Parameter(Mandatory = true, HelpMessage = "The path to the file to upload. This should include a path and file name. If you omit the path, the current location will be used.")]
17+
[Runtime.Info(
18+
Required = true,
19+
ReadOnly = false,
20+
Description = @"The path to the file to upload. This should include a path and file name. If you omit the path, the current location will be used.",
21+
PossibleTypes = new[] { typeof(string) })]
22+
[ValidateNotNullOrEmpty()]
23+
[Category(ParameterCategory.Runtime)]
24+
public string InFile { get => this._inFile; set => this._inFile = value; }
25+
26+
/// <summary>
27+
/// Creates a file stream from the provided input file.
28+
/// </summary>
29+
/// <returns>A file stream.</returns>
30+
internal Stream GetFileAsStream()
31+
{
32+
if (MyInvocation.BoundParameters.ContainsKey(nameof(InFile)))
33+
{
34+
string resolvedFilePath = this.GetProviderPath(InFile, true);
35+
var fileProvider = ProtectedFileProvider.CreateFileProvider(resolvedFilePath, FileProtection.SharedRead, new DiskDataStore());
36+
return fileProvider.Stream;
37+
}
38+
else
39+
{
40+
return null;
41+
}
42+
}
43+
}
44+
}

tools/Custom/PSCmdletExtensions.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
namespace Microsoft.Graph.PowerShell
5+
{
6+
using Microsoft.Graph.PowerShell.Authentication.Common;
7+
using System;
8+
using System.Collections.ObjectModel;
9+
using System.IO;
10+
using System.Management.Automation;
11+
using System.Net.Http;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
15+
internal static class PSCmdletExtensions
16+
{
17+
/// <summary>
18+
/// Gets a resolved or unresolved path from PSPath.
19+
/// </summary>
20+
/// <param name="cmdlet">The calling <see cref="PSCmdlet"/>.</param>
21+
/// <param name="filePath">The file path to get a provider path for.</param>
22+
/// <param name="isResolvedPath">Determines whether get a resolved or unresolved provider path.</param>
23+
/// <returns>The provider path from PSPath.</returns>
24+
internal static string GetProviderPath(this PSCmdlet cmdlet, string filePath, bool isResolvedPath)
25+
{
26+
string providerPath = null;
27+
ProviderInfo provider;
28+
try
29+
{
30+
var paths = new Collection<string>();
31+
if (isResolvedPath)
32+
{
33+
paths = cmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out provider);
34+
}
35+
else
36+
{
37+
paths.Add(cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out provider, out _));
38+
}
39+
40+
if (provider.Name != "FileSystem" || paths.Count == 0)
41+
{
42+
cmdlet.ThrowTerminatingError(new ErrorRecord(new Exception("Invalid path."), string.Empty, ErrorCategory.InvalidArgument, filePath));
43+
}
44+
if (paths.Count > 1)
45+
{
46+
cmdlet.ThrowTerminatingError(new ErrorRecord(new Exception("Multiple paths not allowed."), string.Empty, ErrorCategory.InvalidArgument, filePath));
47+
}
48+
providerPath = paths[0];
49+
}
50+
catch (Exception)
51+
{
52+
providerPath = filePath;
53+
}
54+
55+
return providerPath;
56+
}
57+
58+
/// <summary>
59+
/// Saves a stream to a file on disk.
60+
/// </summary>
61+
/// <param name="cmdlet">The calling <see cref="PSCmdlet"/>.</param>
62+
/// <param name="response">The HTTP response from the service.</param>
63+
/// <param name="inputStream">The stream to write to file.</param>
64+
/// <param name="filePath">The path to write the file to. This should include the file name and extension.</param>
65+
/// <param name="cancellationToken">A cancellation token that will be used to cancel the operation by the user.</param>
66+
internal static void WriteToFile(this PSCmdlet cmdlet, HttpResponseMessage response, Stream inputStream, string filePath, CancellationToken cancellationToken)
67+
{
68+
using (var fileProvider = ProtectedFileProvider.CreateFileProvider(filePath, FileProtection.ExclusiveWrite, new DiskDataStore()))
69+
{
70+
string downloadUrl = response?.RequestMessage?.RequestUri.ToString();
71+
cmdlet.WriteToStream(inputStream, fileProvider.Stream, downloadUrl, cancellationToken);
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Writes an input stream to an output stream.
77+
/// </summary>
78+
/// <param name="cmdlet">The calling <see cref="PSCmdlet"/>.</param>
79+
/// <param name="inputStream">The stream to write to an output stream.</param>
80+
/// <param name="outputStream">The stream to write the input stream to.</param>
81+
/// <param name="cancellationToken">A cancellation token that will be used to cancel the operation by the user.</param>
82+
private static void WriteToStream(this PSCmdlet cmdlet, Stream inputStream, Stream outputStream, string downloadUrl, CancellationToken cancellationToken)
83+
{
84+
Task copyTask = inputStream.CopyToAsync(outputStream);
85+
ProgressRecord record = new ProgressRecord(
86+
activityId: 0,
87+
activity: $"Downloading {downloadUrl ?? "file"}",
88+
statusDescription: $"{outputStream.Position} of {outputStream.Length} bytes downloaded.");
89+
try
90+
{
91+
do
92+
{
93+
cmdlet.WriteProgress(GetProgress(record, outputStream));
94+
95+
Task.Delay(1000, cancellationToken).Wait(cancellationToken);
96+
} while (!copyTask.IsCompleted && !cancellationToken.IsCancellationRequested);
97+
98+
if (copyTask.IsCompleted)
99+
{
100+
cmdlet.WriteProgress(GetProgress(record, outputStream));
101+
}
102+
}
103+
catch (OperationCanceledException)
104+
{
105+
}
106+
}
107+
108+
/// <summary>
109+
/// Calculates and updates the progress record of the provided stream.
110+
/// </summary>
111+
/// <param name="currentProgressRecord">The <see cref="ProgressRecord"/> to update.</param>
112+
/// <param name="stream">The stream to calculate its progress.</param>
113+
/// <returns>An updated <see cref="ProgressRecord"/>.</returns>
114+
private static ProgressRecord GetProgress(ProgressRecord currentProgressRecord, Stream stream)
115+
{
116+
currentProgressRecord.StatusDescription = $"{stream.Position} of {stream.Length} bytes downloaded.";
117+
currentProgressRecord.PercentComplete = (int)Math.Round((double)(100 * stream.Position) / stream.Length);
118+
return currentProgressRecord;
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)