From c6e02046f709d24fdaa21f1a7eeaef4d5bf290f5 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Mon, 11 Jul 2022 13:02:38 +0200 Subject: [PATCH 01/22] Add support for PRONOM-analysis within archive files ARKADE-629 --- .../Arkivverket.Arkade.Core.Tests.csproj | 3 + .../Base/Siard/SiardXmlTableReaderTests.cs | 4 +- .../TestData/FileTypes/fileTypes.zip | Bin 0 -> 89899 bytes .../FileFormatIdentifierTests.cs | 40 +++++++-- .../FileFormatScanMode.cs | 1 + .../SiegfriedFileFormatIdentifier.cs | 82 +++++++++++++++--- .../SiegfriedFileInfo.cs | 6 -- .../SiegfriedProcessRunner.cs | 4 +- 8 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 src/Arkivverket.Arkade.Core.Tests/TestData/FileTypes/fileTypes.zip diff --git a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj index 7f9251da0..123fc179b 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj +++ b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj @@ -90,6 +90,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Arkivverket.Arkade.Core.Tests/Base/Siard/SiardXmlTableReaderTests.cs b/src/Arkivverket.Arkade.Core.Tests/Base/Siard/SiardXmlTableReaderTests.cs index ce18e30f0..380408986 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Base/Siard/SiardXmlTableReaderTests.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Base/Siard/SiardXmlTableReaderTests.cs @@ -6,6 +6,7 @@ using Arkivverket.Arkade.Core.Logging; using Arkivverket.Arkade.Core.Util.FileFormatIdentification; using FluentAssertions; +using Moq; namespace Arkivverket.Arkade.Core.Tests.Base.Siard { @@ -15,7 +16,8 @@ public class SiardXmlTableReaderTests public SiardXmlTableReaderTests() { - _fileFormatIdentifier = new SiegfriedFileFormatIdentifier(new SiegfriedProcessRunner(new StatusEventHandler())); + IStatusEventHandler statusEventHandler = new Mock().Object; + _fileFormatIdentifier = new SiegfriedFileFormatIdentifier(new SiegfriedProcessRunner(statusEventHandler), statusEventHandler); } [Fact] diff --git a/src/Arkivverket.Arkade.Core.Tests/TestData/FileTypes/fileTypes.zip b/src/Arkivverket.Arkade.Core.Tests/TestData/FileTypes/fileTypes.zip new file mode 100644 index 0000000000000000000000000000000000000000..a0f7709319770ce8d7afd0d0ab97439de7a9c043 GIT binary patch literal 89899 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

R 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 r.Id.Equals("fmt/276") && + r.Format.Equals("Acrobat PDF 1.7 - Portable Document Format") && + r.Version.Equals("1.7")); + fileFormatResults.Should().Contain(r => r.Id.Equals("fmt/354") && + r.Format.Equals("Acrobat PDF/A - Portable Document Format") && + r.Version.Equals("1b")); + fileFormatResults.Should().Contain(r => r.Id.Equals("fmt/412") && + r.Format.Equals("Microsoft Word for Windows") && + r.Version.Equals("2007 onwards")); + fileFormatResults.Should().Contain(r => r.Id.Equals("fmt/479") && + r.Format.Equals("Acrobat PDF/A - Portable Document Format") && + r.Version.Equals("3a")); + } + private static IFileFormatIdentifier CreateFileFormatIdentifier() { - return new SiegfriedFileFormatIdentifier(new SiegfriedProcessRunner(new StatusEventHandler())); + IStatusEventHandler statusEventHandler = new Mock().Object; + return new SiegfriedFileFormatIdentifier(new SiegfriedProcessRunner(statusEventHandler), statusEventHandler); } } } diff --git a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/FileFormatScanMode.cs b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/FileFormatScanMode.cs index 63906ee30..1a95ceef4 100644 --- a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/FileFormatScanMode.cs +++ b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/FileFormatScanMode.cs @@ -5,5 +5,6 @@ public enum FileFormatScanMode Directory, File, Stream, + Archive, } } diff --git a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileFormatIdentifier.cs b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileFormatIdentifier.cs index 9fe841061..f9c2cfd7e 100644 --- a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileFormatIdentifier.cs +++ b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileFormatIdentifier.cs @@ -6,6 +6,7 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; +using Arkivverket.Arkade.Core.Logging; using Arkivverket.Arkade.Core.Resources; using CsvHelper; using Serilog; @@ -15,29 +16,75 @@ namespace Arkivverket.Arkade.Core.Util.FileFormatIdentification public class SiegfriedFileFormatIdentifier : IFileFormatIdentifier { private static SiegfriedProcessRunner _processRunner; + private readonly IStatusEventHandler _statusEventHandler; - public SiegfriedFileFormatIdentifier(SiegfriedProcessRunner siegfriedProcessRunner) + private readonly List _supportedZipFormatExtension = new() + { + ".zip", ".tar", ".gz", ".arc", ".warc" + }; + + public SiegfriedFileFormatIdentifier(SiegfriedProcessRunner siegfriedProcessRunner, + IStatusEventHandler statusEventHandler) { _processRunner = siegfriedProcessRunner; + _statusEventHandler = statusEventHandler; } public IEnumerable IdentifyFormats(string target, FileFormatScanMode scanMode) { - Process siegfriedProcess = _processRunner.SetupSiegfriedProcess(scanMode, target); - long numberOfFilesToAnalyse = CalculateNumberOfFilesToAnalyse(scanMode, target); + _statusEventHandler.RaiseEventFormatAnalysisStarted(numberOfFilesToAnalyse); + + IEnumerable siegfriedFileInfoObjects = AnalyseFiles(target, scanMode, numberOfFilesToAnalyse); + + _statusEventHandler.RaiseEventFormatAnalysisFinished(); + + return siegfriedFileInfoObjects; + } + + private IEnumerable AnalyseFiles(string target, FileFormatScanMode scanMode, long numberOfFilesToAnalyse) + { + Process siegfriedProcess = _processRunner.SetupSiegfriedProcess(scanMode, target); + IEnumerable siegfriedResult = _processRunner.Run(siegfriedProcess, numberOfFilesToAnalyse); int siegfriedCloseStatus = ExternalProcessManager.Close(siegfriedProcess.Id); - return siegfriedCloseStatus switch + + List siegfriedFileInfoObjects = siegfriedCloseStatus switch { -1 => throw new SiegfriedFileFormatIdentifierException("Process does not exist"), 1 => throw new SiegfriedFileFormatIdentifierException("Process was terminated"), - _ => GetSiegfriedFileInfoObjects(siegfriedResult) + _ => GetSiegfriedFileInfoObjects(siegfriedResult).ToList(), }; + + if (!SiegfriedFileInfoObjectsContainsArchiveFiles(siegfriedFileInfoObjects, scanMode)) + return siegfriedFileInfoObjects; + + List archiveFilePaths = siegfriedFileInfoObjects.Where(s => + _supportedZipFormatExtension.Contains(s.FileExtension)).ToList(); + + IEnumerable>> archiveFormatAnalysisTasks = archiveFilePaths + .Select(f => AnalyseFilesAsync(f.FileName, FileFormatScanMode.Archive, numberOfFilesToAnalyse)); + + siegfriedFileInfoObjects.AddRange(Task.WhenAll(archiveFormatAnalysisTasks).Result.SelectMany(a => a)); + + return siegfriedFileInfoObjects; } - + + private async Task> AnalyseFilesAsync(string target, FileFormatScanMode scanMode, + long numberOfFilesToAnalyse) + { + return await Task.Run(() => AnalyseFiles(target, scanMode, numberOfFilesToAnalyse)); + } + private bool SiegfriedFileInfoObjectsContainsArchiveFiles( + IEnumerable fileFormatInfoObjects, FileFormatScanMode scanMode) + { + return (scanMode == FileFormatScanMode.Archive + ? fileFormatInfoObjects.Skip(1) // Skip first element when .zip (or similar) have been analysed, as this element is the .zip file itself + : fileFormatInfoObjects).Any(f => _supportedZipFormatExtension.Contains(f.FileExtension)); + } + public IEnumerable IdentifyFormats(IEnumerable>> filePathsAndByteContent) { List> fileFormatTasks = new(); @@ -98,9 +145,9 @@ public IFileFormatInfo IdentifyFormat(KeyValuePair> fi } } - private static IEnumerable GetSiegfriedFileInfoObjects(IEnumerable formatInfoSet) + private static IEnumerable GetSiegfriedFileInfoObjects(IEnumerable formatInfoSet) { - return formatInfoSet.Skip(1).Select(GetSiegfriedFileInfoObject); + return formatInfoSet.Skip(1).Where(f => f != null).Select(GetSiegfriedFileInfoObject); } private static SiegfriedFileInfo GetSiegfriedFileInfoObject(string siegfriedFormatResult) @@ -130,16 +177,31 @@ private long CalculateNumberOfFilesToAnalyse(FileFormatScanMode scanMode, string if (scanMode is FileFormatScanMode.Directory) return CalculateNumberOfFilesToAnalyse(new DirectoryInfo(target)); + if (scanMode is FileFormatScanMode.Archive) + return CalculateNumberOfFilesToAnalyse(target); + return 1; } private long CalculateNumberOfFilesToAnalyse(DirectoryInfo directory) { - return directory.EnumerateFiles("*", new EnumerationOptions + IEnumerable allFiles = directory.EnumerateFiles("*", new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = true, - }).LongCount(); + }); + + long numberOfFilesToAnalyse = allFiles.LongCount(); + + return numberOfFilesToAnalyse + allFiles + .Where(f => _supportedZipFormatExtension.Contains(f.Extension)) + .Select(f => CalculateNumberOfFilesToAnalyse(f.FullName)).Sum(); + } + + private long CalculateNumberOfFilesToAnalyse(string pathToArchiveFile) + { + using var zipArchive = new ZipArchive(File.OpenRead(pathToArchiveFile)); + return zipArchive.Entries.LongCount(e => e.Name != string.Empty); } } diff --git a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileInfo.cs b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileInfo.cs index fcf76aeab..de10562b4 100644 --- a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileInfo.cs +++ b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedFileInfo.cs @@ -25,12 +25,6 @@ public SiegfriedFileInfo(string fileName, string errors, string id, string forma MimeType = mimeType; } - public IFileFormatInfo Create(string fileName, string errors, string id, string format, string version, - string mimeType) - { - return new SiegfriedFileInfo(fileName, errors, id, format, version, mimeType); - } - public bool Equals(SiegfriedFileInfo other) { return Id == other?.Id; diff --git a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedProcessRunner.cs b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedProcessRunner.cs index a833766be..f39a2ff01 100644 --- a/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedProcessRunner.cs +++ b/src/Arkivverket.Arkade.Core/Util/FileFormatIdentification/SiegfriedProcessRunner.cs @@ -44,7 +44,6 @@ internal IEnumerable Run(Process process, long numberOfFilesToAnalyse) try { - _statusEventHandler.RaiseEventFormatAnalysisStarted(numberOfFilesToAnalyse); ExternalProcessManager.Start(process); } catch (Exception e) @@ -61,8 +60,6 @@ internal IEnumerable Run(Process process, long numberOfFilesToAnalyse) if (errors.Any()) errors.ForEach(Log.Debug); - _statusEventHandler.RaiseEventFormatAnalysisFinished(); - return results; } @@ -168,6 +165,7 @@ private static string BuildSiegfriedArgument(FileFormatScanMode scanMode, string FileFormatScanMode.Directory => "-multi 256 ", FileFormatScanMode.File => "", FileFormatScanMode.Stream => "-", + FileFormatScanMode.Archive => "-z", _ => throw new SiegfriedFileFormatIdentifierException( $"Siegfried scan mode {{{nameof(scanMode)}}} is not implemented") }; From 90ed0ef0c045c9d3cf64773d0f9171efd0096437 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Mon, 11 Jul 2022 14:30:27 +0200 Subject: [PATCH 02/22] Add Dependency-IO trait to some tests --- .../FileFormatIdentification/FileFormatIdentifierTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/FileFormatIdentification/FileFormatIdentifierTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/FileFormatIdentification/FileFormatIdentifierTests.cs index 744ea7bbe..54419d7ff 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Util/FileFormatIdentification/FileFormatIdentifierTests.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Util/FileFormatIdentification/FileFormatIdentifierTests.cs @@ -12,6 +12,7 @@ namespace Arkivverket.Arkade.Core.Tests.Util.FileFormatIdentification public class SiegfriedFileFormatIdentifierTests { [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifyTest() { var directoryPath = Path.Combine("TestData", "FileTypes"); // PDF, PDF/A-1b, PDF/A-3a, DOCX @@ -58,6 +59,7 @@ public void IdentifyTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifySingleDocxFileTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); @@ -75,6 +77,7 @@ public void IdentifySingleDocxFileTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifyDocxFileFromFileStreamTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); @@ -94,6 +97,7 @@ public void IdentifyDocxFileFromFileStreamTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifySinglePdfAFileTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); @@ -111,6 +115,7 @@ public void IdentifySinglePdfAFileTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifyPdfAFileFromFileStreamTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); @@ -130,6 +135,7 @@ public void IdentifyPdfAFileFromFileStreamTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifySinglePdfFileTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); @@ -147,6 +153,7 @@ public void IdentifySinglePdfFileTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifyPdfFileFromFileStreamTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); @@ -166,6 +173,7 @@ public void IdentifyPdfFileFromFileStreamTest() } [Fact, Trait("Category", "Integration")] + [Trait("Dependency", "IO")] public void IdentifyArchiveFileContentTest() { IFileFormatIdentifier formatIdentifier = CreateFileFormatIdentifier(); From fd43ab5ddb993269a44e64a7ba39f3422c298460 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Wed, 13 Jul 2022 10:19:07 +0200 Subject: [PATCH 03/22] Rewrite SiardValidator to fulfill DI-principle --- .../Testing/Siard/SiardValidatorRunnerTest.cs | 22 ++++--- .../Base/Siard/SiardTestEngine.cs | 35 ++--------- .../Testing/Siard/ISiardValidator.cs | 9 +++ .../Testing/Siard/SiardValidator.cs | 61 ++++++++++++------- .../Util/ArkadeAutofacModule.cs | 2 + src/Arkivverket.Arkade.GUI/App.xaml.cs | 2 + 6 files changed, 72 insertions(+), 59 deletions(-) create mode 100644 src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs diff --git a/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs b/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs index 3496b3daa..b05db6f76 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs @@ -1,16 +1,24 @@ using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Arkivverket.Arkade.Core.Logging; using Arkivverket.Arkade.Core.Testing.Siard; using FluentAssertions; +using Moq; using Xunit; namespace Arkivverket.Arkade.Core.Tests.Testing.Siard { public class SiardValidatorRunnerTest { + private static readonly ISiardValidator Validator; + + static SiardValidatorRunnerTest() + { + Validator = new SiardValidator(new Mock().Object, new Mock().Object); + } + [Fact] [Trait("Category", "Integration")] [Trait("Dependency", "JRE"), Trait("Dependency", "DBPTK")] @@ -19,7 +27,7 @@ public void ShouldGenerateValidationReportFileAtDesignatedDestination() string inputFilePath = Path.Combine("TestData", "Siard", "dbptk_produced.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - _ = SiardValidator.Validate(inputFilePath, reportFilePath); + Validator.Validate(inputFilePath, reportFilePath); File.Exists(reportFilePath).Should().BeTrue(); // clean up generated files @@ -36,7 +44,7 @@ public void ShouldReportUnsupportedSiardVersion() string inputFilePath = Path.Combine("TestData", "Siard", "siard1_med_blobs.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (List results, _) = SiardValidator.Validate(inputFilePath, reportFilePath); + (List results, _) = Validator.Validate(inputFilePath, reportFilePath); results.Count.Should().Be(2); results[0].Should().Be(Resources.SiardMessages.ErrorMessage); @@ -57,7 +65,7 @@ public void ShouldValidateExtractProducedBySiardGui() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "siardGui", "external", "siardGui.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (_, List errorsAndWarnings) = SiardValidator.Validate(inputFilePath, reportFilePath); + (_, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); var errors = new List(errorsAndWarnings.Where(e => e == null || !e.StartsWith("WARN"))); @@ -79,7 +87,7 @@ public void ShouldValidateExtractProducedByDbptkDeveloper() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "dbPtk", "external", "dbptk.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (_, List errorsAndWarnings) = SiardValidator.Validate(inputFilePath, reportFilePath); + (_, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); var errors = new List(errorsAndWarnings.Where(e => e == null || !e.StartsWith("WARN"))); @@ -103,7 +111,7 @@ public void ShouldFailToValidateExtractProducedBySpectralCoreFullConvert() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "fullConvert", "external", "scfc.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (_, List errorsAndWarnings) = SiardValidator.Validate(inputFilePath, reportFilePath); + (_, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); var errors = new List(errorsAndWarnings.Where(e => e == null || !e.StartsWith("WARN"))); @@ -129,7 +137,7 @@ public void ShouldReportWarningsWhenExternalLobsAreMissing() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "externalLobsMissing", "dbptk.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (List results, List errorsAndWarnings) = SiardValidator.Validate(inputFilePath, reportFilePath); + (List results, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); List summary = results.Where(r => r != null && r.Trim().StartsWith("Number of")).ToList(); diff --git a/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs b/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs index d0c1a15ec..617f4d61b 100644 --- a/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs +++ b/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs @@ -3,36 +3,20 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Arkivverket.Arkade.Core.Logging; using Arkivverket.Arkade.Core.Testing.Siard; -using Arkivverket.Arkade.Core.Util; namespace Arkivverket.Arkade.Core.Base.Siard { public class SiardTestEngine : ITestEngine { - private readonly IStatusEventHandler _statusEventHandler; - private readonly ITestProgressReporter _testProgressReporter; + private readonly ISiardValidator _validator; - public SiardTestEngine(IStatusEventHandler statusEventHandler, ITestProgressReporter testProgressReporter) + public SiardTestEngine(ISiardValidator validator) { - _statusEventHandler = statusEventHandler; - _testProgressReporter = testProgressReporter; + _validator = validator; } public TestSuite RunTestsOnArchive(TestSession testSession) - { - _statusEventHandler.RaiseEventOperationMessage(Resources.Messages.ValidatingExtractMessage, null, OperationMessageStatus.Started); - _testProgressReporter.Begin(ArchiveType.Siard); - - bool validationHasCompleted = RunSiardValidation(testSession); - - _testProgressReporter.Finish(!validationHasCompleted); - - return new TestSuite(); - } - - private bool RunSiardValidation(TestSession testSession) { FileInfo siardFileInfo = testSession.Archive.WorkingDirectory.Content().DirectoryInfo().GetFiles() .First(f => f.Extension.Equals(".siard")); @@ -40,12 +24,7 @@ private bool RunSiardValidation(TestSession testSession) string reportFilePath = Path.Combine(testSession.Archive.WorkingDirectory.RepositoryOperations().ToString(), Resources.OutputFileNames.DbptkValidationReportFile); - _statusEventHandler.RaiseEventOperationMessage( - Resources.SiardMessages.ValidationMessageIdentifier, - string.Format(Resources.SiardMessages.ValidationMessage, siardFileInfo.Name), - OperationMessageStatus.Info); - - (List results, List errors) = SiardValidator.Validate(inputFilePath, reportFilePath); + (List results, List errors) = _validator.Validate(inputFilePath, reportFilePath); List summary = results.Where(r => r != null && r.Contains("number of", StringComparison.InvariantCultureIgnoreCase)).ToList(); @@ -65,11 +44,7 @@ private bool RunSiardValidation(TestSession testSession) testSession.TestSummary = new TestSummary(0, 0, 0, numberOfValidationErrors, numberOfValidationWarnings); - _statusEventHandler.RaiseEventSiardValidationFinished(errors); - - bool validationRanWithoutRunErrors = errors.All(e => e == null || e.StartsWith("WARN")); - - return validationRanWithoutRunErrors; + return new TestSuite(); } private int GetNumberOfXFromSummary(string x, List summary) diff --git a/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs b/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs new file mode 100644 index 000000000..b4a7c6a1f --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Arkivverket.Arkade.Core.Testing.Siard +{ + public interface ISiardValidator + { + (List, List) Validate(string inputFilePath, string reportFilePath); + } +} diff --git a/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs b/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs index 0827e3589..ab0ef8e21 100644 --- a/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs +++ b/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs @@ -5,45 +5,56 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text; using Arkivverket.Arkade.Core.Base; +using Arkivverket.Arkade.Core.Logging; using Arkivverket.Arkade.Core.Resources; using Arkivverket.Arkade.Core.Util; using Serilog; -[assembly: InternalsVisibleTo("Arkivverket.Arkade.Core.Tests")] namespace Arkivverket.Arkade.Core.Testing.Siard { - internal static class SiardValidator + public class SiardValidator : ISiardValidator { - private static readonly ILogger Log = Serilog.Log.ForContext(MethodBase.GetCurrentMethod()?.DeclaringType); - private static readonly string DbptkLibraryDirectoryPath; + private readonly ILogger _log = Log.ForContext(MethodBase.GetCurrentMethod()?.DeclaringType); + private readonly IStatusEventHandler _statusEventHandler; + private readonly ITestProgressReporter _testProgressReporter; - static SiardValidator() + private readonly string _dbptkLibraryDirectoryPath; + + public SiardValidator(IStatusEventHandler statusEventHandler, ITestProgressReporter testProgressReporter) { - DbptkLibraryDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, + _statusEventHandler = statusEventHandler; + _testProgressReporter = testProgressReporter; + _dbptkLibraryDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ArkadeConstants.DirectoryNameThirdPartySoftware, ArkadeConstants.DirectoryNameDbptk); } - public static (List, List) Validate(string inputFilePath, string reportFilePath) + public (List, List) Validate(string inputFilePath, string reportFilePath) { - string dbptkLibraryPath = Path.Combine(DbptkLibraryDirectoryPath, "dbptk-app-2.9.9.jar"); + _statusEventHandler.RaiseEventOperationMessage(Messages.ValidatingExtractMessage, null, OperationMessageStatus.Started); + _testProgressReporter.Begin(ArchiveType.Siard); - if (!File.Exists(dbptkLibraryPath)) + var dbptkLibraryPath = ""; + try + { + dbptkLibraryPath = new DirectoryInfo(_dbptkLibraryDirectoryPath).GetFiles("dbptk-app-2.9.9.jar").First().FullName; + } + catch + { + _testProgressReporter.Finish(hasFailed: true); throw new ArkadeException( string.Format(ExceptionMessages.SiardValidatorLibraryNotFound, Path.GetFileName(dbptkLibraryPath), ArkadeConstants.DbptkLibraryDownloadUrl, - DbptkLibraryDirectoryPath)); + _dbptkLibraryDirectoryPath)); + } Directory.CreateDirectory(Path.GetDirectoryName(reportFilePath)); - const string fileName = @"java"; - var processArguments = $"-jar \"-Dfile.encoding=UTF-8\" \"{dbptkLibraryPath}\" validate -if \"{inputFilePath}\" -r \"{reportFilePath}\""; - Process process = SetupSiardValidatorProcess(fileName, processArguments); + Process process = SetupSiardValidatorProcess(processArguments); (List results, List errors) = RunProcess(process); @@ -51,28 +62,34 @@ public static (List, List) Validate(string inputFilePath, string CleanUpDbptkLogFiles(); + _statusEventHandler.RaiseEventSiardValidationFinished(errors); + + bool validationRanWithoutRunErrors = errors.All(e => e == null || e.StartsWith("WARN")); + + _testProgressReporter.Finish(hasFailed: !validationRanWithoutRunErrors); + return (results, errors); } - private static void CleanUpDbptkLogFiles() + private void CleanUpDbptkLogFiles() { try { - Directory.GetFiles(DbptkLibraryDirectoryPath, "*.txt").ToList().ForEach(File.Delete); + Directory.GetFiles(_dbptkLibraryDirectoryPath, "*.txt").ToList().ForEach(File.Delete); } catch (Exception e) { - Log.Debug("Arkade could not delete log-files from SIARD-validation:\n" + e); + _log.Debug("Arkade could not delete log-files from SIARD-validation:\n" + e); } } - private static Process SetupSiardValidatorProcess(string fileName, string processArguments) + private static Process SetupSiardValidatorProcess(string processArguments) { var siardValidatorProcess = new Process { StartInfo = new ProcessStartInfo { - FileName = fileName, + FileName = @"java", Arguments = processArguments, RedirectStandardError = true, RedirectStandardOutput = true, @@ -86,7 +103,7 @@ private static Process SetupSiardValidatorProcess(string fileName, string proces return siardValidatorProcess; } - private static (List, List) RunProcess(Process process) + private (List, List) RunProcess(Process process) { var results = new List(); var errors = new List(); @@ -104,7 +121,7 @@ private static (List, List) RunProcess(Process process) } catch (Exception e) { - Log.Debug(e.ToString()); + _log.Debug(e.ToString()); throw new ArkadeException(ResolveMessageForException(e)); } @@ -121,7 +138,7 @@ private static string ResolveMessageForException(Exception e) : ExceptionMessages.SiardValidatorError; } - private static void HandleValidationErrors(List errors, List results) + private static void HandleValidationErrors(IEnumerable errors, ICollection results) { if (errors.Any(e => e != null && e.Contains("validator only supports: SIARD 2.1 version"))) { diff --git a/src/Arkivverket.Arkade.Core/Util/ArkadeAutofacModule.cs b/src/Arkivverket.Arkade.Core/Util/ArkadeAutofacModule.cs index 75e005fae..2828f2c14 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArkadeAutofacModule.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArkadeAutofacModule.cs @@ -7,6 +7,7 @@ using Arkivverket.Arkade.Core.Metadata; using Arkivverket.Arkade.Core.Testing; using Arkivverket.Arkade.Core.Testing.Noark5; +using Arkivverket.Arkade.Core.Testing.Siard; using Arkivverket.Arkade.Core.Util.ArchiveFormatValidation; using Arkivverket.Arkade.Core.Util.FileFormatIdentification; using Autofac; @@ -46,6 +47,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().AsSelf(); + builder.RegisterType().As(); builder.RegisterType().AsSelf(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); diff --git a/src/Arkivverket.Arkade.GUI/App.xaml.cs b/src/Arkivverket.Arkade.GUI/App.xaml.cs index ba92288b5..f0472efb1 100644 --- a/src/Arkivverket.Arkade.GUI/App.xaml.cs +++ b/src/Arkivverket.Arkade.GUI/App.xaml.cs @@ -11,6 +11,7 @@ using Arkivverket.Arkade.Core.Metadata; using Arkivverket.Arkade.Core.Testing; using Arkivverket.Arkade.Core.Testing.Noark5; +using Arkivverket.Arkade.Core.Testing.Siard; using Arkivverket.Arkade.GUI.Util; using Arkivverket.Arkade.Core.Util; using Arkivverket.Arkade.Core.Util.ArchiveFormatValidation; @@ -115,6 +116,7 @@ protected override void RegisterTypes(IContainerRegistry containerRegistry) containerRegistry.Register(); containerRegistry.Register(); containerRegistry.Register(); + containerRegistry.Register(); containerRegistry.Register(); containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); From 4c2091610018d257ced3bf0a819077409bb2ab94 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Wed, 13 Jul 2022 10:46:55 +0200 Subject: [PATCH 04/22] Allow user custom version of dbptk developer ARKADE-639 --- .../Testing/Siard/SiardValidatorRunnerTest.cs | 12 ++++--- .../Base/ArchiveTestingTool.cs | 14 ++++++++ .../Base/Siard/SiardTestEngine.cs | 6 ++-- .../Base/Siard/SiardValidationReport.cs | 17 +++++++++ src/Arkivverket.Arkade.Core/Base/TestSuite.cs | 8 +++++ .../Report/TestReportFactory.cs | 12 ++++--- .../Resources/ExceptionMessages.Designer.cs | 6 ++-- .../Resources/ExceptionMessages.nb-NO.resx | 2 +- .../Resources/ExceptionMessages.resx | 2 +- .../Resources/SiardMessages.Designer.cs | 22 ++++++------ .../Resources/SiardMessages.nb-NO.resx | 6 ++-- .../Resources/SiardMessages.resx | 8 +++-- .../Testing/Siard/ISiardValidator.cs | 4 +-- .../Testing/Siard/SiardValidator.cs | 35 ++++++++++++++----- .../Util/ArkadeConstants.cs | 4 +-- 15 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 src/Arkivverket.Arkade.Core/Base/ArchiveTestingTool.cs create mode 100644 src/Arkivverket.Arkade.Core/Base/Siard/SiardValidationReport.cs diff --git a/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs b/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs index b05db6f76..88af627ad 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Testing/Siard/SiardValidatorRunnerTest.cs @@ -44,7 +44,7 @@ public void ShouldReportUnsupportedSiardVersion() string inputFilePath = Path.Combine("TestData", "Siard", "siard1_med_blobs.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (List results, _) = Validator.Validate(inputFilePath, reportFilePath); + List results = Validator.Validate(inputFilePath, reportFilePath).Results; results.Count.Should().Be(2); results[0].Should().Be(Resources.SiardMessages.ErrorMessage); @@ -65,7 +65,7 @@ public void ShouldValidateExtractProducedBySiardGui() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "siardGui", "external", "siardGui.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (_, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); + List errorsAndWarnings = Validator.Validate(inputFilePath, reportFilePath).Errors; var errors = new List(errorsAndWarnings.Where(e => e == null || !e.StartsWith("WARN"))); @@ -87,7 +87,7 @@ public void ShouldValidateExtractProducedByDbptkDeveloper() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "dbPtk", "external", "dbptk.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (_, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); + List errorsAndWarnings = Validator.Validate(inputFilePath, reportFilePath).Errors; var errors = new List(errorsAndWarnings.Where(e => e == null || !e.StartsWith("WARN"))); @@ -111,7 +111,7 @@ public void ShouldFailToValidateExtractProducedBySpectralCoreFullConvert() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "fullConvert", "external", "scfc.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (_, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); + List errorsAndWarnings = Validator.Validate(inputFilePath, reportFilePath).Errors; var errors = new List(errorsAndWarnings.Where(e => e == null || !e.StartsWith("WARN"))); @@ -137,7 +137,9 @@ public void ShouldReportWarningsWhenExternalLobsAreMissing() string inputFilePath = Path.Combine("TestData", "Siard", "siard2", "externalLobsMissing", "dbptk.siard"); string reportFilePath = Path.Combine("TestData", "Siard", "testReport.txt"); - (List results, List errorsAndWarnings) = Validator.Validate(inputFilePath, reportFilePath); + var report = Validator.Validate(inputFilePath, reportFilePath); + List results = report.Results; + List errorsAndWarnings = report.Errors; List summary = results.Where(r => r != null && r.Trim().StartsWith("Number of")).ToList(); diff --git a/src/Arkivverket.Arkade.Core/Base/ArchiveTestingTool.cs b/src/Arkivverket.Arkade.Core/Base/ArchiveTestingTool.cs new file mode 100644 index 000000000..146d183d3 --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Base/ArchiveTestingTool.cs @@ -0,0 +1,14 @@ +namespace Arkivverket.Arkade.Core.Base +{ + public class ArchiveTestingTool + { + public string Name { get; } + public string Version { get; } + + public ArchiveTestingTool(string name, string version) + { + Name = name; + Version = version; + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs b/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs index 617f4d61b..297e9e6c3 100644 --- a/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs +++ b/src/Arkivverket.Arkade.Core/Base/Siard/SiardTestEngine.cs @@ -24,9 +24,9 @@ public TestSuite RunTestsOnArchive(TestSession testSession) string reportFilePath = Path.Combine(testSession.Archive.WorkingDirectory.RepositoryOperations().ToString(), Resources.OutputFileNames.DbptkValidationReportFile); - (List results, List errors) = _validator.Validate(inputFilePath, reportFilePath); + SiardValidationReport report = _validator.Validate(inputFilePath, reportFilePath); - List summary = results.Where(r => r != null && r.Contains("number of", StringComparison.InvariantCultureIgnoreCase)).ToList(); + List summary = report.Results.Where(r => r != null && r.Contains("number of", StringComparison.InvariantCultureIgnoreCase)).ToList(); int numberOfValidationErrors; int numberOfValidationWarnings; @@ -44,7 +44,7 @@ public TestSuite RunTestsOnArchive(TestSession testSession) testSession.TestSummary = new TestSummary(0, 0, 0, numberOfValidationErrors, numberOfValidationWarnings); - return new TestSuite(); + return new TestSuite(report.TestingTool); } private int GetNumberOfXFromSummary(string x, List summary) diff --git a/src/Arkivverket.Arkade.Core/Base/Siard/SiardValidationReport.cs b/src/Arkivverket.Arkade.Core/Base/Siard/SiardValidationReport.cs new file mode 100644 index 000000000..d8b55ec52 --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Base/Siard/SiardValidationReport.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Arkivverket.Arkade.Core.Base.Siard +{ + public class SiardValidationReport + { + public List Results { get; } + public List Errors { get; } + public ArchiveTestingTool TestingTool { get; set; } + + public SiardValidationReport(List results, List errors) + { + Results = results; + Errors = errors; + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Base/TestSuite.cs b/src/Arkivverket.Arkade.Core/Base/TestSuite.cs index 28afdbf48..c9dc4c4b9 100644 --- a/src/Arkivverket.Arkade.Core/Base/TestSuite.cs +++ b/src/Arkivverket.Arkade.Core/Base/TestSuite.cs @@ -9,6 +9,8 @@ public class TestSuite { public IEnumerable TestRuns => _testRuns.ToList(); + internal ArchiveTestingTool TestTool { get; set; } + private readonly SortedSet _testRuns; public TestSuite() @@ -16,6 +18,12 @@ public TestSuite() _testRuns = new SortedSet(); } + internal TestSuite(ArchiveTestingTool testingTool) + { + _testRuns = new SortedSet(); + TestTool = testingTool; + } + public void AddTestRun(TestRun testRun) { bool testRunWasAdded = _testRuns.Add(testRun); diff --git a/src/Arkivverket.Arkade.Core/Report/TestReportFactory.cs b/src/Arkivverket.Arkade.Core/Report/TestReportFactory.cs index 2e1263feb..854cff722 100644 --- a/src/Arkivverket.Arkade.Core/Report/TestReportFactory.cs +++ b/src/Arkivverket.Arkade.Core/Report/TestReportFactory.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using Arkivverket.Arkade.Core.Base; +using Arkivverket.Arkade.Core.Resources; using Arkivverket.Arkade.Core.Testing; namespace Arkivverket.Arkade.Core.Report @@ -24,7 +25,7 @@ public static TestReport CreateForSiard(TestSession testSession) var testReport = new TestReport { Summary = CreateTestReportSummary(testSession), - TestsResults = GetSiardTestReportResults(), + TestsResults = GetSiardTestReportResults(testSession.TestSuite.TestTool), }; return testReport; @@ -107,14 +108,15 @@ private static List GetResults(IEnumerable testResults) }).ToList(); } - private static List GetSiardTestReportResults() + private static List GetSiardTestReportResults(ArchiveTestingTool testingTool) { return new() { new ExecutedTest { TestId = "externalReport", - TestName = string.Format(Resources.SiardMessages.ValidationResultTestName, Resources.SiardMessages.DbptkDeveloper), + TestName = string.Format(SiardMessages.ValidationResultTestName, + string.Format(SiardMessages.ValidationTool, testingTool.Name, testingTool.Version)), ResultSet = GetSiardResultSet(), HasResults = true, TestType = null, @@ -137,8 +139,8 @@ private static List GetSiardResult() { new Result { - Location = new Location { String = Resources.OutputFileNames.DbptkValidationReportFile }, - Message = Resources.SiardMessages.ValidationResultMessage, + Location = new Location { String = OutputFileNames.DbptkValidationReportFile }, + Message = SiardMessages.ValidationResultMessage, } }; } diff --git a/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.Designer.cs b/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.Designer.cs index 878f322e2..aa72105c1 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.Designer.cs +++ b/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -19,7 +19,7 @@ namespace Arkivverket.Arkade.Core.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class ExceptionMessages { @@ -133,7 +133,7 @@ public static string SiardValidatorError { } ///

- /// Looks up a localized string similar to Could not find the validator library ({0}). Please download the library from {1}, and save the file at {2}.. + /// Looks up a localized string similar to Could not find the validator library. Please download the library from {0}, and save the file at {1}.. /// public static string SiardValidatorLibraryNotFound { get { diff --git a/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.nb-NO.resx b/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.nb-NO.resx index 6fccb8ecb..417fce547 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.nb-NO.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.nb-NO.resx @@ -151,7 +151,7 @@ Klarte ikke starte validatoren. Vennligst forsikre deg om at Java Runtime er installert og tilgjengelig ved å skrive "java -version" i konsollen. - Klarte ikke finne validatorbiblioteket ({0}). Vennligst last ned fra {1}, og lagre filen i {2}. + Klarte ikke finne validatorbiblioteket. Vennligst last ned fra {0}, og lagre filen i {1}. Arkade har ikke rettigheter til å skrive til denne lokasjonen. diff --git a/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.resx b/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.resx index 8a9a473a4..c570d26fd 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ExceptionMessages.resx @@ -151,7 +151,7 @@ Could not start validator. Please make sure that Java Runtime is installed and available by entering "java -version" in a console. - Could not find the validator library ({0}). Please download the library from {1}, and save the file at {2}. + Could not find the validator library. Please download the library from {0}, and save the file at {1}. Arkade does not have permission to write to this location. diff --git a/src/Arkivverket.Arkade.Core/Resources/SiardMessages.Designer.cs b/src/Arkivverket.Arkade.Core/Resources/SiardMessages.Designer.cs index c7417a2ed..b155c2205 100644 --- a/src/Arkivverket.Arkade.Core/Resources/SiardMessages.Designer.cs +++ b/src/Arkivverket.Arkade.Core/Resources/SiardMessages.Designer.cs @@ -19,7 +19,7 @@ namespace Arkivverket.Arkade.Core.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class SiardMessages { @@ -69,15 +69,6 @@ public static string CouldNotFindASiardFile { } } - /// - /// Looks up a localized string similar to Database Preservation Toolkit Developer version 2.9.9. - /// - public static string DbptkDeveloper { - get { - return ResourceManager.GetString("DbptkDeveloper", resourceCulture); - } - } - /// /// Looks up a localized string similar to {0} is not valid for SIARD version {1}: ///{2}. @@ -107,7 +98,7 @@ public static string InlinedLobContentHasUnsupportedEncoding { } /// - /// Looks up a localized string similar to {0} is being validated with Database Preservation Toolkit Developer version 2.9.9. + /// Looks up a localized string similar to {0} is being validated with Database Preservation Toolkit Developer version {1}. /// public static string ValidationMessage { get { @@ -142,6 +133,15 @@ public static string ValidationResultTestName { } } + /// + /// Looks up a localized string similar to {0} version {1}. + /// + public static string ValidationTool { + get { + return ResourceManager.GetString("ValidationTool", resourceCulture); + } + } + /// /// Looks up a localized string similar to SIARD validator only supports SIARD 2.1 version. /// diff --git a/src/Arkivverket.Arkade.Core/Resources/SiardMessages.nb-NO.resx b/src/Arkivverket.Arkade.Core/Resources/SiardMessages.nb-NO.resx index 36a3bba92..28eace4a7 100644 --- a/src/Arkivverket.Arkade.Core/Resources/SiardMessages.nb-NO.resx +++ b/src/Arkivverket.Arkade.Core/Resources/SiardMessages.nb-NO.resx @@ -121,7 +121,7 @@ Siard-validering - {0} blir validert med Database Preservation Toolkit Developer versjon 2.9.9 + {0} blir validert med Database Preservation Toolkit Developer versjon {1} SIARD validatoren støtter kun versjon 2.1 av SIARD @@ -132,8 +132,8 @@ Resultater fra konformitetstest utført med {0} - - Database Preservation Toolkit Developer versjon 2.9.9 + + {0} versjon {1} Valideringsresultater diff --git a/src/Arkivverket.Arkade.Core/Resources/SiardMessages.resx b/src/Arkivverket.Arkade.Core/Resources/SiardMessages.resx index d7fdfc603..87b549b52 100644 --- a/src/Arkivverket.Arkade.Core/Resources/SiardMessages.resx +++ b/src/Arkivverket.Arkade.Core/Resources/SiardMessages.resx @@ -101,7 +101,8 @@ Siard Validation - {0} is being validated with Database Preservation Toolkit Developer version 2.9.9 + {0} is being validated with Database Preservation Toolkit Developer version {1} + {0} target, {1} DBPTK-version SIARD validator only supports SIARD 2.1 version @@ -113,8 +114,9 @@ Results of conformity test performed by {0} {0} tool used for validation - - Database Preservation Toolkit Developer version 2.9.9 + + {0} version {1} + {0}: Name, {1}: version Validation results diff --git a/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs b/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs index b4a7c6a1f..af13f2b5f 100644 --- a/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs +++ b/src/Arkivverket.Arkade.Core/Testing/Siard/ISiardValidator.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; +using Arkivverket.Arkade.Core.Base.Siard; namespace Arkivverket.Arkade.Core.Testing.Siard { public interface ISiardValidator { - (List, List) Validate(string inputFilePath, string reportFilePath); + SiardValidationReport Validate(string inputFilePath, string reportFilePath); } } diff --git a/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs b/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs index ab0ef8e21..6436ac3cc 100644 --- a/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs +++ b/src/Arkivverket.Arkade.Core/Testing/Siard/SiardValidator.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Text; using Arkivverket.Arkade.Core.Base; +using Arkivverket.Arkade.Core.Base.Siard; using Arkivverket.Arkade.Core.Logging; using Arkivverket.Arkade.Core.Resources; using Arkivverket.Arkade.Core.Util; @@ -30,7 +31,7 @@ public SiardValidator(IStatusEventHandler statusEventHandler, ITestProgressRepor ArkadeConstants.DirectoryNameThirdPartySoftware, ArkadeConstants.DirectoryNameDbptk); } - public (List, List) Validate(string inputFilePath, string reportFilePath) + public SiardValidationReport Validate(string inputFilePath, string reportFilePath) { _statusEventHandler.RaiseEventOperationMessage(Messages.ValidatingExtractMessage, null, OperationMessageStatus.Started); _testProgressReporter.Begin(ArchiveType.Siard); @@ -38,14 +39,13 @@ public SiardValidator(IStatusEventHandler statusEventHandler, ITestProgressRepor var dbptkLibraryPath = ""; try { - dbptkLibraryPath = new DirectoryInfo(_dbptkLibraryDirectoryPath).GetFiles("dbptk-app-2.9.9.jar").First().FullName; + dbptkLibraryPath = new DirectoryInfo(_dbptkLibraryDirectoryPath).GetFiles("*.jar").First().FullName; } catch { _testProgressReporter.Finish(hasFailed: true); throw new ArkadeException( string.Format(ExceptionMessages.SiardValidatorLibraryNotFound, - Path.GetFileName(dbptkLibraryPath), ArkadeConstants.DbptkLibraryDownloadUrl, _dbptkLibraryDirectoryPath)); } @@ -56,19 +56,36 @@ public SiardValidator(IStatusEventHandler statusEventHandler, ITestProgressRepor Process process = SetupSiardValidatorProcess(processArguments); - (List results, List errors) = RunProcess(process); + SiardValidationReport siardValidationReport = RunProcess(process); + + siardValidationReport.TestingTool = GetDbptkDeveloperInformation(dbptkLibraryPath); ExternalProcessManager.Close(process); CleanUpDbptkLogFiles(); - _statusEventHandler.RaiseEventSiardValidationFinished(errors); + _statusEventHandler.RaiseEventSiardValidationFinished(siardValidationReport.Errors); - bool validationRanWithoutRunErrors = errors.All(e => e == null || e.StartsWith("WARN")); + bool validationRanWithoutRunErrors = siardValidationReport.Errors.All(e => e == null || e.StartsWith("WARN")); _testProgressReporter.Finish(hasFailed: !validationRanWithoutRunErrors); - return (results, errors); + return siardValidationReport; + } + + private ArchiveTestingTool GetDbptkDeveloperInformation(string dbptkLibraryPath) + { + var processArguments = $"-jar \"-Dfile.encoding=UTF-8\" \"{dbptkLibraryPath}\" -h validate"; + Process process = SetupSiardValidatorProcess(processArguments); + + List results = RunProcess(process).Results; + + string versionString = results.Find(s => s.Contains("version")); + + string version = versionString?.Replace("DBPTK Developer (version ", string.Empty, + StringComparison.InvariantCultureIgnoreCase).TrimEnd(')'); + + return new ArchiveTestingTool("Database Preservation Toolkit Developer", version); } private void CleanUpDbptkLogFiles() @@ -103,7 +120,7 @@ private static Process SetupSiardValidatorProcess(string processArguments) return siardValidatorProcess; } - private (List, List) RunProcess(Process process) + private SiardValidationReport RunProcess(Process process) { var results = new List(); var errors = new List(); @@ -128,7 +145,7 @@ private static Process SetupSiardValidatorProcess(string processArguments) if (errors.Any()) HandleValidationErrors(errors, results); - return (results, errors); + return new SiardValidationReport(results, errors); } private static string ResolveMessageForException(Exception e) diff --git a/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs b/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs index 0f6c83fad..4550b2eff 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs @@ -1,4 +1,4 @@ -namespace Arkivverket.Arkade.Core.Util +namespace Arkivverket.Arkade.Core.Util { public class ArkadeConstants { @@ -82,7 +82,7 @@ public class ArkadeConstants public const string ArkadeWebSiteUrl = "https://arkade.arkivverket.no"; public const string DbptkLibraryDownloadUrl = - "https://github.com/keeps/dbptk-developer/releases/download/v2.9.9/dbptk-app-2.9.9.jar"; + "https://github.com/keeps/dbptk-developer/releases/"; public static readonly string[] SuppressedDbptkWarningMessages = { From 72252e22c3736cd68e3d1d722f03021c13a27095 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Tue, 30 Aug 2022 14:31:53 +0200 Subject: [PATCH 05/22] Fix PDF/A batch validation problems ARKADE-657 --- .../PdfA/PdfAValidator.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs index 2705301cb..293958f5d 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs @@ -102,28 +102,22 @@ private async Task CreatePdfAValidationReportAsync( // counters have been implemented. Additionally, this validator is more strict than veraPDF in which // PDF/A-profiles it accepts - see field _approvedPdfAProfiles. IEnumerable validationJobs = (await _validator.ValidateBatchWithDetailedReportAsync( - new[] { directory.FullName }, "")).Jobs.AllJobs.AsEnumerable(); + new[] { directory.FullName }, "-r")).Jobs.AllJobs.AsEnumerable(); - var partialReport = new PdfAValidationReport(); + var pdfAValidationReport = new PdfAValidationReport(); - foreach (FileSystemInfo fileSystemInfo in directory.EnumerateFileSystemInfos()) + foreach (FileInfo fileInfo in directory.EnumerateFiles("*", SearchOption.AllDirectories)) { - if (fileSystemInfo is DirectoryInfo directoryInfo) - { - partialReport.Merge(await CreatePdfAValidationReportAsync(directoryInfo)); - continue; - } - - partialReport.TotalNumberOfFiles++; + pdfAValidationReport.TotalNumberOfFiles++; - string itemName = Path.GetRelativePath(_baseDirectoryPath, fileSystemInfo.FullName); + string itemName = Path.GetRelativePath(_baseDirectoryPath, fileInfo.FullName); - Job job = validationJobs.FirstOrDefault(j => j.Item.Name.Equals(fileSystemInfo.FullName)); + Job job = validationJobs.FirstOrDefault(j => j.Item.Name.Equals(fileInfo.FullName)); if (job == default(Job)) { - partialReport.NumberOfUndeterminedFiles++; - partialReport.ValidationItems.Add(new PdfAValidationItem + pdfAValidationReport.NumberOfUndeterminedFiles++; + pdfAValidationReport.ValidationItems.Add(new PdfAValidationItem { ItemName = itemName, PdfAProfile = "N/A", @@ -137,10 +131,10 @@ private async Task CreatePdfAValidationReportAsync( bool itemIsValid = validationReport.IsCompliant && _approvedPdfAProfiles.Contains(reportedPdfAProfile); - if (itemIsValid) partialReport.NumberOfValidFiles++; - else partialReport.NumberOfInvalidFiles++; + if (itemIsValid) pdfAValidationReport.NumberOfValidFiles++; + else pdfAValidationReport.NumberOfInvalidFiles++; - partialReport.ValidationItems.Add(new PdfAValidationItem + pdfAValidationReport.ValidationItems.Add(new PdfAValidationItem { ItemName = itemName, PdfAProfile = reportedPdfAProfile, @@ -148,7 +142,7 @@ private async Task CreatePdfAValidationReportAsync( }); } - return partialReport; + return pdfAValidationReport; } public void Dispose() From 4371b08670435b254517f7b9df72ec1340da3e70 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Tue, 30 Aug 2022 15:33:57 +0200 Subject: [PATCH 06/22] Fix PDF/A batch validation issue when target directory contains no pdfs ARKADE-651 --- .../ArchiveFormatValidation/PdfA/PdfAValidator.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs index 293958f5d..ae1e0abd7 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs @@ -101,8 +101,16 @@ private async Task CreatePdfAValidationReportAsync( // information only keep track of files ending with .pdf. All other files are ignored. Hence, custom // counters have been implemented. Additionally, this validator is more strict than veraPDF in which // PDF/A-profiles it accepts - see field _approvedPdfAProfiles. - IEnumerable validationJobs = (await _validator.ValidateBatchWithDetailedReportAsync( - new[] { directory.FullName }, "-r")).Jobs.AllJobs.AsEnumerable(); + IEnumerable validationJobs = null; + try + { + validationJobs = (await _validator.ValidateBatchWithDetailedReportAsync( + new[] { directory.FullName }, "-r")).Jobs.AllJobs.AsEnumerable(); + } + catch (Exception e) + { + Log.Error($"veraPDF encountered a problem: {e}"); + } var pdfAValidationReport = new PdfAValidationReport(); @@ -112,7 +120,7 @@ private async Task CreatePdfAValidationReportAsync( string itemName = Path.GetRelativePath(_baseDirectoryPath, fileInfo.FullName); - Job job = validationJobs.FirstOrDefault(j => j.Item.Name.Equals(fileInfo.FullName)); + Job job = validationJobs?.FirstOrDefault(j => j.Item.Name.Equals(fileInfo.FullName)); if (job == default(Job)) { From 414efe1c98a7d92fcfe2903678ec07d2c9f3ab33 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Tue, 6 Sep 2022 07:33:15 +0200 Subject: [PATCH 07/22] Fix typo --- src/Arkivverket.Arkade.Core/Resources/Messages.nb-NO.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arkivverket.Arkade.Core/Resources/Messages.nb-NO.resx b/src/Arkivverket.Arkade.Core/Resources/Messages.nb-NO.resx index bdc978e9e..e5f926f08 100644 --- a/src/Arkivverket.Arkade.Core/Resources/Messages.nb-NO.resx +++ b/src/Arkivverket.Arkade.Core/Resources/Messages.nb-NO.resx @@ -275,7 +275,7 @@ Aktuell sjekksum: {1} Verdier kortere enn minstelengde: {0} - Kontrollerer om det fines null-verdier i feltet + Kontrollerer om det finnes null-verdier i feltet NULL-verdier finnes From 5b457aaf4b799de9a60378f3fcacbad1ebc55a3e Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Tue, 6 Sep 2022 09:46:26 +0200 Subject: [PATCH 08/22] Fix issues with parsing of quoted fields ARKADE-659 --- .../Addml/DelimiterFileFormatReaderTest.cs | 10 +- .../Util/Util_CsvHelperTests.cs | 47 ++++++++ .../Base/Addml/DelimiterFileFormatReader.cs | 112 +----------------- src/Arkivverket.Arkade.Core/Util/CsvHelper.cs | 109 ++++++++++++++++- .../Util/Extensions/StringExtensions.cs | 52 ++++++++ 5 files changed, 217 insertions(+), 113 deletions(-) create mode 100644 src/Arkivverket.Arkade.Core.Tests/Util/Util_CsvHelperTests.cs diff --git a/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs b/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs index 7ee621bc2..6a569483e 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs @@ -223,7 +223,7 @@ public void QuotingCharWithinQuotingCharsAreNotInterpretedAsQuotingChar() new AddmlFieldDefinitionBuilder().WithRecordDefinition(recordDefinition).Build(); - var csvData = $"{quotingString}A{quotingString}B{quotingString}"; + var csvData = $"{quotingString}A{quotingString}{quotingString}B{quotingString}"; var streamReader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(csvData))); var recordReader = new DelimiterFileFormatReader(new FlatFile(addmlFlatFileDefinition), streamReader); @@ -262,10 +262,10 @@ public void FieldIsOnlyQuotedWhenStartingAndEndingWithQuotingChar() new AddmlFieldDefinitionBuilder().WithRecordDefinition(recordDefinition).Build(); new AddmlFieldDefinitionBuilder().WithRecordDefinition(recordDefinition).Build(); - string csvData = $"{quotingString}A{quotingString}B{quotingString}{fieldSeparator}" + - $"C{quotingString}{fieldSeparator}" + + string csvData = $"{quotingString}A{quotingString}{quotingString}B{quotingString}{fieldSeparator}" + + $"{quotingString}C{quotingString}{quotingString}{quotingString}{fieldSeparator}" + $"{quotingString}D{quotingString}{quotingString}{fieldSeparator} asd{quotingString}{fieldSeparator}" + - $"E{quotingString}noko{quotingString}{fieldSeparator}" + + $"{quotingString}E{quotingString}{quotingString}noko{quotingString}{quotingString}{quotingString}{fieldSeparator}" + "F"; var streamReader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(csvData))); @@ -278,7 +278,7 @@ public void FieldIsOnlyQuotedWhenStartingAndEndingWithQuotingChar() recordReader.Current?.Fields?.Count.Should().Be(5); recordReader.Current?.Fields?[0].Value.Should().Be($"A{quotingString}B"); recordReader.Current?.Fields?[1].Value.Should().Be($"C{quotingString}"); - recordReader.Current?.Fields?[2].Value.Should().Be($"D{quotingString}{quotingString}{fieldSeparator} asd"); + recordReader.Current?.Fields?[2].Value.Should().Be($"D{quotingString}{fieldSeparator} asd"); recordReader.Current?.Fields?[3].Value.Should().Be($"E{quotingString}noko{quotingString}"); recordReader.Current?.Fields?[4].Value.Should().Be("F"); } diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/Util_CsvHelperTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/Util_CsvHelperTests.cs new file mode 100644 index 000000000..c2bb2a48b --- /dev/null +++ b/src/Arkivverket.Arkade.Core.Tests/Util/Util_CsvHelperTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using Xunit; + +namespace Arkivverket.Arkade.Core.Tests.Util +{ + public class Util_CsvHelperTests + { + [Fact] + public void ShouldSplitStringsCorrectly() + { + const string recordDelimiter = "\n"; + string[] delimiters = { ";", "!=/HFA", "|", "#", ",", "-", "<=>" }; + string[] quotingStrings = { "\"", "\"\"\"", "*", "\"s", "as\\d5", "\\*+?{[()^$." }; + + foreach (string d in delimiters) + { + foreach (string q in quotingStrings) + { + string testStr = $"{q}{q}{d}" + + $"{q}A{q}{q}B{q}{d}" + + $"{q}C{q}{d}" + + $"{q}D{q}{q}{d} asd{q}{d}" + + $"{q}E{q}{q}noko{q}{q}{q}{d}" + + $"F{d}" + + $"{q}{q}{d}" + + $"{q}{q}{q}{d}{q}{q}{q}{d}" + + $"{q}{q}{q}encapsulated{d}value{q}{q} plus more{q}{d}" + + $"{q}{q}" + + $"{recordDelimiter}"; + + string[] splitString = Core.Util.CsvHelper.Split(testStr, recordDelimiter, d, q); + + splitString[0].Should().Be(""); + splitString[1].Should().Be($"A{q}B"); + splitString[2].Should().Be("C"); + splitString[3].Should().Be($"D{q}{d} asd"); + splitString[4].Should().Be($"E{q}noko{q}"); + splitString[5].Should().Be("F"); + splitString[6].Should().Be(""); + splitString[7].Should().Be($"{q}{d}{q}"); + splitString[8].Should().Be($"{q}encapsulated{d}value{q} plus more"); + splitString[9].Should().Be(""); + } + } + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Base/Addml/DelimiterFileFormatReader.cs b/src/Arkivverket.Arkade.Core/Base/Addml/DelimiterFileFormatReader.cs index 4b9c4c09a..8b35caf39 100644 --- a/src/Arkivverket.Arkade.Core/Base/Addml/DelimiterFileFormatReader.cs +++ b/src/Arkivverket.Arkade.Core/Base/Addml/DelimiterFileFormatReader.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using Arkivverket.Arkade.Core.Base.Addml.Definitions; @@ -8,6 +7,7 @@ namespace Arkivverket.Arkade.Core.Base.Addml { public class DelimiterFileFormatReader : FileFormatReader { + private readonly string _recordDelimiter; private readonly string _fieldDelimiter; private readonly string _quotingChar; private readonly IEnumerator _lines; @@ -24,7 +24,7 @@ public DelimiterFileFormatReader(FlatFile flatFile) : this(flatFile, GetStream(f public DelimiterFileFormatReader(FlatFile flatFile, StreamReader streamReader) : base(flatFile.Definition) { - string recordDelimiter = GetRecordDelimiter(flatFile); + _recordDelimiter = GetRecordDelimiter(flatFile); _fieldDelimiter = GetFieldDelimiter(flatFile); _quotingChar = flatFile.Definition.QuotingChar; if (_quotingChar != null && _fieldDelimiter.Equals(_quotingChar)) @@ -32,7 +32,7 @@ public DelimiterFileFormatReader(FlatFile flatFile, StreamReader streamReader) : Resources.AddmlMessages.FieldDelimiterAndQuotingCharCannotHaveSameValue, _quotingChar, _fieldDelimiter)); _recordIdentifierPosition = flatFile.GetRecordIdentifierPosition(); - _lines = new DelimiterFileRecordEnumerable(streamReader, recordDelimiter, _quotingChar).GetEnumerator(); + _lines = new DelimiterFileRecordEnumerable(streamReader, _recordDelimiter, _quotingChar).GetEnumerator(); } private string GetFieldDelimiter(FlatFile flatFile) @@ -59,7 +59,8 @@ private Record GetCurrentRecord() string currentLine = _lines.Current; - string [] strings = JoinQuotedValues(Split(currentLine)); + string[] strings = Util.CsvHelper.Split(currentLine, _recordDelimiter, _fieldDelimiter, _quotingChar); + string recordIdentifier = null; if (_recordIdentifierPosition.HasValue) @@ -114,108 +115,5 @@ public override void Reset() { _lines.Reset(); } - - private IEnumerable Split(string stringToSplit) - { - var strings = new List(); - var buffer = ""; - var fdIndex = 0; - - foreach (char c in stringToSplit) - { - buffer += c; - - if (c == _fieldDelimiter[fdIndex]) - { - fdIndex++; - - if (fdIndex != _fieldDelimiter.Length) - continue; - - strings.Add(buffer[..^_fieldDelimiter.Length]); - fdIndex = 0; - buffer = ""; - } - else - fdIndex = 0; - } - - strings.Add(buffer); - - return strings; - } - - private string[] JoinQuotedValues(IEnumerable splitFieldValues) - { - if (_quotingChar == null) - return splitFieldValues.ToArray(); - - var fieldValues = new List(); - var fieldValue = ""; - var concatenating = false; - foreach (string splitFieldValue in splitFieldValues) - { - if (concatenating) - { - fieldValue += _fieldDelimiter; - - if (EndsWithOddNumberOfQuotingChars(splitFieldValue)) - { - fieldValue += TrimQuotingCharFromEnd(splitFieldValue); - fieldValues.Add(fieldValue); - fieldValue = ""; - concatenating = false; - } - else - fieldValue += splitFieldValue; - - } - else - { - if (splitFieldValue.StartsWith(_quotingChar)) - { - if (EndsWithOddNumberOfQuotingChars(splitFieldValue)) - fieldValues.Add(TrimQuotingChar(splitFieldValue)); - else - { - fieldValue = TrimQuotingCharFromStart(splitFieldValue); - concatenating = true; - } - } - else - fieldValues.Add(splitFieldValue); - } - } - - return fieldValues.ToArray(); - } - - private string TrimQuotingChar(string valueWithQuotingChar) - { - return valueWithQuotingChar[_quotingChar.Length..^_quotingChar.Length]; - } - - private string TrimQuotingCharFromStart(string valueWithQuotingChar) - { - return valueWithQuotingChar[_quotingChar.Length..]; - } - - private string TrimQuotingCharFromEnd(string valueWithQuotingChar) - { - return valueWithQuotingChar[..^_quotingChar.Length]; - } - - private bool EndsWithOddNumberOfQuotingChars(string value) - { - var numberOfQuotingChars = 0; - string copy = value; - while (copy.EndsWith(_quotingChar)) - { - numberOfQuotingChars++; - copy = TrimQuotingCharFromEnd(copy); - } - - return numberOfQuotingChars % 2 == 1; - } } } \ No newline at end of file diff --git a/src/Arkivverket.Arkade.Core/Util/CsvHelper.cs b/src/Arkivverket.Arkade.Core/Util/CsvHelper.cs index 0e0a97c45..a421427b7 100644 --- a/src/Arkivverket.Arkade.Core/Util/CsvHelper.cs +++ b/src/Arkivverket.Arkade.Core/Util/CsvHelper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using CsvHelper; @@ -22,5 +23,111 @@ public static void WriteToFile(string filePath, IEnumerable records) using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); csv.WriteRecords(records); } + + public static string[] Split(string stringToSplit, string recordDelimiter, string fieldDelimiter, string quotingChar) + { + if (quotingChar == "\"") + { + using var csvReader = new CsvReader(new StringReader(stringToSplit), + new CsvConfiguration(CultureInfo.InvariantCulture) + { + Quote = '"', + Delimiter = fieldDelimiter, + Escape = '"' + }); + csvReader.Read(); + return csvReader.Parser.Record; + } + string[] strings = stringToSplit.Split(fieldDelimiter, StringSplitOptions.None); + + return JoinQuoted(strings, recordDelimiter, fieldDelimiter, quotingChar); + } + + private static string[] JoinQuoted(string[] strings, string recordDelimiter, string delimiter, string quotingChar) + { + if (string.IsNullOrEmpty(quotingChar)) + return strings; + + var fieldValues = new List(); + var fieldValue = ""; + var concatenating = false; + for (var i = 0; i < strings.Length; i++) + { + string splitFieldValue = strings[i]; + if (concatenating) + { + fieldValue += delimiter; + + if (EndsWithOddNumberOfQuotingChars(splitFieldValue, quotingChar)) + { + fieldValue += splitFieldValue.TrimEnd(quotingChar); + fieldValues.Add(RemoveEscapeChars(fieldValue, quotingChar)); + fieldValue = ""; + concatenating = false; + } + else + { + fieldValue += splitFieldValue; + } + } + else + { + // if not concatenating and last element, no concatenation needed - just add value + if (i == strings.Length - 1) + { + var valueWithoutRecordDelimiter = splitFieldValue.TrimEnd(recordDelimiter); + var valueWithoutQuotingChars = valueWithoutRecordDelimiter.Trim(quotingChar); + fieldValues.Add(RemoveEscapeChars(valueWithoutQuotingChars, + quotingChar)); + } + else if (splitFieldValue.StartsWith(quotingChar)) + { + // Remove leading quoting char in case split value only consists of quoting chars + if (EndsWithOddNumberOfQuotingChars(splitFieldValue.TrimStart(quotingChar), quotingChar)) + { + fieldValues.Add(RemoveEscapeChars(splitFieldValue.Trim(quotingChar), quotingChar)); + } + else + { + fieldValue = splitFieldValue.TrimStart(quotingChar); + concatenating = true; + } + } + else + { + fieldValues.Add(RemoveEscapeChars(splitFieldValue, quotingChar)); + } + } + } + + return fieldValues.ToArray(); + } + + private static string RemoveEscapeChars(string value, string escape) + { + if (value == $"{escape}{escape}" || value == string.Empty) + { + return string.Empty; + } + for (int i; (i = value.IndexOf($"{escape}{escape}", StringComparison.InvariantCulture)) != -1;) + { + value = value.Remove(i, escape.Length); + } + + return value; + } + + private static bool EndsWithOddNumberOfQuotingChars(string value, string quotingChar) + { + var numberOfQuotingChars = 0; + string copy = new(value); + while (copy.EndsWith(quotingChar)) + { + numberOfQuotingChars++; + copy = copy.TrimEnd(quotingChar); + } + + return numberOfQuotingChars % 2 == 1; + } } } diff --git a/src/Arkivverket.Arkade.Core/Util/Extensions/StringExtensions.cs b/src/Arkivverket.Arkade.Core/Util/Extensions/StringExtensions.cs index d83f66c24..2ca18dfb0 100644 --- a/src/Arkivverket.Arkade.Core/Util/Extensions/StringExtensions.cs +++ b/src/Arkivverket.Arkade.Core/Util/Extensions/StringExtensions.cs @@ -6,5 +6,57 @@ public static string ForwardSlashed(this string text) { return text.Replace('\\', '/'); } + + + /// + /// Removes a single leading and trailing occurrence of a specified from the current + /// string. + /// + /// + /// + /// + /// The string that remains after a single occurrence of the parameter are removed + /// from the start and end of the current string. If is null or empty, or if the + /// current instance does not start or end with , the method returns the current instance + /// unchanged. + /// + public static string Trim(this string text, string value) + { + return text.TrimStart(value).TrimEnd(value); + } + + /// + /// Removes a single leading occurrence of a specified from the current string. + /// + /// + /// + /// + /// The string that remains after a single occurrence of the parameter are removed + /// from the start of the current string. If is null or empty, or if the current + /// instance does not start with , the method returns the current instance unchanged. + /// + public static string TrimStart(this string text, string value) + { + if (string.IsNullOrEmpty(value)) + return text; + return text.StartsWith(value) ? text[value.Length..] : text; + } + + /// + /// Removes a single trailing occurrence of a specified from the current string. + /// + /// + /// + /// + /// The string that remains after a single occurrence of the parameter are removed + /// from the end of the current string. If is null or empty, or if the current + /// instance does not end with , the method returns the current instance unchanged. + /// + public static string TrimEnd(this string text, string value) + { + if (string.IsNullOrEmpty(value)) + return text; + return text.EndsWith(value) ? text[..^value.Length] : text; + } } } From fd7c88d51e1e276efc90f2c696ed577a59336078 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Tue, 6 Sep 2022 14:50:08 +0200 Subject: [PATCH 09/22] Fix ArgumentOutOfRangeException for date type validation ARKADE-658 --- .../Base/Addml/Definitions/DataTypes/DateDataType.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Arkivverket.Arkade.Core/Base/Addml/Definitions/DataTypes/DateDataType.cs b/src/Arkivverket.Arkade.Core/Base/Addml/Definitions/DataTypes/DateDataType.cs index d26015664..a2baf3eed 100644 --- a/src/Arkivverket.Arkade.Core/Base/Addml/Definitions/DataTypes/DateDataType.cs +++ b/src/Arkivverket.Arkade.Core/Base/Addml/Definitions/DataTypes/DateDataType.cs @@ -106,6 +106,9 @@ private static string ConvertIso8601CalendarDateTimeStringToCSharpDateTimeString { string iso8601CalendarDateBasicRepresentation = Iso8601Format.ConvertToBasicRepresentation(iso8601DateTimeString); + if (iso8601CalendarDateBasicRepresentation.Length < 8) + return iso8601DateTimeString; + if (!(int.TryParse(iso8601CalendarDateBasicRepresentation[..4], out int year) && int.TryParse(iso8601CalendarDateBasicRepresentation[4..6], out int month) && int.TryParse(iso8601CalendarDateBasicRepresentation[6..8], out int day))) @@ -127,6 +130,9 @@ private static string ConvertIso8601OrdinalDateTimeStringToCSharpDateTimeString( { string iso8601OrdinalDateBasicRepresentation = Iso8601Format.ConvertToBasicRepresentation(iso8601DateTimeString); + if (iso8601OrdinalDateBasicRepresentation.Length < 7) + return iso8601DateTimeString; + if (!(int.TryParse(iso8601OrdinalDateBasicRepresentation[..4], out int year) && int.TryParse((iso8601OrdinalDateBasicRepresentation[4..7]), out int dayNumber))) return iso8601DateTimeString; @@ -157,6 +163,9 @@ private static string ConvertIso8601WeekDateTimeStringToCSharpDateTimeString(str { string iso8601WeekDateBasicRepresentation = Iso8601Format.ConvertToBasicRepresentation(iso8601DateTimeString); + if (iso8601WeekDateBasicRepresentation.Length < 8) + return iso8601DateTimeString; + if (!(int.TryParse(iso8601WeekDateBasicRepresentation[..4], out int year) && int.TryParse(iso8601WeekDateBasicRepresentation[5..7], out int weekOfYear) && int.TryParse(iso8601WeekDateBasicRepresentation[7].ToString(), out int dayNumber))) From 759530e8d1ab05742f09af5b81d4a4a57b5ecea7 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Thu, 8 Sep 2022 11:16:09 +0200 Subject: [PATCH 10/22] Add unit test for quoted date fields ARKADE-658 --- .../Addml/DelimiterFileFormatReaderTest.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs b/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs index 6a569483e..b0bb531b9 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Base/Addml/DelimiterFileFormatReaderTest.cs @@ -4,6 +4,7 @@ using Arkivverket.Arkade.Core.Base; using Arkivverket.Arkade.Core.Base.Addml; using Arkivverket.Arkade.Core.Base.Addml.Definitions; +using Arkivverket.Arkade.Core.Base.Addml.Definitions.DataTypes; using Arkivverket.Arkade.Core.Tests.Base.Addml.Builders; using FluentAssertions; using Xunit; @@ -307,5 +308,44 @@ public void QuotingCharEqualToFieldDelimiterShouldThrowException() actionOfCreatingRecordReader.Should().Throw(); } + + [Fact] + public void DateFieldsEncapsulatedByQuotesShouldBeValid() + { + const string d = ";"; + const string q = "\""; + AddmlFlatFileDefinition addmlFlatFileDefinition = new AddmlFlatFileDefinitionBuilder() + .WithRecordSeparator("CRLF") + .WithFieldSeparator(d) + .WithQuotingChar(q) + .Build(); + + AddmlRecordDefinition recordDefinition = new AddmlRecordDefinitionBuilder() + .WithAddmlFlatFileDefinition(addmlFlatFileDefinition).Build(); + + new AddmlFieldDefinitionBuilder() + .WithRecordDefinition(recordDefinition) + .WithDataType(new DateDataType("YYYY-MM-DD")) + .WithName("date") + .Build(); + new AddmlFieldDefinitionBuilder() + .WithRecordDefinition(recordDefinition) + .WithDataType(new DateDataType("YYYY-MM-DD")) + .WithName("date") + .Build(); + + const string csvData = $"{q}2022-09-06{q}{d}{q}2022-09-05{q}"; + + using var streamReader = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(csvData))); + var recordReader = new DelimiterFileFormatReader(new FlatFile(addmlFlatFileDefinition), streamReader); + var actionOfGettingCurrent = (Action)(() => ((Func)(() => recordReader.Current))()); + + recordReader.MoveNext(); + + actionOfGettingCurrent.Should().NotThrow(); + recordReader.Current?.Fields?.Count.Should().Be(2); + recordReader.Current?.Fields?[0].Value.Should().Be("2022-09-06"); + recordReader.Current?.Fields?[1].Value.Should().Be("2022-09-05"); + } } } From fbc8810a9272ceb471539380cbbbfe5bd9da018a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Tellnes?= Date: Fri, 30 Sep 2022 12:27:31 +0200 Subject: [PATCH 11/22] Update NuGet packages --- .../Arkivverket.Arkade.CLI.Tests.csproj | 8 ++++---- .../Arkivverket.Arkade.Core.Tests.csproj | 6 +++--- .../Arkivverket.Arkade.Core.csproj | 14 +++++++------- .../Arkivverket.Arkade.GUI.csproj | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj b/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj index fbc6ba149..6312c34bc 100644 --- a/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj +++ b/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj @@ -21,10 +21,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj index 123fc179b..bab9d8c86 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj +++ b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj @@ -52,12 +52,12 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj index 938b6045f..6b59b53c1 100644 --- a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj +++ b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -117,13 +117,13 @@ - - - + + + - - - + + + diff --git a/src/Arkivverket.Arkade.GUI/Arkivverket.Arkade.GUI.csproj b/src/Arkivverket.Arkade.GUI/Arkivverket.Arkade.GUI.csproj index dec032be9..1975ab2ed 100644 --- a/src/Arkivverket.Arkade.GUI/Arkivverket.Arkade.GUI.csproj +++ b/src/Arkivverket.Arkade.GUI/Arkivverket.Arkade.GUI.csproj @@ -39,7 +39,7 @@ - + From 2c008c36899fc4ad1432468abb8e1afd960c3d9c Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Mon, 24 Oct 2022 08:16:48 +0200 Subject: [PATCH 12/22] Improve feedback from archive format validation ARKADE-651 --- .../ArchiveFormatValidationMessages.Designer.cs | 15 +++++++++++++-- .../ArchiveFormatValidationMessages.nb-NO.resx | 7 ++++++- .../ArchiveFormatValidationMessages.resx | 9 +++++++-- .../ArchiveFormatValidationReport.cs | 8 +++++++- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs index 1886c2a73..ed5b162be 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs @@ -60,6 +60,16 @@ internal ArchiveFormatValidationMessages() { } } + /// + /// Looks up a localized string similar to Content of {0} has been validated against the selected archive format: + ///{1}. + /// + internal static string DirectoryValidationResultMessage { + get { + return ResourceManager.GetString("DirectoryValidationResultMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to The validation failed. See error log for details.. /// @@ -70,7 +80,8 @@ internal static string FileFormatValidationErrorMessage { } /// - /// Looks up a localized string similar to {0} conforms with the specified archive format: {1}. + /// Looks up a localized string similar to {0} conforms with the selected archive format: + ///{1}. /// internal static string ItemConformsWithFormat { get { @@ -79,7 +90,7 @@ internal static string ItemConformsWithFormat { } /// - /// Looks up a localized string similar to {0} does not conform with the specified archive format. + /// Looks up a localized string similar to {0} does not conform with the selected archive format. ///{1}. /// internal static string ItemDoesNotConformWithFormat { diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx index d1c67d3db..30409321d 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx @@ -121,7 +121,8 @@ Valideringen mislyktes. Se feillogg for detaljer. - {0} samsvarer med det angitte arkivformatet: {1} + {0} samsvarer med det angitte arkivformatet: +{1} {0} samsvarer ikke med det angitte arkivformatet. @@ -134,4 +135,8 @@ Ikke mulig å fastslå: {3} Detaljert rapport: {4} + + Innhold i {0} er validert mot det angitte arkivformat: +{1} + \ No newline at end of file diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx index 69368d454..e5889c4bb 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx @@ -101,10 +101,11 @@ The validation failed. See error log for details. - {0} conforms with the specified archive format: {1} + {0} conforms with the selected archive format: +{1} - {0} does not conform with the specified archive format. + {0} does not conform with the selected archive format. {1} @@ -114,4 +115,8 @@ Unable to determine: {3} Detailed report: {4} + + Content of {0} has been validated against the selected archive format: +{1} + \ No newline at end of file diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs index 7b3e0a956..361048085 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Arkivverket.Arkade.Core.Resources; using static System.Environment; @@ -24,6 +24,12 @@ public ArchiveFormatValidationReport(FileSystemInfo validatedItem, ArchiveFormat public string ValidationSummary() { + if (ValidatedItem is DirectoryInfo && ValidationFormat is ArchiveFormat.PdfA) + { + return string.Format( + ArchiveFormatValidationMessages.DirectoryValidationResultMessage, ValidatedItem.Name, ValidationInfo + ); + } return ValidationResult switch { Valid => string.Format( From 38759de281e4f35392ed523679556d1c9074010f Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Wed, 26 Oct 2022 13:25:44 +0200 Subject: [PATCH 13/22] Add DIAS-validator ARKADE-578 --- .../Options/ValidateOptions.cs | 12 +- src/Arkivverket.Arkade.CLI/Program.cs | 4 +- .../DiasValidation/DiasDirectoryTests.cs | 69 +++++++++ .../Util/DiasValidation/DiasValidatorTests.cs | 145 ++++++++++++++++++ ...Arkivverket.Arkade.Core.csproj.DotSettings | 1 + ...rchiveFormatValidationMessages.Designer.cs | 30 ++++ ...ArchiveFormatValidationMessages.nb-NO.resx | 12 ++ .../ArchiveFormatValidationMessages.resx | 12 ++ .../ArchiveFormatValidation/ArchiveFormat.cs | 8 + .../ArchiveFormatValidationReport.cs | 4 +- .../ArchiveFormatValidator.cs | 3 + .../Dias/DiasDirectory.cs | 107 +++++++++++++ .../ArchiveFormatValidation/Dias/DiasEntry.cs | 14 ++ .../ArchiveFormatValidation/Dias/DiasFile.cs | 16 ++ .../Dias/DiasProvider.cs | 130 ++++++++++++++++ .../Dias/DiasValidator.cs | 137 +++++++++++++++++ .../PdfA/PdfAValidator.cs | 2 +- .../Util/ArkadeConstants.cs | 7 +- .../Resources/ToolsGUI.Designer.cs | 2 +- .../Resources/ToolsGUI.nb-NO.resx | 2 +- .../Resources/ToolsGUI.resx | 2 +- .../ArchiveFormatValidationStatusDisplay.cs | 9 +- .../ViewModels/ToolsDialogViewModel.cs | 2 +- src/Arkivverket.Arkade.sln.DotSettings | 1 + 24 files changed, 717 insertions(+), 14 deletions(-) create mode 100644 src/Arkivverket.Arkade.Core.Tests/Util/ArchiveFormatValidation/DiasValidation/DiasDirectoryTests.cs create mode 100644 src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs create mode 100644 src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasDirectory.cs create mode 100644 src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasEntry.cs create mode 100644 src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasFile.cs create mode 100644 src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs create mode 100644 src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs diff --git a/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs b/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs index f04beafa0..2befbab9a 100644 --- a/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs +++ b/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs @@ -10,11 +10,11 @@ public class ValidateOptions [Option('i', "item", HelpText = "The file or directory to be validated", Required = true)] public string Item { get; set; } - [Option('f', "format", HelpText = "The format which the file or directory is validated against. Available values: PDF/A", Required = true)] + [Option('f', "format", HelpText = "The format which the file or directory is validated against. Available values: PDF/A, DIAS-SIP, DIAS-AIP, DIAS-SIP-Noark5, DIAS-AIP-Noark5", Required = true)] public string Format { get; set; } [Option('o', "output-directory", - HelpText = "Directory to place Arkade output files. Required if -i/--item is a directory.", + HelpText = "Directory to place Arkade output files. Required if -i/--item is a directory and -f/--format is PDF/A.", Required = false)] public string OutputDirectory { get; set; } @@ -30,6 +30,14 @@ public static IEnumerable Examples Item = "/path/to/pdfA-file", Format = "PDF/A" }); + + yield return new Example("Validate a specified tar or directory to fulfill the DIAS standard", + OptionsConfig.FormatStyle, + new ValidateOptions + { + Item = "/path/to/dias-tar-or-directory", + Format = "DIAS SIP" + }); } } } diff --git a/src/Arkivverket.Arkade.CLI/Program.cs b/src/Arkivverket.Arkade.CLI/Program.cs index e77adc387..03b6eaf30 100644 --- a/src/Arkivverket.Arkade.CLI/Program.cs +++ b/src/Arkivverket.Arkade.CLI/Program.cs @@ -134,9 +134,9 @@ private static bool ReadyToRun(ValidateOptions validateOptions) if (Directory.Exists(validateOptions.Item)) { - if (IsNullOrEmpty(validateOptions.OutputDirectory)) + if (archiveFormat == ArchiveFormat.PdfA && IsNullOrEmpty(validateOptions.OutputDirectory)) { - Log.Error(@"The -o/--output-directory argument is required when -i/--item is a directory."); + Log.Error(@"The -o/--output-directory argument is required when -i/--item is a directory and -f/--format is PDF/A."); return false; } diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/ArchiveFormatValidation/DiasValidation/DiasDirectoryTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/ArchiveFormatValidation/DiasValidation/DiasDirectoryTests.cs new file mode 100644 index 000000000..9f60fa678 --- /dev/null +++ b/src/Arkivverket.Arkade.Core.Tests/Util/ArchiveFormatValidation/DiasValidation/DiasDirectoryTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Arkivverket.Arkade.Core.Util.ArchiveFormatValidation; +using FluentAssertions; +using Xunit; + +namespace Arkivverket.Arkade.Core.Tests.Util.ArchiveFormatValidation.DiasValidation +{ + public class DiasDirectoryTests + { + [Fact] + public void GetEntryPathsTest() + { + var dias = + new DiasDirectory("SomeDiasDirectory", + new DiasDirectory("SomeDiasSubDirectory", + new DiasFile("SomeDiasFile.txt"))); + + IEnumerable entryPaths = dias.GetEntryPaths("SomeDiasDirectory", recursive: true); + + string expectedEntryPath = Path.Combine("SomeDiasDirectory", "SomeDiasSubDirectory", "SomeDiasFile.txt"); + + entryPaths.Should().Contain(e => e.Equals(expectedEntryPath)); + } + + [Fact] + public void GetMissingEntryPathsTest() + { + DirectoryInfo diasOnDisk = CreateTestDiasDirectory(); + + var dias = + new DiasDirectory(diasOnDisk.FullName, + new DiasDirectory("SomeDiasSubDirectory", + new DiasFile("SomeDiasFile.txt"), + new DiasFile("SomeOtherDiasFile.txt") // Not on disk + )); + + IEnumerable entryPaths = + dias.GetEntryPaths(diasOnDisk.FullName, getNonExistingOnly: true, recursive: true); + + string expectedMissingEntryPath = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + "SomeDiasDirectory", + "SomeDiasSubDirectory", + "SomeOtherDiasFile.txt" + ); + + entryPaths.Should().Contain(e => e.Equals(expectedMissingEntryPath)); + + diasOnDisk.Delete(true); + } + + private static DirectoryInfo CreateTestDiasDirectory() + { + DirectoryInfo diasRootDirectory = Directory.CreateDirectory( + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SomeDiasDirectory") + ); + + DirectoryInfo testSubDirectory = Directory.CreateDirectory( + Path.Combine(diasRootDirectory.FullName, "SomeDiasSubDirectory") + ); + + File.Create(Path.Combine(testSubDirectory.FullName, "SomeDiasFile.txt")).Dispose(); + + return diasRootDirectory; + } + } +} diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs new file mode 100644 index 000000000..0962c7298 --- /dev/null +++ b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Arkivverket.Arkade.Core.Util.ArchiveFormatValidation; +using FluentAssertions; +using ICSharpCode.SharpZipLib.Tar; +using Xunit; +using static Arkivverket.Arkade.Core.Util.ArkadeConstants; + +namespace Arkivverket.Arkade.Core.Tests.Util.DiasValidation +{ + public class DiasValidatorTests + { + private readonly DiasValidator _diasValidator = new(); + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateN5SipDirectory() + { + DiasDirectory diasDirectory = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSipN5); + + var testDirectory = new DirectoryInfo("testDirectory"); + + DiasProvider.Write(diasDirectory, testDirectory.FullName); + + _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasSipN5).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(testDirectory); + } + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldInvalidateN5SipDirectory() + { + DiasDirectory diasDirectory = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSip); + + var testDirectory = new DirectoryInfo("testDirectory"); + + DiasProvider.Write(diasDirectory, testDirectory.FullName); + + ArchiveFormatValidationReport report = _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasSipN5) + .Result; + + report.ValidationResult.Should().Be(ArchiveFormatValidationResult.Invalid); + report.ValidationSummary().Should().Contain(Path.Combine(DirectoryNameContent, ArkivstrukturXmlFileName)); + + Delete(testDirectory); + } + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateN5SipZipArchive() + { + DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSipN5).GetEntries(); + + FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); + + _diasValidator.ValidateAsync(diasTarArchive, ArchiveFormat.DiasSipN5).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(diasTarArchive); + } + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldInvalidateN5SipZipArchive() + { + DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSip).GetEntries(); + + FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); + + ArchiveFormatValidationReport report = + _diasValidator.ValidateAsync(diasTarArchive, ArchiveFormat.DiasSipN5).Result; + + report.ValidationResult.Should().Be(ArchiveFormatValidationResult.Invalid); + report.ValidationSummary().Should().Contain(Path.Combine(DirectoryNameContent, ArkivstrukturXmlFileName)); + + Delete(diasTarArchive); + } + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldReportInvalidButAcceptableDias() + { + DiasDirectory diasSource = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSipN5); + diasSource.DeleteEntry(ChangeLogXmlFileName, true); + + DiasEntry[] diasEntries = diasSource.GetEntries(); + FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); + + ArchiveFormatValidationReport report = + _diasValidator.ValidateAsync(diasTarArchive, ArchiveFormat.DiasSipN5).Result; + + report.ValidationResult.Should().Be(ArchiveFormatValidationResult.Invalid); + report.IsAcceptable.Should().BeTrue(); + report.ValidationSummary().Should().Contain(Path.Combine(DirectoryNameContent, ChangeLogXmlFileName)); + + Delete(diasTarArchive); + } + + private static FileInfo CreateDiasTarArchive(string fileName, IEnumerable diasEntries) + { + var outputStream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite); + + using var tarOutputStream = new TarOutputStream(outputStream, Encoding.Latin1); + + CreateDiasTarArchiveEntries(diasEntries, Path.GetFileNameWithoutExtension(fileName), tarOutputStream); + + return new FileInfo(fileName); + } + + private static void CreateDiasTarArchiveEntries(IEnumerable diasEntries, string path, TarOutputStream archive) + { + foreach (DiasEntry diasEntry in diasEntries) + { + var tarEntry = TarEntry.CreateTarEntry(Path.Join(path, diasEntry.Name)); + + if (diasEntry is DiasDirectory diasSubDirectory) + { + tarEntry.TarHeader.TypeFlag = TarHeader.LF_DIR; + + string diasSubDirectoryPath = Path.Join(path, diasSubDirectory.Name); + + CreateDiasTarArchiveEntries(diasSubDirectory.GetEntries(), diasSubDirectoryPath, archive); + } + + archive.PutNextEntry(tarEntry); + archive.CloseEntry(); + } + } + + private static void Delete(FileSystemInfo item) + { + if (item is DirectoryInfo directory) + directory.Delete(recursive: true); + + else item.Delete(); + + if (item.Exists) + throw new Exception("Unable to delete: " + item); + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj.DotSettings b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj.DotSettings index 37efae1d7..9883b4167 100644 --- a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj.DotSettings +++ b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj.DotSettings @@ -1,3 +1,4 @@  + True True True \ No newline at end of file diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs index ed5b162be..fc31317a6 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs @@ -60,6 +60,17 @@ internal ArchiveFormatValidationMessages() { } } + /// + /// Looks up a localized string similar to The National Archives of Norway accepts this DIAS structure. + ///However, the DIAS standard additionally requires the following item(s): + ///{0}. + /// + internal static string DiasAcceptableWithMissingEntries { + get { + return ResourceManager.GetString("DiasAcceptableWithMissingEntries", resourceCulture); + } + } + /// /// Looks up a localized string similar to Content of {0} has been validated against the selected archive format: ///{1}. @@ -99,6 +110,25 @@ internal static string ItemDoesNotConformWithFormat { } } + /// + /// Looks up a localized string similar to All mandatory files and directories were found. + /// + internal static string MandatoryDiasEntriesWereFound { + get { + return ResourceManager.GetString("MandatoryDiasEntriesWereFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing files/directories: + ///{0}. + /// + internal static string MissingDiasEntries { + get { + return ResourceManager.GetString("MissingDiasEntries", resourceCulture); + } + } + /// /// Looks up a localized string similar to Total number of validated files: {0} /// Valid files: {1} diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx index 30409321d..5faac67e1 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx @@ -127,6 +127,18 @@ {0} samsvarer ikke med det angitte arkivformatet. {1} + + + Manglende filer/kataloger: +{0} + + + Alle obligatoriske filer og kataloger ble funnet + + + Arkivverket aksepterer denne DIAS-strukturen. +For å oppfylle DIAS-standarden kreves imidlertid følgende element(er): +{0} Totalt antall filer: {0} diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx index e5889c4bb..539baa113 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx @@ -107,6 +107,18 @@ {0} does not conform with the selected archive format. {1} + + + Missing files/directories: +{0} + + + All mandatory files and directories were found + + + The National Archives of Norway accepts this DIAS structure. +However, the DIAS standard additionally requires the following item(s): +{0} Total number of validated files: {0} diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormat.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormat.cs index 129a97fd6..63f966dfb 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormat.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormat.cs @@ -6,5 +6,13 @@ public enum ArchiveFormat { [Description("PDF/A")] PdfA, + [Description("DIAS-SIP")] + DiasSip, + [Description("DIAS-AIP")] + DiasAip, + [Description("DIAS-SIP-NOARK5")] + DiasSipN5, + [Description("DIAS-AIP-NOARK5")] + DiasAipN5, } } diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs index 361048085..9a000c045 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidationReport.cs @@ -11,14 +11,16 @@ public class ArchiveFormatValidationReport public readonly FileSystemInfo ValidatedItem; public readonly ArchiveFormat ValidationFormat; public readonly ArchiveFormatValidationResult ValidationResult; + public readonly bool IsAcceptable; public readonly string ValidationInfo; public ArchiveFormatValidationReport(FileSystemInfo validatedItem, ArchiveFormat validationFormat, - ArchiveFormatValidationResult validationResult, string validationInfo = "") + ArchiveFormatValidationResult validationResult, bool isAcceptable = false, string validationInfo = "") { ValidationResult = validationResult; ValidationFormat = validationFormat; ValidatedItem = validatedItem; + IsAcceptable = isAcceptable; ValidationInfo = validationInfo; } diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidator.cs index eae9579db..93b3ecda6 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidator.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/ArchiveFormatValidator.cs @@ -7,10 +7,12 @@ namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation public class ArchiveFormatValidator : IArchiveFormatValidator { private readonly PdfAValidator _pdfAValidator; + private readonly DiasValidator _diasValidator; public ArchiveFormatValidator() { _pdfAValidator = new PdfAValidator(); + _diasValidator = new DiasValidator(); } public async Task ValidateAsync(FileSystemInfo item, ArchiveFormat format, string resultFileDirectoryPath="") @@ -18,6 +20,7 @@ public async Task ValidateAsync(FileSystemInfo it return format switch { ArchiveFormat.PdfA => await _pdfAValidator.ValidateAsync(item, resultFileDirectoryPath), + ArchiveFormat.DiasSip or ArchiveFormat.DiasAip or ArchiveFormat.DiasAipN5 or ArchiveFormat.DiasSipN5 => await _diasValidator.ValidateAsync(item, format), _ => throw new ArgumentOutOfRangeException($"No validator for {format}") }; } diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasDirectory.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasDirectory.cs new file mode 100644 index 000000000..1444b630d --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasDirectory.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation +{ + public class DiasDirectory : DiasEntry + { + private readonly HashSet _entries; + + public DiasDirectory(string directoryName, params DiasEntry[] diasEntries) : base(directoryName) + { + _entries = diasEntries.ToHashSet(); + } + + public void AddEntry(DiasEntry diasEntry) + { + _entries.Add(diasEntry); + } + + public void AddEntries(params DiasEntry[] diasEntries) + { + foreach (DiasEntry diasEntry in diasEntries) + AddEntry(diasEntry); + } + + public DiasDirectory GetSubDirectory(string subDirectoryName) + { + return (DiasDirectory)_entries.FirstOrDefault(e => e is DiasDirectory && e.Name.Equals(subDirectoryName)); + } + + public override bool ExistsAtPath(string path) + { + return Directory.Exists(Path.Combine(path, Name)); + } + + public DiasEntry[] GetEntries(bool recursive = false) + { + var entries = new List(); + + foreach (DiasEntry diasEntry in _entries) + { + entries.Add(diasEntry); + + if (recursive && diasEntry is DiasDirectory diasDirectory) + entries.AddRange(diasDirectory.GetEntries(true)); + } + + return entries.ToArray(); + } + + public void DeleteEntry(string entryName, bool recursive = false) + { + DiasEntry entry = _entries.FirstOrDefault(e => e.Name.Equals(entryName)); + if (entry != default(DiasEntry)) + _entries.Remove(entry); + + if (!recursive) + return; + + foreach (DiasEntry diasEntry in _entries.Where(e => e is DiasDirectory)) + ((DiasDirectory)diasEntry).DeleteEntry(entryName, true); + } + + public List GetEntryPaths(string basePath = "", bool getNonExistingOnly = false, bool recursive = false) + { + var entryPaths = new List(); + + foreach (DiasEntry diasEntry in _entries) + { + string entryPath = Path.Combine(basePath, diasEntry.Name); + + if (!(getNonExistingOnly && diasEntry.ExistsAtPath(basePath))) + entryPaths.Add(entryPath); + + if (recursive && diasEntry is DiasDirectory diasDirectory) + entryPaths.AddRange(diasDirectory.GetEntryPaths(entryPath, getNonExistingOnly)); + } + + return entryPaths; + } + + public void Merge(DiasDirectory directory) + { + + foreach (DiasEntry directoryEntry in directory._entries) + { + if (directoryEntry is DiasDirectory diasDirectory) + { + var existingEntry = (DiasDirectory)_entries.FirstOrDefault(e => e.Name.Equals(directoryEntry.Name)); + if (existingEntry == default(DiasEntry)) + { + _entries.Add(diasDirectory); + } + else + { + existingEntry.Merge(diasDirectory); + } + } + else + { + _entries.Add(directoryEntry); + } + } + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasEntry.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasEntry.cs new file mode 100644 index 000000000..9f2eae70d --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasEntry.cs @@ -0,0 +1,14 @@ +namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation +{ + public abstract class DiasEntry + { + public readonly string Name; + + public abstract bool ExistsAtPath(string path); + + protected DiasEntry(string name) + { + Name = name; + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasFile.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasFile.cs new file mode 100644 index 000000000..5a0416da3 --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasFile.cs @@ -0,0 +1,16 @@ +using System.IO; + +namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation +{ + public class DiasFile : DiasEntry + { + public DiasFile(string fileName) : base(fileName) + { + } + + public override bool ExistsAtPath(string path) + { + return File.Exists(Path.Combine(path, Name)); + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs new file mode 100644 index 000000000..4adaa6264 --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.IO; +using Arkivverket.Arkade.Core.Base; +using static Arkivverket.Arkade.Core.Util.ArkadeConstants; + +namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation +{ + public static class DiasProvider + { + public static DiasDirectory ProvideForFormat(ArchiveFormat format) + { + return format switch + { + ArchiveFormat.DiasSip => ProvideSipStructureFagsystem(), + ArchiveFormat.DiasAip => ProvideAipStructureFagsystem(), + ArchiveFormat.DiasSipN5 => ProvideSipStructureNoark5(), + ArchiveFormat.DiasAipN5 => ProvideAipStructureNoark5(), + _ => throw new ArkadeException($"Archive format type {format} is not implemented."), + }; + } + + public static void Write(DiasDirectory diasDirectory, string directoryName) + { + WriteEntries(diasDirectory.GetEntries(), Directory.CreateDirectory(directoryName).FullName); + } + + private static DiasDirectory ProvideSipStructureFagsystem() + { + DiasDirectory diasDirectory = GetSipStructureBase(); + + diasDirectory.GetSubDirectory(DirectoryNameAdministrativeMetadata).AddEntries( + new DiasFile(AddmlXmlFileName), + new DiasFile(AddmlXsdFileName)); + + return diasDirectory; + } + + private static DiasDirectory ProvideAipStructureFagsystem() + { + DiasDirectory diasDirectory = GetAipStructureBase(); + + diasDirectory.GetSubDirectory(DirectoryNameAdministrativeMetadata).AddEntries( + new DiasFile(AddmlXmlFileName), + new DiasFile(AddmlXsdFileName)); + + return diasDirectory; + } + + private static DiasDirectory ProvideSipStructureNoark5() + { + DiasDirectory diasDirectory = GetSipStructureBase(); + + diasDirectory.GetSubDirectory(DirectoryNameAdministrativeMetadata).AddEntries( + new DiasFile(ArkivuttrekkXmlFileName), + new DiasFile(AddmlXsdFileName)); + + diasDirectory.GetSubDirectory(DirectoryNameContent).AddEntries( + new DiasFile(ArkivstrukturXmlFileName), + new DiasFile(ArkivstrukturXsdFileName), + new DiasFile(ChangeLogXmlFileName), + new DiasFile(ChangeLogXsdFileName), + new DiasFile(RunningJournalXmlFileName), + new DiasFile(RunningJournalXsdFileName), + new DiasFile(PublicJournalXmlFileName), + new DiasFile(PublicJournalXsdFileName), + new DiasFile(MetadatakatalogXsdFileName)); + + return diasDirectory; + } + + private static DiasDirectory ProvideAipStructureNoark5() + { + DiasDirectory diasDirectory = GetAipStructureBase(); + + diasDirectory.Merge(ProvideSipStructureNoark5()); + + return diasDirectory; + } + + private static DiasDirectory GetSipStructureBase() + { + return new DiasDirectory("SipStructureBase", + new DiasFile(DiasMetsXmlFileName), + new DiasFile(DiasMetsXsdFileName), + new DiasDirectory(DirectoryNameDescriptiveMetadata), + new DiasDirectory(DirectoryNameAdministrativeMetadata, + new DiasFile(DiasPremisXmlFileName), + new DiasFile(DiasPremisXsdFileName)), + new DiasDirectory(DirectoryNameContent)); + } + + private static DiasDirectory GetAipStructureBase() + { + DiasEntry[] sipStructureBaseEntries = GetSipStructureBase().GetEntries(); + + var diasDirectory = new DiasDirectory("AipStructureBase", sipStructureBaseEntries); + + diasDirectory.GetSubDirectory(DirectoryNameDescriptiveMetadata).AddEntries( + new DiasFile(EadXmlFileName), + new DiasFile(EadXsdFileName), + new DiasFile(EacCpfXmlFileName), + new DiasFile(EacCpfXsdFileName)); + + diasDirectory.GetSubDirectory(DirectoryNameAdministrativeMetadata).AddEntry( + new DiasDirectory(DirectoryNameRepositoryOperations)); + + diasDirectory.GetSubDirectory(DirectoryNameContent).AddEntry( + new DiasFile(SystemhaandbokPdfFileName)); + + return diasDirectory; + } + + private static void WriteEntries(IEnumerable entries, string path) + { + foreach (DiasEntry entry in entries) + { + string entryName = Path.Join(path, entry.Name); + + if (entry is DiasFile) + File.Create(entryName); + + if (entry is DiasDirectory diasDirectory) + { + DirectoryInfo directory = Directory.CreateDirectory(entryName); + WriteEntries(diasDirectory.GetEntries(), directory.FullName); + } + } + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs new file mode 100644 index 000000000..be1e66b85 --- /dev/null +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Arkivverket.Arkade.Core.Base; +using ICSharpCode.SharpZipLib.Tar; +using static System.Environment; +using static Arkivverket.Arkade.Core.Resources.ArchiveFormatValidationMessages; +using static Arkivverket.Arkade.Core.Util.ArkadeConstants; +using static Arkivverket.Arkade.Core.Util.ArchiveFormatValidation.ArchiveFormatValidationResult; + +namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation +{ + public class DiasValidator + { + public async Task ValidateAsync(FileSystemInfo item, ArchiveFormat format) + { + List missingEntries = await GetMissingEntriesAsync(item, format); + + ArchiveFormatValidationResult result; + var resultIsAcceptable = true; + + if (missingEntries == null || !missingEntries.Any()) + result = Valid; + else + { + result = Invalid; + resultIsAcceptable = DetermineAcceptability(missingEntries, format); + + string rootPath = item.Extension == ".tar" + ? $"{Path.GetFileNameWithoutExtension(item.Name)}{Path.DirectorySeparatorChar}" + : item.FullName; + + // Excluding DIAS root directory name from entry paths + missingEntries = missingEntries.Select(e => Path.GetRelativePath(rootPath, e)).ToList(); + } + + string validationInfo = CreateValidationInfoString(result, resultIsAcceptable, missingEntries); + + return new ArchiveFormatValidationReport(item, format, result, resultIsAcceptable, validationInfo); + } + + private async Task> GetMissingEntriesAsync(FileSystemInfo item, ArchiveFormat format) + { + DiasDirectory dias = DiasProvider.ProvideForFormat(format); + + return item is FileInfo { Extension: ".tar" } tarArchive + ? await Task.Run(() => GetEntryPathsNotInTarArchiveAsync(tarArchive, dias)) + : await Task.Run(() => dias.GetEntryPaths(item.FullName, getNonExistingOnly: true, recursive: true)); + } + + private async Task> GetEntryPathsNotInTarArchiveAsync(FileInfo tarArchive, DiasDirectory validDias) + { + string tarArchiveRootDirectoryName = Path.GetFileNameWithoutExtension(tarArchive.Name); + List diasEntryPaths = validDias.GetEntryPaths(tarArchiveRootDirectoryName, recursive: true); + + await using var tarInputStream = new TarInputStream(File.OpenRead(tarArchive.FullName), Encoding.Latin1); + var tarEntryPaths = new HashSet(); + await Task.Run(() => + { + while (tarInputStream.GetNextEntry() is { } entry) + tarEntryPaths.Add(Path.TrimEndingDirectorySeparator(entry.Name.ForwardSlashed())); + }); + + return diasEntryPaths.Where(diasEntryPath => !tarEntryPaths.Contains(Path.TrimEndingDirectorySeparator(diasEntryPath.ForwardSlashed()))).ToList(); + } + + private static bool DetermineAcceptability(List missingEntries, ArchiveFormat format) + { + if (format is ArchiveFormat.DiasSip) + return false; + + if (format is ArchiveFormat.DiasAip) + return missingEntries.All(e => + e.Contains(EadXmlFileName) || e.Contains(EadXsdFileName) || + e.Contains(EacCpfXmlFileName) || e.Contains(EacCpfXsdFileName) || + e.Contains(SystemhaandbokPdfFileName)); + + var isMissingChangeLogXml = false; + var isMissingChangeLogXsd = false; + var isMissingRunningJournalXml = false; + var isMissingRunningJournalXsd = false; + var isMissingPublicJournalXml = false; + var isMissingPublicJournalXsd = false; + + foreach (string missingEntry in missingEntries) + { + // format will always be either DiasSipN5 or DiasAipN5 + if (!isMissingChangeLogXml && missingEntry.Contains(ChangeLogXmlFileName)) + isMissingChangeLogXml = true; + else if (!isMissingChangeLogXsd && missingEntry.Contains(ChangeLogXsdFileName)) + isMissingChangeLogXsd = true; + else if (!isMissingRunningJournalXml && missingEntry.Contains(RunningJournalXmlFileName)) + isMissingRunningJournalXml = true; + else if (!isMissingRunningJournalXsd && missingEntry.Contains(RunningJournalXsdFileName)) + isMissingRunningJournalXsd = true; + else if (!isMissingPublicJournalXml && missingEntry.Contains(PublicJournalXmlFileName)) + isMissingPublicJournalXml = true; + else if (!isMissingPublicJournalXsd && missingEntry.Contains(PublicJournalXsdFileName)) + isMissingPublicJournalXsd = true; + else if (missingEntry.Contains(EadXmlFileName) || missingEntry.Contains(EadXsdFileName) || + missingEntry.Contains(EacCpfXmlFileName) || missingEntry.Contains(EacCpfXsdFileName) || + missingEntry.Contains(SystemhaandbokPdfFileName)) + // ReSharper disable once RedundantJumpStatement + continue; + else + return false; + } + + if (!isMissingChangeLogXml && isMissingChangeLogXsd) + return false; + if (!isMissingRunningJournalXml && isMissingRunningJournalXsd) + return false; + if (!isMissingPublicJournalXml && isMissingPublicJournalXsd) + return false; + if (isMissingChangeLogXml || isMissingRunningJournalXml || isMissingPublicJournalXml) + return true; + + return false; + } + + private static string CreateValidationInfoString( + ArchiveFormatValidationResult resultType, + bool resultIsAcceptable, + IEnumerable missingEntries) + { + return resultType switch + { + Valid => NewLine + MandatoryDiasEntriesWereFound, + Invalid when resultIsAcceptable => string.Format(DiasAcceptableWithMissingEntries, $"\t{string.Join($"{NewLine}\t", missingEntries)}"), + Invalid => string.Format(MissingDiasEntries, $"\t{string.Join($"{NewLine}\t", missingEntries)}"), + _ => throw new ArkadeException($"Result type {resultType} is not implemented."), + }; + } + } +} diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs index ae1e0abd7..cafd4f31b 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/PdfA/PdfAValidator.cs @@ -56,7 +56,7 @@ public async Task ValidateAsync(FileSystemInfo it Log.Error("Validation failed: " + exception.Message); return new ArchiveFormatValidationReport( - item, ArchiveFormat.PdfA, Error, FileFormatValidationErrorMessage + item, ArchiveFormat.PdfA, Error, false, FileFormatValidationErrorMessage ); } } diff --git a/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs b/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs index 46802b3b6..8af18bcce 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArkadeConstants.cs @@ -1,4 +1,4 @@ -namespace Arkivverket.Arkade.Core.Util +namespace Arkivverket.Arkade.Core.Util { public class ArkadeConstants { @@ -12,7 +12,9 @@ public class ArkadeConstants public const string MetadatakatalogXsdFileName = "metadatakatalog.xsd"; public const string ArkadeXmlLogFileName = "arkade-log.xml"; public const string EadXmlFileName = "ead.xml"; + public const string EadXsdFileName = "ead.xsd"; public const string EacCpfXmlFileName = "eac-cpf.xml"; + public const string EacCpfXsdFileName = "eac-cpf.xsd"; public const string DiasPremisXmlFileName = "dias-premis.xml"; public const string DiasPremisXsdFileName = "dias-premis.xsd"; public const string DiasMetsXmlFileName = "dias-mets.xml"; @@ -26,6 +28,7 @@ public class ArkadeConstants public const string RunningJournalXsdFileName = "loependeJournal.xsd"; public const string ChangeLogXmlFileName = "endringslogg.xml"; public const string ChangeLogXsdFileName = "endringslogg.xsd"; + public const string SystemhaandbokPdfFileName = "systemhåndbok.pdf"; public const string SiardHeaderDirectoryName = "header"; public const string SiardMetadataXmlFileName = "metadata.xml"; @@ -59,6 +62,8 @@ public class ArkadeConstants public const string DirectoryNameRepositoryOperations = "repository_operations"; public const string DirectoryNameContent = "content"; public const string DirectoryNameAppDataArkadeSubFolder = "Arkivverket"; + public const string DirectoryNameDescriptiveMetadata = "descriptive_metadata"; + public const string DirectoryNameAdministrativeMetadata = "administrative_metadata"; public const string DirectoryNameThirdPartySoftware = "ThirdPartySoftware"; public const string DirectoryNameSiegfried = "Siegfried"; diff --git a/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.Designer.cs b/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.Designer.cs index dfd1b7d1b..ea8517f72 100644 --- a/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.Designer.cs +++ b/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.Designer.cs @@ -88,7 +88,7 @@ public static string ArchiveFormatValidationDirectorySelectDialogTitle { } /// - /// Looks up a localized string similar to Archive format files|*.pdf. + /// Looks up a localized string similar to Archive format files (*.pdf;*.tar)|*.pdf;*.tar. /// public static string ArchiveFormatValidationFileSelectDialogFilter { get { diff --git a/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.nb-NO.resx b/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.nb-NO.resx index c49dd645e..5445c097a 100644 --- a/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.nb-NO.resx +++ b/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.nb-NO.resx @@ -170,7 +170,7 @@ Resultat lagret i: Utfører validering av arkivformat ... - Arkivformat-filer|*.pdf + Arkivformat-filer (*.pdf;*.tar)|*.pdf;*.tar Velg katalog ... diff --git a/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.resx b/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.resx index fab3e9f5f..ad17a5350 100644 --- a/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.resx +++ b/src/Arkivverket.Arkade.GUI/Resources/ToolsGUI.resx @@ -170,7 +170,7 @@ Result saved at: Archive format - Archive format files|*.pdf + Archive format files (*.pdf;*.tar)|*.pdf;*.tar Choose directory ... diff --git a/src/Arkivverket.Arkade.GUI/ViewModels/ArchiveFormatValidationStatusDisplay.cs b/src/Arkivverket.Arkade.GUI/ViewModels/ArchiveFormatValidationStatusDisplay.cs index bb4909479..8091a889d 100644 --- a/src/Arkivverket.Arkade.GUI/ViewModels/ArchiveFormatValidationStatusDisplay.cs +++ b/src/Arkivverket.Arkade.GUI/ViewModels/ArchiveFormatValidationStatusDisplay.cs @@ -64,7 +64,7 @@ public void DisplayRunning() public void DisplayFinished(ArchiveFormatValidationReport validationReport) { Reset(); - ConfigureIconByValidationResult(validationReport.ValidationResult); + ConfigureIconByValidationResult(validationReport); ResultIconVisibility = Visibility.Visible; StatusMessage = validationReport.ValidationSummary(); } @@ -76,13 +76,16 @@ public void Reset() ProgressBarVisibility = Visibility.Collapsed; } - private void ConfigureIconByValidationResult(ArchiveFormatValidationResult result) + private void ConfigureIconByValidationResult(ArchiveFormatValidationReport result) { - (ResultIconKind, ResultIconColor) = result switch + (ResultIconKind, ResultIconColor) = result.ValidationResult switch { ArchiveFormatValidationResult.Valid => ("CheckBold", new SolidColorBrush(Colors.Teal)), + ArchiveFormatValidationResult.Invalid when result.IsAcceptable => + ("Information", new SolidColorBrush(Colors.RoyalBlue)), + ArchiveFormatValidationResult.Invalid => ("MinusCircleOutline", new SolidColorBrush(Colors.DarkRed)), diff --git a/src/Arkivverket.Arkade.GUI/ViewModels/ToolsDialogViewModel.cs b/src/Arkivverket.Arkade.GUI/ViewModels/ToolsDialogViewModel.cs index c0f331faf..f7a052191 100644 --- a/src/Arkivverket.Arkade.GUI/ViewModels/ToolsDialogViewModel.cs +++ b/src/Arkivverket.Arkade.GUI/ViewModels/ToolsDialogViewModel.cs @@ -337,7 +337,7 @@ out string fileToValidate private async void ValidateArchiveFormat() { var resultFileDirectoryPath = ""; - if (_archiveFormatValidationItem is DirectoryInfo) + if (ArchiveFormatValidationFormat == ArchiveFormat.PdfA.GetDescription() && _archiveFormatValidationItem is DirectoryInfo) { var canWriteToResultFileDirectory = false; diff --git a/src/Arkivverket.Arkade.sln.DotSettings b/src/Arkivverket.Arkade.sln.DotSettings index 37af92cae..1743f3dc1 100644 --- a/src/Arkivverket.Arkade.sln.DotSettings +++ b/src/Arkivverket.Arkade.sln.DotSettings @@ -185,6 +185,7 @@ True True True + True True True True From 7a09a388264cc76a846a0b9f11aec471ad951529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Tellnes?= Date: Thu, 27 Oct 2022 15:27:13 +0200 Subject: [PATCH 14/22] Improve DIAS-validator user feedback ARKADE-578 --- src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs | 2 +- src/Arkivverket.Arkade.CLI/Program.cs | 2 +- .../Resources/ArchiveFormatValidationMessages.Designer.cs | 3 +-- .../Resources/ArchiveFormatValidationMessages.nb-NO.resx | 3 +-- .../Resources/ArchiveFormatValidationMessages.resx | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs b/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs index 2befbab9a..3c745af4d 100644 --- a/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs +++ b/src/Arkivverket.Arkade.CLI/Options/ValidateOptions.cs @@ -31,7 +31,7 @@ public static IEnumerable Examples Format = "PDF/A" }); - yield return new Example("Validate a specified tar or directory to fulfill the DIAS standard", + yield return new Example("Validate the file/directory structure of a specified IP against DIAS", OptionsConfig.FormatStyle, new ValidateOptions { diff --git a/src/Arkivverket.Arkade.CLI/Program.cs b/src/Arkivverket.Arkade.CLI/Program.cs index 03b6eaf30..76830c983 100644 --- a/src/Arkivverket.Arkade.CLI/Program.cs +++ b/src/Arkivverket.Arkade.CLI/Program.cs @@ -136,7 +136,7 @@ private static bool ReadyToRun(ValidateOptions validateOptions) { if (archiveFormat == ArchiveFormat.PdfA && IsNullOrEmpty(validateOptions.OutputDirectory)) { - Log.Error(@"The -o/--output-directory argument is required when -i/--item is a directory and -f/--format is PDF/A."); + Log.Error(@"The -o/--output-directory argument is required when -f/--format is PDF/A and -i/--item is a directory."); return false; } diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs index fc31317a6..36bfebda5 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.Designer.cs @@ -61,8 +61,7 @@ internal ArchiveFormatValidationMessages() { } /// - /// Looks up a localized string similar to The National Archives of Norway accepts this DIAS structure. - ///However, the DIAS standard additionally requires the following item(s): + /// Looks up a localized string similar to The National Archives of Norway accepts this DIAS structure despite the following missing files/directories: ///{0}. /// internal static string DiasAcceptableWithMissingEntries { diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx index 5faac67e1..e297a8096 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.nb-NO.resx @@ -136,8 +136,7 @@ Alle obligatoriske filer og kataloger ble funnet - Arkivverket aksepterer denne DIAS-strukturen. -For å oppfylle DIAS-standarden kreves imidlertid følgende element(er): + Arkivverket aksepterer denne DIAS-strukturen på tross av følgende manglende filer/kataloger: {0} diff --git a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx index 539baa113..813d16713 100644 --- a/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx +++ b/src/Arkivverket.Arkade.Core/Resources/ArchiveFormatValidationMessages.resx @@ -116,8 +116,7 @@ All mandatory files and directories were found - The National Archives of Norway accepts this DIAS structure. -However, the DIAS standard additionally requires the following item(s): + The National Archives of Norway accepts this DIAS structure despite the following missing files/directories: {0} From 0c066ecbb6aa3d31c975db539c368d6d386f106a Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Fri, 28 Oct 2022 09:22:05 +0200 Subject: [PATCH 15/22] Move methods only used for testing to test assembly ARKADE-578 --- .../Util/DiasValidation/DiasValidatorTests.cs | 26 +++++++++++++++++-- .../Dias/DiasProvider.cs | 26 +------------------ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs index 0962c7298..741b33bc1 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs @@ -22,7 +22,7 @@ public void ShouldValidateN5SipDirectory() var testDirectory = new DirectoryInfo("testDirectory"); - DiasProvider.Write(diasDirectory, testDirectory.FullName); + Write(diasDirectory, testDirectory.FullName); _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasSipN5).Result .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); @@ -38,7 +38,7 @@ public void ShouldInvalidateN5SipDirectory() var testDirectory = new DirectoryInfo("testDirectory"); - DiasProvider.Write(diasDirectory, testDirectory.FullName); + Write(diasDirectory, testDirectory.FullName); ArchiveFormatValidationReport report = _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasSipN5) .Result; @@ -131,6 +131,28 @@ private static void CreateDiasTarArchiveEntries(IEnumerable diasEntri } } + private static void Write(DiasDirectory diasDirectory, string directoryName) + { + WriteEntries(diasDirectory.GetEntries(), Directory.CreateDirectory(directoryName).FullName); + } + + private static void WriteEntries(IEnumerable entries, string path) + { + foreach (DiasEntry entry in entries) + { + string entryName = Path.Join(path, entry.Name); + + if (entry is DiasFile) + File.Create(entryName); + + if (entry is DiasDirectory diasDirectory) + { + DirectoryInfo directory = Directory.CreateDirectory(entryName); + WriteEntries(diasDirectory.GetEntries(), directory.FullName); + } + } + } + private static void Delete(FileSystemInfo item) { if (item is DirectoryInfo directory) diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs index 4adaa6264..222e7cdbb 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasProvider.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.IO; -using Arkivverket.Arkade.Core.Base; +using Arkivverket.Arkade.Core.Base; using static Arkivverket.Arkade.Core.Util.ArkadeConstants; namespace Arkivverket.Arkade.Core.Util.ArchiveFormatValidation @@ -19,11 +17,6 @@ public static DiasDirectory ProvideForFormat(ArchiveFormat format) }; } - public static void Write(DiasDirectory diasDirectory, string directoryName) - { - WriteEntries(diasDirectory.GetEntries(), Directory.CreateDirectory(directoryName).FullName); - } - private static DiasDirectory ProvideSipStructureFagsystem() { DiasDirectory diasDirectory = GetSipStructureBase(); @@ -109,22 +102,5 @@ private static DiasDirectory GetAipStructureBase() return diasDirectory; } - - private static void WriteEntries(IEnumerable entries, string path) - { - foreach (DiasEntry entry in entries) - { - string entryName = Path.Join(path, entry.Name); - - if (entry is DiasFile) - File.Create(entryName); - - if (entry is DiasDirectory diasDirectory) - { - DirectoryInfo directory = Directory.CreateDirectory(entryName); - WriteEntries(diasDirectory.GetEntries(), directory.FullName); - } - } - } } } From 2edc1543083e43ba464eade7e41e1bcaadd3777c Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Fri, 28 Oct 2022 09:22:49 +0200 Subject: [PATCH 16/22] Fix IO-issues with tests ARKADE-578 --- .../Util/DiasValidation/DiasValidatorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs index 741b33bc1..d5a59a630 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs @@ -143,7 +143,7 @@ private static void WriteEntries(IEnumerable entries, string path) string entryName = Path.Join(path, entry.Name); if (entry is DiasFile) - File.Create(entryName); + using (File.Create(entryName)) { } //This is needed to release the resource before cleanup if (entry is DiasDirectory diasDirectory) { From 456feea1caf38e1556a0f609a82a07d24fffa5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Tellnes?= Date: Fri, 28 Oct 2022 11:26:40 +0200 Subject: [PATCH 17/22] Update NuGet packages ARKADE-668 --- .../Arkivverket.Arkade.CLI.Tests.csproj | 2 +- .../Arkivverket.Arkade.Core.Tests.csproj | 4 ++-- src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj b/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj index 6312c34bc..1b35a5bdd 100644 --- a/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj +++ b/src/Arkivverket.Arkade.CLI.Tests/Arkivverket.Arkade.CLI.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj index bab9d8c86..38b62140d 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj +++ b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj @@ -51,11 +51,11 @@ - + - + diff --git a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj index 6b59b53c1..fdab96b0a 100644 --- a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj +++ b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj @@ -118,14 +118,14 @@ - - + + - + From 0711c8822e289e8e0b8504a483b9e461e3dff684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Tellnes?= Date: Fri, 28 Oct 2022 11:47:33 +0200 Subject: [PATCH 18/22] Rollback NuGet package SharpZipLib ARKADE-668 --- .../Arkivverket.Arkade.Core.Tests.csproj | 2 +- src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj index 38b62140d..c9d476a43 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj +++ b/src/Arkivverket.Arkade.Core.Tests/Arkivverket.Arkade.Core.Tests.csproj @@ -55,7 +55,7 @@ - + diff --git a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj index fdab96b0a..dc1974a19 100644 --- a/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj +++ b/src/Arkivverket.Arkade.Core/Arkivverket.Arkade.Core.csproj @@ -125,7 +125,7 @@ - + From 1a02ceebd7bce03a3d436a5b2ee0126e817144d2 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Thu, 3 Nov 2022 12:04:05 +0100 Subject: [PATCH 19/22] DIAS-validator: Handle errors when loading .tar file ARKADE-578 --- .../ArchiveFormatValidation/Dias/DiasValidator.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs index be1e66b85..3dcfbb922 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -5,6 +6,7 @@ using System.Threading.Tasks; using Arkivverket.Arkade.Core.Base; using ICSharpCode.SharpZipLib.Tar; +using Serilog; using static System.Environment; using static Arkivverket.Arkade.Core.Resources.ArchiveFormatValidationMessages; using static Arkivverket.Arkade.Core.Util.ArkadeConstants; @@ -16,7 +18,17 @@ public class DiasValidator { public async Task ValidateAsync(FileSystemInfo item, ArchiveFormat format) { - List missingEntries = await GetMissingEntriesAsync(item, format); + List missingEntries; + + try + { + missingEntries = await GetMissingEntriesAsync(item, format); + } + catch (Exception e) + { + Log.Error(e, e.Message); + return new ArchiveFormatValidationReport(item, format, Error); + } ArchiveFormatValidationResult result; var resultIsAcceptable = true; From d4bed1153998e534ae53c66b3cbfa6e068b911ac Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Thu, 3 Nov 2022 12:04:22 +0100 Subject: [PATCH 20/22] Increase test coverage for DIAS-validator --- .../Util/DiasValidation/DiasValidatorTests.cs | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs index d5a59a630..ff2dcad8a 100644 --- a/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs +++ b/src/Arkivverket.Arkade.Core.Tests/Util/DiasValidation/DiasValidatorTests.cs @@ -14,6 +14,38 @@ public class DiasValidatorTests { private readonly DiasValidator _diasValidator = new(); + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateSipDirectory() + { + DiasDirectory diasDirectory = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSip); + + var testDirectory = new DirectoryInfo("testDirectory"); + + Write(diasDirectory, testDirectory.FullName); + + _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasSip).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(testDirectory); + } + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateAipDirectory() + { + DiasDirectory diasDirectory = DiasProvider.ProvideForFormat(ArchiveFormat.DiasAip); + + var testDirectory = new DirectoryInfo("testDirectory"); + + Write(diasDirectory, testDirectory.FullName); + + _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasAip).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(testDirectory); + } + [Fact] [Trait("Dependency", "IO")] public void ShouldValidateN5SipDirectory() @@ -30,6 +62,22 @@ public void ShouldValidateN5SipDirectory() Delete(testDirectory); } + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateN5AipDirectory() + { + DiasDirectory diasDirectory = DiasProvider.ProvideForFormat(ArchiveFormat.DiasAipN5); + + var testDirectory = new DirectoryInfo("testDirectory"); + + Write(diasDirectory, testDirectory.FullName); + + _diasValidator.ValidateAsync(testDirectory, ArchiveFormat.DiasAipN5).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(testDirectory); + } + [Fact] [Trait("Dependency", "IO")] public void ShouldInvalidateN5SipDirectory() @@ -49,6 +97,34 @@ public void ShouldInvalidateN5SipDirectory() Delete(testDirectory); } + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateSipZipArchive() + { + DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSip).GetEntries(); + + FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); + + _diasValidator.ValidateAsync(diasTarArchive, ArchiveFormat.DiasSip).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(diasTarArchive); + } + + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateAipZipArchive() + { + DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasAip).GetEntries(); + + FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); + + _diasValidator.ValidateAsync(diasTarArchive, ArchiveFormat.DiasAip).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(diasTarArchive); + } + [Fact] [Trait("Dependency", "IO")] public void ShouldValidateN5SipZipArchive() @@ -63,11 +139,25 @@ public void ShouldValidateN5SipZipArchive() Delete(diasTarArchive); } + [Fact] + [Trait("Dependency", "IO")] + public void ShouldValidateN5AipZipArchive() + { + DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasAipN5).GetEntries(); + + FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); + + _diasValidator.ValidateAsync(diasTarArchive, ArchiveFormat.DiasAipN5).Result + .ValidationResult.Should().Be(ArchiveFormatValidationResult.Valid); + + Delete(diasTarArchive); + } + [Fact] [Trait("Dependency", "IO")] public void ShouldInvalidateN5SipZipArchive() { - DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasSip).GetEntries(); + DiasEntry[] diasEntries = DiasProvider.ProvideForFormat(ArchiveFormat.DiasAip).GetEntries(); FileInfo diasTarArchive = CreateDiasTarArchive("testArchive.tar", diasEntries); From a072762db0c2828757e31025bf6f455c62e32cb7 Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Thu, 3 Nov 2022 13:47:34 +0100 Subject: [PATCH 21/22] Fix "acceptable" condition for DIAS-AIP-N5 ARKADE-578 --- .../Util/ArchiveFormatValidation/Dias/DiasValidator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs index 3dcfbb922..3d679fd60 100644 --- a/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs +++ b/src/Arkivverket.Arkade.Core/Util/ArchiveFormatValidation/Dias/DiasValidator.cs @@ -129,6 +129,12 @@ private static bool DetermineAcceptability(List missingEntries, ArchiveF if (isMissingChangeLogXml || isMissingRunningJournalXml || isMissingPublicJournalXml) return true; + if (format == ArchiveFormat.DiasAipN5) + return missingEntries.All(e => + e.Contains(EadXmlFileName) || e.Contains(EadXsdFileName) || + e.Contains(EacCpfXmlFileName) || e.Contains(EacCpfXsdFileName) || + e.Contains(SystemhaandbokPdfFileName)); + return false; } From f95dc9f77e57bdb7a0d04dc526ae0d655708d22c Mon Sep 17 00:00:00 2001 From: Leif Halvor Date: Mon, 7 Nov 2022 17:02:24 +0100 Subject: [PATCH 22/22] Add file extension -> MIMETYPE-mapper ARKADE-673 --- .../Metadata/DiasMetsCreator.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Arkivverket.Arkade.Core/Metadata/DiasMetsCreator.cs b/src/Arkivverket.Arkade.Core/Metadata/DiasMetsCreator.cs index 6af459a4b..3fae4d6ff 100644 --- a/src/Arkivverket.Arkade.Core/Metadata/DiasMetsCreator.cs +++ b/src/Arkivverket.Arkade.Core/Metadata/DiasMetsCreator.cs @@ -405,13 +405,10 @@ private static void CreateFileSec(mets mets, ArchiveMetadata metadata) foreach (FileDescription fileDescription in metadata.FileDescriptions) { - if (!Enum.TryParse($"application/{fileDescription.Extension}", true, out mdSecTypeMdRefMIMETYPE mimeType)) - mimeType = default; - metsFiles.Add(new fileType { ID = $"fileId_{fileDescription.Id}", - MIMETYPE = mimeType, + MIMETYPE = MimeTypeParser(fileDescription), USE = "Datafile", CHECKSUMTYPE = mdSecTypeMdRefCHECKSUMTYPE.SHA256, CHECKSUM = fileDescription.Sha256Checksum.ToLower(), @@ -434,5 +431,22 @@ private static void CreateFileSec(mets mets, ArchiveMetadata metadata) mets.fileSec = new metsTypeFileSec { fileGrp = new[] { metsTypeFileSecFileGrp } }; } + + private static mdSecTypeMdRefMIMETYPE MimeTypeParser(FileDescription fileDescription) + { + //https://mimetype.io/ + return fileDescription.Extension switch + { + "pdf" => mdSecTypeMdRefMIMETYPE.imagepdf, + "jpe" or "jpeg" or "jpg" or "pjpg" or "jfif" or "jfif-tbnl" or "jif" => mdSecTypeMdRefMIMETYPE.imagejpg, + "tiff" or "tif" => mdSecTypeMdRefMIMETYPE.imagetiff, + "xml" or "xpdl" or "xsl" or "gml" or "xsd" => mdSecTypeMdRefMIMETYPE.applicationxml, + "tar" => mdSecTypeMdRefMIMETYPE.applicationxtar, + "m2a" or "m3a" or "mp2" or "mp2a" or "mp3" or "mpga" => mdSecTypeMdRefMIMETYPE.audiomp3, + "m1v" or "m2v" or "mpa" or "mpe" or "mpeg" or "mpg" => mdSecTypeMdRefMIMETYPE.videompg, + "conf" or "def" or "diff" or "in" or "ksh" or "list" or "log" or "pl" or "text" or "txt" => mdSecTypeMdRefMIMETYPE.textplain, + _ => mdSecTypeMdRefMIMETYPE.textplain // todo: should have a 'undefined' fallback of sorts - not possible with current version (1.9) of DIAS-METS.xsd + }; + } } }