Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d872aad
move MediaUri into a media folder
hahn-kev Jul 21, 2025
d38032d
add a no-op save file implementation
hahn-kev Jul 21, 2025
1306f96
setup typegen
hahn-kev Jul 21, 2025
f2b5a96
ensure blobs are correctly transformed into JSStreamReferences
hahn-kev Jul 21, 2025
a40a59b
change save to not expect a media uri, and type IJsStreamReference
hahn-kev Jul 21, 2025
19829d6
handle saving files locally when using FwData
hahn-kev Jul 21, 2025
c86d7e2
update the local media path cache with newly created files
hahn-kev Jul 21, 2025
166f96b
store recordings as a file with a generated name
hahn-kev Jul 21, 2025
e1932d7
prevent reporting an infinite duration which can crash the browser du…
hahn-kev Jul 21, 2025
05098cd
generate a filename based on the time and guess the extension by mime…
hahn-kev Jul 21, 2025
1b24ab2
save files locally using harmony resources
hahn-kev Jul 21, 2025
d3078ec
show an error for files which are too big
hahn-kev Jul 21, 2025
1a76966
handle file saving and upload to lexbox
hahn-kev Jul 21, 2025
cb45b25
expose a media files gql endpoint
hahn-kev Jul 21, 2025
49b2cd5
attempt to upload pending media files on sync
hahn-kev Jul 22, 2025
ae32663
update harmony
hahn-kev Jul 22, 2025
bcac0b5
fix ambiguity
hahn-kev Jul 22, 2025
0125051
fix crash searching when an entry has a null lexeme form
hahn-kev Jul 22, 2025
195122c
protect against overwriting a file which already exists, delete a cre…
hahn-kev Jul 23, 2025
61e6ef9
correct error in file size
hahn-kev Jul 23, 2025
731b28b
make style consistent
hahn-kev Jul 23, 2025
b868963
handle file not found better in FwData
hahn-kev Jul 23, 2025
dd90c61
write media tests base
hahn-kev Jul 23, 2025
40352b0
implement media tests for Fwdata and harmony, and handle file path is…
hahn-kev Jul 23, 2025
26d2bc4
properly handle duplicate filenames by returning a message that the f…
hahn-kev Jul 23, 2025
79af587
remove unused parameter
hahn-kev Jul 23, 2025
fb5671f
change MediaAdapter to work off full file paths
hahn-kev Jul 23, 2025
3807cb7
record workaround reason
hahn-kev Jul 23, 2025
6fa062b
use `is true` to avoid issues with different return types in the future
hahn-kev Jul 23, 2025
ccd4d39
fix failing test
hahn-kev Jul 23, 2025
3186449
fix lint issue
hahn-kev Jul 23, 2025
149971f
disable warning about custom element props
hahn-kev Jul 23, 2025
107c849
remove unused var
hahn-kev Jul 23, 2025
463b81d
fix test failure due to getting uri before the file existed, ensure m…
hahn-kev Jul 23, 2025
0e4a5eb
create SafeLength extension method for streams as CanSeek does not in…
hahn-kev Jul 23, 2025
de7fa0d
enable custom elements for storybook
hahn-kev Jul 24, 2025
8cdaff0
setup permission manager for android blazor web client
hahn-kev Jul 29, 2025
99df7d8
add origin of permission code
hahn-kev Jul 29, 2025
ca99d72
setup permission requests on windows maui
hahn-kev Jul 29, 2025
afa919d
Merge branch 'develop' into save-audio
hahn-kev Jul 29, 2025
a82cf76
remove unused variable
hahn-kev Jul 29, 2025
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
2 changes: 2 additions & 0 deletions backend/FwHeadless/LexboxFwDataMediaAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using LexCore.Exceptions;
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Media;
using SIL.LCModel;
using MediaFile = LexCore.Entities.MediaFile;

namespace FwHeadless;

Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/FwDataMiniLcmBridge.Tests/MediaFileTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.Media;
using FwDataMiniLcmBridge.Tests.Fixtures;
using MiniLcm.Media;
using MiniLcm.Models;
using SIL.LCModel.Infrastructure;

Expand Down
48 changes: 47 additions & 1 deletion backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Exceptions;
using MiniLcm.Media;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using MiniLcm.Validators;
Expand Down Expand Up @@ -148,7 +149,7 @@
}
}

public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem)

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var type = writingSystem.Type;
var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems;
Expand Down Expand Up @@ -235,7 +236,7 @@
? FromLcmPartOfSpeech(partOfSpeech) : null);
}

