From 818414eb3755d32d5dad0201220b5280fc34c8f5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Apr 2019 12:12:43 -0400 Subject: [PATCH] sync service --- src/Core/Abstractions/IApiService.cs | 2 +- src/Core/Abstractions/ISyncService.cs | 19 ++ src/Core/Enums/NotificationType.cs | 20 ++ .../Models/Response/NotificationResponse.cs | 34 ++++ src/Core/Models/Response/SyncResponse.cs | 2 +- src/Core/Services/ApiService.cs | 2 +- src/Core/Services/SyncService.cs | 190 +++++++++++++++++- 7 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 src/Core/Abstractions/ISyncService.cs create mode 100644 src/Core/Enums/NotificationType.cs create mode 100644 src/Core/Models/Response/NotificationResponse.cs diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 6c36d4a80..7bef3400b 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -22,7 +22,7 @@ namespace Bit.Core.Abstractions Task GetCipherAsync(string id); Task GetFolderAsync(string id); Task GetProfileAsync(); - Task GetSyncAsync(string id); + Task GetSyncAsync(); Task PostAccountKeysAsync(KeysRequest request); Task PostCipherAsync(CipherRequest request); Task PostCipherCreateAsync(CipherCreateRequest request); diff --git a/src/Core/Abstractions/ISyncService.cs b/src/Core/Abstractions/ISyncService.cs new file mode 100644 index 000000000..285d974b4 --- /dev/null +++ b/src/Core/Abstractions/ISyncService.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Response; + +namespace Bit.Core.Abstractions +{ + public interface ISyncService + { + bool SyncInProgress { get; set; } + + Task FullSyncAsync(bool forceSync); + Task GetLastSyncAsync(); + Task SetLastSyncAsync(DateTime date); + Task SyncDeleteCipherAsync(SyncCipherNotification notification); + Task SyncDeleteFolderAsync(SyncFolderNotification notification); + Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit); + Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit); + } +} \ No newline at end of file diff --git a/src/Core/Enums/NotificationType.cs b/src/Core/Enums/NotificationType.cs new file mode 100644 index 000000000..2e35dbf9b --- /dev/null +++ b/src/Core/Enums/NotificationType.cs @@ -0,0 +1,20 @@ +namespace Bit.Core.Enums +{ + public enum NotificationType : byte + { + SyncCipherUpdate = 0, + SyncCipherCreate = 1, + SyncLoginDelete = 2, + SyncFolderDelete = 3, + SyncCiphers = 4, + + SyncVault = 5, + SyncOrgKeys = 6, + SyncFolderCreate = 7, + SyncFolderUpdate = 8, + SyncCipherDelete = 9, + SyncSettings = 10, + + LogOut = 11, + } +} diff --git a/src/Core/Models/Response/NotificationResponse.cs b/src/Core/Models/Response/NotificationResponse.cs new file mode 100644 index 000000000..34ad01ddf --- /dev/null +++ b/src/Core/Models/Response/NotificationResponse.cs @@ -0,0 +1,34 @@ +using Bit.Core.Enums; +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class PushNotificationResponse + { + public NotificationType Type { get; set; } + public string Payload { get; set; } + } + + public class SyncCipherNotification + { + public string Id { get; set; } + public string UserId { get; set; } + public string OrganizationId { get; set; } + public HashSet CollectionIds { get; set; } + public DateTime RevisionDate { get; set; } + } + + public class SyncFolderNotification + { + public string Id { get; set; } + public string UserId { get; set; } + public DateTime RevisionDate { get; set; } + } + + public class UserNotification + { + public string UserId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/Core/Models/Response/SyncResponse.cs b/src/Core/Models/Response/SyncResponse.cs index b2f12f229..76e4fb46c 100644 --- a/src/Core/Models/Response/SyncResponse.cs +++ b/src/Core/Models/Response/SyncResponse.cs @@ -6,7 +6,7 @@ namespace Bit.Core.Models.Response { public ProfileResponse Profile { get; set; } public List Folders { get; set; } = new List(); - public List Collections { get; set; } = new List(); + public List Collections { get; set; } = new List(); public List Ciphers { get; set; } = new List(); public DomainsResponse Domains { get; set; } } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 4b6373644..8d5273397 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -254,7 +254,7 @@ namespace Bit.Core.Services #region Sync APIs - public Task GetSyncAsync(string id) + public Task GetSyncAsync() { return SendAsync(HttpMethod.Get, "/sync", null, true, true); } diff --git a/src/Core/Services/SyncService.cs b/src/Core/Services/SyncService.cs index e581c2d05..764ddbb0f 100644 --- a/src/Core/Services/SyncService.cs +++ b/src/Core/Services/SyncService.cs @@ -1,16 +1,16 @@ using Bit.Core.Abstractions; +using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Response; using Bit.Core.Utilities; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Bit.Core.Services { - public class SyncService + public class SyncService : ISyncService { private const string Keys_LastSyncFormat = "lastSync_{0}"; @@ -58,7 +58,7 @@ namespace Bit.Core.Services return await _storageService.GetAsync(string.Format(Keys_LastSyncFormat, userId)); } - public async Task SetLastSync(DateTime date) + public async Task SetLastSyncAsync(DateTime date) { var userId = await _userService.GetUserIdAsync(); if(userId == null) @@ -68,6 +68,188 @@ namespace Bit.Core.Services await _storageService.SaveAsync(string.Format(Keys_LastSyncFormat, userId), date); } + public async Task FullSyncAsync(bool forceSync) + { + SyncStarted(); + var isAuthenticated = await _userService.IsAuthenticatedAsync(); + if(!isAuthenticated) + { + return SyncCompleted(false); + } + var now = DateTime.UtcNow; + var needsSyncResult = await NeedsSyncingAsync(forceSync); + var needsSync = needsSyncResult.Item1; + var skipped = needsSyncResult.Item2; + if(skipped) + { + return SyncCompleted(false); + } + if(!needsSync) + { + await SetLastSyncAsync(now); + return SyncCompleted(false); + } + var userId = await _userService.GetUserIdAsync(); + try + { + var response = await _apiService.GetSyncAsync(); + await SyncProfileAsync(response.Profile); + await SyncFoldersAsync(userId, response.Folders); + await SyncCollectionsAsync(response.Collections); + await SyncCiphersAsync(userId, response.Ciphers); + await SyncSettingsAsync(userId, response.Domains); + await SetLastSyncAsync(now); + return SyncCompleted(true); + } + catch + { + return SyncCompleted(false); + } + } + + public async Task SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + try + { + var localFolder = await _folderService.GetAsync(notification.Id); + if((!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.RevisionDate < notification.RevisionDate)) + { + var remoteFolder = await _apiService.GetFolderAsync(notification.Id); + if(remoteFolder != null) + { + var userId = await _userService.GetUserIdAsync(); + await _folderService.UpsertAsync(new FolderData(remoteFolder, userId)); + _messagingService.Send("syncedUpsertedFolder", new Dictionary + { + ["folderId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + catch { } + } + return SyncCompleted(false); + } + + public async Task SyncDeleteFolderAsync(SyncFolderNotification notification) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + await _folderService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedFolder", new Dictionary + { + ["folderId"] = notification.Id + }); + return SyncCompleted(true); + } + return SyncCompleted(false); + } + + public async Task SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + try + { + var shouldUpdate = true; + var localCipher = await _cipherService.GetAsync(notification.Id); + if(localCipher != null && localCipher.RevisionDate >= notification.RevisionDate) + { + shouldUpdate = false; + } + + var checkCollections = false; + if(shouldUpdate) + { + if(isEdit) + { + shouldUpdate = localCipher != null; + checkCollections = true; + } + else + { + if(notification.CollectionIds == null || notification.OrganizationId == null) + { + shouldUpdate = localCipher == null; + } + else + { + shouldUpdate = false; + checkCollections = true; + } + } + } + + if(!shouldUpdate && checkCollections && notification.OrganizationId != null && + notification.CollectionIds != null && notification.CollectionIds.Any()) + { + var collections = await _collectionService.GetAllAsync(); + if(collections != null) + { + foreach(var c in collections) + { + if(notification.CollectionIds.Contains(c.Id)) + { + shouldUpdate = true; + break; + } + } + } + } + + if(shouldUpdate) + { + var remoteCipher = await _apiService.GetCipherAsync(notification.Id); + if(remoteCipher != null) + { + var userId = await _userService.GetUserIdAsync(); + await _cipherService.UpsertAsync(new CipherData(remoteCipher, userId)); + _messagingService.Send("syncedUpsertedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + catch(ApiException e) + { + if(e.Error != null && e.Error.StatusCode == System.Net.HttpStatusCode.NotFound && isEdit) + { + await _cipherService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + } + } + return SyncCompleted(false); + } + + public async Task SyncDeleteCipherAsync(SyncCipherNotification notification) + { + SyncStarted(); + if(await _userService.IsAuthenticatedAsync()) + { + await _cipherService.DeleteAsync(notification.Id); + _messagingService.Send("syncedDeletedCipher", new Dictionary + { + ["cipherId"] = notification.Id + }); + return SyncCompleted(true); + } + return SyncCompleted(false); + } + // Helpers private void SyncStarted() @@ -76,7 +258,7 @@ namespace Bit.Core.Services _messagingService.Send("syncStarted"); } - private bool SyncCompelted(bool successfully) + private bool SyncCompleted(bool successfully) { SyncInProgress = false; _messagingService.Send("syncCompleted", new Dictionary { ["successfully"] = successfully });