diff --git a/Notesnook.API/Hubs/SyncHub.cs b/Notesnook.API/Hubs/SyncHub.cs index 1ae9c29..f2c7854 100644 --- a/Notesnook.API/Hubs/SyncHub.cs +++ b/Notesnook.API/Hubs/SyncHub.cs @@ -87,10 +87,11 @@ namespace Notesnook.API.Hubs long dateSynced = transferItem.LastSynced > userSettings.LastSynced ? transferItem.LastSynced : userSettings.LastSynced; - for (int i = 0; i < transferItem.Items.Length; ++i) + Parallel.For(0, transferItem.Items.Length, async (i) => { var data = transferItem.Items[i]; var type = transferItem.Types[i]; + var id = transferItem.Ids[i]; // We intentionally don't await here to speed up the sync. Fire and forget // suits here because we don't really care if the item reaches the other @@ -108,44 +109,39 @@ namespace Notesnook.API.Hubs switch (type) { case "content": - Repositories.Contents.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Contents.UpsertAsync(id, data, userId, dateSynced); break; case "attachment": - Repositories.Attachments.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Attachments.UpsertAsync(id, data, userId, dateSynced); break; case "note": - Repositories.Notes.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Notes.UpsertAsync(id, data, userId, dateSynced); break; case "notebook": - Repositories.Notebooks.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Notebooks.UpsertAsync(id, data, userId, dateSynced); break; case "shortcut": - Repositories.Shortcuts.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Shortcuts.UpsertAsync(id, data, userId, dateSynced); break; case "reminder": - Repositories.Reminders.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Reminders.UpsertAsync(id, data, userId, dateSynced); break; case "relation": - Repositories.Relations.Upsert(JsonSerializer.Deserialize(data), userId, dateSynced); + await Repositories.Relations.UpsertAsync(id, data, userId, dateSynced); break; case "settings": - var settings = JsonSerializer.Deserialize(data); - settings.Id = MongoDB.Bson.ObjectId.Parse(userId); - settings.ItemId = userId; - Repositories.Settings.Upsert(settings, userId, dateSynced); + await Repositories.Settings.UpsertAsync(userId, data, userId, dateSynced); break; case "vaultKey": userSettings.VaultKey = JsonSerializer.Deserialize(data); - Repositories.UsersSettings.Upsert(userSettings, (u) => u.UserId == userId); + await Repositories.UsersSettings.UpsertAsync(userSettings, (u) => u.UserId == userId); break; default: throw new HubException("Invalid item type."); } + }); - } - - return await unit.Commit() ? 1 : 0; - + return 1; } public async Task SyncCompleted(long dateSynced) @@ -262,6 +258,8 @@ namespace Notesnook.API.Hubs [MessagePack.Key("types")] public string[] Types { get; set; } + [MessagePack.Key("ids")] + public string[] Ids { get; set; } [MessagePack.Key("total")] public int Total { get; set; } diff --git a/Notesnook.API/Models/Algorithms.cs b/Notesnook.API/Models/Algorithms.cs index 910ae3c..4baa10f 100644 --- a/Notesnook.API/Models/Algorithms.cs +++ b/Notesnook.API/Models/Algorithms.cs @@ -17,10 +17,17 @@ You should have received a copy of the Affero GNU General Public License along with this program. If not, see . */ +using System.Collections.Generic; + namespace Notesnook.API.Models { public class Algorithms { - public static string Default => "xcha-argon2i13-7"; + public const string Default = "xcha-argon2i13-7"; + static readonly List ALGORITHMS = new List { Algorithms.Default }; + public static bool IsValidAlgorithm(string algorithm) + { + return ALGORITHMS.Contains(algorithm); + } } } \ No newline at end of file diff --git a/Notesnook.API/Repositories/SyncItemsRepository.cs b/Notesnook.API/Repositories/SyncItemsRepository.cs index 1663fdc..4309658 100644 --- a/Notesnook.API/Repositories/SyncItemsRepository.cs +++ b/Notesnook.API/Repositories/SyncItemsRepository.cs @@ -19,90 +19,82 @@ along with this program. If not, see . using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualBasic; -using MongoDB.Bson; using MongoDB.Driver; -using Notesnook.API.Interfaces; using Notesnook.API.Models; -using Streetwriters.Common; -using Streetwriters.Data.Interfaces; -using Streetwriters.Data.Repositories; +using Streetwriters.Data.Attributes; namespace Notesnook.API.Repositories { - public class SyncItemsRepository : Repository where T : SyncItem + public class SyncItemsRepository where T : SyncItem { - public SyncItemsRepository(IDbContext dbContext) : base(dbContext) + const string BASE_DATA_DIR = "data"; + private string GetCollectionName() { - Collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(i => i.UserId).Descending(i => i.DateSynced))); - Collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(i => i.UserId).Ascending((i) => i.ItemId))); - Collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(i => i.UserId))); + var attribute = (BsonCollectionAttribute)typeof(T).GetCustomAttributes( + typeof(BsonCollectionAttribute), + true).FirstOrDefault(); + if (string.IsNullOrEmpty(attribute.CollectionName) || string.IsNullOrEmpty(attribute.DatabaseName)) throw new Exception("Could not get a valid collection or database name."); + return attribute.CollectionName; } - private readonly List ALGORITHMS = new List { Algorithms.Default }; - private bool IsValidAlgorithm(string algorithm) + private string GetUserDirectoryPath(string userId) { - return ALGORITHMS.Contains(algorithm); + return System.IO.Path.Join(BASE_DATA_DIR, userId, GetCollectionName()); } - public async Task> GetItemsSyncedAfterAsync(string userId, long timestamp) + private IEnumerable EnumerateItems(string userId, string searchPattern = "*") { - var cursor = await Collection.FindAsync(n => (n.DateSynced > timestamp) && n.UserId.Equals(userId)); - return cursor.ToList(); + try + { + return System.IO.Directory.EnumerateFiles(GetUserDirectoryPath(userId), searchPattern, System.IO.SearchOption.TopDirectoryOnly); + } + catch + { + return new string[] { }; + } } - // public async Task DeleteIdsAsync(string[] ids, string userId, CancellationToken token = default(CancellationToken)) - // { - // await Collection.DeleteManyAsync((i) => ids.Contains(i.Id) && i.UserId == userId, token); - // } + private string FindItemById(string userId, string id) + { + try + { + var files = Directory.GetFiles(GetUserDirectoryPath(userId), $"{id}-*", System.IO.SearchOption.TopDirectoryOnly); + return files.Length > 0 ? files[0] : null; + } + catch + { + return null; + } + } + + public async Task> GetItemsSyncedAfterAsync(string userId, long timestamp) + { + var items = new List(); + await Parallel.ForEachAsync(EnumerateItems(userId), async (file, ct) => + { + var parts = file.Split("-"); + var id = parts[0]; + var dateSynced = long.Parse(parts[1]); + if (dateSynced > timestamp) items.Add(await File.ReadAllTextAsync(file)); + }); + return items; + } public void DeleteByUserId(string userId) { - dbContext.AddCommand((handle, ct) => Collection.DeleteManyAsync(handle, (i) => i.UserId == userId, cancellationToken: ct)); + Directory.Delete(GetUserDirectoryPath(userId), true); } - public async Task UpsertAsync(T item, string userId, long dateSynced) + public async Task UpsertAsync(string id, string item, string userId, long dateSynced) { - - if (item.Length > 15 * 1024 * 1024) - { - throw new Exception($"Size of item \"{item.ItemId}\" is too large. Maximum allowed size is 15 MB."); - } - - if (!IsValidAlgorithm(item.Algorithm)) - { - throw new Exception($"Invalid alg identifier {item.Algorithm}"); - } - - item.DateSynced = dateSynced; - item.UserId = userId; - - await base.UpsertAsync(item, (x) => (x.ItemId == item.ItemId) && x.UserId == userId); - } - - public void Upsert(T item, string userId, long dateSynced) - { - - if (item.Length > 15 * 1024 * 1024) - { - throw new Exception($"Size of item \"{item.ItemId}\" is too large. Maximum allowed size is 15 MB."); - } - - if (!IsValidAlgorithm(item.Algorithm)) - { - throw new Exception($"Invalid alg identifier {item.Algorithm}"); - } - - item.DateSynced = dateSynced; - item.UserId = userId; - - // await base.UpsertAsync(item, (x) => (x.ItemId == item.ItemId) && x.UserId == userId); - base.Upsert(item, (x) => (x.ItemId == item.ItemId) && x.UserId == userId); + Directory.CreateDirectory(GetUserDirectoryPath(userId)); + var oldPath = FindItemById(userId, id); + var newPath = Path.Join(GetUserDirectoryPath(userId), $"{id}-{dateSynced}"); + await File.WriteAllTextAsync(newPath, item); + if (oldPath != null) File.Delete(oldPath); } } } \ No newline at end of file