diff --git a/src/Serval/src/Serval.DataFiles/Features/DataFiles/CreateDataFile.cs b/src/Serval/src/Serval.DataFiles/Features/DataFiles/CreateDataFile.cs index ced9483df..a9741ca24 100644 --- a/src/Serval/src/Serval.DataFiles/Features/DataFiles/CreateDataFile.cs +++ b/src/Serval/src/Serval.DataFiles/Features/DataFiles/CreateDataFile.cs @@ -27,8 +27,15 @@ public async Task HandleAsync(CreateDataFile request, Ca }; try { - await using Stream fileStream = fileSystem.OpenWrite(path); - await request.FileStream.CopyToAsync(fileStream, cancellationToken); + using (Stream fileStream = fileSystem.OpenWrite(path)) + { + await request.FileStream.CopyToAsync(fileStream, cancellationToken); + } + if (dataFile.Format == FileFormat.Paratext) + { + ParatextMetadata metadata = await ParatextProjectDataParser.ParseParatextMetadataAsync(path); + dataFile = dataFile with { FileMetadata = metadata }; + } await dataFiles.InsertAsync(dataFile, cancellationToken); } catch diff --git a/src/Serval/src/Serval.DataFiles/Features/DataFiles/UpdateDataFile.cs b/src/Serval/src/Serval.DataFiles/Features/DataFiles/UpdateDataFile.cs index b28e0cb16..b076e8587 100644 --- a/src/Serval/src/Serval.DataFiles/Features/DataFiles/UpdateDataFile.cs +++ b/src/Serval/src/Serval.DataFiles/Features/DataFiles/UpdateDataFile.cs @@ -36,6 +36,16 @@ await dataAccessContext.WithTransactionAsync( ); if (originalDataFile is null) throw new EntityNotFoundException($"Could not find the DataFile '{request.FileId}'."); + if (originalDataFile.Format == FileFormat.Paratext) + { + ParatextMetadata metadata = await ParatextProjectDataParser.ParseParatextMetadataAsync(path); + await dataFiles.UpdateAsync( + request.FileId, + u => u.Set(f => f.FileMetadata, metadata), + cancellationToken: ct + ); + } + await deletedFiles.InsertAsync( new DeletedFile { Filename = originalDataFile.Filename, DeletedAt = DateTime.UtcNow }, ct diff --git a/src/Serval/src/Serval.DataFiles/Models/DataFile.cs b/src/Serval/src/Serval.DataFiles/Models/DataFile.cs index 32a660711..b9549daeb 100644 --- a/src/Serval/src/Serval.DataFiles/Models/DataFile.cs +++ b/src/Serval/src/Serval.DataFiles/Models/DataFile.cs @@ -8,4 +8,5 @@ public record DataFile : IOwnedEntity public required string Name { get; init; } public string Filename { get; init; } = ""; public required FileFormat Format { get; init; } + public ParatextMetadata? FileMetadata { get; init; } } diff --git a/src/Serval/src/Serval.DataFiles/Models/ParatextMetadata.cs b/src/Serval/src/Serval.DataFiles/Models/ParatextMetadata.cs new file mode 100644 index 000000000..d7cf93259 --- /dev/null +++ b/src/Serval/src/Serval.DataFiles/Models/ParatextMetadata.cs @@ -0,0 +1,12 @@ +namespace Serval.DataFiles.Models; + +public record ParatextMetadata +{ + public required string ProjectGuid { get; init; } + public required string Name { get; init; } + public required string FullName { get; init; } + public required string Versification { get; init; } + public required string TranslationType { get; init; } + public string? LanguageCode { get; init; } + public string? Visibility { get; init; } +} diff --git a/src/Serval/src/Serval.DataFiles/Services/ParatextProjectDataParser.cs b/src/Serval/src/Serval.DataFiles/Services/ParatextProjectDataParser.cs new file mode 100644 index 000000000..2e82efa98 --- /dev/null +++ b/src/Serval/src/Serval.DataFiles/Services/ParatextProjectDataParser.cs @@ -0,0 +1,32 @@ +namespace Serval.DataFiles.Services; + +public class ParatextProjectDataParser +{ + public static async Task ParseParatextMetadataAsync(string path) + { + using ZipContainer zipContainer = new(path); + try + { + ParatextProjectSettings projectSettings = new Shared.Services.ZipParatextProjectSettingsParser( + zipContainer + ).Parse(); + return new ParatextMetadata + { + ProjectGuid = projectSettings.Guid, + Name = projectSettings.Name, + FullName = projectSettings.FullName, + Versification = projectSettings.Versification.Name, + TranslationType = projectSettings.TranslationType, + LanguageCode = projectSettings.LanguageCode, + Visibility = projectSettings.Visibility, + }; + } + catch (Exception e) when (e is not OperationCanceledException) + { + throw new InvalidOperationException( + "Unable to parse the Paratext project settings for the uploaded data file.", + e + ); + } + } +} diff --git a/src/Serval/src/Serval.DataFiles/Usings.cs b/src/Serval/src/Serval.DataFiles/Usings.cs index 291d07e8f..6c8ba62aa 100644 --- a/src/Serval/src/Serval.DataFiles/Usings.cs +++ b/src/Serval/src/Serval.DataFiles/Usings.cs @@ -26,3 +26,4 @@ global using Serval.Shared.Services; global using Serval.Shared.Utils; global using SIL.DataAccess; +global using SIL.Machine.Corpora; diff --git a/src/Serval/test/Serval.DataFiles.Tests/Features/DataFiles/DataFilesHandlersTests.cs b/src/Serval/test/Serval.DataFiles.Tests/Features/DataFiles/DataFilesHandlersTests.cs index f35c63c44..b4adc8873 100644 --- a/src/Serval/test/Serval.DataFiles.Tests/Features/DataFiles/DataFilesHandlersTests.cs +++ b/src/Serval/test/Serval.DataFiles.Tests/Features/DataFiles/DataFilesHandlersTests.cs @@ -90,6 +90,35 @@ public async Task CreateDataFile_Error() env.FileSystem.Received().DeleteFile(Arg.Any()); } + [Test] + public async Task CreateDataFile_Paratext() + { + var env = new TestEnvironment(); + env.FileSystem.OpenWrite(Arg.Any()) + .Returns(callInfo => new FileStream(callInfo.Arg(), FileMode.Create, FileAccess.Write)); + string paratextZipPath = ZipParatextProject(); + CreateDataFileHandler handler = new(env.DataFiles, env.IdGenerator, env.Options, env.FileSystem, env.Mapper); + using FileStream stream = File.OpenRead(paratextZipPath); + CreateDataFileResponse response = await handler.HandleAsync( + new(Owner, "file1", "file1.txt", FileFormat.Paratext, stream), + CancellationToken.None + ); + DataFile? dataFile = await env.DataFiles.GetAsync(response.DataFile.Id, CancellationToken.None); + Assert.That(dataFile, Is.Not.Null); + Assert.That(dataFile.FileMetadata, Is.Not.Null); + ParatextMetadata metadata = dataFile.FileMetadata; + using (Assert.EnterMultipleScope()) + { + Assert.That(metadata.ProjectGuid, Is.EqualTo("a7e0b3ce0200736062f9f810a444dbfbe64aca35")); + Assert.That(metadata.Name, Is.EqualTo("Te1")); + Assert.That(metadata.FullName, Is.EqualTo("Test1")); + Assert.That(metadata.TranslationType, Is.EqualTo("Standard")); + Assert.That(metadata.Versification, Does.StartWith("English")); + Assert.That(metadata.LanguageCode, Is.EqualTo("en")); + Assert.That(metadata.Visibility, Is.EqualTo("Public")); + } + } + [Test] public async Task DownloadDataFile_FileExists() { @@ -273,4 +302,13 @@ public async Task CreateDataFileAsync(string id = "df00000000000000000 return file; } } + + private static string ZipParatextProject() + { + string path = Path.Combine(Path.GetTempPath(), "pt-project.zip"); + if (File.Exists(path)) + File.Delete(path); + ZipFile.CreateFromDirectory(Path.Combine("..", "..", "..", "data", "pt-project"), path); + return path; + } } diff --git a/src/Serval/test/Serval.DataFiles.Tests/Usings.cs b/src/Serval/test/Serval.DataFiles.Tests/Usings.cs index 5f5be62c5..7bcc11b34 100644 --- a/src/Serval/test/Serval.DataFiles.Tests/Usings.cs +++ b/src/Serval/test/Serval.DataFiles.Tests/Usings.cs @@ -1,3 +1,4 @@ +global using System.IO.Compression; global using System.Text; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; diff --git a/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/41MATTe1.SFM b/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/41MATTe1.SFM new file mode 100644 index 000000000..8130771c2 --- /dev/null +++ b/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/41MATTe1.SFM @@ -0,0 +1,6 @@ +\id MAT - SRC +\c 1 +\v 1 SRC - Chapter one, verse one. +\p new paragraph +\v 2 +\v 3 SRC - Chapter one, verse three. diff --git a/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/Settings.xml b/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/Settings.xml new file mode 100644 index 000000000..6358f4f0b --- /dev/null +++ b/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/Settings.xml @@ -0,0 +1,34 @@ + + usfm.sty + 4 + en::: + English + 8.0.100.76 + Test1 + 65001 + T + + NFC + Te1 + a7e0b3ce0200736062f9f810a444dbfbe64aca35 + Charis SIL + 12 + + + + 41MAT + + Tes.SFM + Major::BiblicalTerms.xml + F + F + F + Public + Standard:: + + 3 + 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + + + \ No newline at end of file diff --git a/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/custom.vrs b/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/custom.vrs new file mode 100644 index 000000000..9c1cd3873 --- /dev/null +++ b/src/Serval/test/Serval.DataFiles.Tests/data/pt-project/custom.vrs @@ -0,0 +1,31 @@ +# custom.vrs + +LEV 14:56 +ROM 14:26 +REV 12:17 +TOB 5:22 +TOB 10:12 +SIR 23:28 +ESG 1:22 +ESG 3:15 +ESG 5:14 +ESG 8:17 +ESG 10:14 +SIR 33:33 +SIR 41:24 +BAR 1:22 +4MA 7:25 +4MA 12:20 + +# deliberately missing verses +-ROM 16:26 +-ROM 16:27 +-3JN 1:15 +-S3Y 1:49 +-ESG 4:6 +-ESG 9:5 +-ESG 9:30 + +LEV 14:55 = LEV 14:55 +LEV 14:55 = LEV 14:56 +LEV 14:56 = LEV 14:57