-
Notifications
You must be signed in to change notification settings - Fork 1
add remote resource support #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e767624
update resource changes to use either Edit or Create change where rel…
hahn-kev 8b1237e
allow multiple object adapters
hahn-kev 034c3dc
fix some lingering issues after updating
hahn-kev dd63456
Merge branch 'main' into remote-resources
hahn-kev 5448bd2
refactor resource methods out of DataModel and into their own service
hahn-kev 41abcca
add resource example in the sample project
hahn-kev a3168c8
add ImageResourceId to dbcontext model verification
hahn-kev 5125d33
rename mock method to be more explicit
hahn-kev 5ed1756
expose api to list all resources
hahn-kev f8d670a
test AllResources api, change `AddLocalResource` to return a CrdtReso…
hahn-kev 4b5369f
fix spelling mistake
hahn-kev 5db0631
rename CrdtResource to HarmonyResource
hahn-kev cf5f20b
upgrade Fluent assert to a version with NRT support
hahn-kev 5d30a33
try uploading resources to the remote server, log an error on failur…
hahn-kev 7d353cf
Merge branch 'main' into remote-resources
hahn-kev 5d4a6a5
update central package version
hahn-kev 16f4333
fix bug caught by Graphite
hahn-kev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| namespace SIL.Harmony.Core; | ||
|
|
||
| public class EntityNotFoundException(string message) : Exception(message); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| namespace SIL.Harmony.Core; | ||
|
|
||
| /// <summary> | ||
| /// interface to facilitate downloading of resources, typically implemented in application code | ||
| /// the remote Id is opaque to the CRDT lib and could be a URL or some other identifier provided by the backend | ||
| /// the local path returned for the application code to use as required, it could be a URL if needed also. | ||
| /// </summary> | ||
| public interface IRemoteResourceService | ||
| { | ||
| /// <summary> | ||
| /// instructs application code to download a resource from the remote server | ||
| /// the service is responsible for downloading the resource and returning the local path | ||
| /// </summary> | ||
| /// <param name="remoteId">ID used to identify the remote resource, could be a URL</param> | ||
| /// <param name="localResourceCachePath">path defined by the CRDT config where the resource should be stored</param> | ||
| /// <returns>download result containing the path to the downloaded file, this is stored in the local db and not synced</returns> | ||
| Task<DownloadResult> DownloadResource(string remoteId, string localResourceCachePath); | ||
| /// <summary> | ||
| /// upload a resource to the remote server | ||
| /// </summary> | ||
| /// <param name="localPath">full path to the resource on the local machine</param> | ||
| /// <returns>an upload result with the remote id, the id will be stored and transmitted to other clients so they can also download the resource</returns> | ||
| Task<UploadResult> UploadResource(string localPath); | ||
hahn-kev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public record DownloadResult(string LocalPath); | ||
| public record UploadResult(string RemoteId); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| using SIL.Harmony.Changes; | ||
| using SIL.Harmony.Entities; | ||
| using SIL.Harmony.Sample.Models; | ||
|
|
||
| namespace SIL.Harmony.Sample.Changes; | ||
|
|
||
| public class AddWordImageChange(Guid entityId, Guid imageId) : EditChange<Word>(entityId), ISelfNamedType<AddWordImageChange> | ||
| { | ||
| public Guid ImageId { get; } = imageId; | ||
|
|
||
| public override async ValueTask ApplyChange(Word entity, ChangeContext context) | ||
| { | ||
| if (!await context.IsObjectDeleted(ImageId)) entity.ImageResourceId = ImageId; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
210 changes: 210 additions & 0 deletions
210
src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| using System.Runtime.CompilerServices; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using SIL.Harmony.Resource; | ||
|
|
||
| namespace SIL.Harmony.Tests.ResourceTests; | ||
|
|
||
| public class RemoteResourcesTests : DataModelTestBase | ||
hahn-kev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| private RemoteServiceMock _remoteServiceMock = new(); | ||
| private ResourceService _resourceService => _services.GetRequiredService<ResourceService>(); | ||
|
|
||
| public RemoteResourcesTests() | ||
| { | ||
| } | ||
|
|
||
| private string CreateFile(string contents, [CallerMemberName] string fileName = "") | ||
| { | ||
| var filePath = Path.GetFullPath(fileName + ".txt"); | ||
| File.WriteAllText(filePath, contents); | ||
| return filePath; | ||
| } | ||
|
|
||
| private async Task<(Guid resourceId, string remoteId)> SetupRemoteResource(string fileContents) | ||
| { | ||
| var remoteId = _remoteServiceMock.CreateRemoteResource(fileContents); | ||
| var resourceId = Guid.NewGuid(); | ||
| await DataModel.AddChange(_localClientId, new CreateRemoteResourceChange(resourceId, remoteId)); | ||
| return (resourceId, remoteId); | ||
| } | ||
|
|
||
| private async Task<(Guid resourceId, string localPath)> SetupLocalFile(string contents, [CallerMemberName] string fileName = "") | ||
| { | ||
| var file = CreateFile(contents, fileName); | ||
| //because resource service is null the file is not uploaded | ||
| var crdtResource = await _resourceService.AddLocalResource(file, _localClientId, resourceService: null); | ||
| return (crdtResource.Id, file); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CreatingAResourceResultsInPendingLocalResources() | ||
| { | ||
| var (_, file) = await SetupLocalFile("contents"); | ||
|
|
||
| //act | ||
| var pending = await _resourceService.ListResourcesPendingUpload(); | ||
|
|
||
|
|
||
| pending.Should().ContainSingle().Which.LocalPath.Should().Be(file); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ResourcesNotLocalShouldShowUpAsNotDownloaded() | ||
| { | ||
| var (resourceId, remoteId) = await SetupRemoteResource("test"); | ||
|
|
||
| //act | ||
| var pending = await _resourceService.ListResourcesPendingDownload(); | ||
|
|
||
|
|
||
| var remoteResource = pending.Should().ContainSingle().Subject; | ||
| remoteResource.RemoteId.Should().Be(remoteId); | ||
| remoteResource.Id.Should().Be(resourceId); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanUploadFileToRemote() | ||
| { | ||
| var fileContents = "resource"; | ||
| var localFile = CreateFile(fileContents); | ||
|
|
||
| //act | ||
| var crdtResource = await _resourceService.AddLocalResource(localFile, _localClientId, resourceService: _remoteServiceMock); | ||
|
|
||
|
|
||
| var resource = await DataModel.GetLatest<RemoteResource>(crdtResource.Id); | ||
| ArgumentNullException.ThrowIfNull(resource); | ||
| ArgumentNullException.ThrowIfNull(resource.RemoteId); | ||
| _remoteServiceMock.ReadFile(resource.RemoteId).Should().Be(fileContents); | ||
| var pendingUpload = await _resourceService.ListResourcesPendingUpload(); | ||
| pendingUpload.Should().BeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task IfUploadingFailsTheResourceIsStillAddedAsPendingUpload() | ||
| { | ||
| var fileContents = "resource"; | ||
| var localFile = CreateFile(fileContents); | ||
|
|
||
| //todo setup a mock that throws an exception when uploading | ||
| _remoteServiceMock.ThrowOnUpload(localFile); | ||
|
|
||
| //act | ||
| var crdtResource = await _resourceService.AddLocalResource(localFile, _localClientId, resourceService: _remoteServiceMock); | ||
|
|
||
| var harmonyResource = await _resourceService.GetResource(crdtResource.Id); | ||
| harmonyResource.Should().NotBeNull(); | ||
| harmonyResource.Id.Should().Be(crdtResource.Id); | ||
| harmonyResource.RemoteId.Should().BeNull(); | ||
| harmonyResource.LocalPath.Should().Be(localFile); | ||
| var pendingUpload = await _resourceService.ListResourcesPendingUpload(); | ||
| pendingUpload.Should().ContainSingle().Which.Id.Should().Be(crdtResource.Id); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task WillUploadMultiplePendingLocalFilesAtOnce() | ||
| { | ||
| await SetupLocalFile("file1", "file1"); | ||
| await SetupLocalFile("file2", "file2"); | ||
|
|
||
| //act | ||
| await _resourceService.UploadPendingResources(_localClientId, _remoteServiceMock); | ||
|
|
||
|
|
||
| _remoteServiceMock.ListRemoteFiles() | ||
| .Select(Path.GetFileName) | ||
| .Should() | ||
| .Contain(["file1.txt", "file2.txt"]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanDownloadFileFromRemote() | ||
| { | ||
| var fileContents = "resource"; | ||
| var (resourceId, _) = await SetupRemoteResource(fileContents); | ||
|
|
||
| //act | ||
| var localResource = await _resourceService.DownloadResource(resourceId, _remoteServiceMock); | ||
|
|
||
|
|
||
| localResource.Id.Should().Be(resourceId); | ||
| var actualFileContents = await File.ReadAllTextAsync(localResource.LocalPath); | ||
| actualFileContents.Should().Be(fileContents); | ||
| var pendingDownloads = await _resourceService.ListResourcesPendingDownload(); | ||
| pendingDownloads.Should().BeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanGetALocalResourceGivenAnId() | ||
| { | ||
| var file = CreateFile("resource"); | ||
| //because resource service is null the file is not uploaded | ||
| var crdtResource = await _resourceService.AddLocalResource(file, _localClientId, resourceService: null); | ||
|
|
||
| //act | ||
| var localResource = await _resourceService.GetLocalResource(crdtResource.Id); | ||
|
|
||
|
|
||
| localResource.Should().NotBeNull(); | ||
| localResource!.LocalPath.Should().Be(file); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task LocalResourceIsNullIfNotDownloaded() | ||
| { | ||
| var (resourceId, _) = await SetupRemoteResource("test"); | ||
| var localResource = await _resourceService.GetLocalResource(resourceId); | ||
| localResource.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanListAllResources() | ||
| { | ||
| var (localResourceId, localResourcePath) = await SetupLocalFile("localOnly", "localOnly.txt"); | ||
| var (remoteResourceId, remoteId) = await SetupRemoteResource("remoteOnly"); | ||
| var localAndRemoteResource = await _resourceService.AddLocalResource(CreateFile("localAndRemove"), _localClientId, resourceService: _remoteServiceMock); | ||
|
|
||
| var crdtResources = await _resourceService.AllResources(); | ||
| crdtResources.Should().BeEquivalentTo( | ||
| [ | ||
| new HarmonyResource | ||
| { | ||
| Id = localResourceId, | ||
| LocalPath = localResourcePath, | ||
| RemoteId = null | ||
| }, | ||
| new HarmonyResource | ||
| { | ||
| Id = remoteResourceId, | ||
| LocalPath = null, | ||
| RemoteId = remoteId | ||
| }, | ||
| localAndRemoteResource | ||
| ]); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanGetAResourceGivenAnId() | ||
| { | ||
| var (localResourceId, localResourcePath) = await SetupLocalFile("localOnly", "localOnly.txt"); | ||
| var (remoteResourceId, remoteId) = await SetupRemoteResource("remoteOnly"); | ||
| var localAndRemoteResource = await _resourceService.AddLocalResource(CreateFile("localAndRemove"), | ||
| _localClientId, | ||
| resourceService: _remoteServiceMock); | ||
|
|
||
| (await _resourceService.GetResource(localResourceId)).Should().BeEquivalentTo(new HarmonyResource | ||
| { | ||
| Id = localResourceId, | ||
| LocalPath = localResourcePath, | ||
| RemoteId = null | ||
| }); | ||
| (await _resourceService.GetResource(remoteResourceId)).Should().BeEquivalentTo(new HarmonyResource | ||
| { | ||
| Id = remoteResourceId, | ||
| LocalPath = null, | ||
| RemoteId = remoteId | ||
| }); | ||
| (await _resourceService.GetResource(localAndRemoteResource.Id)).Should().BeEquivalentTo(localAndRemoteResource); | ||
| (await _resourceService.GetResource(Guid.NewGuid())).Should().BeNull(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.