diff --git a/app/HealthyHabitat/App.config b/app/HealthyHabitat/App.config index 99f37dd..679641c 100644 --- a/app/HealthyHabitat/App.config +++ b/app/HealthyHabitat/App.config @@ -14,9 +14,13 @@ + + + + \ No newline at end of file diff --git a/app/HealthyHabitat/HealthyHabitat.csproj b/app/HealthyHabitat/HealthyHabitat.csproj index dc899d1..98d3de7 100644 --- a/app/HealthyHabitat/HealthyHabitat.csproj +++ b/app/HealthyHabitat/HealthyHabitat.csproj @@ -60,6 +60,9 @@ ..\packages\Microsoft.Azure.Storage.Common.11.1.0\lib\net452\Microsoft.Azure.Storage.Common.dll + + ..\packages\Microsoft.Identity.Client.4.4.0\lib\net45\Microsoft.Identity.Client.dll + ..\packages\MvvmDialogs.5.3.0\lib\net45\MvvmDialogs.dll @@ -69,6 +72,9 @@ + + + ..\packages\MvvmLightLibs.5.4.1.1\lib\net45\System.Windows.Interactivity.dll diff --git a/app/HealthyHabitat/ViewModels/MainWindowViewModel.cs b/app/HealthyHabitat/ViewModels/MainWindowViewModel.cs index 1835c5f..44aa1af 100644 --- a/app/HealthyHabitat/ViewModels/MainWindowViewModel.cs +++ b/app/HealthyHabitat/ViewModels/MainWindowViewModel.cs @@ -20,6 +20,8 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Input; + using Microsoft.Identity.Client; + using Microsoft.Azure.Storage.Auth; public class MainWindowViewModel : ViewModelBase { @@ -48,6 +50,18 @@ public RelayCommand DropCommand private set; } + public RelayCommand SignInCommand + { + get; + private set; + } + + public RelayCommand SignOutCommand + { + get; + private set; + } + public ICommand ShowMessageBoxWithMessageCommand { get; } #endregion Commands @@ -137,8 +151,75 @@ public bool LocationDialogVisible } } + private bool _signInButtonVisible; + public bool SignInButtonVisible + { + get { return _signInButtonVisible; } + set + { + _signInButtonVisible = value; + RaisePropertyChanged("SignInButtonVisible"); + } + } + + private bool _signOutButtonVisible; + public bool SignOutButtonVisible + { + get { return _signOutButtonVisible; } + set + { + _signOutButtonVisible = value; + RaisePropertyChanged("SignOutButtonVisible"); + } + } + + private string _signedInUserName; + public string SignedInUserName + { + get { return _signedInUserName; } + set + { + _signedInUserName = value; + RaisePropertyChanged("SignedInUserName"); + } + } + + private string _displayMessage; + public string DisplayMessage + { + get { return _displayMessage; } + set + { + _displayMessage = value; + RaisePropertyChanged("DisplayMessage"); + } + } + + // Auth definitions ------------------------------------------------------------------- + private readonly string ClientId = ConfigurationManager.AppSettings["ApplicationId"].ToString(); + private readonly string ClientScopes = ConfigurationManager.AppSettings["ClientScopes"].ToString(); + + private IPublicClientApplication PublicClientApp { get; set; } + private AuthenticationResult AuthResult { get; set; } + + // End Auth definitions ------------------------------------------------------------------- + public MainWindowViewModel(IDialogService dialogService) { + // Auth init ------------------------------------------------------------------- + SignInButtonVisible = true; + DisplayMessage = "Please sign in."; + SignedInUserName = ""; + PublicClientApp = PublicClientApplicationBuilder.Create(ClientId) + .WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs) + .WithLogging((level, message, containsPii) => + { + Debug.WriteLine($"MSAL: {level} {message} "); + }, Microsoft.Identity.Client.LogLevel.Warning, enablePiiLogging: false, enableDefaultPlatformLogging: true) + .Build(); + + // End Auth init ------------------------------------------------------------------- + NameValueCollection locationSection = (NameValueCollection)ConfigurationManager.GetSection("locations"); Locations = new ObservableCollection(); @@ -152,6 +233,11 @@ public MainWindowViewModel(IDialogService dialogService) UploadClickCommand = new RelayCommand(() => { + if (this.SelectedLocation == null) + { + return; + } + LocationDialogVisible = false; ProgressVisible = true; @@ -175,8 +261,97 @@ public MainWindowViewModel(IDialogService dialogService) LocationDialogVisible = true; }); + + SignInCommand = new RelayCommand(async () => + { + await Authorize(); + DisplayBasicTokenInfo(); + }); + + SignOutCommand = new RelayCommand(async () => + { + await DeAuthorize(); + DisplayBasicTokenInfo(); + }); + } + + // Auth ------------------------------------------------------------------- + private async Task Authorize() + { + AuthResult = null; + + // It's good practice to not do work on the UI thread, so use ConfigureAwait(false) whenever possible. + IEnumerable accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(true); + IAccount firstAccount = accounts.FirstOrDefault(); + + try + { + AuthResult = await PublicClientApp.AcquireTokenSilent(ClientScopes.Split(), firstAccount) + .ExecuteAsync(); + } + catch (MsalUiRequiredException ex) + { + // A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token + System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}"); + + try + { + AuthResult = await PublicClientApp.AcquireTokenInteractive(ClientScopes.Split()) + .ExecuteAsync() + .ConfigureAwait(false); + } + catch (MsalException msalex) + { + DisplayMessage = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}"; + } + } + catch (Exception ex) + { + DisplayMessage = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}"; + return; + } + + if (AuthResult != null) + { + SignedInUserName = $"Signed in as: {AuthResult.Account.Username}"; + SignInButtonVisible = false; + SignOutButtonVisible = true; + } } + private async Task DeAuthorize() + { + IEnumerable accounts = await PublicClientApp.GetAccountsAsync().ConfigureAwait(false); + IAccount firstAccount = accounts.FirstOrDefault(); + + try + { + await PublicClientApp.RemoveAsync(firstAccount).ConfigureAwait(false); + AuthResult = null; + SignedInUserName = ""; + SignOutButtonVisible = false; + SignInButtonVisible = true; + } + catch (MsalException ex) + { + DisplayMessage = $"Error signing-out user: {ex.Message}"; + } + } + + public void DisplayBasicTokenInfo() + { + if (AuthResult != null) + { + DisplayMessage = $"User Name: {AuthResult.Account.Username}" + Environment.NewLine + $"Token Expires: {AuthResult.ExpiresOn.ToLocalTime()}" + Environment.NewLine; + } + else + { + DisplayMessage = "You have been signed out."; + } + } + + // End Auth ------------------------------------------------------------------- + private async void CopyFilesToLocalCache(string[] files, string location, string season) { @@ -228,11 +403,145 @@ private async void CopyFilesToLocalCache(string[] files, string location, string string[] subFolders = Directory.GetDirectories(cacheLocation); + await CreateStorageContainer(); foreach (string folder in subFolders) { - await InitiateAzCopy(folder, cacheLocation); + //await InitiateAzCopy(folder, cacheLocation); + await CreateStorageBlobs(folder, cacheLocation); + } + } + + private async Task CreateStorageContainer() + { + if (AuthResult == null) + { + await Authorize(); + } + + if (AuthResult == null) + { + return; + } + + int retryLimit = System.Convert.ToInt32(ConfigurationManager.AppSettings["RetryLimit"].ToString()); + int retrySeconds = System.Convert.ToInt32(ConfigurationManager.AppSettings["RetrySeconds"].ToString()); + string storageAccountName = ConfigurationManager.AppSettings["StorageAccountName"].ToString(); + string storageContainerName = ConfigurationManager.AppSettings["ContainerName"].ToString(); + + TokenCredential tokenCredential = new TokenCredential(AuthResult.AccessToken); + StorageCredentials storageCredentials = new StorageCredentials(tokenCredential); + + CloudBlobContainer container = new CloudBlobContainer( + new Uri($"https://{storageAccountName}.blob.core.windows.net/{storageContainerName}"), + storageCredentials); + + DisplayMessage = $"Creating storage container {container.Uri.ToString()}" + Environment.NewLine; + + int createContainerOK = 0; + while (createContainerOK < retryLimit) + { + try + { + container.CreateIfNotExists(); + createContainerOK = retryLimit + 1; + } + catch + { + createContainerOK++; + DisplayMessage += $"...Retry {createContainerOK} of {retryLimit} will start in {retrySeconds} seconds." + Environment.NewLine; + await Task.Delay(retrySeconds * 1000); + } + + if (createContainerOK == retryLimit) + { + DisplayMessage += $"Error creating container, check that you have the correct permissions for this storage account." + Environment.NewLine; + } + } + } + + private async Task CreateStorageBlobs(string sourceDirectory, string cacheDirectory) + { + if (AuthResult == null) + { + return; + } + + int retryLimit = System.Convert.ToInt32(ConfigurationManager.AppSettings["RetryLimit"].ToString()); + int retrySeconds = System.Convert.ToInt32(ConfigurationManager.AppSettings["RetrySeconds"].ToString()); + string storageAccountName = ConfigurationManager.AppSettings["StorageAccountName"].ToString(); + string storageContainerName = ConfigurationManager.AppSettings["ContainerName"].ToString(); + + TokenCredential tokenCredential = new TokenCredential(AuthResult.AccessToken); + StorageCredentials storageCredentials = new StorageCredentials(tokenCredential); + + string sourceDirName = new DirectoryInfo(sourceDirectory).Name; + + foreach (string dir in Directory.GetDirectories(sourceDirectory)) + { + string dirName = new DirectoryInfo(dir).Name; + DisplayMessage += $"Uploading {dir}" + Environment.NewLine; + + int uploadDirectoryOK = 0; + foreach (string file in Directory.GetFiles(dir)) + { + string fileName = new FileInfo(file).Name; + + CloudBlockBlob blob = new CloudBlockBlob( + new Uri($"https://{storageAccountName}.blob.core.windows.net/{storageContainerName}/{sourceDirName}/{dirName}/{fileName}"), + storageCredentials); + + DisplayMessage += $"Creating storage blob {blob.Uri.ToString()}" + Environment.NewLine; + + int createBlobOK = 0; + while (createBlobOK < retryLimit) + { + try + { + await blob.UploadFromFileAsync(file); + createBlobOK = retryLimit + 1; + File.Delete(file); + } + catch + { + createBlobOK++; + DisplayMessage += $"...Retry {createBlobOK} of {retryLimit} will start in {retrySeconds} seconds." + Environment.NewLine; + await Task.Delay(retrySeconds * 1000); + } + + if (createBlobOK == retryLimit) + { + DisplayMessage += $"Error creating blob, check that you have the correct permissions for this storage account." + Environment.NewLine; + } + } + + if (createBlobOK != retryLimit + 1) + { + uploadDirectoryOK++; + } + + if (uploadDirectoryOK == retryLimit) + { + DisplayMessage += $"Failed upload, too many consecutive errors." + Environment.NewLine; + break; + } + } + + if (Directory.GetFiles(dir).Length == 0) + { + DisplayMessage += "Upload successful."; + Directory.Delete(dir, true); + } + } + + if (Directory.GetDirectories(sourceDirectory).Length == 0) + { + Directory.Delete(sourceDirectory, true); + } + + if (Directory.GetDirectories(cacheDirectory).Length == 0) + { + Directory.Delete(cacheDirectory, true); } - } private async Task InitiateAzCopy(string sourceDir, string parentDir) diff --git a/app/HealthyHabitat/Views/MainWindow.xaml b/app/HealthyHabitat/Views/MainWindow.xaml index 286b135..7bfbb50 100644 --- a/app/HealthyHabitat/Views/MainWindow.xaml +++ b/app/HealthyHabitat/Views/MainWindow.xaml @@ -243,5 +243,94 @@ + + + + + + + + + diff --git a/app/HealthyHabitat/packages.config b/app/HealthyHabitat/packages.config index 9ad31ba..c69ea12 100644 --- a/app/HealthyHabitat/packages.config +++ b/app/HealthyHabitat/packages.config @@ -4,7 +4,11 @@ + + + + \ No newline at end of file