public async Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
IPartOfSpeech? lcmPartOfSpeech = null;
if (partOfSpeech.Id == default) partOfSpeech.Id = Guid.NewGuid();
Expand Down Expand Up @@ -437,7 +438,7 @@
return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) };
}

public async Task<ComplexFormType> CreateComplexFormType(ComplexFormType complexFormType)

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (complexFormType.Id == default) complexFormType.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create complex form type",
Expand Down Expand Up @@ -817,7 +818,7 @@
{
if (string.IsNullOrEmpty(query)) return null;
return entry => entry.CitationForm.SearchValue(query) ||
entry.LexemeFormOA.Form.SearchValue(query) ||
entry.LexemeFormOA?.Form.SearchValue(query) == true ||
entry.AllSenses.Any(s => s.Gloss.SearchValue(query));
}

Expand Down Expand Up @@ -1295,7 +1296,7 @@
return Task.FromResult(lcmSense is null ? null : FromLexSense(lcmSense));
}

public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1299 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
Expand Down Expand Up @@ -1440,7 +1441,7 @@
return CmTranslationFactory.Create(parent, freeTranslationType);
}

public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1444 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
Expand Down Expand Up @@ -1540,4 +1541,49 @@
if (!File.Exists(fullPath)) return Task.FromResult(new ReadFileResponse(ReadFileResult.NotFound));
return Task.FromResult(new ReadFileResponse(File.OpenRead(fullPath), Path.GetFileName(fullPath)));
}

public async Task<UploadFileResponse> SaveFile(Stream stream, LcmFileMetadata metadata)
{
var pathRelativeToRoot = Path.Combine(TypeToLinkedFolder(metadata.MimeType), Path.GetFileName(metadata.Filename));
var fullPath = Path.Combine(Cache.LangProject.LinkedFilesRootDir, pathRelativeToRoot);
if (File.Exists(fullPath)) return new UploadFileResponse(UploadFileResult.AlreadyExists);
var directory = Path.GetDirectoryName(fullPath);
if (directory is not null)
{
try
{
Directory.CreateDirectory(directory);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create directory {Directory} for file {Filename}", directory, metadata.Filename);
return new UploadFileResponse($"Failed to create directory: {ex.Message}");
}
}

try
{
await using var fileStream = File.OpenWrite(fullPath);
await stream.CopyToAsync(fileStream);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to save file {Filename} to {Path}", metadata.Filename, fullPath);
return new UploadFileResponse($"Failed to save file: {ex.Message}");
}

var mediaUri = mediaAdapter.MediaUriFromPath(pathRelativeToRoot, Cache);
return new UploadFileResponse(mediaUri, false);
}

