Skip to content

Commit

Permalink
Separated code
Browse files Browse the repository at this point in the history
  • Loading branch information
svrooij committed Aug 13, 2023
1 parent 60cef4b commit 84dd92a
Show file tree
Hide file tree
Showing 18 changed files with 411 additions and 213 deletions.
7 changes: 1 addition & 6 deletions src/WingetIntune.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ private static async Task<int> Main(string[] args)
{
host.ConfigureServices(services =>
{
services.AddHttpClient();
services.AddTransient<IFileManager, DefaultFileManager>();
services.AddTransient<IProcessManager, ProcessManager>();
services.AddTransient<IWingetRepository, WingetManager>();
services.AddHttpClient<IntuneManager>();
services.AddSingleton<Internal.Msal.PublicClientAuth>();
services.AddWingetServices();
});
})
.UseDefaults()
Expand Down
1 change: 1 addition & 0 deletions src/WingetIntune.Cli/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@
"type": "Project",
"dependencies": {
"Azure.Storage.Blobs": "[12.17.0, )",
"Microsoft.Extensions.Http": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.1, )",
"Microsoft.Extensions.Options": "[7.0.1, )",
"Microsoft.Graph.Beta": "[5.43.0-preview, )",
Expand Down
99 changes: 99 additions & 0 deletions src/WingetIntune/GraphExtensions/GraphServiceClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Microsoft.Graph.Beta;
using Microsoft.Graph.Beta.Models;
using Microsoft.Graph.Beta.Models.ODataErrors;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using WingetIntune.Intune;

