diff --git a/src/SIL.Harmony.Core/IRemoteResourceService.cs b/src/SIL.Harmony.Core/IRemoteResourceService.cs
index 22ebb06..3e4eefa 100644
--- a/src/SIL.Harmony.Core/IRemoteResourceService.cs
+++ b/src/SIL.Harmony.Core/IRemoteResourceService.cs
@@ -15,12 +15,14 @@ public interface IRemoteResourceService
/// path defined by the CRDT config where the resource should be stored
/// download result containing the path to the downloaded file, this is stored in the local db and not synced
Task DownloadResource(string remoteId, string localResourceCachePath);
+
///
/// upload a resource to the remote server
///
+ /// id of the resource in the CRDT
/// full path to the resource on the local machine
/// an upload result with the remote id, the id will be stored and transmitted to other clients so they can also download the resource
- Task UploadResource(string localPath);
+ Task UploadResource(Guid resourceId, string localPath);
}
public record DownloadResult(string LocalPath);
diff --git a/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs b/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
index f9d6576..59e9b84 100644
--- a/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
+++ b/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
@@ -207,4 +207,38 @@ public async Task CanGetAResourceGivenAnId()
(await _resourceService.GetResource(localAndRemoteResource.Id)).Should().BeEquivalentTo(localAndRemoteResource);
(await _resourceService.GetResource(Guid.NewGuid())).Should().BeNull();
}
+
+ [Fact]
+ public async Task DeleteResource_RemovesLocalResource()
+ {
+ // Arrange: create a local resource
+ var (resourceId, localPath) = await SetupLocalFile("delete-local");
+ (await _resourceService.GetResource(resourceId)).Should().NotBeNull();
+ (await _resourceService.GetLocalResource(resourceId)).Should().NotBeNull();
+
+ // Act: delete the resource
+ await _resourceService.DeleteResource(_localClientId, resourceId);
+
+ // Assert: resource is gone from all APIs
+ (await _resourceService.GetResource(resourceId)).Should().BeNull();
+ (await _resourceService.GetLocalResource(resourceId)).Should().BeNull();
+ (await _resourceService.AllResources()).Should().NotContain(r => r.Id == resourceId);
+ }
+
+ [Fact]
+ public async Task DeleteResource_RemovesRemoteResource()
+ {
+ // Arrange: create a remote resource
+ var (resourceId, remoteId) = await SetupRemoteResource("delete-remote");
+ (await _resourceService.GetResource(resourceId)).Should().NotBeNull();
+ (await _resourceService.GetLocalResource(resourceId)).Should().BeNull();
+
+ // Act: delete the resource
+ await _resourceService.DeleteResource(_localClientId, resourceId);
+
+ // Assert: resource is gone from all APIs
+ (await _resourceService.GetResource(resourceId)).Should().BeNull();
+ (await _resourceService.GetLocalResource(resourceId)).Should().BeNull();
+ (await _resourceService.AllResources()).Should().NotContain(r => r.Id == resourceId);
+ }
}
diff --git a/src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs b/src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs
index 6375c44..33a4f3b 100644
--- a/src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs
+++ b/src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs
@@ -26,7 +26,7 @@ public Task DownloadResource(string remoteId, string localResour
private readonly Queue _throwOnUpload = new();
- public async Task UploadResource(string localPath)
+ public async Task UploadResource(Guid resourceId, string localPath)
{
await Task.Yield();//yield back to the scheduler to emulate how exceptions are thrown
if (_throwOnUpload.TryPeek(out var throwOnUpload))
diff --git a/src/SIL.Harmony/Db/CrdtRepository.cs b/src/SIL.Harmony/Db/CrdtRepository.cs
index 768613b..a7145cb 100644
--- a/src/SIL.Harmony/Db/CrdtRepository.cs
+++ b/src/SIL.Harmony/Db/CrdtRepository.cs
@@ -30,6 +30,11 @@ public async Task Execute(Func> func)
await using var repo = await CreateRepository();
return await func(repo);
}
+ public async Task Execute(Func func)
+ {
+ await using var repo = await CreateRepository();
+ await func(repo);
+ }
public async ValueTask Execute(Func> func)
{
@@ -390,6 +395,11 @@ public async Task AddLocalResource(LocalResource localResource)
await _dbContext.SaveChangesAsync();
}
+ public async Task DeleteLocalResource(Guid id)
+ {
+ await _dbContext.Set().Where(r => r.Id == id).ExecuteDeleteAsync();
+ }
+
public IAsyncEnumerable LocalResourcesByIds(IEnumerable resourceIds)
{
return _dbContext.Set().Where(r => resourceIds.Contains(r.Id)).AsAsyncEnumerable();
diff --git a/src/SIL.Harmony/Resource/CreateRemoteResourceChange.cs b/src/SIL.Harmony/Resource/CreateRemoteResourceChange.cs
index 685ddb9..6840198 100644
--- a/src/SIL.Harmony/Resource/CreateRemoteResourceChange.cs
+++ b/src/SIL.Harmony/Resource/CreateRemoteResourceChange.cs
@@ -3,7 +3,7 @@
namespace SIL.Harmony.Resource;
-public class CreateRemoteResourceChange(Guid resourceId, string remoteId) : CreateChange(resourceId), IPolyType
+public class CreateRemoteResourceChange(Guid entityId, string remoteId) : CreateChange(entityId), IPolyType
{
public string RemoteId { get; set; } = remoteId;
public override ValueTask NewEntity(Commit commit, IChangeContext context)
diff --git a/src/SIL.Harmony/Resource/CreateRemoteResourcePendingUpload.cs b/src/SIL.Harmony/Resource/CreateRemoteResourcePendingUpload.cs
index 1cce929..73f2a2d 100644
--- a/src/SIL.Harmony/Resource/CreateRemoteResourcePendingUpload.cs
+++ b/src/SIL.Harmony/Resource/CreateRemoteResourcePendingUpload.cs
@@ -3,12 +3,9 @@
namespace SIL.Harmony.Resource;
-public class CreateRemoteResourcePendingUploadChange: CreateChange, IPolyType
+public class CreateRemoteResourcePendingUploadChange(Guid entityId)
+ : CreateChange(entityId), IPolyType
{
- public CreateRemoteResourcePendingUploadChange(Guid resourceId) : base(resourceId)
- {
- }
-
public override ValueTask NewEntity(Commit commit, IChangeContext context)
{
return ValueTask.FromResult(new RemoteResource
diff --git a/src/SIL.Harmony/Resource/HarmonyResource.cs b/src/SIL.Harmony/Resource/HarmonyResource.cs
index 072f29d..cfe455e 100644
--- a/src/SIL.Harmony/Resource/HarmonyResource.cs
+++ b/src/SIL.Harmony/Resource/HarmonyResource.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace SIL.Harmony.Resource;
public class HarmonyResource
@@ -5,6 +7,8 @@ public class HarmonyResource
public required Guid Id { get; init; }
public string? RemoteId { get; init; }
public string? LocalPath { get; init; }
+ [MemberNotNullWhen(true, nameof(LocalPath))]
public bool Local => !string.IsNullOrEmpty(LocalPath);
+ [MemberNotNullWhen(true, nameof(RemoteId))]
public bool Remote => !string.IsNullOrEmpty(RemoteId);
-}
\ No newline at end of file
+}
diff --git a/src/SIL.Harmony/ResourceService.cs b/src/SIL.Harmony/ResourceService.cs
index c4fae85..344765c 100644
--- a/src/SIL.Harmony/ResourceService.cs
+++ b/src/SIL.Harmony/ResourceService.cs
@@ -28,28 +28,42 @@ private void ValidateResourcesSetup()
if (!_crdtConfig.Value.RemoteResourcesEnabled) throw new RemoteResourceNotEnabledException();
}
+ public async Task AddExistingRemoteResource(string resourcePath,
+ Guid clientId,
+ Guid resourceId, string remoteId)
+ {
+ ValidateResourcesSetup();
+ var localResource = new LocalResource
+ {
+ Id = resourceId,
+ LocalPath = Path.GetFullPath(resourcePath)
+ };
+ if (!localResource.FileExists()) throw new FileNotFoundException(localResource.LocalPath);
+
+ await _dataModel.AddChange(clientId, new CreateRemoteResourceChange(localResource.Id, remoteId));
+ await using var repo = await _crdtRepositoryFactory.CreateRepository();
+ await repo.AddLocalResource(localResource);
+ }
+
public async Task AddLocalResource(string resourcePath,
Guid clientId,
Guid id = default,
IRemoteResourceService? resourceService = null)
{
ValidateResourcesSetup();
- await using var repo = await _crdtRepositoryFactory.CreateRepository();
var localResource = new LocalResource
{
Id = id == default ? Guid.NewGuid() : id,
LocalPath = Path.GetFullPath(resourcePath)
};
if (!localResource.FileExists()) throw new FileNotFoundException(localResource.LocalPath);
- await using var transaction = await repo.BeginTransactionAsync();
- await repo.AddLocalResource(localResource);
UploadResult? uploadResult = null;
if (resourceService is not null)
{
try
{
- uploadResult = await resourceService.UploadResource(localResource.LocalPath);
+ uploadResult = await resourceService.UploadResource(localResource.Id, localResource.LocalPath);
}
catch (Exception e)
{
@@ -66,7 +80,7 @@ public async Task AddLocalResource(string resourcePath,
await _dataModel.AddChange(clientId, new CreateRemoteResourcePendingUploadChange(localResource.Id));
}
- await transaction.CommitAsync();
+ await _crdtRepositoryFactory.Execute(repo => repo.AddLocalResource(localResource));
return new HarmonyResource
{
Id = localResource.Id,
@@ -88,12 +102,13 @@ public async Task UploadPendingResources(Guid clientId, IRemoteResourceService r
{
ValidateResourcesSetup();
var pendingUploads = await ListResourcesPendingUpload();
+ if (pendingUploads is []) return;
var changes = new List(pendingUploads.Length);
try
{
foreach (var localResource in pendingUploads)
{
- var uploadResult = await remoteResourceService.UploadResource(localResource.LocalPath);
+ var uploadResult = await remoteResourceService.UploadResource(localResource.Id, localResource.LocalPath);
changes.Add(new RemoteResourceUploadedChange(localResource.Id, uploadResult.RemoteId));
}
}
@@ -116,7 +131,7 @@ public async Task UploadPendingResource(Guid resourceId, Guid clientId, IRemoteR
public async Task UploadPendingResource(LocalResource localResource, Guid clientId, IRemoteResourceService remoteResourceService)
{
ValidateResourcesSetup();
- var uploadResult = await remoteResourceService.UploadResource(localResource.LocalPath);
+ var uploadResult = await remoteResourceService.UploadResource(localResource.Id, localResource.LocalPath);
await _dataModel.AddChange(clientId, new RemoteResourceUploadedChange(localResource.Id, uploadResult.RemoteId));
}
@@ -196,4 +211,11 @@ private async Task> AllResourcesInternal()
var resources = await AllResourcesInternal();
return resources.FirstOrDefault(r => r.Id == resourceId);
}
+
+ public async Task DeleteResource(Guid clientId, Guid resourceId)
+ {
+ await _dataModel.AddChange(clientId, new DeleteChange(resourceId));
+ await using var repo = await _crdtRepositoryFactory.CreateRepository();
+ await repo.DeleteLocalResource(resourceId);
+ }
}