private string TypeToLinkedFolder(string mimeType)
{
return mimeType switch
{
{ } s when s.StartsWith("audio/") => AudioVisualFolder,
{} s when s.StartsWith("video/") => AudioVisualFolder,
{ } s when s.StartsWith("image/") => "Pictures",
_ => "Others"
};
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using MiniLcm;
using MiniLcm.Media;
using MiniLcm.Models;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Core.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class LexEntryFilterMapProvider : EntryFilterMapProvider<ILexEntry>
public override Expression<Func<ILexEntry, string, object?>> EntrySensesGloss => (entry, ws) => entry.AllSenses.Select(s => s.PickText(s.Gloss, ws));
public override Expression<Func<ILexEntry, string, object?>> EntrySensesDefinition => (entry, ws) => entry.AllSenses.Select(s => s.PickText(s.Definition, ws));
public override Expression<Func<ILexEntry, string, object?>> EntryNote => (entry, ws) => entry.PickText(entry.Comment, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLexemeForm => (entry, ws) => entry.PickText(entry.LexemeFormOA.Form, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLexemeForm => (entry, ws) =>
entry.LexemeFormOA == null ? null : entry.PickText(entry.LexemeFormOA.Form, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryCitationForm => (entry, ws) => entry.PickText(entry.CitationForm, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLiteralMeaning => (entry, ws) => entry.PickText(entry.LiteralMeaning, ws);
public override Expression<Func<ILexEntry, object?>> EntryComplexFormTypes => e => EmptyToNull(e.ComplexFormEntryRefs.SelectMany(r => r.ComplexEntryTypesRS));
Expand Down
1 change: 1 addition & 0 deletions backend/FwLite/FwDataMiniLcmBridge/Media/IMediaAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MiniLcm;
using MiniLcm.Media;
using SIL.LCModel;

namespace FwDataMiniLcmBridge.Media;
Expand Down
11 changes: 10 additions & 1 deletion backend/FwLite/FwDataMiniLcmBridge/Media/LocalMediaAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Caching.Memory;
using MiniLcm;
using MiniLcm.Exceptions;
using MiniLcm.Media;
using SIL.LCModel;
using UUIDNext;

Expand All @@ -22,14 +23,22 @@ private Dictionary<Guid, string> Paths(LcmCache cache)
return Directory
.EnumerateFiles(cache.LangProject.LinkedFilesRootDir, "*", SearchOption.AllDirectories)
.Select(file => Path.GetRelativePath(cache.LangProject.LinkedFilesRootDir, file))
.ToDictionary(file => MediaUriFromPath(file, cache).FileId, file => file);
.ToDictionary(file => PathToUri(file).FileId, file => file);
}) ?? throw new Exception("Failed to get paths");
}

//path is expected to be relative to the LinkedFilesRootDir
public MediaUri MediaUriFromPath(string path, LcmCache cache)
{
if (!File.Exists(Path.Combine(cache.LangProject.LinkedFilesRootDir, path))) return MediaUri.NotFound;
var uri = PathToUri(path);
//this may be a new file, so we need to add it to the cache
Paths(cache)[uri.FileId] = path;
return uri;
}

private static MediaUri PathToUri(string path)
{
return new MediaUri(NewGuidV5(path), LocalMediaAuthority);
}

Expand Down
21 changes: 21 additions & 0 deletions backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using FwLiteShared.Sync;
using LcmCrdt;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using MiniLcm;
using MiniLcm.Media;
using MiniLcm.Models;
using MiniLcm.Validators;
using Reinforced.Typings.Attributes;
Expand All @@ -12,6 +14,7 @@ public class MiniLcmJsInvokable(
IMiniLcmApi api,
BackgroundSyncService backgroundSyncService,
IProjectIdentifier project,
ILogger<MiniLcmJsInvokable> logger,
MiniLcmApiNotifyWrapperFactory notificationWrapperFactory,
MiniLcmApiValidationWrapperFactory validationWrapperFactory) : IDisposable
{
Expand Down Expand Up @@ -343,6 +346,24 @@ public record ReadFileResponseJs(
string? FileName,
ReadFileResult Result,
string? ErrorMessage);
public const int TenMbFileLimit = 10 * 1024 * 1024;

[JSInvokable]
public async Task<UploadFileResponse> SaveFile(IJSStreamReference streamReference, LcmFileMetadata metadata)
{
if (streamReference.Length > TenMbFileLimit) return new(UploadFileResult.TooBig);
await using var stream = await streamReference.OpenReadStreamAsync(TenMbFileLimit);
var result = await _wrappedApi.SaveFile(stream, metadata);
try
{
await streamReference.DisposeAsync();
}
catch (Exception e)
{
logger.LogError(e, "Error disposing stream reference");
}
return result;
}

public void Dispose()
{
Expand Down
16 changes: 16 additions & 0 deletions backend/FwLite/FwLiteShared/Sync/SyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FwLiteShared.Projects;
using LexCore.Sync;
using LcmCrdt;
using LcmCrdt.MediaServer;
using LcmCrdt.RemoteSync;
using LcmCrdt.Utils;
using Microsoft.EntityFrameworkCore;
Expand All @@ -26,6 +27,7 @@ public class SyncService(
ProjectEventBus changeEventBus,
LexboxProjectService lexboxProjectService,
IMiniLcmApi lexboxApi,
LcmMediaService lcmMediaService,
IOptions<AuthConfig> authOptions,
ILogger<SyncService> logger,
IDbContextFactory<LcmCrdtDbContext> dbContextFactory)
Expand Down Expand Up @@ -80,6 +82,8 @@ public async Task<SyncResults> ExecuteSync(bool skipNotifications = false)
UpdateSyncStatus(SyncStatus.Offline);
return new SyncResults([], [], false);
}

await UploadPendingMedia();
var syncDate = DateTimeOffset.UtcNow;//create sync date first to ensure it's consistent and not based on how long it takes to sync
var syncResults = await dataModel.SyncWith(remoteModel);
if (!syncResults.IsSynced)
Expand Down Expand Up @@ -139,6 +143,18 @@ public async Task<ProjectSyncStatus> GetSyncStatus()
return new PendingCommits(localChanges.Value, remoteChanges);
}

public async Task UploadPendingMedia()
{
try
{
await lcmMediaService.UploadPendingResources();
}
catch (Exception e)
{
logger.LogError(e, "Failed to upload pending media");
}
}

private void UpdateSyncStatus(SyncStatus status)
{
changeEventBus.PublishEvent(currentProjectService.Project, new SyncEvent(status));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
using System.Runtime.CompilerServices;
using FwLiteShared.AppUpdate;
using FwLiteShared.Sync;
using MiniLcm.Media;
using MediaFile = MiniLcm.Media.MediaFile;

namespace FwLiteShared.TypeGen;

Expand Down Expand Up @@ -53,6 +55,7 @@ public static void Configure(ConfigurationBuilder builder)
exportBuilder => exportBuilder.WithName("DotNet.DotNetObject").Imports([
new() { From = "@microsoft/dotnet-js-interop", Target = "type {DotNet}" }
]));
builder.Substitute(typeof(IJSStreamReference), new RtSimpleTypeName("Blob | ArrayBuffer | Uint8Array"));
builder.Substitute(typeof(DotNetStreamReference), new RtSimpleTypeName("{stream: () => Promise<ReadableStream>, arrayBuffer: () => Promise<ArrayBuffer>}"));
builder.ExportAsInterface<IAsyncDisposable>();

Expand All @@ -63,6 +66,7 @@ public static void Configure(ConfigurationBuilder builder)
private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
{
builder.Substitute(typeof(WritingSystemId), new RtSimpleTypeName("string"));
builder.Substitute(typeof(MediaUri), new RtSimpleTypeName("string"));
builder.ExportAsThirdParty<MultiString>().WithName("IMultiString").Imports([
new() { From = "$lib/dotnet-types/i-multi-string", Target = "type {IMultiString}" }
]);
Expand All @@ -78,6 +82,9 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
typeof(IObjectWithId),
typeof(RichString),
typeof(RichTextObjectData),

typeof(MediaFile),
typeof(LcmFileMetadata)
],
exportBuilder => exportBuilder.WithPublicNonStaticProperties(exportBuilder =>
{
Expand All @@ -100,6 +107,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
]);
builder.ExportAsEnum<WritingSystemType>();
builder.ExportAsEnum<ReadFileResult>().UseString();
builder.ExportAsEnum<UploadFileResult>().UseString();
builder.ExportAsInterface<MiniLcmJsInvokable>()
.FlattenHierarchy()
.WithPublicProperties()
Expand All @@ -111,7 +119,8 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
typeof(SortOptions),
typeof(ExemplarOptions),
typeof(EntryFilter),
typeof(MiniLcmJsInvokable.ReadFileResponseJs)
typeof(MiniLcmJsInvokable.ReadFileResponseJs),
typeof(UploadFileResponse)
],
exportBuilder => exportBuilder.WithPublicNonStaticProperties(propExportBuilder =>
{
Expand Down
15 changes: 15 additions & 0 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using MiniLcm.SyncHelpers;
using SIL.Harmony.Core;
using MiniLcm.Culture;
using MiniLcm.Media;
using SystemTextJsonPatch;

namespace LcmCrdt;
Expand Down Expand Up @@ -699,6 +700,20 @@ public async Task<ReadFileResponse> GetFileStream(MediaUri mediaUri)
return await lcmMediaService.GetFileStream(mediaUri.FileId);
}

public async Task<UploadFileResponse> SaveFile(Stream stream, LcmFileMetadata metadata)
{
try
{
var result = await lcmMediaService.SaveFile(stream, metadata);
return new UploadFileResponse(new MediaUri(result.Id, ProjectData.ServerId ?? "lexbox.org"), result.Remote);
}
catch (Exception e)
{
logger.LogError(e, "Failed to save file {Filename}", metadata.Filename);
return new UploadFileResponse(e.Message);
}
}

public void Dispose()
{
}
Expand Down
10 changes: 10 additions & 0 deletions backend/FwLite/LcmCrdt/MediaServer/IMediaServerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,14 @@ public interface IMediaServerClient
{
[Get("/api/media/{fileId}")]
Task<HttpResponseMessage> DownloadFile(Guid fileId);

[Post("/api/media")]
[Multipart]
Task<MediaUploadFileResponse> UploadFile(MultipartItem file,
[Query] Guid projectId,
string fileId,//using a string because Refit doesn't handle a Guid properly
string? author = null,
string? filename = null);
}

public record MediaUploadFileResponse(Guid Guid);
Loading
Loading