namespace WingetIntune.GraphExtensions;
internal static class GraphServiceClientExtensions
{
// These extensions are on the service client, not the request builder.
// until this issue is resolved: https://github.com/microsoft/kiota-abstractions-dotnet/issues/113

public static Task<Entity?> Intune_CreateWin32LobAppContentVersionAsync(this GraphServiceClient graphServiceClient, string win32LobAppId, CancellationToken cancellationToken)
{
var requestInfo = new RequestInformation
{
HttpMethod = Method.POST,
URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions"),
};
requestInfo.Headers.Add("Content-Type", "application/json");
requestInfo.Content = new MemoryStream(Encoding.UTF8.GetBytes("{}"));

return graphServiceClient.RequestAdapter.SendAsync<Entity>(requestInfo, Entity.CreateFromDiscriminatorValue, errorMapping: ErrorMapping, cancellationToken: cancellationToken);

}

public static Task<MobileAppContentFile?> Intune_CreateWin32LobAppContentVersionFileAsync(this GraphServiceClient graphServiceClient, string win32LobAppId, string contentVersionId, MobileAppContentFile mobileAppContentFile, CancellationToken cancellationToken)
{
var requestInfo = new RequestInformation
{
HttpMethod = Method.POST,
URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files"),
};
requestInfo.SetContentFromParsable(graphServiceClient.RequestAdapter, "application/json", mobileAppContentFile);
return graphServiceClient.RequestAdapter.SendAsync<MobileAppContentFile>(requestInfo, MobileAppContentFile.CreateFromDiscriminatorValue, errorMapping: ErrorMapping, cancellationToken: cancellationToken);
}

public static Task<MobileAppContentFile?> Intune_GetWin32LobAppContentVersionFileAsync(this GraphServiceClient graphServiceClient, string win32LobAppId, string contentVersionId, string mobileAppContentFileId, CancellationToken cancellationToken)
{
var requestInfo = new RequestInformation
{
HttpMethod = Method.GET,
URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{mobileAppContentFileId}"),
};
return graphServiceClient.RequestAdapter.SendAsync<MobileAppContentFile>(requestInfo, MobileAppContentFile.CreateFromDiscriminatorValue, errorMapping: ErrorMapping, cancellationToken: cancellationToken);
}

public static async Task<MobileAppContentFile?> Intune_WaitForFinalCommitStateAsync(this GraphServiceClient graphServiceClient, string win32LobAppId, string contentVersionId, string mobileAppContentFileId, CancellationToken cancellationToken)
{
while(true)
{
var result = await graphServiceClient.Intune_GetWin32LobAppContentVersionFileAsync(win32LobAppId, contentVersionId, mobileAppContentFileId, cancellationToken)!;
switch(result!.UploadState)
{
case MobileAppContentFileUploadState.CommitFileSuccess:
return result;
case MobileAppContentFileUploadState.CommitFilePending:
await Task.Delay(1000, cancellationToken);
break;
case MobileAppContentFileUploadState.CommitFileFailed:
throw new Exception("Commit failed");
case MobileAppContentFileUploadState.CommitFileTimedOut:
throw new Exception("Commit timed out");

default:
throw new Exception("Unexpected state");
}
}
}

public static Task Intune_CommitWin32LobAppContentVersionFileAsync(this GraphServiceClient graphServiceClient, string win32LobAppId, string contentVersionId, string mobileAppContentFileId, FileEncryptionInfo fileEncryptionInfo, CancellationToken cancellationToken)
{
var body = new MobileAppContentFileCommitBody
{
FileEncryptionInfo = fileEncryptionInfo,
};
var data = JsonSerializer.SerializeToUtf8Bytes(body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var requestInfo = new RequestInformation
{
HttpMethod = Method.POST,
Content = new MemoryStream(data),
URI = new Uri($"https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/{win32LobAppId}/microsoft.graph.win32LobApp/contentVersions/{contentVersionId}/files/{mobileAppContentFileId}/commit"),
};
requestInfo.Headers.Add("Content-Type", "application/json");
return graphServiceClient.RequestAdapter.SendNoContentAsync(requestInfo, errorMapping: ErrorMapping, cancellationToken: cancellationToken);
}

public static Dictionary<string, ParsableFactory<IParsable>> ErrorMapping => new Dictionary<string, ParsableFactory<IParsable>> {
{"4XX", ODataError.CreateFromDiscriminatorValue},
{"5XX", ODataError.CreateFromDiscriminatorValue},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Graph.Beta.DeviceAppManagement.MobileApps;
using Microsoft.Graph.Beta.Models;
using Microsoft.Graph.Beta.Models.ODataErrors;
using Microsoft.Kiota.Abstractions.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WingetIntune.GraphExtensions;
internal static class MobileAppsRequestBuilderExtensions
{
public static async Task<Win32LobApp?> PostAsync(this MobileAppsRequestBuilder builder, Win32LobApp win32LobApp, CancellationToken cancellationToken)
{
return await builder.PostAsync(win32LobApp, cancellationToken: cancellationToken) as Win32LobApp;
}

public static async Task<Win32LobApp?> PatchAsync(this MobileAppsRequestBuilder builder, Win32LobApp win32LobApp, CancellationToken cancellationToken)
{
return await builder.PatchAsync(win32LobApp, cancellationToken: cancellationToken) as Win32LobApp;
}


}
60 changes: 60 additions & 0 deletions src/WingetIntune/Implementations/AzCopyAzureUploader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WingetIntune;
internal class AzCopyAzureUploader : IAzureFileUploader
{
private readonly string azCopyPath;
private readonly ILogger<AzCopyAzureUploader> logger;
private readonly IProcessManager processManager;
private readonly IFileManager fileManager;

public AzCopyAzureUploader(ILogger<AzCopyAzureUploader> logger, IProcessManager processManager, IFileManager fileManager)
{
azCopyPath = Path.Combine(Path.GetTempPath(), "intunewin", "azcopy.exe");
this.logger = logger;
this.processManager = processManager;
this.fileManager = fileManager;
}

private async Task DownloadAzCopyIfNeeded(CancellationToken cancellationToken)
{
if(!fileManager.FileExists(azCopyPath))
{
logger.LogInformation("Downloading AzCopy to {azCopyPath}", azCopyPath);
var azCopyDownloadUrl = "https://aka.ms/downloadazcopy-v10-windows";
var downloadPath = Path.GetTempFileName();
await fileManager.DownloadFileAsync(azCopyDownloadUrl, downloadPath, overrideFile: true, cancellationToken);


var extractFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
logger.LogInformation("Extracting AzCopy to {path}", extractFolder);
fileManager.ExtractFileToFolder(downloadPath, extractFolder);
var azCopyExe = fileManager.FindFile(extractFolder, "azcopy.exe");
fileManager.CopyFile(azCopyExe, azCopyPath);
fileManager.DeleteFileOrFolder(extractFolder);
fileManager.DeleteFileOrFolder(downloadPath);
}
}

public async Task UploadFileToAzureAsync(string filename, Uri sasUri, CancellationToken cancellationToken)
{
await DownloadAzCopyIfNeeded(cancellationToken);
var args = $"copy \"{filename}\" \"{sasUri}\" --output-type \"json\"";
var result = await processManager.RunProcessAsync(azCopyPath, args, cancellationToken);
logger.LogInformation("AzCopy result: {result}", result);
if (result.ExitCode != 0)
{
var exception = new Exception($"AzCopy resulted in a non-zero exitcode.");
exception.Data.Add("ExitCode", result.ExitCode);
exception.Data.Add("Output", result.Output);
exception.Data.Add("Error", result.Error);
logger.LogWarning(exception, "AzCopy resulted in a non-zero exitcode.");
throw exception;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ namespace WingetIntune;
public partial class DefaultFileManager : IFileManager
{
private readonly ILogger<DefaultFileManager> logger;
private readonly HttpClient httpClient;

public DefaultFileManager(ILogger<DefaultFileManager> logger)
public DefaultFileManager(ILogger<DefaultFileManager> logger, HttpClient? httpClient)
{
this.logger = logger;
this.httpClient = httpClient ?? new HttpClient();
}

public void CopyFile(string sourcePath, string destinationPath, bool overwrite = false)
{
var destinationFolder = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(destinationFolder))
Directory.CreateDirectory(destinationFolder!);
File.Copy(sourcePath, destinationPath, overwrite);
}

public void CreateFolder(string path)
Expand All @@ -26,12 +36,56 @@ public string CreateFolderForPackage(string parentFolder, string packageName, st
return folder;
}

public void DeleteFileOrFolder(string path)
{
if(Path.Exists(path))
{
if (Directory.Exists(path))
Directory.Delete(path, recursive: true);
else
File.Delete(path);
}
}

public async Task DownloadFileAsync(string url, string path, bool overrideFile = false, CancellationToken cancellationToken = default)
{
if(overrideFile || !File.Exists(path))
{
logger.LogInformation("Downloading {url} to {path}", url, path);
var result = await httpClient.GetAsync(url, cancellationToken);
result.EnsureSuccessStatusCode();
var data = await result.Content.ReadAsByteArrayAsync(cancellationToken);
await File.WriteAllBytesAsync(path, data, cancellationToken);
}
else
{
logger.LogInformation("Skipping download of {url} to {path} because the file already exists", url, path);
}
}

public void ExtractFileToFolder(string zipPath, string folderPath)
{
System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, folderPath);
}

public bool FileExists(string path)
{
LogFileExists(path);
return File.Exists(path);
}

public string FindFile(string folder, string filename)
{
// Recursursively search for the file in the folder
foreach (var file in Directory.GetFiles(folder, filename, SearchOption.AllDirectories))
{
if (Path.GetFileName(file).Equals(filename, StringComparison.OrdinalIgnoreCase))
return file;
}

throw new FileNotFoundException($"Could not find file {filename} in folder {folder}");
}

public Task<byte[]> ReadAllBytesAsync(string path, CancellationToken cancellationToken)
{
return File.ReadAllBytesAsync(path, cancellationToken);
Expand Down
File renamed without changes.
File renamed without changes.
11 changes: 11 additions & 0 deletions src/WingetIntune/Interfaces/IAzureFileUploader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WingetIntune;
public interface IAzureFileUploader
{
Task UploadFileToAzureAsync(string filename, Uri sasUri, CancellationToken cancellationToken);
}
9 changes: 9 additions & 0 deletions src/WingetIntune/Interfaces/IFileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

public interface IFileManager
{
void CopyFile(string sourcePath, string destinationPath, bool overwrite = false);
void CreateFolder(string path);

string CreateFolderForPackage(string parentFolder, string packageName, string packageVersion);

void DeleteFileOrFolder(string path);

Task DownloadFileAsync(string url, string path, bool overrideFile = false, CancellationToken cancellationToken = default);

void ExtractFileToFolder(string zipPath, string folderPath);

string FindFile(string folder, string filename);

bool FileExists(string path);

Task<byte[]> ReadAllBytesAsync(string path, CancellationToken cancellationToken);
Expand Down
Loading

0 comments on commit 84dd92a

Please sign in to comment.