mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 19:22:45 +00:00
sync: v3 compatible sync
This commit is contained in:
74
Notesnook.API/Controllers/SyncDeviceController.cs
Normal file
74
Notesnook.API/Controllers/SyncDeviceController.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the Affero GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
Affero GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models.Responses;
|
||||
using Notesnook.API.Services;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("devices")]
|
||||
public class SyncDeviceController : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> RegisterDevice([FromQuery] string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
SyncDeviceService.RegisterDevice(userId, deviceId);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<UsersController>.Error(nameof(UnregisterDevice), "Couldn't register device.", ex.ToString());
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
public async Task<IActionResult> UnregisterDevice([FromQuery] string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = this.User.FindFirstValue("sub");
|
||||
SyncDeviceService.UnregisterDevice(userId, deviceId);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<UsersController>.Error(nameof(UnregisterDevice), "Couldn't unregister device.", ex.ToString());
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
Notesnook.API/Hubs/SyncV2Hub.cs
Normal file
284
Notesnook.API/Hubs/SyncV2Hub.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the Affero GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
Affero GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Authorization;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Services;
|
||||
using Streetwriters.Data.Interfaces;
|
||||
|
||||
namespace Notesnook.API.Hubs
|
||||
{
|
||||
public interface ISyncV2HubClient
|
||||
{
|
||||
Task<bool> SendItems(SyncTransferItemV2 transferItem);
|
||||
Task<bool> SendVaultKey(EncryptedData vaultKey);
|
||||
Task PushCompleted();
|
||||
}
|
||||
|
||||
[Authorize("Sync")]
|
||||
public class SyncV2Hub : Hub<ISyncV2HubClient>
|
||||
{
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
private readonly IUnitOfWork unit;
|
||||
private readonly string[] CollectionKeys = [
|
||||
"settingitem",
|
||||
"attachment",
|
||||
"note",
|
||||
"notebook",
|
||||
"content",
|
||||
"shortcut",
|
||||
"reminder",
|
||||
"color",
|
||||
"tag",
|
||||
"vault",
|
||||
"relation", // relations must sync at the end to prevent invalid state
|
||||
];
|
||||
|
||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork)
|
||||
{
|
||||
Repositories = syncItemsRepositoryAccessor;
|
||||
unit = unitOfWork;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync/v2"));
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault();
|
||||
throw new HubException(reason?.Message ?? "Unauthorized");
|
||||
}
|
||||
var id = Context.User.FindFirstValue("sub");
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, id);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
private Action<SyncItem, string, long> MapTypeToUpsertAction(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"settingitem" => Repositories.Settings.Upsert,
|
||||
"attachment" => Repositories.Attachments.Upsert,
|
||||
"note" => Repositories.Notes.Upsert,
|
||||
"notebook" => Repositories.Notebooks.Upsert,
|
||||
"content" => Repositories.Contents.Upsert,
|
||||
"shortcut" => Repositories.Shortcuts.Upsert,
|
||||
"reminder" => Repositories.Reminders.Upsert,
|
||||
"relation" => Repositories.Relations.Upsert,
|
||||
"color" => Repositories.Colors.Upsert,
|
||||
"vault" => Repositories.Vaults.Upsert,
|
||||
"tag" => Repositories.Tags.Upsert,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>> MapTypeToFindItemsAction(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"settingitem" => Repositories.Settings.FindItemsById,
|
||||
"attachment" => Repositories.Attachments.FindItemsById,
|
||||
"note" => Repositories.Notes.FindItemsById,
|
||||
"notebook" => Repositories.Notebooks.FindItemsById,
|
||||
"content" => Repositories.Contents.FindItemsById,
|
||||
"shortcut" => Repositories.Shortcuts.FindItemsById,
|
||||
"reminder" => Repositories.Reminders.FindItemsById,
|
||||
"relation" => Repositories.Relations.FindItemsById,
|
||||
"color" => Repositories.Colors.FindItemsById,
|
||||
"vault" => Repositories.Vaults.FindItemsById,
|
||||
"tag" => Repositories.Tags.FindItemsById,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> PushItems(string deviceId, SyncTransferItemV2 pushItem)
|
||||
{
|
||||
var userId = Context.User.FindFirstValue("sub");
|
||||
if (string.IsNullOrEmpty(userId)) return 0;
|
||||
|
||||
var UpsertItem = MapTypeToUpsertAction(pushItem.Type) ?? throw new Exception($"Invalid item type: {pushItem.Type}.");
|
||||
foreach (var item in pushItem.Items)
|
||||
{
|
||||
UpsertItem(item, userId, 1);
|
||||
}
|
||||
|
||||
if (!await unit.Commit()) return 0;
|
||||
|
||||
await SyncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, pushItem.Items.Select((i) => $"{i.ItemId}:{pushItem.Type}").ToList());
|
||||
return 1;
|
||||
}
|
||||
|
||||
public async Task<bool> PushCompleted()
|
||||
{
|
||||
var userId = Context.User.FindFirstValue("sub");
|
||||
await Clients.OthersInGroup(userId).PushCompleted();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(Func<string, string[], bool, int, Task<IAsyncCursor<SyncItem>>>[] collections, string[] types, string userId, string[] ids, int size, bool resetSync, long maxBytes)
|
||||
{
|
||||
var chunksProcessed = 0;
|
||||
for (int i = 0; i < collections.Length; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
|
||||
var filteredIds = ids.Where((id) => id.EndsWith($":{type}")).Select((id) => id.Split(":")[0]).ToArray();
|
||||
if (!resetSync && filteredIds.Length == 0) continue;
|
||||
|
||||
using var cursor = await collections[i](userId, filteredIds, resetSync, size);
|
||||
|
||||
var chunk = new List<SyncItem>();
|
||||
long totalBytes = 0;
|
||||
long METADATA_BYTES = 5 * 1024;
|
||||
|
||||
while (await cursor.MoveNextAsync())
|
||||
{
|
||||
foreach (var item in cursor.Current)
|
||||
{
|
||||
chunk.Add(item);
|
||||
totalBytes += item.Length + METADATA_BYTES;
|
||||
if (totalBytes >= maxBytes)
|
||||
{
|
||||
yield return new SyncTransferItemV2
|
||||
{
|
||||
Items = chunk,
|
||||
Type = type,
|
||||
Count = chunksProcessed
|
||||
};
|
||||
|
||||
totalBytes = 0;
|
||||
chunk.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chunk.Count > 0)
|
||||
{
|
||||
yield return new SyncTransferItemV2
|
||||
{
|
||||
Items = chunk,
|
||||
Type = type,
|
||||
Count = chunksProcessed
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SyncV2Metadata> RequestFetch(string deviceId)
|
||||
{
|
||||
var userId = Context.User.FindFirstValue("sub");
|
||||
|
||||
if (!SyncDeviceService.IsDeviceRegistered(userId, deviceId))
|
||||
SyncDeviceService.RegisterDevice(userId, deviceId);
|
||||
|
||||
var isResetSync = SyncDeviceService.IsSyncReset(userId, deviceId);
|
||||
if (!SyncDeviceService.IsUnsynced(userId, deviceId) &&
|
||||
!SyncDeviceService.IsSyncPending(userId, deviceId) &&
|
||||
!isResetSync)
|
||||
return new SyncV2Metadata { Synced = true };
|
||||
|
||||
string[] ids = await SyncDeviceService.FetchUnsyncedIdsAsync(userId, deviceId);
|
||||
|
||||
var chunks = PrepareChunks(
|
||||
collections: [
|
||||
Repositories.Settings.FindItemsById,
|
||||
Repositories.Attachments.FindItemsById,
|
||||
Repositories.Notes.FindItemsById,
|
||||
Repositories.Notebooks.FindItemsById,
|
||||
Repositories.Contents.FindItemsById,
|
||||
Repositories.Shortcuts.FindItemsById,
|
||||
Repositories.Reminders.FindItemsById,
|
||||
Repositories.Colors.FindItemsById,
|
||||
Repositories.Tags.FindItemsById,
|
||||
Repositories.Vaults.FindItemsById,
|
||||
Repositories.Relations.FindItemsById,
|
||||
],
|
||||
types: CollectionKeys,
|
||||
userId,
|
||||
ids,
|
||||
size: 1000,
|
||||
resetSync: isResetSync,
|
||||
maxBytes: 7 * 1024 * 1024
|
||||
);
|
||||
|
||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId.Equals(userId));
|
||||
if (userSettings.VaultKey != null)
|
||||
{
|
||||
if (!await Clients.Caller.SendVaultKey(userSettings.VaultKey).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected vault key.");
|
||||
}
|
||||
|
||||
await foreach (var chunk in chunks)
|
||||
{
|
||||
if (!await Clients.Caller.SendItems(chunk).WaitAsync(TimeSpan.FromMinutes(10))) throw new HubException("Client rejected sent items.");
|
||||
|
||||
if (!isResetSync)
|
||||
{
|
||||
var syncedIds = chunk.Items.Select((i) => $"{i.ItemId}:{chunk.Type}").ToHashSet();
|
||||
ids = ids.Where((id) => !syncedIds.Contains(id)).ToArray();
|
||||
await SyncDeviceService.WritePendingIdsAsync(userId, deviceId, ids);
|
||||
}
|
||||
}
|
||||
|
||||
SyncDeviceService.Reset(userId, deviceId);
|
||||
|
||||
return new SyncV2Metadata
|
||||
{
|
||||
Synced = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePack.MessagePackObject]
|
||||
public struct SyncV2Metadata
|
||||
{
|
||||
[MessagePack.Key("synced")]
|
||||
[JsonPropertyName("synced")]
|
||||
public bool Synced { get; set; }
|
||||
}
|
||||
|
||||
[MessagePack.MessagePackObject]
|
||||
public struct SyncV2TransferItem
|
||||
{
|
||||
[MessagePack.Key("items")]
|
||||
[JsonPropertyName("items")]
|
||||
public IEnumerable<SyncItem> Items { get; set; }
|
||||
|
||||
[MessagePack.Key("type")]
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[MessagePack.Key("final")]
|
||||
[JsonPropertyName("final")]
|
||||
public bool Final { get; set; }
|
||||
|
||||
[MessagePack.Key("vaultKey")]
|
||||
[JsonPropertyName("vaultKey")]
|
||||
public EncryptedData VaultKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel;
|
||||
using Microsoft.VisualBasic;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
@@ -68,6 +69,35 @@ namespace Notesnook.API.Repositories
|
||||
Sort = new SortDefinitionBuilder<SyncItem>().Ascending((a) => a.Id)
|
||||
});
|
||||
}
|
||||
|
||||
public Task<IAsyncCursor<SyncItem>> FindItemsById(string userId, IEnumerable<string> ids, bool all, int batchSize)
|
||||
{
|
||||
var filters = new List<FilterDefinition<SyncItem>>(new[] { Builders<SyncItem>.Filter.Eq((i) => i.UserId, userId) });
|
||||
|
||||
if (!all) filters.Add(Builders<SyncItem>.Filter.In((i) => i.ItemId, ids));
|
||||
|
||||
return Collection.FindAsync(Builders<SyncItem>.Filter.And(filters), new FindOptions<SyncItem>
|
||||
{
|
||||
BatchSize = batchSize,
|
||||
AllowDiskUse = true,
|
||||
AllowPartialResults = false,
|
||||
NoCursorTimeout = true
|
||||
});
|
||||
}
|
||||
|
||||
public Task<IAsyncCursor<SyncItem>> GetIdsAsync(string userId, int batchSize)
|
||||
{
|
||||
var filter = Builders<SyncItem>.Filter.Eq((i) => i.UserId, userId);
|
||||
return Collection.FindAsync(filter, new FindOptions<SyncItem>
|
||||
{
|
||||
BatchSize = batchSize,
|
||||
AllowDiskUse = true,
|
||||
AllowPartialResults = false,
|
||||
NoCursorTimeout = true,
|
||||
Sort = new SortDefinitionBuilder<SyncItem>().Ascending((a) => a.Id),
|
||||
Projection = Builders<SyncItem>.Projection.Include((i) => i.ItemId)
|
||||
});
|
||||
}
|
||||
// public async Task DeleteIdsAsync(string[] ids, string userId, CancellationToken token = default(CancellationToken))
|
||||
// {
|
||||
// await Collection.DeleteManyAsync<T>((i) => ids.Contains(i.Id) && i.UserId == userId, token);
|
||||
|
||||
168
Notesnook.API/Services/SyncDeviceService.cs
Normal file
168
Notesnook.API/Services/SyncDeviceService.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the Affero GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
Affero GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the Affero GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Notesnook.API.Services
|
||||
{
|
||||
public class SyncDeviceService
|
||||
{
|
||||
private static string UserSyncDirectoryPath(string userId) => Path.Join("sync", userId);
|
||||
private static string UserDeviceDirectoryPath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserSyncDirectoryPath(userId), deviceId);
|
||||
|
||||
private static string PendingIdsFilePath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserDeviceDirectoryPath(userId, deviceId), "pending");
|
||||
|
||||
private static string UnsyncedIdsFilePath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserDeviceDirectoryPath(userId, deviceId), "unsynced");
|
||||
|
||||
private static string ResetSyncFilePath(string userId, string deviceId) => Path.Join(SyncDeviceService.UserDeviceDirectoryPath(userId, deviceId), "reset-sync");
|
||||
|
||||
public static async Task<string[]> GetUnsyncedIdsAsync(string userId, string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await File.ReadAllLinesAsync(UnsyncedIdsFilePath(userId, deviceId));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string[]> FetchUnsyncedIdsAsync(string userId, string deviceId)
|
||||
{
|
||||
if (IsSyncReset(userId, deviceId)) return Array.Empty<string>();
|
||||
if (UnsyncedIdsFileLocks.TryGetValue(deviceId, out SemaphoreSlim fileLock) && fileLock.CurrentCount == 0)
|
||||
await fileLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var unsyncedIds = await GetUnsyncedIdsAsync(userId, deviceId);
|
||||
if (IsSyncPending(userId, deviceId))
|
||||
{
|
||||
unsyncedIds = unsyncedIds.Union(await File.ReadAllLinesAsync(PendingIdsFilePath(userId, deviceId))).ToArray();
|
||||
}
|
||||
|
||||
if (unsyncedIds.Length == 0) return Array.Empty<string>();
|
||||
|
||||
File.Delete(UnsyncedIdsFilePath(userId, deviceId));
|
||||
await File.WriteAllLinesAsync(PendingIdsFilePath(userId, deviceId), unsyncedIds);
|
||||
|
||||
return unsyncedIds;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fileLock != null && fileLock.CurrentCount == 0) fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task WritePendingIdsAsync(string userId, string deviceId, IEnumerable<string> ids)
|
||||
{
|
||||
await File.WriteAllLinesAsync(PendingIdsFilePath(userId, deviceId), ids);
|
||||
}
|
||||
|
||||
public static bool IsSyncReset(string userId, string deviceId)
|
||||
{
|
||||
return File.Exists(ResetSyncFilePath(userId, deviceId));
|
||||
}
|
||||
|
||||
public static bool IsSyncPending(string userId, string deviceId)
|
||||
{
|
||||
return File.Exists(PendingIdsFilePath(userId, deviceId));
|
||||
}
|
||||
|
||||
public static bool IsUnsynced(string userId, string deviceId)
|
||||
{
|
||||
return File.Exists(UnsyncedIdsFilePath(userId, deviceId));
|
||||
}
|
||||
|
||||
public static void Reset(string userId, string deviceId)
|
||||
{
|
||||
File.Delete(ResetSyncFilePath(userId, deviceId));
|
||||
File.Delete(PendingIdsFilePath(userId, deviceId));
|
||||
}
|
||||
|
||||
public static bool IsDeviceRegistered(string userId, string deviceId)
|
||||
{
|
||||
return Directory.Exists(UserDeviceDirectoryPath(userId, deviceId));
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ListDevices(string userId)
|
||||
{
|
||||
return Directory.EnumerateDirectories(UserSyncDirectoryPath(userId)).Select((path) => Path.GetFileName(path));
|
||||
}
|
||||
|
||||
public static void ResetDevices(string userId)
|
||||
{
|
||||
if (File.Exists(UserSyncDirectoryPath(userId))) File.Delete(UserSyncDirectoryPath(userId));
|
||||
Directory.CreateDirectory(UserSyncDirectoryPath(userId));
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, SemaphoreSlim> UnsyncedIdsFileLocks = new();
|
||||
public static async Task AddIdsToOtherDevicesAsync(string userId, string deviceId, List<string> ids)
|
||||
{
|
||||
foreach (var id in ListDevices(userId))
|
||||
{
|
||||
if (id == deviceId || IsSyncReset(userId, id)) continue;
|
||||
if (!UnsyncedIdsFileLocks.TryGetValue(id, out SemaphoreSlim fileLock))
|
||||
{
|
||||
fileLock = new SemaphoreSlim(1, 1);
|
||||
UnsyncedIdsFileLocks.Add(id, fileLock);
|
||||
}
|
||||
|
||||
await fileLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!IsDeviceRegistered(userId, id)) Directory.CreateDirectory(UserDeviceDirectoryPath(userId, id));
|
||||
|
||||
var oldIds = await GetUnsyncedIdsAsync(userId, id);
|
||||
await File.WriteAllLinesAsync(UnsyncedIdsFilePath(userId, id), ids.Union(oldIds));
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void RegisterDevice(string userId, string deviceId)
|
||||
{
|
||||
Directory.CreateDirectory(UserDeviceDirectoryPath(userId, deviceId));
|
||||
File.Create(ResetSyncFilePath(userId, deviceId)).Close();
|
||||
}
|
||||
|
||||
public static void UnregisterDevice(string userId, string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(UserDeviceDirectoryPath(userId, deviceId), true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,40 +174,68 @@ namespace Notesnook.API.Services
|
||||
|
||||
public async Task<bool> DeleteUserAsync(string userId, string jti)
|
||||
{
|
||||
var cc = new CancellationTokenSource();
|
||||
try
|
||||
{
|
||||
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "Deleting user account", userId);
|
||||
|
||||
SyncDeviceService.ResetDevices(userId);
|
||||
|
||||
var cc = new CancellationTokenSource();
|
||||
|
||||
Repositories.Notes.DeleteByUserId(userId);
|
||||
Repositories.Notebooks.DeleteByUserId(userId);
|
||||
Repositories.Shortcuts.DeleteByUserId(userId);
|
||||
Repositories.Contents.DeleteByUserId(userId);
|
||||
Repositories.Settings.DeleteByUserId(userId);
|
||||
Repositories.LegacySettings.DeleteByUserId(userId);
|
||||
Repositories.Attachments.DeleteByUserId(userId);
|
||||
Repositories.Reminders.DeleteByUserId(userId);
|
||||
Repositories.Relations.DeleteByUserId(userId);
|
||||
Repositories.Colors.DeleteByUserId(userId);
|
||||
Repositories.Tags.DeleteByUserId(userId);
|
||||
Repositories.Vaults.DeleteByUserId(userId);
|
||||
Repositories.UsersSettings.Delete((u) => u.UserId == userId);
|
||||
Repositories.Monographs.DeleteMany((m) => m.UserId == userId);
|
||||
|
||||
if (!Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
|
||||
{
|
||||
AppId = ApplicationType.NOTESNOOK,
|
||||
UserId = userId
|
||||
});
|
||||
}
|
||||
var result = await unit.Commit();
|
||||
await Slogger<UserService>.Info(nameof(DeleteUserAsync), "User account deleted", userId, result.ToString());
|
||||
if (!result) return false;
|
||||
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
SendToAll = false,
|
||||
OriginTokenId = jti,
|
||||
UserId = userId,
|
||||
Message = new Message
|
||||
if (!Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
Type = "logout",
|
||||
Data = JsonSerializer.Serialize(new { reason = "Account deleted." })
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
|
||||
{
|
||||
AppId = ApplicationType.NOTESNOOK,
|
||||
UserId = userId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await S3Service.DeleteDirectoryAsync(userId);
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
SendToAll = false,
|
||||
OriginTokenId = jti,
|
||||
UserId = userId,
|
||||
Message = new Message
|
||||
{
|
||||
Type = "logout",
|
||||
Data = JsonSerializer.Serialize(new { reason = "Account deleted." })
|
||||
}
|
||||
});
|
||||
|
||||
return await unit.Commit();
|
||||
await S3Service.DeleteDirectoryAsync(userId);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Slogger<UserService>.Error(nameof(DeleteUserAsync), "User account not deleted", userId, ex.ToString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)
|
||||
{
|
||||
SyncDeviceService.ResetDevices(userId);
|
||||
|
||||
var cc = new CancellationTokenSource();
|
||||
|
||||
Repositories.Notes.DeleteByUserId(userId);
|
||||
@@ -229,7 +257,6 @@ namespace Notesnook.API.Services
|
||||
|
||||
userSettings.AttachmentsKey = null;
|
||||
userSettings.VaultKey = null;
|
||||
userSettings.Profile = null;
|
||||
userSettings.LastSynced = 0;
|
||||
|
||||
await Repositories.UsersSettings.UpsertAsync(userSettings, (s) => s.UserId == userId);
|
||||
|
||||
@@ -160,41 +160,69 @@ namespace Notesnook.API
|
||||
});
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(UserSettings)))
|
||||
{
|
||||
BsonClassMap.RegisterClassMap<UserSettings>();
|
||||
}
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(EncryptedData)))
|
||||
{
|
||||
BsonClassMap.RegisterClassMap<EncryptedData>();
|
||||
}
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction)))
|
||||
{
|
||||
BsonClassMap.RegisterClassMap<CallToAction>();
|
||||
}
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Announcement)))
|
||||
{
|
||||
BsonClassMap.RegisterClassMap<Announcement>();
|
||||
}
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Attachment)))
|
||||
BsonClassMap.RegisterClassMap<Attachment>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Content)))
|
||||
BsonClassMap.RegisterClassMap<Content>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Note)))
|
||||
BsonClassMap.RegisterClassMap<Note>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Notebook)))
|
||||
BsonClassMap.RegisterClassMap<Notebook>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Relation)))
|
||||
BsonClassMap.RegisterClassMap<Relation>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Reminder)))
|
||||
BsonClassMap.RegisterClassMap<Reminder>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Setting)))
|
||||
BsonClassMap.RegisterClassMap<Setting>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(SettingItem)))
|
||||
BsonClassMap.RegisterClassMap<SettingItem>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Shortcut)))
|
||||
BsonClassMap.RegisterClassMap<Shortcut>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Tag)))
|
||||
BsonClassMap.RegisterClassMap<Tag>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Color)))
|
||||
BsonClassMap.RegisterClassMap<Color>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(Vault)))
|
||||
BsonClassMap.RegisterClassMap<Vault>();
|
||||
|
||||
services.AddScoped<IDbContext, MongoDbContext>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
services.AddScoped((provider) => new Repository<UserSettings>(provider.GetRequiredService<IDbContext>(), "notesnook", "user_settings"));
|
||||
services.AddScoped((provider) => new Repository<Monograph>(provider.GetRequiredService<IDbContext>(), "notesnook", "monographs"));
|
||||
services.AddScoped((provider) => new Repository<Announcement>(provider.GetRequiredService<IDbContext>(), "notesnook", "announcements"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Attachment>(provider.GetRequiredService<IDbContext>(), "notesnook", "attachments"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Content>(provider.GetRequiredService<IDbContext>(), "notesnook", "content"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Note>(provider.GetRequiredService<IDbContext>(), "notesnook", "notes"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Notebook>(provider.GetRequiredService<IDbContext>(), "notesnook", "notebooks"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Relation>(provider.GetRequiredService<IDbContext>(), "notesnook", "relations"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Reminder>(provider.GetRequiredService<IDbContext>(), "notesnook", "reminders"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Setting>(provider.GetRequiredService<IDbContext>(), "notesnook", "settings"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Shortcut>(provider.GetRequiredService<IDbContext>(), "notesnook", "shortcuts"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Tag>(provider.GetRequiredService<IDbContext>(), "notesnook", "tags"));
|
||||
services.AddScoped((provider) => new SyncItemsRepository<Color>(provider.GetRequiredService<IDbContext>(), "notesnook", "colors"));
|
||||
services.AddRepository<UserSettings>("user_settings")
|
||||
.AddRepository<Monograph>("monographs")
|
||||
.AddRepository<Announcement>("announcements");
|
||||
|
||||
services.AddSyncRepository<SettingItem>("settingsv2")
|
||||
.AddSyncRepository<Attachment>("attachments")
|
||||
.AddSyncRepository<Content>("content")
|
||||
.AddSyncRepository<Note>("notes")
|
||||
.AddSyncRepository<Notebook>("notebooks")
|
||||
.AddSyncRepository<Relation>("relations")
|
||||
.AddSyncRepository<Reminder>("reminders")
|
||||
.AddSyncRepository<Setting>("settings")
|
||||
.AddSyncRepository<Shortcut>("shortcuts")
|
||||
.AddSyncRepository<Tag>("tags")
|
||||
.AddSyncRepository<Color>("colors")
|
||||
.AddSyncRepository<Vault>("vaults");
|
||||
|
||||
services.TryAddTransient<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||
services.TryAddTransient<IUserService, UserService>();
|
||||
@@ -202,7 +230,7 @@ namespace Notesnook.API
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
services.AddHealthChecks().AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
|
||||
services.AddHealthChecks(); // .AddMongoDb(dbSettings.ConnectionString, dbSettings.DatabaseName, "database-check");
|
||||
services.AddSignalR((hub) =>
|
||||
{
|
||||
hub.MaximumReceiveMessageSize = 100 * 1024 * 1024;
|
||||
@@ -245,15 +273,17 @@ namespace Notesnook.API
|
||||
|
||||
app.UseWamp(WampServers.NotesnookServer, (realm, server) =>
|
||||
{
|
||||
IUserService service = app.GetScopedService<IUserService>();
|
||||
|
||||
realm.Subscribe<DeleteUserMessage>(IdentityServerTopics.DeleteUserTopic, async (ev) =>
|
||||
{
|
||||
IUserService service = app.GetScopedService<IUserService>();
|
||||
await service.DeleteUserAsync(ev.UserId, null);
|
||||
});
|
||||
|
||||
IDistributedCache cache = app.GetScopedService<IDistributedCache>();
|
||||
realm.Subscribe<ClearCacheMessage>(IdentityServerTopics.ClearCacheTopic, (ev) => ev.Keys.ForEach((key) => cache.Remove(key)));
|
||||
realm.Subscribe<ClearCacheMessage>(IdentityServerTopics.ClearCacheTopic, (ev) =>
|
||||
{
|
||||
IDistributedCache cache = app.GetScopedService<IDistributedCache>();
|
||||
ev.Keys.ForEach((key) => cache.Remove(key));
|
||||
});
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
@@ -270,7 +300,27 @@ namespace Notesnook.API
|
||||
options.CloseOnAuthenticationExpiration = false;
|
||||
options.Transports = HttpTransportType.WebSockets;
|
||||
});
|
||||
endpoints.MapHub<SyncV2Hub>("/hubs/sync/v2", options =>
|
||||
{
|
||||
options.CloseOnAuthenticationExpiration = false;
|
||||
options.Transports = HttpTransportType.WebSockets;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class ServiceCollectionRepositoryExtensions
|
||||
{
|
||||
public static IServiceCollection AddRepository<T>(this IServiceCollection services, string collectionName, string database = "notesnook") where T : class
|
||||
{
|
||||
services.AddScoped((provider) => new Repository<T>(provider.GetRequiredService<IDbContext>(), database, collectionName));
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddSyncRepository<T>(this IServiceCollection services, string collectionName, string database = "notesnook") where T : SyncItem
|
||||
{
|
||||
services.AddScoped((provider) => new SyncItemsRepository<T>(provider.GetRequiredService<IDbContext>(), database, collectionName));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user