mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 19:22:45 +00:00
sync: migrate sync devices from fs to mongodb
This commit is contained in:
@@ -46,6 +46,8 @@ namespace Notesnook.API.Accessors
|
||||
public Repository<Monograph> Monographs { get; }
|
||||
public Repository<InboxApiKey> InboxApiKey { get; }
|
||||
public Repository<InboxSyncItem> InboxItems { get; }
|
||||
public Repository<SyncDevice> SyncDevices { get; }
|
||||
public Repository<DeviceIdsChunk> DeviceIdsChunks { get; }
|
||||
|
||||
public SyncItemsRepositoryAccessor(IDbContext dbContext,
|
||||
|
||||
@@ -74,13 +76,20 @@ namespace Notesnook.API.Accessors
|
||||
[FromKeyedServices(Collections.TagsKey)]
|
||||
IMongoCollection<SyncItem> tags,
|
||||
|
||||
Repository<UserSettings> usersSettings, Repository<Monograph> monographs,
|
||||
Repository<InboxApiKey> inboxApiKey, Repository<InboxSyncItem> inboxItems, ILogger<SyncItemsRepository> logger)
|
||||
Repository<UserSettings> usersSettings,
|
||||
Repository<Monograph> monographs,
|
||||
Repository<InboxApiKey> inboxApiKey,
|
||||
Repository<InboxSyncItem> inboxItems,
|
||||
Repository<SyncDevice> syncDevices,
|
||||
Repository<DeviceIdsChunk> deviceIdsChunks,
|
||||
ILogger<SyncItemsRepository> logger)
|
||||
{
|
||||
UsersSettings = usersSettings;
|
||||
Monographs = monographs;
|
||||
InboxApiKey = inboxApiKey;
|
||||
InboxItems = inboxItems;
|
||||
SyncDevices = syncDevices;
|
||||
DeviceIdsChunks = deviceIdsChunks;
|
||||
Notebooks = new SyncItemsRepository(dbContext, notebooks, logger);
|
||||
Notes = new SyncItemsRepository(dbContext, notes, logger);
|
||||
Contents = new SyncItemsRepository(dbContext, content, logger);
|
||||
|
||||
@@ -14,7 +14,9 @@ namespace Notesnook.API
|
||||
public const string TagsKey = "tags";
|
||||
public const string ColorsKey = "colors";
|
||||
public const string VaultsKey = "vaults";
|
||||
public const string InboxItems = "inbox_items";
|
||||
public const string InboxItemsKey = "inbox_items";
|
||||
public const string InboxApiKeysKey = "inbox_api_keys";
|
||||
public const string SyncDevicesKey = "sync_devices";
|
||||
public const string DeviceIdsChunksKey = "device_ids_chunks";
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Accessors;
|
||||
using Notesnook.API.Models;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Accessors;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Data.Repositories;
|
||||
@@ -35,25 +38,26 @@ namespace Notesnook.API.Controllers
|
||||
// TODO: this should be moved out into its own microservice
|
||||
[ApiController]
|
||||
[Route("announcements")]
|
||||
public class AnnouncementController : ControllerBase
|
||||
public class AnnouncementController(Repository<Announcement> announcements, WampServiceAccessor serviceAccessor) : ControllerBase
|
||||
{
|
||||
private Repository<Announcement> Announcements { get; set; }
|
||||
public AnnouncementController(Repository<Announcement> announcements)
|
||||
{
|
||||
Announcements = announcements;
|
||||
}
|
||||
|
||||
[HttpGet("active")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> GetActiveAnnouncements([FromQuery] string? userId)
|
||||
{
|
||||
var totalActive = await Announcements.Collection.CountDocumentsAsync(Builders<Announcement>.Filter.Eq("IsActive", true));
|
||||
if (totalActive <= 0) return Ok(Array.Empty<Announcement>());
|
||||
|
||||
var announcements = (await Announcements.FindAsync((a) => a.IsActive)).Where((a) => a.UserIds == null || a.UserIds.Length == 0 || a.UserIds.Contains(userId));
|
||||
foreach (var announcement in announcements)
|
||||
var filter = Builders<Announcement>.Filter.Eq(x => x.IsActive, true);
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
if (announcement.UserIds != null && !announcement.UserIds.Contains(userId)) continue;
|
||||
var userFilter = Builders<Announcement>.Filter.Or(
|
||||
Builders<Announcement>.Filter.Eq(x => x.UserIds, null),
|
||||
Builders<Announcement>.Filter.Size(x => x.UserIds, 0),
|
||||
Builders<Announcement>.Filter.AnyEq(x => x.UserIds, userId)
|
||||
);
|
||||
filter = Builders<Announcement>.Filter.And(filter, userFilter);
|
||||
}
|
||||
var userAnnouncements = await announcements.Collection.Find(filter).ToListAsync();
|
||||
foreach (var announcement in userAnnouncements)
|
||||
{
|
||||
if (userId != null && announcement.UserIds != null && !announcement.UserIds.Contains(userId)) continue;
|
||||
|
||||
foreach (var item in announcement.Body)
|
||||
{
|
||||
@@ -66,13 +70,13 @@ namespace Notesnook.API.Controllers
|
||||
|
||||
if (action.Data.Contains("{{Email}}"))
|
||||
{
|
||||
var user = string.IsNullOrEmpty(userId) ? null : await (await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic)).GetUserAsync(Clients.Notesnook.Id, userId);
|
||||
var user = string.IsNullOrEmpty(userId) ? null : await serviceAccessor.UserAccountService.GetUserAsync(Clients.Notesnook.Id, userId);
|
||||
action.Data = action.Data.Replace("{{Email}}", user?.Email ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(announcements);
|
||||
return Ok(userAnnouncements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace Notesnook.API.Controllers
|
||||
Repository<InboxApiKey> inboxApiKeysRepository,
|
||||
Repository<UserSettings> userSettingsRepository,
|
||||
Repository<InboxSyncItem> inboxItemsRepository,
|
||||
SyncDeviceService syncDeviceService,
|
||||
ILogger<InboxController> logger) : ControllerBase
|
||||
{
|
||||
|
||||
@@ -182,8 +183,7 @@ namespace Notesnook.API.Controllers
|
||||
request.UserId = userId;
|
||||
request.ItemId = ObjectId.GenerateNewId().ToString();
|
||||
await inboxItemsRepository.InsertAsync(request);
|
||||
new SyncDeviceService(new SyncDevice(userId, string.Empty))
|
||||
.AddIdsToAllDevices([$"{request.ItemId}:inboxItems"]);
|
||||
await syncDeviceService.AddIdsToAllDevicesAsync(userId, [new(request.ItemId, "inbox_item")]);
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
OriginTokenId = null,
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Notesnook.API.Controllers
|
||||
[ApiController]
|
||||
[Route("monographs")]
|
||||
[Authorize("Sync")]
|
||||
public class MonographsController(Repository<Monograph> monographs, IURLAnalyzer analyzer, ILogger<MonographsController> logger) : ControllerBase
|
||||
public class MonographsController(Repository<Monograph> monographs, IURLAnalyzer analyzer, SyncDeviceService syncDeviceService, ILogger<MonographsController> logger) : ControllerBase
|
||||
{
|
||||
const string SVG_PIXEL = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><circle r='9'/></svg>";
|
||||
private const int MAX_DOC_SIZE = 15 * 1024 * 1024;
|
||||
@@ -317,32 +317,16 @@ namespace Notesnook.API.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private static async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti)
|
||||
private async Task MarkMonographForSyncAsync(string userId, string monographId, string? deviceId, string? jti)
|
||||
{
|
||||
if (deviceId == null) return;
|
||||
|
||||
new SyncDeviceService(new SyncDevice(userId, deviceId)).AddIdsToOtherDevices([$"{monographId}:monograph"]);
|
||||
await SendTriggerSyncEventAsync(userId, jti);
|
||||
await syncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, [new(monographId, "monograph")]);
|
||||
}
|
||||
|
||||
private static async Task MarkMonographForSyncAsync(string userId, string monographId)
|
||||
private async Task MarkMonographForSyncAsync(string userId, string monographId)
|
||||
{
|
||||
new SyncDeviceService(new SyncDevice(userId, string.Empty)).AddIdsToAllDevices([$"{monographId}:monograph"]);
|
||||
await SendTriggerSyncEventAsync(userId, sendToAllDevices: true);
|
||||
}
|
||||
|
||||
private static async Task SendTriggerSyncEventAsync(string userId, string? jti = null, bool sendToAllDevices = false)
|
||||
{
|
||||
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
|
||||
{
|
||||
OriginTokenId = sendToAllDevices ? null : jti,
|
||||
UserId = userId,
|
||||
Message = new Message
|
||||
{
|
||||
Type = "triggerSync",
|
||||
Data = JsonSerializer.Serialize(new { reason = "Monographs updated." })
|
||||
}
|
||||
});
|
||||
await syncDeviceService.AddIdsToAllDevicesAsync(userId, [new(monographId, "monograph")]);
|
||||
}
|
||||
|
||||
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string? content)
|
||||
|
||||
@@ -17,23 +17,25 @@ 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 Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Amazon.S3.Model;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Claims;
|
||||
using Notesnook.API.Interfaces;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Models;
|
||||
using Notesnook.API.Helpers;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3.Model;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Accessors;
|
||||
using Notesnook.API.Helpers;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Accessors;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Models;
|
||||
|
||||
namespace Notesnook.API.Controllers
|
||||
{
|
||||
@@ -41,7 +43,7 @@ namespace Notesnook.API.Controllers
|
||||
[Route("s3")]
|
||||
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
|
||||
[Authorize("Sync")]
|
||||
public class S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor repositories, ILogger<S3Controller> logger) : ControllerBase
|
||||
public class S3Controller(IS3Service s3Service, ISyncItemsRepositoryAccessor repositories, WampServiceAccessor serviceAccessor, ILogger<S3Controller> logger) : ControllerBase
|
||||
{
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> Upload([FromQuery] string name)
|
||||
@@ -74,8 +76,7 @@ namespace Notesnook.API.Controllers
|
||||
{
|
||||
var userSettings = await repositories.UsersSettings.FindOneAsync((u) => u.UserId == userId);
|
||||
|
||||
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
var subscription = await serviceAccessor.UserSubscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
|
||||
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
|
||||
throw new Exception("Max file size exceeded.");
|
||||
|
||||
@@ -37,15 +37,15 @@ namespace Notesnook.API.Controllers
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("devices")]
|
||||
public class SyncDeviceController(ILogger<SyncDeviceController> logger) : ControllerBase
|
||||
public class SyncDeviceController(SyncDeviceService syncDeviceService, ILogger<SyncDeviceController> logger) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public IActionResult RegisterDevice([FromQuery] string deviceId)
|
||||
public async Task<IActionResult> RegisterDevice([FromQuery] string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = this.User.GetUserId();
|
||||
new SyncDeviceService(new SyncDevice(userId, deviceId)).RegisterDevice();
|
||||
await syncDeviceService.RegisterDeviceAsync(userId, deviceId);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -57,12 +57,12 @@ namespace Notesnook.API.Controllers
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
public IActionResult UnregisterDevice([FromQuery] string deviceId)
|
||||
public async Task<IActionResult> UnregisterDevice([FromQuery] string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = this.User.GetUserId();
|
||||
new SyncDeviceService(new SyncDevice(userId, deviceId)).UnregisterDevice();
|
||||
await syncDeviceService.UnregisterDeviceAsync(userId, deviceId);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -46,12 +46,14 @@ namespace Notesnook.API.Hubs
|
||||
Task<bool> SendMonographs(IEnumerable<MonographMetadata> monographs);
|
||||
Task<bool> SendInboxItems(IEnumerable<InboxSyncItem> inboxItems);
|
||||
Task PushCompleted();
|
||||
Task PushCompletedV2(string deviceId);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
public class SyncV2Hub : Hub<ISyncV2HubClient>
|
||||
{
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
private SyncDeviceService SyncDeviceService { get; }
|
||||
private readonly IUnitOfWork unit;
|
||||
private static readonly string[] CollectionKeys = [
|
||||
"settingitem",
|
||||
@@ -67,14 +69,15 @@ namespace Notesnook.API.Hubs
|
||||
"relation", // relations must sync at the end to prevent invalid state
|
||||
];
|
||||
private readonly FrozenDictionary<string, Action<IEnumerable<SyncItem>, string, long>> UpsertActionsMap;
|
||||
private readonly Func<string, string[], bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
|
||||
private readonly Func<string, IEnumerable<string>, bool, int, Task<IAsyncCursor<SyncItem>>>[] Collections;
|
||||
ILogger<SyncV2Hub> Logger { get; }
|
||||
|
||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, ILogger<SyncV2Hub> logger)
|
||||
public SyncV2Hub(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, IUnitOfWork unitOfWork, SyncDeviceService syncDeviceService, ILogger<SyncV2Hub> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
Repositories = syncItemsRepositoryAccessor;
|
||||
unit = unitOfWork;
|
||||
SyncDeviceService = syncDeviceService;
|
||||
|
||||
Collections = [
|
||||
Repositories.Settings.FindItemsById,
|
||||
@@ -133,7 +136,7 @@ namespace Notesnook.API.Hubs
|
||||
|
||||
if (!await unit.Commit()) return 0;
|
||||
|
||||
new SyncDeviceService(new SyncDevice(userId, deviceId)).AddIdsToOtherDevices(pushItem.Items.Select((i) => $"{i.ItemId}:{pushItem.Type}").ToList());
|
||||
await SyncDeviceService.AddIdsToOtherDevicesAsync(userId, deviceId, pushItem.Items.Select((i) => new ItemKey(i.ItemId, pushItem.Type)));
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
@@ -149,14 +152,22 @@ namespace Notesnook.API.Hubs
|
||||
return true;
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, string[] ids, int size, bool resetSync, long maxBytes)
|
||||
public async Task<bool> PushCompletedV2(string deviceId)
|
||||
{
|
||||
var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found.");
|
||||
await Clients.OthersInGroup(userId).PushCompleted();
|
||||
await Clients.OthersInGroup(userId).PushCompletedV2(deviceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SyncTransferItemV2> PrepareChunks(string userId, HashSet<ItemKey> ids, int size, bool resetSync, long maxBytes)
|
||||
{
|
||||
var itemsProcessed = 0;
|
||||
for (int i = 0; i < Collections.Length; i++)
|
||||
{
|
||||
var type = CollectionKeys[i];
|
||||
|
||||
var filteredIds = ids.Where((id) => id.EndsWith($":{type}")).Select((id) => id.Split(":")[0]).ToArray();
|
||||
var filteredIds = ids.Where((id) => id.Type == type).Select((id) => id.ItemId).ToArray();
|
||||
if (!resetSync && filteredIds.Length == 0) continue;
|
||||
|
||||
using var cursor = await Collections[i](userId, filteredIds, resetSync, size);
|
||||
@@ -220,61 +231,47 @@ namespace Notesnook.API.Hubs
|
||||
|
||||
SyncEventCounterSource.Log.FetchV2();
|
||||
|
||||
var device = new SyncDevice(userId, deviceId);
|
||||
var deviceService = new SyncDeviceService(device);
|
||||
if (!deviceService.IsDeviceRegistered()) deviceService.RegisterDevice();
|
||||
|
||||
device.LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
var isResetSync = deviceService.IsSyncReset();
|
||||
if (!deviceService.IsUnsynced() &&
|
||||
!deviceService.IsSyncPending() &&
|
||||
!isResetSync)
|
||||
return new SyncV2Metadata { Synced = true };
|
||||
var device = await SyncDeviceService.GetDeviceAsync(userId, deviceId);
|
||||
if (device == null)
|
||||
device = await SyncDeviceService.RegisterDeviceAsync(userId, deviceId);
|
||||
else
|
||||
await SyncDeviceService.UpdateLastAccessTimeAsync(userId, deviceId);
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
string[] ids = deviceService.FetchUnsyncedIds();
|
||||
var ids = await SyncDeviceService.FetchUnsyncedIdsAsync(userId, deviceId);
|
||||
if (!device.IsSyncReset && ids.Count == 0)
|
||||
return new SyncV2Metadata { Synced = true };
|
||||
|
||||
var chunks = PrepareChunks(
|
||||
userId,
|
||||
ids,
|
||||
size: 1000,
|
||||
resetSync: isResetSync,
|
||||
resetSync: device.IsSyncReset,
|
||||
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)
|
||||
if (!device.IsSyncReset)
|
||||
{
|
||||
var syncedIds = chunk.Items.Select((i) => $"{i.ItemId}:{chunk.Type}").ToHashSet();
|
||||
ids = ids.Where((id) => !syncedIds.Contains(id)).ToArray();
|
||||
deviceService.WritePendingIds(ids);
|
||||
ids.ExceptWith(chunk.Items.Select(i => new ItemKey(i.ItemId, chunk.Type)));
|
||||
await SyncDeviceService.WritePendingIdsAsync(userId, deviceId, ids);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeMonographs)
|
||||
{
|
||||
var isSyncingMonographsForFirstTime = !device.HasInitialMonographsSync;
|
||||
var unsyncedMonographs = ids.Where((id) => id.EndsWith(":monograph")).ToHashSet();
|
||||
var unsyncedMonographIds = unsyncedMonographs.Select((id) => id.Split(":")[0]).ToArray();
|
||||
FilterDefinition<Monograph> filter = isResetSync || isSyncingMonographsForFirstTime
|
||||
? Builders<Monograph>.Filter.Eq("UserId", userId)
|
||||
var unsyncedMonographIds = ids.Where(k => k.Type == "monograph").Select(k => k.ItemId);
|
||||
FilterDefinition<Monograph> filter = device.IsSyncReset
|
||||
? Builders<Monograph>.Filter.Eq(m => m.UserId, userId)
|
||||
: Builders<Monograph>.Filter.And(
|
||||
Builders<Monograph>.Filter.Eq("UserId", userId),
|
||||
Builders<Monograph>.Filter.Eq(m => m.UserId, userId),
|
||||
Builders<Monograph>.Filter.Or(
|
||||
Builders<Monograph>.Filter.In("ItemId", unsyncedMonographIds),
|
||||
Builders<Monograph>.Filter.In(m => m.ItemId, unsyncedMonographIds),
|
||||
Builders<Monograph>.Filter.In("_id", unsyncedMonographIds)
|
||||
)
|
||||
);
|
||||
@@ -285,30 +282,26 @@ namespace Notesnook.API.Hubs
|
||||
Password = m.Password,
|
||||
SelfDestruct = m.SelfDestruct,
|
||||
Title = m.Title,
|
||||
ItemId = m.ItemId ?? m.Id.ToString(),
|
||||
ViewCount = m.ViewCount
|
||||
ItemId = m.ItemId ?? m.Id.ToString()
|
||||
}).ToListAsync();
|
||||
|
||||
if (userMonographs.Count > 0 && !await Clients.Caller.SendMonographs(userMonographs).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||
throw new HubException("Client rejected monographs.");
|
||||
|
||||
device.HasInitialMonographsSync = true;
|
||||
}
|
||||
|
||||
if (includeInboxItems)
|
||||
{
|
||||
var unsyncedInboxItems = ids.Where((id) => id.EndsWith(":inboxItems")).ToHashSet();
|
||||
var unsyncedInboxItemIds = unsyncedInboxItems.Select((id) => id.Split(":")[0]).ToArray();
|
||||
var userInboxItems = isResetSync
|
||||
var unsyncedInboxItemIds = ids.Where(k => k.Type == "inbox_item").Select(k => k.ItemId);
|
||||
var userInboxItems = device.IsSyncReset
|
||||
? await Repositories.InboxItems.FindAsync(m => m.UserId == userId)
|
||||
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId));
|
||||
: await Repositories.InboxItems.FindAsync(m => m.UserId == userId && unsyncedInboxItemIds.Contains(m.ItemId ?? m.Id.ToString()));
|
||||
if (userInboxItems.Any() && !await Clients.Caller.SendInboxItems(userInboxItems).WaitAsync(TimeSpan.FromMinutes(10)))
|
||||
{
|
||||
throw new HubException("Client rejected inbox items.");
|
||||
}
|
||||
}
|
||||
|
||||
deviceService.Reset();
|
||||
await SyncDeviceService.ResetAsync(userId, deviceId);
|
||||
|
||||
return new SyncV2Metadata
|
||||
{
|
||||
|
||||
@@ -42,5 +42,7 @@ namespace Notesnook.API.Interfaces
|
||||
Repository<Monograph> Monographs { get; }
|
||||
Repository<InboxApiKey> InboxApiKey { get; }
|
||||
Repository<InboxSyncItem> InboxItems { get; }
|
||||
Repository<SyncDevice> SyncDevices { get; }
|
||||
Repository<DeviceIdsChunk> DeviceIdsChunks { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Services;
|
||||
using Quartz;
|
||||
|
||||
namespace Notesnook.API.Jobs
|
||||
{
|
||||
public class DeviceCleanupJob : IJob
|
||||
public class DeviceCleanupJob(ISyncItemsRepositoryAccessor repositories) : IJob
|
||||
{
|
||||
public Task Execute(IJobExecutionContext context)
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
ParallelOptions parallelOptions = new()
|
||||
var cutoffDate = DateTimeOffset.UtcNow.AddMonths(-1).ToUnixTimeMilliseconds();
|
||||
var deviceFilter = Builders<SyncDevice>.Filter.Lt(x => x.LastAccessTime, cutoffDate);
|
||||
|
||||
using var cursor = await repositories.SyncDevices.Collection.Find(deviceFilter, new FindOptions { BatchSize = 1000 })
|
||||
.Project(x => x.DeviceId)
|
||||
.ToCursorAsync();
|
||||
|
||||
var deleteModels = new List<WriteModel<DeviceIdsChunk>>();
|
||||
while (await cursor.MoveNextAsync())
|
||||
{
|
||||
MaxDegreeOfParallelism = 100,
|
||||
CancellationToken = context.CancellationToken,
|
||||
};
|
||||
Parallel.ForEach(Directory.EnumerateDirectories("sync"), parallelOptions, (userDir, ct) =>
|
||||
if (!cursor.Current.Any()) continue;
|
||||
deleteModels.Add(new DeleteManyModel<DeviceIdsChunk>(Builders<DeviceIdsChunk>.Filter.In(x => x.DeviceId, cursor.Current)));
|
||||
}
|
||||
|
||||
if (deleteModels.Count > 0)
|
||||
{
|
||||
foreach (var device in Directory.EnumerateDirectories(userDir))
|
||||
{
|
||||
string lastAccessFile = Path.Combine(device, "LastAccessTime");
|
||||
var bulkOptions = new BulkWriteOptions { IsOrdered = false };
|
||||
await repositories.DeviceIdsChunks.Collection.BulkWriteAsync(deleteModels, bulkOptions);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(lastAccessFile))
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
string content = File.ReadAllText(lastAccessFile);
|
||||
if (!long.TryParse(content, out long lastAccessTime) || lastAccessTime <= 0)
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTimeOffset accessTime;
|
||||
try
|
||||
{
|
||||
accessTime = DateTimeOffset.FromUnixTimeMilliseconds(lastAccessTime);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the device hasn't been accessed for more than one month, delete it.
|
||||
if (accessTime.AddMonths(1) < DateTimeOffset.UtcNow)
|
||||
{
|
||||
Directory.Delete(device, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error and continue processing other directories.
|
||||
Console.Error.WriteLine($"Error processing device '{device}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
await repositories.SyncDevices.Collection.DeleteManyAsync(deviceFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Notesnook.API/Models/DeviceIdsChunk.cs
Normal file
16
Notesnook.API/Models/DeviceIdsChunk.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class DeviceIdsChunk
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
public required string UserId { get; set; }
|
||||
public required string DeviceId { get; set; }
|
||||
public required string Key { get; set; }
|
||||
public required string[] Ids { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ 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.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
@@ -48,8 +48,5 @@ namespace Notesnook.API.Models
|
||||
|
||||
[JsonPropertyName("deleted")]
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
[JsonPropertyName("viewCount")]
|
||||
public int ViewCount { get; set; }
|
||||
}
|
||||
}
|
||||
19
Notesnook.API/Models/SyncDevice.cs
Normal file
19
Notesnook.API/Models/SyncDevice.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Notesnook.API.Models
|
||||
{
|
||||
public class SyncDevice
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
|
||||
public required string UserId { get; set; }
|
||||
public required string DeviceId { get; set; }
|
||||
public required long LastAccessTime { get; set; }
|
||||
public required bool IsSyncReset { get; set; }
|
||||
public string? AppVersion { get; set; }
|
||||
public string? DatabaseVersion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,12 @@ 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Streetwriters.Common;
|
||||
using System.Net;
|
||||
|
||||
namespace Notesnook.API
|
||||
{
|
||||
|
||||
@@ -30,10 +30,12 @@ using Amazon.S3.Model;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Accessors;
|
||||
using Notesnook.API.Helpers;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Accessors;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Models;
|
||||
@@ -52,6 +54,7 @@ namespace Notesnook.API.Services
|
||||
private readonly string INTERNAL_BUCKET_NAME = Constants.S3_INTERNAL_BUCKET_NAME ?? Constants.S3_BUCKET_NAME;
|
||||
private readonly S3FailoverHelper S3Client;
|
||||
private ISyncItemsRepositoryAccessor Repositories { get; }
|
||||
private WampServiceAccessor ServiceAccessor { get; }
|
||||
|
||||
// When running in a dockerized environment the sync server doesn't have access
|
||||
// to the host's S3 Service URL. It can only talk to S3 server via its own internal
|
||||
@@ -64,9 +67,10 @@ namespace Notesnook.API.Services
|
||||
private readonly S3FailoverHelper S3InternalClient;
|
||||
private readonly HttpClient httpClient = new();
|
||||
|
||||
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, ILogger<S3Service> logger)
|
||||
public S3Service(ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor, WampServiceAccessor wampServiceAccessor, ILogger<S3Service> logger)
|
||||
{
|
||||
Repositories = syncItemsRepositoryAccessor;
|
||||
ServiceAccessor = wampServiceAccessor;
|
||||
S3Client = new S3FailoverHelper(
|
||||
S3ClientFactory.CreateS3Clients(
|
||||
Constants.S3_SERVICE_URL,
|
||||
@@ -240,8 +244,7 @@ namespace Notesnook.API.Services
|
||||
|
||||
if (!Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
var subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
var subscription = await ServiceAccessor.UserSubscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
|
||||
long fileSize = await GetMultipartUploadSizeAsync(userId, uploadRequest.Key, uploadRequest.UploadId);
|
||||
if (StorageHelper.IsFileSizeExceeded(subscription, fileSize))
|
||||
|
||||
@@ -24,220 +24,201 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Driver;
|
||||
using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
|
||||
namespace Notesnook.API.Services
|
||||
{
|
||||
public struct SyncDevice(string userId, string deviceId)
|
||||
public readonly record struct ItemKey(string ItemId, string Type)
|
||||
{
|
||||
public readonly string DeviceId => deviceId;
|
||||
public readonly string UserId => userId;
|
||||
|
||||
public string UserSyncDirectoryPath = CreateFilePath(userId);
|
||||
public string UserDeviceDirectoryPath = CreateFilePath(userId, deviceId);
|
||||
public string PendingIdsFilePath = CreateFilePath(userId, deviceId, "pending");
|
||||
public string UnsyncedIdsFilePath = CreateFilePath(userId, deviceId, "unsynced");
|
||||
public string ResetSyncFilePath = CreateFilePath(userId, deviceId, "reset-sync");
|
||||
|
||||
public readonly long LastAccessTime
|
||||
{
|
||||
get => long.Parse(GetMetadata("LastAccessTime") ?? "0");
|
||||
set => SetMetadata("LastAccessTime", value.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the monographs have been synced for the first time
|
||||
/// ever on a device.
|
||||
/// </summary>
|
||||
public readonly bool HasInitialMonographsSync
|
||||
{
|
||||
get => !string.IsNullOrEmpty(GetMetadata("HasInitialMonographsSync"));
|
||||
set => SetMetadata("HasInitialMonographsSync", value.ToString());
|
||||
}
|
||||
|
||||
private static string CreateFilePath(string userId, string? deviceId = null, string? metadataKey = null)
|
||||
{
|
||||
return Path.Join("sync", userId, deviceId, metadataKey);
|
||||
}
|
||||
|
||||
private readonly string? GetMetadata(string metadataKey)
|
||||
{
|
||||
var path = CreateFilePath(userId, deviceId, metadataKey);
|
||||
if (!File.Exists(path)) return null;
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
private readonly void SetMetadata(string metadataKey, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = CreateFilePath(userId, deviceId, metadataKey);
|
||||
File.WriteAllText(path, value);
|
||||
}
|
||||
catch (DirectoryNotFoundException) { }
|
||||
}
|
||||
public override string ToString() => $"{ItemId}:{Type}";
|
||||
}
|
||||
|
||||
public class SyncDeviceService(SyncDevice device)
|
||||
public class SyncDeviceService(ISyncItemsRepositoryAccessor repositories, ILogger<SyncDeviceService> logger)
|
||||
{
|
||||
public string[] GetUnsyncedIds()
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ReadAllLines(device.UnsyncedIdsFilePath);
|
||||
}
|
||||
catch { return []; }
|
||||
}
|
||||
private static FilterDefinition<SyncDevice> DeviceFilter(string userId, string deviceId) =>
|
||||
Builders<SyncDevice>.Filter.Eq(x => x.UserId, userId) &
|
||||
Builders<SyncDevice>.Filter.Eq(x => x.DeviceId, deviceId);
|
||||
private static FilterDefinition<DeviceIdsChunk> DeviceIdsChunkFilter(string userId, string deviceId, string key) =>
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.UserId, userId) &
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.DeviceId, deviceId) &
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.Key, key);
|
||||
|
||||
public string[] GetUnsyncedIds(string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ReadAllLines(Path.Join(device.UserSyncDirectoryPath, deviceId, "unsynced"));
|
||||
}
|
||||
catch { return []; }
|
||||
}
|
||||
private static FilterDefinition<DeviceIdsChunk> DeviceIdsChunkFilter(string userId, string deviceId) =>
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.UserId, userId) &
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.DeviceId, deviceId);
|
||||
|
||||
public string[] FetchUnsyncedIds()
|
||||
private static FilterDefinition<DeviceIdsChunk> DeviceIdsChunkFilter(string userId) =>
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.UserId, userId);
|
||||
|
||||
private static FilterDefinition<SyncDevice> UserFilter(string userId) => Builders<SyncDevice>.Filter.Eq(x => x.UserId, userId);
|
||||
|
||||
|
||||
public async Task<HashSet<ItemKey>> GetIdsAsync(string userId, string deviceId, string key)
|
||||
{
|
||||
if (IsSyncReset()) return [];
|
||||
try
|
||||
var cursor = await repositories.DeviceIdsChunks.Collection.FindAsync(DeviceIdsChunkFilter(userId, deviceId, key));
|
||||
var result = new HashSet<ItemKey>();
|
||||
while (await cursor.MoveNextAsync())
|
||||
{
|
||||
var unsyncedIds = GetUnsyncedIds();
|
||||
lock (device.DeviceId)
|
||||
foreach (var chunk in cursor.Current)
|
||||
{
|
||||
if (IsSyncPending())
|
||||
foreach (var id in chunk.Ids)
|
||||
{
|
||||
unsyncedIds = unsyncedIds.Union(File.ReadAllLines(device.PendingIdsFilePath)).ToArray();
|
||||
var parts = id.Split(':', 2);
|
||||
result.Add(new ItemKey(parts[0], parts[1]));
|
||||
}
|
||||
|
||||
if (unsyncedIds.Length == 0) return [];
|
||||
|
||||
File.Delete(device.UnsyncedIdsFilePath);
|
||||
File.WriteAllLines(device.PendingIdsFilePath, unsyncedIds);
|
||||
}
|
||||
return unsyncedIds;
|
||||
}
|
||||
catch
|
||||
return result;
|
||||
}
|
||||
|
||||
const int MaxIdsPerChunk = 400_000;
|
||||
public async Task AppendIdsAsync(string userId, string deviceId, string key, IEnumerable<ItemKey> ids)
|
||||
{
|
||||
var filter = DeviceIdsChunkFilter(userId, deviceId, key) & Builders<DeviceIdsChunk>.Filter.Where(x => x.Ids.Length < MaxIdsPerChunk);
|
||||
var chunk = await repositories.DeviceIdsChunks.Collection.Find(filter).FirstOrDefaultAsync();
|
||||
|
||||
if (chunk != null)
|
||||
{
|
||||
return [];
|
||||
var update = Builders<DeviceIdsChunk>.Update.PushEach(x => x.Ids, ids.Select(i => i.ToString()));
|
||||
await repositories.DeviceIdsChunks.Collection.UpdateOneAsync(
|
||||
Builders<DeviceIdsChunk>.Filter.Eq(x => x.Id, chunk.Id),
|
||||
update
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void WritePendingIds(IEnumerable<string> ids)
|
||||
{
|
||||
lock (device.DeviceId)
|
||||
else
|
||||
{
|
||||
File.WriteAllLines(device.PendingIdsFilePath, ids);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSyncReset()
|
||||
{
|
||||
return File.Exists(device.ResetSyncFilePath);
|
||||
}
|
||||
public bool IsSyncReset(string deviceId)
|
||||
{
|
||||
return File.Exists(Path.Join(device.UserSyncDirectoryPath, deviceId, "reset-sync"));
|
||||
}
|
||||
|
||||
public bool IsSyncPending()
|
||||
{
|
||||
return File.Exists(device.PendingIdsFilePath);
|
||||
}
|
||||
|
||||
public bool IsUnsynced()
|
||||
{
|
||||
return File.Exists(device.UnsyncedIdsFilePath);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (device.UserId)
|
||||
var newChunk = new DeviceIdsChunk
|
||||
{
|
||||
File.Delete(device.ResetSyncFilePath);
|
||||
File.Delete(device.PendingIdsFilePath);
|
||||
}
|
||||
UserId = userId,
|
||||
DeviceId = deviceId,
|
||||
Key = key,
|
||||
Ids = [.. ids.Select(i => i.ToString())]
|
||||
};
|
||||
await repositories.DeviceIdsChunks.Collection.InsertOneAsync(newChunk);
|
||||
}
|
||||
catch (FileNotFoundException) { }
|
||||
catch (DirectoryNotFoundException) { }
|
||||
|
||||
var emptyChunksFilter = DeviceIdsChunkFilter(userId, deviceId, key) & Builders<DeviceIdsChunk>.Filter.Size(x => x.Ids, 0);
|
||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(emptyChunksFilter);
|
||||
}
|
||||
|
||||
public bool IsDeviceRegistered()
|
||||
public async Task WriteIdsAsync(string userId, string deviceId, string key, IEnumerable<ItemKey> ids)
|
||||
{
|
||||
return Directory.Exists(device.UserDeviceDirectoryPath);
|
||||
}
|
||||
public bool IsDeviceRegistered(string deviceId)
|
||||
{
|
||||
return Directory.Exists(Path.Join(device.UserSyncDirectoryPath, deviceId));
|
||||
}
|
||||
|
||||
public string[] ListDevices()
|
||||
{
|
||||
return Directory.GetDirectories(device.UserSyncDirectoryPath).Select((path) => path[(path.LastIndexOf(Path.DirectorySeparatorChar) + 1)..]).ToArray();
|
||||
}
|
||||
|
||||
public void ResetDevices()
|
||||
{
|
||||
lock (device.UserId)
|
||||
var writes = new List<WriteModel<DeviceIdsChunk>>
|
||||
{
|
||||
if (File.Exists(device.UserSyncDirectoryPath)) File.Delete(device.UserSyncDirectoryPath);
|
||||
Directory.CreateDirectory(device.UserSyncDirectoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddIdsToOtherDevices(List<string> ids)
|
||||
{
|
||||
device.LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
foreach (string id in ListDevices())
|
||||
new DeleteManyModel<DeviceIdsChunk>(DeviceIdsChunkFilter(userId, deviceId, key))
|
||||
};
|
||||
var chunks = ids.Chunk(MaxIdsPerChunk);
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
if (id == device.DeviceId || IsSyncReset(id)) continue;
|
||||
|
||||
lock (id)
|
||||
var newChunk = new DeviceIdsChunk
|
||||
{
|
||||
if (!IsDeviceRegistered(id)) Directory.CreateDirectory(Path.Join(device.UserSyncDirectoryPath, id));
|
||||
|
||||
var oldIds = GetUnsyncedIds(id);
|
||||
File.WriteAllLines(Path.Join(device.UserSyncDirectoryPath, id, "unsynced"), ids.Union(oldIds));
|
||||
}
|
||||
UserId = userId,
|
||||
DeviceId = deviceId,
|
||||
Key = key,
|
||||
Ids = [.. chunk.Select(i => i.ToString())]
|
||||
};
|
||||
writes.Add(new InsertOneModel<DeviceIdsChunk>(newChunk));
|
||||
}
|
||||
await repositories.DeviceIdsChunks.Collection.BulkWriteAsync(writes);
|
||||
}
|
||||
|
||||
public void AddIdsToAllDevices(List<string> ids)
|
||||
public async Task<HashSet<ItemKey>> FetchUnsyncedIdsAsync(string userId, string deviceId)
|
||||
{
|
||||
foreach (var id in ListDevices())
|
||||
var device = await GetDeviceAsync(userId, deviceId);
|
||||
if (device == null || device.IsSyncReset) return [];
|
||||
|
||||
var unsyncedIds = await GetIdsAsync(userId, deviceId, "unsynced");
|
||||
var pendingIds = await GetIdsAsync(userId, deviceId, "pending");
|
||||
|
||||
unsyncedIds = [.. unsyncedIds, .. pendingIds];
|
||||
|
||||
if (unsyncedIds.Count == 0) return [];
|
||||
|
||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId, deviceId, "unsynced"));
|
||||
await WriteIdsAsync(userId, deviceId, "pending", unsyncedIds);
|
||||
|
||||
return unsyncedIds;
|
||||
}
|
||||
|
||||
public async Task WritePendingIdsAsync(string userId, string deviceId, HashSet<ItemKey> ids)
|
||||
{
|
||||
await WriteIdsAsync(userId, deviceId, "pending", ids);
|
||||
}
|
||||
|
||||
public async Task ResetAsync(string userId, string deviceId)
|
||||
{
|
||||
await repositories.SyncDevices.Collection.UpdateOneAsync(DeviceFilter(userId, deviceId), Builders<SyncDevice>.Update
|
||||
.Set(x => x.IsSyncReset, false));
|
||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId, deviceId, "pending"));
|
||||
}
|
||||
|
||||
public async Task<SyncDevice?> GetDeviceAsync(string userId, string deviceId)
|
||||
{
|
||||
return await repositories.SyncDevices.Collection.Find(DeviceFilter(userId, deviceId)).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<SyncDevice> ListDevicesAsync(string userId)
|
||||
{
|
||||
using var cursor = await repositories.SyncDevices.Collection.FindAsync(UserFilter(userId));
|
||||
while (await cursor.MoveNextAsync())
|
||||
{
|
||||
if (IsSyncReset(id)) return;
|
||||
lock (id)
|
||||
foreach (var device in cursor.Current)
|
||||
{
|
||||
if (!IsDeviceRegistered(id)) Directory.CreateDirectory(Path.Join(device.UserSyncDirectoryPath, id));
|
||||
|
||||
var oldIds = GetUnsyncedIds(id);
|
||||
File.WriteAllLinesAsync(Path.Join(device.UserSyncDirectoryPath, id, "unsynced"), ids.Union(oldIds));
|
||||
yield return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterDevice()
|
||||
public async Task ResetDevicesAsync(string userId)
|
||||
{
|
||||
lock (device.UserId)
|
||||
await repositories.SyncDevices.Collection.DeleteManyAsync(UserFilter(userId));
|
||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId));
|
||||
}
|
||||
|
||||
public async Task UpdateLastAccessTimeAsync(string userId, string deviceId)
|
||||
{
|
||||
await repositories.SyncDevices.Collection.UpdateOneAsync(DeviceFilter(userId, deviceId), Builders<SyncDevice>.Update
|
||||
.Set(x => x.LastAccessTime, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
|
||||
}
|
||||
|
||||
public async Task AddIdsToOtherDevicesAsync(string userId, string deviceId, IEnumerable<ItemKey> ids)
|
||||
{
|
||||
await UpdateLastAccessTimeAsync(userId, deviceId);
|
||||
await foreach (var device in ListDevicesAsync(userId))
|
||||
{
|
||||
if (Directory.Exists(device.UserDeviceDirectoryPath))
|
||||
Directory.Delete(device.UserDeviceDirectoryPath, true);
|
||||
Directory.CreateDirectory(device.UserDeviceDirectoryPath);
|
||||
File.Create(device.ResetSyncFilePath).Close();
|
||||
device.LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (device.DeviceId == deviceId || device.IsSyncReset) continue;
|
||||
await AppendIdsAsync(userId, device.DeviceId, "unsynced", ids);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterDevice()
|
||||
public async Task AddIdsToAllDevicesAsync(string userId, IEnumerable<ItemKey> ids)
|
||||
{
|
||||
lock (device.UserId)
|
||||
await foreach (var device in ListDevicesAsync(userId))
|
||||
{
|
||||
if (!Path.Exists(device.UserDeviceDirectoryPath)) return;
|
||||
Directory.Delete(device.UserDeviceDirectoryPath, true);
|
||||
if (device.IsSyncReset) continue;
|
||||
await AppendIdsAsync(userId, device.DeviceId, "unsynced", ids);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SyncDevice> RegisterDeviceAsync(string userId, string deviceId)
|
||||
{
|
||||
var newDevice = new SyncDevice
|
||||
{
|
||||
UserId = userId,
|
||||
DeviceId = deviceId,
|
||||
LastAccessTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
IsSyncReset = true
|
||||
};
|
||||
await repositories.SyncDevices.Collection.InsertOneAsync(newDevice);
|
||||
return newDevice;
|
||||
}
|
||||
|
||||
public async Task UnregisterDeviceAsync(string userId, string deviceId)
|
||||
{
|
||||
await repositories.SyncDevices.Collection.DeleteOneAsync(DeviceFilter(userId, deviceId));
|
||||
await repositories.DeviceIdsChunks.Collection.DeleteManyAsync(DeviceIdsChunkFilter(userId, deviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,9 @@ using Notesnook.API.Interfaces;
|
||||
using Notesnook.API.Models;
|
||||
using Notesnook.API.Models.Responses;
|
||||
using Streetwriters.Common;
|
||||
using Streetwriters.Common.Accessors;
|
||||
using Streetwriters.Common.Enums;
|
||||
using Streetwriters.Common.Extensions;
|
||||
using Streetwriters.Common.Interfaces;
|
||||
using Streetwriters.Common.Messages;
|
||||
using Streetwriters.Common.Models;
|
||||
using Streetwriters.Data.Interfaces;
|
||||
@@ -41,7 +41,7 @@ namespace Notesnook.API.Services
|
||||
{
|
||||
public class UserService(IHttpContextAccessor accessor,
|
||||
ISyncItemsRepositoryAccessor syncItemsRepositoryAccessor,
|
||||
IUnitOfWork unitOfWork, IS3Service s3Service, ILogger<UserService> logger) : IUserService
|
||||
IUnitOfWork unitOfWork, IS3Service s3Service, SyncDeviceService syncDeviceService, WampServiceAccessor serviceAccessor, ILogger<UserService> logger) : IUserService
|
||||
{
|
||||
private static readonly System.Security.Cryptography.RandomNumberGenerator Rng = System.Security.Cryptography.RandomNumberGenerator.Create();
|
||||
private readonly HttpClient httpClient = new();
|
||||
@@ -88,9 +88,7 @@ namespace Notesnook.API.Services
|
||||
|
||||
public async Task<UserResponse> GetUserAsync(string userId)
|
||||
{
|
||||
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
|
||||
|
||||
var user = await userService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
|
||||
var user = await serviceAccessor.UserAccountService.GetUserAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User not found.");
|
||||
|
||||
Subscription? subscription = null;
|
||||
if (Constants.IS_SELF_HOSTED)
|
||||
@@ -109,8 +107,7 @@ namespace Notesnook.API.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
var subscriptionService = await WampServers.SubscriptionServer.GetServiceAsync<IUserSubscriptionService>(SubscriptionServerTopics.UserSubscriptionServiceTopic);
|
||||
subscription = await subscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
subscription = await serviceAccessor.UserSubscriptionService.GetUserSubscriptionAsync(Clients.Notesnook.Id, userId) ?? throw new Exception("User subscription not found.");
|
||||
}
|
||||
|
||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((u) => u.UserId == user.UserId) ?? throw new Exception("User settings not found.");
|
||||
@@ -185,8 +182,6 @@ namespace Notesnook.API.Services
|
||||
|
||||
public async Task DeleteUserAsync(string userId)
|
||||
{
|
||||
new SyncDeviceService(new SyncDevice(userId, userId)).ResetDevices();
|
||||
|
||||
var cc = new CancellationTokenSource();
|
||||
|
||||
Repositories.Notes.DeleteByUserId(userId);
|
||||
@@ -209,6 +204,8 @@ namespace Notesnook.API.Services
|
||||
logger.LogInformation("User data deleted for user {UserId}: {Result}", userId, result);
|
||||
if (!result) throw new Exception("Could not delete user data.");
|
||||
|
||||
await syncDeviceService.ResetDevicesAsync(userId);
|
||||
|
||||
if (!Constants.IS_SELF_HOSTED)
|
||||
{
|
||||
await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage
|
||||
@@ -225,8 +222,7 @@ namespace Notesnook.API.Services
|
||||
{
|
||||
logger.LogInformation("Deleting user account: {UserId}", userId);
|
||||
|
||||
var userService = await WampServers.IdentityServer.GetServiceAsync<IUserAccountService>(IdentityServerTopics.UserAccountServiceTopic);
|
||||
await userService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
|
||||
await serviceAccessor.UserAccountService.DeleteUserAsync(Clients.Notesnook.Id, userId, password);
|
||||
|
||||
await DeleteUserAsync(userId);
|
||||
|
||||
@@ -246,7 +242,6 @@ namespace Notesnook.API.Services
|
||||
|
||||
public async Task<bool> ResetUserAsync(string userId, bool removeAttachments)
|
||||
{
|
||||
new SyncDeviceService(new SyncDevice(userId, userId)).ResetDevices();
|
||||
|
||||
var cc = new CancellationTokenSource();
|
||||
|
||||
@@ -266,6 +261,8 @@ namespace Notesnook.API.Services
|
||||
Repositories.InboxApiKey.DeleteMany((t) => t.UserId == userId);
|
||||
if (!await unit.Commit()) return false;
|
||||
|
||||
await syncDeviceService.ResetDevicesAsync(userId);
|
||||
|
||||
var userSettings = await Repositories.UsersSettings.FindOneAsync((s) => s.UserId == userId);
|
||||
|
||||
userSettings.AttachmentsKey = null;
|
||||
|
||||
@@ -169,14 +169,19 @@ namespace Notesnook.API
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(CallToAction)))
|
||||
BsonClassMap.RegisterClassMap<CallToAction>();
|
||||
|
||||
if (!BsonClassMap.IsClassMapRegistered(typeof(SyncDevice)))
|
||||
BsonClassMap.RegisterClassMap<SyncDevice>();
|
||||
|
||||
services.AddScoped<IDbContext, MongoDbContext>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
services.AddRepository<UserSettings>("user_settings", "notesnook")
|
||||
.AddRepository<Monograph>("monographs", "notesnook")
|
||||
.AddRepository<Announcement>("announcements", "notesnook")
|
||||
.AddRepository<DeviceIdsChunk>(Collections.DeviceIdsChunksKey, "notesnook")
|
||||
.AddRepository<SyncDevice>(Collections.SyncDevicesKey, "notesnook")
|
||||
.AddRepository<InboxApiKey>(Collections.InboxApiKeysKey, "notesnook")
|
||||
.AddRepository<InboxSyncItem>(Collections.InboxItems, "notesnook");
|
||||
.AddRepository<InboxSyncItem>(Collections.InboxItemsKey, "notesnook");
|
||||
|
||||
services.AddMongoCollection(Collections.SettingsKey)
|
||||
.AddMongoCollection(Collections.AttachmentsKey)
|
||||
@@ -190,14 +195,17 @@ namespace Notesnook.API
|
||||
.AddMongoCollection(Collections.TagsKey)
|
||||
.AddMongoCollection(Collections.ColorsKey)
|
||||
.AddMongoCollection(Collections.VaultsKey)
|
||||
.AddMongoCollection(Collections.InboxItems)
|
||||
.AddMongoCollection(Collections.InboxItemsKey)
|
||||
.AddMongoCollection(Collections.InboxApiKeysKey);
|
||||
|
||||
services.AddScoped<ISyncItemsRepositoryAccessor, SyncItemsRepositoryAccessor>();
|
||||
services.AddScoped<SyncDeviceService>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddScoped<IS3Service, S3Service>();
|
||||
services.AddScoped<IURLAnalyzer, URLAnalyzer>();
|
||||
|
||||
services.AddWampServiceAccessor(Servers.NotesnookAPI);
|
||||
|
||||
services.AddControllers();
|
||||
|
||||
services.AddHealthChecks();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.AspNetCore.SignalR": "Trace",
|
||||
|
||||
Reference in New Issue
Block a user