/// <summary> /// Force to synchronize that data with the cloud. /// </summary> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> internal async Task SynchronizeAsync() { if (IsSynchronizing) { return; } Logger.Instance.Information("Synchronization with the cloud started."); IsSynchronizing = true; if (CurrentCloudStorageProvider == null) { Logger.Instance.Warning("The user is not logged to any cloud storage provider. The synchronization stopped."); IsSynchronizing = false; return; } if (!CoreHelper.IsUnitTesting() && !SystemInfoHelper.CheckForInternetConnection()) { Logger.Instance.Warning("There is no internet connection. The synchronization stopped."); IsSynchronizing = false; return; } SynchronizationStarted?.Invoke(this, new EventArgs()); try { if (!await CurrentCloudStorageProvider.TryAuthenticateAsync()) { Logger.Instance.Warning("The user is not authenticated correctly. Consider unlink the app and connect again. The synchronization stopped."); IsSynchronizing = false; SynchronizationEnded?.Invoke(this, new EventArgs()); return; } var userId = SecurityHelper.ToSecureString(await CurrentCloudStorageProvider.GetUserIdAsync()); if (string.IsNullOrWhiteSpace(SecurityHelper.ToUnsecureString(userId))) { Logger.Instance.Warning("The user's id from the cloud storage provider has not been found. The synchronization stopped."); IsSynchronizing = false; SynchronizationEnded?.Invoke(this, new EventArgs()); SynchronizationFailed?.Invoke(this, new EventArgs()); return; } Logger.Instance.Information("Freezing the data before synchronize."); var dataService = ServiceLocator.GetService <DataService>(); var cloudDataEntryFromServer = new List <CloudDataEntry>(); var cloudAppFolder = await CurrentCloudStorageProvider.GetAppFolderAsync(); var cloudDataEntryFilePath = Path.Combine(cloudAppFolder.FullPath, Consts.DataEntryFileName); var cloudDataPassword = SecurityHelper.ToSecureString(SecurityHelper.EncryptString(userId, SecurityHelper.ToSecureString(await CurrentCloudStorageProvider.GetUserNameAsync()))); var localFrozenDataEntries = DataHelper.FromByteArray <AsyncObservableCollection <DataEntry> >(DataHelper.ToByteArray(dataService.DataEntries)); var localFrozenCache = DataHelper.FromByteArray <List <DataEntryCache> >(DataHelper.ToByteArray(dataService.Cache)); // Download data from server. if (cloudAppFolder.Files.Any(file => file.FullPath == cloudDataEntryFilePath)) { Logger.Instance.Information("Downloading the data entry file from the server."); try { using (var memoryStream = new MemoryStream()) { await CurrentCloudStorageProvider.DownloadFileAsync(cloudDataEntryFilePath, memoryStream); memoryStream.Position = 0; using (var aesStream = new AesStream(memoryStream, cloudDataPassword, SecurityHelper.GetSaltKeys(cloudDataPassword).GetBytes(16))) { var data = new byte[aesStream.Length]; aesStream.Read(data, 0, data.Length); cloudDataEntryFromServer = JsonConvert.DeserializeObject <List <CloudDataEntry> >(Encoding.UTF8.GetString(data)); } } } catch (Exception exception) { Logger.Instance.Warning($"Unable to download or read the data file entry from the cloud for the following reason : {exception.Message}"); IsSynchronizing = false; SynchronizationEnded?.Invoke(this, new EventArgs()); SynchronizationFailed?.Invoke(this, new EventArgs()); return; } } else { Logger.Instance.Information("There is no data entry file on the server yet."); } // Synchronize locally the data. The result must corresponds to what we will have locally and on the server at the end of the synchronization process. var cloudDataEntryToServer = dataService.DifferenceLocalAndCloudDataEntries(cloudDataEntryFromServer); // Download the needed data from the server to the local machine. Logger.Instance.Information("Downloading the needed data from the server to the local machine."); var dataToDownload = cloudDataEntryFromServer.Cast <DataEntryBase>().Except(localFrozenDataEntries, (item1, item2) => item1.Identifier == item2.Identifier).ToList(); var taskList = new List <Task>(); foreach (var cloudDataEntry in dataToDownload) { if (dataToDownload.Any(data => localFrozenCache.Any(item => data.Identifier == item.Identifier && item.Status == DataEntryStatus.Deleted))) { continue; } foreach (var dataEntryDataIdentifier in cloudDataEntry.DataIdentifiers) { var task = DownloadDataFileAsync(dataService.ClipboardDataPath, cloudAppFolder, cloudDataPassword, dataEntryDataIdentifier); taskList.Add(task); } } await Task.WhenAll(taskList); // Delete the needed data from the server Logger.Instance.Information("Deleting the needed data from the server."); taskList = new List <Task>(); foreach (var dataServiceDataEntry in localFrozenDataEntries.Where(item => !item.CanSynchronize)) { foreach (var dataEntryDataIdentifier in dataServiceDataEntry.DataIdentifiers) { var task = DeleteFileAsync(cloudAppFolder, dataEntryDataIdentifier); taskList.Add(task); } } await Task.WhenAll(taskList); taskList = new List <Task>(); foreach (var cacheEntry in localFrozenCache.Where(item => item.Status == DataEntryStatus.Deleted)) { var dataEntry = cloudDataEntryFromServer.SingleOrDefault(item => item.Identifier == cacheEntry.Identifier); if (dataEntry != null) { foreach (var dataEntryDataIdentifier in dataEntry.DataIdentifiers) { var task = DeleteFileAsync(cloudAppFolder, dataEntryDataIdentifier); taskList.Add(task); } } } await Task.WhenAll(taskList); await dataService.MakeCacheSynchronized(cloudDataEntryToServer, true, localFrozenCache); localFrozenDataEntries = DataHelper.FromByteArray <AsyncObservableCollection <DataEntry> >(DataHelper.ToByteArray(dataService.DataEntries)); localFrozenCache = DataHelper.FromByteArray <List <DataEntryCache> >(DataHelper.ToByteArray(dataService.Cache)); // Upload the needed data from the server to the local machine Logger.Instance.Information("Uploading the needed data from the server to the local machine."); var dataToUpload = localFrozenDataEntries.Cast <DataEntryBase>().Except(cloudDataEntryFromServer, (item1, item2) => item1.Identifier == item2.Identifier); taskList = new List <Task>(); foreach (var dataEntry in dataToUpload) { var localDataEntry = localFrozenDataEntries.Single(item => item.Identifier == dataEntry.Identifier); if (!localDataEntry.CanSynchronize || localDataEntry.Thumbnail.Type == ThumbnailDataType.Files) { continue; } foreach (var dataEntryDataIdentifier in dataEntry.DataIdentifiers) { var task = UploadDataFileAsync(dataService.ClipboardDataPath, cloudAppFolder, cloudDataPassword, dataEntryDataIdentifier); taskList.Add(task); } } await Task.WhenAll(taskList); // Upload the new data to the server. Logger.Instance.Information("Uploading the data entry file to the server."); using (var memoryStream = new MemoryStream()) using (var aesStream = new AesStream(memoryStream, cloudDataPassword, SecurityHelper.GetSaltKeys(cloudDataPassword).GetBytes(16))) { var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cloudDataEntryToServer)); aesStream.Write(data, 0, data.Length); aesStream.Position = 0; await CurrentCloudStorageProvider.UploadFileAsync(memoryStream, cloudDataEntryFilePath); } await dataService.MakeCacheSynchronized(cloudDataEntryToServer, false, localFrozenCache); } catch (Exception exception) { Logger.Instance.Warning($"Unable to synchronize for the following reason : {exception.Message}. {exception.InnerException?.Message}"); SynchronizationFailed?.Invoke(this, new EventArgs()); } _synchronizeTimer.Stop(); _synchronizeTimer.Start(); IsSynchronizing = false; SynchronizationEnded?.Invoke(this, new EventArgs()); }
private async Task SynchronizeAsync(CancellationToken cancellationToken) { // The semaphore acts like queue. using (await _sempahore.WaitAsync(cancellationToken).ConfigureAwait(false)) { var synchronizationStarted = false; var succeeded = false; var requiresReloadLocalData = false; try { if (!_settingsProvider.GetSetting(SettingsDefinitions.SyncDataWithCloud)) { return; } string targetterProviderName = _settingsProvider.GetSetting(SettingsDefinitions.RemoteStorageProviderName); IRemoteStorageProvider remoteStorageProvider = _remoteStorageProviders.SingleOrDefault(m => string.Equals(m.Metadata.ProviderName, targetterProviderName, StringComparison.Ordinal))?.Value; if (remoteStorageProvider == null) { return; } if (!CoreHelper.IsInternetAccess()) { return; } SynchronizationStarted?.Invoke(this, EventArgs.Empty); synchronizationStarted = true; if (!await remoteStorageProvider.SignInAsync(interactive: false, cancellationToken).ConfigureAwait(false)) { // If fails to authenticate, disables synchronization and sign out. _settingsProvider.SetSetting(SettingsDefinitions.SyncDataWithCloud, false); await remoteStorageProvider.SignOutAsync().ConfigureAwait(false); // TODO: Add a log to notify to let the user know it signed out and he should re-authenticate. // returning here will still trigger the Finally block. return; } // Retrieve the list of online files. IReadOnlyList <RemoteFileInfo> roamingFiles = await remoteStorageProvider.GetFilesAsync(Constants.DataFileCountLimit, cancellationToken).ConfigureAwait(false); RemoteFileInfo roamingUserDataBundleFile = roamingFiles.FirstOrDefault(file => string.Equals(Path.GetFileName(file.FullPath), Constants.UserDataBundleFileName, StringComparison.Ordinal)); IEnumerable <RemoteFileInfo> allOtherRoamingFiles = roamingFiles.Where(file => !string.Equals(Path.GetFileName(file.FullPath), Constants.UserDataBundleFileName, StringComparison.Ordinal)); // Retrieve the list of local files. var localUserDataFolder = await CoreHelper.GetOrCreateUserDataStorageFolderAsync().ConfigureAwait(false); StorageFile localUserDataBundleFile = await localUserDataFolder.TryGetItemAsync(Constants.UserDataBundleFileName) as StorageFile; IEnumerable <StorageFile> allOtherLocalFiles = (await localUserDataFolder.GetFilesAsync()) .Where(file => !string.Equals(file.Name, Constants.UserDataBundleFileName, StringComparison.Ordinal)); if (localUserDataBundleFile == null && roamingUserDataBundleFile == RemoteFileInfo.Empty) { // Nothing locally and remotely? succeeded = true; return; } if (localUserDataBundleFile == null || (roamingUserDataBundleFile != RemoteFileInfo.Empty && roamingUserDataBundleFile.CreatedDateTime.ToUniversalTime() > (await localUserDataBundleFile.GetBasicPropertiesAsync()).DateModified.ToUniversalTime())) { // If there is no local user data file, or that the file on the server is more recent than the local one, // then we want to merge by taking the version from the server. await DownloadRoamingDataFromServerAsync( remoteStorageProvider, allOtherRoamingFiles, allOtherLocalFiles, cancellationToken).ConfigureAwait(false); // The local file changed, since we downloaded the one from the server, so let's indicate // that we want to reload (and merge) the local data. requiresReloadLocalData = true; } else { // Else, then it means the local file is more recent than the one on the server, // or that there is simply no file on the server, // so we want to merge by taking the version from the local file. await UploadLocalDataToServerAsync( remoteStorageProvider, localUserDataBundleFile, allOtherRoamingFiles, allOtherLocalFiles, cancellationToken).ConfigureAwait(false); } succeeded = true; } catch (OperationCanceledException) { _logger.LogEvent(SynchronizeCanceledEvent, "The synchronization with the cloud has been canceled."); } catch (Exception ex) { _logger.LogFault(SynchronizeFaultEvent, "Failed to synchronize the data with the cloud.", ex); } finally { if (synchronizationStarted) { RaiseSynchronizationCompleted(succeeded, requiresReloadLocalData); } } } }