Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion LexBox.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncReverseProxy", "backend\SyncReverseProxy\SyncReverseProxy.csproj", "{5C589976-854A-4CE8-A661-A2E256AC2FC4}"
EndProject
Expand Down Expand Up @@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwLiteMaui.Tests", "backend
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FwLiteShared.Tests", "backend\FwLite\FwLiteShared.Tests\FwLiteShared.Tests.csproj", "{ED3FE7AD-323D-45C5-ABC9-2E2CFDDC4A3D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SIL.Harmony.Linq2db", "backend\harmony\src\SIL.Harmony.Linq2db\SIL.Harmony.Linq2db.csproj", "{295A3192-EE14-406D-95B8-B29723A787D0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -169,6 +171,10 @@ Global
{ED3FE7AD-323D-45C5-ABC9-2E2CFDDC4A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED3FE7AD-323D-45C5-ABC9-2E2CFDDC4A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED3FE7AD-323D-45C5-ABC9-2E2CFDDC4A3D}.Release|Any CPU.Build.0 = Release|Any CPU
{295A3192-EE14-406D-95B8-B29723A787D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{295A3192-EE14-406D-95B8-B29723A787D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{295A3192-EE14-406D-95B8-B29723A787D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{295A3192-EE14-406D-95B8-B29723A787D0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -192,6 +198,7 @@ Global
{73DC604C-C501-410D-B56B-0544AD6EF1C2} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090}
{5A277FCE-454F-4956-8C33-7D9726C4E409} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090}
{ED3FE7AD-323D-45C5-ABC9-2E2CFDDC4A3D} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090}
{295A3192-EE14-406D-95B8-B29723A787D0} = {7B6E21C4-5AF4-4505-B7D9-59A3886C5090}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {440AE83C-6DB0-4F18-B2C1-BCD33F0645B6}
Expand Down
14 changes: 2 additions & 12 deletions backend/FwHeadless/Controllers/MediaFileController.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using FwHeadless.Models;
using LexCore.Entities;
using LexData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using Microsoft.Net.Http.Headers;
using System.Globalization;
using FwHeadless.Media;
using LexCore.Exceptions;
using Microsoft.AspNetCore.Mvc;
using MimeMapping;

namespace FwHeadless.Controllers;
Expand Down Expand Up @@ -94,7 +93,6 @@ public static async Task<Results<Ok<PostFileResult>, Created<PostFileResult>, No
return result;
}

[HttpPost]
public static async Task<Results<Ok<PostFileResult>, Created<PostFileResult>, NotFound, BadRequest<FileUploadErrorMessage>, ProblemHttpResult>> PostFile(
[FromQuery] Guid projectId,
[FromForm] Guid? fileId,
Expand Down Expand Up @@ -154,7 +152,7 @@ public static async
// Add ETag to the POST results so uploaders could, in theory, save it and use it later in a GET operation
var entityTag = mediaFile.Metadata!.Sha256Hash;
httpContext.Response.Headers.ETag = $"\"{entityTag}\"";
var responseBody = new PostFileResult(mediaFile.Id);
var responseBody = new PostFileResult(mediaFile.Id, mediaFile.Metadata);
if (newFile)
{
var newLocation = $"{Routes.MediaFileRoutes.RootRoute}/{fileId}";
Expand All @@ -179,14 +177,6 @@ public static async
return null;
}

private static async Task AddEntityTagMetadata(MediaFile mediaFile, string filePath)
{
mediaFile.InitializeMetadataIfNeeded(filePath);
await using var stream = File.OpenRead(filePath);
var hash = await SHA256.HashDataAsync(stream);
mediaFile.Metadata.Sha256Hash = Convert.ToHexStringLower(hash);
}

private static async Task<bool> AddEntityTagMetadataIfNotPresent(MediaFile mediaFile, string filePath)
{
if (mediaFile.Metadata?.Sha256Hash is null)
Expand Down
18 changes: 14 additions & 4 deletions backend/FwHeadless/Media/MediaFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Options;
using MiniLcm.Media;
using SIL.LCModel;
using FileMetadata = LexCore.Entities.FileMetadata;
using MediaFile = LexCore.Entities.MediaFile;

namespace FwHeadless.Media;
Expand Down Expand Up @@ -45,7 +46,7 @@ public virtual async Task<MediaFileSyncResult> SyncMediaFiles(LcmCache cache)
Metadata = new FileMetadata
{
MimeType = MimeMapping.MimeUtility.GetMimeMapping(newFwFile),
SizeInBytes = (int)new FileInfo(Path.Join(cache.ProjectId.ProjectFolder, newFwFile)).Length,
SizeInBytes = new FileInfo(Path.Join(cache.ProjectId.ProjectFolder, newFwFile)).Length,
}
};
dbContext.Files.Add(mediaFile);
Expand Down Expand Up @@ -112,19 +113,28 @@ public virtual async Task SyncMediaFiles(Guid projectId, LcmMediaService lcmMedi
var existingDbFiles = dbContext.Files.Where(p => p.ProjectId == projectId).AsAsyncEnumerable();
await foreach (var existingDbFile in existingDbFiles)
{
if (lcmResources.Remove(existingDbFile.Id))
if (lcmResources.Remove(existingDbFile.Id, out var lcmResource))
{
//the file was already tracked in harmony, but the metadata is missing, so add it
if (lcmResource.Metadata is null)
await lcmMediaService.AddMissingMetadata(lcmResource, ToLcmFileMetadata(existingDbFile));
//nothing to do, the file was already tracked in harmony
continue;
}
await lcmMediaService.AddExistingRemoteResource(existingDbFile.Id, FilePath(existingDbFile));

await lcmMediaService.AddExistingRemoteResource(existingDbFile.Id, FilePath(existingDbFile), ToLcmFileMetadata(existingDbFile));
}
foreach (var lcmResource in lcmResources.Values)
{
await lcmMediaService.DeleteResource(lcmResource.Id);
}
}

private static LcmFileMetadata ToLcmFileMetadata(MediaFile existingDbFile)
{
return new LcmFileMetadata(existingDbFile.Filename, existingDbFile.Metadata?.MimeType ?? "application/octet-stream", existingDbFile.Metadata?.Author, existingDbFile.Metadata?.UploadDate, existingDbFile.Metadata?.SizeInBytes);
}

public async Task SaveMediaFile(MediaFile mediaFile, Stream fileStream)
{
if ((fileStream.SafeLength() ?? 0) > config.Value.MaxUploadFileSizeBytes)
Expand Down Expand Up @@ -167,7 +177,7 @@ public async Task SaveMediaFile(MediaFile mediaFile, Stream fileStream)
await sendReceiveService.CommitFile(filePath, $"Uploaded file {Path.GetFileName(filePath)}");

mediaFile.InitializeMetadataIfNeeded(filePath);
mediaFile.Metadata.SizeInBytes = (int)fileLength;
mediaFile.Metadata.SizeInBytes = fileLength;
mediaFile.Metadata.Sha256Hash = await Sha256OfFile(filePath);

mediaFile.UpdateUpdatedDate();
Expand Down
3 changes: 2 additions & 1 deletion backend/FwHeadless/Models/MediaFileModels.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System.Text.Json.Serialization;
using LexCore.Entities;

namespace FwHeadless.Models;

public record PostFileResult(Guid guid);
public record PostFileResult(Guid guid, FileMetadata? metadata);

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum FileUploadErrorMessage
Expand Down
2 changes: 2 additions & 0 deletions backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ IServiceProvider services
DotnetService.ProjectServicesProvider => typeof(ProjectServicesProvider),
DotnetService.HistoryService => typeof(HistoryServiceJsInvokable),
DotnetService.SyncService => typeof(SyncServiceJsInvokable),
DotnetService.MediaFilesService => typeof(MediaFilesServiceJsInvokable),
DotnetService.AppLauncher => typeof(IAppLauncher),
DotnetService.TroubleshootingService => typeof(ITroubleshootingService),
DotnetService.TestingService => typeof(TestingService),
Expand Down Expand Up @@ -109,6 +110,7 @@ public enum DotnetService
ProjectServicesProvider,
HistoryService,
SyncService,
MediaFilesService,
AppLauncher,
TroubleshootingService,
TestingService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.JSInterop;
using LcmCrdt.MediaServer;
using SIL.Harmony.Resource;
using MiniLcm.Media;

namespace FwLiteShared.Services;

public class MediaFilesServiceJsInvokable(LcmMediaService mediaService)
{
[JSInvokable]
public async Task<HarmonyResource<LcmFileMetadata>[]> AllResources()
{
return await mediaService.AllResources();
}

[JSInvokable]
public async Task DownloadResources(IEnumerable<Guid> resourceIds)
{
await mediaService.DownloadResources(resourceIds);
}

[JSInvokable]
public async Task UploadPendingResources()
{
await mediaService.UploadPendingResources();
}

[JSInvokable]
public async Task<LcmFileMetadata> GetFileMetadata(Guid fileId)
{
return await mediaService.GetFileMetadata(fileId);
}

[JSInvokable]
public async Task<MiniLcmJsInvokable.ReadFileResponseJs> GetFileStream(Guid fileId)
{
var result = await mediaService.GetFileStream(fileId);
var stream = result.Stream is null ? null : new DotNetStreamReference(result.Stream);
return new MiniLcmJsInvokable.ReadFileResponseJs(stream, result.FileName, result.Result, result.ErrorMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ public Task<ProjectScope> OpenCrdtProject(string code)
scope.Server = server;
scope.SetCrdtServices(
ActivatorUtilities.CreateInstance<HistoryServiceJsInvokable>(scopedServices),
ActivatorUtilities.CreateInstance<SyncServiceJsInvokable>(scopedServices)
ActivatorUtilities.CreateInstance<SyncServiceJsInvokable>(scopedServices),
ActivatorUtilities.CreateInstance<MediaFilesServiceJsInvokable>(scopedServices)
);
_projectScopes.TryAdd(scope, scope);
return scope;
Expand Down Expand Up @@ -166,10 +167,12 @@ public ProjectScope(AsyncServiceScope serviceScope,

public void SetCrdtServices(
HistoryServiceJsInvokable historyService,
SyncServiceJsInvokable syncService)
SyncServiceJsInvokable syncService,
MediaFilesServiceJsInvokable mediaFilesService)
{
HistoryService = DotNetObjectReference.Create(historyService);
SyncService = DotNetObjectReference.Create(syncService);
MediaFilesService = DotNetObjectReference.Create(mediaFilesService);
}

public ValueTask CleanupAsync()
Expand All @@ -184,4 +187,5 @@ public ValueTask CleanupAsync()
public DotNetObjectReference<MiniLcmJsInvokable> MiniLcm { get; set; }
public DotNetObjectReference<HistoryServiceJsInvokable>? HistoryService { get; set; }
public DotNetObjectReference<SyncServiceJsInvokable>? SyncService { get; set; }
public DotNetObjectReference<MediaFilesServiceJsInvokable>? MediaFilesService { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using MediaFile = MiniLcm.Media.MediaFile;
using Microsoft.Extensions.Logging;
using SIL.Harmony.Changes;
using SIL.Harmony.Resource;

namespace FwLiteShared.TypeGen;

Expand Down Expand Up @@ -182,6 +183,7 @@ private static void ConfigureFwLiteSharedTypes(ConfigurationBuilder builder)
typeof(ProjectScope),
typeof(FwLiteRelease),
typeof(AvailableUpdate),
typeof(HarmonyResource<LcmFileMetadata>),
], exportBuilder => exportBuilder.WithPublicProperties());

builder.ExportAsEnum<FwEventType>().UseString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,12 +632,37 @@
{
"$type": "create:remote-resource",
"RemoteId": "Buckinghamshire",
"Metadata": null,
"EntityId": "188a57e7-7d61-8742-59eb-c0bfbf959737"
},
{
"$type": "create:remote-resource",
"RemoteId": "CSS",
"Metadata": {
"Filename": "e-enable",
"MimeType": "copying",
"Author": "website",
"UploadDate": "2026-06-16T21:24:55.5941247+07:00",
"SizeInBytes": 4141568263519832022
},
"EntityId": "0cba554e-f8d4-a6cc-ea43-a2a81d5212e3"
},
{
"$type": "create:pendingUpload",
"Metadata": null,
"EntityId": "52d7432c-ad4f-430c-8304-925603b3fefa"
},
{
"$type": "create:pendingUpload",
"Metadata": {
"Filename": "Fundamental",
"MimeType": "impactful",
"Author": "Bolivia",
"UploadDate": "2026-06-17T11:45:20.8253427+07:00",
"SizeInBytes": 5905491568126813952
},
"EntityId": "a4c3d3e4-2cdf-b2c4-bcf7-e6d3aa3172f2"
},
{
"$type": "delete:RemoteResource",
"EntityId": "e14de071-3ed0-b12a-7865-da43fbfe8f84"
Expand Down Expand Up @@ -894,5 +919,16 @@
"MorphType": "Prefix",
"HomographNumber": 4,
"EntityId": "d8005217-eba2-7937-34f9-b76d6543eea6"
},
{
"$type": "set:remote-resource-metadata",
"Metadata": {
"Filename": "Dynamic",
"MimeType": "Technician",
"Author": "Checking Account",
"UploadDate": "2026-06-17T12:54:00.0315148+07:00",
"SizeInBytes": -7233844666964157622
},
"EntityId": "71c0c643-4ef6-4e3d-c657-919bbcb9ecfa"
}
]
14 changes: 11 additions & 3 deletions backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using LcmCrdt.Changes.CustomJsonPatches;
using LcmCrdt.Changes.Entries;
using LcmCrdt.Changes.ExampleSentences;
using MiniLcm.Media;
using MiniLcm.SyncHelpers;
using SIL.Harmony.Changes;
using SIL.Harmony.Resource;
Expand Down Expand Up @@ -263,12 +264,19 @@ private static IEnumerable<ChangeWithDependencies> GetAllChanges()
var removePublicationChange = new RemovePublicationChange(entry.Id, publication2.Id);
yield return new ChangeWithDependencies(removePublicationChange, [replacePublicationChange]);

yield return new ChangeWithDependencies(new CreateRemoteResourceChange(Guid.NewGuid(), "test-remote-id"));
var createRemoteResourcePendingUploadChange = new CreateRemoteResourcePendingUploadChange(Guid.NewGuid());
yield return new ChangeWithDependencies(new CreateRemoteResourceChange<LcmFileMetadata>(Guid.NewGuid(), "test-remote-id"));
var createRemoteResourcePendingUploadChange = new CreateRemoteResourcePendingUploadChange<LcmFileMetadata>(Guid.NewGuid());
yield return new ChangeWithDependencies(createRemoteResourcePendingUploadChange);
yield return new ChangeWithDependencies(
new RemoteResourceUploadedChange(createRemoteResourcePendingUploadChange.EntityId, "test-remote-id"),
new RemoteResourceUploadedChange<LcmFileMetadata>(createRemoteResourcePendingUploadChange.EntityId, "test-remote-id"),
[createRemoteResourcePendingUploadChange]);
yield return new ChangeWithDependencies(
new SetRemoteResourceMetadataChange<LcmFileMetadata>(createRemoteResourcePendingUploadChange.EntityId, new LcmFileMetadata("test.txt", "text/plain")),
[createRemoteResourcePendingUploadChange]);

var createRemoteResourceChange = new CreateRemoteResourceChange<LcmFileMetadata>(createRemoteResourcePendingUploadChange.EntityId, "test-remote-id2");
yield return new ChangeWithDependencies(createRemoteResourceChange);
yield return new ChangeWithDependencies(new DeleteRemoteResourceChange<LcmFileMetadata>(createRemoteResourceChange.EntityId), [createRemoteResourceChange]);

var customView = new CustomView
{
Expand Down
4 changes: 3 additions & 1 deletion backend/FwLite/LcmCrdt.Tests/ConfigRegistrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FluentAssertions.Execution;
using LcmCrdt.Changes;
using LcmCrdt.Changes.Entries;
using MiniLcm.Media;
using SIL.Harmony.Changes;
using SIL.Harmony.Resource;

Expand All @@ -14,10 +15,11 @@ public class ConfigRegistrationTests
[
typeof(ReplaceComplexFormTypeChange), //not currently in use
typeof(JsonPatchChange<ComplexFormComponent>), //not supported
typeof(JsonPatchChange<RemoteResource>), //not supported
typeof(JsonPatchChange<RemoteResource<LcmFileMetadata>>), //not supported
typeof(JsonPatchChange<ExampleSentence>), //replaced by JsonPatchExampleSentenceChange
typeof(JsonPatchChange<CustomView>), //not supported. Use EditCustomViewChange
typeof(DeleteChange<MorphType>), //MorphTypes cannot be deleted
typeof(DeleteChange<RemoteResource<LcmFileMetadata>>)//Not used, instead DeleteRemoteResourceChange is used
];

private readonly CrdtConfig _config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1611,7 +1611,21 @@
"$type": "RemoteResource",
"Id": "fce4cb31-93bd-f380-fd73-3fb0d09c0fa5",
"DeletedAt": "2025-09-17T23:53:03.6147633+02:00",
"RemoteId": "end-to-end"
"RemoteId": "end-to-end",
"Metadata": null
},
{
"$type": "RemoteResource",
"Id": "c7b9ae71-c594-7f5f-e28b-f62a6b347dc3",
"DeletedAt": "2026-06-16T17:24:01.4577797+07:00",
"RemoteId": "Idaho",
"Metadata": {
"Filename": "hacking",
"MimeType": "programming",
"Author": "Auto Loan Account",
"UploadDate": "2026-06-16T18:13:32.6197982+07:00",
"SizeInBytes": -6811753233150785094
}
},
{
"$type": "MiniLcmCrdtAdapter",
Expand Down
Loading
Loading