diff --git a/.editorconfig b/.editorconfig index 9ac4f101b7..5bfd224680 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,7 +45,7 @@ csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_within_query_expression_clauses = true csharp_wrap_before_eq = false place_attribute_on_same_line = "never" -csharp_style_namespace_declarations = file_scoped:warning +# csharp_style_namespace_declarations = file_scoped:warning # Indentation preferences csharp_indent_block_contents = true @@ -65,3 +65,10 @@ dotnet_code_quality_unused_parameters = all:warning dotnet_remove_unnecessary_suppression_exclusions = none:warning dotnet_style_namespace_match_folder = true:suggestion + +# unused usings +dotnet_diagnostic.IDE0005.severity = warning + +# CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. +# Consider applying the 'await' operator to the result of the call. +dotnet_diagnostic.CS4014.severity = error \ No newline at end of file diff --git a/documentation/Program.cs b/documentation/Program.cs index b5db76754b..0e2886a294 100644 --- a/documentation/Program.cs +++ b/documentation/Program.cs @@ -1,20 +1,19 @@ using System.Diagnostics; -static string GetSolutionParentFolder() +static string GetSolutionParentFolder() { var strExeFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location; - return Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(strExeFilePath))))!; + return Path.GetDirectoryName( + Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(strExeFilePath))))!; } static Process NpmCommand(string command) { - var startInfo = new ProcessStartInfo() + var startInfo = new ProcessStartInfo { - FileName = "npm", - Arguments = command, - WorkingDirectory = GetSolutionParentFolder() - }; - var proc = new Process() { StartInfo = startInfo }; + FileName = "npm", Arguments = command, WorkingDirectory = GetSolutionParentFolder() + }; + var proc = new Process { StartInfo = startInfo }; return proc; } diff --git a/documentation/docs/developer-guide/contributing/SECURITY.md b/documentation/docs/developer-guide/contributing/SECURITY.md index 0f91680a0d..fbcebc8a9e 100644 --- a/documentation/docs/developer-guide/contributing/SECURITY.md +++ b/documentation/docs/developer-guide/contributing/SECURITY.md @@ -7,7 +7,8 @@ Breaking changes are reported in the [changelog/history](https://docs.qdraw.nl/d | Version | Supported | |---------|--------------------| -| 0.5.x | :white_check_mark: | +| 0.6.x | :white_check_mark: | +| 0.5.x | :x: | | 0.4.x | :x: | | < 0.3 | :x: | diff --git a/documentation/static/openapi/openapi.json b/documentation/static/openapi/openapi.json index 184577234e..39a4ca64d6 100644 --- a/documentation/static/openapi/openapi.json +++ b/documentation/static/openapi/openapi.json @@ -4501,39 +4501,6 @@ "parameters": [], "extensions": {} }, - "/api/health/application-insights": { - "operations": { - "Get": { - "tags": [ - { - "name": "Health", - "extensions": {}, - "unresolvedReference": false - } - ], - "summary": "Add Application Insights script to user context", - "parameters": [], - "responses": { - "200": { - "description": "Ok", - "headers": {}, - "content": {}, - "links": {}, - "extensions": {}, - "unresolvedReference": false - } - }, - "callbacks": {}, - "deprecated": false, - "security": [], - "servers": [], - "extensions": {} - } - }, - "servers": [], - "parameters": [], - "extensions": {} - }, "/api/health/version": { "operations": { "Post": { @@ -11856,54 +11823,6 @@ "extensions": {}, "unresolvedReference": false }, - "applicationInsightsConnectionString": { - "type": "string", - "readOnly": false, - "writeOnly": false, - "allOf": [], - "oneOf": [], - "anyOf": [], - "required": [], - "properties": {}, - "additionalPropertiesAllowed": true, - "enum": [], - "nullable": true, - "deprecated": false, - "extensions": {}, - "unresolvedReference": false - }, - "applicationInsightsLog": { - "type": "boolean", - "readOnly": false, - "writeOnly": false, - "allOf": [], - "oneOf": [], - "anyOf": [], - "required": [], - "properties": {}, - "additionalPropertiesAllowed": true, - "enum": [], - "nullable": true, - "deprecated": false, - "extensions": {}, - "unresolvedReference": false - }, - "applicationInsightsDatabaseTracking": { - "type": "boolean", - "readOnly": false, - "writeOnly": false, - "allOf": [], - "oneOf": [], - "anyOf": [], - "required": [], - "properties": {}, - "additionalPropertiesAllowed": true, - "enum": [], - "nullable": true, - "deprecated": false, - "extensions": {}, - "unresolvedReference": false - }, "maxDegreesOfParallelism": { "type": "integer", "format": "int32", diff --git a/pipelines/azure/app-ci.yml b/pipelines/azure/app-ci.yml index 7d647b512f..b6fce7bb42 100644 --- a/pipelines/azure/app-ci.yml +++ b/pipelines/azure/app-ci.yml @@ -62,6 +62,7 @@ stages: - template: /pipelines/azure/steps/build_server.yml parameters: vstest: false + readyToRunEnabled: true runtimeArg: '--runtime "win-x64,linux-x64,osx-x64,osx-arm64"' nugetPackages: $(NUGET_PACKAGES) diff --git a/pipelines/azure/steps/build_server.yml b/pipelines/azure/steps/build_server.yml index 251e65153f..c43cd2bcd7 100644 --- a/pipelines/azure/steps/build_server.yml +++ b/pipelines/azure/steps/build_server.yml @@ -6,6 +6,9 @@ parameters: nugetPackages: $(Pipeline.Workspace)/.nuget/packages steps: + - template: /pipelines/azure/steps/cache_nuget_packages.yml + - template: /pipelines/azure/steps/cache_dependencies.yml + - task: DotNetCoreCLI@2 displayName: "[Nuke/server] dotnet tool restore" continueOnError: true @@ -14,11 +17,8 @@ steps: custom: tool arguments: "restore --tool-manifest starsky/.config/dotnet-tools.json" - - template: /pipelines/azure/steps/cache_nuget_packages.yml - - template: /pipelines/azure/steps/cache_dependencies.yml - - task: PowerShell@2 - displayName: "Nuke SonarBuildTest (with tests) " + displayName: "Nuke SonarBuildTest (with tests)" condition: and(succeeded(), eq('${{ parameters.vstest }}', true) ) env: BUILD_SOURCEBRANCH: $(Build.SourceBranch) diff --git a/pipelines/azure/steps/use_dotnet_version.yml b/pipelines/azure/steps/use_dotnet_version.yml index 26c0d4f540..83c2e853a2 100644 --- a/pipelines/azure/steps/use_dotnet_version.yml +++ b/pipelines/azure/steps/use_dotnet_version.yml @@ -1,6 +1,6 @@ steps: - task: UseDotNet@2 - displayName: 'Use .NET Core sdk 8.0.101' + displayName: 'Use .NET SDK 8.0.101' enabled: true inputs: packageType: sdk diff --git a/starsky-tools/build-tools/dotnet-sdk-version-update.js b/starsky-tools/build-tools/dotnet-sdk-version-update.js index e50e83c176..90af0cb8fb 100644 --- a/starsky-tools/build-tools/dotnet-sdk-version-update.js +++ b/starsky-tools/build-tools/dotnet-sdk-version-update.js @@ -419,7 +419,7 @@ async function updateAzureYmlFile(filePathList, sdkVersion) { fileContent = fileContent.replace( displayNameTagRegex, displayNameTag + - "'Use .NET Core sdk " + + "'Use .NET SDK " + sdkVersion + "'" ); diff --git a/starsky/Directory.Build.props b/starsky/Directory.Build.props index 4e2a2566b8..4bf5014ddb 100644 --- a/starsky/Directory.Build.props +++ b/starsky/Directory.Build.props @@ -1,9 +1,15 @@ - - $(OverwriteRuntimeIdentifier) - true - "Copyright (c) 2018 - $([System.DateTime]::Now.ToString(`yyyy`)) Qdraw and other authors" - Qdraw and contributors - Qdraw - + + $(OverwriteRuntimeIdentifier) + true + "Copyright (c) 2018 - $([System.DateTime]::Now.ToString(`yyyy`)) Qdraw and other authors" + Qdraw and contributors + Qdraw + + + true + $(NoWarn);CS1591;CS1573 + true + + \ No newline at end of file diff --git a/starsky/build/Build.cs b/starsky/build/Build.cs index 0d90de1b60..6ba7f2bb53 100644 --- a/starsky/build/Build.cs +++ b/starsky/build/Build.cs @@ -64,7 +64,7 @@ public sealed class Build : NukeBuild /// Nuget & NPM dependencies are always installed /// [Parameter("Skip Dependencies download e.g. exiftool / " + - "geo data, nuget/npm deps are always installed")] + "geo data, nuget/npm deps are always installed")] readonly bool NoDependencies; /// @@ -94,7 +94,7 @@ bool IsPublishDisabled() /// /// only if overwritten [Parameter("Overwrite branch name")] - readonly string Branch; + readonly string Branch = string.Empty; /// /// Overwrite Branch name @@ -104,7 +104,7 @@ string GetBranchName() { var branchName = Branch; if ( !string.IsNullOrEmpty(branchName) && - branchName.StartsWith("refs/heads/") ) + branchName.StartsWith("refs/heads/") ) { branchName = branchName.Replace("refs/heads/", ""); } @@ -147,7 +147,7 @@ List GetRuntimesWithoutGeneric() /// Solution .sln file /// [Solution(SuppressBuildProjectCheck = true)] - readonly Solution Solution; + readonly Solution Solution = new(); /// /// List of output projects @@ -236,7 +236,7 @@ void ShowSettingsInfo() $"Current RID: {RuntimeIdentifier.GetCurrentRuntimeIdentifier()}"); Log.Information("SolutionParentFolder: " + - WorkingDirectory.GetSolutionParentFolder()); + WorkingDirectory.GetSolutionParentFolder()); Log.Information(NoClient ? "Client is: disabled" @@ -251,8 +251,8 @@ void ShowSettingsInfo() : "Publish: enabled"); Log.Information(NoSonar || - string.IsNullOrEmpty(SonarQube.GetSonarKey()) || - string.IsNullOrEmpty(SonarQube.GetSonarToken()) + string.IsNullOrEmpty(SonarQube.GetSonarKey()) || + string.IsNullOrEmpty(SonarQube.GetSonarToken()) ? "Sonarcloud scan: disabled" : "Sonarcloud scan: enabled"); @@ -311,7 +311,7 @@ void ShowSettingsInfo() Configuration); DotnetTestHelper.TestNetCoreGenericCommand(Configuration, IsUnitTestDisabled()); - DotnetGenericHelper.DownloadDependencies(Configuration, GeoCliCsproj, + DotnetGenericHelper.DownloadDependencies(Configuration, GeoCliCsproj, NoDependencies, GenericRuntimeName); MergeCoverageFiles.Merge(IsUnitTestDisabled()); SonarQube.SonarEnd(IsUnitTestDisabled(), NoSonar); diff --git a/starsky/build/_build.csproj b/starsky/build/_build.csproj index 03c5c7511c..44aea6ec99 100644 --- a/starsky/build/_build.csproj +++ b/starsky/build/_build.csproj @@ -1,35 +1,35 @@ - - Exe - net8.0 - - CS0649;CS0169 - .. - .. - 1 - - true - + + Exe + net8.0 + + CS0649;CS0169 + .. + .. + 1 + enable + {b0a4bfd3-6321-4962-a15c-142aace9a4c9} + - - - - - - - + + + + + + + - - - + + + - - - + + + + + + + - - - - diff --git a/starsky/build/helpers/ClientHelper.cs b/starsky/build/helpers/ClientHelper.cs index a74c81b3fb..be47afe6d6 100644 --- a/starsky/build/helpers/ClientHelper.cs +++ b/starsky/build/helpers/ClientHelper.cs @@ -6,7 +6,7 @@ namespace helpers { - + public static class ClientHelper { public static string GetClientAppFolder() @@ -20,26 +20,26 @@ public static string GetClientAppFolder() throw new DirectoryNotFoundException("rootDirectory is null, this is wrong"); return Path.Combine(rootDirectory, ClientAppFolder); } - + public static void NpmPreflight() { Log.Information("Checking if Npm (and implicit: Node) is installed, will fail if not on this step"); Run(NpmBaseCommand, "-v"); - + Log.Information("Checking if Node is installed, will fail if not on this step"); Run(NodeBaseCommand, "-v"); - } - + } + public static void ClientCiCommand() { Run(NpmBaseCommand, "ci --legacy-peer-deps --prefer-offline --no-audit --no-fund", GetClientAppFolder()); } - + public static void ClientBuildCommand() { Run(NpmBaseCommand, "run build", GetClientAppFolder()); } - + public static void ClientTestCommand() { Run(NpmBaseCommand, "run test:ci", GetClientAppFolder()); diff --git a/starsky/build/helpers/CoverageReportHelper.cs b/starsky/build/helpers/CoverageReportHelper.cs index 0bf2d85bdc..4e107f02f6 100644 --- a/starsky/build/helpers/CoverageReportHelper.cs +++ b/starsky/build/helpers/CoverageReportHelper.cs @@ -4,7 +4,6 @@ namespace helpers { - public static class CoverageReportHelper { static void Information(string value) @@ -12,14 +11,14 @@ static void Information(string value) Log.Information(value); } - public static string GenerateHtml(bool noUnitTest) + public static string? GenerateHtml(bool noUnitTest) { if ( noUnitTest ) { Information(">> MergeCoverageFiles is disable due the --no-unit-test flag"); return null; } - + Information(">> Next: MergeCoverageFiles "); var rootDirectory = Directory.GetParent(AppDomain.CurrentDomain @@ -38,17 +37,15 @@ public static string GenerateHtml(bool noUnitTest) var args = new[] { - $"-reports:{outputCoverageFile}", - $"-targetdir:{reportFolder}", + $"-reports:{outputCoverageFile}", $"-targetdir:{reportFolder}", "-reporttypes:HtmlInline" }; Palmmedia.ReportGenerator.Core.Program.Main(args); - + Information(">> ReportGenerator done"); return reportFolder; } - } } diff --git a/starsky/build/helpers/DotnetGenericHelper.cs b/starsky/build/helpers/DotnetGenericHelper.cs index 38105987c1..e23212ec2a 100644 --- a/starsky/build/helpers/DotnetGenericHelper.cs +++ b/starsky/build/helpers/DotnetGenericHelper.cs @@ -122,7 +122,7 @@ public static void PublishNetCoreGenericCommand(Configuration configuration, static string BasePath() { return Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory) - ?.Parent?.Parent?.Parent?.FullName; + ?.Parent?.Parent?.Parent?.FullName!; } } } diff --git a/starsky/build/helpers/DotnetRuntimeSpecificHelper.cs b/starsky/build/helpers/DotnetRuntimeSpecificHelper.cs index f9ddc80a76..568eed030e 100644 --- a/starsky/build/helpers/DotnetRuntimeSpecificHelper.cs +++ b/starsky/build/helpers/DotnetRuntimeSpecificHelper.cs @@ -20,7 +20,7 @@ public static class DotnetRuntimeSpecificHelper static string BasePath() { return Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory) - ?.Parent?.Parent?.Parent?.FullName; + ?.Parent?.Parent?.Parent?.FullName!; } public static void Clean(List runtimesWithoutGeneric) @@ -37,7 +37,7 @@ public static void Clean(List runtimesWithoutGeneric) var runtimeZip = $"{ZipperHelper.ZipPrefix}{runtime}.zip"; Log.Information("\tRuntimeZip: " + runtimeZip + " exists: " + - File.Exists(runtimeZip)); + File.Exists(runtimeZip)); if ( File.Exists(runtimeZip) ) { File.Delete(runtimeZip); @@ -95,7 +95,7 @@ public static void CopyDependenciesFiles(bool noDependencies, // For Windows its not needed to copy unix dependencies if ( runtime.StartsWith("win") && - Directory.Exists(Path.Combine(runtimeTempFolder, "exiftool-unix")) ) + Directory.Exists(Path.Combine(runtimeTempFolder, "exiftool-unix")) ) { Directory.Delete(Path.Combine(runtimeTempFolder, "exiftool-unix"), true); Log.Information("removed exiftool-unix for windows"); @@ -103,7 +103,7 @@ public static void CopyDependenciesFiles(bool noDependencies, // ReSharper disable once InvertIf if ( runtime.StartsWith("win") && - File.Exists(Path.Combine(runtimeTempFolder, "exiftool.tar.gz")) ) + File.Exists(Path.Combine(runtimeTempFolder, "exiftool.tar.gz")) ) { File.Delete(Path.Combine(runtimeTempFolder, "exiftool.tar.gz")); Log.Information("removed exiftool.tar.gz for windows"); @@ -123,7 +123,7 @@ public static void BuildNetCoreCommand(Solution solution, Configuration configuration, string runtime, bool isReadyToRunEnabled) { Log.Information("> dotnet build next for: solution: " + solution + " runtime: " + - runtime); + runtime); var readyToRunArgument = RuntimeIdentifier.IsReadyToRunSupported(runtime) && isReadyToRunEnabled @@ -166,7 +166,7 @@ public static void PublishNetCoreGenericCommand(Configuration configuration, foreach ( var publishProject in Build.PublishProjectsList ) { Log.Information(">> next publishProject: " + - publishProject + " runtime: " + runtime); + publishProject + " runtime: " + runtime); var publishProjectFullPath = Path.Combine( WorkingDirectory.GetSolutionParentFolder(), diff --git a/starsky/build/helpers/DotnetTestHelper.cs b/starsky/build/helpers/DotnetTestHelper.cs index 1d753df7de..4a32d729f2 100644 --- a/starsky/build/helpers/DotnetTestHelper.cs +++ b/starsky/build/helpers/DotnetTestHelper.cs @@ -24,39 +24,39 @@ static bool DirectoryExists(string path) public static void TestNetCoreGenericCommand(Configuration configuration, bool noUnitTest) { Information(">> next: TestNetCoreGenericCommand"); - - if(noUnitTest) + + if ( noUnitTest ) { Information($">> TestNetCore is disable due the --no-unit-test flag"); return; } - + var projects = GetFilesHelper.GetFiles("*test/*.csproj"); if ( projects.Count == 0 ) { - throw new FileNotFoundException("missing tests in *test/*.csproj" ); + throw new FileNotFoundException("missing tests in *test/*.csproj"); } - - foreach(var project in projects) + + foreach ( var project in projects ) { var projectFullPath = Path.Combine(WorkingDirectory.GetSolutionParentFolder(), project); Information("Testing project " + project); - var testParentPath = Directory.GetParent(projectFullPath)?.FullName; + var testParentPath = Directory.GetParent(projectFullPath)?.FullName!; Information("testParentPath " + testParentPath); /* clean test results */ - var testResultsFolder = Path.Combine(testParentPath!, "TestResults"); - if (DirectoryExists(testResultsFolder)) + var testResultsFolder = Path.Combine(testParentPath, "TestResults"); + if ( DirectoryExists(testResultsFolder) ) { Information(">> Removing folder => " + testResultsFolder); - Directory.Delete(testResultsFolder,true); + Directory.Delete(testResultsFolder, true); } - + var runSettingsFile = Path.Combine( WorkingDirectory.GetSolutionParentFolder(), "build.vstest.runsettings"); - + Log.Information("runSettingsFile " + runSettingsFile); try @@ -66,7 +66,7 @@ public static void TestNetCoreGenericCommand(Configuration configuration, bool n .SetConfiguration(configuration) .EnableNoRestore() .EnableNoBuild() - .SetVerbosity(DotNetVerbosity.Normal) + .SetVerbosity(DotNetVerbosity.normal) .SetLoggers("trx;LogFileName=test_results.trx") .SetDataCollector("XPlat Code Coverage") .SetSettingsFile(runSettingsFile) @@ -83,7 +83,6 @@ public static void TestNetCoreGenericCommand(Configuration configuration, bool n throw; } - var coverageEnum = GetFilesHelper.GetFiles("**/coverage.opencover.xml"); foreach ( var coverageItem in coverageEnum ) @@ -92,16 +91,18 @@ public static void TestNetCoreGenericCommand(Configuration configuration, bool n } // Get the FirstOrDefault() but there is no LINQ here - var coverageFilePath = Path.Combine(testParentPath, "netcore-coverage.opencover.xml"); + var coverageFilePath = + Path.Combine(testParentPath, "netcore-coverage.opencover.xml"); Information("next copy: coverageFilePath " + coverageFilePath); - foreach(var item in coverageEnum) + foreach ( var item in coverageEnum ) { - CopyFile(Path.Combine(WorkingDirectory.GetSolutionParentFolder(), item), + CopyFile(Path.Combine(WorkingDirectory.GetSolutionParentFolder(), item), coverageFilePath, FileExistsPolicy.Overwrite); } - if (!FileExists(coverageFilePath)) { + if ( !FileExists(coverageFilePath) ) + { throw new FileNotFoundException("CoverageFile missing " + coverageFilePath); } } @@ -111,5 +112,5 @@ static bool FileExists(string path) { return File.Exists(path); } - } + } } diff --git a/starsky/build/helpers/GetFilesHelper.cs b/starsky/build/helpers/GetFilesHelper.cs index 6184f8ed0e..c579098608 100644 --- a/starsky/build/helpers/GetFilesHelper.cs +++ b/starsky/build/helpers/GetFilesHelper.cs @@ -11,7 +11,7 @@ public static class GetFilesHelper public static List GetFiles(string globSearch) { Matcher matcher = new(); - matcher.AddIncludePatterns( new List{globSearch}); + matcher.AddIncludePatterns(new List { globSearch }); var result = matcher.Execute( new DirectoryInfoWrapper( new DirectoryInfo(WorkingDirectory.GetSolutionParentFolder()))); diff --git a/starsky/build/helpers/HttpQuery.cs b/starsky/build/helpers/HttpQuery.cs index ed6bc7dafc..7bb478d542 100644 --- a/starsky/build/helpers/HttpQuery.cs +++ b/starsky/build/helpers/HttpQuery.cs @@ -4,17 +4,13 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; -using JetBrains.Annotations; using Serilog; - namespace helpers; public static class HttpQuery { - - [ItemCanBeNull] - public static async Task GetJsonFromApi(string apiUrl) + public static async Task GetJsonFromApi(string apiUrl) { try { @@ -28,27 +24,26 @@ public static async Task GetJsonFromApi(string apiUrl) // Read the content as a string return await response.Content.ReadAsStringAsync(); } - catch ( HttpRequestException exception) + catch ( HttpRequestException exception ) { Log.Information($"GetJsonFromApi {exception.StatusCode} {exception.Message}"); return null; } } - + public static Version ParseJsonVersionNumbers(string json) { var jsonDocument = JsonDocument.Parse(json); var rootElement = jsonDocument.RootElement; var versionsResult = new List(); - if (rootElement.TryGetProperty("versions", out var versionsElement)) + if ( rootElement.TryGetProperty("versions", out var versionsElement) ) { - versionsResult = versionsElement.EnumerateArray().Select(v => v.GetString()).ToList(); + versionsResult = versionsElement.EnumerateArray().Select(v => v.GetString()) + .Cast().ToList(); } - var hightestVersion = versionsResult.MaxBy(v => new Version(v)); + var hightestVersion = versionsResult.MaxBy(v => new Version(v))!; return new Version(hightestVersion); } } - - diff --git a/starsky/build/helpers/MergeCoverageFiles.cs b/starsky/build/helpers/MergeCoverageFiles.cs index fa29cf37a2..fc655ddac8 100644 --- a/starsky/build/helpers/MergeCoverageFiles.cs +++ b/starsky/build/helpers/MergeCoverageFiles.cs @@ -10,90 +10,97 @@ static void Information(string value) { Log.Information(value); } - + static bool FileExists(string value) { return File.Exists(value); } - + static void DeleteFile(string value) { File.Delete(value); } - + static void MoveFile(string from, string to) { - File.Move(from,to, true); + File.Move(from, to, true); } - + static void CopyFile(string from, string to) { - File.Copy(from,to, true); + File.Copy(from, to, true); } - + public static void Merge(bool noUnitTest) { var rootDirectory = Directory.GetParent(AppDomain.CurrentDomain .BaseDirectory!)!.Parent!.Parent!.Parent!.FullName; - - if(noUnitTest) + + if ( noUnitTest ) { Information($">> MergeCoverageFiles is disable due the --no-unit-test flag"); return; } var jestCoverageFile = Path.Combine(rootDirectory, "starsky/clientapp/coverage/cobertura-coverage.xml"); - if (! FileExists(jestCoverageFile)) { + if ( !FileExists(jestCoverageFile) ) + { throw new FileNotFoundException($"Missing jest coverage file {jestCoverageFile}"); } var netCoreCoverageFile = Path.Combine(rootDirectory, "starskytest/netcore-coverage.opencover.xml"); - if (! FileExists(netCoreCoverageFile)) { + if ( !FileExists(netCoreCoverageFile) ) + { throw new FileNotFoundException($"Missing .NET coverage file ${netCoreCoverageFile}"); } - var outputCoverageFile = Path.Combine(rootDirectory,"starskytest/coverage-merge-cobertura.xml"); + var outputCoverageFile = Path.Combine(rootDirectory, "starskytest/coverage-merge-cobertura.xml"); - if (FileExists(outputCoverageFile)) { + if ( FileExists(outputCoverageFile) ) + { DeleteFile(outputCoverageFile); } - var outputCoverageSonarQubeFile = Path.Combine(rootDirectory,"starskytest/coverage-merge-sonarqube.xml"); + var outputCoverageSonarQubeFile = Path.Combine(rootDirectory, "starskytest/coverage-merge-sonarqube.xml"); - if (FileExists(outputCoverageSonarQubeFile)) { + if ( FileExists(outputCoverageSonarQubeFile) ) + { DeleteFile(outputCoverageSonarQubeFile); } // Gets the coverage file from the client folder - if (FileExists($"./starsky/clientapp/coverage/cobertura-coverage.xml")) { + if ( FileExists($"./starsky/clientapp/coverage/cobertura-coverage.xml") ) + { Information($"Copy ./starsky/clientapp/coverage/cobertura-coverage.xml ./starskytest/jest-coverage.cobertura.xml"); CopyFile($"./starsky/clientapp/coverage/cobertura-coverage.xml", $"./starskytest/jest-coverage.cobertura.xml"); } - - var args = new [] + + var args = new[] { $"-reports:{rootDirectory}/starskytest/*coverage.*.xml", $"-targetdir:{rootDirectory}/starskytest/", "-reporttypes:Cobertura;SonarQube" }; - + Palmmedia.ReportGenerator.Core.Program.Main(args); // And rename it MoveFile($"{rootDirectory}/starskytest/Cobertura.xml", outputCoverageFile); MoveFile($"{rootDirectory}/starskytest/SonarQube.xml", outputCoverageSonarQubeFile); - - if (!FileExists(outputCoverageSonarQubeFile)) { + + if ( !FileExists(outputCoverageSonarQubeFile) ) + { throw new FileNotFoundException($"Missing Sonarqube coverage file {outputCoverageSonarQubeFile}"); } Information($"Sonarqube Coverage file is ready: {outputCoverageSonarQubeFile}"); - if (!FileExists(outputCoverageFile)) { + if ( !FileExists(outputCoverageFile) ) + { throw new FileNotFoundException($"Missing Cobertura coverage file {outputCoverageFile}"); } - + Information($"Cobertura Coverage file is ready: {outputCoverageFile}"); } } - + } diff --git a/starsky/build/helpers/ProjectCheckNetCoreCommandHelper.cs b/starsky/build/helpers/ProjectCheckNetCoreCommandHelper.cs index 908ab498ac..ac6a69efd8 100644 --- a/starsky/build/helpers/ProjectCheckNetCoreCommandHelper.cs +++ b/starsky/build/helpers/ProjectCheckNetCoreCommandHelper.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; +using Serilog; using static SimpleExec.Command; using static build.Build; namespace helpers { - public static class ProjectCheckNetCoreCommandHelper + public static partial class ProjectCheckNetCoreCommandHelper { static string GetBuildToolsFolder() { @@ -13,29 +16,86 @@ static string GetBuildToolsFolder() .BaseDirectory; if ( baseDirectory == null ) throw new DirectoryNotFoundException("base directory is null, this is wrong"); - var slnRootDirectory = Directory.GetParent(baseDirectory)?.Parent?.Parent?.Parent?.Parent?.FullName; + var slnRootDirectory = Directory.GetParent(baseDirectory)?.Parent?.Parent?.Parent + ?.Parent?.FullName; if ( slnRootDirectory == null ) throw new DirectoryNotFoundException("slnRootDirectory is null, this is wrong"); return Path.Combine(slnRootDirectory, BuildToolsPath); } - + + static void CheckForNullable() + { + var projects = GetFilesHelper.GetFiles("**.csproj"); + var missingProjects = new List(); + foreach ( var project in projects ) + { + var projectFullPath = + Path.Combine(WorkingDirectory.GetSolutionParentFolder(), project); + var projectContent = File.ReadAllText(projectFullPath); + + if ( !projectContent.Contains("enable") ) + { + missingProjects.Add(project); + } + } + + if ( missingProjects.Count > 0 ) + { + throw new ArgumentException("Missing enable in: " + + string.Join(" , ", missingProjects) + " projects " + + "Please add enable to the .csproj files"); + } + } + + static void ProjectGuid() + { + var projects = GetFilesHelper.GetFiles("**.csproj"); + var uniqueGuids = new List(); + + foreach ( var project in projects ) + { + var projectFullPath = + Path.Combine(WorkingDirectory.GetSolutionParentFolder(), project); + var projectContent = File.ReadAllText(projectFullPath); + + var fileXmlMatch = ProjectGuidRegex().Match(projectContent); + + if ( !fileXmlMatch.Success ) + { + throw new NotSupportedException($"✖ {project} - No ProjectGuid in file"); + } + + if ( uniqueGuids.Contains(fileXmlMatch.Groups[0].Value) ) + { + throw new NotSupportedException($"✖ {project} - ProjectGuid is not Unique"); + } + + uniqueGuids.Add(fileXmlMatch.Groups[0].Value); + Log.Information($"✓ {project} - Is Ok"); + } + } + public static void ProjectCheckNetCoreCommand() { + CheckForNullable(); + + ProjectGuid(); + ClientHelper.NpmPreflight(); // check branch names on CI // release-version-check.js triggers app-version-update.js to update the csproj and package.json files Run(NpmBaseCommand, "run release-version-check", GetBuildToolsFolder()); - - /* Checks for valid Project GUIDs in csproj files */ - Run(NpmBaseCommand, "run project-guid", GetBuildToolsFolder()); - + /* List of nuget packages */ Run(NpmBaseCommand, "run nuget-package-list", GetBuildToolsFolder()); } const string BuildToolsPath = "starsky-tools/build-tools/"; + + [GeneratedRegex( + "(){(([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}))}(<\\/ProjectGuid>)", + RegexOptions.IgnoreCase, "nl-NL")] + private static partial Regex ProjectGuidRegex(); } } - - diff --git a/starsky/build/helpers/RuntimeIdentifier.cs b/starsky/build/helpers/RuntimeIdentifier.cs index 985182872a..70976c669b 100644 --- a/starsky/build/helpers/RuntimeIdentifier.cs +++ b/starsky/build/helpers/RuntimeIdentifier.cs @@ -13,8 +13,8 @@ public static string GetCurrentRuntimeIdentifier() { os = "win"; } - if ( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || - RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) ) + if ( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD) ) { os = "linux"; } @@ -22,9 +22,9 @@ public static string GetCurrentRuntimeIdentifier() { os = "osx"; } - + var architecture = RuntimeInformation.OSArchitecture.ToString().ToLower(); - + return $"{os}-{architecture}"; } @@ -70,7 +70,7 @@ public static string GetCurrentRuntimeIdentifier() ["linux-x64", "linux-arm", "linux-arm64", "osx-x64", "osx-arm64"] } }; - + public static bool IsReadyToRunSupported(string toRuntimeIdentifier) { var currentIdentifier = GetCurrentRuntimeIdentifier(); @@ -79,7 +79,7 @@ public static bool IsReadyToRunSupported(string toRuntimeIdentifier) static bool IsReadyToRunSupported(string currentIdentifier, string toRuntimeIdentifier) { - if (SupportedPlatforms.TryGetValue(currentIdentifier, out var supportedTargets)) + if ( SupportedPlatforms.TryGetValue(currentIdentifier, out var supportedTargets) ) { return supportedTargets.Contains(toRuntimeIdentifier); } diff --git a/starsky/build/helpers/SonarQube.cs b/starsky/build/helpers/SonarQube.cs index 0166f90756..bec314f44f 100644 --- a/starsky/build/helpers/SonarQube.cs +++ b/starsky/build/helpers/SonarQube.cs @@ -18,33 +18,37 @@ namespace helpers public static class SonarQube { public const string SonarQubePackageName = "dotnet-sonarscanner"; + /// /// @see: https://www.nuget.org/packages/dotnet-sonarscanner /// public const string SonarQubePackageVersion = "6.1.0"; - - public const string SonarQubeDotnetSonarScannerApi = "https://api.nuget.org/v3-flatcontainer/dotnet-sonarscanner/index.json"; + + public const string SonarQubeDotnetSonarScannerApi = + "https://api.nuget.org/v3-flatcontainer/dotnet-sonarscanner/index.json"; public const string GitCommand = "git"; public const string DefaultBranchName = "master"; public static void InstallSonarTool(bool noUnitTest, bool noSonar) { - if(noUnitTest) + if ( noUnitTest ) { Information($">> SonarBegin is disable due the --no-unit-test flag"); return; } - if( noSonar ) { + + if ( noSonar ) + { Information($">> SonarBegin is disable due the --no-sonar flag"); return; } CheckLatestVersionDotNetSonarScanner().Wait(); - + var rootDirectory = Directory.GetParent(AppDomain.CurrentDomain .BaseDirectory!)!.Parent!.Parent!.Parent!.FullName; - + Log.Information(rootDirectory); var envs = @@ -52,8 +56,8 @@ public static void InstallSonarTool(bool noUnitTest, bool noSonar) IReadOnlyDictionary; var toolList = DotNet($"tool list", rootDirectory, envs, null, true); - if ( toolList.Any(p => p.Text.Contains(SonarQubePackageName) - && toolList.Any(p => p.Text.Contains(SonarQubePackageVersion)) )) + if ( toolList.Any(p => p.Text.Contains(SonarQubePackageName) + && toolList.Any(p => p.Text.Contains(SonarQubePackageVersion))) ) { Log.Information("Next: tool restore"); DotNet($"tool restore", rootDirectory, envs, null, true); @@ -72,7 +76,7 @@ public static void InstallSonarTool(bool noUnitTest, bool noSonar) .SetVersion(SonarQubePackageVersion)); } - private static string EnvironmentVariable(string input) + private static string? EnvironmentVariable(string input) { return Environment.GetEnvironmentVariable(input); } @@ -88,22 +92,22 @@ static async Task CheckLatestVersionDotNetSonarScanner() if ( result == null ) { Log.Information($"Nuget API is not available, " + - $"so skip checking the latest version of {SonarQubePackageName}"); + $"so skip checking the latest version of {SonarQubePackageName}"); return; } - + var latestVersionByApi = HttpQuery.ParseJsonVersionNumbers(result); if ( latestVersionByApi > new Version(SonarQubePackageVersion) ) { Log.Warning($"Please upgrade to the latest version " + - $"of dotnet-sonarscanner {latestVersionByApi} \n\n" + - "Update the following values: \n" + - $"- build/helpers/SonarQube.cs -> SonarQubePackageVersion to {latestVersionByApi} \n" + + $"of dotnet-sonarscanner {latestVersionByApi} \n\n" + + "Update the following values: \n" + + $"- build/helpers/SonarQube.cs -> SonarQubePackageVersion to {latestVersionByApi} \n" + "The _build project will auto update: \n" + - "- .config/dotnet-tools.json"); + "- .config/dotnet-tools.json"); } } - + private static void IsJavaInstalled() { @@ -111,46 +115,53 @@ private static void IsJavaInstalled() Run(Build.JavaBaseCommand, "-version"); } - public static string GetSonarToken() + public static string? GetSonarToken() { var sonarToken = EnvironmentVariable("STARSKY_SONAR_TOKEN"); - if( string.IsNullOrEmpty(sonarToken) ) { + if ( string.IsNullOrEmpty(sonarToken) ) + { sonarToken = EnvironmentVariable("SONAR_TOKEN"); } + return sonarToken; } - public static string GetSonarKey() + public static string? GetSonarKey() { return EnvironmentVariable("STARSKY_SONAR_KEY"); } - - public static bool SonarBegin(bool noUnitTest, bool noSonar, string branchName, string clientAppProject, string coverageFile) + + public static bool SonarBegin(bool noUnitTest, bool noSonar, string branchName, + string clientAppProject, string coverageFile) { - Information($">> SonarQube key={GetSonarKey()}"); - + var sonarToken = GetSonarToken(); var organisation = EnvironmentVariable("STARSKY_SONAR_ORGANISATION"); var url = EnvironmentVariable("STARSKY_SONAR_URL"); - if(string.IsNullOrEmpty(url)) { + if ( string.IsNullOrEmpty(url) ) + { url = "https://sonarcloud.io"; } - if( string.IsNullOrEmpty(GetSonarKey()) || string.IsNullOrEmpty(sonarToken) || string.IsNullOrEmpty(organisation) ) { - Information($">> SonarQube is disabled $ key={GetSonarKey()}|token={sonarToken}|organisation={organisation}"); + if ( string.IsNullOrEmpty(GetSonarKey()) || string.IsNullOrEmpty(sonarToken) || + string.IsNullOrEmpty(organisation) ) + { + Information( + $">> SonarQube is disabled $ key={GetSonarKey()}|token={sonarToken}|organisation={organisation}"); return false; } - if(noUnitTest) + if ( noUnitTest ) { Information($">> SonarBegin is disable due the --no-unit-test flag"); return false; } - if( noSonar ) { + if ( noSonar ) + { Information($">> SonarBegin is disable due the --no-sonar flag"); return false; } @@ -159,36 +170,40 @@ public static bool SonarBegin(bool noUnitTest, bool noSonar, string branchName, // Current branch name var mainRepoPath = Directory.GetParent(".")?.FullName; - - var (gitBranchName,_) = ReadAsync(GitCommand, " branch --show-current", mainRepoPath!).Result; + + var (gitBranchName, _) = + ReadAsync(GitCommand, " branch --show-current", mainRepoPath!).Result; // allow to overwrite the branch name - if (string.IsNullOrEmpty(branchName) && !string.IsNullOrEmpty(gitBranchName)) { + if ( string.IsNullOrEmpty(branchName) && !string.IsNullOrEmpty(gitBranchName) ) + { branchName = gitBranchName.Trim(); // fallback as (no branch) } - + // replace default value to master - if (branchName == "(no branch)" || string.IsNullOrEmpty(branchName)) { + if ( branchName == "(no branch)" || string.IsNullOrEmpty(branchName) ) + { branchName = DefaultBranchName; } - + /* this should fix No inputs were found in config file 'tsconfig.json'. */ - var tsconfig = Path.Combine(clientAppProject,"tsconfig.json"); + var tsconfig = Path.Combine(clientAppProject, "tsconfig.json"); Information(">> tsconfig: " + tsconfig); // For Pull Requests - var isPrBuild = EnvironmentVariable("GITHUB_ACTIONS") != null && - EnvironmentVariable("GITHUB_JOB") != null && - EnvironmentVariable("GITHUB_BASE_REF") != null && + var isPrBuild = EnvironmentVariable("GITHUB_ACTIONS") != null && + EnvironmentVariable("GITHUB_JOB") != null && + EnvironmentVariable("GITHUB_BASE_REF") != null && !string.IsNullOrEmpty(EnvironmentVariable("PR_NUMBER_GITHUB")); - + var githubPrNumber = EnvironmentVariable("PR_NUMBER_GITHUB"); - var githubBaseBranch = EnvironmentVariable("GITHUB_BASE_REF"); - var githubRepoSlug = EnvironmentVariable("GITHUB_REPOSITORY"); - + var githubBaseBranch = EnvironmentVariable("GITHUB_BASE_REF"); + var githubRepoSlug = EnvironmentVariable("GITHUB_REPOSITORY"); + Information($">> Selecting Branch: {branchName}"); - - var sonarQubeCoverageFile = Path.Combine(WorkingDirectory.GetSolutionParentFolder(), coverageFile); + + var sonarQubeCoverageFile = + Path.Combine(WorkingDirectory.GetSolutionParentFolder(), coverageFile); Information(">> SonarQubeCoverageFile: " + sonarQubeCoverageFile); var sonarArguments = new StringBuilder() @@ -199,37 +214,39 @@ public static bool SonarBegin(bool noUnitTest, bool noSonar, string branchName, .Append($"/k:{GetSonarKey()} ") .Append($"/n:Starsky ") .Append($"/d:sonar.token={sonarToken} ") - .Append($"/o:" + organisation +" ") + .Append($"/o:" + organisation + " ") .Append($"/d:sonar.typescript.tsconfigPath={tsconfig} ") .Append($"/d:sonar.coverageReportPaths={sonarQubeCoverageFile} ") .Append($"/d:sonar.exclusions=**/build/*,**/build/helpers/*," + - "**/documentation/*,"+ - "**/Interfaces/IQuery.cs," + - $"**/setupTests.js,**/react-app-env.d.ts,**/service-worker.ts," + - $"*webhtmlcli/**/*.js,**/wwwroot/js/**/*,**/*/Migrations/*,**/*spec.tsx," + - $"**/*stories.tsx,**/*spec.ts,**/src/main.tsx,**/src/index.tsx,**/src/style/css/vendor/*,**/node_modules/*," + - $"**/prestorybook.js,**/vite.config.ts,**/.storybook/**,**/jest.setup.ts," + - $"**/_bigimages-helper.js ") + "**/documentation/*," + + "**/Interfaces/IQuery.cs," + + $"**/setupTests.js,**/react-app-env.d.ts,**/service-worker.ts," + + $"*webhtmlcli/**/*.js,**/wwwroot/js/**/*,**/*/Migrations/*,**/*spec.tsx," + + $"**/*stories.tsx,**/*spec.ts,**/src/main.tsx,**/src/index.tsx,**/src/style/css/vendor/*,**/node_modules/*," + + $"**/prestorybook.js,**/vite.config.ts,**/.storybook/**,**/jest.setup.ts," + + $"**/_bigimages-helper.js ") .Append($"/d:sonar.coverage.exclusions=**/build/*,**/build/helpers/*," + - "**/documentation/*,"+ - "**/Interfaces/IQuery.cs," + - $"**/setupTests.js,**/react-app-env.d.ts,**/service-worker.ts," + - $"*webhtmlcli/**/*.js,**/wwwroot/js/**/*,**/*/Migrations/*," + - $"**/*spec.ts,**/*stories.tsx,**/*spec.tsx,**/src/main.tsx,**/src/index.tsx,**/node_modules/*," + - $"**/prestorybook.js,**/vite.config.ts,**/.storybook/**,**/jest.setup.ts," + - $"**/_bigimages-helper.js "); - + "**/documentation/*," + + "**/Interfaces/IQuery.cs," + + $"**/setupTests.js,**/react-app-env.d.ts,**/service-worker.ts," + + $"*webhtmlcli/**/*.js,**/wwwroot/js/**/*,**/*/Migrations/*," + + $"**/*spec.ts,**/*stories.tsx,**/*spec.tsx,**/src/main.tsx,**/src/index.tsx,**/node_modules/*," + + $"**/prestorybook.js,**/vite.config.ts,**/.storybook/**,**/jest.setup.ts," + + $"**/_bigimages-helper.js "); + // Normal build - if (!isPrBuild) { + if ( !isPrBuild ) + { Information($">> Normal Build (non-pr)"); sonarArguments .Append($"/d:sonar.branch.name={branchName} "); } - + // Pull Request Build - if (isPrBuild) { + if ( isPrBuild ) + { Information($">> PR Build isPRBuild={true} githubPrNumber " + - $"{githubPrNumber} githubBaseBranch {githubBaseBranch} githubRepoSlug {githubRepoSlug}"); + $"{githubPrNumber} githubBaseBranch {githubBaseBranch} githubRepoSlug {githubRepoSlug}"); sonarArguments .Append($"/d:sonar.pullrequest.key={githubPrNumber} ") @@ -247,17 +264,20 @@ public static bool SonarBegin(bool noUnitTest, bool noSonar, string branchName, public static void SonarEnd(bool noUnitTest, bool noSonar) { var sonarToken = GetSonarToken(); - if( string.IsNullOrEmpty(sonarToken) ) { + if ( string.IsNullOrEmpty(sonarToken) ) + { Information($">> SonarQube is disabled $ login={sonarToken}"); return; } - if(noUnitTest) + if ( noUnitTest ) { Information($">> SonarEnd is disable due the --no-unit-test flag"); return; } - if( noSonar ) { + + if ( noSonar ) + { Information($">> SonarEnd is disable due the --no-sonar flag"); return; } @@ -268,8 +288,8 @@ public static void SonarEnd(bool noUnitTest, bool noSonar) .Append($"/d:sonar.token={sonarToken} "); DotNet(sonarArguments.ToString()); - + Log.Information("- - - - - - - - - - Sonar done - - - - - - - - - - \n"); } - } + } } diff --git a/starsky/build/helpers/TrxParserHelper.cs b/starsky/build/helpers/TrxParserHelper.cs index 6e344a7628..2bf89047bc 100644 --- a/starsky/build/helpers/TrxParserHelper.cs +++ b/starsky/build/helpers/TrxParserHelper.cs @@ -40,7 +40,7 @@ public static void DisplayFileTests(string trxFullFilePath) return; } - var results = new List>(); + var results = new List>(); foreach ( var unitTestResult in unitTestResultsList ) { if ( unitTestResult.Attribute("outcome")?.Value != "Failed" ) @@ -59,7 +59,7 @@ public static void DisplayFileTests(string trxFullFilePath) ?.Element(TeamTestNamespace + "StackTrace")?.Value; results.Add( - new Tuple(testName, message, + new Tuple(testName, message, stackTrace)); } diff --git a/starsky/build/helpers/WorkingDirectory.cs b/starsky/build/helpers/WorkingDirectory.cs index 44f23b7ee9..30e3f24147 100644 --- a/starsky/build/helpers/WorkingDirectory.cs +++ b/starsky/build/helpers/WorkingDirectory.cs @@ -4,9 +4,10 @@ namespace helpers; public static class WorkingDirectory { - public static string GetSolutionParentFolder() + public static string GetSolutionParentFolder() { var strExeFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location; - return Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(strExeFilePath)))); + return Path.GetDirectoryName( + Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(strExeFilePath))))!; } } diff --git a/starsky/build/helpers/ZipperHelper.cs b/starsky/build/helpers/ZipperHelper.cs index 1479c972a3..6bce0f25b1 100644 --- a/starsky/build/helpers/ZipperHelper.cs +++ b/starsky/build/helpers/ZipperHelper.cs @@ -7,11 +7,11 @@ namespace helpers { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "S1118:Add a 'protected' constructor " + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", + "S1118:Add a 'protected' constructor " + "or the 'static' keyword to the class declaration", Justification = "Not production code.")] public sealed class ZipperHelper { - public const string ZipPrefix = "starsky-"; static string BasePath() @@ -19,14 +19,15 @@ static string BasePath() return Directory.GetParent(AppDomain.CurrentDomain .BaseDirectory)!.Parent!.Parent!.Parent!.FullName; } - + public static void ZipGeneric() { var fromFolder = Path.Join(BasePath(), Build.GenericRuntimeName); - + if ( !Directory.Exists(fromFolder) ) { - throw new DirectoryNotFoundException($"dir {Build.GenericRuntimeName} not found {fromFolder}"); + throw new DirectoryNotFoundException( + $"dir {Build.GenericRuntimeName} not found {fromFolder}"); } var zipPath = Path.Join(BasePath(), @@ -36,18 +37,18 @@ public static void ZipGeneric() { File.Delete(zipPath); } - + Log.Information($"next: {Build.GenericRuntimeName} zip ~ {fromFolder} -> {zipPath}"); - ZipFile.CreateFromDirectory(fromFolder, + ZipFile.CreateFromDirectory(fromFolder, zipPath); } - + public static void ZipRuntimes(List getRuntimesWithoutGeneric) { if ( getRuntimesWithoutGeneric.Count == 0 ) { Log.Information("There are no runtime specific items selected\n" + - "So skip ZipRuntimes"); + "So skip ZipRuntimes"); return; } @@ -57,37 +58,38 @@ public static void ZipRuntimes(List getRuntimesWithoutGeneric) if ( !Directory.Exists(runtimeFullPath) ) { - throw new DirectoryNotFoundException($"dir {Build.GenericRuntimeName} not found ~ {runtimeFullPath}"); + throw new DirectoryNotFoundException( + $"dir {Build.GenericRuntimeName} not found ~ {runtimeFullPath}"); } var zipPath = Path.Join(BasePath(), ZipPrefix + runtime + ".zip"); - + if ( File.Exists(zipPath) ) { File.Delete(zipPath); } Log.Information($"next: {runtime} zip ~ {runtimeFullPath} -> {zipPath}"); - + ZipFile.CreateFromDirectory(runtimeFullPath, zipPath); } } const string CoverageReportZip = "coverage-report.zip"; - - public static void ZipHtmlCoverageReport(string fromFolder, + + public static void ZipHtmlCoverageReport(string? fromFolder, bool noUnitTest) { if ( noUnitTest ) { Log.Information(">> ZipHtmlCoverageReport " + - "is disable due the --no-unit-test flag\n" + - "So skip ZipHtmlCoverageReport"); + "is disable due the --no-unit-test flag\n" + + "So skip ZipHtmlCoverageReport"); return; } - - if ( !Directory.Exists(fromFolder) ) + + if ( string.IsNullOrEmpty(fromFolder) || !Directory.Exists(fromFolder) ) { throw new DirectoryNotFoundException($"dir {fromFolder} not found"); } @@ -98,9 +100,9 @@ public static void ZipHtmlCoverageReport(string fromFolder, { File.Delete(zipPath); } - + Log.Information($"next: zip {fromFolder} -> {zipPath}"); - ZipFile.CreateFromDirectory(fromFolder, + ZipFile.CreateFromDirectory(fromFolder, zipPath); } } diff --git a/starsky/docs/migrations.md b/starsky/docs/migrations.md index d5dc298980..4e16a88843 100644 --- a/starsky/docs/migrations.md +++ b/starsky/docs/migrations.md @@ -1,66 +1,67 @@ - # Add Migrations + This document describes how to add a migration to the application. ## Install Dotnet EF as global installer + ```bash dotnet tool install -g dotnet-ef ``` ## Or update to latest version + ```bash dotnet tool update --global dotnet-ef ``` ```bash # https://www.nuget.org/packages/dotnet-ef#versions-body-tab -dotnet tool update --global dotnet-ef --version 6.0.12 +dotnet tool update --global dotnet-ef --version 8.0.1 ``` -## Set constance for EF Core -Define constance in `starsky.foundation.database.csproj` -```xml - ENABLE_DEFAULT_DATABASE - -``` -or mysql: +## Set constance for EF Core (optional) + +Define constance in `starsky.foundation.database.csproj` (to remove it after the migration) + +only when used mysql: + ```xml - ENABLE_MYSQL_DATABASE + +ENABLE_MYSQL_DATABASE ``` ## Run Migration + +> Please think about this issue: +> `warning CS8981: The type name 'limitdataprotectionkeyslength' only contains lower-cased ascii characters.` +> `Such names may become reserved for the language` + +Run the following command: + ```bash cd starsky/starsky.foundation.database -dotnet ef --startup-project ../starsky/starsky.csproj --project starsky.foundation.database.csproj migrations add test +dotnet ef --project starsky.foundation.database.csproj migrations add test ``` The migration should be ready :) -You should test **both** with MySQL and SQLite. +You should test **both** with MySQL and SQLite. For MySql its the easiest to run the database and/or the application with docker-compose. +We use this for the migration: +https://learn.microsoft.com/en-us/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-a-design-time-factory + +## Remove + +remove the following value if added: + +``` + ENABLE_MYSQL_DATABASE +``` ## undo latest migration ```bash cd starsky/starsky.foundation.database -dotnet ef --startup-project ../starsky/starsky.csproj --project starsky.foundation.database.csproj migrations remove --force +dotnet ef --project starsky.foundation.database.csproj migrations remove --force ``` -## Instead of setting constance (is replaced by defined constance) - -This is not needed anymore but kept here for explaining what is done in the code - -(Optional) : See code : SetupDatabaseTypes.cs - -```c# - // dirty hack - _services.AddDbContext(options => - options.UseSqlite(_appSettings.DatabaseConnection, - b => - { - if (! string.IsNullOrWhiteSpace(foundationDatabaseName) ) - { - b.MigrationsAssembly(foundationDatabaseName); - } - })); -``` \ No newline at end of file diff --git a/starsky/format.sh b/starsky/format.sh new file mode 100755 index 0000000000..56b7b791db --- /dev/null +++ b/starsky/format.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Loop over the output of the find command +while IFS= read -r file; do + # Process each file here, for example, print the file name + echo "Processing file: $file" + dotnet format $file + +done < <(find . -iname '*.csproj') diff --git a/starsky/nuget-packages-list.json b/starsky/nuget-packages-list.json index 2db87b4e9a..777e60b6ad 100644 --- a/starsky/nuget-packages-list.json +++ b/starsky/nuget-packages-list.json @@ -14,11 +14,8 @@ "MedallionShell 1.6.2", "RazorLight 2.3.1", "SixLabors.ImageSharp 3.1.2", - "SixLabors.ImageSharp.Drawing 2.1.0", + "SixLabors.ImageSharp.Drawing 2.1.1", "Microsoft.AspNetCore.Authorization 8.0.1", - "Microsoft.ApplicationInsights.WorkerService 2.22.0", - "Microsoft.Extensions.DependencyInjection.Abstractions 8.0.0", - "Microsoft.Extensions.Logging 8.0.0", "Microsoft.EntityFrameworkCore.Analyzers 8.0.1", "Microsoft.EntityFrameworkCore.Design 8.0.1", "Microsoft.EntityFrameworkCore.InMemory 8.0.1", @@ -29,8 +26,7 @@ "Microsoft.Extensions.Identity.Stores 8.0.1", "Pomelo.EntityFrameworkCore.MySql 8.0.0-beta.2", "System.ComponentModel.Annotations 5.0.0", - "Microsoft.ApplicationInsights 2.22.0", - "Microsoft.ApplicationInsights.DependencyCollector 2.22.0", + "Microsoft.Extensions.DependencyInjection.Abstractions 8.0.0", "Microsoft.CSharp 4.7.0", "Microsoft.Extensions.Configuration 8.0.0", "Microsoft.Extensions.Configuration.Binder 8.0.1", @@ -43,20 +39,21 @@ "GeoTimeZone 5.3.0", "XmpCore 6.1.10.1", "MetadataExtractor 2.8.1", - "Microsoft.ApplicationInsights.AspNetCore 2.22.0", "Microsoft.Extensions.Logging.Console 8.0.0", "OpenTelemetry 1.7.0", "OpenTelemetry.Api 1.7.0", "OpenTelemetry.Exporter.OpenTelemetryProtocol 1.7.0", "OpenTelemetry.Extensions.Hosting 1.7.0", "OpenTelemetry.Instrumentation.AspNetCore 1.7.0", + "OpenTelemetry.Instrumentation.EntityFrameworkCore 1.0.0-beta.10", "OpenTelemetry.Instrumentation.Runtime 1.7.0", + "System.Diagnostics.DiagnosticSource 8.0.0", "System.Text.Json 8.0.1", "Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.1", "Microsoft.Extensions.Hosting 8.0.0", - "Microsoft.NET.Test.Sdk 17.8.0", - "MSTest.TestAdapter 3.1.1", - "MSTest.TestFramework 3.1.1", + "Microsoft.NET.Test.Sdk 17.9.0", + "MSTest.TestAdapter 3.2.0", + "MSTest.TestFramework 3.2.0", "coverlet.collector 6.0.0", "Microsoft.AspNetCore.Mvc.Formatters.Json 2.2.0", "Microsoft.AspNetCore.Identity 2.2.0" diff --git a/starsky/starsky.feature.demo/Helpers/CleanDemoDataServiceCli.cs b/starsky/starsky.feature.demo/Helpers/CleanDemoDataServiceCli.cs index 36a3fcef77..7ed3887ba3 100644 --- a/starsky/starsky.feature.demo/Helpers/CleanDemoDataServiceCli.cs +++ b/starsky/starsky.feature.demo/Helpers/CleanDemoDataServiceCli.cs @@ -20,8 +20,8 @@ public class CleanDemoDataServiceCli private readonly ISynchronize _sync; private readonly IConsole _console; - public CleanDemoDataServiceCli(AppSettings appSettings, - IHttpClientHelper httpClientHelper, ISelectorStorage selectorStorage, + public CleanDemoDataServiceCli(AppSettings appSettings, + IHttpClientHelper httpClientHelper, ISelectorStorage selectorStorage, IWebLogger webLogger, IConsole console, ISynchronize sync) { @@ -38,14 +38,14 @@ public CleanDemoDataServiceCli(AppSettings appSettings, public async Task SeedCli(string[] args) { _appSettings.Verbose = ArgsHelper.NeedVerbose(args); - - if ( ArgsHelper.NeedHelp(args)) + + if ( ArgsHelper.NeedHelp(args) ) { _appSettings.ApplicationType = AppSettings.StarskyAppType.DemoSeed; new ArgsHelper(_appSettings, _console).NeedHelpShowDialog(); return; } - + await CleanDemoDataService.DownloadAsync(_appSettings, _httpClientHelper, _hostStorage, _subPathStorage, _webLogger); await _sync.Sync("/"); } diff --git a/starsky/starsky.feature.demo/Services/CleanDemoDataService.cs b/starsky/starsky.feature.demo/Services/CleanDemoDataService.cs index 0e9608688a..5bc3c4e202 100644 --- a/starsky/starsky.feature.demo/Services/CleanDemoDataService.cs +++ b/starsky/starsky.feature.demo/Services/CleanDemoDataService.cs @@ -53,17 +53,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var appSettings = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService(); - if (appSettings.DemoUnsafeDeleteStorageFolder != true || appSettings.ApplicationType != AppSettings.StarskyAppType.WebController ) + if ( appSettings.DemoUnsafeDeleteStorageFolder != true || appSettings.ApplicationType != AppSettings.StarskyAppType.WebController ) { return false; } - - if ( Environment.GetEnvironmentVariable("app__storageFolder") == null) + + if ( Environment.GetEnvironmentVariable("app__storageFolder") == null ) { logger.LogError("[demo mode on] Environment variable app__storageFolder is not set"); return null; } - + var selectorStorage = scope.ServiceProvider.GetRequiredService(); var sync = scope.ServiceProvider.GetRequiredService(); var subStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); @@ -71,8 +71,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var httpClientHelper = scope.ServiceProvider.GetRequiredService(); CleanData(subStorage, logger); - await DownloadAsync(appSettings, httpClientHelper,hostStorage,subStorage, logger); - await sync.Sync("/",PushToSockets); + await DownloadAsync(appSettings, httpClientHelper, hostStorage, subStorage, logger); + await sync.Sync("/", PushToSockets); return true; } @@ -83,14 +83,14 @@ internal static void CleanData(IStorage subStorage, IWebLogger logger) logger.LogError("stfolder exists so exit"); return; } - + // parent directories var directories = subStorage.GetDirectories("/"); foreach ( var directory in directories ) { subStorage.FolderDelete(directory); } - + // clean files in root var getAllFiles = subStorage.GetAllFilesInDirectory("/") .Where(p => p != "/.gitkeep" && p != "/.gitignore").ToList(); @@ -111,7 +111,7 @@ internal async Task PushToSockets(List updatedList) using var scope = _serviceScopeFactory.CreateScope(); var connectionsService = scope.ServiceProvider.GetRequiredService(); var notificationQuery = scope.ServiceProvider.GetRequiredService(); - + var webSocketResponse = new ApiNotificationResponseModel>(filtered, ApiNotificationType.CleanDemoData); @@ -119,17 +119,17 @@ internal async Task PushToSockets(List updatedList) await notificationQuery.AddNotification(webSocketResponse); return true; } - + private const string DemoFolderName = "demo"; - + internal static PublishManifestDemo? Deserialize(string result, IWebLogger webLogger, IStorage hostStorage, string settingsJsonFullPath) { PublishManifestDemo? data = null; try { - data = JsonSerializer.Deserialize(result); + data = JsonSerializer.Deserialize(result); } - catch ( JsonException exception) + catch ( JsonException exception ) { webLogger.LogError("[Deserialize] catch-ed", exception); // and delete to retry @@ -138,7 +138,7 @@ internal async Task PushToSockets(List updatedList) return data; } - + internal static async Task DownloadAsync(AppSettings appSettings, IHttpClientHelper httpClientHelper, IStorage hostStorage, IStorage subStorage, IWebLogger webLogger) @@ -148,28 +148,28 @@ internal static async Task DownloadAsync(AppSettings appSettings, webLogger.LogError("DemoData is empty"); return false; } - + webLogger.LogInformation("Download demo data"); var cacheFolder = Path.Combine(appSettings.TempFolder, DemoFolderName); hostStorage.CreateDirectory(cacheFolder); - + foreach ( var (jsonUrl, dir) in appSettings.DemoData ) { hostStorage.CreateDirectory(Path.Combine(cacheFolder, dir)); var settingsJsonFullPath = Path.Combine(cacheFolder, dir, "_settings.json"); - if ( !hostStorage.ExistFile(settingsJsonFullPath) && !await httpClientHelper.Download(jsonUrl, settingsJsonFullPath)) + if ( !hostStorage.ExistFile(settingsJsonFullPath) && !await httpClientHelper.Download(jsonUrl, settingsJsonFullPath) ) { webLogger.LogInformation("Skip due not exists: " + settingsJsonFullPath); continue; } - + var result = await StreamToStringHelper.StreamToStringAsync( hostStorage.ReadStream(settingsJsonFullPath)); - var data = Deserialize(result, webLogger, hostStorage, settingsJsonFullPath); + var data = Deserialize(result, webLogger, hostStorage, settingsJsonFullPath); if ( data == null ) { webLogger.LogError("[DownloadAsync] data is null"); @@ -177,29 +177,29 @@ internal static async Task DownloadAsync(AppSettings appSettings, } var baseUrl = jsonUrl.Replace("_settings.json", string.Empty); // ends with slash - - foreach ( var keyValuePairKey in data.Copy.Where(p => p.Value - && p.Key.Contains("1000")).Select(p => p.Key) ) + + foreach ( var keyValuePairKey in data.Copy.Where(p => p.Value + && p.Key.Contains("1000")).Select(p => p.Key) ) { - var regex = new Regex("\\?.+$", + var regex = new Regex("\\?.+$", RegexOptions.None, TimeSpan.FromMilliseconds(100)); var fileName = FilenamesHelper.GetFileName(regex.Replace(keyValuePairKey, string.Empty)); - var cacheFilePath = Path.Combine(cacheFolder,dir, fileName); + var cacheFilePath = Path.Combine(cacheFolder, dir, fileName); if ( !hostStorage.ExistFile(cacheFilePath) ) { - await httpClientHelper.Download(baseUrl+ keyValuePairKey,cacheFilePath); + await httpClientHelper.Download(baseUrl + keyValuePairKey, cacheFilePath); } subStorage.CreateDirectory(dir); - + await subStorage.WriteStreamAsync( hostStorage.ReadStream(cacheFilePath), PathHelper.AddSlash(dir) + fileName); } } - + webLogger.LogInformation("Demo data seed done"); return true; } diff --git a/starsky/starsky.feature.demo/starsky.feature.demo.csproj b/starsky/starsky.feature.demo/starsky.feature.demo.csproj index b94fb07507..0942437a55 100644 --- a/starsky/starsky.feature.demo/starsky.feature.demo.csproj +++ b/starsky/starsky.feature.demo/starsky.feature.demo.csproj @@ -1,18 +1,20 @@ - - net8.0 - - {34d46dc0-f965-46bf-9178-b83a9a6627e7} - 0.6.0-beta.0 - enable - starsky.feature.demo + + net8.0 + + {34d46dc0-f965-46bf-9178-b83a9a6627e7} + 0.6.0-beta.0 + enable + starsky.feature.demo + - - - - - - + + + + + + true + diff --git a/starsky/starsky.feature.export/Interfaces/IExport.cs b/starsky/starsky.feature.export/Interfaces/IExport.cs index a0fe34af47..0dd5b81d9d 100644 --- a/starsky/starsky.feature.export/Interfaces/IExport.cs +++ b/starsky/starsky.feature.export/Interfaces/IExport.cs @@ -15,6 +15,6 @@ Task>> PreflightAsync( bool collections = true, bool thumbnail = false); - Tuple StatusIsReady(string zipOutputFileName); + Tuple StatusIsReady(string zipOutputFileName); } } diff --git a/starsky/starsky.feature.export/Services/ExportService.cs b/starsky/starsky.feature.export/Services/ExportService.cs index a597e1c0b0..3ddf9158c0 100644 --- a/starsky/starsky.feature.export/Services/ExportService.cs +++ b/starsky/starsky.feature.export/Services/ExportService.cs @@ -23,246 +23,260 @@ using starsky.foundation.thumbnailgeneration.Interfaces; [assembly: InternalsVisibleTo("starskytest")] -namespace starsky.feature.export.Services + +namespace starsky.feature.export.Services; + +/// +/// Also known as Download +/// +[Service(typeof(IExport), InjectionLifetime = InjectionLifetime.Scoped)] +public class ExportService : IExport { + private readonly IQuery _query; + private readonly AppSettings _appSettings; + private readonly IStorage _iStorage; + private readonly IStorage _hostFileSystemStorage; + private readonly IWebLogger _logger; + private readonly IThumbnailService _thumbnailService; + + public ExportService(IQuery query, AppSettings appSettings, + ISelectorStorage selectorStorage, IWebLogger logger, IThumbnailService thumbnailService) + { + _appSettings = appSettings; + _query = query; + _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _hostFileSystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _thumbnailService = thumbnailService; + _logger = logger; + } + /// - /// Also known as Download + /// Export preflight /// - [Service(typeof(IExport), InjectionLifetime = InjectionLifetime.Scoped)] - public class ExportService: IExport + /// list of subPaths + /// is stack collections enabled + /// should export thumbnail or not + /// zipHash, fileIndexResultsList + public async Task>> PreflightAsync( + string[] inputFilePaths, + bool collections = true, + bool thumbnail = false) { - private readonly IQuery _query; - private readonly AppSettings _appSettings; - private readonly IStorage _iStorage; - private readonly IStorage _hostFileSystemStorage; - private readonly IWebLogger _logger; - private readonly IThumbnailService _thumbnailService; - - public ExportService(IQuery query, AppSettings appSettings, - ISelectorStorage selectorStorage, IWebLogger logger, IThumbnailService thumbnailService) - { - _appSettings = appSettings; - _query = query; - _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _hostFileSystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); - _thumbnailService = thumbnailService; - _logger = logger; - } + // the result list + var fileIndexResultsList = new List(); - /// - /// Export preflight - /// - /// list of subPaths - /// is stack collections enabled - /// should export thumbnail or not - /// zipHash, fileIndexResultsList - public async Task>> PreflightAsync( - string[] inputFilePaths, - bool collections = true, - bool thumbnail = false) + foreach ( var subPath in inputFilePaths ) { - // the result list - var fileIndexResultsList = new List(); - - foreach ( var subPath in inputFilePaths ) + var detailView = _query.SingleItem(subPath, null, collections, false); + if ( string.IsNullOrEmpty(detailView?.FileIndexItem?.FilePath) ) { - var detailView = _query.SingleItem(subPath, null, collections, false); - if (string.IsNullOrEmpty(detailView?.FileIndexItem?.FilePath)) - { - StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), - FileIndexItem.ExifStatus.NotFoundNotInIndex, - fileIndexResultsList); - continue; - } - - if ( _iStorage.IsFolderOrFile(detailView.FileIndexItem.FilePath) == - FolderOrFileModel.FolderOrFileTypeList.Deleted ) - { - StatusCodesHelper.ReturnExifStatusError(detailView.FileIndexItem, - FileIndexItem.ExifStatus.NotFoundSourceMissing, - fileIndexResultsList); - continue; - } - - if ( detailView.FileIndexItem.IsDirectory == true ) - { - await AddFileIndexResultsListForDirectory(detailView, fileIndexResultsList); - continue; - } - - // Now Add Collection based images - AddCollectionBasedImages(detailView, fileIndexResultsList, collections, subPath); + StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), + FileIndexItem.ExifStatus.NotFoundNotInIndex, + fileIndexResultsList); + continue; } - var isThumbnail = thumbnail ? "TN" : "SR"; // has:notHas - var zipHash = isThumbnail + GetName(fileIndexResultsList); - - return new Tuple>(zipHash, fileIndexResultsList); - } + if ( _iStorage.IsFolderOrFile(detailView.FileIndexItem.FilePath) == + FolderOrFileModel.FolderOrFileTypeList.Deleted ) + { + StatusCodesHelper.ReturnExifStatusError(detailView.FileIndexItem, + FileIndexItem.ExifStatus.NotFoundSourceMissing, + fileIndexResultsList); + continue; + } - private async Task AddFileIndexResultsListForDirectory(DetailView detailView, List fileIndexResultsList) - { - var allFilesInFolder = - await _query.GetAllRecursiveAsync(detailView? - .FileIndexItem?.FilePath!); - foreach ( var item in - allFilesInFolder. - Where(item => item.FilePath != null && _iStorage.ExistFile(item.FilePath)) ) + if ( detailView.FileIndexItem.IsDirectory == true ) { - item.Status = FileIndexItem.ExifStatus.Ok; - fileIndexResultsList.Add(item); + await AddFileIndexResultsListForDirectory(detailView, fileIndexResultsList); + continue; } + + // Now Add Collection based images + AddCollectionBasedImages(detailView, fileIndexResultsList, collections, subPath); } - private void AddCollectionBasedImages(DetailView detailView, List fileIndexResultsList, bool collections, string subPath) + var isThumbnail = thumbnail ? "TN" : "SR"; // has:notHas + var zipHash = isThumbnail + GetName(fileIndexResultsList); + + return new Tuple>(zipHash, fileIndexResultsList); + } + + private async Task AddFileIndexResultsListForDirectory(DetailView detailView, + List fileIndexResultsList) + { + var allFilesInFolder = + await _query.GetAllRecursiveAsync(detailView + .FileIndexItem?.FilePath!); + foreach ( var item in + allFilesInFolder.Where(item => + item.FilePath != null && _iStorage.ExistFile(item.FilePath)) ) { - var collectionSubPathList = DetailView.GetCollectionSubPathList(detailView.FileIndexItem!, collections, subPath); - foreach ( var item in collectionSubPathList ) - { - var itemFileIndexItem = _query.SingleItem(item, null, - false, false)?.FileIndexItem; - if ( itemFileIndexItem == null ) continue; - itemFileIndexItem.Status = FileIndexItem.ExifStatus.Ok; - fileIndexResultsList.Add(itemFileIndexItem); - } + item.Status = FileIndexItem.ExifStatus.Ok; + fileIndexResultsList.Add(item); } + } - /// - /// Based on the preflight create a Zip Export - /// - /// Result of Preflight - /// isThumbnail? - /// filename of zip file (no extension) - /// nothing - public async Task CreateZip(List fileIndexResultsList, bool thumbnail, - string zipOutputFileName) + private void AddCollectionBasedImages(DetailView detailView, + List fileIndexResultsList, bool collections, string subPath) + { + var collectionSubPathList = + DetailView.GetCollectionSubPathList(detailView.FileIndexItem!, collections, subPath); + foreach ( var item in collectionSubPathList ) { - var filePaths = await CreateListToExport(fileIndexResultsList, thumbnail); - var fileNames = await FilePathToFileNameAsync(filePaths, thumbnail); - - new Zipper().CreateZip(_appSettings.TempFolder,filePaths,fileNames,zipOutputFileName); - - // Write a single file to be sure that writing is ready - var doneFileFullPath = Path.Combine(_appSettings.TempFolder,zipOutputFileName) + ".done"; - await _hostFileSystemStorage. - WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), doneFileFullPath); - if(_appSettings.IsVerbose()) _logger.LogInformation("[CreateZip] Zip done: " + doneFileFullPath); + var itemFileIndexItem = _query.SingleItem(item, null, + false, false)?.FileIndexItem; + if ( itemFileIndexItem == null ) continue; + itemFileIndexItem.Status = FileIndexItem.ExifStatus.Ok; + fileIndexResultsList.Add(itemFileIndexItem); } - - /// - /// This list will be included in the zip - Export is called Download in the UI - /// - /// the items - /// add the thumbnail or the source image - /// list of file paths - public async Task> CreateListToExport(List fileIndexResultsList, bool thumbnail) + } + + /// + /// Based on the preflight create a Zip Export + /// + /// Result of Preflight + /// isThumbnail? + /// filename of zip file (no extension) + /// nothing + public async Task CreateZip(List fileIndexResultsList, bool thumbnail, + string zipOutputFileName) + { + var filePaths = await CreateListToExport(fileIndexResultsList, thumbnail); + var fileNames = await FilePathToFileNameAsync(filePaths, thumbnail); + + new Zipper().CreateZip(_appSettings.TempFolder, filePaths, fileNames, zipOutputFileName); + + // Write a single file to be sure that writing is ready + var doneFileFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".done"; + await _hostFileSystemStorage.WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), + doneFileFullPath); + if ( _appSettings.IsVerbose() ) + _logger.LogInformation("[CreateZip] Zip done: " + doneFileFullPath); + } + + /// + /// This list will be included in the zip - Export is called Download in the UI + /// + /// the items + /// add the thumbnail or the source image + /// list of file paths + public async Task> CreateListToExport(List fileIndexResultsList, + bool thumbnail) + { + var filePaths = new List(); + + foreach ( var item in fileIndexResultsList.Where(p => + p.Status == FileIndexItem.ExifStatus.Ok && p.FileHash != null).ToList() ) { - var filePaths = new List(); + if ( thumbnail ) + { + var sourceThumb = Path.Combine(_appSettings.ThumbnailTempFolder, + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.Large, true)); + + await _thumbnailService + .CreateThumbAsync(item.FilePath!, item.FileHash!, true); + + filePaths.Add(sourceThumb); + continue; + } - foreach ( var item in fileIndexResultsList.Where(p => - p.Status == FileIndexItem.ExifStatus.Ok && p.FileHash != null ).ToList() ) + var sourceFile = _appSettings.DatabasePathToFilePath(item.FilePath!); + + if ( !_hostFileSystemStorage.ExistFile(sourceFile) ) { - if ( thumbnail ) - { - var sourceThumb = Path.Combine(_appSettings.ThumbnailTempFolder, - ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.Large, true)); - - await _thumbnailService - .CreateThumbAsync(item.FilePath!, item.FileHash, true); - - filePaths.Add(sourceThumb); - continue; - } - - var sourceFile = _appSettings.DatabasePathToFilePath(item.FilePath!); - - if ( !_hostFileSystemStorage.ExistFile(sourceFile) ) - { - continue; - } - - // the jpeg file for example - filePaths.Add(sourceFile); - - // when there is .xmp sidecar file (but only when file is a RAW file, ignored when for example jpeg) - if ( !ExtensionRolesHelper.IsExtensionForceXmp(item.FilePath) || - !_iStorage.ExistFile( - ExtensionRolesHelper.ReplaceExtensionWithXmp( - item.FilePath)) ) continue; - - var xmpFileFullPath = _appSettings.DatabasePathToFilePath( - ExtensionRolesHelper.ReplaceExtensionWithXmp( - item.FilePath)); - - if ( !_hostFileSystemStorage.ExistFile(xmpFileFullPath) ) - { - continue; - } - filePaths.Add(xmpFileFullPath); + continue; } - return filePaths; - } - - /// - /// Get the filename (in case of thumbnail the source image name) - /// - /// the full file paths - /// copy the thumbnail (true) or the source image (false) - /// - internal async Task> FilePathToFileNameAsync(IEnumerable filePaths, bool thumbnail) - { - var fileNames = new List(); - foreach ( var filePath in filePaths ) + + // the jpeg file for example + filePaths.Add(sourceFile); + + // when there is .xmp sidecar file (but only when file is a RAW file, ignored when for example jpeg) + if ( !ExtensionRolesHelper.IsExtensionForceXmp(item.FilePath) || + !_iStorage.ExistFile( + ExtensionRolesHelper.ReplaceExtensionWithXmp( + item.FilePath)) ) continue; + + var xmpFileFullPath = _appSettings.DatabasePathToFilePath( + ExtensionRolesHelper.ReplaceExtensionWithXmp( + item.FilePath)); + + if ( !_hostFileSystemStorage.ExistFile(xmpFileFullPath) ) { - if ( thumbnail ) - { - // We use base32 fileHashes but export - // the file with the original name - - var thumbFilename = Path.GetFileNameWithoutExtension(filePath); - var subPath = await _query.GetSubPathByHashAsync(thumbFilename); - var filename = subPath?.Split('/').LastOrDefault(); // first a string - fileNames.Add(filename); - continue; - } - fileNames.Add(Path.GetFileName(filePath)); + continue; } - return fileNames; + + filePaths.Add(xmpFileFullPath); } - /// - /// to create a unique name of the zip using c# get hashcode - /// - /// list of objects with fileHashes - /// unique 'get hashcode' string - private static string GetName(IEnumerable fileIndexResultsList) + return filePaths; + } + + /// + /// Get the filename (in case of thumbnail the source image name) + /// + /// the full file paths + /// copy the thumbnail (true) or the source image (false) + /// + internal async Task> FilePathToFileNameAsync(IEnumerable filePaths, + bool thumbnail) + { + var fileNames = new List(); + foreach ( var filePath in filePaths ) { - var tempFileNameStringBuilder = new StringBuilder(); - foreach ( var item in fileIndexResultsList ) + if ( thumbnail ) { - tempFileNameStringBuilder.Append(item.FileHash); + // We use base32 fileHashes but export + // the file with the original name + + var thumbFilename = Path.GetFileNameWithoutExtension(filePath); + var subPath = await _query.GetSubPathByHashAsync(thumbFilename); + var filename = subPath?.Split('/').LastOrDefault()!; // first a string + fileNames.Add(filename); + continue; } - // to be sure that the max string limit - var shortName = tempFileNameStringBuilder.ToString().GetHashCode() - .ToString(CultureInfo.InvariantCulture).ToLower().Replace("-","A"); - - return shortName; + + fileNames.Add(Path.GetFileName(filePath)); } - /// - /// Is Zip Ready? - /// - /// fileName without extension - /// null if status file is not found, true if done file exist - public Tuple StatusIsReady(string zipOutputFileName) + return fileNames; + } + + /// + /// to create a unique name of the zip using c# get hashcode + /// + /// list of objects with fileHashes + /// unique 'get hashcode' string + private static string GetName(IEnumerable fileIndexResultsList) + { + var tempFileNameStringBuilder = new StringBuilder(); + foreach ( var item in fileIndexResultsList ) { - var sourceFullPath = Path.Combine(_appSettings.TempFolder,zipOutputFileName) + ".zip"; - var doneFileFullPath = Path.Combine(_appSettings.TempFolder,zipOutputFileName) + ".done"; + tempFileNameStringBuilder.Append(item.FileHash); + } - if ( !_hostFileSystemStorage.ExistFile(sourceFullPath) ) return new Tuple(null,null); + // to be sure that the max string limit + var shortName = tempFileNameStringBuilder.ToString().GetHashCode() + .ToString(CultureInfo.InvariantCulture).ToLower().Replace("-", "A"); - // Read a single file to be sure that writing is ready - return new Tuple(_hostFileSystemStorage.ExistFile(doneFileFullPath), sourceFullPath); - } + return shortName; + } + + /// + /// Is Zip Ready? + /// + /// fileName without extension + /// null if status file is not found, true if done file exist + public Tuple StatusIsReady(string zipOutputFileName) + { + var sourceFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".zip"; + var doneFileFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".done"; + + if ( !_hostFileSystemStorage.ExistFile(sourceFullPath) ) + return new Tuple(null, null); + + // Read a single file to be sure that writing is ready + return new Tuple(_hostFileSystemStorage.ExistFile(doneFileFullPath), + sourceFullPath); } } diff --git a/starsky/starsky.feature.export/starsky.feature.export.csproj b/starsky/starsky.feature.export/starsky.feature.export.csproj index 6cf00f8bf2..753b8bce37 100644 --- a/starsky/starsky.feature.export/starsky.feature.export.csproj +++ b/starsky/starsky.feature.export/starsky.feature.export.csproj @@ -4,11 +4,12 @@ net8.0 {09ccbfa9-612f-4d5e-ae27-c0c06e7114c9} 0.6.0-beta.0 + enable - + - - + + diff --git a/starsky/starsky.feature.geolookup/Models/GeoCacheStatus.cs b/starsky/starsky.feature.geolookup/Models/GeoCacheStatus.cs index 7714784466..7b722292e0 100644 --- a/starsky/starsky.feature.geolookup/Models/GeoCacheStatus.cs +++ b/starsky/starsky.feature.geolookup/Models/GeoCacheStatus.cs @@ -5,7 +5,7 @@ public enum StatusType Total, Current } - + public class GeoCacheStatus { public int Total { get; set; } diff --git a/starsky/starsky.feature.geolookup/Models/GeoLocationModel.cs b/starsky/starsky.feature.geolookup/Models/GeoLocationModel.cs index 1bf3c5a864..f2a69c0c36 100644 --- a/starsky/starsky.feature.geolookup/Models/GeoLocationModel.cs +++ b/starsky/starsky.feature.geolookup/Models/GeoLocationModel.cs @@ -6,7 +6,7 @@ public sealed class GeoLocationModel public double Latitude { get; set; } public double Longitude { get; set; } - + public string? LocationCity { get; set; } public string? LocationCountry { get; set; } public string? LocationCountryCode { get; set; } diff --git a/starsky/starsky.feature.geolookup/Services/GeoBackgroundTask.cs b/starsky/starsky.feature.geolookup/Services/GeoBackgroundTask.cs index 93709bbbe0..592e5580ac 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoBackgroundTask.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoBackgroundTask.cs @@ -29,8 +29,8 @@ public class GeoBackgroundTask : IGeoBackgroundTask private readonly GeoIndexGpx _geoIndexGpx; private readonly IGeoReverseLookup _geoReverseLookup; - public GeoBackgroundTask(AppSettings appSettings, ISelectorStorage selectorStorage, - IGeoLocationWrite geoLocationWrite, IMemoryCache memoryCache, + public GeoBackgroundTask(AppSettings appSettings, ISelectorStorage selectorStorage, + IGeoLocationWrite geoLocationWrite, IMemoryCache memoryCache, IWebLogger logger, IGeoReverseLookup geoReverseLookup) { _appSettings = appSettings; @@ -42,7 +42,7 @@ public GeoBackgroundTask(AppSettings appSettings, ISelectorStorage selectorStora _geoIndexGpx = new GeoIndexGpx(_appSettings, _iStorage, logger, memoryCache); _geoReverseLookup = geoReverseLookup; } - + public async Task> GeoBackgroundTaskAsync( string f = "/", bool index = true, @@ -53,18 +53,18 @@ public async Task> GeoBackgroundTaskAsync( var listOfFiles = _iStorage.GetAllFilesInDirectory(f) .Where(ExtensionRolesHelper.IsExtensionSyncSupported).ToList(); - var fileIndexList = await _readMeta + var fileIndexList = await _readMeta .ReadExifAndXmpFromFileAddFilePathHashAsync(listOfFiles); - + var toMetaFilesUpdate = new List(); if ( index ) { toMetaFilesUpdate = await _geoIndexGpx .LoopFolderAsync(fileIndexList); - + if ( _appSettings.IsVerbose() ) Console.Write("¬"); - + await _geoLocationWrite .LoopFolderAsync(toMetaFilesUpdate, false); } @@ -72,7 +72,7 @@ await _geoLocationWrite fileIndexList = await _geoReverseLookup .LoopFolderLookup(fileIndexList, overwriteLocationNames); - + if ( fileIndexList.Count >= 1 ) { await _geoLocationWrite.LoopFolderAsync( @@ -86,8 +86,8 @@ await _geoLocationWrite.LoopFolderAsync( foreach ( var item in fileIndexList.GroupBy(i => i.FilePath).Select(g => g.First()) .ToList() ) { - var newThumb = (await new FileHash(_iStorage).GetHashCodeAsync(item.FilePath!)).Key; - if ( item.FileHash == newThumb) continue; + var newThumb = ( await new FileHash(_iStorage).GetHashCodeAsync(item.FilePath!) ).Key; + if ( item.FileHash == newThumb ) continue; new ThumbnailFileMoveAllSizes(_thumbnailStorage).FileMove(item.FileHash!, newThumb); if ( _appSettings.IsVerbose() ) _logger.LogInformation("[/api/geo/sync] thumb rename + `" + item.FileHash + "`" + newThumb); diff --git a/starsky/starsky.feature.geolookup/Services/GeoCacheStatusService.cs b/starsky/starsky.feature.geolookup/Services/GeoCacheStatusService.cs index 4d62417cf2..c87952a4c7 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoCacheStatusService.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoCacheStatusService.cs @@ -8,41 +8,41 @@ namespace starsky.feature.geolookup.Services public class GeoCacheStatusService { private readonly IMemoryCache? _cache; - - public GeoCacheStatusService( IMemoryCache? memoryCache = null) + + public GeoCacheStatusService(IMemoryCache? memoryCache = null) { _cache = memoryCache; } public GeoCacheStatus Status(string path) { - if(_cache == null || string.IsNullOrWhiteSpace(path)) return new GeoCacheStatus{Total = -1}; + if ( _cache == null || string.IsNullOrWhiteSpace(path) ) return new GeoCacheStatus { Total = -1 }; var totalCacheName = nameof(GeoCacheStatus) + path + StatusType.Total; var result = new GeoCacheStatus(); - - if(_cache.TryGetValue(totalCacheName, out var statusObjectTotal) && - TryParse(statusObjectTotal?.ToString(), out var totalStatus)) + + if ( _cache.TryGetValue(totalCacheName, out var statusObjectTotal) && + TryParse(statusObjectTotal?.ToString(), out var totalStatus) ) { result.Total = totalStatus; } - + var currentCacheName = nameof(GeoCacheStatus) + path + StatusType.Current; - if(_cache.TryGetValue(currentCacheName, out var statusObjectCurrent) && - TryParse(statusObjectCurrent?.ToString(), out var currentStatus)) + if ( _cache.TryGetValue(currentCacheName, out var statusObjectCurrent) && + TryParse(statusObjectCurrent?.ToString(), out var currentStatus) ) { result.Current = currentStatus; } - + return result; } - + public void StatusUpdate(string path, int current, StatusType type) { - if(_cache == null || string.IsNullOrWhiteSpace(path)) return; - + if ( _cache == null || string.IsNullOrWhiteSpace(path) ) return; + var queryGeoCacheName = nameof(GeoCacheStatus) + path + type; - _cache.Set(queryGeoCacheName, current, new TimeSpan(10,0,0)); + _cache.Set(queryGeoCacheName, current, new TimeSpan(10, 0, 0)); } } diff --git a/starsky/starsky.feature.geolookup/Services/GeoCli.cs b/starsky/starsky.feature.geolookup/Services/GeoCli.cs index af3a030d7d..73f404b2b8 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoCli.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoCli.cs @@ -36,8 +36,8 @@ public sealed class GeoCli private readonly IWebLogger _logger; [SuppressMessage("Usage", "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] - public GeoCli(IGeoReverseLookup geoReverseLookup, - IGeoLocationWrite geoLocationWrite, ISelectorStorage selectorStorage, AppSettings appSettings, IConsole console, + public GeoCli(IGeoReverseLookup geoReverseLookup, + IGeoLocationWrite geoLocationWrite, ISelectorStorage selectorStorage, AppSettings appSettings, IConsole console, IGeoFileDownload geoFileDownload, IExifToolDownload exifToolDownload, IWebLogger logger) { _geoReverseLookup = geoReverseLookup; @@ -51,7 +51,7 @@ public GeoCli(IGeoReverseLookup geoReverseLookup, _geoFileDownload = geoFileDownload; _logger = logger; } - + /// /// Command line importer to Database and update disk /// @@ -62,25 +62,25 @@ public async Task CommandLineAsync(string[] args) _appSettings.Verbose = ArgsHelper.NeedVerbose(args); // Set type of GeoReverseLookup _appSettings.ApplicationType = AppSettings.StarskyAppType.Geo; - + // Download ExifTool await _exifToolDownload.DownloadExifTool(_appSettings.IsWindows); - + // Geo cities1000 download if ( _appSettings.GeoFilesSkipDownloadOnStartup != true ) { await _geoFileDownload.DownloadAsync(); } - + if ( ArgsHelper.NeedHelp(args) || - ( new ArgsHelper(_appSettings).GetPathFormArgs(args, false).Length <= 1 - && ArgsHelper.GetSubPathFormArgs(args).Length <= 1 - && new ArgsHelper(_appSettings).GetRelativeValue(args) == null ) ) + ( new ArgsHelper(_appSettings).GetPathFormArgs(args, false).Length <= 1 + && ArgsHelper.GetSubPathFormArgs(args).Length <= 1 + && new ArgsHelper(_appSettings).GetRelativeValue(args) == null ) ) { new ArgsHelper(_appSettings, _console).NeedHelpShowDialog(); return; } - + // Using both options string inputPath; // -s = if subPath || -p is path @@ -95,36 +95,36 @@ public async Task CommandLineAsync(string[] args) { inputPath = new ArgsHelper(_appSettings).GetPathFormArgs(args, false); } - + // overwrite subPath with relative days // use -g or --SubPathRelative to use it. // envs are not supported var getSubPathRelative = new ArgsHelper(_appSettings).GetRelativeValue(args); - if (getSubPathRelative != null) + if ( getSubPathRelative != null ) { - var dateTime = DateTime.Now.AddDays(( double ) getSubPathRelative); + var dateTime = DateTime.Now.AddDays(( double )getSubPathRelative); var path = _appSettings.DatabasePathToFilePath( new StructureService(_iStorage, _appSettings.Structure) .ParseSubfolders(dateTime)); inputPath = !string.IsNullOrEmpty(path) ? path : string.Empty; } - + // used in this session to find the files back _appSettings.StorageFolder = inputPath; - + if ( inputPath == null || _iStorage.IsFolderOrFile("/") == FolderOrFileModel.FolderOrFileTypeList.Deleted ) { _console.WriteLine("Folder location is not found \n" + - $"Please try the `-h` command to get help \nDid search for: {inputPath}"); + $"Please try the `-h` command to get help \nDid search for: {inputPath}"); return; } - + // use relative to StorageFolder var listOfFiles = _iStorage.GetAllFilesInDirectory("/") .Where(ExtensionRolesHelper.IsExtensionSyncSupported).ToList(); - + var fileIndexList = await _readMeta.ReadExifAndXmpFromFileAddFilePathHashAsync(listOfFiles); - + var toMetaFilesUpdate = new List(); if ( ArgsHelper.GetIndexMode(args) ) { @@ -133,25 +133,25 @@ public async Task CommandLineAsync(string[] args) var geoIndexGpx = new GeoIndexGpx(_appSettings, _iStorage, _logger); toMetaFilesUpdate = await geoIndexGpx.LoopFolderAsync(fileIndexList); - + _console.Write("¬"); await _geoLocationWrite.LoopFolderAsync(toMetaFilesUpdate, false); _console.Write("(gps added)"); } - + fileIndexList = await _geoReverseLookup.LoopFolderLookup(fileIndexList, ArgsHelper.GetAll(args)); - + if ( fileIndexList.Count >= 1 ) { _console.Write("~ Add city, state and country info ~"); - + await _geoLocationWrite.LoopFolderAsync(fileIndexList, true); } - + _console.Write("^\n"); _console.Write("~ Rename thumbnails ~"); - + // Loop though all options fileIndexList.AddRange(toMetaFilesUpdate); @@ -164,10 +164,10 @@ private async Task RenameFileHash(IEnumerable fileIndexList) { // update thumbs to avoid unnecessary re-generation foreach ( var item in fileIndexList.GroupBy(i => i.FilePath). - Select(g => g.First()) - .ToList() ) + Select(g => g.First()) + .ToList() ) { - var newThumb = (await new FileHash(_iStorage).GetHashCodeAsync(item.FilePath!)).Key; + var newThumb = ( await new FileHash(_iStorage).GetHashCodeAsync(item.FilePath!) ).Key; if ( item.FileHash == newThumb ) continue; new ThumbnailFileMoveAllSizes(_thumbnailStorage).FileMove( item.FileHash!, newThumb); diff --git a/starsky/starsky.feature.geolookup/Services/GeoFileDownload.cs b/starsky/starsky.feature.geolookup/Services/GeoFileDownload.cs index 84d5690a69..c32da8c5bc 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoFileDownload.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoFileDownload.cs @@ -20,7 +20,7 @@ public sealed class GeoFileDownload : IGeoFileDownload public const string CountryName = "cities1000"; internal long MinimumSizeInBytes { get; set; } = 7000000; // 7 MB - + public GeoFileDownload(AppSettings appSettings, IHttpClientHelper httpClientHelper, ISelectorStorage selectorStorage) { _appSettings = appSettings; @@ -31,38 +31,38 @@ public GeoFileDownload(AppSettings appSettings, IHttpClientHelper httpClientHelp internal const string BaseUrl = "download.geonames.org/export/dump/"; internal const string MirrorUrl = "qdraw.nl/special/mirror/geonames/"; - + public async Task DownloadAsync() { RemoveFailedDownload(); CreateDependenciesFolder(); - - if(!_hostStorage.ExistFile( - Path.Combine(_appSettings.DependenciesFolder,CountryName + ".txt")) ) + + if ( !_hostStorage.ExistFile( + Path.Combine(_appSettings.DependenciesFolder, CountryName + ".txt")) ) { var outputZip = Path.Combine(_appSettings.DependenciesFolder, CountryName + ".zip"); - var baseResult = await _httpClientHelper.Download( "https://" + BaseUrl + CountryName + ".zip",outputZip); + var baseResult = await _httpClientHelper.Download("https://" + BaseUrl + CountryName + ".zip", outputZip); if ( !baseResult ) { - await _httpClientHelper.Download("https://" + MirrorUrl + CountryName + ".zip",outputZip); + await _httpClientHelper.Download("https://" + MirrorUrl + CountryName + ".zip", outputZip); } new Zipper().ExtractZip(outputZip, _appSettings.DependenciesFolder); } - if(!_hostStorage.ExistFile( - Path.Combine(_appSettings.DependenciesFolder,"admin1CodesASCII.txt"))) + if ( !_hostStorage.ExistFile( + Path.Combine(_appSettings.DependenciesFolder, "admin1CodesASCII.txt")) ) { // code for the second administrative division, // a county in the US, see file admin2Codes.txt; varchar(80) var outputFile = Path.Combine(_appSettings.DependenciesFolder, "admin1CodesASCII.txt"); var baseResult = await _httpClientHelper.Download("https://" + - BaseUrl + "admin1CodesASCII.txt",outputFile); + BaseUrl + "admin1CodesASCII.txt", outputFile); if ( !baseResult ) { await _httpClientHelper.Download("https://" + - MirrorUrl + "admin1CodesASCII.txt",outputFile); + MirrorUrl + "admin1CodesASCII.txt", outputFile); } } } @@ -81,12 +81,12 @@ internal void CreateDependenciesFolder() internal void RemoveFailedDownload() { if ( !_hostStorage.ExistFile(Path.Combine( - _appSettings.DependenciesFolder, - CountryName + ".zip")) ) + _appSettings.DependenciesFolder, + CountryName + ".zip")) ) { return; } - + // When trying to download a file var zipLength = _hostStorage .ReadStream(Path.Combine(_appSettings.DependenciesFolder, CountryName + ".zip")) diff --git a/starsky/starsky.feature.geolookup/Services/GeoFileDownloadBackgroundService.cs b/starsky/starsky.feature.geolookup/Services/GeoFileDownloadBackgroundService.cs index 60c8762a0d..bc408dcafe 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoFileDownloadBackgroundService.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoFileDownloadBackgroundService.cs @@ -31,17 +31,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await DownloadAsync(stoppingToken); } - + internal async Task DownloadAsync(CancellationToken _ = default) { - using (var scope = _serviceScopeFactory.CreateScope()) + using ( var scope = _serviceScopeFactory.CreateScope() ) { var appSettings = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService(); // Geo Helper has a direct need of this, other are downloaded when needed // This Background service is for running offline - if ( appSettings.ApplicationType == AppSettings.StarskyAppType.Geo) return; + if ( appSettings.ApplicationType == AppSettings.StarskyAppType.Geo ) return; if ( appSettings.GeoFilesSkipDownloadOnStartup == true ) { logger.LogInformation("GeoFilesSkipDownloadOnStartup is true, skip download"); diff --git a/starsky/starsky.feature.geolookup/Services/GeoIndexGpx.cs b/starsky/starsky.feature.geolookup/Services/GeoIndexGpx.cs index d621fb467b..6efdc25b20 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoIndexGpx.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoIndexGpx.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -17,118 +17,118 @@ [assembly: InternalsVisibleTo("starskytest")] namespace starsky.feature.geolookup.Services { - public class GeoIndexGpx : IGeoIndexGpx - { - private readonly AppSettings _appSettings; - private readonly IStorage _iStorage; - private readonly IMemoryCache? _cache; - private readonly IWebLogger _logger; - - public GeoIndexGpx(AppSettings appSettings, IStorage iStorage, - IWebLogger logger, IMemoryCache? memoryCache = null ) - { - _appSettings = appSettings; - _iStorage = iStorage; - _cache = memoryCache; - _logger = logger; - } - - private static List GetNoLocationItems(IEnumerable metaFilesInDirectory) - { - return metaFilesInDirectory.Where( - metaFileItem => - (Math.Abs(metaFileItem.Latitude) < 0.001 && Math.Abs(metaFileItem.Longitude) < 0.001) - && metaFileItem.DateTime.Year > 2) // ignore files without a date - .ToList(); - } - - private async Task> GetGpxFileAsync(List metaFilesInDirectory) - { - var geoList = new List(); - foreach (var metaFileItem in metaFilesInDirectory) - { - - if( !ExtensionRolesHelper.IsExtensionForceGpx(metaFileItem.FileName) ) continue; - - if ( !_iStorage.ExistFile(metaFileItem.FilePath!) ) continue; - - using ( var stream = _iStorage.ReadStream(metaFileItem.FilePath!) ) - { - geoList.AddRange(await new ReadMetaGpx(_logger).ReadGpxFileAsync(stream, geoList)); - } - } - return geoList; - } - - /// - /// Convert to the appSettings timezone setting - /// - /// current DateTime - /// optional only to display errors - /// The time in the specified timezone - /// DateTime Kind should not be Local - internal DateTime ConvertTimeZone(DateTime valueDateTime, string subPath = "") - { - if ( valueDateTime.Kind == DateTimeKind.Utc ) - { - return valueDateTime; - } - - // Not supported by TimeZoneInfo convert - if ( valueDateTime.Kind != DateTimeKind.Unspecified || _appSettings.CameraTimeZoneInfo == null ) - { - throw new ArgumentException($"valueDateTime DateTime-Kind '{valueDateTime.Kind}' " + - $"'{subPath}' should be Unspecified", nameof(valueDateTime)); - } - - return TimeZoneInfo.ConvertTime(valueDateTime, - _appSettings.CameraTimeZoneInfo, TimeZoneInfo.Utc); - } - - public async Task> LoopFolderAsync(List metaFilesInDirectory) - { - var toUpdateMetaFiles = new List(); - - var gpxList = await GetGpxFileAsync(metaFilesInDirectory); - if ( gpxList.Count == 0 ) - { - return toUpdateMetaFiles; - } - - metaFilesInDirectory = GetNoLocationItems(metaFilesInDirectory); - - var subPath = metaFilesInDirectory.FirstOrDefault()?.ParentDirectory!; - new GeoCacheStatusService(_cache).StatusUpdate(subPath, - metaFilesInDirectory.Count, StatusType.Total); - - foreach (var metaFileItem in metaFilesInDirectory.Select( - (value, index) => new { value, index })) - { - var dateTimeCameraUtc = ConvertTimeZone(metaFileItem.value.DateTime, - metaFileItem.value.FilePath!); - - var fileGeoData = gpxList.MinBy(p => Math.Abs((p.DateTime - dateTimeCameraUtc).Ticks)); - if(fileGeoData == null) continue; - - var minutesDifference = (dateTimeCameraUtc - fileGeoData.DateTime).TotalMinutes; - if(minutesDifference < -5 || minutesDifference > 5) continue; - - metaFileItem.value.Latitude = fileGeoData.Latitude; - metaFileItem.value.Longitude = fileGeoData.Longitude; - metaFileItem.value.LocationAltitude = fileGeoData.Altitude; - - toUpdateMetaFiles.Add(metaFileItem.value); - - // status update - new GeoCacheStatusService(_cache).StatusUpdate(metaFileItem.value.ParentDirectory!, - metaFileItem.index, StatusType.Current); - } - - // Ready signal - new GeoCacheStatusService(_cache).StatusUpdate(subPath, - metaFilesInDirectory.Count, StatusType.Current); - - return toUpdateMetaFiles; - } - } + public class GeoIndexGpx : IGeoIndexGpx + { + private readonly AppSettings _appSettings; + private readonly IStorage _iStorage; + private readonly IMemoryCache? _cache; + private readonly IWebLogger _logger; + + public GeoIndexGpx(AppSettings appSettings, IStorage iStorage, + IWebLogger logger, IMemoryCache? memoryCache = null) + { + _appSettings = appSettings; + _iStorage = iStorage; + _cache = memoryCache; + _logger = logger; + } + + private static List GetNoLocationItems(IEnumerable metaFilesInDirectory) + { + return metaFilesInDirectory.Where( + metaFileItem => + ( Math.Abs(metaFileItem.Latitude) < 0.001 && Math.Abs(metaFileItem.Longitude) < 0.001 ) + && metaFileItem.DateTime.Year > 2) // ignore files without a date + .ToList(); + } + + private async Task> GetGpxFileAsync(List metaFilesInDirectory) + { + var geoList = new List(); + foreach ( var metaFileItem in metaFilesInDirectory ) + { + + if ( !ExtensionRolesHelper.IsExtensionForceGpx(metaFileItem.FileName) ) continue; + + if ( !_iStorage.ExistFile(metaFileItem.FilePath!) ) continue; + + using ( var stream = _iStorage.ReadStream(metaFileItem.FilePath!) ) + { + geoList.AddRange(await new ReadMetaGpx(_logger).ReadGpxFileAsync(stream, geoList)); + } + } + return geoList; + } + + /// + /// Convert to the appSettings timezone setting + /// + /// current DateTime + /// optional only to display errors + /// The time in the specified timezone + /// DateTime Kind should not be Local + internal DateTime ConvertTimeZone(DateTime valueDateTime, string subPath = "") + { + if ( valueDateTime.Kind == DateTimeKind.Utc ) + { + return valueDateTime; + } + + // Not supported by TimeZoneInfo convert + if ( valueDateTime.Kind != DateTimeKind.Unspecified || _appSettings.CameraTimeZoneInfo == null ) + { + throw new ArgumentException($"valueDateTime DateTime-Kind '{valueDateTime.Kind}' " + + $"'{subPath}' should be Unspecified", nameof(valueDateTime)); + } + + return TimeZoneInfo.ConvertTime(valueDateTime, + _appSettings.CameraTimeZoneInfo, TimeZoneInfo.Utc); + } + + public async Task> LoopFolderAsync(List metaFilesInDirectory) + { + var toUpdateMetaFiles = new List(); + + var gpxList = await GetGpxFileAsync(metaFilesInDirectory); + if ( gpxList.Count == 0 ) + { + return toUpdateMetaFiles; + } + + metaFilesInDirectory = GetNoLocationItems(metaFilesInDirectory); + + var subPath = metaFilesInDirectory.FirstOrDefault()?.ParentDirectory!; + new GeoCacheStatusService(_cache).StatusUpdate(subPath, + metaFilesInDirectory.Count, StatusType.Total); + + foreach ( var metaFileItem in metaFilesInDirectory.Select( + (value, index) => new { value, index }) ) + { + var dateTimeCameraUtc = ConvertTimeZone(metaFileItem.value.DateTime, + metaFileItem.value.FilePath!); + + var fileGeoData = gpxList.MinBy(p => Math.Abs(( p.DateTime - dateTimeCameraUtc ).Ticks)); + if ( fileGeoData == null ) continue; + + var minutesDifference = ( dateTimeCameraUtc - fileGeoData.DateTime ).TotalMinutes; + if ( minutesDifference < -5 || minutesDifference > 5 ) continue; + + metaFileItem.value.Latitude = fileGeoData.Latitude; + metaFileItem.value.Longitude = fileGeoData.Longitude; + metaFileItem.value.LocationAltitude = fileGeoData.Altitude; + + toUpdateMetaFiles.Add(metaFileItem.value); + + // status update + new GeoCacheStatusService(_cache).StatusUpdate(metaFileItem.value.ParentDirectory!, + metaFileItem.index, StatusType.Current); + } + + // Ready signal + new GeoCacheStatusService(_cache).StatusUpdate(subPath, + metaFilesInDirectory.Count, StatusType.Current); + + return toUpdateMetaFiles; + } + } } diff --git a/starsky/starsky.feature.geolookup/Services/GeoReverseLookup.cs b/starsky/starsky.feature.geolookup/Services/GeoReverseLookup.cs index 03b80e95f9..b2a792f4dc 100644 --- a/starsky/starsky.feature.geolookup/Services/GeoReverseLookup.cs +++ b/starsky/starsky.feature.geolookup/Services/GeoReverseLookup.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -24,211 +23,212 @@ namespace starsky.feature.geolookup.Services [Service(typeof(IGeoReverseLookup), InjectionLifetime = InjectionLifetime.Singleton)] [SuppressMessage("Performance", "CA1822:Mark members as static")] public sealed class GeoReverseLookup : IGeoReverseLookup - { - private ReverseGeoCode? _reverseGeoCode; - private IEnumerable? _admin1CodesAscii; - private readonly IMemoryCache? _cache; - private readonly AppSettings _appSettings; - private readonly IWebLogger _logger; - private readonly IGeoFileDownload _geoFileDownload; - - /// - /// Getting GeoData - /// - /// to know where to store the deps files - /// used to get IGeoFileDownload - Abstraction to download Geo Data - /// for keeping status - /// debug logger - public GeoReverseLookup(AppSettings appSettings, - IServiceScopeFactory serviceScopeFactory, IWebLogger logger, - IMemoryCache? memoryCache = null) - { - _appSettings = appSettings; - _logger = logger; - _geoFileDownload = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - _reverseGeoCode = null; - _admin1CodesAscii = null; - _cache = memoryCache; - } - - /// - /// Internal API - Getting GeoData - /// - /// to know where to store the deps files - /// Abstraction to download Geo Data - /// for keeping status - /// debug logger - internal GeoReverseLookup(AppSettings appSettings, IGeoFileDownload geoFileDownload, IWebLogger logger, IMemoryCache? memoryCache = null) - { - _appSettings = appSettings; - _logger = logger; - // Get the IGeoFileDownload from the service scope due different injection lifetime (singleton vs scoped) - _geoFileDownload = geoFileDownload; - _reverseGeoCode = null; - _admin1CodesAscii = null; - _cache = memoryCache; - } - - internal async Task<(IEnumerable, ReverseGeoCode)> SetupAsync() - { - await _geoFileDownload.DownloadAsync(); - + { + private ReverseGeoCode? _reverseGeoCode; + private IEnumerable? _admin1CodesAscii; + private readonly IMemoryCache? _cache; + private readonly AppSettings _appSettings; + private readonly IWebLogger _logger; + private readonly IGeoFileDownload _geoFileDownload; + + /// + /// Getting GeoData + /// + /// to know where to store the deps files + /// used to get IGeoFileDownload - Abstraction to download Geo Data + /// for keeping status + /// debug logger + public GeoReverseLookup(AppSettings appSettings, + IServiceScopeFactory serviceScopeFactory, IWebLogger logger, + IMemoryCache? memoryCache = null) + { + _appSettings = appSettings; + _logger = logger; + _geoFileDownload = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + _reverseGeoCode = null; + _admin1CodesAscii = null; + _cache = memoryCache; + } + + /// + /// Internal API - Getting GeoData + /// + /// to know where to store the deps files + /// Abstraction to download Geo Data + /// for keeping status + /// debug logger + internal GeoReverseLookup(AppSettings appSettings, IGeoFileDownload geoFileDownload, IWebLogger logger, IMemoryCache? memoryCache = null) + { + _appSettings = appSettings; + _logger = logger; + // Get the IGeoFileDownload from the service scope due different injection lifetime (singleton vs scoped) + _geoFileDownload = geoFileDownload; + _reverseGeoCode = null; + _admin1CodesAscii = null; + _cache = memoryCache; + } + + internal async Task<(IEnumerable, ReverseGeoCode)> SetupAsync() + { + await _geoFileDownload.DownloadAsync(); + _admin1CodesAscii = GeoFileReader.ReadAdmin1Codes( Path.Combine(_appSettings.DependenciesFolder, "admin1CodesASCII.txt")); - + _reverseGeoCode = new ReverseGeoCode( GeoFileReader.ReadExtendedGeoNames( Path.Combine(_appSettings.DependenciesFolder, GeoFileDownload.CountryName + ".txt"))); - return (_admin1CodesAscii, _reverseGeoCode ); - } - - internal string? GetAdmin1Name(string countryCode, string[] adminCodes) - { - if (_admin1CodesAscii == null || adminCodes.Length != 4) return null; - - var admin1Code = countryCode + "." + adminCodes[0]; - - var admin2Object = _admin1CodesAscii.FirstOrDefault(p => p.Code == admin1Code); - return admin2Object?.NameASCII; - } - - /// - /// Checks for files that already done - /// if latitude is not location 0,0, That's default - /// If one of the meta items are missing, keep in list - /// If extension in exifTool supported, so no gpx - /// - /// List of files with metadata - /// true = overwrite the location names, that have a gps location - /// list that can be updated - public List RemoveNoUpdateItems(IEnumerable metaFilesInDirectory, - bool overwriteLocationNames) - { - // this will overwrite the location names, that have a gps location - if (overwriteLocationNames) - return metaFilesInDirectory.Where( - metaFileItem => - Math.Abs(metaFileItem.Latitude) > 0.001 && Math.Abs(metaFileItem.Longitude) > 0.001) - .ToList(); - - // the default situation - return metaFilesInDirectory.Where( - metaFileItem => - ((Math.Abs(metaFileItem.Latitude) > 0.001 && Math.Abs(metaFileItem.Longitude) > 0.001) - && (string.IsNullOrEmpty(metaFileItem.LocationCity) - || string.IsNullOrEmpty(metaFileItem.LocationState) - || string.IsNullOrEmpty(metaFileItem.LocationCountry) - )) // for now NO check on: metaFileItem.LocationCountryCode - && ExtensionRolesHelper.IsExtensionExifToolSupported(metaFileItem.FileName) - ).ToList(); - } - - /// - /// Reverse Geo Syncing for a folder - /// - /// list of files to lookup - /// true = overwrite the location names, that have a gps location - /// - public async Task> LoopFolderLookup(List metaFilesInDirectory, - bool overwriteLocationNames) - { - metaFilesInDirectory = RemoveNoUpdateItems(metaFilesInDirectory,overwriteLocationNames); - - var subPath = metaFilesInDirectory.FirstOrDefault()?.ParentDirectory; - if ( subPath == null ) return metaFilesInDirectory; - - new GeoCacheStatusService(_cache).StatusUpdate(subPath, metaFilesInDirectory.Count*2, StatusType.Total); - - foreach (var metaFileItem in metaFilesInDirectory.Select( - (value, index) => new { value, index })) - { - - var result = await GetLocation(metaFileItem.value.Latitude, metaFileItem.value.Longitude); - new GeoCacheStatusService(_cache).StatusUpdate(metaFileItem.value.ParentDirectory!, - metaFileItem.index, StatusType.Current); - if ( !result.IsSuccess ) - { - continue; - } - metaFileItem.value.LocationCity = result.LocationCity; - metaFileItem.value.LocationState = result.LocationState; - metaFileItem.value.LocationCountry = result.LocationCountry; - metaFileItem.value.LocationCountryCode = result.LocationCountryCode; - } - - // Ready signal - new GeoCacheStatusService(_cache).StatusUpdate(subPath, - metaFilesInDirectory.Count, StatusType.Current); - - return metaFilesInDirectory; - } - - public async Task GetLocation(double latitude, double longitude) - { - if ( _reverseGeoCode == null ) - { - (_, _reverseGeoCode) = await SetupAsync(); - } - - var status = new GeoLocationModel - { - Longitude = longitude, - Latitude = latitude, - IsSuccess = false, - ErrorReason = "Unknown" - }; - - if ( !ValidateLocation.ValidateLatitudeLongitude(latitude,longitude) ) - { - status.ErrorReason = "Non-valid location"; - return status; - } - - // Create a point from a lat/long pair from which we want to conduct our search(es) (center) - var place = _reverseGeoCode.CreateFromLatLong( - status.Latitude, status.Longitude); - - // Find nearest - var nearestPlace = _reverseGeoCode.NearestNeighbourSearch(place, 1).FirstOrDefault(); - - if ( nearestPlace == null ) { - status.ErrorReason = "No nearest place found"; - return status; - } - - // Distance to avoid non logic locations - var distanceTo = GeoDistanceTo.GetDistance( - nearestPlace.Latitude, - nearestPlace.Longitude, - status.Latitude, - status.Longitude); - - if ( distanceTo > 35 ) - { + return (_admin1CodesAscii, _reverseGeoCode); + } + + internal string? GetAdmin1Name(string countryCode, string[] adminCodes) + { + if ( _admin1CodesAscii == null || adminCodes.Length != 4 ) return null; + + var admin1Code = countryCode + "." + adminCodes[0]; + + var admin2Object = _admin1CodesAscii.FirstOrDefault(p => p.Code == admin1Code); + return admin2Object?.NameASCII; + } + + /// + /// Checks for files that already done + /// if latitude is not location 0,0, That's default + /// If one of the meta items are missing, keep in list + /// If extension in exifTool supported, so no gpx + /// + /// List of files with metadata + /// true = overwrite the location names, that have a gps location + /// list that can be updated + public List RemoveNoUpdateItems(IEnumerable metaFilesInDirectory, + bool overwriteLocationNames) + { + // this will overwrite the location names, that have a gps location + if ( overwriteLocationNames ) + return metaFilesInDirectory.Where( + metaFileItem => + Math.Abs(metaFileItem.Latitude) > 0.001 && Math.Abs(metaFileItem.Longitude) > 0.001) + .ToList(); + + // the default situation + return metaFilesInDirectory.Where( + metaFileItem => + ( ( Math.Abs(metaFileItem.Latitude) > 0.001 && Math.Abs(metaFileItem.Longitude) > 0.001 ) + && ( string.IsNullOrEmpty(metaFileItem.LocationCity) + || string.IsNullOrEmpty(metaFileItem.LocationState) + || string.IsNullOrEmpty(metaFileItem.LocationCountry) + ) ) // for now NO check on: metaFileItem.LocationCountryCode + && ExtensionRolesHelper.IsExtensionExifToolSupported(metaFileItem.FileName) + ).ToList(); + } + + /// + /// Reverse Geo Syncing for a folder + /// + /// list of files to lookup + /// true = overwrite the location names, that have a gps location + /// + public async Task> LoopFolderLookup(List metaFilesInDirectory, + bool overwriteLocationNames) + { + metaFilesInDirectory = RemoveNoUpdateItems(metaFilesInDirectory, overwriteLocationNames); + + var subPath = metaFilesInDirectory.FirstOrDefault()?.ParentDirectory; + if ( subPath == null ) return metaFilesInDirectory; + + new GeoCacheStatusService(_cache).StatusUpdate(subPath, metaFilesInDirectory.Count * 2, StatusType.Total); + + foreach ( var metaFileItem in metaFilesInDirectory.Select( + (value, index) => new { value, index }) ) + { + + var result = await GetLocation(metaFileItem.value.Latitude, metaFileItem.value.Longitude); + new GeoCacheStatusService(_cache).StatusUpdate(metaFileItem.value.ParentDirectory!, + metaFileItem.index, StatusType.Current); + if ( !result.IsSuccess ) + { + continue; + } + metaFileItem.value.LocationCity = result.LocationCity; + metaFileItem.value.LocationState = result.LocationState; + metaFileItem.value.LocationCountry = result.LocationCountry; + metaFileItem.value.LocationCountryCode = result.LocationCountryCode; + } + + // Ready signal + new GeoCacheStatusService(_cache).StatusUpdate(subPath, + metaFilesInDirectory.Count, StatusType.Current); + + return metaFilesInDirectory; + } + + public async Task GetLocation(double latitude, double longitude) + { + if ( _reverseGeoCode == null ) + { + (_, _reverseGeoCode) = await SetupAsync(); + } + + var status = new GeoLocationModel + { + Longitude = longitude, + Latitude = latitude, + IsSuccess = false, + ErrorReason = "Unknown" + }; + + if ( !ValidateLocation.ValidateLatitudeLongitude(latitude, longitude) ) + { + status.ErrorReason = "Non-valid location"; + return status; + } + + // Create a point from a lat/long pair from which we want to conduct our search(es) (center) + var place = _reverseGeoCode.CreateFromLatLong( + status.Latitude, status.Longitude); + + // Find nearest + var nearestPlace = _reverseGeoCode.NearestNeighbourSearch(place, 1).FirstOrDefault(); + + if ( nearestPlace == null ) + { + status.ErrorReason = "No nearest place found"; + return status; + } + + // Distance to avoid non logic locations + var distanceTo = GeoDistanceTo.GetDistance( + nearestPlace.Latitude, + nearestPlace.Longitude, + status.Latitude, + status.Longitude); + + if ( distanceTo > 35 ) + { status.ErrorReason = "Distance to nearest place is too far"; - return status; - } - - status.ErrorReason = "Success"; - status.IsSuccess = true; - status.LocationCity = nearestPlace.NameASCII; - - // Catch is used for example the region VA (Vatican City) - try - { - var region = new RegionInfo(nearestPlace.CountryCode); - status.LocationCountry = region.NativeName; - status.LocationCountryCode = region.ThreeLetterISORegionName; - } - catch ( ArgumentException e ) - { - _logger.LogInformation("[GeoReverseLookup] " + e.Message); - } - - status.LocationState = GetAdmin1Name(nearestPlace.CountryCode, nearestPlace.Admincodes); - - return status; - } - } + return status; + } + + status.ErrorReason = "Success"; + status.IsSuccess = true; + status.LocationCity = nearestPlace.NameASCII; + + // Catch is used for example the region VA (Vatican City) + try + { + var region = new RegionInfo(nearestPlace.CountryCode); + status.LocationCountry = region.NativeName; + status.LocationCountryCode = region.ThreeLetterISORegionName; + } + catch ( ArgumentException e ) + { + _logger.LogInformation("[GeoReverseLookup] " + e.Message); + } + + status.LocationState = GetAdmin1Name(nearestPlace.CountryCode, nearestPlace.Admincodes); + + return status; + } + } } diff --git a/starsky/starsky.feature.health/HealthCheck/DateAssemblyHealthCheck.cs b/starsky/starsky.feature.health/HealthCheck/DateAssemblyHealthCheck.cs index 39de0a879b..9ddecc0061 100644 --- a/starsky/starsky.feature.health/HealthCheck/DateAssemblyHealthCheck.cs +++ b/starsky/starsky.feature.health/HealthCheck/DateAssemblyHealthCheck.cs @@ -26,9 +26,9 @@ public Task CheckHealthAsync( CancellationToken cancellationToken = default(CancellationToken)) { var assemblyDate = DateAssembly.GetBuildDate(Assembly.GetExecutingAssembly()); - return Task.FromResult(assemblyDate.AddDays(-2) > DateTime.UtcNow ? + return Task.FromResult(assemblyDate.AddDays(-2) > DateTime.UtcNow ? HealthCheckResult.Unhealthy($"Current Date {assemblyDate.AddDays(-2)}>" + - $"{DateTime.UtcNow} is earlier then the Assembly is build") : + $"{DateTime.UtcNow} is earlier then the Assembly is build") : HealthCheckResult.Healthy("Current Date is after the Assembly is build :)")); } diff --git a/starsky/starsky.feature.health/HealthCheck/DiskOptionsPercentageSetup.cs b/starsky/starsky.feature.health/HealthCheck/DiskOptionsPercentageSetup.cs index bfa82b68fb..ff0f4893aa 100644 --- a/starsky/starsky.feature.health/HealthCheck/DiskOptionsPercentageSetup.cs +++ b/starsky/starsky.feature.health/HealthCheck/DiskOptionsPercentageSetup.cs @@ -15,9 +15,9 @@ public static void Setup(string fullFilePath, DiskStorageOptions diskOptions, fl { var directoryInfo = new FileInfo(fullFilePath).Directory; if ( directoryInfo == null ) return; - + var tenPercentInBytes = Convert.ToInt64(( ( new DriveInfo(directoryInfo.Root.FullName).TotalFreeSpace / - 1024f ) / 1024f ) * percentage ); + 1024f ) / 1024f ) * percentage); if ( tenPercentInBytes < 100 ) return; diskOptions.AddDrive( diff --git a/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheck.cs b/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheck.cs index bb2dba5e4b..edd72524df 100644 --- a/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheck.cs +++ b/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheck.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -19,7 +18,7 @@ public class DiskStorageHealthCheck : IHealthCheck public DiskStorageHealthCheck(DiskStorageOptions options) { var diskStorageOptions = options; - _options = diskStorageOptions ?? throw new ArgumentNullException(nameof (options)); + _options = diskStorageOptions ?? throw new ArgumentNullException(nameof(options)); } public Task CheckHealthAsync( @@ -28,22 +27,26 @@ public Task CheckHealthAsync( { try { - foreach (var (driveName, num) in _options.ConfiguredDrives.Values) + foreach ( var (driveName, num) in _options.ConfiguredDrives.Values ) { var (exists4, actualFreeMegabytes4) = GetSystemDriveInfo(driveName); - if (!exists4) - return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, + if ( !exists4 ) + return Task.FromResult(new HealthCheckResult( + context.Registration.FailureStatus, "Configured drive " + driveName + " is not present on system")); - if (actualFreeMegabytes4 < num) - return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, + if ( actualFreeMegabytes4 < num ) + return Task.FromResult(new HealthCheckResult( + context.Registration.FailureStatus, $"Minimum configured megabytes for disk {driveName} is {num} " + $"but actual free space are {actualFreeMegabytes4} megabytes")); } + return Task.FromResult(HealthCheckResult.Healthy()); } - catch (Exception ex) + catch ( Exception ex ) { - return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, null, ex)); + return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, + null, ex)); } } @@ -54,15 +57,17 @@ private static (bool Exists, long ActualFreeMegabytes) GetSystemDriveInfo(string { drivesList = DriveInfo.GetDrives(); } - catch (Exception) + catch ( Exception ) { - return ( false, 0L ); + return (false, 0L); } var driveInfo = Array.Find(drivesList, drive => string.Equals(drive.Name, driveName, StringComparison.InvariantCultureIgnoreCase)); - return driveInfo?.AvailableFreeSpace != null ? (true, driveInfo.AvailableFreeSpace / 1024L / 1024L) : (false, 0L); + return driveInfo?.AvailableFreeSpace != null + ? (true, driveInfo.AvailableFreeSpace / 1024L / 1024L) + : (false, 0L); } } } diff --git a/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheckExtensions.cs b/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheckExtensions.cs index dc0869dba6..9c8b978500 100644 --- a/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheckExtensions.cs +++ b/starsky/starsky.feature.health/HealthCheck/DiskStorageHealthCheckExtensions.cs @@ -9,15 +9,15 @@ public static class DiskStorageHealthCheckExtensions { public static IHealthChecksBuilder AddDiskStorageHealthCheck( this IHealthChecksBuilder builder, - Action setup, - string name = null, + Action? setup, + string? name = null, HealthStatus? failureStatus = null, - IEnumerable tags = null, + IEnumerable? tags = null, TimeSpan? timeout = null) { var options = new DiskStorageOptions(); setup?.Invoke(options); - return builder.Add(new HealthCheckRegistration(name ?? "diskstorage", sp => + return builder.Add(new HealthCheckRegistration(name ?? "diskstorage", sp => new DiskStorageHealthCheck(options), failureStatus, tags, timeout)); } } diff --git a/starsky/starsky.feature.health/HealthCheck/DiskStorageOptions.cs b/starsky/starsky.feature.health/HealthCheck/DiskStorageOptions.cs index ea65eefe82..72537d6a1f 100644 --- a/starsky/starsky.feature.health/HealthCheck/DiskStorageOptions.cs +++ b/starsky/starsky.feature.health/HealthCheck/DiskStorageOptions.cs @@ -4,7 +4,7 @@ namespace starsky.feature.health.HealthCheck { public class DiskStorageOptions { - internal Dictionary ConfiguredDrives { get; } = + internal Dictionary ConfiguredDrives { get; } = new Dictionary(); public void AddDrive(string driveName, diff --git a/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheck.cs b/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheck.cs index 5b18244a64..d4ffe993d2 100644 --- a/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheck.cs +++ b/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheck.cs @@ -17,26 +17,26 @@ public class PathExistHealthCheck : IHealthCheck public PathExistHealthCheck(PathExistOptions options) { - var diskStorageOptions = options; - _options = diskStorageOptions ?? throw new ArgumentNullException(nameof(options)); + _options = options ?? throw new ArgumentNullException(nameof(options)); } public Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { - var resultsList = _options.ConfiguredPaths.Select(path => new StorageHostFullPathFilesystem() - .IsFolderOrFile(path)).ToList(); + var resultsList = _options.ConfiguredPaths.Select(path => + new StorageHostFullPathFilesystem() + .IsFolderOrFile(path)).ToList(); if ( resultsList.Count == 0 ) return Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, $"Not configured")); return Task.FromResult( - resultsList.Exists(p => p == FolderOrFileModel.FolderOrFileTypeList.Deleted) ? - new HealthCheckResult(context.Registration.FailureStatus, $"Configured path is not present on system") : - HealthCheckResult.Healthy("Configured path is present")); + resultsList.Exists(p => p == FolderOrFileModel.FolderOrFileTypeList.Deleted) + ? new HealthCheckResult(context.Registration.FailureStatus, + $"Configured path is not present on system") + : HealthCheckResult.Healthy("Configured path is present")); } - } } diff --git a/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheckExtensions.cs b/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheckExtensions.cs index 109147ecf2..c4a5d33f92 100644 --- a/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheckExtensions.cs +++ b/starsky/starsky.feature.health/HealthCheck/PathExistHealthCheckExtensions.cs @@ -9,16 +9,16 @@ public static class PathExistHealthCheckExtensions { public static IHealthChecksBuilder AddPathExistHealthCheck( this IHealthChecksBuilder builder, - Action setup, - string name = null, + Action? setup, + string? name = null, HealthStatus? failureStatus = null, - IEnumerable tags = null, + IEnumerable? tags = null, TimeSpan? timeout = null) { var options = new PathExistOptions(); setup?.Invoke(options); - return builder.Add(new HealthCheckRegistration(name ?? "pathexist", sp => - (IHealthCheck) new PathExistHealthCheck(options), failureStatus, tags, timeout)); + return builder.Add(new HealthCheckRegistration(name ?? "pathexist", sp => + new PathExistHealthCheck(options), failureStatus, tags, timeout)); } } } diff --git a/starsky/starsky.feature.health/HealthCheck/SetupHealthCheck.cs b/starsky/starsky.feature.health/HealthCheck/SetupHealthCheck.cs index 63d34395b3..da4a004892 100644 --- a/starsky/starsky.feature.health/HealthCheck/SetupHealthCheck.cs +++ b/starsky/starsky.feature.health/HealthCheck/SetupHealthCheck.cs @@ -23,58 +23,58 @@ public SetupHealthCheck(AppSettings appSettings, IServiceCollection services) public void BuilderHealth() { _services.AddHealthChecks() - .AddDbContextCheck() - .AddDiskStorageHealthCheck( - setup: diskOptions => - { - DiskOptionsPercentageSetup.Setup(_appSettings.StorageFolder, - diskOptions); - }, - name: "Storage_StorageFolder") - .AddDiskStorageHealthCheck( - setup: diskOptions => - { - DiskOptionsPercentageSetup.Setup(_appSettings.ThumbnailTempFolder, - diskOptions); - }, - name: "Storage_ThumbnailTempFolder") - .AddDiskStorageHealthCheck( - setup: diskOptions => - { - DiskOptionsPercentageSetup.Setup(_appSettings.TempFolder, - diskOptions); - }, - name: "Storage_TempFolder") - .AddPathExistHealthCheck( - setup: pathOptions => pathOptions.AddPath(_appSettings.StorageFolder), - name: "Exist_StorageFolder") - .AddPathExistHealthCheck( - setup: pathOptions => pathOptions.AddPath(_appSettings.TempFolder), - name: "Exist_TempFolder") - .AddPathExistHealthCheck( - setup: pathOptions => pathOptions.AddPath(_appSettings.ExifToolPath), - name: "Exist_ExifToolPath") - .AddPathExistHealthCheck( - setup: pathOptions => pathOptions.AddPath(_appSettings.ThumbnailTempFolder), - name: "Exist_ThumbnailTempFolder") - .AddCheck("DateAssemblyHealthCheck"); - - var healthSqlQuery = "SELECT * FROM `__EFMigrationsHistory` WHERE ProductVersion > 9"; + .AddDbContextCheck() + .AddDiskStorageHealthCheck( + setup: diskOptions => + { + DiskOptionsPercentageSetup.Setup(_appSettings.StorageFolder, + diskOptions); + }, + name: "Storage_StorageFolder") + .AddDiskStorageHealthCheck( + setup: diskOptions => + { + DiskOptionsPercentageSetup.Setup(_appSettings.ThumbnailTempFolder, + diskOptions); + }, + name: "Storage_ThumbnailTempFolder") + .AddDiskStorageHealthCheck( + setup: diskOptions => + { + DiskOptionsPercentageSetup.Setup(_appSettings.TempFolder, + diskOptions); + }, + name: "Storage_TempFolder") + .AddPathExistHealthCheck( + setup: pathOptions => pathOptions.AddPath(_appSettings.StorageFolder), + name: "Exist_StorageFolder") + .AddPathExistHealthCheck( + setup: pathOptions => pathOptions.AddPath(_appSettings.TempFolder), + name: "Exist_TempFolder") + .AddPathExistHealthCheck( + setup: pathOptions => pathOptions.AddPath(_appSettings.ExifToolPath), + name: "Exist_ExifToolPath") + .AddPathExistHealthCheck( + setup: pathOptions => pathOptions.AddPath(_appSettings.ThumbnailTempFolder), + name: "Exist_ThumbnailTempFolder") + .AddCheck("DateAssemblyHealthCheck"); + + var healthSqlQuery = "SELECT * FROM `__EFMigrationsHistory` WHERE ProductVersion > 9"; + + switch ( _appSettings.DatabaseType ) + { + case ( AppSettings.DatabaseTypeList.Mysql ): + _services.AddHealthChecks().AddMySql(_appSettings.DatabaseConnection); + break; + case AppSettings.DatabaseTypeList.Sqlite: + _services.AddHealthChecks().AddSqlite(_appSettings.DatabaseConnection, healthSqlQuery); + break; + case AppSettings.DatabaseTypeList.InMemoryDatabase: + break; + default: + throw new AggregateException("database type does not exist"); + } - switch (_appSettings.DatabaseType) - { - case (AppSettings.DatabaseTypeList.Mysql): - _services.AddHealthChecks().AddMySql(_appSettings.DatabaseConnection); - break; - case AppSettings.DatabaseTypeList.Sqlite: - _services.AddHealthChecks().AddSqlite(_appSettings.DatabaseConnection, healthSqlQuery); - break; - case AppSettings.DatabaseTypeList.InMemoryDatabase: - break; - default: - throw new AggregateException("database type does not exist"); - } - } } } diff --git a/starsky/starsky.feature.health/UpdateCheck/Models/ReleaseModel.cs b/starsky/starsky.feature.health/UpdateCheck/Models/ReleaseModel.cs index 2f3655cd86..0632161320 100644 --- a/starsky/starsky.feature.health/UpdateCheck/Models/ReleaseModel.cs +++ b/starsky/starsky.feature.health/UpdateCheck/Models/ReleaseModel.cs @@ -19,19 +19,20 @@ public class ReleaseModel public bool Draft { get; set; } private string _tagName = string.Empty; - + /// /// Should start with v /// [JsonPropertyName("tag_name")] - public string TagName { + public string TagName + { get => _tagName; set { - if ( string.IsNullOrWhiteSpace(value)) return; + if ( string.IsNullOrWhiteSpace(value) ) return; if ( !value.StartsWith('v') ) Console.WriteLine($"{_tagName} Should start with v"); _tagName = value; - } + } } } } diff --git a/starsky/starsky.feature.health/UpdateCheck/Services/CheckForUpdates.cs b/starsky/starsky.feature.health/UpdateCheck/Services/CheckForUpdates.cs index 2687c7de3f..824313e846 100644 --- a/starsky/starsky.feature.health/UpdateCheck/Services/CheckForUpdates.cs +++ b/starsky/starsky.feature.health/UpdateCheck/Services/CheckForUpdates.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -22,7 +21,7 @@ namespace starsky.feature.health.UpdateCheck.Services public class CheckForUpdates : ICheckForUpdates { internal const string GithubStarskyReleaseApi = "https://api.github.com/repos/qdraw/starsky/releases"; - + private readonly AppSettings? _appSettings; private readonly IMemoryCache? _cache; private readonly IHttpClientHelper _httpClientHelper; @@ -33,7 +32,7 @@ public CheckForUpdates(IHttpClientHelper httpClientHelper, AppSettings? appSetti _appSettings = appSettings; _cache = cache; } - + internal const string QueryCheckForUpdatesCacheName = "CheckForUpdates"; /// @@ -44,53 +43,53 @@ public CheckForUpdates(IHttpClientHelper httpClientHelper, AppSettings? appSetti [SuppressMessage("Usage", "S2589:cache & appSettings null")] public async Task> IsUpdateNeeded(string currentVersion = "") { - if (_appSettings == null || _appSettings.CheckForUpdates == false ) - return new KeyValuePair(UpdateStatus.Disabled,""); + if ( _appSettings == null || _appSettings.CheckForUpdates == false ) + return new KeyValuePair(UpdateStatus.Disabled, ""); currentVersion = string.IsNullOrWhiteSpace(currentVersion) - ? _appSettings.AppVersion : currentVersion; - + ? _appSettings.AppVersion : currentVersion; + // The CLI programs uses no cache if ( _cache == null || _appSettings?.AddMemoryCache != true ) { - return Parse(await QueryIsUpdateNeededAsync(),currentVersion); + return Parse(await QueryIsUpdateNeededAsync(), currentVersion); } - if ( _cache.TryGetValue(QueryCheckForUpdatesCacheName, - out var cacheResult) && cacheResult != null ) + if ( _cache.TryGetValue(QueryCheckForUpdatesCacheName, + out var cacheResult) && cacheResult != null ) { - return Parse(( List ) cacheResult, currentVersion); + return Parse(( List )cacheResult, currentVersion); } cacheResult = await QueryIsUpdateNeededAsync(); - _cache.Set(QueryCheckForUpdatesCacheName, cacheResult, - new TimeSpan(48,0,0)); + _cache.Set(QueryCheckForUpdatesCacheName, cacheResult, + new TimeSpan(48, 0, 0)); - return Parse(( List? ) cacheResult,currentVersion); + return Parse(( List? )cacheResult, currentVersion); } internal async Task?> QueryIsUpdateNeededAsync() { // argument check is done in QueryIsUpdateNeeded var (key, value) = await _httpClientHelper.ReadString(GithubStarskyReleaseApi); - return !key ? new List() : + return !key ? new List() : JsonSerializer.Deserialize>(value, DefaultJsonSerializer.CamelCase); } - - internal static KeyValuePair Parse(IEnumerable? releaseModelList, - string currentVersion ) + + internal static KeyValuePair Parse(IEnumerable? releaseModelList, + string currentVersion) { - var orderedReleaseModelList = + var orderedReleaseModelList = releaseModelList?.OrderByDescending(p => p.TagName); - + var tagName = orderedReleaseModelList? .FirstOrDefault(p => p is { Draft: false, PreRelease: false })?.TagName; - + if ( string.IsNullOrWhiteSpace(tagName) || - !tagName.StartsWith('v') ) + !tagName.StartsWith('v') ) { - return new KeyValuePair(UpdateStatus.NoReleasesFound,string.Empty); + return new KeyValuePair(UpdateStatus.NoReleasesFound, string.Empty); } try @@ -101,7 +100,7 @@ internal static KeyValuePair Parse(IEnumerable(status, latestVersion.ToString()); } - catch ( ArgumentException) + catch ( ArgumentException ) { return new KeyValuePair(UpdateStatus.InputNotValid, string.Empty); } diff --git a/starsky/starsky.feature.health/starsky.feature.health.csproj b/starsky/starsky.feature.health/starsky.feature.health.csproj index c45bb8f514..434c13c2c7 100644 --- a/starsky/starsky.feature.health/starsky.feature.health.csproj +++ b/starsky/starsky.feature.health/starsky.feature.health.csproj @@ -5,21 +5,22 @@ {d9c8e6e0-2526-4978-ad8c-b4e74993cfd8} starsky.feature.health 0.6.0-beta.0 + enable - + - - + + - + - - + + - + build$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss")) diff --git a/starsky/starsky.feature.import/Helpers/UpdateImportTransformations.cs b/starsky/starsky.feature.import/Helpers/UpdateImportTransformations.cs index 007f4dfd88..713d2afa12 100644 --- a/starsky/starsky.feature.import/Helpers/UpdateImportTransformations.cs +++ b/starsky/starsky.feature.import/Helpers/UpdateImportTransformations.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; using System.Threading.Tasks; using starsky.foundation.database.Interfaces; @@ -24,8 +23,9 @@ public class UpdateImportTransformations private readonly AppSettings _appSettings; private readonly IThumbnailQuery _thumbnailQuery; - public UpdateImportTransformations(IWebLogger logger, - IExifTool exifTool, ISelectorStorage selectorStorage, AppSettings appSettings, IThumbnailQuery thumbnailQuery) + public UpdateImportTransformations(IWebLogger logger, + IExifTool exifTool, ISelectorStorage selectorStorage, AppSettings appSettings, + IThumbnailQuery thumbnailQuery) { _logger = logger; _exifTool = exifTool; @@ -35,12 +35,14 @@ public UpdateImportTransformations(IWebLogger logger, _thumbnailQuery = thumbnailQuery; } - + public delegate Task QueryUpdateDelegate(FileIndexItem fileIndexItem); - public delegate Task?> QueryThumbnailUpdateDelegate(List thumbnailItems); + + public delegate Task?> QueryThumbnailUpdateDelegate( + List thumbnailItems); /// - /// Run Transformation on Import to the files in the database && Update fileHash in database + /// Run Transformation on Import to the files in the database and Update fileHash in database /// /// /// information @@ -53,18 +55,21 @@ internal async Task UpdateTransformations( int colorClassTransformation, bool dateTimeParsedFromFileName, bool indexMode) { - if ( !ExtensionRolesHelper.IsExtensionExifToolSupported(fileIndexItem.FileName) ) return fileIndexItem; + if ( !ExtensionRolesHelper.IsExtensionExifToolSupported(fileIndexItem.FileName) ) + return fileIndexItem; var comparedNamesList = new List(); if ( dateTimeParsedFromFileName ) { - _logger.LogInformation($"[Import] DateTimeParsedFromFileName ExifTool Sync {fileIndexItem.FilePath}"); + _logger.LogInformation( + $"[Import] DateTimeParsedFromFileName ExifTool Sync {fileIndexItem.FilePath}"); comparedNamesList = DateTimeParsedComparedNamesList(); } if ( colorClassTransformation >= 0 ) { - _logger.LogInformation($"[Import] ColorClassComparedNamesList ExifTool Sync {fileIndexItem.FilePath}"); + _logger.LogInformation( + $"[Import] ColorClassComparedNamesList ExifTool Sync {fileIndexItem.FilePath}"); comparedNamesList = ColorClassComparedNamesList(comparedNamesList); } @@ -84,15 +89,17 @@ internal async Task UpdateTransformations( { return fileIndexItem; } - + // Hash is changed after transformation - fileIndexItem.FileHash = (await new FileHash(_subPathStorage).GetHashCodeAsync(fileIndexItem.FilePath!)).Key; + fileIndexItem.FileHash = + ( await new FileHash(_subPathStorage).GetHashCodeAsync(fileIndexItem.FilePath!) ) + .Key; await queryUpdateDelegate(fileIndexItem); return fileIndexItem.Clone(); } - + internal static List DateTimeParsedComparedNamesList() { return new List @@ -101,13 +108,11 @@ internal static List DateTimeParsedComparedNamesList() nameof(FileIndexItem.DateTime).ToLowerInvariant(), }; } - + internal static List ColorClassComparedNamesList(List list) { list.Add(nameof(FileIndexItem.ColorClass).ToLowerInvariant()); return list; } - } } - diff --git a/starsky/starsky.feature.import/Interfaces/IImport.cs b/starsky/starsky.feature.import/Interfaces/IImport.cs index b9032008c6..4f0fc94acb 100644 --- a/starsky/starsky.feature.import/Interfaces/IImport.cs +++ b/starsky/starsky.feature.import/Interfaces/IImport.cs @@ -1,29 +1,29 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using starsky.feature.import.Models; using starsky.foundation.database.Models; namespace starsky.feature.import.Interfaces { - public interface IImport - { - /// - /// Test if file can be imported - /// - /// list of paths - /// settings - /// - Task> Preflight(List fullFilePathsList, - ImportSettingsModel importSettings); + public interface IImport + { + /// + /// Test if file can be imported + /// + /// list of paths + /// settings + /// + Task> Preflight(List fullFilePathsList, + ImportSettingsModel importSettings); - /// - /// Run Import - /// - /// list of paths - /// settings - /// - Task> Importer(IEnumerable inputFullPathList, - ImportSettingsModel importSettings); + /// + /// Run Import + /// + /// list of paths + /// settings + /// + Task> Importer(IEnumerable inputFullPathList, + ImportSettingsModel importSettings); - } + } } diff --git a/starsky/starsky.feature.import/Models/ImportFileSettingsModel.cs b/starsky/starsky.feature.import/Models/ImportFileSettingsModel.cs index 27cba262b0..0119b4a631 100644 --- a/starsky/starsky.feature.import/Models/ImportFileSettingsModel.cs +++ b/starsky/starsky.feature.import/Models/ImportFileSettingsModel.cs @@ -1,101 +1,106 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using starsky.foundation.platform.Models; namespace starsky.feature.import.Models { - public class ImportSettingsModel - { - // Default constructor - public ImportSettingsModel() - { - DeleteAfter = false; - RecursiveDirectory = false; - IndexMode = true; - // ColorClass defaults in prop - // Structure defaults in appSettings - } - - /// - /// Construct model using a request - /// - /// - public ImportSettingsModel(HttpRequest request) - { - // the header defaults to zero, and that's not the correct default value - if ( !string.IsNullOrWhiteSpace(request.Headers["ColorClass"]) && - int.TryParse(request.Headers["ColorClass"], out var colorClassNumber)) - { + public class ImportSettingsModel + { + // Default constructor + public ImportSettingsModel() + { + DeleteAfter = false; + RecursiveDirectory = false; + IndexMode = true; + // ColorClass defaults in prop + // Structure defaults in appSettings + } + + /// + /// Construct model using a request + /// + /// + public ImportSettingsModel(HttpRequest request) + { + // the header defaults to zero, and that's not the correct default value + if ( !string.IsNullOrWhiteSpace(request.Headers["ColorClass"]) && + int.TryParse(request.Headers["ColorClass"], out var colorClassNumber) ) + { ColorClass = colorClassNumber; - } - - Structure = request.Headers["Structure"].ToString(); - - // Always when importing using a request - // otherwise it will stick in the temp folder - DeleteAfter = true; - - // For the index Mode, false is always copy, true is check if exist in db, default true - IndexMode = true; - - if ( request.Headers["IndexMode"].ToString().Equals("false", - System.StringComparison.CurrentCultureIgnoreCase) ) - { - IndexMode = false; - } - - } - - - // This is optional, when not in use ignore this setting - private string _structure; - public string Structure - { - get => string.IsNullOrEmpty(_structure) ? string.Empty : _structure; // if null>stringEmpty - set - { - // Changed this => value used te be without check - if (string.IsNullOrEmpty(value)) return; - AppSettings.StructureCheck(value); - _structure = value; - } - } - - public bool DeleteAfter { get; set; } - - public bool RecursiveDirectory { get; set; } - - /// - /// -1 is ignore - /// - private int _colorClass = -1; - - /// - /// Overwrite ColorClass settings - /// Int value between 0 and 8 - /// - public int ColorClass { - get => _colorClass; - set { - if (value is >= 0 and <= 8) // hardcoded in FileIndexModel - { - _colorClass = value; - return; - } - _colorClass = -1; - } - } - - /// - /// indexing, false is always copy, true is check if exist in db, - /// default true - /// - public bool IndexMode { get; set; } - - public ConsoleOutputMode ConsoleOutputMode { get; set; } - - public bool IsConsoleOutputModeDefault() - { - return ConsoleOutputMode.Default == ConsoleOutputMode; - } - } + } + + Structure = request.Headers["Structure"].ToString(); + + // Always when importing using a request + // otherwise it will stick in the temp folder + DeleteAfter = true; + + // For the index Mode, false is always copy, true is check if exist in db, default true + IndexMode = true; + + if ( request.Headers["IndexMode"].ToString().Equals("false", + System.StringComparison.CurrentCultureIgnoreCase) ) + { + IndexMode = false; + } + } + + + // This is optional, when not in use ignore this setting + private string _structure = string.Empty; + + public string Structure + { + get => string.IsNullOrEmpty(_structure) + ? string.Empty + : _structure; // if null>stringEmpty + set + { + // Changed this => value used te be without check + if ( string.IsNullOrEmpty(value) ) return; + AppSettings.StructureCheck(value); + _structure = value; + } + } + + public bool DeleteAfter { get; set; } + + public bool RecursiveDirectory { get; set; } + + /// + /// -1 is ignore + /// + private int _colorClass = -1; + + /// + /// Overwrite ColorClass settings + /// Int value between 0 and 8 + /// + public int ColorClass + { + get => _colorClass; + set + { + if ( value is >= 0 and <= 8 ) // hardcoded in FileIndexModel + { + _colorClass = value; + return; + } + + _colorClass = -1; + } + } + + /// + /// indexing, false is always copy, true is check if exist in db, + /// default true + /// + public bool IndexMode { get; set; } + + public ConsoleOutputMode ConsoleOutputMode { get; set; } + + public bool IsConsoleOutputModeDefault() + { + return ConsoleOutputMode.Default == ConsoleOutputMode; + } + } } diff --git a/starsky/starsky.feature.import/Services/Import.cs b/starsky/starsky.feature.import/Services/Import.cs index 918302ff19..a6a2d6f8f5 100644 --- a/starsky/starsky.feature.import/Services/Import.cs +++ b/starsky/starsky.feature.import/Services/Import.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -34,6 +33,7 @@ using starsky.foundation.writemeta.Services; [assembly: InternalsVisibleTo("starskytest")] + namespace starsky.feature.import.Services { /// @@ -43,7 +43,7 @@ namespace starsky.feature.import.Services public class Import : IImport { private readonly IImportQuery? _importQuery; - + // storage providers private readonly IStorage _filesystemStorage; private readonly IStorage _subPathStorage; @@ -54,7 +54,7 @@ public class Import : IImport private readonly ReadMeta _readMetaHost; private readonly IExifTool _exifTool; private readonly IQuery _query; - + private readonly IConsole _console; private readonly IMetaExifThumbnailService _metaExifThumbnailService; @@ -69,7 +69,8 @@ public class Import : IImport /// internal const string MessageDateTimeBasedOnFilename = "Date and Time based on filename"; - [SuppressMessage("Usage", "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] + [SuppressMessage("Usage", + "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] public Import( ISelectorStorage selectorStorage, AppSettings appSettings, @@ -84,22 +85,24 @@ public Import( IServiceScopeFactory? serviceScopeFactory = null) { _importQuery = importQuery; - - _filesystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); - _subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); - - _appSettings = appSettings; - _readMetaHost = new ReadMeta(_filesystemStorage, appSettings, null!, logger); - _exifTool = exifTool; - _query = query; - _console = console; - _metaExifThumbnailService = metaExifThumbnailService; - _memoryCache = memoryCache; - _serviceScopeFactory = serviceScopeFactory; - _logger = logger; - _updateImportTransformations = new UpdateImportTransformations(logger, _exifTool, selectorStorage, appSettings, thumbnailQuery); - _thumbnailQuery = thumbnailQuery; + + _filesystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + + _appSettings = appSettings; + _readMetaHost = new ReadMeta(_filesystemStorage, appSettings, null!, logger); + _exifTool = exifTool; + _query = query; + _console = console; + _metaExifThumbnailService = metaExifThumbnailService; + _memoryCache = memoryCache; + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + _updateImportTransformations = new UpdateImportTransformations(logger, _exifTool, + selectorStorage, appSettings, thumbnailQuery); + _thumbnailQuery = thumbnailQuery; } /// @@ -109,49 +112,53 @@ public Import( /// paths /// settings /// - public async Task> Preflight(List fullFilePathsList, + public async Task> Preflight(List fullFilePathsList, ImportSettingsModel importSettings) { var includedDirectoryFilePaths = AppendDirectoryFilePaths( - fullFilePathsList, + fullFilePathsList, importSettings).ToList(); - + // When Directory is Empty if ( includedDirectoryFilePaths.Count == 0 ) { return new List(); } - - var importIndexItemsList = (await includedDirectoryFilePaths + + var importIndexItemsList = ( await includedDirectoryFilePaths .ForEachAsync( - async (includedFilePath) + async (includedFilePath) => await PreflightPerFile(includedFilePath, importSettings), - _appSettings.MaxDegreesOfParallelism))!.ToList(); - + _appSettings.MaxDegreesOfParallelism) )!.ToList(); + var directoriesContent = ParentFoldersDictionary(importIndexItemsList); - importIndexItemsList = CheckForDuplicateNaming(importIndexItemsList.ToList(), directoriesContent); + importIndexItemsList = + CheckForDuplicateNaming(importIndexItemsList.ToList(), directoriesContent); CheckForReadOnlyFileSystems(importIndexItemsList, importSettings.DeleteAfter); return importIndexItemsList; } - internal List>> CheckForReadOnlyFileSystems( List importIndexItemsList, bool deleteAfter = true) + internal List>> CheckForReadOnlyFileSystems( + List importIndexItemsList, bool deleteAfter = true) { if ( !deleteAfter ) { return new List>>(); } - + var parentFolders = new List>>(); - foreach ( var itemSourceFullFilePath in importIndexItemsList.Select(item => item.SourceFullFilePath) ) + foreach ( var itemSourceFullFilePath in importIndexItemsList.Select(item => + item.SourceFullFilePath) ) { var parentFolder = Directory.GetParent(itemSourceFullFilePath) ?.FullName; if ( parentFolders.TrueForAll(p => p.Item1 != parentFolder) ) { - parentFolders.Add(new Tuple>(parentFolder, new List{itemSourceFullFilePath})); + parentFolders.Add(new Tuple>(parentFolder, + new List { itemSourceFullFilePath })); continue; } @@ -163,24 +170,25 @@ public async Task> Preflight(List fullFilePathsLis { var fileStorageInfo = _filesystemStorage.Info(parentFolder.Item1!); if ( fileStorageInfo.IsFolderOrFile != - FolderOrFileModel.FolderOrFileTypeList.Folder || - fileStorageInfo.IsFileSystemReadOnly != true ) + FolderOrFileModel.FolderOrFileTypeList.Folder || + fileStorageInfo.IsFileSystemReadOnly != true ) { continue; } - var items = parentFolder.Item2.Select(parentItem => - Array.Find(importIndexItemsList.ToArray(), - p => p.SourceFullFilePath == parentItem)).Cast(); - + var items = parentFolder.Item2.Select(parentItem => + Array.Find(importIndexItemsList.ToArray(), + p => p.SourceFullFilePath == parentItem)).Cast(); + foreach ( var item in items ) { importIndexItemsList[importIndexItemsList.IndexOf(item)] .Status = ImportStatus.ReadOnlyFileSystem; - + if ( _appSettings.IsVerbose() ) { - _console.WriteLine($"🤷🗜️ Ignored, source file system is readonly try without move to copy {item.SourceFullFilePath}"); + _console.WriteLine( + $"🤷🗜️ Ignored, source file system is readonly try without move to copy {item.SourceFullFilePath}"); } } } @@ -194,18 +202,25 @@ public async Task> Preflight(List fullFilePathsLis /// /// files to import, use FileIndexItem.ParentDirectory and status Ok /// All parent folders with content - internal Dictionary> ParentFoldersDictionary(List importIndexItemsList) + internal Dictionary> ParentFoldersDictionary( + List importIndexItemsList) { - var directoriesContent = new Dictionary>(); - foreach ( var importIndexItemFileIndexItemParentDirectory in importIndexItemsList.Where(p => - p.Status == ImportStatus.Ok).Select(p => p.FileIndexItem?.ParentDirectory) ) + var directoriesContent = new Dictionary>(); + foreach ( var importIndexItemFileIndexItemParentDirectory in importIndexItemsList.Where( + p => + p.Status == ImportStatus.Ok) + .Select(p => p.FileIndexItem?.ParentDirectory) ) { - if ( importIndexItemFileIndexItemParentDirectory == null || directoriesContent.ContainsKey(importIndexItemFileIndexItemParentDirectory) ) + if ( importIndexItemFileIndexItemParentDirectory == null || + directoriesContent.ContainsKey(importIndexItemFileIndexItemParentDirectory) ) continue; - + var parentDirectoryList = - _subPathStorage.GetAllFilesInDirectory(importIndexItemFileIndexItemParentDirectory).ToList(); - directoriesContent.Add(importIndexItemFileIndexItemParentDirectory, parentDirectoryList); + _subPathStorage + .GetAllFilesInDirectory(importIndexItemFileIndexItemParentDirectory) + .ToList(); + directoriesContent.Add(importIndexItemFileIndexItemParentDirectory, + parentDirectoryList); } return directoriesContent; @@ -218,108 +233,120 @@ internal Dictionary> ParentFoldersDictionary(ListDictionary of all parent folders /// updated ImportIndexItem list /// when there are to many files with the same name - internal List CheckForDuplicateNaming(List importIndexItemsList, - Dictionary> directoriesContent) + internal List CheckForDuplicateNaming( + List importIndexItemsList, + Dictionary> directoriesContent) { - foreach ( var importIndexItem in importIndexItemsList.Where(p => - p.Status == ImportStatus.Ok ) ) + foreach ( var importIndexItem in importIndexItemsList.Where(p => + p.Status == ImportStatus.Ok) ) { if ( importIndexItem.FileIndexItem == null ) { _logger.LogInformation("[CheckForDuplicateNaming] FileIndexItem is missing"); continue; } - + // Try again until the max var updatedFilePath = ""; var indexer = 0; for ( var i = 0; i < MaxTryGetDestinationPath; i++ ) { updatedFilePath = AppendIndexerToFilePath( - importIndexItem.FileIndexItem!.ParentDirectory!, + importIndexItem.FileIndexItem!.ParentDirectory!, importIndexItem.FileIndexItem!.FileName!, indexer); - + var currentDirectoryContent = directoriesContent[importIndexItem.FileIndexItem.ParentDirectory!]; - - if ( currentDirectoryContent.Contains(updatedFilePath) ) + + if ( currentDirectoryContent.Contains(updatedFilePath) ) { indexer++; continue; } + currentDirectoryContent.Add(updatedFilePath); break; } - + if ( indexer >= MaxTryGetDestinationPath || string.IsNullOrEmpty(updatedFilePath) ) { throw new AggregateException($"tried after {MaxTryGetDestinationPath} times"); } - + importIndexItem.FileIndexItem!.FilePath = updatedFilePath; importIndexItem.FileIndexItem.FileName = PathHelper.GetFileName(updatedFilePath); importIndexItem.FilePath = updatedFilePath; } + return importIndexItemsList; } - + /// /// To Add files form directory to list /// /// full file Path /// settings to add recursive - private List> AppendDirectoryFilePaths(List fullFilePathsList, + private List> AppendDirectoryFilePaths( + List fullFilePathsList, ImportSettingsModel importSettings) { - var includedDirectoryFilePaths = new List>(); + var includedDirectoryFilePaths = new List>(); foreach ( var fullFilePath in fullFilePathsList ) { - if ( _filesystemStorage.ExistFolder(fullFilePath) && importSettings.RecursiveDirectory) + if ( _filesystemStorage.ExistFolder(fullFilePath) && + importSettings.RecursiveDirectory ) { // recursive - includedDirectoryFilePaths.AddRange(_filesystemStorage. - GetAllFilesInDirectoryRecursive(fullFilePath) + includedDirectoryFilePaths.AddRange(_filesystemStorage + .GetAllFilesInDirectoryRecursive(fullFilePath) .Where(ExtensionRolesHelper.IsExtensionSyncSupported) .Select(syncedFiles => new KeyValuePair(syncedFiles, true))); continue; } - if ( _filesystemStorage.ExistFolder(fullFilePath) && !importSettings.RecursiveDirectory) + + if ( _filesystemStorage.ExistFolder(fullFilePath) && + !importSettings.RecursiveDirectory ) { // non-recursive - includedDirectoryFilePaths.AddRange(_filesystemStorage.GetAllFilesInDirectory(fullFilePath) + includedDirectoryFilePaths.AddRange(_filesystemStorage + .GetAllFilesInDirectory(fullFilePath) .Where(ExtensionRolesHelper.IsExtensionSyncSupported) .Select(syncedFiles => new KeyValuePair(syncedFiles, true))); continue; } - + includedDirectoryFilePaths.Add( - new KeyValuePair(fullFilePath,_filesystemStorage.ExistFile(fullFilePath)) + new KeyValuePair(fullFilePath, + _filesystemStorage.ExistFile(fullFilePath)) ); } return includedDirectoryFilePaths; } - internal async Task PreflightPerFile(KeyValuePair inputFileFullPath, + internal async Task PreflightPerFile( + KeyValuePair inputFileFullPath, ImportSettingsModel importSettings) { if ( _appSettings.ImportIgnore.Exists(p => inputFileFullPath.Key.Contains(p)) ) { ConsoleIfVerbose($"❌ skip due rules: {inputFileFullPath.Key} "); - return new ImportIndexItem{ - Status = ImportStatus.Ignore, + return new ImportIndexItem + { + Status = ImportStatus.Ignore, FilePath = inputFileFullPath.Key, SourceFullFilePath = inputFileFullPath.Key, AddToDatabase = DateTime.UtcNow }; } - + if ( !inputFileFullPath.Value || !_filesystemStorage.ExistFile(inputFileFullPath.Key) ) { ConsoleIfVerbose($"❌ not found: {inputFileFullPath.Key}"); - return new ImportIndexItem{ - Status = ImportStatus.NotFound, + return new ImportIndexItem + { + Status = ImportStatus.NotFound, FilePath = inputFileFullPath.Key, SourceFullFilePath = inputFileFullPath.Key, AddToDatabase = DateTime.UtcNow @@ -327,63 +354,65 @@ internal async Task PreflightPerFile(KeyValuePair } var imageFormat = ExtensionRolesHelper.GetImageFormat( - _filesystemStorage.ReadStream(inputFileFullPath.Key, - 160)); - + _filesystemStorage.ReadStream(inputFileFullPath.Key, + 160)); + // Check if extension is correct && Check if the file is correct if ( !ExtensionRolesHelper.IsExtensionSyncSupported(inputFileFullPath.Key) || - !ExtensionRolesHelper.IsExtensionSyncSupported($".{imageFormat}") ) + !ExtensionRolesHelper.IsExtensionSyncSupported($".{imageFormat}") ) { ConsoleIfVerbose($"❌ extension not supported: {inputFileFullPath.Key}"); return new ImportIndexItem { - Status = ImportStatus.FileError, - FilePath = inputFileFullPath.Key, + Status = ImportStatus.FileError, + FilePath = inputFileFullPath.Key, SourceFullFilePath = inputFileFullPath.Key }; } - - var hashList = await + + var hashList = await new FileHash(_filesystemStorage).GetHashCodeAsync(inputFileFullPath.Key); if ( !hashList.Value ) { ConsoleIfVerbose($"❌ FileHash error {inputFileFullPath.Key}"); return new ImportIndexItem { - Status = ImportStatus.FileError, + Status = ImportStatus.FileError, FilePath = inputFileFullPath.Key, SourceFullFilePath = inputFileFullPath.Key }; } - - if (importSettings.IndexMode && await _importQuery!.IsHashInImportDbAsync(hashList.Key) ) + + if ( importSettings.IndexMode && + await _importQuery!.IsHashInImportDbAsync(hashList.Key) ) { ConsoleIfVerbose($"🤷 Ignored, exist already {inputFileFullPath.Key}"); return new ImportIndexItem { - Status = ImportStatus.IgnoredAlreadyImported, + Status = ImportStatus.IgnoredAlreadyImported, FilePath = inputFileFullPath.Key, FileHash = hashList.Key, AddToDatabase = DateTime.UtcNow, SourceFullFilePath = inputFileFullPath.Key }; - } - + } + // Only accept files with correct meta data // Check if there is a xmp file that contains data - var fileIndexItem = await _readMetaHost.ReadExifAndXmpFromFileAsync(inputFileFullPath.Key); - + var fileIndexItem = + await _readMetaHost.ReadExifAndXmpFromFileAsync(inputFileFullPath.Key); + // Parse the filename and create a new importIndexItem object - var importIndexItem = ObjectCreateIndexItem(inputFileFullPath.Key, imageFormat, + var importIndexItem = ObjectCreateIndexItem(inputFileFullPath.Key, imageFormat, hashList.Key, fileIndexItem!, importSettings.ColorClass, _filesystemStorage.Info(inputFileFullPath.Key).Size); - + // Update the parent and filenames importIndexItem = ApplyStructure(importIndexItem, importSettings.Structure); - + return importIndexItem; } - + private void ConsoleIfVerbose(string message) { if ( _appSettings.IsVerbose() ) @@ -404,12 +433,12 @@ private void ConsoleIfVerbose(string message) /// Add filesize in bytes /// private ImportIndexItem ObjectCreateIndexItem( - string inputFileFullPath, - ExtensionRolesHelper.ImageFormat imageFormat, - string fileHashCode, - FileIndexItem fileIndexItem, - int colorClassTransformation, - long size) + string inputFileFullPath, + ExtensionRolesHelper.ImageFormat imageFormat, + string fileHashCode, + FileIndexItem fileIndexItem, + int colorClassTransformation, + long size) { var importIndexItem = new ImportIndexItem(_appSettings) { @@ -421,11 +450,12 @@ private ImportIndexItem ObjectCreateIndexItem( FilePath = fileIndexItem.FilePath, ColorClass = fileIndexItem.ColorClass }; - + // used for files without a Exif Date for example WhatsApp images if ( fileIndexItem.DateTime.Year == 1 ) { - importIndexItem.FileIndexItem.DateTime = importIndexItem.ParseDateTimeFromFileName(); + importIndexItem.FileIndexItem.DateTime = + importIndexItem.ParseDateTimeFromFileName(); // used to sync exifTool and to let the user know that the transformation has been applied importIndexItem.FileIndexItem.Description = MessageDateTimeBasedOnFilename; // only set when date is parsed if not ignore update @@ -436,12 +466,12 @@ private ImportIndexItem ObjectCreateIndexItem( } // Also add Camera brand to list - importIndexItem.MakeModel = importIndexItem.FileIndexItem.MakeModel; - + importIndexItem.MakeModel = importIndexItem.FileIndexItem.MakeModel; + // AddToDatabase is Used by the importer History agent importIndexItem.FileIndexItem.AddToDatabase = DateTime.UtcNow; importIndexItem.AddToDatabase = DateTime.UtcNow; - + importIndexItem.FileIndexItem.Size = size; importIndexItem.FileIndexItem.FileHash = fileHashCode; importIndexItem.FileIndexItem.ImageFormat = imageFormat; @@ -449,7 +479,7 @@ private ImportIndexItem ObjectCreateIndexItem( if ( colorClassTransformation < 0 ) return importIndexItem; // only when set in ImportSettingsModel - var colorClass = ( ColorClassParser.Color ) colorClassTransformation; + var colorClass = ( ColorClassParser.Color )colorClassTransformation; importIndexItem.FileIndexItem.ColorClass = colorClass; importIndexItem.ColorClass = colorClass; return importIndexItem; @@ -461,42 +491,47 @@ private ImportIndexItem ObjectCreateIndexItem( /// /// to overwrite, keep empty to ignore /// Names applied to FileIndexItem - private ImportIndexItem ApplyStructure(ImportIndexItem importIndexItem, string overwriteStructure) + private ImportIndexItem ApplyStructure(ImportIndexItem importIndexItem, + string overwriteStructure) { importIndexItem.Structure = _appSettings.Structure; - + // Feature to overwrite structures when importing using a header // Overwrite the structure in the ImportIndexItem - if (!string.IsNullOrWhiteSpace(overwriteStructure)) + if ( !string.IsNullOrWhiteSpace(overwriteStructure) ) { importIndexItem.Structure = overwriteStructure; } - + var structureService = new StructureService(_subPathStorage, importIndexItem.Structure); - + importIndexItem.FileIndexItem!.ParentDirectory = structureService.ParseSubfolders( - importIndexItem.FileIndexItem.DateTime, importIndexItem.FileIndexItem.FileCollectionName!, - FilenamesHelper.GetFileExtensionWithoutDot(importIndexItem.FileIndexItem.FileName!)); - + importIndexItem.FileIndexItem.DateTime, + importIndexItem.FileIndexItem.FileCollectionName!, + FilenamesHelper.GetFileExtensionWithoutDot(importIndexItem.FileIndexItem + .FileName!)); + importIndexItem.FileIndexItem.FileName = structureService.ParseFileName( - importIndexItem.FileIndexItem.DateTime, importIndexItem.FileIndexItem.FileCollectionName!, - FilenamesHelper.GetFileExtensionWithoutDot(importIndexItem.FileIndexItem.FileName!)); + importIndexItem.FileIndexItem.DateTime, + importIndexItem.FileIndexItem.FileCollectionName!, + FilenamesHelper.GetFileExtensionWithoutDot(importIndexItem.FileIndexItem + .FileName!)); importIndexItem.FilePath = importIndexItem.FileIndexItem.FilePath; - + return importIndexItem; } - + /// /// Run import on list of files and folders (full path style) /// /// list of files and folders (full path style) /// settings /// status object - public async Task> Importer(IEnumerable inputFullPathList, + public async Task> Importer(IEnumerable inputFullPathList, ImportSettingsModel importSettings) { var preflightItemList = await Preflight(inputFullPathList.ToList(), importSettings); - + // When directory is empty if ( preflightItemList.Count == 0 ) { @@ -506,12 +541,12 @@ public async Task> Importer(IEnumerable inputFullP var directoriesContent = ParentFoldersDictionary(preflightItemList); if ( importSettings.IndexMode ) await CreateParentFolders(directoriesContent); - var importIndexItemsList = (await preflightItemList.AsEnumerable() + var importIndexItemsList = ( await preflightItemList.AsEnumerable() .ForEachAsync( - async (preflightItem) + async (preflightItem) => await Importer(preflightItem, importSettings), - _appSettings.MaxDegreesOfParallelism))!.ToList(); - + _appSettings.MaxDegreesOfParallelism) )!.ToList(); + return importIndexItemsList; } @@ -521,23 +556,26 @@ public async Task> Importer(IEnumerable inputFullP /// /// /// - internal async Task> CreateMataThumbnail(IEnumerable - importIndexItemsList, ImportSettingsModel importSettings) + internal async Task> CreateMataThumbnail( + IEnumerable + importIndexItemsList, ImportSettingsModel importSettings) { if ( _appSettings.MetaThumbnailOnImport == false || - !importSettings.IndexMode ) + !importSettings.IndexMode ) { return new List<(bool, bool, string, string?)>(); } - + var items = importIndexItemsList .Where(p => p.Status == ImportStatus.Ok) - .Select(p => (p.FilePath, p.FileIndexItem!.FileHash)).Cast<(string,string)>().ToList(); - + .Select(p => (p.FilePath, p.FileIndexItem!.FileHash)).Cast<(string, string)>() + .ToList(); + if ( items.Count == 0 ) { return new List<(bool, bool, string, string?)>(); } + return await _metaExifThumbnailService.AddMetaThumbnail(items); } @@ -548,81 +586,94 @@ public async Task> Importer(IEnumerable inputFullP /// config file /// optional settings /// status - internal async Task Importer(ImportIndexItem? importIndexItem, + internal async Task Importer(ImportIndexItem? importIndexItem, ImportSettingsModel importSettings) { if ( importIndexItem is not { Status: ImportStatus.Ok } ) return importIndexItem!; // True when exist and file type is raw var xmpExistForThisFileType = ExistXmpSidecarForThisFileType(importIndexItem); - - if ( xmpExistForThisFileType || (_appSettings.ExifToolImportXmpCreate - && ExtensionRolesHelper.IsExtensionForceXmp(importIndexItem.FilePath))) + + if ( xmpExistForThisFileType || ( _appSettings.ExifToolImportXmpCreate + && ExtensionRolesHelper.IsExtensionForceXmp( + importIndexItem.FilePath) ) ) { // When a xmp file already exist (only for raws) // AND when this created afterwards with the ExifToolImportXmpCreate setting (only for raws) importIndexItem.FileIndexItem!.AddSidecarExtension("xmp"); } - + // Add item to database await AddToQueryAndImportDatabaseAsync(importIndexItem, importSettings); - + // Copy - if ( _appSettings.IsVerbose() ) _logger.LogInformation("[Import] Next Action = Copy" + - $" {importIndexItem.SourceFullFilePath} {importIndexItem.FilePath}"); - using (var sourceStream = _filesystemStorage.ReadStream(importIndexItem.SourceFullFilePath)) + if ( _appSettings.IsVerbose() ) + _logger.LogInformation("[Import] Next Action = Copy" + + $" {importIndexItem.SourceFullFilePath} {importIndexItem.FilePath}"); + using ( var sourceStream = + _filesystemStorage.ReadStream(importIndexItem.SourceFullFilePath) ) await _subPathStorage.WriteStreamAsync(sourceStream, importIndexItem.FilePath!); - + // Copy the sidecar file - if ( xmpExistForThisFileType) - { - var xmpSourceFullFilePath = ExtensionRolesHelper.ReplaceExtensionWithXmp(importIndexItem.SourceFullFilePath); - var destinationXmpFullPath = ExtensionRolesHelper.ReplaceExtensionWithXmp(importIndexItem.FilePath); - _filesystemStorage.FileCopy(xmpSourceFullFilePath, destinationXmpFullPath); - } - - await CreateSideCarFile(importIndexItem, xmpExistForThisFileType); - - // Run Exiftool to Update for example colorClass - UpdateImportTransformations.QueryUpdateDelegate? updateItemAsync = null; - UpdateImportTransformations.QueryThumbnailUpdateDelegate? queryThumbnailUpdateDelegate = null; - - if ( importSettings.IndexMode ) - { - var queryFactory = new QueryFactory( - new SetupDatabaseTypes(_appSettings), _query, - _memoryCache, _appSettings, _serviceScopeFactory, _logger); - updateItemAsync = queryFactory.Query()!.UpdateItemAsync; - queryThumbnailUpdateDelegate = (thumbnailItems) => new ThumbnailQueryFactory( - new SetupDatabaseTypes(_appSettings), _serviceScopeFactory, - _thumbnailQuery, _logger).ThumbnailQuery()!.AddThumbnailRangeAsync(thumbnailItems); - } - - await CreateMataThumbnail(new List{importIndexItem}, importSettings); - - // next: and save the database item - importIndexItem.FileIndexItem = await _updateImportTransformations - .UpdateTransformations(updateItemAsync, importIndexItem.FileIndexItem!, - importSettings.ColorClass, importIndexItem.DateTimeFromFileName, importSettings.IndexMode); - - await UpdateCreateMetaThumbnail(queryThumbnailUpdateDelegate, importIndexItem.FileIndexItem?.FileHash, importSettings.IndexMode); - - DeleteFileAfter(importSettings, importIndexItem); - - if ( _appSettings.IsVerbose() ) _console.Write("+"); - return importIndexItem; + if ( xmpExistForThisFileType ) + { + var xmpSourceFullFilePath = + ExtensionRolesHelper.ReplaceExtensionWithXmp(importIndexItem + .SourceFullFilePath); + var destinationXmpFullPath = + ExtensionRolesHelper.ReplaceExtensionWithXmp(importIndexItem.FilePath); + _filesystemStorage.FileCopy(xmpSourceFullFilePath, destinationXmpFullPath); + } + + await CreateSideCarFile(importIndexItem, xmpExistForThisFileType); + + // Run Exiftool to Update for example colorClass + UpdateImportTransformations.QueryUpdateDelegate? updateItemAsync = null; + UpdateImportTransformations.QueryThumbnailUpdateDelegate? queryThumbnailUpdateDelegate = + null; + + if ( importSettings.IndexMode ) + { + var queryFactory = new QueryFactory( + new SetupDatabaseTypes(_appSettings), _query, + _memoryCache, _appSettings, _serviceScopeFactory, _logger); + updateItemAsync = queryFactory.Query()!.UpdateItemAsync; + queryThumbnailUpdateDelegate = (thumbnailItems) => new ThumbnailQueryFactory( + new SetupDatabaseTypes(_appSettings), _serviceScopeFactory, + _thumbnailQuery, _logger).ThumbnailQuery()! + .AddThumbnailRangeAsync(thumbnailItems); + } + + await CreateMataThumbnail(new List { importIndexItem }, + importSettings); + + // next: and save the database item + importIndexItem.FileIndexItem = await _updateImportTransformations + .UpdateTransformations(updateItemAsync, importIndexItem.FileIndexItem!, + importSettings.ColorClass, importIndexItem.DateTimeFromFileName, + importSettings.IndexMode); + + await UpdateCreateMetaThumbnail(queryThumbnailUpdateDelegate, + importIndexItem.FileIndexItem?.FileHash, importSettings.IndexMode); + + DeleteFileAfter(importSettings, importIndexItem); + + if ( _appSettings.IsVerbose() ) _console.Write("+"); + return importIndexItem; } - private async Task UpdateCreateMetaThumbnail( UpdateImportTransformations.QueryThumbnailUpdateDelegate? queryThumbnailUpdateDelegate, + private async Task UpdateCreateMetaThumbnail( + UpdateImportTransformations.QueryThumbnailUpdateDelegate? queryThumbnailUpdateDelegate, string? fileHash, bool indexMode) { - if ( fileHash == null || _appSettings.MetaThumbnailOnImport == false || !indexMode || queryThumbnailUpdateDelegate == null) return; + if ( fileHash == null || _appSettings.MetaThumbnailOnImport == false || !indexMode || + queryThumbnailUpdateDelegate == null ) return; // Check if fastest version is available to show var setStatus = _thumbnailStorage.ExistFile( ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.TinyMeta)); await queryThumbnailUpdateDelegate(new List { - new ThumbnailResultDataTransferModel(fileHash,setStatus) + new ThumbnailResultDataTransferModel(fileHash, setStatus) }); } @@ -640,6 +691,7 @@ private void DeleteFileAfter(ImportSettingsModel importSettings, { _console.WriteLine($"🚮 Delete file: {importIndexItem.SourceFullFilePath}"); } + _filesystemStorage.FileDelete(importIndexItem.SourceFullFilePath); } @@ -649,38 +701,39 @@ private void DeleteFileAfter(ImportSettingsModel importSettings, /// /// /// - private async Task CreateSideCarFile(ImportIndexItem importIndexItem, bool xmpExistForThisFileType) + private async Task CreateSideCarFile(ImportIndexItem importIndexItem, + bool xmpExistForThisFileType) { - if ( _appSettings.ExifToolImportXmpCreate && !xmpExistForThisFileType) + if ( _appSettings.ExifToolImportXmpCreate && !xmpExistForThisFileType ) { - var exifCopy = new ExifCopy(_subPathStorage, - _thumbnailStorage, _exifTool, new ReadMeta(_subPathStorage, - _appSettings, null!, _logger),_thumbnailQuery); + var exifCopy = new ExifCopy(_subPathStorage, + _thumbnailStorage, _exifTool, new ReadMeta(_subPathStorage, + _appSettings, null!, _logger), _thumbnailQuery); await exifCopy.XmpSync(importIndexItem.FileIndexItem!.FilePath!); } } /// - /// Support for include sidecar files - True when exist && current filetype is raw + /// Support for include sidecar files - True when exist and current filetype is raw /// /// to get the SourceFullFilePath - /// True when exist && current filetype is raw + /// True when exist and current filetype is raw internal bool ExistXmpSidecarForThisFileType(ImportIndexItem importIndexItem) { if ( string.IsNullOrEmpty(importIndexItem.SourceFullFilePath) ) { return false; } - + // Support for include sidecar files var xmpSourceFullFilePath = ExtensionRolesHelper.ReplaceExtensionWithXmp(importIndexItem .SourceFullFilePath); return ExtensionRolesHelper.IsExtensionForceXmp(importIndexItem - .SourceFullFilePath) && - _filesystemStorage.ExistFile(xmpSourceFullFilePath); + .SourceFullFilePath) && + _filesystemStorage.ExistFile(xmpSourceFullFilePath); } - + /// /// Add item to database /// @@ -690,9 +743,10 @@ internal async Task AddToQueryAndImportDatabaseAsync( { if ( !importSettings.IndexMode || _importQuery?.TestConnection() != true ) { - if ( _appSettings.IsVerbose() ) _logger.LogInformation(" AddToQueryAndImportDatabaseAsync Ignored - " + - $"IndexMode {importSettings.IndexMode} " + - $"TestConnection {_importQuery?.TestConnection()}"); + if ( _appSettings.IsVerbose() ) + _logger.LogInformation(" AddToQueryAndImportDatabaseAsync Ignored - " + + $"IndexMode {importSettings.IndexMode} " + + $"TestConnection {_importQuery?.TestConnection()}"); return; } @@ -702,11 +756,13 @@ internal async Task AddToQueryAndImportDatabaseAsync( _memoryCache, _appSettings, _serviceScopeFactory, _logger); var query = queryFactory.Query(); await query!.AddItemAsync(importIndexItem.FileIndexItem!); - + // Add to check db, to avoid duplicate input - var importQuery = new ImportQueryFactory(new SetupDatabaseTypes(_appSettings), _importQuery,_console, _logger).ImportQuery(); - await importQuery!.AddAsync(importIndexItem, importSettings.IsConsoleOutputModeDefault() ); - + var importQuery = new ImportQueryFactory(new SetupDatabaseTypes(_appSettings), + _importQuery, _console, _logger).ImportQuery(); + await importQuery!.AddAsync(importIndexItem, + importSettings.IsConsoleOutputModeDefault()); + await query.DisposeAsync(); } @@ -723,7 +779,8 @@ internal async Task AddToQueryAndImportDatabaseAsync( /// number /// subPath style /// test_1.jpg with complete filePath - internal static string AppendIndexerToFilePath(string parentDirectory, string fileName , int index) + internal static string AppendIndexerToFilePath(string parentDirectory, string fileName, + int index) { if ( index >= 1 ) { @@ -733,14 +790,15 @@ internal static string AppendIndexerToFilePath(string parentDirectory, string fi FilenamesHelper.GetFileExtensionWithoutDot(fileName) ); } + return PathHelper.AddSlash(parentDirectory) + PathHelper.RemovePrefixDbSlash(fileName); } - + /// /// Create parent folders if the folder does not exist on disk /// /// List of all ParentFolders - private async Task CreateParentFolders(Dictionary> directoriesContent) + private async Task CreateParentFolders(Dictionary> directoriesContent) { foreach ( var parentDirectoryPath in directoriesContent ) { @@ -756,15 +814,16 @@ private async Task CreateParentFolders(Dictionary> directori await CreateNewDatabaseDirectory(parentPath.ToString()); - if ( _subPathStorage.ExistFolder(parentPath.ToString())) + if ( _subPathStorage.ExistFolder(parentPath.ToString()) ) { continue; } + _subPathStorage.CreateDirectory(parentPath.ToString()); } } } - + /// /// Temp place to store parent Directories to avoid lots of Database requests /// @@ -777,16 +836,16 @@ private async Task CreateParentFolders(Dictionary> directori /// async task private async Task CreateNewDatabaseDirectory(string parentPath) { - if ( AddedParentDirectories.Contains(parentPath) || - _query.SingleItem(parentPath) != null ) return; - + if ( AddedParentDirectories.Contains(parentPath) || + _query.SingleItem(parentPath) != null ) return; + var item = new FileIndexItem(PathHelper.RemoveLatestSlash(parentPath)) { AddToDatabase = DateTime.UtcNow, IsDirectory = true, ColorClass = ColorClassParser.Color.None }; - + await _query.AddItemAsync(item); AddedParentDirectories.Add(parentPath); } diff --git a/starsky/starsky.feature.import/Services/ImportCli.cs b/starsky/starsky.feature.import/Services/ImportCli.cs index 91f50c5862..4d2e5c6789 100644 --- a/starsky/starsky.feature.import/Services/ImportCli.cs +++ b/starsky/starsky.feature.import/Services/ImportCli.cs @@ -27,7 +27,7 @@ public ImportCli(IImport importService, AppSettings appSettings, IConsole consol _console = console; _exifToolDownload = exifToolDownload; } - + /// /// Command line importer to Database and update disk /// @@ -41,21 +41,22 @@ public async Task Importer(string[] args) await _exifToolDownload.DownloadExifTool(_appSettings.IsWindows); _appSettings.ApplicationType = AppSettings.StarskyAppType.Importer; - if (ArgsHelper.NeedHelp(args) || new ArgsHelper(_appSettings) - .GetPathFormArgs(args,false).Length <= 1) + if ( ArgsHelper.NeedHelp(args) || new ArgsHelper(_appSettings) + .GetPathFormArgs(args, false).Length <= 1 ) { new ArgsHelper(_appSettings, _console).NeedHelpShowDialog(); return; } - + var inputPathListFormArgs = new ArgsHelper(_appSettings).GetPathListFormArgs(args); - + if ( _appSettings.IsVerbose() ) foreach ( var inputPath in inputPathListFormArgs ) + { + _console.WriteLine($">> import: {inputPath}"); + } + + var importSettings = new ImportSettingsModel { - _console.WriteLine($">> import: {inputPath}"); - } - - var importSettings = new ImportSettingsModel { DeleteAfter = ArgsHelper.GetMove(args), RecursiveDirectory = ArgsHelper.NeedRecursive(args), IndexMode = ArgsHelper.GetIndexMode(args), @@ -63,15 +64,15 @@ public async Task Importer(string[] args) ConsoleOutputMode = ArgsHelper.GetConsoleOutputMode(args) }; - if ( _appSettings.IsVerbose() ) + if ( _appSettings.IsVerbose() ) { _console.WriteLine($"Options: DeleteAfter: {importSettings.DeleteAfter}, " + - $"RecursiveDirectory {importSettings.RecursiveDirectory}, " + - $"ColorClass (overwrite) {importSettings.ColorClass}, " + - $"Structure {_appSettings.Structure}, " + - $"IndexMode {importSettings.IndexMode}"); + $"RecursiveDirectory {importSettings.RecursiveDirectory}, " + + $"ColorClass (overwrite) {importSettings.ColorClass}, " + + $"Structure {_appSettings.Structure}, " + + $"IndexMode {importSettings.IndexMode}"); } - + var stopWatch = Stopwatch.StartNew(); var result = await _importService.Importer(inputPathListFormArgs, importSettings); @@ -86,9 +87,10 @@ private void WriteOutputStatus(ImportSettingsModel importSettings, List p.Status == ImportStatus.Ok); _console.WriteLine($"\nDone Importing {okCount}"); - if ( okCount != 0 ) { + if ( okCount != 0 ) + { _console.WriteLine($"Time: {Math.Round(stopWatch.Elapsed.TotalSeconds, 1)} " + - $"sec. or {Math.Round(stopWatch.Elapsed.TotalMinutes, 1)} min."); + $"sec. or {Math.Round(stopWatch.Elapsed.TotalMinutes, 1)} min."); } _console.WriteLine($"Failed: {result.Count(p => p.Status != ImportStatus.Ok)}"); } @@ -97,16 +99,16 @@ private void WriteOutputStatus(ImportSettingsModel importSettings, List {e9c60bf0-09b6-40c9-95b5-25c7a185365e} 0.6.0-beta.0 + enable - + - - - + + + - + diff --git a/starsky/starsky.feature.metaupdate/Helpers/AddNotFoundInIndexStatus.cs b/starsky/starsky.feature.metaupdate/Helpers/AddNotFoundInIndexStatus.cs index 65124442d0..4512d17d50 100644 --- a/starsky/starsky.feature.metaupdate/Helpers/AddNotFoundInIndexStatus.cs +++ b/starsky/starsky.feature.metaupdate/Helpers/AddNotFoundInIndexStatus.cs @@ -1,6 +1,4 @@ using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; using starsky.foundation.database.Helpers; using starsky.foundation.database.Models; @@ -10,14 +8,14 @@ public static class AddNotFoundInIndexStatus { internal static void Update(IEnumerable inputFilePaths, List fileIndexResultsList) { - foreach (var subPath in inputFilePaths) + foreach ( var subPath in inputFilePaths ) { // when item is not in the database if ( fileIndexResultsList.Exists(p => p.FilePath == subPath) ) { continue; } - StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), + StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), FileIndexItem.ExifStatus.NotFoundNotInIndex, fileIndexResultsList); } diff --git a/starsky/starsky.feature.metaupdate/Helpers/AddParentCacheIfNotExist.cs b/starsky/starsky.feature.metaupdate/Helpers/AddParentCacheIfNotExist.cs index 9b6af936e2..cfdb76191d 100644 --- a/starsky/starsky.feature.metaupdate/Helpers/AddParentCacheIfNotExist.cs +++ b/starsky/starsky.feature.metaupdate/Helpers/AddParentCacheIfNotExist.cs @@ -17,7 +17,7 @@ public AddParentCacheIfNotExist(IQuery query, IWebLogger logger) _query = query; _logger = logger; } - + internal async Task> AddParentCacheIfNotExistAsync(IEnumerable updatedPaths) { var parentDirectoryList = new HashSet(); @@ -27,25 +27,25 @@ internal async Task> AddParentCacheIfNotExistAsync(IEnumerable + var shouldAddParentDirectoriesToCache = parentDirectoryList.Where(parentDirectory => !_query.CacheGetParentFolder(parentDirectory).Item1).ToList(); - + if ( shouldAddParentDirectoriesToCache.Count == 0 ) { return new List(); } var databaseQueryResult = await _query.GetAllObjectsAsync(shouldAddParentDirectoriesToCache); - - _logger.LogInformation("[AddParentCacheIfNotExist] files added to cache " + - string.Join(",", shouldAddParentDirectoriesToCache)); - + + _logger.LogInformation("[AddParentCacheIfNotExist] files added to cache " + + string.Join(",", shouldAddParentDirectoriesToCache)); + foreach ( var directory in shouldAddParentDirectoriesToCache ) { var byDirectory = databaseQueryResult.Where(p => p.ParentDirectory == directory).ToList(); _query.AddCacheParentItem(directory, byDirectory); } - return shouldAddParentDirectoriesToCache; + return shouldAddParentDirectoriesToCache; } } } diff --git a/starsky/starsky.feature.metaupdate/Helpers/AppendXmpPathsWhenCollectionsFalse.cs b/starsky/starsky.feature.metaupdate/Helpers/AppendXmpPathsWhenCollectionsFalse.cs index 922af793a6..75a47697d6 100644 --- a/starsky/starsky.feature.metaupdate/Helpers/AppendXmpPathsWhenCollectionsFalse.cs +++ b/starsky/starsky.feature.metaupdate/Helpers/AppendXmpPathsWhenCollectionsFalse.cs @@ -12,7 +12,7 @@ public static List AppendXmpPathsWhenCollectionsFalse(bool collections, { return inputFilePaths; } - + var inputFilePathsWithXmpFiles = new List(); // append xmp files to list (does not need to exist on disk) @@ -23,7 +23,7 @@ public static List AppendXmpPathsWhenCollectionsFalse(bool collections, ExtensionRolesHelper.ReplaceExtensionWithXmp( inputFilePath)); } - + inputFilePaths.AddRange(inputFilePathsWithXmpFiles); return inputFilePaths; } diff --git a/starsky/starsky.feature.metaupdate/Interfaces/IMetaPreflight.cs b/starsky/starsky.feature.metaupdate/Interfaces/IMetaPreflight.cs index 96c9a6988f..3b5db0d34b 100644 --- a/starsky/starsky.feature.metaupdate/Interfaces/IMetaPreflight.cs +++ b/starsky/starsky.feature.metaupdate/Interfaces/IMetaPreflight.cs @@ -7,7 +7,7 @@ namespace starsky.feature.metaupdate.Interfaces public interface IMetaPreflight { Task<(List fileIndexResultsList, Dictionary> changedFileIndexItemName)> - PreflightAsync(FileIndexItem inputModel, List inputFilePaths, + PreflightAsync(FileIndexItem? inputModel, List inputFilePaths, bool append, bool collections, int rotateClock); } diff --git a/starsky/starsky.feature.metaupdate/Interfaces/IMetaReplaceService.cs b/starsky/starsky.feature.metaupdate/Interfaces/IMetaReplaceService.cs index d680707141..eee22f8b78 100644 --- a/starsky/starsky.feature.metaupdate/Interfaces/IMetaReplaceService.cs +++ b/starsky/starsky.feature.metaupdate/Interfaces/IMetaReplaceService.cs @@ -6,7 +6,7 @@ namespace starsky.feature.metaupdate.Interfaces { public interface IMetaReplaceService { - Task> Replace(string f, string fieldName, + Task> Replace(string f, string fieldName, string search, string replace, bool collections); } } diff --git a/starsky/starsky.feature.metaupdate/Interfaces/IMetaUpdateService.cs b/starsky/starsky.feature.metaupdate/Interfaces/IMetaUpdateService.cs index 235cce73e8..aa8d0f20df 100644 --- a/starsky/starsky.feature.metaupdate/Interfaces/IMetaUpdateService.cs +++ b/starsky/starsky.feature.metaupdate/Interfaces/IMetaUpdateService.cs @@ -17,9 +17,9 @@ public interface IMetaUpdateService /// /// Task> UpdateAsync( - Dictionary> changedFileIndexItemName, + Dictionary> changedFileIndexItemName, List fileIndexResultsList, - FileIndexItem inputModel, // only when changedFileIndexItemName = null + FileIndexItem? inputModel, // only when changedFileIndexItemName = null bool collections, bool append, // only when changedFileIndexItemName = null int rotateClock); diff --git a/starsky/starsky.feature.metaupdate/Services/DeleteItem.cs b/starsky/starsky.feature.metaupdate/Services/DeleteItem.cs index b7266765fd..e488fe1c21 100644 --- a/starsky/starsky.feature.metaupdate/Services/DeleteItem.cs +++ b/starsky/starsky.feature.metaupdate/Services/DeleteItem.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using starsky.feature.metaupdate.Interfaces; @@ -29,50 +30,60 @@ public DeleteItem(IQuery query, AppSettings appSettings, ISelectorStorage select _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); _statusCodeHelper = new StatusCodesHelper(appSettings); } - + public async Task> DeleteAsync(string filePath, bool collections) { var inputFilePaths = PathHelper.SplitInputFilePaths(filePath); var fileIndexResultsList = new List(); var collectionAndInsideDirectoryList = new List(); - foreach (var subPath in inputFilePaths) + foreach ( var subPath in inputFilePaths ) { var detailView = _query.SingleItem(subPath, null, collections, false); - if (detailView?.FileIndexItem?.FilePath == null) + if ( detailView?.FileIndexItem?.FilePath == null ) { HandleNotFoundStatus(subPath, fileIndexResultsList); continue; } // Status should be deleted before you can delete the item - if (_iStorage.IsFolderOrFile(detailView.FileIndexItem.FilePath) == FolderOrFileModel.FolderOrFileTypeList.Deleted) + if ( _iStorage.IsFolderOrFile(detailView.FileIndexItem.FilePath) == + FolderOrFileModel.FolderOrFileTypeList.Deleted ) { - HandleNotFoundSourceMissingStatus(detailView.FileIndexItem, fileIndexResultsList); + HandleNotFoundSourceMissingStatus(detailView.FileIndexItem, + fileIndexResultsList); continue; } // Dir is readonly / don't delete - if (_statusCodeHelper.IsReadOnlyStatus(detailView) == FileIndexItem.ExifStatus.ReadOnly) + if ( _statusCodeHelper.IsReadOnlyStatus(detailView) == + FileIndexItem.ExifStatus.ReadOnly ) { HandleReadOnlyStatus(detailView.FileIndexItem, fileIndexResultsList); continue; } - if (StatusCodesHelper.IsDeletedStatus(detailView) != FileIndexItem.ExifStatus.Deleted) + if ( StatusCodesHelper.IsDeletedStatus(detailView) != + FileIndexItem.ExifStatus.Deleted ) { - HandleOperationNotSupportedStatus(detailView.FileIndexItem, fileIndexResultsList); + HandleOperationNotSupportedStatus(detailView.FileIndexItem, + fileIndexResultsList); continue; } - collectionAndInsideDirectoryList.AddRange(DetailView.GetCollectionSubPathList(detailView.FileIndexItem, collections, subPath)); + collectionAndInsideDirectoryList.AddRange( + DetailView.GetCollectionSubPathList(detailView.FileIndexItem, collections, + subPath)); // For deleting content of an entire directory if ( detailView.FileIndexItem.IsDirectory != true ) continue; // when deleting a folder the collections setting does nothing - collectionAndInsideDirectoryList.AddRange((await _query.GetAllFilesAsync(detailView.FileIndexItem.FilePath)).Select(itemInDirectory => itemInDirectory.FilePath)); + var items = + ( await _query.GetAllFilesAsync(detailView.FileIndexItem.FilePath) ).Select( + itemInDirectory => itemInDirectory.FilePath!); + collectionAndInsideDirectoryList.AddRange(items); } await HandleCollectionDeletion(collectionAndInsideDirectoryList, fileIndexResultsList); @@ -80,21 +91,24 @@ public async Task> DeleteAsync(string filePath, bool collect return fileIndexResultsList; } - private async Task HandleCollectionDeletion(List collectionAndInsideDirectoryList, List fileIndexResultsList) + [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] + private async Task HandleCollectionDeletion(List collectionAndInsideDirectoryList, + List fileIndexResultsList) { // collectionAndInsideDirectoryList should not have duplicate items - foreach (var collectionSubPath in new HashSet(collectionAndInsideDirectoryList)) + foreach ( var collectionSubPath in + new HashSet(collectionAndInsideDirectoryList) ) { var detailViewItem = _query.SingleItem(collectionSubPath, null, false, false); // null only happens when some other process also delete this item - if (detailViewItem == null) continue; + if ( detailViewItem == null ) continue; // return a Ok, which means the file is deleted - detailViewItem.FileIndexItem.Status = FileIndexItem.ExifStatus.Ok; - + detailViewItem.FileIndexItem!.Status = FileIndexItem.ExifStatus.Ok; + // remove thumbnail from disk - _thumbnailStorage.FileDelete(detailViewItem.FileIndexItem.FileHash); + _thumbnailStorage.FileDelete(detailViewItem.FileIndexItem!.FileHash!); fileIndexResultsList.Add(detailViewItem.FileIndexItem.Clone()); // remove item from db @@ -104,9 +118,11 @@ private async Task HandleCollectionDeletion(List collectionAndInsideDire RemoveFileOrFolderFromDisk(detailViewItem); // the child directories are still stored in the database - if (detailViewItem.FileIndexItem.IsDirectory != true) continue; + if ( detailViewItem.FileIndexItem.IsDirectory != true ) continue; - foreach (var item in (await _query.GetAllRecursiveAsync(collectionSubPath)).Where(p => p.IsDirectory == true)) + foreach ( var item in + ( await _query.GetAllRecursiveAsync(collectionSubPath) ).Where(p => + p.IsDirectory == true) ) { item.Status = FileIndexItem.ExifStatus.Deleted; fileIndexResultsList.Add(item.Clone()); @@ -115,30 +131,38 @@ private async Task HandleCollectionDeletion(List collectionAndInsideDire } } - private static void HandleNotFoundStatus(string subPath, List fileIndexResultsList) + private static void HandleNotFoundStatus(string subPath, + List fileIndexResultsList) { - StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), FileIndexItem.ExifStatus.NotFoundNotInIndex, fileIndexResultsList); + StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), + FileIndexItem.ExifStatus.NotFoundNotInIndex, fileIndexResultsList); } - private static void HandleNotFoundSourceMissingStatus(FileIndexItem fileIndexItem, List fileIndexResultsList) + private static void HandleNotFoundSourceMissingStatus(FileIndexItem fileIndexItem, + List fileIndexResultsList) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.NotFoundSourceMissing, fileIndexResultsList); + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + FileIndexItem.ExifStatus.NotFoundSourceMissing, fileIndexResultsList); } - private static void HandleReadOnlyStatus(FileIndexItem fileIndexItem, List fileIndexResultsList) + private static void HandleReadOnlyStatus(FileIndexItem fileIndexItem, + List fileIndexResultsList) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.ReadOnly, fileIndexResultsList); + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + FileIndexItem.ExifStatus.ReadOnly, fileIndexResultsList); } - private static void HandleOperationNotSupportedStatus(FileIndexItem fileIndexItem, List fileIndexResultsList) + private static void HandleOperationNotSupportedStatus(FileIndexItem fileIndexItem, + List fileIndexResultsList) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.OperationNotSupported, fileIndexResultsList); + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + FileIndexItem.ExifStatus.OperationNotSupported, fileIndexResultsList); } private void RemoveXmpSideCarFile(DetailView detailViewItem) { // remove the sidecar file (if exist) - if ( ExtensionRolesHelper.IsExtensionForceXmp(detailViewItem.FileIndexItem + if ( ExtensionRolesHelper.IsExtensionForceXmp(detailViewItem.FileIndexItem! .FileName) ) { _iStorage.FileDelete( @@ -151,21 +175,21 @@ private void RemoveJsonSideCarFile(DetailView detailViewItem) { // remove the json sidecar file (if exist) var jsonSubPath = JsonSidecarLocation.JsonLocation(detailViewItem - .FileIndexItem.ParentDirectory, detailViewItem - .FileIndexItem.FileName); + .FileIndexItem!.ParentDirectory!, detailViewItem + .FileIndexItem.FileName!); _iStorage.FileDelete(jsonSubPath); } private void RemoveFileOrFolderFromDisk(DetailView detailViewItem) { - if ( detailViewItem.FileIndexItem.IsDirectory == true ) + if ( detailViewItem.FileIndexItem!.IsDirectory == true ) { - _iStorage.FolderDelete(detailViewItem.FileIndexItem.FilePath); + _iStorage.FolderDelete(detailViewItem.FileIndexItem!.FilePath!); return; } - + // and remove the actual file - _iStorage.FileDelete(detailViewItem.FileIndexItem.FilePath); + _iStorage.FileDelete(detailViewItem.FileIndexItem.FilePath!); } } } diff --git a/starsky/starsky.feature.metaupdate/Services/MetaInfo.cs b/starsky/starsky.feature.metaupdate/Services/MetaInfo.cs index 35039e278b..017c726d8f 100644 --- a/starsky/starsky.feature.metaupdate/Services/MetaInfo.cs +++ b/starsky/starsky.feature.metaupdate/Services/MetaInfo.cs @@ -28,47 +28,47 @@ public MetaInfo(IQuery query, AppSettings appSettings, ISelectorStorage selector { _query = query; _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _readMeta = new ReadMeta(_iStorage,appSettings, memoryCache, logger); + _readMeta = new ReadMeta(_iStorage, appSettings, memoryCache, logger); _statusCodeHelper = new StatusCodesHelper(appSettings); } - + public async Task> GetInfoAsync(List inputFilePaths, bool collections) { // the result list var fileIndexResultsList = new List(); - - foreach (var subPath in inputFilePaths) + + foreach ( var subPath in inputFilePaths ) { var detailView = _query.SingleItem(subPath, null, collections, false); - + if ( detailView?.FileIndexItem == null ) { - StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), + StatusCodesHelper.ReturnExifStatusError(new FileIndexItem(subPath), FileIndexItem.ExifStatus.NotFoundNotInIndex, fileIndexResultsList); continue; } - + if ( !_iStorage.ExistFile(detailView.FileIndexItem.FilePath!) ) { - StatusCodesHelper.ReturnExifStatusError(detailView.FileIndexItem!, + StatusCodesHelper.ReturnExifStatusError(detailView.FileIndexItem!, FileIndexItem.ExifStatus.NotFoundSourceMissing, fileIndexResultsList); - continue; + continue; } - + // Check if extension is supported for ExtensionExifToolSupportedList // Not all files are able to write with exifTool - if(!ExtensionRolesHelper.IsExtensionExifToolSupported(detailView.FileIndexItem!.FileName) - && !ExtensionRolesHelper.IsExtensionSidecar(detailView.FileIndexItem!.FileName)) + if ( !ExtensionRolesHelper.IsExtensionExifToolSupported(detailView.FileIndexItem!.FileName) + && !ExtensionRolesHelper.IsExtensionSidecar(detailView.FileIndexItem!.FileName) ) { StatusCodesHelper.ReturnExifStatusError( - await new FileIndexItemJsonParser(_iStorage).ReadAsync(detailView.FileIndexItem), + await new FileIndexItemJsonParser(_iStorage).ReadAsync(detailView.FileIndexItem), FileIndexItem.ExifStatus.ExifWriteNotSupported, fileIndexResultsList); continue; } - + var statusResults = StatusCodesHelper.IsDeletedStatus(detailView); // only when default status to avoid unneeded checks if ( statusResults == FileIndexItem.ExifStatus.Default ) statusResults = _statusCodeHelper.IsReadOnlyStatus(detailView); @@ -76,11 +76,11 @@ public async Task> GetInfoAsync(List inputFilePaths, if ( statusResults == FileIndexItem.ExifStatus.Default ) statusResults = FileIndexItem.ExifStatus.Ok; var collectionSubPathList = DetailView.GetCollectionSubPathList(detailView.FileIndexItem, collections, subPath); - + foreach ( var collectionSubPath in collectionSubPathList ) { var collectionItem = await _readMeta.ReadExifAndXmpFromFileAsync(collectionSubPath); - + collectionItem!.Status = statusResults; collectionItem.CollectionPaths = collectionSubPathList; collectionItem.ImageFormat = diff --git a/starsky/starsky.feature.metaupdate/Services/MetaPreflight.cs b/starsky/starsky.feature.metaupdate/Services/MetaPreflight.cs index 393ead7620..3d3372dcba 100644 --- a/starsky/starsky.feature.metaupdate/Services/MetaPreflight.cs +++ b/starsky/starsky.feature.metaupdate/Services/MetaPreflight.cs @@ -13,7 +13,6 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Models; using starsky.foundation.storage.Storage; -#nullable enable namespace starsky.feature.metaupdate.Services { @@ -30,7 +29,8 @@ public MetaPreflight(IQuery query, AppSettings appSettings, ISelectorStorage? se _query = query; _appSettings = appSettings; _logger = logger; - if ( selectorStorage != null ){ + if ( selectorStorage != null ) + { _iStorage = selectorStorage.Get( SelectorStorage.StorageServices.SubPath); } @@ -38,44 +38,44 @@ public MetaPreflight(IQuery query, AppSettings appSettings, ISelectorStorage? se public async Task<(List fileIndexResultsList, Dictionary> changedFileIndexItemName)> - PreflightAsync(FileIndexItem inputModel, List inputFilePaths, + PreflightAsync(FileIndexItem? inputModel, List inputFilePaths, bool append, bool collections, int rotateClock) { // the result list var fileIndexUpdateList = new List(); - + // Per file stored key = string[fileHash] item => List // FileIndexItem.name (e.g. Tags) that are changed var changedFileIndexItemName = new Dictionary>(); - + // Prefill cache to avoid fast updating issues - await new AddParentCacheIfNotExist(_query,_logger).AddParentCacheIfNotExistAsync(inputFilePaths); + await new AddParentCacheIfNotExist(_query, _logger).AddParentCacheIfNotExistAsync(inputFilePaths); // add xmp files to the list inputFilePaths = AppendXmpPathsWhenCollectionsFalseHelper.AppendXmpPathsWhenCollectionsFalse(collections, inputFilePaths); - + var resultFileIndexItemsList = await _query.GetObjectsByFilePathAsync( inputFilePaths, collections); foreach ( var fileIndexItem in resultFileIndexItemsList ) { // Files that are not on disk - if ( _iStorage!.IsFolderOrFile(fileIndexItem.FilePath!) == - FolderOrFileModel.FolderOrFileTypeList.Deleted ) + if ( _iStorage!.IsFolderOrFile(fileIndexItem.FilePath!) == + FolderOrFileModel.FolderOrFileTypeList.Deleted ) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.NotFoundSourceMissing, fileIndexUpdateList); - continue; + continue; } - + // Dir is readonly / don't edit - if ( new StatusCodesHelper(_appSettings).IsReadOnlyStatus(fileIndexItem) - == FileIndexItem.ExifStatus.ReadOnly) + if ( new StatusCodesHelper(_appSettings).IsReadOnlyStatus(fileIndexItem) + == FileIndexItem.ExifStatus.ReadOnly ) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.ReadOnly, fileIndexUpdateList); - continue; + continue; } CompareAllLabelsAndRotation(changedFileIndexItemName, @@ -85,18 +85,18 @@ public MetaPreflight(IQuery query, AppSettings appSettings, ISelectorStorage? se CheckGeoLocationStatus(fileIndexItem); // this one is good :) - if ( fileIndexItem.Status is FileIndexItem.ExifStatus.Default or FileIndexItem.ExifStatus.OkAndSame) + if ( fileIndexItem.Status is FileIndexItem.ExifStatus.Default or FileIndexItem.ExifStatus.OkAndSame ) { fileIndexItem.Status = FileIndexItem.ExifStatus.Ok; } - + // Deleted is allowed but the status need be updated - if (( StatusCodesHelper.IsDeletedStatus(fileIndexItem) - == FileIndexItem.ExifStatus.Deleted) ) + if ( ( StatusCodesHelper.IsDeletedStatus(fileIndexItem) + == FileIndexItem.ExifStatus.Deleted ) ) { fileIndexItem.Status = FileIndexItem.ExifStatus.Deleted; } - + // The hash in FileIndexItem is not correct // Clone to not change after update fileIndexUpdateList.Add(fileIndexItem); @@ -119,7 +119,7 @@ private static void CheckGeoLocationStatus(FileIndexItem fileIndexItem) { if ( fileIndexItem.Latitude == 0 || fileIndexItem.Longitude == 0 ) return; - + var result = ValidateLocation.ValidateLatitudeLongitude( fileIndexItem.Latitude, fileIndexItem.Longitude); if ( !result ) @@ -138,18 +138,18 @@ private static void CheckGeoLocationStatus(FileIndexItem fileIndexItem) /// object that include the changes /// true= for tags to add /// rotation value 1 left, -1 right, 0 nothing - public static void CompareAllLabelsAndRotation( Dictionary> changedFileIndexItemName, - FileIndexItem collectionsFileIndexItem, FileIndexItem statusModel, bool append, int rotateClock) + public static void CompareAllLabelsAndRotation(Dictionary> changedFileIndexItemName, + FileIndexItem collectionsFileIndexItem, FileIndexItem? statusModel, bool append, int rotateClock) { if ( changedFileIndexItemName == null || string.IsNullOrEmpty(collectionsFileIndexItem.FilePath) ) throw new MissingFieldException(nameof(changedFileIndexItemName)); - + // compare and add changes to collectionsDetailView - var comparedNamesList = FileIndexCompareHelper.Compare(collectionsFileIndexItem, + var comparedNamesList = FileIndexCompareHelper.Compare(collectionsFileIndexItem, statusModel, append); // if requested, add changes to rotation - collectionsFileIndexItem = + collectionsFileIndexItem = RotationCompare(rotateClock, collectionsFileIndexItem, comparedNamesList); collectionsFileIndexItem.LastChanged = comparedNamesList; @@ -162,7 +162,7 @@ public static void CompareAllLabelsAndRotation( Dictionary> // overwrite list if already exist changedFileIndexItemName[collectionsFileIndexItem.FilePath!] = comparedNamesList; } - + /// /// Add to comparedNames list and add to detail view /// @@ -170,14 +170,14 @@ public static void CompareAllLabelsAndRotation( Dictionary> /// main db object /// list of types that are changes /// updated image - public static FileIndexItem RotationCompare(int rotateClock, FileIndexItem fileIndexItem, + public static FileIndexItem RotationCompare(int rotateClock, FileIndexItem fileIndexItem, ICollection comparedNamesList) { // Do orientation / Rotate if needed (after compare) - if (!FileIndexItem.IsRelativeOrientation(rotateClock)) return fileIndexItem; + if ( !FileIndexItem.IsRelativeOrientation(rotateClock) ) return fileIndexItem; // run this on detail view => statusModel is always default fileIndexItem.SetRelativeOrientation(rotateClock); - + // list of exifTool to update this field if ( !comparedNamesList.Contains(nameof(fileIndexItem.Orientation).ToLowerInvariant()) ) { diff --git a/starsky/starsky.feature.metaupdate/Services/MetaReplaceService.cs b/starsky/starsky.feature.metaupdate/Services/MetaReplaceService.cs index 2fe49e59da..702a606f02 100644 --- a/starsky/starsky.feature.metaupdate/Services/MetaReplaceService.cs +++ b/starsky/starsky.feature.metaupdate/Services/MetaReplaceService.cs @@ -24,7 +24,7 @@ public class MetaReplaceService : IMetaReplaceService { private readonly IQuery _query; private readonly AppSettings _appSettings; - private readonly IStorage _iStorage; + private readonly IStorage _iStorage = new StorageHostFullPathFilesystem(); private readonly IWebLogger _logger; /// Replace meta content @@ -32,11 +32,16 @@ public class MetaReplaceService : IMetaReplaceService /// Settings of the application /// storage abstraction /// web logger - public MetaReplaceService(IQuery query, AppSettings appSettings, ISelectorStorage selectorStorage, IWebLogger logger) + public MetaReplaceService(IQuery query, AppSettings appSettings, + ISelectorStorage? selectorStorage, IWebLogger logger) { _query = query; _appSettings = appSettings; - if ( selectorStorage != null ) _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + if ( selectorStorage != null ) + { + _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + } + _logger = logger; } @@ -48,60 +53,74 @@ public MetaReplaceService(IQuery query, AppSettings appSettings, ISelectorStorag /// from /// to /// stack collections - public async Task> Replace(string f, string fieldName, string search, string replace, bool collections) + public async Task> Replace(string f, string fieldName, string search, + string replace, bool collections) { // when you search for nothing, your fast done - if ( string.IsNullOrEmpty(search) ) return new List + if ( string.IsNullOrEmpty(search) ) { - new FileIndexItem{Status = FileIndexItem.ExifStatus.OperationNotSupported} - }; + return + [ + new FileIndexItem { Status = FileIndexItem.ExifStatus.OperationNotSupported } + ]; + } // escaping null values if ( string.IsNullOrEmpty(replace) ) replace = string.Empty; - if ( ! FileIndexCompareHelper.CheckIfPropertyExist(fieldName) ) return new List - { - new FileIndexItem{Status = FileIndexItem.ExifStatus.OperationNotSupported} - }; + if ( !FileIndexCompareHelper.CheckIfPropertyExist(fieldName) ) + return new List + { + new FileIndexItem + { + Status = FileIndexItem.ExifStatus.OperationNotSupported + } + }; var inputFilePaths = PathHelper.SplitInputFilePaths(f).ToList(); - inputFilePaths = AppendXmpPathsWhenCollectionsFalseHelper.AppendXmpPathsWhenCollectionsFalse(collections, inputFilePaths); + inputFilePaths = + AppendXmpPathsWhenCollectionsFalseHelper.AppendXmpPathsWhenCollectionsFalse( + collections, inputFilePaths); // the result list var fileIndexUpdatedList = new List(); // Prefill cache to avoid fast updating issues - await new AddParentCacheIfNotExist(_query,_logger).AddParentCacheIfNotExistAsync(inputFilePaths); - + await new AddParentCacheIfNotExist(_query, _logger).AddParentCacheIfNotExistAsync( + inputFilePaths); + // Assumes that this give status Ok back by default var queryFileIndexItemsList = await _query.GetObjectsByFilePathAsync( inputFilePaths, collections); - + // to collect foreach ( var fileIndexItem in queryFileIndexItemsList ) { // if folder is deleted - if ( _iStorage.IsFolderOrFile(fileIndexItem.FilePath!) == FolderOrFileModel.FolderOrFileTypeList.Deleted ) + if ( _iStorage.IsFolderOrFile(fileIndexItem.FilePath!) == + FolderOrFileModel.FolderOrFileTypeList.Deleted ) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.NotFoundSourceMissing, fileIndexUpdatedList); - continue; + continue; } - + // Dir is readonly / don't edit - if ( new StatusCodesHelper(_appSettings).IsReadOnlyStatus(fileIndexItem) - == FileIndexItem.ExifStatus.ReadOnly) + if ( new StatusCodesHelper(_appSettings).IsReadOnlyStatus(fileIndexItem) + == FileIndexItem.ExifStatus.ReadOnly ) { - StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, FileIndexItem.ExifStatus.ReadOnly, fileIndexUpdatedList); - continue; + continue; } + fileIndexUpdatedList.Add(fileIndexItem); } - fileIndexUpdatedList = SearchAndReplace(fileIndexUpdatedList, fieldName, search, replace); + fileIndexUpdatedList = + SearchAndReplace(fileIndexUpdatedList, fieldName, search, replace); AddNotFoundInIndexStatus.Update(inputFilePaths, fileIndexUpdatedList); @@ -109,34 +128,34 @@ public async Task> Replace(string f, string fieldName, strin foreach ( var fileIndexItem in fileIndexUpdatedList ) { // Status Ok is already set - + // Deleted is allowed but the status need be updated - if ((fileIndexItem.Status == FileIndexItem.ExifStatus.Ok) && - StatusCodesHelper.IsDeletedStatus(fileIndexItem) == - FileIndexItem.ExifStatus.Deleted) + if ( ( fileIndexItem.Status == FileIndexItem.ExifStatus.Ok ) && + StatusCodesHelper.IsDeletedStatus(fileIndexItem) == + FileIndexItem.ExifStatus.Deleted ) { fileIndexItem.Status = FileIndexItem.ExifStatus.Deleted; } - + fileIndexResultList.Add(fileIndexItem); } - + return await new Duplicate(_query).RemoveDuplicateAsync(fileIndexResultList); } - public static List SearchAndReplace(List fileIndexResultsList, + public static List SearchAndReplace(List fileIndexResultsList, string fieldName, string search, string replace) { - foreach ( var fileIndexItem in fileIndexResultsList.Where( - p => p.Status - is FileIndexItem.ExifStatus.Ok - or FileIndexItem.ExifStatus.OkAndSame - or FileIndexItem.ExifStatus.Deleted - or FileIndexItem.ExifStatus.DeletedAndSame) ) + foreach ( var fileIndexItem in fileIndexResultsList.Where( + p => p.Status + is FileIndexItem.ExifStatus.Ok + or FileIndexItem.ExifStatus.OkAndSame + or FileIndexItem.ExifStatus.Deleted + or FileIndexItem.ExifStatus.DeletedAndSame) ) { var searchInObject = FileIndexCompareHelper.Get(fileIndexItem, fieldName); var replacedToObject = new object(); - + var propertiesA = new FileIndexItem().GetType().GetProperties( BindingFlags.Public | BindingFlags.Instance); @@ -144,15 +163,15 @@ or FileIndexItem.ExifStatus.Deleted p.Name, fieldName, StringComparison.InvariantCultureIgnoreCase)); - if ( property?.PropertyType == typeof(string)) + if ( property?.PropertyType == typeof(string) ) { - var searchIn = searchInObject != null ? ( string ) searchInObject : string.Empty; - + var searchIn = searchInObject != null ? ( string )searchInObject : string.Empty; + // Replace Ignore Case replacedToObject = Regex.Replace( searchIn, - Regex.Escape(search), - replace.Replace("$","$$"), + Regex.Escape(search), + replace.Replace("$", "$$"), RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100) ); @@ -164,7 +183,5 @@ or FileIndexItem.ExifStatus.Deleted return fileIndexResultsList; } - - } } diff --git a/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs b/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs index 4fcf0027d4..da631430ad 100644 --- a/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs +++ b/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs @@ -21,188 +21,193 @@ using ExifToolCmdHelper = starsky.foundation.writemeta.Helpers.ExifToolCmdHelper; [assembly: InternalsVisibleTo("starskytest")] -namespace starsky.feature.metaupdate.Services + +namespace starsky.feature.metaupdate.Services; + +[Service(typeof(IMetaUpdateService), InjectionLifetime = InjectionLifetime.Scoped)] +public class MetaUpdateService : IMetaUpdateService { - [Service(typeof(IMetaUpdateService), InjectionLifetime = InjectionLifetime.Scoped)] - public class MetaUpdateService : IMetaUpdateService - { - private readonly IQuery _query; - private readonly IExifTool _exifTool; - private readonly IReadMetaSubPathStorage _readMeta; - private readonly IStorage _iStorage; - private readonly IStorage _thumbnailStorage; - private readonly IMetaPreflight _metaPreflight; - private readonly IWebLogger _logger; - private readonly IThumbnailService _thumbnailService; - private readonly IThumbnailQuery _thumbnailQuery; - - [SuppressMessage("Usage", "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] - public MetaUpdateService( - IQuery query, - IExifTool exifTool, - ISelectorStorage selectorStorage, - IMetaPreflight metaPreflight, - IWebLogger logger, - IReadMetaSubPathStorage readMetaSubPathStorage, - IThumbnailService thumbnailService, - IThumbnailQuery thumbnailQuery) + private readonly IQuery _query; + private readonly IExifTool _exifTool; + private readonly IReadMetaSubPathStorage _readMeta; + private readonly IStorage _iStorage; + private readonly IStorage _thumbnailStorage; + private readonly IMetaPreflight _metaPreflight; + private readonly IWebLogger _logger; + private readonly IThumbnailService _thumbnailService; + private readonly IThumbnailQuery _thumbnailQuery; + + [SuppressMessage("Usage", + "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] + public MetaUpdateService( + IQuery query, + IExifTool exifTool, + ISelectorStorage selectorStorage, + IMetaPreflight metaPreflight, + IWebLogger logger, + IReadMetaSubPathStorage readMetaSubPathStorage, + IThumbnailService thumbnailService, + IThumbnailQuery thumbnailQuery) + { + _query = query; + _exifTool = exifTool; + _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + _readMeta = readMetaSubPathStorage; + _metaPreflight = metaPreflight; + _logger = logger; + _thumbnailService = thumbnailService; + _thumbnailQuery = thumbnailQuery; + } + + /// + /// Run Update + /// + /// Per file stored string{fileHash}, + /// List*string*{FileIndexItem.name (e.g. Tags) that are changed} + /// items stored in the database + /// (only used when cache is disabled) + /// This model is overwritten in the database and ExifTool + /// enable or disable this feature + /// only for disabled cache or changedFileIndexItemName=null + /// rotation value 1 left, -1 right, 0 nothing + public async Task> UpdateAsync( + Dictionary>? changedFileIndexItemName, + List fileIndexResultsList, + FileIndexItem? inputModel, // only when changedFileIndexItemName = null + bool collections, bool append, // only when changedFileIndexItemName = null + int rotateClock) // <- this one is needed + { + if ( changedFileIndexItemName == null ) { - _query = query; - _exifTool = exifTool; - _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); - _readMeta = readMetaSubPathStorage; - _metaPreflight = metaPreflight; - _logger = logger; - _thumbnailService = thumbnailService; - _thumbnailQuery = thumbnailQuery; + changedFileIndexItemName = ( await _metaPreflight.PreflightAsync(inputModel, + fileIndexResultsList.Select(p => p.FilePath!).ToList(), append, collections, + rotateClock) ).changedFileIndexItemName; } - /// - /// Run Update - /// - /// Per file stored string{fileHash}, - /// List*string*{FileIndexItem.name (e.g. Tags) that are changed} - /// items stored in the database - /// (only used when cache is disabled) - /// This model is overwritten in the database and ExifTool - /// enable or disable this feature - /// only for disabled cache or changedFileIndexItemName=null - /// rotation value 1 left, -1 right, 0 nothing - public async Task> UpdateAsync( - Dictionary> changedFileIndexItemName, - List fileIndexResultsList, - FileIndexItem inputModel, // only when changedFileIndexItemName = null - bool collections, bool append, // only when changedFileIndexItemName = null - int rotateClock) // <- this one is needed + var updatedItems = new List(); + var fileIndexItemList = fileIndexResultsList + .Where(p => p.Status is FileIndexItem.ExifStatus.Ok + or FileIndexItem.ExifStatus.Deleted).ToList(); + + foreach ( var fileIndexItem in fileIndexItemList ) { - if ( changedFileIndexItemName == null ) - { - changedFileIndexItemName = (await _metaPreflight.PreflightAsync(inputModel, - fileIndexResultsList.Select(p => p.FilePath).ToList(), append, collections, - rotateClock)).changedFileIndexItemName; - } - - var updatedItems = new List(); - var fileIndexItemList = fileIndexResultsList - .Where(p => p.Status is FileIndexItem.ExifStatus.Ok - or FileIndexItem.ExifStatus.Deleted).ToList(); - - foreach ( var fileIndexItem in fileIndexItemList ) + if ( changedFileIndexItemName.TryGetValue(fileIndexItem.FilePath!, out var value) ) { - if (changedFileIndexItemName.TryGetValue(fileIndexItem.FilePath!, out List value) ) - { - // used for tracking differences, in the database/ExifTool compared to the inputModel - - await UpdateWriteDiskDatabase(fileIndexItem, value, rotateClock); - updatedItems.Add(fileIndexItem); - continue; - } - - _logger.LogError($"Missing in key: {fileIndexItem.FilePath}", - new InvalidDataException($"changedFileIndexItemName: " + - $"{string.Join(",",changedFileIndexItemName)}")); - throw new ArgumentException($"Missing in key: {fileIndexItem.FilePath}", - nameof(changedFileIndexItemName)); + // used for tracking differences, in the database/ExifTool compared to the inputModel + + await UpdateWriteDiskDatabase(fileIndexItem, value, rotateClock); + updatedItems.Add(fileIndexItem); + continue; } - return updatedItems; + _logger.LogError($"Missing in key: {fileIndexItem.FilePath}", + new InvalidDataException($"changedFileIndexItemName: " + + $"{string.Join(",", changedFileIndexItemName)}")); + throw new ArgumentException($"Missing in key: {fileIndexItem.FilePath}", + nameof(changedFileIndexItemName)); } - public void UpdateReadMetaCache(IEnumerable returnNewResultList) - { - _readMeta.UpdateReadMetaCache(returnNewResultList); - } + return updatedItems; + } + + public void UpdateReadMetaCache(IEnumerable returnNewResultList) + { + _readMeta.UpdateReadMetaCache(returnNewResultList); + } + + /// + /// Update ExifTool, Thumbnail, Database and if needed rotateClock + /// + /// output database object + /// name of fields updated by exifTool + /// rotation value (if needed) + private async Task UpdateWriteDiskDatabase(FileIndexItem fileIndexItem, + List comparedNamesList, int rotateClock = 0) + { + // do rotation on thumbs + await RotationThumbnailExecute(rotateClock, fileIndexItem); - /// - /// Update ExifTool, Thumbnail, Database and if needed rotateClock - /// - /// output database object - /// name of fields updated by exifTool - /// rotation value (if needed) - private async Task UpdateWriteDiskDatabase(FileIndexItem fileIndexItem, List comparedNamesList, int rotateClock = 0) + if ( fileIndexItem.IsDirectory != true + && ExtensionRolesHelper.IsExtensionExifToolSupported(fileIndexItem.FileName) ) { - // do rotation on thumbs - await RotationThumbnailExecute(rotateClock, fileIndexItem); + // feature to exif update + var exifUpdateFilePaths = new List { fileIndexItem.FilePath! }; + var exifTool = new ExifToolCmdHelper(_exifTool, _iStorage, _thumbnailStorage, + _readMeta, _thumbnailQuery); - if ( fileIndexItem.IsDirectory != true - && ExtensionRolesHelper.IsExtensionExifToolSupported(fileIndexItem.FileName) ) - { - // feature to exif update - var exifUpdateFilePaths = new List - { - fileIndexItem.FilePath - }; - var exifTool = new ExifToolCmdHelper(_exifTool,_iStorage, _thumbnailStorage,_readMeta, _thumbnailQuery); - - // to avoid diskWatcher catch up - _query.SetGetObjectByFilePathCache(fileIndexItem.FilePath!, fileIndexItem, TimeSpan.FromSeconds(5)); - - // Do an Exif Sync for all files, including thumbnails - var (exifResult,newFileHashes) = await exifTool.UpdateAsync(fileIndexItem, - exifUpdateFilePaths, comparedNamesList,true, true); - - await ApplyOrGenerateUpdatedFileHash(newFileHashes, fileIndexItem); - - _logger.LogInformation(string.IsNullOrEmpty(exifResult) - ? $"[UpdateWriteDiskDatabase] ExifTool result is Nothing or " + - $"Null for: path:{fileIndexItem.FilePath} {DateTime.UtcNow.ToShortTimeString()}" - : $"[UpdateWriteDiskDatabase] ExifTool result: {exifResult} path:{fileIndexItem.FilePath}"); - } - else if ( fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp && - fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json ) - { - await new FileIndexItemJsonParser(_iStorage).WriteAsync(fileIndexItem); - } - - // set last edited - fileIndexItem.LastEdited = _iStorage.Info(fileIndexItem.FilePath!).LastWriteTime; - - // Do a database sync + cache sync - await _query.UpdateItemAsync(fileIndexItem); - - // to avoid diskWatcher catch up (and updates the last edited dateTime) - _query.SetGetObjectByFilePathCache(fileIndexItem.FilePath!, fileIndexItem, TimeSpan.FromSeconds(5)); - - // > async > force you to read the file again - // do not include thumbs in MetaCache - // only the full path url of the source image - _readMeta.RemoveReadMetaCache(fileIndexItem.FilePath); + // to avoid diskWatcher catch up + _query.SetGetObjectByFilePathCache(fileIndexItem.FilePath!, fileIndexItem, + TimeSpan.FromSeconds(5)); + + // Do an Exif Sync for all files, including thumbnails + var (exifResult, newFileHashes) = await exifTool.UpdateAsync(fileIndexItem, + exifUpdateFilePaths, comparedNamesList, true, true); + + await ApplyOrGenerateUpdatedFileHash(newFileHashes, fileIndexItem); + + _logger.LogInformation(string.IsNullOrEmpty(exifResult) + ? $"[UpdateWriteDiskDatabase] ExifTool result is Nothing or " + + $"Null for: path:{fileIndexItem.FilePath} {DateTime.UtcNow.ToShortTimeString()}" + : $"[UpdateWriteDiskDatabase] ExifTool result: {exifResult} path:{fileIndexItem.FilePath}"); + } + else if ( fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp && + fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json ) + { + await new FileIndexItemJsonParser(_iStorage).WriteAsync(fileIndexItem); } - internal async Task ApplyOrGenerateUpdatedFileHash(List newFileHashes, FileIndexItem fileIndexItem) + // set last edited + fileIndexItem.LastEdited = _iStorage.Info(fileIndexItem.FilePath!).LastWriteTime; + + // Do a database sync + cache sync + await _query.UpdateItemAsync(fileIndexItem); + + // to avoid diskWatcher catch up (and updates the last edited dateTime) + _query.SetGetObjectByFilePathCache(fileIndexItem.FilePath!, fileIndexItem, + TimeSpan.FromSeconds(5)); + + // > async > force you to read the file again + // do not include thumbs in MetaCache + // only the full path url of the source image + _readMeta.RemoveReadMetaCache(fileIndexItem.FilePath!); + } + + internal async Task ApplyOrGenerateUpdatedFileHash(List newFileHashes, + FileIndexItem fileIndexItem) + { + if ( !string.IsNullOrWhiteSpace(newFileHashes.FirstOrDefault()) ) { - if ( !string.IsNullOrWhiteSpace(newFileHashes.FirstOrDefault())) - { - fileIndexItem.FileHash = newFileHashes.FirstOrDefault(); - _logger.LogInformation($"use fileHash from exiftool {fileIndexItem.FileHash}"); - return; - } - // when newFileHashes is null or string.empty - // rename is done in the exiftool helper - var newFileHash = (await new FileHash(_iStorage).GetHashCodeAsync(fileIndexItem.FilePath!)).Key; - _thumbnailStorage.FileMove(fileIndexItem.FileHash!, newFileHash); - fileIndexItem.FileHash = newFileHash; + fileIndexItem.FileHash = newFileHashes.FirstOrDefault(); + _logger.LogInformation($"use fileHash from exiftool {fileIndexItem.FileHash}"); + return; } - /// - /// Run the Orientation changes on the thumbnail (only relative) - /// - /// -1 or 1 - /// object contains fileHash - /// updated image - internal async Task RotationThumbnailExecute(int rotateClock, FileIndexItem fileIndexItem) + // when newFileHashes is null or string.empty + // rename is done in the exiftool helper + var newFileHash = + ( await new FileHash(_iStorage).GetHashCodeAsync(fileIndexItem.FilePath!) ).Key; + _thumbnailStorage.FileMove(fileIndexItem.FileHash!, newFileHash); + fileIndexItem.FileHash = newFileHash; + } + + /// + /// Run the Orientation changes on the thumbnail (only relative) + /// + /// -1 or 1 + /// object contains fileHash + /// updated image + internal async Task RotationThumbnailExecute(int rotateClock, FileIndexItem fileIndexItem) + { + // Do orientation + if ( FileIndexItem.IsRelativeOrientation(rotateClock) ) { - // Do orientation - if ( FileIndexItem.IsRelativeOrientation(rotateClock) ) + foreach ( var size in ThumbnailNameHelper.AllThumbnailSizes ) { - foreach ( var size in ThumbnailNameHelper.AllThumbnailSizes ) - { - var fileHash = ThumbnailNameHelper.Combine( - fileIndexItem.FileHash!, size); - await _thumbnailService.RotateThumbnail(fileHash,rotateClock, - ThumbnailNameHelper.GetSize(size)); - } + var fileHash = ThumbnailNameHelper.Combine( + fileIndexItem.FileHash!, size); + await _thumbnailService.RotateThumbnail(fileHash, rotateClock, + ThumbnailNameHelper.GetSize(size)); } } } diff --git a/starsky/starsky.feature.metaupdate/starsky.feature.metaupdate.csproj b/starsky/starsky.feature.metaupdate/starsky.feature.metaupdate.csproj index 3d2507251e..2d410cf14b 100644 --- a/starsky/starsky.feature.metaupdate/starsky.feature.metaupdate.csproj +++ b/starsky/starsky.feature.metaupdate/starsky.feature.metaupdate.csproj @@ -5,15 +5,16 @@ starsky.feature.metaupdate {9567d576-4dee-481b-b316-c55d493416f4} 0.6.0-beta.0 + enable - + - - - - - - + + + + + + diff --git a/starsky/starsky.feature.packagetelemetry/Services/DeviceIdService.cs b/starsky/starsky.feature.packagetelemetry/Services/DeviceIdService.cs index 9814d6ab4c..ba0f26f9ea 100644 --- a/starsky/starsky.feature.packagetelemetry/Services/DeviceIdService.cs +++ b/starsky/starsky.feature.packagetelemetry/Services/DeviceIdService.cs @@ -28,34 +28,34 @@ public DeviceIdService(ISelectorStorage selectorStorage, ISettingsService settin _settingsService = settingsService; _hostStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); } - + public string IoReg { get; set; } = "ioreg"; public string DbusMachineIdPath { get; set; } = "/var/lib/dbus/machine-id"; public string MachineIdPath2 { get; set; } = "/etc/machine-id"; public string BsdHostIdPath { get; set; } = "/etc/hostid"; - public async Task DeviceId(OSPlatform? currentPlatform ) + public async Task DeviceId(OSPlatform? currentPlatform) { var id = string.Empty; if ( currentPlatform == OSPlatform.OSX ) { id = await DeviceIdOsX(); } - + if ( currentPlatform == OSPlatform.Windows ) { id = DeviceIdWindows(currentPlatform); } - - if ( currentPlatform == OSPlatform.Linux || currentPlatform == OSPlatform.FreeBSD) + + if ( currentPlatform == OSPlatform.Linux || currentPlatform == OSPlatform.FreeBSD ) { id = await DeviceIdLinuxBsdAsync(); } // For privacy reason this content of this id will be anonymous id = Sha256.ComputeSha256(id); - + if ( string.IsNullOrEmpty(id) ) { id = await DeviceIdDatabaseId(); @@ -80,13 +80,13 @@ private async Task DeviceIdLinuxBsdAsync() var stream = _hostStorage.ReadStream(DbusMachineIdPath); return await StreamToStringHelper.StreamToStringAsync(stream); } - + if ( _hostStorage.ExistFile(MachineIdPath2) ) { var stream = _hostStorage.ReadStream(MachineIdPath2); return await StreamToStringHelper.StreamToStringAsync(stream); } - + if ( !_hostStorage.ExistFile(BsdHostIdPath) ) return string.Empty; var streamBsd = _hostStorage.ReadStream(BsdHostIdPath); return await StreamToStringHelper.StreamToStringAsync(streamBsd); @@ -101,8 +101,8 @@ internal async Task DeviceIdOsX() // ioreg -rd1 -c IOPlatformExpertDevice var result = await Command.Run(IoReg, "-rd1", "-c", "IOPlatformExpertDevice").Task; if ( !result.Success ) return string.Empty; - - var match = Regex.Match(result.StandardOutput,"\"IOPlatformUUID\" = \"[\\d+\\w+-]+\"", + + var match = Regex.Match(result.StandardOutput, "\"IOPlatformUUID\" = \"[\\d+\\w+-]+\"", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); var id = match.Value.Replace("\"IOPlatformUUID\" = \"", "").Replace('\"', ' ').Trim(); return id; @@ -111,7 +111,7 @@ internal async Task DeviceIdOsX() [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] internal static string DeviceIdWindows(OSPlatform? currentPlatform) { - if (currentPlatform != OSPlatform.Windows) + if ( currentPlatform != OSPlatform.Windows ) { return string.Empty; } diff --git a/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetry.cs b/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetry.cs index e4675c7a0a..db76e87d42 100644 --- a/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetry.cs +++ b/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetry.cs @@ -19,7 +19,7 @@ [assembly: InternalsVisibleTo("starskytest")] namespace starsky.feature.packagetelemetry.Services { - + [Service(typeof(IPackageTelemetry), InjectionLifetime = InjectionLifetime.Scoped)] public class PackageTelemetry : IPackageTelemetry { @@ -48,9 +48,9 @@ public PackageTelemetry(IHttpClientHelper httpClientHelper, AppSettings appSetti internal static OSPlatform? GetCurrentOsPlatform() { OSPlatform? currentPlatform = null; - foreach ( var platform in new List{OSPlatform.Linux, - OSPlatform.Windows, OSPlatform.OSX, - OSPlatform.FreeBSD}.Where(RuntimeInformation.IsOSPlatform) ) + foreach ( var platform in new List{OSPlatform.Linux, + OSPlatform.Windows, OSPlatform.OSX, + OSPlatform.FreeBSD}.Where(RuntimeInformation.IsOSPlatform) ) { currentPlatform = platform; } @@ -63,9 +63,9 @@ internal List> GetSystemData(OSPlatform? currentPla currentPlatform ??= GetCurrentOsPlatform(); var dockerContainer = currentPlatform == OSPlatform.Linux && - Environment.GetEnvironmentVariable( - "DOTNET_RUNNING_IN_CONTAINER") == "true"; - + Environment.GetEnvironmentVariable( + "DOTNET_RUNNING_IN_CONTAINER") == "true"; + var data = new List> { new KeyValuePair("UTCTime", DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), @@ -79,7 +79,7 @@ internal List> GetSystemData(OSPlatform? currentPla new KeyValuePair("DockerContainer", dockerContainer.ToString()), new KeyValuePair("CurrentCulture", CultureInfo.CurrentCulture.ThreeLetterISOLanguageName), new KeyValuePair("AspNetCoreEnvironment", Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Not set"), - new KeyValuePair("WebsiteName", GetEncryptedSiteName()), + new KeyValuePair("WebsiteName", GetEncryptedSiteName()), new KeyValuePair("DeviceId", deviceId ?? "Not set"), }; return data; @@ -98,7 +98,7 @@ internal async Task>> AddDatabaseData(List>> AddDatabaseData(List("FileIndexItemDirectoryCount",fileIndexItemDirectoryCount.ToString()), new KeyValuePair("FileIndexItemCount",fileIndexItemCount.ToString()) }); - + return data; } - internal List> AddAppSettingsData( List> data) + internal List> AddAppSettingsData(List> data) { var type = typeof(AppSettings); var properties = type.GetProperties(); // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var property in properties) + foreach ( var property in properties ) { var someAttribute = Array.Find(Attribute.GetCustomAttributes(property), x => x is PackageTelemetryAttribute); if ( someAttribute == null ) @@ -137,13 +137,13 @@ internal List> AddAppSettingsData( List) || - propValue?.GetType() == typeof(Dictionary>) ) + + if ( propValue?.GetType() == typeof(List) || + propValue?.GetType() == typeof(Dictionary>) ) { value = ParseContent(propValue); } @@ -160,17 +160,17 @@ internal static string ParseContent(object propValue) private async Task PostData(HttpContent formUrlEncodedContent) { - return (await _httpClientHelper.PostString("https://" + PackageTelemetryUrl,formUrlEncodedContent, - _appSettings.EnablePackageTelemetryDebug == true)).Key; + return ( await _httpClientHelper.PostString("https://" + PackageTelemetryUrl, formUrlEncodedContent, + _appSettings.EnablePackageTelemetryDebug == true) ).Key; } - + public async Task PackageTelemetrySend() { if ( _appSettings.EnablePackageTelemetry == false ) { return null; } - + var currentOsPlatform = GetCurrentOsPlatform(); var deviceId = await _deviceIdService.DeviceId(currentOsPlatform); @@ -184,12 +184,12 @@ private async Task PostData(HttpContent formUrlEncodedContent) { return await PostData(formEncodedData); } - + foreach ( var (key, value) in telemetryDataItems ) { _logger.LogInformation($"[EnablePackageTelemetryDebug] {key} - {value}"); } - + return null; } } diff --git a/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetryBackgroundService.cs b/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetryBackgroundService.cs index 21c251f857..6708a2199e 100644 --- a/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetryBackgroundService.cs +++ b/starsky/starsky.feature.packagetelemetry/Services/PackageTelemetryBackgroundService.cs @@ -32,7 +32,7 @@ public PackageTelemetryBackgroundService(IServiceScopeFactory serviceScopeFactor /// CompletedTask protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - using (var scope = _serviceScopeFactory.CreateScope()) + using ( var scope = _serviceScopeFactory.CreateScope() ) { var appSettings = scope.ServiceProvider.GetRequiredService(); var httpClientHelper = scope.ServiceProvider.GetRequiredService(); @@ -42,7 +42,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if ( appSettings.ApplicationType == AppSettings.StarskyAppType.WebController ) { - await new PackageTelemetry(httpClientHelper, appSettings,logger,query,deviceIdService).PackageTelemetrySend(); + await new PackageTelemetry(httpClientHelper, appSettings, logger, query, deviceIdService).PackageTelemetrySend(); } } } diff --git a/starsky/starsky.feature.packagetelemetry/starsky.feature.packagetelemetry.csproj b/starsky/starsky.feature.packagetelemetry/starsky.feature.packagetelemetry.csproj index ee0eacabde..1ad8f11aaa 100644 --- a/starsky/starsky.feature.packagetelemetry/starsky.feature.packagetelemetry.csproj +++ b/starsky/starsky.feature.packagetelemetry/starsky.feature.packagetelemetry.csproj @@ -1,24 +1,24 @@ - - - net8.0 - starsky.feature.packagetelemetry - {6fbad8a6-53fa-41d2-98a4-61eb46d70794} - 0.6.0-beta.0 - enable - - - - - - - - - - - - - - true - + + + net8.0 + starsky.feature.packagetelemetry + {6fbad8a6-53fa-41d2-98a4-61eb46d70794} + 0.6.0-beta.0 + enable + + + + + + + + + + + + + + true + diff --git a/starsky/starsky.feature.realtime/Services/CleanUpConnectionBackgroundService.cs b/starsky/starsky.feature.realtime/Services/CleanUpConnectionBackgroundService.cs index 3d13f3a71b..4afac55d52 100644 --- a/starsky/starsky.feature.realtime/Services/CleanUpConnectionBackgroundService.cs +++ b/starsky/starsky.feature.realtime/Services/CleanUpConnectionBackgroundService.cs @@ -26,10 +26,10 @@ public CleanUpConnectionBackgroundService(IServiceScopeFactory serviceScopeFacto /// CompletedTask protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - using (var scope = _serviceScopeFactory.CreateScope()) + using ( var scope = _serviceScopeFactory.CreateScope() ) { var connectionsService = scope.ServiceProvider.GetRequiredService(); - + //exception is already catch-ed in the service await connectionsService.CleanOldMessagesAsync(); } diff --git a/starsky/starsky.feature.realtime/Services/RealtimeConnectionsService.cs b/starsky/starsky.feature.realtime/Services/RealtimeConnectionsService.cs index 2a67aa5dcc..0ca40f8180 100644 --- a/starsky/starsky.feature.realtime/Services/RealtimeConnectionsService.cs +++ b/starsky/starsky.feature.realtime/Services/RealtimeConnectionsService.cs @@ -40,7 +40,7 @@ public async Task CleanOldMessagesAsync() } catch ( Exception exception ) { - if (! exception.Message.Contains("Notifications' doesn't exist") ) + if ( !exception.Message.Contains("Notifications' doesn't exist") ) { _logger.LogError(exception, "[CleanOldMessagesAsync] catch-ed exception"); } diff --git a/starsky/starsky.feature.realtime/starsky.feature.realtime.csproj b/starsky/starsky.feature.realtime/starsky.feature.realtime.csproj index 15168efe44..86a630eb34 100644 --- a/starsky/starsky.feature.realtime/starsky.feature.realtime.csproj +++ b/starsky/starsky.feature.realtime/starsky.feature.realtime.csproj @@ -1,19 +1,19 @@ - - net8.0 - starsky.feature.realtime - {4a749ec1-4e6d-4c68-b69c-00c5c80f5660} - 0.6.0-beta.0 - enable - + + net8.0 + starsky.feature.realtime + {4a749ec1-4e6d-4c68-b69c-00c5c80f5660} + 0.6.0-beta.0 + enable + - - - - + + + + - - true - + + true + diff --git a/starsky/starsky.feature.rename/Services/RenameService.cs b/starsky/starsky.feature.rename/Services/RenameService.cs index da9e2e9017..7dfbf89244 100644 --- a/starsky/starsky.feature.rename/Services/RenameService.cs +++ b/starsky/starsky.feature.rename/Services/RenameService.cs @@ -11,505 +11,545 @@ using starsky.foundation.storage.Models; [assembly: InternalsVisibleTo("starskytest")] -namespace starsky.feature.rename.Services + +namespace starsky.feature.rename.Services; + +public class RenameService { - public class RenameService - { - private readonly IQuery _query; - private readonly IStorage _iStorage; + private readonly IQuery _query; + private readonly IStorage _iStorage; + + public RenameService(IQuery query, IStorage iStorage) + { + _query = query; + _iStorage = iStorage; + } + + /// Move or rename files and update the database. + /// The services also returns the source folders/files as NotFoundSourceMissing + /// subPath to file or folder + /// subPath location to move + /// true = copy files with the same name + public async Task> Rename(string f, string to, bool collections = true) + { + // -- param name="addDirectoryIfNotExist">true = create an directory if an parent directory is missing + + var ((inputFileSubPaths, toFileSubPaths), fileIndexResultsList) = + InputOutputSubPathsPreflight(f, to, collections); - public RenameService(IQuery query, IStorage iStorage) + for ( var i = 0; i < toFileSubPaths.Length; i++ ) { - _query = query; - _iStorage = iStorage; + // options + // 1. FromFolderToDeleted: + // folder rename + // 2. FromFolderToFolder: + // folder with child folders to folder + // 3. Not named + // file to file + // - overwrite a file is not supported + // 4. FromFileToDeleted: + // rename a file to new location + // 5. FromFileToFolder: + // file to direct folder file.jpg -> /folder/ + // 6. folder merge parent folder with current folder (not covered), /test/ => /test/test/ + + var inputFileSubPath = inputFileSubPaths[i]; + var toFileSubPath = toFileSubPaths[i]; + var detailView = _query.SingleItem(inputFileSubPath, null, collections, false); + + // The To location must be + var inputFileFolderStatus = _iStorage.IsFolderOrFile(inputFileSubPath); + var toFileFolderStatus = _iStorage.IsFolderOrFile(toFileSubPath); + + var fileIndexItems = new List(); + switch ( inputFileFolderStatus ) + { + case FolderOrFileModel.FolderOrFileTypeList.Folder when toFileFolderStatus == + FolderOrFileModel.FolderOrFileTypeList.Deleted: + await FromFolderToDeleted(inputFileSubPath, toFileSubPath, + fileIndexResultsList, detailView); + break; + case FolderOrFileModel.FolderOrFileTypeList.Folder when toFileFolderStatus == + FolderOrFileModel.FolderOrFileTypeList.Folder: + await FromFolderToFolder(inputFileSubPath, toFileSubPath, + fileIndexResultsList, detailView!); + break; + case FolderOrFileModel.FolderOrFileTypeList.File when toFileFolderStatus == + FolderOrFileModel.FolderOrFileTypeList.File: + // overwrite a file is not supported + fileIndexResultsList.Add(new FileIndexItem + { + Status = FileIndexItem.ExifStatus.OperationNotSupported + }); + break; + case FolderOrFileModel.FolderOrFileTypeList.File when toFileFolderStatus == + FolderOrFileModel.FolderOrFileTypeList.Deleted: + // toFileSubPath should contain the full subPath + await FromFileToDeleted(inputFileSubPath, toFileSubPath, + fileIndexResultsList, fileIndexItems, detailView!); + break; + case FolderOrFileModel.FolderOrFileTypeList.File when toFileFolderStatus == + FolderOrFileModel.FolderOrFileTypeList.Folder: + toFileSubPath = GetFileName(toFileSubPath, inputFileSubPath); + // toFileSubPath must be the to copy directory, the filename is kept the same + await FromFileToFolder(inputFileSubPath, toFileSubPath, + fileIndexResultsList, fileIndexItems, detailView!); + break; + } } - /// Move or rename files and update the database. - /// The services also returns the source folders/files as NotFoundSourceMissing - /// subPath to file or folder - /// subPath location to move - /// true = copy files with the same name - public async Task> Rename(string f, string to, bool collections = true) - { - // -- param name="addDirectoryIfNotExist">true = create an directory if an parent directory is missing + return fileIndexResultsList; + } + + private async Task SaveToDatabaseAsync(List fileIndexItems, + List fileIndexResultsList, DetailView detailView, string toFileSubPath) + { + // Rename parent item >eg the folder or file + detailView.FileIndexItem!.SetFilePath(toFileSubPath); + detailView.FileIndexItem.Status = FileIndexItem.ExifStatus.Ok; - var ((inputFileSubPaths, toFileSubPaths), fileIndexResultsList) = - InputOutputSubPathsPreflight(f, to, collections); + fileIndexItems.Add(detailView.FileIndexItem); - for (var i = 0; i < toFileSubPaths.Length; i++) + // To update the file that is changed + await _query.UpdateItemAsync(fileIndexItems); + + fileIndexResultsList.AddRange(fileIndexItems); + } + + private async Task FromFolderToDeleted(string inputFileSubPath, + string toFileSubPath, List fileIndexResultsList, + DetailView? detailView) + { + // clean from cache + _query.RemoveCacheParentItem(inputFileSubPath); + + var fileIndexItems = await _query.GetAllRecursiveAsync(inputFileSubPath); + // Rename child items + fileIndexItems.ForEach(p => { - // options - // 1. FromFolderToDeleted: - // folder rename - // 2. FromFolderToFolder: - // folder with child folders to folder - // 3. Not named - // file to file - // - overwrite a file is not supported - // 4. FromFileToDeleted: - // rename a file to new location - // 5. FromFileToFolder: - // file to direct folder file.jpg -> /folder/ - // 6. folder merge parent folder with current folder (not covered), /test/ => /test/test/ - - var inputFileSubPath = inputFileSubPaths[i]; - var toFileSubPath = toFileSubPaths[i]; - var detailView = _query.SingleItem(inputFileSubPath, null, collections, false); - - // The To location must be - var inputFileFolderStatus = _iStorage.IsFolderOrFile(inputFileSubPath); - var toFileFolderStatus = _iStorage.IsFolderOrFile(toFileSubPath); - - var fileIndexItems = new List(); - switch ( inputFileFolderStatus ) - { - case FolderOrFileModel.FolderOrFileTypeList.Folder when toFileFolderStatus == FolderOrFileModel.FolderOrFileTypeList.Deleted: - await FromFolderToDeleted(inputFileSubPath, toFileSubPath, fileIndexResultsList, detailView); - break; - case FolderOrFileModel.FolderOrFileTypeList.Folder when toFileFolderStatus == FolderOrFileModel.FolderOrFileTypeList.Folder: - await FromFolderToFolder(inputFileSubPath, toFileSubPath, fileIndexResultsList, detailView); - break; - case FolderOrFileModel.FolderOrFileTypeList.File when toFileFolderStatus == FolderOrFileModel.FolderOrFileTypeList.File: - // overwrite a file is not supported - fileIndexResultsList.Add(new FileIndexItem - { - Status = FileIndexItem.ExifStatus.OperationNotSupported - }); - break; - case FolderOrFileModel.FolderOrFileTypeList.File when toFileFolderStatus == FolderOrFileModel.FolderOrFileTypeList.Deleted: - // toFileSubPath should contain the full subPath - await FromFileToDeleted(inputFileSubPath, toFileSubPath, - fileIndexResultsList, fileIndexItems, detailView); - break; - case FolderOrFileModel.FolderOrFileTypeList.File when toFileFolderStatus == FolderOrFileModel.FolderOrFileTypeList.Folder: - toFileSubPath = GetFileName(toFileSubPath, inputFileSubPath); - // toFileSubPath must be the to copy directory, the filename is kept the same - await FromFileToFolder(inputFileSubPath, toFileSubPath, fileIndexResultsList, fileIndexItems, detailView); - break; - } + var parentDirectory = p.ParentDirectory! + .Replace(inputFileSubPath, toFileSubPath); + p.ParentDirectory = parentDirectory; + p.Status = FileIndexItem.ExifStatus.Ok; + p.Tags = p.Tags!.Replace(TrashKeyword.TrashKeywordString, string.Empty); } - return fileIndexResultsList; - } + ); - private async Task SaveToDatabaseAsync(List fileIndexItems, - List fileIndexResultsList, DetailView detailView, string toFileSubPath) + // when there is already a database item in the output folder, but not on disk + // in the final step we going to update the database item to the new name + var toCheckList = fileIndexItems.Select(p => p.FilePath).Cast().ToList(); + toCheckList.Add(toFileSubPath); + var checkOutput = await _query.GetObjectsByFilePathQueryAsync(toCheckList); + foreach ( var item in checkOutput ) { - // Rename parent item >eg the folder or file - detailView.FileIndexItem!.SetFilePath(toFileSubPath); - detailView.FileIndexItem.Status = FileIndexItem.ExifStatus.Ok; - - fileIndexItems.Add(detailView.FileIndexItem); - - // To update the file that is changed - await _query.UpdateItemAsync(fileIndexItems); - - fileIndexResultsList.AddRange(fileIndexItems); + await _query.RemoveItemAsync(item); } - private async Task FromFolderToDeleted(string inputFileSubPath, - string toFileSubPath, List fileIndexResultsList, - DetailView detailView) + // save before changing on disk + await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, + detailView!, toFileSubPath); + + // move entire folder + _iStorage.FolderMove(inputFileSubPath, toFileSubPath); + + fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath) { - // clean from cache - _query.RemoveCacheParentItem(inputFileSubPath); - - var fileIndexItems = await _query.GetAllRecursiveAsync(inputFileSubPath); - // Rename child items - fileIndexItems.ForEach(p => + Status = FileIndexItem.ExifStatus.NotFoundSourceMissing + }); + } + + private static string GetFileName(string toFileSubPath, string inputFileSubPath) + { + // Needed to create SetFilePath() for item that is copied, not the folder + // no double slash when moving to root folder + return toFileSubPath == "/" + ? $"/{FilenamesHelper.GetFileName(inputFileSubPath)}" + : $"{toFileSubPath}/{FilenamesHelper.GetFileName(inputFileSubPath)}"; + } + + /// + /// Checks for inputs that denied the request + /// + /// list of filePaths in string format (dot comma separated) + /// list of filePaths in string format (dot comma separated) + /// is Collections enabled + /// Tuple that contains two items: + /// item1) Tuple of the input output string - when fails this two array's has no items + /// item2) the list of fileIndex Items. + /// This contains only values when something is wrong and the request is denied + internal Tuple, List> InputOutputSubPathsPreflight + (string f, string to, bool collections) + { + var inputFileSubPaths = PathHelper.SplitInputFilePaths(f).Cast().ToList(); + var toFileSubPaths = PathHelper.SplitInputFilePaths(to).Cast().ToList(); + + // check for the same input + if ( inputFileSubPaths.SequenceEqual(toFileSubPaths) ) + { + return new Tuple, List>( + new Tuple(Array.Empty(), Array.Empty()), + new List { - var parentDirectory = p.ParentDirectory! - .Replace(inputFileSubPath, toFileSubPath); - p.ParentDirectory = parentDirectory; - p.Status = FileIndexItem.ExifStatus.Ok; - p.Tags = p.Tags!.Replace(TrashKeyword.TrashKeywordString, string.Empty); + new FileIndexItem + { + Status = FileIndexItem.ExifStatus.OperationNotSupported + } } ); + } + + // the result list + var fileIndexResultsList = new List(); + + for ( var i = 0; i < inputFileSubPaths.Count; i++ ) + { + var inputFileSubPath = PathHelper.RemoveLatestSlash(inputFileSubPaths[i]!); + inputFileSubPaths[i] = + PathHelper.PrefixDbSlash(PathHelper.RemovePrefixDbSlash(inputFileSubPath)); - // when there is already a database item in the output folder, but not on disk - // in the final step we going to update the database item to the new name - var toCheckList = fileIndexItems.Select(p => p.FilePath).ToList(); - toCheckList.Add(toFileSubPath); - var checkOutput = await _query.GetObjectsByFilePathQueryAsync(toCheckList); - foreach ( var item in checkOutput ) + var detailView = _query.SingleItem(inputFileSubPaths[i]!, null, collections, false); + if ( detailView == null ) { - await _query.RemoveItemAsync(item); + inputFileSubPaths[i] = null; } - - // save before changing on disk - await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, - detailView, toFileSubPath); - - // move entire folder - _iStorage.FolderMove(inputFileSubPath,toFileSubPath); - - fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath){Status = FileIndexItem.ExifStatus.NotFoundSourceMissing}); } - private static string GetFileName(string toFileSubPath, string inputFileSubPath) - { - // Needed to create SetFilePath() for item that is copied, not the folder - // no double slash when moving to root folder - return toFileSubPath == "/" ? $"/{FilenamesHelper.GetFileName(inputFileSubPath)}" - : $"{toFileSubPath}/{FilenamesHelper.GetFileName(inputFileSubPath)}"; - } - - /// - /// Checks for inputs that denied the request - /// - /// list of filePaths in string format (dot comma separated) - /// list of filePaths in string format (dot comma separated) - /// is Collections enabled - /// Tuple that contains two items: - /// item1) Tuple of the input output string - when fails this two array's has no items - /// item2) the list of fileIndex Items. - /// This contains only values when something is wrong and the request is denied - internal Tuple,List> InputOutputSubPathsPreflight - (string f, string to, bool collections) + // To check if the file/or folder has a unique name (in database) + for ( var i = 0; i < toFileSubPaths.Count; i++ ) { - var inputFileSubPaths = PathHelper.SplitInputFilePaths(f).ToList(); - var toFileSubPaths = PathHelper.SplitInputFilePaths(to).ToList(); + var toFileSubPath = PathHelper.RemoveLatestSlash(toFileSubPaths[i]!); + toFileSubPaths[i] = PathHelper.PrefixDbSlash(toFileSubPath); + + // to move + var detailView = _query.SingleItem(toFileSubPaths[i]!, null, collections, false); - // check for the same input - if ( inputFileSubPaths.SequenceEqual(toFileSubPaths) ) + // skip for files + if ( detailView?.FileIndexItem == null ) { - return new Tuple, List>( - new Tuple(Array.Empty(), Array.Empty()), - new List - { - new FileIndexItem - { - Status = FileIndexItem.ExifStatus.OperationNotSupported - } - } - ); + // do NOT set null because you move to location that currently doesn't exist + // and avoid mixing up the order of files + continue; } - - // the result list - var fileIndexResultsList = new List(); - - for (var i = 0; i < inputFileSubPaths.Count; i++) - { - var inputFileSubPath = PathHelper.RemoveLatestSlash(inputFileSubPaths[i]); - inputFileSubPaths[i] = PathHelper.PrefixDbSlash(PathHelper.RemovePrefixDbSlash(inputFileSubPath)); - var detailView = _query.SingleItem(inputFileSubPaths[i], null, collections, false); - if ( detailView == null ) - { - inputFileSubPaths[i] = null; - } - } - - // To check if the file/or folder has a unique name (in database) - for (var i = 0; i < toFileSubPaths.Count; i++) + // dirs are mergeable, when it isn't a directory + if ( detailView.FileIndexItem.IsDirectory == false ) { - var toFileSubPath = PathHelper.RemoveLatestSlash(toFileSubPaths[i]); - toFileSubPaths[i] = PathHelper.PrefixDbSlash(toFileSubPath); - - // to move - var detailView = _query.SingleItem(toFileSubPaths[i], null, collections, false); - - // skip for files - if ( detailView?.FileIndexItem == null ) - { - // do NOT set null because you move to location that currently doesn't exist - // and avoid mixing up the order of files - continue; - } - // dirs are mergeable, when it isn't a directory - if ( detailView.FileIndexItem.IsDirectory == false ) - { - toFileSubPaths[i] = null; - } + toFileSubPaths[i] = null; } - - // // Remove null from list - // remove both values when ONE OF those two values are null - for ( var i = 0; i < toFileSubPaths.Count; i++ ) + } + + // // Remove null from list + // remove both values when ONE OF those two values are null + for ( var i = 0; i < toFileSubPaths.Count; i++ ) + { + if ( toFileSubPaths[i] != null && inputFileSubPaths[i] != null ) continue; + toFileSubPaths.RemoveAt(i); + inputFileSubPaths.RemoveAt(i); + fileIndexResultsList.Add(new FileIndexItem { - if ( toFileSubPaths[i] != null && inputFileSubPaths[i] != null ) continue; - toFileSubPaths.RemoveAt(i); - inputFileSubPaths.RemoveAt(i); - fileIndexResultsList.Add(new FileIndexItem - { - Status = FileIndexItem.ExifStatus.NotFoundNotInIndex - }); - } - - // Check if two list are the same Length - Change this in the future BadRequest("f != to") - // when moving a file that does not exist (/non-exist.jpg to /non-exist2.jpg) - if (toFileSubPaths.Count != inputFileSubPaths.Count || - toFileSubPaths.Count == 0 || inputFileSubPaths.Count == 0) - { - // files that not exist - fileIndexResultsList.Add(new FileIndexItem - { - Status = FileIndexItem.ExifStatus.NotFoundNotInIndex - }); - return new Tuple, List>( - new Tuple(Array.Empty(), Array.Empty()), - fileIndexResultsList - ); - } - return CollectionAddPreflight(inputFileSubPaths, toFileSubPaths, fileIndexResultsList, collections); + Status = FileIndexItem.ExifStatus.NotFoundNotInIndex + }); } - /// - /// Get the collections items when preflighting - /// Returns as Tuple - /// item1: inputFileSubPaths, toFileSubPaths - /// item2: list of fileIndex Results (which contains only error cases) - /// - /// from where to copy (file or folder) - /// copy to (file or folder) - /// results list - /// enable file collections - /// inputFileSubPaths list, toFileSubPaths list and fileIndexResultsList - private Tuple, List> CollectionAddPreflight( - IReadOnlyList inputFileSubPaths, IReadOnlyList toFileSubPaths, - List fileIndexResultsList, bool collections) + // Check if two list are the same Length - Change this in the future BadRequest("f != to") + // when moving a file that does not exist (/non-exist.jpg to /non-exist2.jpg) + if ( toFileSubPaths.Count != inputFileSubPaths.Count || + toFileSubPaths.Count == 0 || inputFileSubPaths.Count == 0 ) { - if ( !collections ) + // files that not exist + fileIndexResultsList.Add(new FileIndexItem { - return new Tuple, List>( - new Tuple(inputFileSubPaths.ToArray(), toFileSubPaths.ToArray()), - fileIndexResultsList - ); + Status = FileIndexItem.ExifStatus.NotFoundNotInIndex + }); + return new Tuple, List>( + new Tuple(Array.Empty(), Array.Empty()), + fileIndexResultsList + ); + } + + return CollectionAddPreflight(inputFileSubPaths!, toFileSubPaths!, fileIndexResultsList, + collections); + } + + /// + /// Get the collections items when preflighting + /// Returns as Tuple + /// item1: inputFileSubPaths, toFileSubPaths + /// item2: list of fileIndex Results (which contains only error cases) + /// + /// from where to copy (file or folder) + /// copy to (file or folder) + /// results list + /// enable file collections + /// inputFileSubPaths list, toFileSubPaths list and fileIndexResultsList + private Tuple, List> CollectionAddPreflight( + IReadOnlyList inputFileSubPaths, IReadOnlyList toFileSubPaths, + List fileIndexResultsList, bool collections) + { + if ( !collections ) + { + return new Tuple, List>( + new Tuple(inputFileSubPaths.ToArray(), + toFileSubPaths.ToArray()), + fileIndexResultsList + ); + } + + var inputCollectionFileSubPaths = new List(); + var toCollectionFileSubPaths = new List(); + + for ( var i = 0; i < inputFileSubPaths.Count; i++ ) + { + // When the input is a folder, just copy the array + if ( _iStorage.ExistFolder(inputFileSubPaths[i]) ) + { + inputCollectionFileSubPaths.Add(inputFileSubPaths[i]); + toCollectionFileSubPaths.Add(toFileSubPaths[i]); + continue; } - - var inputCollectionFileSubPaths = new List(); - var toCollectionFileSubPaths = new List(); - for (var i = 0; i < inputFileSubPaths.Count; i++) + // when it is a file update the 'to paths' + var querySingleItemCollections = _query.SingleItem(inputFileSubPaths[i], + null, true, false); + var collectionPaths = querySingleItemCollections!.FileIndexItem!.CollectionPaths; + + inputCollectionFileSubPaths.AddRange(collectionPaths); + + for ( var j = 0; j < collectionPaths.Count; j++ ) { - // When the input is a folder, just copy the array - if ( _iStorage.ExistFolder(inputFileSubPaths[i]) ) + var collectionItem = collectionPaths[j]; + // When moving to a folder + if ( _iStorage.ExistFolder(toFileSubPaths[i]) ) { - inputCollectionFileSubPaths.Add(inputFileSubPaths[i]); toCollectionFileSubPaths.Add(toFileSubPaths[i]); continue; } - - // when it is a file update the 'to paths' - var querySingleItemCollections = _query.SingleItem(inputFileSubPaths[i], - null, true, false); - var collectionPaths = querySingleItemCollections!.FileIndexItem!.CollectionPaths; - - inputCollectionFileSubPaths.AddRange(collectionPaths); - - for ( var j = 0; j < collectionPaths.Count; j++ ) + + var extensionWithoutDot = + FilenamesHelper.GetFileExtensionWithoutDot(collectionItem); + // when rename-ing the current file, but the other ones are implicit copied + if ( j == 0 ) { - var collectionItem = collectionPaths[j]; - // When moving to a folder - if ( _iStorage.ExistFolder(toFileSubPaths[i]) ) - { - toCollectionFileSubPaths.Add(toFileSubPaths[i]); - continue; - } - - var extensionWithoutDot = FilenamesHelper.GetFileExtensionWithoutDot(collectionItem); - // when rename-ing the current file, but the other ones are implicit copied - if ( j == 0 ) - { - extensionWithoutDot = FilenamesHelper.GetFileExtensionWithoutDot(toFileSubPaths[i]); - } - - // Rename other sidecar files - // From file to Deleted - var parentFolder = PathHelper.AddSlash(FilenamesHelper.GetParentPath(toFileSubPaths[i])); - var baseName = FilenamesHelper.GetFileNameWithoutExtension(toFileSubPaths[i]); - toCollectionFileSubPaths.Add($"{parentFolder}{baseName}.{extensionWithoutDot}"); + extensionWithoutDot = + FilenamesHelper.GetFileExtensionWithoutDot(toFileSubPaths[i]); } - } - return new Tuple, List>( - new Tuple(inputCollectionFileSubPaths.ToArray(), toCollectionFileSubPaths.ToArray()), - fileIndexResultsList - ); + // Rename other sidecar files + // From file to Deleted + var parentFolder = + PathHelper.AddSlash(FilenamesHelper.GetParentPath(toFileSubPaths[i])); + var baseName = FilenamesHelper.GetFileNameWithoutExtension(toFileSubPaths[i]); + toCollectionFileSubPaths.Add($"{parentFolder}{baseName}.{extensionWithoutDot}"); + } } - /// - /// Move sidecar files when those exist - /// - /// from path - /// to path - private void MoveSidecarFile(string inputFileSubPath, string toFileSubPath) + return new Tuple, List>( + new Tuple(inputCollectionFileSubPaths.ToArray(), + toCollectionFileSubPaths.ToArray()), + fileIndexResultsList + ); + } + + /// + /// Move sidecar files when those exist + /// + /// from path + /// to path + private void MoveSidecarFile(string inputFileSubPath, string toFileSubPath) + { + // json sidecar move + var jsonInputFileSubPathSidecarFile = JsonSidecarLocation + .JsonLocation(inputFileSubPath); + var jsonSidecarFile = JsonSidecarLocation.JsonLocation(toFileSubPath); + + if ( _iStorage.ExistFile(jsonInputFileSubPathSidecarFile) ) { - // json sidecar move - var jsonInputFileSubPathSidecarFile = JsonSidecarLocation - .JsonLocation(inputFileSubPath); - var jsonSidecarFile = JsonSidecarLocation.JsonLocation(toFileSubPath); - - if ( _iStorage.ExistFile(jsonInputFileSubPathSidecarFile) ) - { - _iStorage.FileMove(jsonInputFileSubPathSidecarFile,jsonSidecarFile); - } + _iStorage.FileMove(jsonInputFileSubPathSidecarFile, jsonSidecarFile); + } - // xmp sidecar file move - if ( !ExtensionRolesHelper.IsExtensionForceXmp(inputFileSubPath) ) - { - return; - } - var xmpInputFileSubPathSidecarFile = ExtensionRolesHelper - .ReplaceExtensionWithXmp(inputFileSubPath); - var xmpSidecarFile = ExtensionRolesHelper - .ReplaceExtensionWithXmp(toFileSubPath); - if ( _iStorage.ExistFile(xmpInputFileSubPathSidecarFile) ) - { - _iStorage.FileMove(xmpInputFileSubPathSidecarFile, xmpSidecarFile); - } + // xmp sidecar file move + if ( !ExtensionRolesHelper.IsExtensionForceXmp(inputFileSubPath) ) + { + return; } - - internal Task FromFolderToFolder(string inputFileSubPath, - string toFileSubPath, List fileIndexResultsList, DetailView detailView) + var xmpInputFileSubPathSidecarFile = ExtensionRolesHelper + .ReplaceExtensionWithXmp(inputFileSubPath); + var xmpSidecarFile = ExtensionRolesHelper + .ReplaceExtensionWithXmp(toFileSubPath); + if ( _iStorage.ExistFile(xmpInputFileSubPathSidecarFile) ) { - if ( fileIndexResultsList == null ) - { - throw new ArgumentNullException(nameof(fileIndexResultsList), "Should contain value"); - } - return FromFolderToFolderAsync(inputFileSubPath, toFileSubPath, fileIndexResultsList,detailView); + _iStorage.FileMove(xmpInputFileSubPathSidecarFile, xmpSidecarFile); } - - /// - /// Copy from a folder to a folder - /// - /// from path - /// to path - /// - /// - /// fileIndexItems is null - private async Task FromFolderToFolderAsync(string inputFileSubPath, - string toFileSubPath, List fileIndexResultsList, DetailView detailView) + } + + + internal Task FromFolderToFolder(string inputFileSubPath, + string toFileSubPath, List fileIndexResultsList, DetailView detailView) + { + if ( fileIndexResultsList == null ) { - // 1. Get Direct child files - // 2. Get Direct folder and child folders - // 3. move child files - // 4. remove old folder - - // Store Child folders - var directChildFolders = new List(); - directChildFolders.AddRange(_iStorage.GetDirectoryRecursive(inputFileSubPath).Select(p => p.Key)); - - // Store direct files - var directChildItems = new List(); - directChildItems.AddRange(_iStorage.GetAllFilesInDirectory(inputFileSubPath)); - - // Replace all Recursive items in Query - // Does only replace in existing database items - var fileIndexItems = await _query.GetAllRecursiveAsync(inputFileSubPath); - - // Rename child items - fileIndexItems.ForEach(p => - { - p.ParentDirectory = p.ParentDirectory! - .Replace(inputFileSubPath, toFileSubPath); - p.Status = FileIndexItem.ExifStatus.Ok; - } - ); - - // save before changing on disk - await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, - detailView, toFileSubPath); - - // rename child folders - foreach ( var inputChildFolder in directChildFolders ) + throw new ArgumentNullException(nameof(fileIndexResultsList), + "Should contain value"); + } + + return FromFolderToFolderAsync(inputFileSubPath, toFileSubPath, fileIndexResultsList, + detailView); + } + + /// + /// Copy from a folder to a folder + /// + /// from path + /// to path + /// + /// + /// fileIndexItems is null + private async Task FromFolderToFolderAsync(string inputFileSubPath, + string toFileSubPath, List fileIndexResultsList, DetailView detailView) + { + // 1. Get Direct child files + // 2. Get Direct folder and child folders + // 3. move child files + // 4. remove old folder + + // Store Child folders + var directChildFolders = new List(); + directChildFolders.AddRange(_iStorage.GetDirectoryRecursive(inputFileSubPath) + .Select(p => p.Key)); + + // Store direct files + var directChildItems = new List(); + directChildItems.AddRange(_iStorage.GetAllFilesInDirectory(inputFileSubPath)); + + // Replace all Recursive items in Query + // Does only replace in existing database items + var fileIndexItems = await _query.GetAllRecursiveAsync(inputFileSubPath); + + // Rename child items + fileIndexItems.ForEach(p => { - // First FileSys (with folders) - var outputChildItem = inputChildFolder + p.ParentDirectory = p.ParentDirectory! .Replace(inputFileSubPath, toFileSubPath); - _iStorage.FolderMove(inputChildFolder,outputChildItem); + p.Status = FileIndexItem.ExifStatus.Ok; } + ); - // rename child files - foreach ( var inputChildItem in directChildItems ) - { - // First FileSys - var outputChildItem = inputChildItem.Replace(inputFileSubPath, toFileSubPath); - _iStorage.FileMove(inputChildItem,outputChildItem); - } - - // when renaming a folder it should warn the UI that it should remove the source item - fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath){Status = FileIndexItem.ExifStatus.NotFoundSourceMissing}); + // save before changing on disk + await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, + detailView, toFileSubPath); + + // rename child folders + foreach ( var inputChildFolder in directChildFolders ) + { + // First FileSys (with folders) + var outputChildItem = inputChildFolder + .Replace(inputFileSubPath, toFileSubPath); + _iStorage.FolderMove(inputChildFolder, outputChildItem); } - private async Task FromFileToDeleted(string inputFileSubPath, string toFileSubPath, - List fileIndexResultsList, List fileIndexItems, DetailView detailView) + // rename child files + foreach ( var inputChildItem in directChildItems ) { - // when trying to rename something wrongs - var fileName = FilenamesHelper.GetFileName(toFileSubPath); - if ( !FilenamesHelper.IsValidFileName(fileName) ) - { - fileIndexResultsList.Add(new FileIndexItem - { - Status = FileIndexItem.ExifStatus.OperationNotSupported - }); - return; //next - } - - // from/input cache should be cleared - var inputParentSubFolder = FilenamesHelper.GetParentPath(inputFileSubPath); - _query.RemoveCacheParentItem(inputParentSubFolder); - - var toParentSubFolder = FilenamesHelper.GetParentPath(toFileSubPath); - if ( string.IsNullOrEmpty(toParentSubFolder) ) toParentSubFolder = "/"; - - // clear cache (to FileSubPath parents) - _query.RemoveCacheParentItem(toParentSubFolder); - - // Check if the parent folder exist in the database - await _query.AddParentItemsAsync(toParentSubFolder); - - // Save in database before change on disk - await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, - detailView, toFileSubPath); - - // add folder to file system - if ( !_iStorage.ExistFolder(toParentSubFolder) ) + // First FileSys + var outputChildItem = inputChildItem.Replace(inputFileSubPath, toFileSubPath); + _iStorage.FileMove(inputChildItem, outputChildItem); + } + + // when renaming a folder it should warn the UI that it should remove the source item + fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath) + { + Status = FileIndexItem.ExifStatus.NotFoundSourceMissing + }); + } + + private async Task FromFileToDeleted(string inputFileSubPath, string toFileSubPath, + List fileIndexResultsList, List fileIndexItems, + DetailView detailView) + { + // when trying to rename something wrongs + var fileName = FilenamesHelper.GetFileName(toFileSubPath); + if ( !FilenamesHelper.IsValidFileName(fileName) ) + { + fileIndexResultsList.Add(new FileIndexItem { - _iStorage.CreateDirectory(toParentSubFolder); - fileIndexResultsList.Add(new FileIndexItem(toParentSubFolder){Status = FileIndexItem.ExifStatus.Ok}); - } - - _iStorage.FileMove(inputFileSubPath,toFileSubPath); - MoveSidecarFile(inputFileSubPath, toFileSubPath); - - // when renaming a folder it should warn the UI that it should remove the source item - fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath){Status = FileIndexItem.ExifStatus.NotFoundSourceMissing}); + Status = FileIndexItem.ExifStatus.OperationNotSupported + }); + return; //next } - private async Task FromFileToFolder(string inputFileSubPath, string toFileSubPath, - List fileIndexResultsList, List fileIndexItems, DetailView detailView) + // from/input cache should be cleared + var inputParentSubFolder = FilenamesHelper.GetParentPath(inputFileSubPath); + _query.RemoveCacheParentItem(inputParentSubFolder); + + var toParentSubFolder = FilenamesHelper.GetParentPath(toFileSubPath); + if ( string.IsNullOrEmpty(toParentSubFolder) ) toParentSubFolder = "/"; + + // clear cache (to FileSubPath parents) + _query.RemoveCacheParentItem(toParentSubFolder); + + // Check if the parent folder exist in the database + await _query.AddParentItemsAsync(toParentSubFolder); + + // Save in database before change on disk + await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, + detailView, toFileSubPath); + + // add folder to file system + if ( !_iStorage.ExistFolder(toParentSubFolder) ) + { + _iStorage.CreateDirectory(toParentSubFolder); + fileIndexResultsList.Add( + new FileIndexItem(toParentSubFolder) { Status = FileIndexItem.ExifStatus.Ok }); + } + + _iStorage.FileMove(inputFileSubPath, toFileSubPath); + MoveSidecarFile(inputFileSubPath, toFileSubPath); + + // when renaming a folder it should warn the UI that it should remove the source item + fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath) + { + Status = FileIndexItem.ExifStatus.NotFoundSourceMissing + }); + } + + private async Task FromFileToFolder(string inputFileSubPath, string toFileSubPath, + List fileIndexResultsList, List fileIndexItems, + DetailView detailView) + { + // you can't move the file to the same location + if ( inputFileSubPath == toFileSubPath ) { - // you can't move the file to the same location - if ( inputFileSubPath == toFileSubPath ) + fileIndexResultsList.Add(new FileIndexItem { - fileIndexResultsList.Add(new FileIndexItem - { - Status = FileIndexItem.ExifStatus.OperationNotSupported - }); - return; //next - } - // when renaming a folder it should warn the UI that it should remove the source item - fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath){Status = FileIndexItem.ExifStatus.NotFoundSourceMissing}); - - // from/input cache should be cleared - var inputParentSubFolder = Breadcrumbs.BreadcrumbHelper(inputFileSubPath).LastOrDefault(); - _query.RemoveCacheParentItem(inputParentSubFolder!); - - // clear cache // parentSubFolder (to FileSubPath parents) - var toParentSubFolder = Breadcrumbs.BreadcrumbHelper(toFileSubPath).LastOrDefault(); - _query.RemoveCacheParentItem(toParentSubFolder!); - - // Check if the parent folder exist in the database // parentSubFolder - await _query.AddParentItemsAsync(toParentSubFolder); - - await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, - detailView, toFileSubPath); - - // First update database and then update for disk watcher - _iStorage.FileMove(inputFileSubPath, toFileSubPath); - MoveSidecarFile(inputFileSubPath, toFileSubPath); + Status = FileIndexItem.ExifStatus.OperationNotSupported + }); + return; //next } - } + // when renaming a folder it should warn the UI that it should remove the source item + fileIndexResultsList.Add(new FileIndexItem(inputFileSubPath) + { + Status = FileIndexItem.ExifStatus.NotFoundSourceMissing + }); + + // from/input cache should be cleared + var inputParentSubFolder = + Breadcrumbs.BreadcrumbHelper(inputFileSubPath).LastOrDefault(); + _query.RemoveCacheParentItem(inputParentSubFolder!); + + // clear cache // parentSubFolder (to FileSubPath parents) + var toParentSubFolder = Breadcrumbs.BreadcrumbHelper(toFileSubPath).LastOrDefault(); + _query.RemoveCacheParentItem(toParentSubFolder!); + + // Check if the parent folder exist in the database // parentSubFolder + await _query.AddParentItemsAsync(toParentSubFolder!); + + await SaveToDatabaseAsync(fileIndexItems, fileIndexResultsList, + detailView, toFileSubPath); + + // First update database and then update for disk watcher + _iStorage.FileMove(inputFileSubPath, toFileSubPath); + MoveSidecarFile(inputFileSubPath, toFileSubPath); + } } diff --git a/starsky/starsky.feature.rename/starsky.feature.rename.csproj b/starsky/starsky.feature.rename/starsky.feature.rename.csproj index c48ddb77dc..ef635bdf64 100644 --- a/starsky/starsky.feature.rename/starsky.feature.rename.csproj +++ b/starsky/starsky.feature.rename/starsky.feature.rename.csproj @@ -4,11 +4,12 @@ net8.0 {a864f834-133f-4ea8-9a4d-53e5cad837ab} 0.6.0-beta.0 + enable - + - - + + diff --git a/starsky/starsky.feature.search/Interfaces/ISearch.cs b/starsky/starsky.feature.search/Interfaces/ISearch.cs index 1ebe83d200..37b370558e 100644 --- a/starsky/starsky.feature.search/Interfaces/ISearch.cs +++ b/starsky/starsky.feature.search/Interfaces/ISearch.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using starsky.feature.search.ViewModels; namespace starsky.feature.search.Interfaces diff --git a/starsky/starsky.feature.search/Interfaces/ISearchSuggest.cs b/starsky/starsky.feature.search/Interfaces/ISearchSuggest.cs index 270305c239..e90f3c5d72 100644 --- a/starsky/starsky.feature.search/Interfaces/ISearchSuggest.cs +++ b/starsky/starsky.feature.search/Interfaces/ISearchSuggest.cs @@ -3,7 +3,7 @@ namespace starsky.feature.search.Interfaces { - + public interface ISearchSuggest { Task> SearchSuggest(string query); diff --git a/starsky/starsky.feature.search/Services/SearchService.cs b/starsky/starsky.feature.search/Services/SearchService.cs index e127697541..0be8a6d209 100644 --- a/starsky/starsky.feature.search/Services/SearchService.cs +++ b/starsky/starsky.feature.search/Services/SearchService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -17,418 +17,417 @@ using starsky.foundation.platform.Models; using starsky.feature.search.Interfaces; using starsky.feature.search.ViewModels; -#nullable enable namespace starsky.feature.search.Services { [Service(typeof(ISearch), InjectionLifetime = InjectionLifetime.Scoped)] public class SearchService : ISearch - { - private readonly ApplicationDbContext _context; - private readonly IMemoryCache? _cache; - private readonly AppSettings? _appSettings; - private readonly IWebLogger _logger; - - public SearchService( - ApplicationDbContext context, - IWebLogger logger, - IMemoryCache? memoryCache = null, - AppSettings? appSettings = null) - { - _context = context; - _cache = memoryCache; - _appSettings = appSettings; - _logger = logger; - } - - /// - /// Search in database - /// - /// where to search in - /// current page (0 = page 1) - /// enable searchcache (in trash this is disabled) - /// - public async Task Search(string query = "", - int pageNumber = 0, bool enableCache = true) - { - if ( !string.IsNullOrEmpty(query) && query.Length >= 500 ) - { - throw new ArgumentException("Search Input Query is longer then 500 chars"); - } - - if ( query == TrashKeyword.TrashKeywordString ) - { - _logger.LogInformation("Skip cache for trash"); - enableCache = false; - } - - if ( !enableCache || - _cache == null || _appSettings?.AddMemoryCache == false ) - { - return SkipSearchItems(await SearchDirect(query),pageNumber); - } - - // Return values from IMemoryCache - var querySearchCacheName = "search-" + query; - - // Return Cached object if it exist - if ( _cache.TryGetValue(querySearchCacheName, - out var objectSearchModel) ) - { - return SkipSearchItems(objectSearchModel, pageNumber); - } - - // Try to catch a new object - objectSearchModel = await SearchDirect(query); - _cache.Set(querySearchCacheName, objectSearchModel, new TimeSpan(0,10,0)); - return SkipSearchItems(objectSearchModel, pageNumber); - } - - public bool? RemoveCache(string query) - { - // Add protection for disabled caching - if( _cache == null || _appSettings?.AddMemoryCache == false) return null; - - var queryCacheName = "search-" + query; - if (!_cache.TryGetValue(queryCacheName, out _)) return false; - _cache.Remove(queryCacheName); - return true; - } - - /// - /// Skip un-needed items - /// - /// - /// current page (0 = page 1) - /// - private static SearchViewModel SkipSearchItems(object? objectSearchModel, int pageNumber) - { - if ( objectSearchModel is not SearchViewModel searchModel) - { - return new SearchViewModel(); - } - - // Clone the item to avoid removing items from cache - searchModel = searchModel.Clone(); - if ( searchModel.FileIndexItems == null ) - { - return new SearchViewModel(); - } - - searchModel.PageNumber = pageNumber; - - var skipFirstNumber = pageNumber * NumberOfResultsInView; - var skipLastNumber = searchModel.SearchCount - ( pageNumber * NumberOfResultsInView ) - NumberOfResultsInView; - - - // Remove the last items - var skippedLastList = searchModel.FileIndexItems - .Skip(skipFirstNumber) - .SkipLast(skipLastNumber); - - var skippedHashSet = new HashSet(skippedLastList); - searchModel.FileIndexItems = skippedHashSet.ToList(); - - return searchModel; - } - - /// - /// Return all results - /// - /// where to search on - /// - private async Task SearchDirect(string? query = "") - { - var stopWatch = Stopwatch.StartNew(); - - // Create an view model - var model = new SearchViewModel - { - SearchQuery = query ?? string.Empty, - Breadcrumb = new List {"/", query ?? string.Empty } - // Null check will safe you from error 500 with Empty request - }; - - if ( query == null || model.FileIndexItems == null ) - { - return model; - } - - _orginalSearchQuery = model.SearchQuery; - - model.SearchQuery = QuerySafe(model.SearchQuery); - model.SearchQuery = QueryShortcuts(model.SearchQuery); - model = MatchSearch(model); - - model = await WideSearch(_context.FileIndex.AsNoTracking(),model); - - model = SearchViewModel.NarrowSearch(model); - - // Remove duplicates from list - model.FileIndexItems = model.FileIndexItems! - .Where(p => p.FilePath != null) - .GroupBy(s => s.FilePath) - .Select(grp => grp.FirstOrDefault()) - .OrderBy(s => s!.FilePath) - .ToList()!; - - model.SearchCount = model.FileIndexItems.Count; - - model.FileIndexItems = model.FileIndexItems - .OrderByDescending(p => p.DateTime).ToList(); - - model.LastPageNumber = GetLastPageNumber(model.SearchCount); - - model.ElapsedSeconds = stopWatch.Elapsed.TotalSeconds; - return model; - } - - /// - /// Main method to query the database, in other function there is sorting needed - /// - /// IQueryable database - /// temp output model - /// search model with content - [SuppressMessage("ReSharper", "AccessToModifiedClosure")] - [SuppressMessage("Performance", "CA1862:Use the \'StringComparison\' " + - "method overloads to perform case-insensitive string comparisons", - Justification = "EF Core does not support this: System.InvalidOperationException: The LINQ expression 'DbSet()")] - private async Task WideSearch(IQueryable sourceList, - SearchViewModel model) - { - var predicates = new List>>(); - - // .AsNoTracking() => never change data to update - for ( var i = 0; i < model.SearchIn.Count; i++ ) - { - Enum.TryParse(model.SearchIn[i].ToLowerInvariant(), + { + private readonly ApplicationDbContext _context; + private readonly IMemoryCache? _cache; + private readonly AppSettings? _appSettings; + private readonly IWebLogger _logger; + + public SearchService( + ApplicationDbContext context, + IWebLogger logger, + IMemoryCache? memoryCache = null, + AppSettings? appSettings = null) + { + _context = context; + _cache = memoryCache; + _appSettings = appSettings; + _logger = logger; + } + + /// + /// Search in database + /// + /// where to search in + /// current page (0 = page 1) + /// enable searchcache (in trash this is disabled) + /// + public async Task Search(string query = "", + int pageNumber = 0, bool enableCache = true) + { + if ( !string.IsNullOrEmpty(query) && query.Length >= 500 ) + { + throw new ArgumentException("Search Input Query is longer then 500 chars"); + } + + if ( query == TrashKeyword.TrashKeywordString ) + { + _logger.LogInformation("Skip cache for trash"); + enableCache = false; + } + + if ( !enableCache || + _cache == null || _appSettings?.AddMemoryCache == false ) + { + return SkipSearchItems(await SearchDirect(query), pageNumber); + } + + // Return values from IMemoryCache + var querySearchCacheName = "search-" + query; + + // Return Cached object if it exist + if ( _cache.TryGetValue(querySearchCacheName, + out var objectSearchModel) ) + { + return SkipSearchItems(objectSearchModel, pageNumber); + } + + // Try to catch a new object + objectSearchModel = await SearchDirect(query); + _cache.Set(querySearchCacheName, objectSearchModel, new TimeSpan(0, 10, 0)); + return SkipSearchItems(objectSearchModel, pageNumber); + } + + public bool? RemoveCache(string query) + { + // Add protection for disabled caching + if ( _cache == null || _appSettings?.AddMemoryCache == false ) return null; + + var queryCacheName = "search-" + query; + if ( !_cache.TryGetValue(queryCacheName, out _) ) return false; + _cache.Remove(queryCacheName); + return true; + } + + /// + /// Skip un-needed items + /// + /// + /// current page (0 = page 1) + /// + private static SearchViewModel SkipSearchItems(object? objectSearchModel, int pageNumber) + { + if ( objectSearchModel is not SearchViewModel searchModel ) + { + return new SearchViewModel(); + } + + // Clone the item to avoid removing items from cache + searchModel = searchModel.Clone(); + if ( searchModel.FileIndexItems == null ) + { + return new SearchViewModel(); + } + + searchModel.PageNumber = pageNumber; + + var skipFirstNumber = pageNumber * NumberOfResultsInView; + var skipLastNumber = searchModel.SearchCount - ( pageNumber * NumberOfResultsInView ) - NumberOfResultsInView; + + + // Remove the last items + var skippedLastList = searchModel.FileIndexItems + .Skip(skipFirstNumber) + .SkipLast(skipLastNumber); + + var skippedHashSet = new HashSet(skippedLastList); + searchModel.FileIndexItems = skippedHashSet.ToList(); + + return searchModel; + } + + /// + /// Return all results + /// + /// where to search on + /// + private async Task SearchDirect(string? query = "") + { + var stopWatch = Stopwatch.StartNew(); + + // Create an view model + var model = new SearchViewModel + { + SearchQuery = query ?? string.Empty, + Breadcrumb = new List { "/", query ?? string.Empty } + // Null check will safe you from error 500 with Empty request + }; + + if ( query == null || model.FileIndexItems == null ) + { + return model; + } + + _orginalSearchQuery = model.SearchQuery; + + model.SearchQuery = QuerySafe(model.SearchQuery); + model.SearchQuery = QueryShortcuts(model.SearchQuery); + model = MatchSearch(model); + + model = await WideSearch(_context.FileIndex.AsNoTracking(), model); + + model = SearchViewModel.NarrowSearch(model); + + // Remove duplicates from list + model.FileIndexItems = model.FileIndexItems! + .Where(p => p.FilePath != null) + .GroupBy(s => s.FilePath) + .Select(grp => grp.FirstOrDefault()) + .OrderBy(s => s!.FilePath) + .ToList()!; + + model.SearchCount = model.FileIndexItems.Count; + + model.FileIndexItems = model.FileIndexItems + .OrderByDescending(p => p.DateTime).ToList(); + + model.LastPageNumber = GetLastPageNumber(model.SearchCount); + + model.ElapsedSeconds = stopWatch.Elapsed.TotalSeconds; + return model; + } + + /// + /// Main method to query the database, in other function there is sorting needed + /// + /// IQueryable database + /// temp output model + /// search model with content + [SuppressMessage("ReSharper", "AccessToModifiedClosure")] + [SuppressMessage("Performance", "CA1862:Use the \'StringComparison\' " + + "method overloads to perform case-insensitive string comparisons", + Justification = "EF Core does not support this: System.InvalidOperationException: The LINQ expression 'DbSet()")] + private async Task WideSearch(IQueryable sourceList, + SearchViewModel model) + { + var predicates = new List>>(); + + // .AsNoTracking() => never change data to update + for ( var i = 0; i < model.SearchIn.Count; i++ ) + { + Enum.TryParse(model.SearchIn[i].ToLowerInvariant(), true, out var searchInType); - if ( model.SearchForOptions[i] == SearchViewModel.SearchForOptionType.Not ) - { - continue; - } - - switch ( searchInType ) - { - case SearchViewModel.SearchInTypes.imageformat: - if ( Enum.TryParse( - model.SearchFor[i].ToLowerInvariant(), out var castImageFormat) ) - { - var result = castImageFormat; - predicates.Add(x => x.ImageFormat == result); - } - break; - case SearchViewModel.SearchInTypes.description: + if ( model.SearchForOptions[i] == SearchViewModel.SearchForOptionType.Not ) + { + continue; + } + + switch ( searchInType ) + { + case SearchViewModel.SearchInTypes.imageformat: + if ( Enum.TryParse( + model.SearchFor[i].ToLowerInvariant(), out var castImageFormat) ) + { + var result = castImageFormat; + predicates.Add(x => x.ImageFormat == result); + } + break; + case SearchViewModel.SearchInTypes.description: // need to have description out of the Func<> // ToLowerInvariant.Contains(__description_1))' could not be translated. - var description = model.SearchFor[i]; - predicates.Add(x => x.Description!.ToLower().Contains(description)); - break; - case SearchViewModel.SearchInTypes.filename: - var filename = model.SearchFor[i]; - predicates.Add(x => x.FileName!.ToLower().Contains(filename)); - break; - case SearchViewModel.SearchInTypes.filepath: - var filePath = model.SearchFor[i]; - predicates.Add(x => x.FilePath!.ToLower().Contains(filePath)); - break; - case SearchViewModel.SearchInTypes.parentdirectory: - var parentDirectory = model.SearchFor[i]; - predicates.Add(x => x.ParentDirectory!.ToLower().Contains(parentDirectory)); - break; - case SearchViewModel.SearchInTypes.title: - var title = model.SearchFor[i]; - predicates.Add(x => x.Title!.ToLower().Contains(title)); - break; - case SearchViewModel.SearchInTypes.make: - // is in the database one field => will be filtered in narrowSearch - var make = model.SearchFor[i]; - predicates.Add(x => x.MakeModel!.ToLower().Contains(make)); - break; - case SearchViewModel.SearchInTypes.model: - // is in the database one field => will be filtered in narrowSearch - var modelMake = model.SearchFor[i]; - predicates.Add(x => x.MakeModel!.ToLower().Contains(modelMake)); - break; - case SearchViewModel.SearchInTypes.filehash: - var fileHash = model.SearchFor[i]; - predicates.Add(x => x.FileHash != null && x.FileHash.ToLower().Contains(fileHash) ); - break; - case SearchViewModel.SearchInTypes.software: - var software = model.SearchFor[i]; - predicates.Add(x => x.Software!.ToLower().Contains(software)); - break; - case SearchViewModel.SearchInTypes.isdirectory: - if ( bool.TryParse(model.SearchFor[i].ToLowerInvariant(), - out var boolIsDirectory) ) - { - predicates.Add(x => x.IsDirectory == boolIsDirectory); - model.SearchFor[i] = boolIsDirectory.ToString(); - } - break; - case SearchViewModel.SearchInTypes.lastedited: - predicates.Add(SearchWideDateTime.WideSearchDateTimeGet(model,i,SearchWideDateTime.WideSearchDateTimeGetType.LastEdited)); - break; - case SearchViewModel.SearchInTypes.addtodatabase: - predicates.Add(SearchWideDateTime.WideSearchDateTimeGet(model,i,SearchWideDateTime.WideSearchDateTimeGetType.AddToDatabase)); - break; - case SearchViewModel.SearchInTypes.datetime: - predicates.Add(SearchWideDateTime.WideSearchDateTimeGet(model,i,SearchWideDateTime.WideSearchDateTimeGetType.DateTime)); - break; - case SearchViewModel.SearchInTypes.colorclass: - if ( Enum.TryParse( - model.SearchFor[i].ToLowerInvariant(), out var castColorClass) ) - { - predicates.Add(x => x.ColorClass == castColorClass); - } - break; - default: - var tags = model.SearchFor[i]; - predicates.Add(x => x.Tags!.ToLower().Contains(tags)); - break; - } - // Need to have the type registered in FileIndexPropList - } - - _logger.LogInformation($"search --> {model.SearchQuery}"); - - var predicate = PredicateExecution(predicates, model); - - model.FileIndexItems = await sourceList.Where(predicate).ToListAsync(); - - return model; - } - private static Expression> PredicateExecution( - IReadOnlyList>> predicates, - SearchViewModel model) - { - var predicate = PredicateBuilder.False(); - for ( var i = 0; i < predicates.Count; i++ ) - { - if ( i == 0 ) - { - predicate = predicates[i]; - continue; - } - - var item = predicates[i - 1]; - var item2 = predicates[i]; - - // Search for OR - if ( !model.SearchOperatorContinue(i, model.SearchIn.Count) ) - { - predicate = item.Or(item2); - continue; - } - - predicate = item.AndAlso(item2); - } - - return predicate; - } - - /// + var description = model.SearchFor[i]; + predicates.Add(x => x.Description!.ToLower().Contains(description)); + break; + case SearchViewModel.SearchInTypes.filename: + var filename = model.SearchFor[i]; + predicates.Add(x => x.FileName!.ToLower().Contains(filename)); + break; + case SearchViewModel.SearchInTypes.filepath: + var filePath = model.SearchFor[i]; + predicates.Add(x => x.FilePath!.ToLower().Contains(filePath)); + break; + case SearchViewModel.SearchInTypes.parentdirectory: + var parentDirectory = model.SearchFor[i]; + predicates.Add(x => x.ParentDirectory!.ToLower().Contains(parentDirectory)); + break; + case SearchViewModel.SearchInTypes.title: + var title = model.SearchFor[i]; + predicates.Add(x => x.Title!.ToLower().Contains(title)); + break; + case SearchViewModel.SearchInTypes.make: + // is in the database one field => will be filtered in narrowSearch + var make = model.SearchFor[i]; + predicates.Add(x => x.MakeModel!.ToLower().Contains(make)); + break; + case SearchViewModel.SearchInTypes.model: + // is in the database one field => will be filtered in narrowSearch + var modelMake = model.SearchFor[i]; + predicates.Add(x => x.MakeModel!.ToLower().Contains(modelMake)); + break; + case SearchViewModel.SearchInTypes.filehash: + var fileHash = model.SearchFor[i]; + predicates.Add(x => x.FileHash != null && x.FileHash.ToLower().Contains(fileHash)); + break; + case SearchViewModel.SearchInTypes.software: + var software = model.SearchFor[i]; + predicates.Add(x => x.Software!.ToLower().Contains(software)); + break; + case SearchViewModel.SearchInTypes.isdirectory: + if ( bool.TryParse(model.SearchFor[i].ToLowerInvariant(), + out var boolIsDirectory) ) + { + predicates.Add(x => x.IsDirectory == boolIsDirectory); + model.SearchFor[i] = boolIsDirectory.ToString(); + } + break; + case SearchViewModel.SearchInTypes.lastedited: + predicates.Add(SearchWideDateTime.WideSearchDateTimeGet(model, i, SearchWideDateTime.WideSearchDateTimeGetType.LastEdited)); + break; + case SearchViewModel.SearchInTypes.addtodatabase: + predicates.Add(SearchWideDateTime.WideSearchDateTimeGet(model, i, SearchWideDateTime.WideSearchDateTimeGetType.AddToDatabase)); + break; + case SearchViewModel.SearchInTypes.datetime: + predicates.Add(SearchWideDateTime.WideSearchDateTimeGet(model, i, SearchWideDateTime.WideSearchDateTimeGetType.DateTime)); + break; + case SearchViewModel.SearchInTypes.colorclass: + if ( Enum.TryParse( + model.SearchFor[i].ToLowerInvariant(), out var castColorClass) ) + { + predicates.Add(x => x.ColorClass == castColorClass); + } + break; + default: + var tags = model.SearchFor[i]; + predicates.Add(x => x.Tags!.ToLower().Contains(tags)); + break; + } + // Need to have the type registered in FileIndexPropList + } + + _logger.LogInformation($"search --> {model.SearchQuery}"); + + var predicate = PredicateExecution(predicates, model); + + model.FileIndexItems = await sourceList.Where(predicate).ToListAsync(); + + return model; + } + private static Expression> PredicateExecution( + IReadOnlyList>> predicates, + SearchViewModel model) + { + var predicate = PredicateBuilder.False(); + for ( var i = 0; i < predicates.Count; i++ ) + { + if ( i == 0 ) + { + predicate = predicates[i]; + continue; + } + + var item = predicates[i - 1]; + var item2 = predicates[i]; + + // Search for OR + if ( !model.SearchOperatorContinue(i, model.SearchIn.Count) ) + { + predicate = item.Or(item2); + continue; + } + + predicate = item.AndAlso(item2); + } + + return predicate; + } + + /// /// Store the query during search /// - private string _defaultQuery = string.Empty; - - /// - /// The orginal user search query - /// - private string _orginalSearchQuery = string.Empty; - - /// - /// Parse search query for -Tags and default search queries e.g. "test" - /// - /// Search model - /// filled fields in model - public SearchViewModel MatchSearch(SearchViewModel model) - { - // return nulls to avoid errors - if ( string.IsNullOrWhiteSpace(model.SearchQuery) ) - { - return model; - } - - _defaultQuery = model.SearchQuery; - - // Need to have the type registered in FileIndexPropList - - foreach (var itemName in FileIndexItem.FileIndexPropList()) - { - SearchItemName(model, itemName); - } - + private string _defaultQuery = string.Empty; + + /// + /// The orginal user search query + /// + private string _orginalSearchQuery = string.Empty; + + /// + /// Parse search query for -Tags and default search queries e.g. "test" + /// + /// Search model + /// filled fields in model + public SearchViewModel MatchSearch(SearchViewModel model) + { + // return nulls to avoid errors + if ( string.IsNullOrWhiteSpace(model.SearchQuery) ) + { + return model; + } + + _defaultQuery = model.SearchQuery; + + // Need to have the type registered in FileIndexPropList + + foreach ( var itemName in FileIndexItem.FileIndexPropList() ) + { + SearchItemName(model, itemName); + } + // handle keywords without for example -Tags, or -DateTime prefix - model.ParseDefaultOption(_defaultQuery); - - model.SearchQuery = _orginalSearchQuery; - return model; - } - - /// - /// Search for e.g. -Tags:"test" - /// - /// Model - /// e.g. Tags or Description - private void SearchItemName(SearchViewModel model, string itemName) - { - if ( model.SearchQuery == null ) return; - - // ignore double quotes - model.SearchQuery = model.SearchQuery.Replace("\"\"", "\""); - - // Escape special quotes - model.SearchQuery = Regex.Replace(model.SearchQuery, "[“”‘’]", "\"", - RegexOptions.None, TimeSpan.FromMilliseconds(100)); - - // new: unescaped - // (:|=|;|>|<|-)((["'])(\\?.)*?\3|[\w\!\~\-_\.\/:,;]+)( \|\|| \&\&)? - Regex inurlRegex = new Regex( - "-" + itemName + - "(:|=|;|>|<|-)(([\"\'])(\\\\?.)*?\\3|[\\w\\!\\~\\-_\\.\\/:,;]+)( \\|\\|| \\&\\&)?", - RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); - - _defaultQuery = inurlRegex.Replace(_defaultQuery,""); - // the current query is removed from the list, so the next item will not search on it - - var regexInUrlMatches = inurlRegex.Matches(model.SearchQuery); - if(regexInUrlMatches.Count == 0) return; - - foreach (var regexInUrlValue in regexInUrlMatches.Select(p => p.Value)) - { - var itemQuery = regexInUrlValue; - - // ignore fake results - if ( string.IsNullOrEmpty(itemQuery) ) continue; - - // put ||&& in operator field => next regex > removed - var itemQueryWithOperator = itemQuery; - - Regex rgx = new Regex("-"+ itemName +"(:|=|;|>|<|-)", - RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); - - // To Search Type - var itemNameSearch = rgx.Match(itemQuery).Value; - if (itemNameSearch.Length == 0 ) continue; - - // replace - itemQuery = rgx.Replace(itemQuery, string.Empty); - - // Option last of itemNameSearch (Get last option = ^1) - var searchForOption = itemNameSearch[^1].ToString(); - - // Remove parenthesis - itemQuery = itemQuery.Replace("\"", string.Empty); - itemQuery = itemQuery.Replace("'", string.Empty); - - // Remove || / && at the end of the string - // (\|\||\&\&)$ - const string pattern = "(\\|\\||\\&\\&)$"; - itemQuery = Regex.Replace(itemQuery, pattern, string.Empty, + model.ParseDefaultOption(_defaultQuery); + + model.SearchQuery = _orginalSearchQuery; + return model; + } + + /// + /// Search for e.g. -Tags:"test" + /// + /// Model + /// e.g. Tags or Description + private void SearchItemName(SearchViewModel model, string itemName) + { + if ( model.SearchQuery == null ) return; + + // ignore double quotes + model.SearchQuery = model.SearchQuery.Replace("\"\"", "\""); + + // Escape special quotes + model.SearchQuery = Regex.Replace(model.SearchQuery, "[“”‘’]", "\"", + RegexOptions.None, TimeSpan.FromMilliseconds(100)); + + // new: unescaped + // (:|=|;|>|<|-)((["'])(\\?.)*?\3|[\w\!\~\-_\.\/:,;]+)( \|\|| \&\&)? + Regex inurlRegex = new Regex( + "-" + itemName + + "(:|=|;|>|<|-)(([\"\'])(\\\\?.)*?\\3|[\\w\\!\\~\\-_\\.\\/:,;]+)( \\|\\|| \\&\\&)?", + RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); + + _defaultQuery = inurlRegex.Replace(_defaultQuery, ""); + // the current query is removed from the list, so the next item will not search on it + + var regexInUrlMatches = inurlRegex.Matches(model.SearchQuery); + if ( regexInUrlMatches.Count == 0 ) return; + + foreach ( var regexInUrlValue in regexInUrlMatches.Select(p => p.Value) ) + { + var itemQuery = regexInUrlValue; + + // ignore fake results + if ( string.IsNullOrEmpty(itemQuery) ) continue; + + // put ||&& in operator field => next regex > removed + var itemQueryWithOperator = itemQuery; + + Regex rgx = new Regex("-" + itemName + "(:|=|;|>|<|-)", + RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); + + // To Search Type + var itemNameSearch = rgx.Match(itemQuery).Value; + if ( itemNameSearch.Length == 0 ) continue; + + // replace + itemQuery = rgx.Replace(itemQuery, string.Empty); + + // Option last of itemNameSearch (Get last option = ^1) + var searchForOption = itemNameSearch[^1].ToString(); + + // Remove parenthesis + itemQuery = itemQuery.Replace("\"", string.Empty); + itemQuery = itemQuery.Replace("'", string.Empty); + + // Remove || / && at the end of the string + // (\|\||\&\&)$ + const string pattern = "(\\|\\||\\&\\&)$"; + itemQuery = Regex.Replace(itemQuery, pattern, string.Empty, RegexOptions.None, TimeSpan.FromMilliseconds(100)); - + // is | or & var andOrChar = SearchViewModel.AndOrRegex(itemQueryWithOperator); @@ -446,76 +445,76 @@ private void SearchItemName(SearchViewModel model, string itemName) model.SetAddSearchFor(itemSingleQuery.Trim()); model.SetAddSearchInStringType(itemName); } - } - } - - /// - /// Trim value (remove spaces) - /// - /// searchQuery - /// trimmed value - public static string QuerySafe(string query) - { - query = query.Trim(); - return query; - } - - /// - /// Allow -inurl shortcut - /// - /// search Query - /// replaced Url - public static string QueryShortcuts(string query) - { - // should be ignoring case - query = Regex.Replace(query, "-inurl", "-FilePath", - RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); - return query; - } - - /// - /// The amount of search results on one single page - /// Including the trash page - /// - private const int NumberOfResultsInView = 120; - - /// - /// Get the last page number - /// Roundup by NumberOfResultsInView - /// - /// number of search results - /// last page number (0=index) - private static int GetLastPageNumber(int fileIndexQueryCount) - { - var searchLastPageNumbers = (RoundUp(fileIndexQueryCount) / NumberOfResultsInView) - 1; - - if (fileIndexQueryCount <= NumberOfResultsInView) - { - searchLastPageNumbers = 0; - } - return searchLastPageNumbers; - } - - /// - /// Roundup - /// - /// to round e.g. 10 - /// roundup value - public static int RoundUp(int toRound) - { - // 10 => ResultsInView - if (toRound % NumberOfResultsInView == 0) return toRound; - return (NumberOfResultsInView - toRound % NumberOfResultsInView) + toRound; - } - - /// - /// round number down - /// - /// to round - /// round down value - public static int RoundDown(int toRound) - { - return toRound - toRound % 10; - } - } + } + } + + /// + /// Trim value (remove spaces) + /// + /// searchQuery + /// trimmed value + public static string QuerySafe(string query) + { + query = query.Trim(); + return query; + } + + /// + /// Allow -inurl shortcut + /// + /// search Query + /// replaced Url + public static string QueryShortcuts(string query) + { + // should be ignoring case + query = Regex.Replace(query, "-inurl", "-FilePath", + RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); + return query; + } + + /// + /// The amount of search results on one single page + /// Including the trash page + /// + private const int NumberOfResultsInView = 120; + + /// + /// Get the last page number + /// Roundup by NumberOfResultsInView + /// + /// number of search results + /// last page number (0=index) + private static int GetLastPageNumber(int fileIndexQueryCount) + { + var searchLastPageNumbers = ( RoundUp(fileIndexQueryCount) / NumberOfResultsInView ) - 1; + + if ( fileIndexQueryCount <= NumberOfResultsInView ) + { + searchLastPageNumbers = 0; + } + return searchLastPageNumbers; + } + + /// + /// Roundup + /// + /// to round e.g. 10 + /// roundup value + public static int RoundUp(int toRound) + { + // 10 => ResultsInView + if ( toRound % NumberOfResultsInView == 0 ) return toRound; + return ( NumberOfResultsInView - toRound % NumberOfResultsInView ) + toRound; + } + + /// + /// round number down + /// + /// to round + /// round down value + public static int RoundDown(int toRound) + { + return toRound - toRound % 10; + } + } } diff --git a/starsky/starsky.feature.search/Services/SearchSuggestionsInflateHostedService.cs b/starsky/starsky.feature.search/Services/SearchSuggestionsInflateHostedService.cs index 3e8a1f68aa..f43d080c12 100644 --- a/starsky/starsky.feature.search/Services/SearchSuggestionsInflateHostedService.cs +++ b/starsky/starsky.feature.search/Services/SearchSuggestionsInflateHostedService.cs @@ -29,7 +29,7 @@ public SearchSuggestionsInflateHostedService(IServiceScopeFactory scopeFactory, public async Task StartAsync(CancellationToken cancellationToken) { - using (var scope = _scopeFactory.CreateScope()) + using ( var scope = _scopeFactory.CreateScope() ) { var dbContext = scope.ServiceProvider.GetRequiredService(); await new SearchSuggestionsService(dbContext, _memoryCache, _logger, _appSettings).Inflate(); diff --git a/starsky/starsky.feature.search/Services/SearchSuggestionsService.cs b/starsky/starsky.feature.search/Services/SearchSuggestionsService.cs index 4826070797..d4f4f38550 100644 --- a/starsky/starsky.feature.search/Services/SearchSuggestionsService.cs +++ b/starsky/starsky.feature.search/Services/SearchSuggestionsService.cs @@ -14,7 +14,7 @@ namespace starsky.feature.search.Services { - + [Service(typeof(ISearchSuggest), InjectionLifetime = InjectionLifetime.Scoped)] public class SearchSuggestionsService : ISearchSuggest { @@ -24,9 +24,9 @@ public class SearchSuggestionsService : ISearchSuggest private readonly IWebLogger _logger; public SearchSuggestionsService( - ApplicationDbContext context, + ApplicationDbContext context, IMemoryCache? memoryCache, - IWebLogger logger, + IWebLogger logger, AppSettings appSettings) { _context = context; @@ -45,23 +45,23 @@ public SearchSuggestionsService( [SuppressMessage("Performance", "CA1827:Do not use Count() or LongCount() when Any() can be used")] [SuppressMessage("Performance", "S1155:Do not use Count() or LongCount() when Any() can be used", Justification = "ANY is not supported by EF Core")] - public async Task>> Inflate() + public async Task>> Inflate() { - if ( _cache == null) return new List>(); - - if (_cache.TryGetValue(nameof(SearchSuggestionsService), out _)) - return new Dictionary().ToList(); + if ( _cache == null ) return new List>(); + + if ( _cache.TryGetValue(nameof(SearchSuggestionsService), out _) ) + return new Dictionary().ToList(); var allFilesList = new List>(); try { allFilesList = await _context.FileIndex - .Where(p => !string.IsNullOrEmpty(p.Tags) ) + .Where(p => !string.IsNullOrEmpty(p.Tags)) .GroupBy(i => i.Tags) // ReSharper disable once UseMethodAny.1 .Where(x => x.Count() >= 1) // .ANY is not supported by EF Core .TagWith("Inflate SearchSuggestionsService") - .Select(val => + .Select(val => new KeyValuePair(val.Key!, val.Count())).ToListAsync(); } catch ( Exception exception ) @@ -73,12 +73,12 @@ public async Task>> Inflate() return allFilesList; } - var suggestions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var suggestions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach ( var tag in allFilesList ) { if ( string.IsNullOrEmpty(tag.Key) ) continue; - + var keywordsHashSet = HashSetHelper.StringToHashSet(tag.Key.Trim()); foreach ( var keyword in keywordsHashSet ) @@ -89,20 +89,20 @@ public async Task>> Inflate() } else { - suggestions.Add(keyword,tag.Value); + suggestions.Add(keyword, tag.Value); } } } - + var suggestionsFiltered = suggestions .Where(p => p.Value >= 10) .OrderByDescending(p => p.Value) .ToList(); - var cacheExpire = suggestionsFiltered.Count != 0 ? - new TimeSpan(120,0,0) : new TimeSpan(0, 1, 0); - - _cache.Set(nameof(SearchSuggestionsService), suggestionsFiltered, + var cacheExpire = suggestionsFiltered.Count != 0 ? + new TimeSpan(120, 0, 0) : new TimeSpan(0, 1, 0); + + _cache.Set(nameof(SearchSuggestionsService), suggestionsFiltered, cacheExpire); return suggestionsFiltered; @@ -114,15 +114,15 @@ public async Task>> Inflate() /// Key/Value pared list public async Task>> GetAllSuggestions() { - if( _cache == null || _appSettings.AddMemoryCache == false) - return new Dictionary(); + if ( _cache == null || _appSettings.AddMemoryCache == false ) + return new Dictionary(); if ( _cache.TryGetValue(nameof(SearchSuggestionsService), - out var objectFileFolders) ) + out var objectFileFolders) ) { - return objectFileFolders as List> ?? new List>(); + return objectFileFolders as List> ?? new List>(); } - + return await Inflate(); } @@ -134,20 +134,20 @@ public async Task>> GetAllSuggestions() public async Task> SearchSuggest(string query) { if ( string.IsNullOrEmpty(query) ) return new List(); - if( _cache == null || _appSettings.AddMemoryCache == false) return new List(); - + if ( _cache == null || _appSettings.AddMemoryCache == false ) return new List(); + var allSuggestions = await GetAllSuggestions(); - - var results = allSuggestions.Where(p => + + var results = allSuggestions.Where(p => p.Key.StartsWith(query, StringComparison.InvariantCultureIgnoreCase)) .Take(MaxResult) .OrderByDescending(p => p.Value).Select(p => p.Key) .ToList(); - + results.AddRange(SystemResults() .Where(p => p.StartsWith(query, StringComparison.InvariantCultureIgnoreCase)) - .Take(MaxResult) ); - + .Take(MaxResult)); + return results; } diff --git a/starsky/starsky.feature.search/Services/SearchWideDateTime.cs b/starsky/starsky.feature.search/Services/SearchWideDateTime.cs index 8e0e0bd9f6..9a90e0beec 100644 --- a/starsky/starsky.feature.search/Services/SearchWideDateTime.cs +++ b/starsky/starsky.feature.search/Services/SearchWideDateTime.cs @@ -8,17 +8,16 @@ namespace starsky.feature.search.Services { public class SearchWideDateTime { - /// - /// Query for DateTime: in between values, entire days, from, type of queries - /// - /// Query Source - /// output - /// number of search query (i) - /// - public static Expression> WideSearchDateTimeGet(SearchViewModel model, int indexer, WideSearchDateTimeGetType type) - { - SearchForEntireDay(model,indexer); + /// Query for DateTime: in between values, entire days, from, type of queries + /// + /// output + /// number of search query (i) + /// + public static Expression> WideSearchDateTimeGet( + SearchViewModel model, int indexer, WideSearchDateTimeGetType type) + { + SearchForEntireDay(model, indexer); // faster search for searching within // how ever this is still triggered multiple times @@ -31,26 +30,28 @@ public static Expression> WideSearchDateTimeGet(SearchV { var beforeDateTime = SearchViewModel.ParseDateTime(model.SearchFor[beforeIndexSearchForOptions]); - + var afterDateTime = SearchViewModel.ParseDateTime(model.SearchFor[afterIndexSearchForOptions]); // We have now an extra query, and this is always AND model.SetAndOrOperator('&', -2); - + switch ( type ) { case WideSearchDateTimeGetType.DateTime: - return (p => p.DateTime >= beforeDateTime && p.DateTime <= afterDateTime); + return ( p => p.DateTime >= beforeDateTime && p.DateTime <= afterDateTime ); case WideSearchDateTimeGetType.LastEdited: - return (p => p.LastEdited >= beforeDateTime && p.LastEdited <= afterDateTime); + return ( p => + p.LastEdited >= beforeDateTime && p.LastEdited <= afterDateTime ); case WideSearchDateTimeGetType.AddToDatabase: - return (p => p.AddToDatabase >= beforeDateTime && p.AddToDatabase <= afterDateTime); + return ( p => + p.AddToDatabase >= beforeDateTime && p.AddToDatabase <= afterDateTime ); default: throw new ArgumentException("enum incomplete", nameof(type)); } } - + var dateTime = SearchViewModel.ParseDateTime(model.SearchFor[indexer]); // Normal search @@ -61,11 +62,11 @@ public static Expression> WideSearchDateTimeGet(SearchV switch ( type ) { case WideSearchDateTimeGetType.DateTime: - return (p => p.DateTime <= dateTime); + return ( p => p.DateTime <= dateTime ); case WideSearchDateTimeGetType.LastEdited: - return (p => p.LastEdited <= dateTime); + return ( p => p.LastEdited <= dateTime ); case WideSearchDateTimeGetType.AddToDatabase: - return (p => p.AddToDatabase <= dateTime); + return ( p => p.AddToDatabase <= dateTime ); default: throw new ArgumentNullException(nameof(type)); } @@ -73,11 +74,11 @@ public static Expression> WideSearchDateTimeGet(SearchV switch ( type ) { case WideSearchDateTimeGetType.DateTime: - return (p => p.DateTime >= dateTime); + return ( p => p.DateTime >= dateTime ); case WideSearchDateTimeGetType.LastEdited: - return (p => p.LastEdited >= dateTime); + return ( p => p.LastEdited >= dateTime ); case WideSearchDateTimeGetType.AddToDatabase: - return (p => p.AddToDatabase >= dateTime); + return ( p => p.AddToDatabase >= dateTime ); default: throw new ArgumentNullException(nameof(type)); } @@ -85,17 +86,17 @@ public static Expression> WideSearchDateTimeGet(SearchV switch ( type ) { case WideSearchDateTimeGetType.DateTime: - return (p => p.DateTime == dateTime); + return ( p => p.DateTime == dateTime ); case WideSearchDateTimeGetType.LastEdited: - return (p => p.LastEdited == dateTime); + return ( p => p.LastEdited == dateTime ); case WideSearchDateTimeGetType.AddToDatabase: - return (p => p.AddToDatabase == dateTime); + return ( p => p.AddToDatabase == dateTime ); default: throw new ArgumentNullException(nameof(type)); } } - } - + } + /// /// Convert 1 to today /// @@ -104,15 +105,15 @@ public static Expression> WideSearchDateTimeGet(SearchV private static void SearchForEntireDay(SearchViewModel model, int indexer) { var dateTime = SearchViewModel.ParseDateTime(model.SearchFor[indexer]); - + model.SearchFor[indexer] = dateTime.ToString("dd-MM-yyyy HH:mm:ss", CultureInfo.InvariantCulture); - + // Searching for entire day if ( model.SearchForOptions[indexer] != SearchViewModel.SearchForOptionType.Equal || - dateTime.Hour != 0 || dateTime.Minute != 0 || dateTime.Second != 0 || - dateTime.Millisecond != 0 ) return; - + dateTime.Hour != 0 || dateTime.Minute != 0 || dateTime.Second != 0 || + dateTime.Millisecond != 0 ) return; + model.SearchForOptions[indexer] = SearchViewModel.SearchForOptionType.GreaterThen; model.SearchForOptions.Add(SearchViewModel.SearchForOptionType.LessThen); @@ -122,7 +123,7 @@ private static void SearchForEntireDay(SearchViewModel model, int indexer) model.SearchFor.Add(add24Hours); model.SearchIn.Add("DateTime"); } - + /// /// Static binded types that are supported /// diff --git a/starsky/starsky.feature.search/ViewModels/SearchViewModel.cs b/starsky/starsky.feature.search/ViewModels/SearchViewModel.cs index e5bb5ac66e..33c746b873 100644 --- a/starsky/starsky.feature.search/ViewModels/SearchViewModel.cs +++ b/starsky/starsky.feature.search/ViewModels/SearchViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; @@ -34,12 +34,12 @@ public SearchViewModel() /// Private field: Used to know how old the search query is /// private readonly DateTime _dateTime; - + /// /// Items on the page /// public List? FileIndexItems { get; set; } - + /// /// Full location specification /// @@ -47,18 +47,18 @@ public SearchViewModel() // ReSharper disable once CollectionNeverQueried.Global // ReSharper disable once PropertyCanBeMadeInitOnly.Global public List Breadcrumb { get; set; } - + /// /// Where to search for /// public string? SearchQuery { get; set; } = string.Empty; - + /// /// Current page number (index=0) /// // ReSharper disable once UnusedAutoPropertyAccessor.Global public int PageNumber { get; set; } - + /// /// The last page (index=0) /// @@ -120,28 +120,28 @@ public void SetAddSearchInStringType(string value) { // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract SearchIn ??= new List(); - + // use ctor to have an empty list var fileIndexPropList = FileIndexItem.FileIndexPropList(); var fileIndexPropListIndex = fileIndexPropList.FindIndex (x => x.Equals(value, StringComparison.OrdinalIgnoreCase)); - if (fileIndexPropListIndex != -1 ) + if ( fileIndexPropListIndex != -1 ) { SearchIn.Add(fileIndexPropList[fileIndexPropListIndex]); - } + } } - - + + /// /// Private field: Search for the following value in using SearchFor inside: _searchIn /// internal List? SearchForInternal = new(); - + /// /// The values to search for, to know which field use the same indexer in _searchIn /// public List SearchFor - { + { // don't change it to 'SearchFor => _searchFor' get { @@ -158,7 +158,7 @@ public void SetAddSearchFor(string value) SearchForInternal ??= new List(); SearchForInternal.Add(value.Trim().ToLowerInvariant()); } - + /// /// The search for types /// @@ -171,26 +171,26 @@ public enum SearchForOptionType /// [Display(Name = ">")] // in json it is GreaterThen GreaterThen, - + /// /// < /// [Display(Name = "<")] LessThen, - + /// /// = /// [Display(Name = "=")] Equal, - + /// /// - /// [Display(Name = "!-")] Not } - + /// /// Private field: Search Options >, <,=. (greater than sign, less than sign, equal sign) /// to know which field use the same indexer in _searchIn or _searchFor @@ -202,7 +202,7 @@ public enum SearchForOptionType /// to know which field use the same indexer in _searchIn or _searchFor /// public List SearchForOptions - { + { get { return SearchForOptionsInternal ?? new List(); @@ -244,13 +244,14 @@ public SearchForOptionType SetAddSearchForOptions(string value) /// /// The type of page returns, (Search or Trash) /// - public string PageType { + public string PageType + { get { - if (string.IsNullOrEmpty(SearchQuery) ) return PageViewType.PageType.Search.ToString(); - return SearchQuery == TrashKeyword.TrashKeywordString ? PageViewType.PageType.Trash.ToString() + if ( string.IsNullOrEmpty(SearchQuery) ) return PageViewType.PageType.Search.ToString(); + return SearchQuery == TrashKeyword.TrashKeywordString ? PageViewType.PageType.Trash.ToString() : PageViewType.PageType.Search.ToString(); - } + } } /// @@ -269,14 +270,14 @@ public double ElapsedSeconds _elapsedSeconds = value - value % 0.001; } } - + /// /// Used to know how old the search query is /// Used to know if a page is cached /// [SuppressMessage("Usage", "S6561: Avoid using DateTime.Now " + - "for benchmarking or timespan calculation operations.")] - public double Offset => Math.Round(Math.Abs((DateTime.UtcNow - _dateTime).TotalSeconds), 2); + "for benchmarking or timespan calculation operations.")] + public double Offset => Math.Round(Math.Abs(( DateTime.UtcNow - _dateTime ).TotalSeconds), 2); /// @@ -300,32 +301,32 @@ public void SetAndOrOperator(char andOrChar, int relativeLocation = 0) andOrBool = false; } - if (SearchOperatorOptionsInternal.Count == 0 && andOrChar == '|') + if ( SearchOperatorOptionsInternal.Count == 0 && andOrChar == '|' ) { SearchOperatorOptionsInternal.Add(false); } - + // Store item on a different location in the List if ( relativeLocation == 0 ) { SearchOperatorOptionsInternal.Add(andOrBool); } - else if ( SearchOperatorOptionsInternal.Count+relativeLocation <= -1 ) + else if ( SearchOperatorOptionsInternal.Count + relativeLocation <= -1 ) { SearchOperatorOptionsInternal.Insert(0, andOrBool); } else { - SearchOperatorOptionsInternal.Insert(SearchOperatorOptionsInternal.Count+relativeLocation,andOrBool); + SearchOperatorOptionsInternal.Insert(SearchOperatorOptionsInternal.Count + relativeLocation, andOrBool); } - + } - + /// /// Search Operator, eg. || && /// public List SearchOperatorOptions - { + { get { return SearchOperatorOptionsInternal ?? new List(); @@ -342,13 +343,13 @@ public List SearchOperatorOptions public bool SearchOperatorContinue(int indexer, int max) { if ( SearchOperatorOptionsInternal == null ) return true; - if ( indexer <= -1 || indexer > max) return true; + if ( indexer <= -1 || indexer > max ) return true; // for -Datetime=1 (03-03-2019 00:00:00-03-03-2019 23:59:59), this are two queries >= fail!! - if (indexer >= SearchOperatorOptionsInternal.Count ) return true; // used when general words without update + if ( indexer >= SearchOperatorOptionsInternal.Count ) return true; // used when general words without update var returnResult = SearchOperatorOptionsInternal[indexer]; return returnResult; } - + /// /// ||[OR] = |, else = &, default = string.Emphy /// @@ -362,23 +363,23 @@ public static char AndOrRegex(string item) // To Search Type var lastStringValue = rgx.Match(item).Value; - + // set default if ( string.IsNullOrEmpty(lastStringValue) ) lastStringValue = string.Empty; - + if ( lastStringValue == "||" ) return '|'; return '&'; } - + /// /// Copy the current object in memory /// /// public SearchViewModel Clone() { - return (SearchViewModel) MemberwiseClone(); + return ( SearchViewModel )MemberwiseClone(); } - + /// /// For reparsing keywords to -Tags:"keyword" /// handle keywords without for example -Tags, or -DateTime prefix @@ -393,7 +394,7 @@ public string ParseDefaultOption(string defaultQuery) // fallback situation // search on for example: '%' - if ( SearchFor.Count == 0 ) + if ( SearchFor.Count == 0 ) { SetAddSearchFor(defaultQuery); SetAddSearchInStringType("tags"); @@ -405,7 +406,7 @@ public string ParseDefaultOption(string defaultQuery) // // &&|\|\| Regex andOrRegex = new Regex("&&|\\|\\|", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); - + var andOrRegexMatches = andOrRegex.Matches(defaultQuery); foreach ( Match andOrValue in andOrRegexMatches ) @@ -421,26 +422,26 @@ public string ParseDefaultOption(string defaultQuery) SetAndOrOperator(AndOrRegex("&&")); } } - + return returnQueryBuilder.ToString(); } - private (string defaultQuery, StringBuilder returnQueryBuilder) + private (string defaultQuery, StringBuilder returnQueryBuilder) ParseQuotedValues(string defaultQuery, StringBuilder returnQueryBuilder) { // Get Quoted values // (["'])(\\?.)*?\1 - + // Quoted or words // [\w!]+|(["'])(\\?.)*?\1 - + Regex inUrlRegex = new Regex("[\\w!]+|([\"\'])(\\\\?.)*?\\1", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); // Escape special quotes - defaultQuery = Regex.Replace(defaultQuery, "[“”‘’]", "\"", + defaultQuery = Regex.Replace(defaultQuery, "[“”‘’]", "\"", RegexOptions.None, TimeSpan.FromMilliseconds(100)); - + var regexInUrlMatches = inUrlRegex.Matches(defaultQuery); foreach ( Match regexInUrl in regexInUrlMatches ) @@ -459,28 +460,28 @@ public string ParseDefaultOption(string defaultQuery) var startIndexer = regexInUrl.Index; var startLength = regexInUrl.Length; - var lastChar = defaultQuery[startIndexer + regexInUrl.Length -1 ]; - + var lastChar = defaultQuery[startIndexer + regexInUrl.Length - 1]; + if ( defaultQuery[regexInUrl.Index] == '"' && - lastChar == '"' || - defaultQuery[regexInUrl.Index] == '\'' && - lastChar == '\'' ) + lastChar == '"' || + defaultQuery[regexInUrl.Index] == '\'' && + lastChar == '\'' ) { startIndexer = regexInUrl.Index + 1; startLength = regexInUrl.Length - 2; } - + // Get Value var searchForQuery = defaultQuery.Substring(startIndexer, startLength); - + returnQueryBuilder.Append($"-Tags:\"{searchForQuery}\" "); - + SetAddSearchFor(searchForQuery); SetAddSearchInStringType("tags"); // Detecting Not Queries (string must be at least 3 chars) - if ( ( regexInUrl.Index - 1 >= 0 && defaultQuery[regexInUrl.Index - 1] == '-' ) - || ( regexInUrl.Index + 2 <= regexInUrl.Length && defaultQuery[regexInUrl.Index + 2] == '-' ) ) + if ( ( regexInUrl.Index - 1 >= 0 && defaultQuery[regexInUrl.Index - 1] == '-' ) + || ( regexInUrl.Index + 2 <= regexInUrl.Length && defaultQuery[regexInUrl.Index + 2] == '-' ) ) { SetAddSearchForOptions("-"); continue; @@ -488,9 +489,9 @@ public string ParseDefaultOption(string defaultQuery) SetAddSearchForOptions("="); } - return ( defaultQuery, returnQueryBuilder ); + return (defaultQuery, returnQueryBuilder); } - + /// /// Filter for WideSearch /// Always after wideSearch @@ -505,7 +506,7 @@ public static SearchViewModel NarrowSearch(SearchViewModel model) for ( var i = 0; i < model.SearchIn.Count; i++ ) { - var propertyStringName = FileIndexItem.FileIndexPropList().Find( p => + var propertyStringName = FileIndexItem.FileIndexPropList().Find(p => string.Equals(p, model.SearchIn[i], StringComparison.InvariantCultureIgnoreCase)); @@ -515,18 +516,18 @@ public static SearchViewModel NarrowSearch(SearchViewModel model) } var property = new FileIndexItem().GetType().GetProperty(propertyStringName)!; - + // skip OR searches if ( !model.SearchOperatorContinue(i, model.SearchIn.Count) ) { continue; } - PropertySearch(model, property, model.SearchFor[i],model.SearchForOptions[i]); + PropertySearch(model, property, model.SearchFor[i], model.SearchForOptions[i]); } - + // hide xmp files in default view - if ( model.SearchIn.TrueForAll(p => !string.Equals(p, nameof(SearchInTypes.imageformat), - StringComparison.InvariantCultureIgnoreCase))) + if ( model.SearchIn.TrueForAll(p => !string.Equals(p, nameof(SearchInTypes.imageformat), + StringComparison.InvariantCultureIgnoreCase)) ) { model.FileIndexItems = model.FileIndexItems! .Where(p => p.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp).ToList(); @@ -540,23 +541,23 @@ internal static SearchViewModel PropertySearchStringType( PropertyInfo property, string searchForQuery, SearchForOptionType searchType) { - switch (searchType) + switch ( searchType ) { case SearchForOptionType.Not: model.FileIndexItems = model.FileIndexItems!.Where( - p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && ! // not - p.GetType().GetProperty(property.Name)!.GetValue(p, null)? - .ToString()?.ToLowerInvariant().Contains(searchForQuery, - StringComparison.InvariantCultureIgnoreCase) == true + p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ! // not + p.GetType().GetProperty(property.Name)!.GetValue(p, null)? + .ToString()?.ToLowerInvariant().Contains(searchForQuery, + StringComparison.InvariantCultureIgnoreCase) == true ).ToList(); break; default: model.FileIndexItems = model.FileIndexItems? - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && p.GetType().GetProperty(property.Name)!.GetValue(p, null)? - .ToString()?.ToLowerInvariant().Contains(searchForQuery, - StringComparison.InvariantCultureIgnoreCase) == true + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && p.GetType().GetProperty(property.Name)!.GetValue(p, null)? + .ToString()?.ToLowerInvariant().Contains(searchForQuery, + StringComparison.InvariantCultureIgnoreCase) == true ).ToList(); break; } @@ -572,40 +573,40 @@ internal static SearchViewModel PropertySearchBoolType( { return new SearchViewModel(); } - - if ( property == null) + + if ( property == null ) { return model; } - + model.FileIndexItems = model.FileIndexItems? - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && (bool?) p.GetType().GetProperty(property.Name)?.GetValue(p, null) == boolIsValue + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ( bool? )p.GetType().GetProperty(property.Name)?.GetValue(p, null) == boolIsValue ).ToList(); return model; } - + private static SearchViewModel PropertySearchImageFormatType( SearchViewModel model, PropertyInfo property, ExtensionRolesHelper.ImageFormat castImageFormat, SearchForOptionType searchType) { - switch (searchType) + switch ( searchType ) { case SearchForOptionType.Not: model.FileIndexItems = model.FileIndexItems! - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && (ExtensionRolesHelper.ImageFormat) p.GetType().GetProperty(property.Name)? - .GetValue(p, null)! - != // not - castImageFormat + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ( ExtensionRolesHelper.ImageFormat )p.GetType().GetProperty(property.Name)? + .GetValue(p, null)! + != // not + castImageFormat ).ToList(); break; default: model.FileIndexItems = model.FileIndexItems! - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && (ExtensionRolesHelper.ImageFormat) p.GetType().GetProperty(property.Name)? - .GetValue(p, null)! == castImageFormat + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ( ExtensionRolesHelper.ImageFormat )p.GetType().GetProperty(property.Name)? + .GetValue(p, null)! == castImageFormat ).ToList(); break; } @@ -619,32 +620,32 @@ private static SearchViewModel PropertySearchDateTimeType( SearchForOptionType searchType) { var parsedDateTime = ParseDateTime(searchForQuery); - - switch (searchType) + + switch ( searchType ) { case SearchForOptionType.LessThen: model.FileIndexItems = model.FileIndexItems! - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && (DateTime) p.GetType().GetProperty(property.Name)?.GetValue(p, null)! - <= parsedDateTime + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ( DateTime )p.GetType().GetProperty(property.Name)?.GetValue(p, null)! + <= parsedDateTime ).ToList(); break; case SearchForOptionType.GreaterThen: model.FileIndexItems = model.FileIndexItems! - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && (DateTime) p.GetType().GetProperty(property.Name)?.GetValue(p, null)! - >= parsedDateTime + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ( DateTime )p.GetType().GetProperty(property.Name)?.GetValue(p, null)! + >= parsedDateTime ).ToList(); break; default: model.FileIndexItems = model.FileIndexItems! - .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name - && (DateTime) p.GetType().GetProperty(property.Name)?.GetValue(p, null)! - == parsedDateTime + .Where(p => p.GetType().GetProperty(property.Name)?.Name == property.Name + && ( DateTime )p.GetType().GetProperty(property.Name)?.GetValue(p, null)! + == parsedDateTime ).ToList(); break; } - + return model; } @@ -656,7 +657,7 @@ private static SearchViewModel PropertySearchDateTimeType( /// the query to search for (always string) /// greater then, equal /// search values - internal static SearchViewModel PropertySearch(SearchViewModel model, + internal static SearchViewModel PropertySearch(SearchViewModel model, PropertyInfo property, string searchForQuery, SearchForOptionType searchType) { @@ -665,27 +666,27 @@ internal static SearchViewModel PropertySearch(SearchViewModel model, return PropertySearchStringType(model, property, searchForQuery, searchType); } - if ( (property.PropertyType == typeof(bool) || property.PropertyType == typeof(bool?)) && - bool.TryParse(searchForQuery, out var boolIsValue)) + if ( ( property.PropertyType == typeof(bool) || property.PropertyType == typeof(bool?) ) && + bool.TryParse(searchForQuery, out var boolIsValue) ) { return PropertySearchBoolType(model, property, boolIsValue); } - - if ( property.PropertyType == typeof(ExtensionRolesHelper.ImageFormat) && - Enum.TryParse( - searchForQuery.ToLowerInvariant(), out var castImageFormat) ) + + if ( property.PropertyType == typeof(ExtensionRolesHelper.ImageFormat) && + Enum.TryParse( + searchForQuery.ToLowerInvariant(), out var castImageFormat) ) { - return PropertySearchImageFormatType(model, property, castImageFormat, searchType); + return PropertySearchImageFormatType(model, property, castImageFormat, searchType); } - + if ( property.PropertyType == typeof(DateTime) ) { - return PropertySearchDateTimeType(model, property, searchForQuery, searchType); + return PropertySearchDateTimeType(model, property, searchForQuery, searchType); } return model; } - + /// /// Internal API: to parse datetime objects /// @@ -695,38 +696,38 @@ internal static DateTime ParseDateTime(string input) { // For relative values - if ( Regex.IsMatch(input, @"^\d+$", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) && - int.TryParse(input, out var relativeValue) ) + if ( Regex.IsMatch(input, @"^\d+$", + RegexOptions.None, TimeSpan.FromMilliseconds(100)) && + int.TryParse(input, out var relativeValue) ) { - if(relativeValue >= 1) relativeValue *= -1; // always in the past + if ( relativeValue >= 1 ) relativeValue *= -1; // always in the past if ( relativeValue > -60000 ) // 24-11-1854 { return DateTime.Today.AddDays(relativeValue); } } - + var patternLab = new List { "yyyy-MM-dd\\tHH:mm:ss", // < lowercase :) "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd-HH:mm:ss", - "yyyy-MM-dd", - "dd-MM-yyyy", + "yyyy-MM-dd", + "dd-MM-yyyy", "dd-MM-yyyy HH:mm:ss", "dd-MM-yyyy\\tHH:mm:ss", "MM/dd/yyyy HH:mm:ss", // < used by the next string rule 01/30/2018 00:00:00 }; - + DateTime dateTime = DateTime.MinValue; - - foreach (var pattern in patternLab) + + foreach ( var pattern in patternLab ) { - DateTime.TryParseExact(input, - pattern, - CultureInfo.InvariantCulture, + DateTime.TryParseExact(input, + pattern, + CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime); - if(dateTime.Year > 2) return dateTime; + if ( dateTime.Year > 2 ) return dateTime; } return dateTime.Year > 2 ? dateTime : DateTime.Now; } diff --git a/starsky/starsky.feature.search/starsky.feature.search.csproj b/starsky/starsky.feature.search/starsky.feature.search.csproj index 67eafe62da..6ba1351866 100644 --- a/starsky/starsky.feature.search/starsky.feature.search.csproj +++ b/starsky/starsky.feature.search/starsky.feature.search.csproj @@ -7,10 +7,14 @@ enable starsky.feature.search + - - - + + + + + true + diff --git a/starsky/starsky.feature.settings/Services/UpdateAppSettingsByPath.cs b/starsky/starsky.feature.settings/Services/UpdateAppSettingsByPath.cs index e249534447..c642eb38d8 100644 --- a/starsky/starsky.feature.settings/Services/UpdateAppSettingsByPath.cs +++ b/starsky/starsky.feature.settings/Services/UpdateAppSettingsByPath.cs @@ -23,10 +23,11 @@ public UpdateAppSettingsByPath(AppSettings appSettings, ISelectorStorage selecto _hostStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); } - - public async Task UpdateAppSettingsAsync(AppSettingsTransferObject appSettingTransferObject) + + public async Task UpdateAppSettingsAsync( + AppSettingsTransferObject appSettingTransferObject) { - if ( !string.IsNullOrEmpty(appSettingTransferObject.StorageFolder)) + if ( !string.IsNullOrEmpty(appSettingTransferObject.StorageFolder) ) { if ( !_appSettings.StorageFolderAllowEdit ) { @@ -37,7 +38,8 @@ public async Task UpdateAppSettingsAsync(AppSettin "There is an Environment variable set so you can't update it here" }; } - if (!_hostStorage.ExistFolder(appSettingTransferObject.StorageFolder) ) + + if ( !_hostStorage.ExistFolder(appSettingTransferObject.StorageFolder) ) { return new UpdateAppSettingsStatusModel { @@ -47,23 +49,18 @@ public async Task UpdateAppSettingsAsync(AppSettin }; } } - + AppSettingsCompareHelper.Compare(_appSettings, appSettingTransferObject); - var transfer = ( AppSettingsTransferObject ) _appSettings; - + var transfer = ( AppSettingsTransferObject )_appSettings; + // should not forget app: prefix - var jsonOutput = JsonSerializer.Serialize(new { app = transfer }, DefaultJsonSerializer.NoNamingPolicy); + var jsonOutput = JsonSerializer.Serialize(new { app = transfer }, + DefaultJsonSerializer.NoNamingPolicyBoolAsString); await _hostStorage.WriteStreamAsync( StringToStreamHelper.StringToStream(jsonOutput), _appSettings.AppSettingsPath); - - return new UpdateAppSettingsStatusModel - { - StatusCode = 200, - Message = "Updated" - }; - } - + return new UpdateAppSettingsStatusModel { StatusCode = 200, Message = "Updated" }; + } } diff --git a/starsky/starsky.feature.syncbackground/Helpers/OnStartupSync.cs b/starsky/starsky.feature.syncbackground/Helpers/OnStartupSync.cs index e1c73fa57c..181939e14f 100644 --- a/starsky/starsky.feature.syncbackground/Helpers/OnStartupSync.cs +++ b/starsky/starsky.feature.syncbackground/Helpers/OnStartupSync.cs @@ -36,7 +36,7 @@ public class OnStartupSync /// /// /// - public OnStartupSync(IServiceScopeFactory serviceScopeFactory, IDiskWatcherBackgroundTaskQueue backgroundTaskQueue, AppSettings appSettings, + public OnStartupSync(IServiceScopeFactory serviceScopeFactory, IDiskWatcherBackgroundTaskQueue backgroundTaskQueue, AppSettings appSettings, ISynchronize synchronize, ISettingsService settingsService, IWebLogger logger) { _serviceScopeFactory = serviceScopeFactory; @@ -57,22 +57,22 @@ await _backgroundTaskQueue.QueueBackgroundWorkItemAsync(async token => public async Task StartUpSyncTask() { - if ( _appSettings.SyncOnStartup != true ) + if ( _appSettings.SyncOnStartup != true ) { return; } var lastUpdatedValue = await _settingsService.GetSetting( SettingsType.LastSyncBackgroundDateTime); - + await _synchronize.Sync("/", PushToSockets, lastUpdatedValue.ToLocalTime()); await _settingsService.AddOrUpdateSetting( SettingsType.LastSyncBackgroundDateTime, DateTime.UtcNow.ToString(SettingsFormats.LastSyncBackgroundDateTime, CultureInfo.InvariantCulture)); - + _logger.LogInformation("Sync on startup done"); } - + internal async Task PushToSockets(List updatedList) { using var scope = _serviceScopeFactory.CreateScope(); @@ -83,6 +83,6 @@ internal async Task PushToSockets(List updatedList) await webSocketConnectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None); await notificationQuery.AddNotification(webSocketResponse); - } + } } diff --git a/starsky/starsky.feature.syncbackground/Services/OnStartupSyncBackgroundService.cs b/starsky/starsky.feature.syncbackground/Services/OnStartupSyncBackgroundService.cs index ee137f731d..1d17cbf748 100644 --- a/starsky/starsky.feature.syncbackground/Services/OnStartupSyncBackgroundService.cs +++ b/starsky/starsky.feature.syncbackground/Services/OnStartupSyncBackgroundService.cs @@ -7,15 +7,14 @@ using starsky.foundation.injection; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; -using starsky.foundation.realtime.Interfaces; using starsky.foundation.settings.Interfaces; using starsky.foundation.sync.SyncInterfaces; using starsky.foundation.sync.WatcherBackgroundService; [assembly: InternalsVisibleTo("starskytest")] + namespace starsky.feature.syncbackground.Services { - [Service(typeof(IHostedService), InjectionLifetime = InjectionLifetime.Singleton)] public class OnStartupSyncBackgroundService : BackgroundService { @@ -35,13 +34,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var scope = _serviceScopeFactory.CreateScope(); var appSettings = scope.ServiceProvider.GetRequiredService(); - var diskWatcherBackgroundTaskQueue = scope.ServiceProvider.GetRequiredService(); + var diskWatcherBackgroundTaskQueue = scope.ServiceProvider + .GetRequiredService(); var synchronize = scope.ServiceProvider.GetRequiredService(); var settingsService = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService(); - await new OnStartupSync(_serviceScopeFactory, diskWatcherBackgroundTaskQueue, appSettings, synchronize, settingsService, logger).StartUpSync(); - } + await new OnStartupSync(_serviceScopeFactory, diskWatcherBackgroundTaskQueue, + appSettings, synchronize, settingsService, logger).StartUpSync(); + } } } diff --git a/starsky/starsky.feature.syncbackground/starsky.feature.syncbackground.csproj b/starsky/starsky.feature.syncbackground/starsky.feature.syncbackground.csproj index c011803818..2cbf838962 100644 --- a/starsky/starsky.feature.syncbackground/starsky.feature.syncbackground.csproj +++ b/starsky/starsky.feature.syncbackground/starsky.feature.syncbackground.csproj @@ -1,23 +1,23 @@ + + net8.0 + starsky.feature.syncbackground + {15e1493e-6e79-4314-907f-b3ef18eb9046} + 0.6.0-beta.0 + enable + - - net8.0 - starsky.feature.syncbackground - {15e1493e-6e79-4314-907f-b3ef18eb9046} - 0.6.0-beta.0 - enable - + + + - - true - + + + + - - - + + true + - - - - diff --git a/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs b/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs index a3cc3287fa..88a7be0fe5 100644 --- a/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs +++ b/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs @@ -29,8 +29,8 @@ public class DatabaseThumbnailGenerationService : IDatabaseThumbnailGenerationSe private readonly IUpdateStatusGeneratedThumbnailService _updateStatusGeneratedThumbnailService; private readonly IThumbnailQuery _thumbnailQuery; - public DatabaseThumbnailGenerationService(IQuery query, IWebLogger logger, IWebSocketConnectionsService connectionsService, - IThumbnailService thumbnailService, IThumbnailQuery thumbnailQuery, + public DatabaseThumbnailGenerationService(IQuery query, IWebLogger logger, IWebSocketConnectionsService connectionsService, + IThumbnailService thumbnailService, IThumbnailQuery thumbnailQuery, IThumbnailQueuedHostedService bgTaskQueue, IUpdateStatusGeneratedThumbnailService updateStatusGeneratedThumbnailService) { @@ -42,12 +42,12 @@ public DatabaseThumbnailGenerationService(IQuery query, IWebLogger logger, IWebS _bgTaskQueue = bgTaskQueue; _updateStatusGeneratedThumbnailService = updateStatusGeneratedThumbnailService; } - + public async Task StartBackgroundQueue(DateTime endTime) { var thumbnailItems = await _thumbnailQuery.UnprocessedGeneratedThumbnails(); var queryItems = await _query.GetObjectsByFileHashAsync(thumbnailItems.Select(p => p.FileHash).ToList()); - + foreach ( var chuckedItems in thumbnailItems.ChunkyEnumerable(50) ) { // When the CPU is to high its gives a Error 500 @@ -68,22 +68,22 @@ internal async Task> FilterAndWorkThumbnailGeneration { return await WorkThumbnailGeneration(chuckedItems, queryItems); } - + _logger.LogInformation("Cancel job due timeout"); return new List(); } - + internal async Task> WorkThumbnailGeneration( List chuckedItems, List fileIndexItems) { var resultData = new List(); - + foreach ( var item in chuckedItems ) { var fileIndexItem = fileIndexItems.Find(p => p.FileHash == item.FileHash); - if ( fileIndexItem?.FilePath == null || - fileIndexItem.Status != FileIndexItem.ExifStatus.Ok ) + if ( fileIndexItem?.FilePath == null || + fileIndexItem.Status != FileIndexItem.ExifStatus.Ok ) { // when null set to false item.Small ??= false; @@ -92,10 +92,10 @@ internal async Task> WorkThumbnailGeneration( await _thumbnailQuery.UpdateAsync(item); continue; } - - var generationResultModels = (await _thumbnailService.CreateThumbAsync(fileIndexItem - .FilePath!, fileIndexItem.FileHash!)).ToList(); - + + var generationResultModels = ( await _thumbnailService.CreateThumbAsync(fileIndexItem + .FilePath!, fileIndexItem.FileHash!) ).ToList(); + await _updateStatusGeneratedThumbnailService.AddOrUpdateStatusAsync( generationResultModels); var removedItems = await _updateStatusGeneratedThumbnailService @@ -103,13 +103,13 @@ await _updateStatusGeneratedThumbnailService.AddOrUpdateStatusAsync( if ( removedItems.Count != 0 ) { _logger.LogInformation($"[DatabaseThumbnailGenerationService] removed items ({DateTime.UtcNow:HH:mm:ss})" + - $" items: {string.Join(",",removedItems)}"); + $" items: {string.Join(",", removedItems)}"); continue; } resultData.Add(fileIndexItem); } - + var filteredData = resultData .Where(p => p.Status == FileIndexItem.ExifStatus.Ok).ToList(); @@ -118,10 +118,10 @@ await _updateStatusGeneratedThumbnailService.AddOrUpdateStatusAsync( _logger.LogInformation($"[DatabaseThumbnailGenerationService] no items ({DateTime.UtcNow:HH:mm:ss})"); return chuckedItems; } - + _logger.LogInformation($"[DatabaseThumbnailGenerationService] done ({DateTime.UtcNow:HH:mm:ss})" + - $" {filteredData.Count} items: " + - $"{string.Join(",",filteredData.Select(p => p.FilePath).ToList())}"); + $" {filteredData.Count} items: " + + $"{string.Join(",", filteredData.Select(p => p.FilePath).ToList())}"); var webSocketResponse = new ApiNotificationResponseModel>(filteredData, ApiNotificationType.ThumbnailGeneration); diff --git a/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs b/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs index 8a2eb96ae8..826c71f6e5 100644 --- a/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs +++ b/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs @@ -28,16 +28,16 @@ public class ManualThumbnailGenerationService : IManualThumbnailGenerationServic private readonly IWebSocketConnectionsService _connectionsService; private readonly IThumbnailService _thumbnailService; private readonly IThumbnailQueuedHostedService _bgTaskQueue; - - public ManualThumbnailGenerationService(IQuery query, IWebLogger logger, IWebSocketConnectionsService connectionsService, - IThumbnailService thumbnailService, + + public ManualThumbnailGenerationService(IQuery query, IWebLogger logger, IWebSocketConnectionsService connectionsService, + IThumbnailService thumbnailService, IThumbnailQueuedHostedService bgTaskQueue) { _query = query; _logger = logger; _connectionsService = connectionsService; _thumbnailService = thumbnailService; - _bgTaskQueue = bgTaskQueue; + _bgTaskQueue = bgTaskQueue; } public async Task ManualBackgroundQueue(string subPath) @@ -48,7 +48,7 @@ await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ => await WorkThumbnailGeneration(subPath); }, subPath); } - + internal async Task WorkThumbnailGeneration(string subPath) { try @@ -69,7 +69,7 @@ internal async Task WorkThumbnailGeneration(string subPath) var webSocketResponse = new ApiNotificationResponseModel>(result, ApiNotificationType.ThumbnailGeneration); await _connectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None); - + _logger.LogInformation($"[ThumbnailGenerationController] done {subPath}"); } catch ( UnauthorizedAccessException e ) @@ -88,7 +88,7 @@ internal static List WhichFilesNeedToBePushedForUpdates(List {"LastEdited", "FileHash"}; + item.LastChanged = new List { "LastEdited", "FileHash" }; result.Add(item); } diff --git a/starsky/starsky.feature.thumbnail/Services/PeriodicThumbnailScanHostedService.cs b/starsky/starsky.feature.thumbnail/Services/PeriodicThumbnailScanHostedService.cs index 95e89c36ec..9165d6e0ac 100644 --- a/starsky/starsky.feature.thumbnail/Services/PeriodicThumbnailScanHostedService.cs +++ b/starsky/starsky.feature.thumbnail/Services/PeriodicThumbnailScanHostedService.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Hosting; using starsky.feature.thumbnail.Interfaces; using starsky.foundation.injection; -using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; @@ -27,16 +26,16 @@ public class PeriodicThumbnailScanHostedService : BackgroundService internal TimeSpan Period { get; set; } internal int MinimumIntervalInMinutes { get; set; } = 3; - + internal bool IsEnabled { get; set; } - + public PeriodicThumbnailScanHostedService(AppSettings appSettings, - IWebLogger logger, + IWebLogger logger, IServiceScopeFactory factory) { _logger = logger; _factory = factory; - + if ( appSettings.ThumbnailGenerationIntervalInMinutes >= MinimumIntervalInMinutes ) { Period = TimeSpan.FromMinutes(appSettings @@ -44,10 +43,10 @@ public PeriodicThumbnailScanHostedService(AppSettings appSettings, IsEnabled = true; return; } - + Period = TimeSpan.FromMinutes(60); } - + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // why Task.Yield -> https://medium.com/@thepen0411/how-to-resolve-the-net-background-service-blocking-issue-c96086de8acd @@ -61,8 +60,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await RunJob(cancellationToken); } - - if ( !IsEnabled) + + if ( !IsEnabled ) { return false; } @@ -81,19 +80,19 @@ await timer.WaitForNextTickAsync(cancellationToken) ) { _logger.LogError("[StartBackgroundAsync] catch-ed OperationCanceledException", exception); } - + return null; } internal async Task RunJob(CancellationToken cancellationToken = default) { - if (! IsEnabled ) + if ( !IsEnabled ) { _logger.LogInformation( $"Skipped {nameof(PeriodicThumbnailScanHostedService)}"); return false; } - + cancellationToken.ThrowIfCancellationRequested(); try @@ -108,7 +107,7 @@ await timer.WaitForNextTickAsync(cancellationToken) ) $" Count: {_executionCount} ({DateTime.UtcNow:HH:mm:ss})"); return true; } - catch (Exception ex) + catch ( Exception ex ) { _logger.LogInformation( $"Failed to execute {nameof(PeriodicThumbnailScanHostedService)} " + diff --git a/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs b/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs index ed47aaada7..469dcaf294 100644 --- a/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs +++ b/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs @@ -13,7 +13,7 @@ public interface IMoveToTrashService /// list of files Task> MoveToTrashAsync(List inputFilePaths, bool collections); - + /// /// Is it supported to use the system trash /// But it does NOT check if the feature toggle is enabled diff --git a/starsky/starsky.feature.trash/Services/MoveToTrashService.cs b/starsky/starsky.feature.trash/Services/MoveToTrashService.cs index 457a417774..1f09bfc099 100644 --- a/starsky/starsky.feature.trash/Services/MoveToTrashService.cs +++ b/starsky/starsky.feature.trash/Services/MoveToTrashService.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Text; using starsky.feature.metaupdate.Interfaces; using starsky.feature.trash.Interfaces; @@ -24,8 +24,8 @@ public class MoveToTrashService : IMoveToTrashService private readonly ITrashConnectionService _connectionService; public MoveToTrashService(AppSettings appSettings, IQuery query, - IMetaPreflight metaPreflight, - IUpdateBackgroundTaskQueue queue, + IMetaPreflight metaPreflight, + IUpdateBackgroundTaskQueue queue, ITrashService systemTrashService, IMetaUpdateService metaUpdateService, ITrashConnectionService connectionService ) @@ -38,7 +38,7 @@ ITrashConnectionService connectionService _metaUpdateService = metaUpdateService; _connectionService = connectionService; } - + /// /// Is supported and enabled in the feature toggle /// @@ -46,7 +46,7 @@ ITrashConnectionService connectionService public bool IsEnabled() { return _appSettings.UseSystemTrash == true && - _systemTrashService.DetectToUseSystemTrash(); + _systemTrashService.DetectToUseSystemTrash(); } /// @@ -70,37 +70,37 @@ public async Task> MoveToTrashAsync( List inputFilePaths, bool collections) { var inputModel = new FileIndexItem { Tags = TrashKeyword.TrashKeywordString }; - var (fileIndexResultsList, changedFileIndexItemName) = + var (fileIndexResultsList, changedFileIndexItemName) = await _metaPreflight.PreflightAsync(inputModel, inputFilePaths, false, collections, 0); (fileIndexResultsList, changedFileIndexItemName) = await AppendChildItemsToTrashList(fileIndexResultsList, changedFileIndexItemName); - + var moveToTrashList = fileIndexResultsList.Where(p => p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.Deleted).ToList(); var isSystemTrashEnabled = IsEnabled(); - + await _queue.QueueBackgroundWorkItemAsync(async _ => { await _connectionService.ConnectionServiceAsync(moveToTrashList, isSystemTrashEnabled); - + if ( isSystemTrashEnabled ) { await SystemTrashInQueue(moveToTrashList); return; } - - await MetaTrashInQueue(changedFileIndexItemName!, + + await MetaTrashInQueue(changedFileIndexItemName!, fileIndexResultsList, inputModel, collections); - + }, "trash"); - + return TrashConnectionService.StatusUpdate(moveToTrashList, isSystemTrashEnabled); } - private async Task MetaTrashInQueue(Dictionary> changedFileIndexItemName, + private async Task MetaTrashInQueue(Dictionary> changedFileIndexItemName, List fileIndexResultsList, FileIndexItem inputModel, bool collections) { await _metaUpdateService.UpdateAsync(changedFileIndexItemName, @@ -112,8 +112,8 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName, /// /// /// - internal async Task<(List, Dictionary>?)> AppendChildItemsToTrashList(List moveToTrash, - Dictionary> changedFileIndexItemName) + internal async Task<(List, Dictionary>?)> AppendChildItemsToTrashList(List moveToTrash, + Dictionary> changedFileIndexItemName) { var parentSubPaths = moveToTrash .Where(p => !string.IsNullOrEmpty(p.FilePath) && p.IsDirectory == true) @@ -122,24 +122,24 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName, if ( parentSubPaths.Count == 0 ) { - return ( moveToTrash, changedFileIndexItemName ); + return (moveToTrash, changedFileIndexItemName); } var childItems = ( await _query.GetAllObjectsAsync(parentSubPaths) ) .Where(p => p.FilePath != null).ToList(); - + moveToTrash.AddRange(childItems); - foreach ( var childItem in childItems) + foreach ( var childItem in childItems ) { var builder = new StringBuilder(childItem.Tags); builder.Append(", "); builder.Append(TrashKeyword.TrashKeywordString); childItem.Tags = builder.ToString(); - - changedFileIndexItemName.TryAdd(childItem.FilePath!, new List {"tags"}); + + changedFileIndexItemName.TryAdd(childItem.FilePath!, new List { "tags" }); } - return (moveToTrash,changedFileIndexItemName); + return (moveToTrash, changedFileIndexItemName); } private async Task SystemTrashInQueue(List moveToTrash) @@ -148,9 +148,9 @@ private async Task SystemTrashInQueue(List moveToTrash) .Where(p => p.FilePath != null) .Select(p => _appSettings.DatabasePathToFilePath(p.FilePath!)) .ToList(); - + _systemTrashService.Trash(fullFilePaths); - + await _query.RemoveItemAsync(moveToTrash); } } diff --git a/starsky/starsky.feature.trash/Services/TrashConnectionService.cs b/starsky/starsky.feature.trash/Services/TrashConnectionService.cs index 420d369565..9e6c97e009 100644 --- a/starsky/starsky.feature.trash/Services/TrashConnectionService.cs +++ b/starsky/starsky.feature.trash/Services/TrashConnectionService.cs @@ -8,14 +8,14 @@ namespace starsky.feature.trash.Services; -[Service(typeof(ITrashConnectionService), +[Service(typeof(ITrashConnectionService), InjectionLifetime = InjectionLifetime.Scoped)] public class TrashConnectionService : ITrashConnectionService { private readonly IWebSocketConnectionsService _webSocketConnectionsService; private readonly INotificationQuery _notificationQuery; - - public TrashConnectionService(IWebSocketConnectionsService webSocketConnectionsService, + + public TrashConnectionService(IWebSocketConnectionsService webSocketConnectionsService, INotificationQuery notificationQuery) { _webSocketConnectionsService = webSocketConnectionsService; @@ -29,7 +29,7 @@ public static List StatusUpdate( var status = isSystemTrash ? FileIndexItem.ExifStatus.NotFoundSourceMissing : FileIndexItem.ExifStatus.Deleted; - + foreach ( var item in moveToTrash ) { item.Status = status; @@ -37,13 +37,13 @@ public static List StatusUpdate( return moveToTrash; } - public async Task> ConnectionServiceAsync( List moveToTrash, + public async Task> ConnectionServiceAsync(List moveToTrash, bool isSystemTrash) { moveToTrash = StatusUpdate(moveToTrash, isSystemTrash); - + var webSocketResponse = new ApiNotificationResponseModel>( - moveToTrash,ApiNotificationType.MoveToTrash); + moveToTrash, ApiNotificationType.MoveToTrash); await _webSocketConnectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None); await _notificationQuery.AddNotification(webSocketResponse); return moveToTrash; diff --git a/starsky/starsky.feature.trash/starsky.feature.trash.csproj b/starsky/starsky.feature.trash/starsky.feature.trash.csproj index 4d34e42094..add9db259e 100644 --- a/starsky/starsky.feature.trash/starsky.feature.trash.csproj +++ b/starsky/starsky.feature.trash/starsky.feature.trash.csproj @@ -10,10 +10,13 @@ - - - - + + + + - + + + true + diff --git a/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebRequest.cs b/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebRequest.cs index a285f21cbc..2ff756eb2b 100644 --- a/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebRequest.cs +++ b/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebRequest.cs @@ -12,7 +12,7 @@ public WrapFtpWebRequest(FtpWebRequest request) { _request = request; } - + /// /// /// Selects FTP command to use. WebRequestMethods.Ftp.DownloadFile is default. @@ -30,24 +30,25 @@ public string Method /// public NetworkCredential Credentials { - get => null; + get => null!; set => _request.Credentials = value; } - public bool UsePassive + public bool UsePassive { get => _request.UsePassive; set => _request.UsePassive = value; } - + /// /// True by default, false allows transmission using text mode /// - public bool UseBinary + public bool UseBinary { get => _request.UseBinary; set => _request.UseBinary = value; } + public bool KeepAlive { get => _request.KeepAlive; @@ -60,7 +61,7 @@ public bool KeepAlive /// Wrapper response public IFtpWebResponse GetResponse() { - return new WrapFtpWebResponse((FtpWebResponse)_request.GetResponse()); + return new WrapFtpWebResponse(( FtpWebResponse )_request.GetResponse()); } /// @@ -71,5 +72,4 @@ public Stream GetRequestStream() return _request.GetRequestStream(); } } - } diff --git a/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebResponse.cs b/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebResponse.cs index 7b34fc0f00..e08cf692cb 100644 --- a/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebResponse.cs +++ b/starsky/starsky.feature.webftppublish/FtpAbstractions/Helpers/WrapFtpWebResponse.cs @@ -7,7 +7,7 @@ namespace starsky.feature.webftppublish.FtpAbstractions.Helpers { public class WrapFtpWebResponse : IFtpWebResponse { - private FtpWebResponse _response; + private FtpWebResponse? _response; public WrapFtpWebResponse(FtpWebResponse response) { @@ -24,7 +24,7 @@ protected virtual void Dispose(bool disposing) { if ( !disposing ) return; if ( _response == null ) return; - ((IDisposable)_response).Dispose(); + ( ( IDisposable )_response ).Dispose(); _response = null; } diff --git a/starsky/starsky.feature.webftppublish/FtpAbstractions/Services/FtpWebRequestFactory.cs b/starsky/starsky.feature.webftppublish/FtpAbstractions/Services/FtpWebRequestFactory.cs index 0c80475f5b..23c5ecda6e 100644 --- a/starsky/starsky.feature.webftppublish/FtpAbstractions/Services/FtpWebRequestFactory.cs +++ b/starsky/starsky.feature.webftppublish/FtpAbstractions/Services/FtpWebRequestFactory.cs @@ -20,7 +20,7 @@ public class FtpWebRequestFactory : IFtpWebRequestFactory /// new Requester public IFtpWebRequest Create(string uri) { - return new WrapFtpWebRequest((FtpWebRequest)WebRequest.Create(uri)); + return new WrapFtpWebRequest(( FtpWebRequest )WebRequest.Create(uri)); } } } diff --git a/starsky/starsky.feature.webftppublish/Helpers/WebFtpCli.cs b/starsky/starsky.feature.webftppublish/Helpers/WebFtpCli.cs index e6097c6f2b..033a3978a0 100644 --- a/starsky/starsky.feature.webftppublish/Helpers/WebFtpCli.cs +++ b/starsky/starsky.feature.webftppublish/Helpers/WebFtpCli.cs @@ -21,50 +21,52 @@ public class WebFtpCli private readonly IStorage _hostStorageProvider; private readonly IFtpWebRequestFactory _webRequestFactory; - public WebFtpCli(AppSettings appSettings, ISelectorStorage selectorStorage, IConsole console, + public WebFtpCli(AppSettings appSettings, ISelectorStorage selectorStorage, + IConsole console, IFtpWebRequestFactory webRequestFactory) { _appSettings = appSettings; _console = console; - _argsHelper = new ArgsHelper(_appSettings,console); - _hostStorageProvider = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _argsHelper = new ArgsHelper(_appSettings, console); + _hostStorageProvider = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); _webRequestFactory = webRequestFactory; } public async Task RunAsync(string[] args) { _appSettings.Verbose = ArgsHelper.NeedVerbose(args); - - if ( ArgsHelper.NeedHelp(args)) + + if ( ArgsHelper.NeedHelp(args) ) { _appSettings.ApplicationType = AppSettings.StarskyAppType.WebFtp; _argsHelper.NeedHelpShowDialog(); return; } - + var inputFullFileDirectory = new ArgsHelper(_appSettings) - .GetPathFormArgs(args,false); + .GetPathFormArgs(args, false); - if (string.IsNullOrWhiteSpace(inputFullFileDirectory)) + if ( string.IsNullOrWhiteSpace(inputFullFileDirectory) ) { _console.WriteLine("Please use the -p to add a path first"); return; } - + // used in this session to find the files back - if ( _hostStorageProvider.IsFolderOrFile(inputFullFileDirectory) - == FolderOrFileModel.FolderOrFileTypeList.Deleted ) + if ( _hostStorageProvider.IsFolderOrFile(inputFullFileDirectory) + == FolderOrFileModel.FolderOrFileTypeList.Deleted ) { _console.WriteLine($"Folder location {inputFullFileDirectory} " + - $"is not found \nPlease try the `-h` command to get help "); + $"is not found \nPlease try the `-h` command to get help "); return; } - + // check if settings is valid if ( string.IsNullOrEmpty(_appSettings.WebFtp) ) { - _console.WriteLine($"Please update the WebFtp settings in appsettings.json" ); + _console.WriteLine($"Please update the WebFtp settings in appsettings.json"); return; } @@ -72,24 +74,24 @@ public async Task RunAsync(string[] args) if ( !_hostStorageProvider.ExistFile(settingsFullFilePath) ) { _console.WriteLine($"Please run 'starskywebhtmlcli' " + - $"first to generate a settings file" ); + $"first to generate a settings file"); return; } - var settings = await + var settings = await new DeserializeJson(_hostStorageProvider).ReadAsync( settingsFullFilePath); - var ftpService = new FtpService(_appSettings,_hostStorageProvider, + var ftpService = new FtpService(_appSettings, _hostStorageProvider, _console, _webRequestFactory) - .Run(inputFullFileDirectory, settings.Slug, settings.Copy); + .Run(inputFullFileDirectory, settings!.Slug, settings.Copy); if ( !ftpService ) { _console.WriteLine("Ftp copy failed"); return; } - + _console.WriteLine($"Ftp copy successful done: {settings.Slug}"); } } diff --git a/starsky/starsky.feature.webftppublish/Models/FtpPublishManifestModel.cs b/starsky/starsky.feature.webftppublish/Models/FtpPublishManifestModel.cs index 7fa331876b..8554ae1b97 100644 --- a/starsky/starsky.feature.webftppublish/Models/FtpPublishManifestModel.cs +++ b/starsky/starsky.feature.webftppublish/Models/FtpPublishManifestModel.cs @@ -7,8 +7,8 @@ public class FtpPublishManifestModel /// /// Short for name, without spaces and non-ascii /// - public string Slug { get; set; } - + public string Slug { get; set; } = string.Empty; + /// /// List of files to Copy, string is relative path and bool is True for copy /// diff --git a/starsky/starsky.feature.webftppublish/Services/FtpService.cs b/starsky/starsky.feature.webftppublish/Services/FtpService.cs index 461c44254d..51c8a24700 100644 --- a/starsky/starsky.feature.webftppublish/Services/FtpService.cs +++ b/starsky/starsky.feature.webftppublish/Services/FtpService.cs @@ -72,7 +72,7 @@ public FtpService(AppSettings appSettings, IStorage storage, IConsole console, public bool Run(string parentDirectory, string slug, Dictionary copyContent) { foreach ( var thisDirectory in - CreateListOfRemoteDirectories(parentDirectory, slug, copyContent) ) + CreateListOfRemoteDirectories(parentDirectory, slug, copyContent) ) { _console.Write(","); if ( DoesFtpDirectoryExist(thisDirectory) ) continue; @@ -117,7 +117,7 @@ internal IEnumerable CreateListOfRemoteDirectories(string parentDirector { var parentItems = Breadcrumbs.BreadcrumbHelper(copyItem.Key); foreach ( var item in parentItems.Where(p => - p != Path.DirectorySeparatorChar.ToString()) ) + p != Path.DirectorySeparatorChar.ToString()) ) { if ( _storage.ExistFolder(parentDirectory + item) ) { @@ -157,8 +157,8 @@ internal bool MakeUpload(string parentDirectory, string slug, { const string pathDelimiter = "/"; var toFtpPath = PathHelper.RemoveLatestSlash(_webFtpNoLogin) + pathDelimiter + - GenerateSlugHelper.GenerateSlug(slug, true) + pathDelimiter + - item; + GenerateSlugHelper.GenerateSlug(slug, true) + pathDelimiter + + item; _console.Write("."); @@ -253,7 +253,7 @@ internal bool CreateFtpDirectory(string directory) } catch ( WebException ex ) { - var ftpWebResponse = ( FtpWebResponse )ex.Response; + var ftpWebResponse = ( FtpWebResponse? )ex.Response; ftpWebResponse?.Close(); return ftpWebResponse?.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable; } diff --git a/starsky/starsky.feature.webftppublish/starsky.feature.webftppublish.csproj b/starsky/starsky.feature.webftppublish/starsky.feature.webftppublish.csproj index d66dec8926..33dd1688c0 100644 --- a/starsky/starsky.feature.webftppublish/starsky.feature.webftppublish.csproj +++ b/starsky/starsky.feature.webftppublish/starsky.feature.webftppublish.csproj @@ -5,12 +5,13 @@ {31df1419-6c81-4372-b7ae-a6ebb429e7e9} 0.6.0-beta.0 + enable - + - - - + + + diff --git a/starsky/starsky.feature.webhtmlpublish/Extensions/DictionaryExtensions.cs b/starsky/starsky.feature.webhtmlpublish/Extensions/DictionaryExtensions.cs index a635863d44..fbf2227ed3 100644 --- a/starsky/starsky.feature.webhtmlpublish/Extensions/DictionaryExtensions.cs +++ b/starsky/starsky.feature.webhtmlpublish/Extensions/DictionaryExtensions.cs @@ -15,7 +15,7 @@ public static void AddRangeOverride(this IDictionary } public static void ForEach(this IEnumerable source, Action action) { - foreach (var item in source) + foreach ( var item in source ) action(item); } } diff --git a/starsky/starsky.feature.webhtmlpublish/Helpers/CopyPublishedContent.cs b/starsky/starsky.feature.webhtmlpublish/Helpers/CopyPublishedContent.cs index dc8296bf58..7b0c1abe20 100644 --- a/starsky/starsky.feature.webhtmlpublish/Helpers/CopyPublishedContent.cs +++ b/starsky/starsky.feature.webhtmlpublish/Helpers/CopyPublishedContent.cs @@ -17,8 +17,9 @@ public sealed class CopyPublishedContent public CopyPublishedContent(ToCreateSubfolder toCreateSubfolder, ISelectorStorage selectorStorage) { + ArgumentNullException.ThrowIfNull(selectorStorage); + _toCreateSubfolder = toCreateSubfolder; - if ( selectorStorage == null ) return; _hostStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); } @@ -49,10 +50,10 @@ public Dictionary CopyContent( internal static string GetContentFolder() { return PathHelper.RemoveLatestBackslash(AppDomain.CurrentDomain.BaseDirectory) + - Path.DirectorySeparatorChar + - "WebHtmlPublish" + - Path.DirectorySeparatorChar + - "PublishedContent"; + Path.DirectorySeparatorChar + + "WebHtmlPublish" + + Path.DirectorySeparatorChar + + "PublishedContent"; } } } diff --git a/starsky/starsky.feature.webhtmlpublish/Helpers/EmbeddedViewsPath.cs b/starsky/starsky.feature.webhtmlpublish/Helpers/EmbeddedViewsPath.cs index 4d6831fae1..e2d7cea315 100644 --- a/starsky/starsky.feature.webhtmlpublish/Helpers/EmbeddedViewsPath.cs +++ b/starsky/starsky.feature.webhtmlpublish/Helpers/EmbeddedViewsPath.cs @@ -8,11 +8,11 @@ public static class EmbeddedViewsPath public static string GetViewFullPath(string viewName) { return AppDomain.CurrentDomain.BaseDirectory + - Path.DirectorySeparatorChar + - "WebHtmlPublish" + - Path.DirectorySeparatorChar + - "EmbeddedViews" + - Path.DirectorySeparatorChar + viewName; + Path.DirectorySeparatorChar + + "WebHtmlPublish" + + Path.DirectorySeparatorChar + + "EmbeddedViews" + + Path.DirectorySeparatorChar + viewName; } } } diff --git a/starsky/starsky.feature.webhtmlpublish/Helpers/ParseRazor.cs b/starsky/starsky.feature.webhtmlpublish/Helpers/ParseRazor.cs index 20301a86a5..90dab2cf73 100644 --- a/starsky/starsky.feature.webhtmlpublish/Helpers/ParseRazor.cs +++ b/starsky/starsky.feature.webhtmlpublish/Helpers/ParseRazor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using RazorLight; using starsky.feature.webhtmlpublish.ViewModels; @@ -9,47 +9,47 @@ namespace starsky.feature.webhtmlpublish.Helpers { - public class ParseRazor - { - private readonly RazorLightEngine _engine; - private readonly IStorage _hostFileSystemStorage; - private readonly IWebLogger _logger; + public class ParseRazor + { + private readonly RazorLightEngine _engine; + private readonly IStorage _hostFileSystemStorage; + private readonly IWebLogger _logger; - public ParseRazor(IStorage fileSystemStorage, IWebLogger logger) - { - _hostFileSystemStorage = fileSystemStorage; - _logger = logger; - _engine = new RazorLightEngineBuilder() - .UseEmbeddedResourcesProject(typeof(PublishManifest)) + public ParseRazor(IStorage fileSystemStorage, IWebLogger logger) + { + _hostFileSystemStorage = fileSystemStorage; + _logger = logger; + _engine = new RazorLightEngineBuilder() + .UseEmbeddedResourcesProject(typeof(PublishManifest)) .UseEmbeddedResourcesProject(typeof(WebHtmlViewModel)) .UseEmbeddedResourcesProject(typeof(AppSettings)) .UseEmbeddedResourcesProject(typeof(FileIndexItem)) - .UseEmbeddedResourcesProject(typeof(System.Linq.Enumerable)) - .UseEmbeddedResourcesProject(typeof(AppSettingsPublishProfiles)) - .UseFileSystemProject(AppDomain.CurrentDomain.BaseDirectory ) - // > starskywebhtmlcli/bin folder - .UseMemoryCachingProvider() - .Build(); - } + .UseEmbeddedResourcesProject(typeof(System.Linq.Enumerable)) + .UseEmbeddedResourcesProject(typeof(AppSettingsPublishProfiles)) + .UseFileSystemProject(AppDomain.CurrentDomain.BaseDirectory) + // > starskywebhtmlcli/bin folder + .UseMemoryCachingProvider() + .Build(); + } - public bool Exist(string viewName) - { - var path = EmbeddedViewsPath.GetViewFullPath(viewName); - return _hostFileSystemStorage.ExistFile(path); - } - - public async Task EmbeddedViews(string viewName, object viewModel) - { - if ( Exist(viewName) ) - { - // has an dependency on the filesystem by _engine.CompileRenderAsync - return await - _engine.CompileRenderAsync( - "WebHtmlPublish/EmbeddedViews/" + viewName, viewModel); - } - - _logger.LogInformation("View Not Exist " + EmbeddedViewsPath.GetViewFullPath(viewName)); - return string.Empty; - } - } + public bool Exist(string viewName) + { + var path = EmbeddedViewsPath.GetViewFullPath(viewName); + return _hostFileSystemStorage.ExistFile(path); + } + + public async Task EmbeddedViews(string viewName, object viewModel) + { + if ( Exist(viewName) ) + { + // has an dependency on the filesystem by _engine.CompileRenderAsync + return await + _engine.CompileRenderAsync( + "WebHtmlPublish/EmbeddedViews/" + viewName, viewModel); + } + + _logger.LogInformation("View Not Exist " + EmbeddedViewsPath.GetViewFullPath(viewName)); + return string.Empty; + } + } } diff --git a/starsky/starsky.feature.webhtmlpublish/Helpers/PublishCli.cs b/starsky/starsky.feature.webhtmlpublish/Helpers/PublishCli.cs index d732fd8cb2..cbcb9b1cb2 100644 --- a/starsky/starsky.feature.webhtmlpublish/Helpers/PublishCli.cs +++ b/starsky/starsky.feature.webhtmlpublish/Helpers/PublishCli.cs @@ -38,7 +38,7 @@ public PublishCli(ISelectorStorage storageSelector, IPublishPreflight publishPre _storageSelector = storageSelector; _logger = logger; } - + /// /// Command Line Helper to server WebHtml Content /// @@ -47,38 +47,38 @@ public PublishCli(ISelectorStorage storageSelector, IPublishPreflight publishPre public async Task Publisher(string[] args) { _appSettings.Verbose = ArgsHelper.NeedVerbose(args); - - if ( ArgsHelper.NeedHelp(args)) + + if ( ArgsHelper.NeedHelp(args) ) { _appSettings.ApplicationType = AppSettings.StarskyAppType.WebHtml; _argsHelper.NeedHelpShowDialog(); return; } - - var inputFullPath = _argsHelper.GetPathFormArgs(args,false); - if (string.IsNullOrWhiteSpace(inputFullPath)) + + var inputFullPath = _argsHelper.GetPathFormArgs(args, false); + if ( string.IsNullOrWhiteSpace(inputFullPath) ) { _console.WriteLine("Please use the -p to add a path first"); return; } - + if ( _hostFileSystemStorage.IsFolderOrFile(inputFullPath) != - FolderOrFileModel.FolderOrFileTypeList.Folder ) + FolderOrFileModel.FolderOrFileTypeList.Folder ) { _console.WriteLine("Please add a valid folder: " + inputFullPath + ". " + - "This folder is not found"); + "This folder is not found"); return; } - + var settingsFullFilePath = Path.Combine(inputFullPath, "_settings.json"); if ( _hostFileSystemStorage.ExistFile(settingsFullFilePath) ) { _console.WriteLine($"You have already run this program for this folder remove the " + - $"_settings.json first and try it again" ); + $"_settings.json first and try it again"); return; } - var itemName = _publishPreflight.GetNameConsole(inputFullPath,args); + var itemName = _publishPreflight.GetNameConsole(inputFullPath, args); if ( _appSettings.IsVerbose() ) { @@ -89,21 +89,21 @@ public async Task Publisher(string[] args) // used in this session to find the photos back // outside the webRoot of iStorage _appSettings.StorageFolder = inputFullPath; - + // use relative to StorageFolder var listOfFiles = _subPathStorage.GetAllFilesInDirectory("/") .Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList(); - + var fileIndexList = await new ReadMeta(_subPathStorage, _appSettings, null!, _logger) .ReadExifAndXmpFromFileAddFilePathHashAsync(listOfFiles); - + // todo introduce selector - var profileName = new PublishPreflight(_appSettings,_console, _storageSelector, _logger) + var profileName = new PublishPreflight(_appSettings, _console, _storageSelector, _logger) .GetPublishProfileNameByIndex(0); - - await _publishService.RenderCopy(fileIndexList, profileName, + + await _publishService.RenderCopy(fileIndexList, profileName, itemName, inputFullPath, true); - + _console.WriteLine("publish done"); } } diff --git a/starsky/starsky.feature.webhtmlpublish/Helpers/PublishManifest.cs b/starsky/starsky.feature.webhtmlpublish/Helpers/PublishManifest.cs index a171fe9721..428f9b3b81 100644 --- a/starsky/starsky.feature.webhtmlpublish/Helpers/PublishManifest.cs +++ b/starsky/starsky.feature.webhtmlpublish/Helpers/PublishManifest.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; using starsky.feature.webhtmlpublish.Models; using starsky.foundation.platform.JsonConverter; using starsky.foundation.storage.Helpers; @@ -26,20 +25,17 @@ public PublishManifest(IStorage storage) /// without ManifestName /// /// - public PublishManifestModel ExportManifest( string parentFullFilePath, string itemName, - Dictionary copyContent) + public PublishManifestModel ExportManifest(string parentFullFilePath, string itemName, + Dictionary? copyContent) { - var manifest = new PublishManifestModel - { - Name = itemName, - Copy = copyContent - }; + copyContent ??= new Dictionary(); + var manifest = new PublishManifestModel { Name = itemName, Copy = copyContent }; var output = JsonSerializer.Serialize(manifest, DefaultJsonSerializer.NoNamingPolicy); var outputLocation = Path.Combine(parentFullFilePath, ManifestName); - + _storage.FileDelete(outputLocation); _storage.WriteStream(StringToStreamHelper.StringToStream(output), outputLocation); - + return manifest; } } diff --git a/starsky/starsky.feature.webhtmlpublish/Helpers/ToBase64DataUriList.cs b/starsky/starsky.feature.webhtmlpublish/Helpers/ToBase64DataUriList.cs index 405618d129..e54e570361 100644 --- a/starsky/starsky.feature.webhtmlpublish/Helpers/ToBase64DataUriList.cs +++ b/starsky/starsky.feature.webhtmlpublish/Helpers/ToBase64DataUriList.cs @@ -17,40 +17,45 @@ public class ToBase64DataUriList private readonly IWebLogger _logger; private readonly AppSettings _appSettings; - public ToBase64DataUriList(IStorage iStorage, IStorage thumbnailStorage, IWebLogger logger, AppSettings appSettings) + public ToBase64DataUriList(IStorage iStorage, IStorage thumbnailStorage, IWebLogger logger, + AppSettings appSettings) { _iStorage = iStorage; _thumbnailStorage = thumbnailStorage; _logger = logger; _appSettings = appSettings; } - + [SuppressMessage("Usage", "S3966: Resource 'memoryStream' has " + - "already been disposed explicitly or through a using statement implicitly. " + - "Remove the redundant disposal.")] + "already been disposed explicitly or through a using statement implicitly. " + + "Remove the redundant disposal.")] public async Task Create(List fileIndexList) { var base64ImageArray = new string[fileIndexList.Count]; - for (var i = 0; i /// Create SubFolders by the profile.Folder setting /// @@ -24,16 +24,16 @@ public void Create(AppSettingsPublishProfiles profile, string parentFolder) // check if subfolder '1000' exist on disk // used for moving subfolders first var profileFolderStringBuilder = new StringBuilder(); - if (!string.IsNullOrEmpty(parentFolder)) + if ( !string.IsNullOrEmpty(parentFolder) ) { profileFolderStringBuilder.Append(parentFolder); profileFolderStringBuilder.Append('/'); } - + profileFolderStringBuilder.Append(profile.Folder); - if ( _hostFileSystemStorage.IsFolderOrFile(profileFolderStringBuilder.ToString()) - == FolderOrFileModel.FolderOrFileTypeList.Deleted) + if ( _hostFileSystemStorage.IsFolderOrFile(profileFolderStringBuilder.ToString()) + == FolderOrFileModel.FolderOrFileTypeList.Deleted ) { _hostFileSystemStorage.CreateDirectory(profileFolderStringBuilder.ToString()); } diff --git a/starsky/starsky.feature.webhtmlpublish/Interfaces/IPublishPreflight.cs b/starsky/starsky.feature.webhtmlpublish/Interfaces/IPublishPreflight.cs index 6c8bad6759..d80a743687 100644 --- a/starsky/starsky.feature.webhtmlpublish/Interfaces/IPublishPreflight.cs +++ b/starsky/starsky.feature.webhtmlpublish/Interfaces/IPublishPreflight.cs @@ -10,15 +10,15 @@ public interface IPublishPreflight /// Get all publish profile names /// /// (string: name, bool: isValid) - IEnumerable> GetAllPublishProfileNames(); - + IEnumerable> GetAllPublishProfileNames(); + /// /// Check if the profile is valid /// /// profile key /// (bool and list of errors) - Tuple> IsProfileValid(string publishProfileName); - + Tuple> IsProfileValid(string publishProfileName); + string GetNameConsole(string inputPath, IReadOnlyList args); List GetPublishProfileName(string publishProfileName); } diff --git a/starsky/starsky.feature.webhtmlpublish/Interfaces/IWebHtmlPublishService.cs b/starsky/starsky.feature.webhtmlpublish/Interfaces/IWebHtmlPublishService.cs index 056ea5c4bb..3e6d593eb9 100644 --- a/starsky/starsky.feature.webhtmlpublish/Interfaces/IWebHtmlPublishService.cs +++ b/starsky/starsky.feature.webhtmlpublish/Interfaces/IWebHtmlPublishService.cs @@ -6,12 +6,12 @@ namespace starsky.feature.webhtmlpublish.Interfaces { public interface IWebHtmlPublishService { - Task> RenderCopy(List fileIndexItemsList, + Task?> RenderCopy(List fileIndexItemsList, string publishProfileName, string itemName, string outputParentFullFilePathFolder, bool moveSourceFiles = false); Task GenerateZip(string fullFileParentFolderPath, string itemName, - Dictionary renderCopyResult, + Dictionary? renderCopyResult, bool deleteFolderAfterwards = false); } } diff --git a/starsky/starsky.feature.webhtmlpublish/Models/PublishManifestModel.cs b/starsky/starsky.feature.webhtmlpublish/Models/PublishManifestModel.cs index dbb063b01a..e8370a6d50 100644 --- a/starsky/starsky.feature.webhtmlpublish/Models/PublishManifestModel.cs +++ b/starsky/starsky.feature.webhtmlpublish/Models/PublishManifestModel.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Reflection; using starsky.foundation.platform.Helpers; -using starsky.foundation.platform.Models; namespace starsky.feature.webhtmlpublish.Models { @@ -14,20 +13,17 @@ public class PublishManifestModel /// /// Display name /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Which files or folders need to be copied /// - public Dictionary Copy { get; set; } = new Dictionary(); + public Dictionary Copy { get; set; } = new(); /// /// Slug, Name without spaces, but underscores are allowed /// - public string Slug - { - get { return GenerateSlugHelper.GenerateSlug(Name, true); } - } + public string Slug => GenerateSlugHelper.GenerateSlug(Name, true); /// /// When did the export happen @@ -38,6 +34,6 @@ public string Slug /// /// Starsky Version /// - public string Version => Assembly.GetExecutingAssembly().GetName().Version?.ToString(); + public string? Version => Assembly.GetExecutingAssembly().GetName().Version?.ToString(); } } diff --git a/starsky/starsky.feature.webhtmlpublish/Services/OverlayImage.cs b/starsky/starsky.feature.webhtmlpublish/Services/OverlayImage.cs index ffcf16e23a..86c82b7c74 100644 --- a/starsky/starsky.feature.webhtmlpublish/Services/OverlayImage.cs +++ b/starsky/starsky.feature.webhtmlpublish/Services/OverlayImage.cs @@ -22,38 +22,52 @@ public class OverlayImage : IOverlayImage public OverlayImage(ISelectorStorage selectorStorage) { - if ( selectorStorage == null ) return; _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); _hostFileSystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); } - public string FilePathOverlayImage(string sourceFilePath, AppSettingsPublishProfiles profile) + public string FilePathOverlayImage(string sourceFilePath, + AppSettingsPublishProfiles profile) { var result = profile.Folder + GenerateSlugHelper.GenerateSlug( - Path.GetFileNameWithoutExtension(sourceFilePath), true) - + profile.Append + profile.GetExtensionWithDot(sourceFilePath); + Path.GetFileNameWithoutExtension(sourceFilePath), true) + + profile.Append + + profile.GetExtensionWithDot(sourceFilePath); return result; } - - public string FilePathOverlayImage(string outputParentFullFilePathFolder, string sourceFilePath, + + public string FilePathOverlayImage(string outputParentFullFilePathFolder, + string sourceFilePath, AppSettingsPublishProfiles profile) { var result = PathHelper.AddBackslash(outputParentFullFilePathFolder) + - FilePathOverlayImage(sourceFilePath, profile); + FilePathOverlayImage(sourceFilePath, profile); return result; } - - public Task ResizeOverlayImageThumbnails(string itemFileHash, string outputFullFilePath, AppSettingsPublishProfiles profile) + + public Task ResizeOverlayImageThumbnails(string itemFileHash, + string outputFullFilePath, AppSettingsPublishProfiles profile) { - if ( string.IsNullOrWhiteSpace(itemFileHash) ) throw new ArgumentNullException(nameof(itemFileHash)); - if ( !_thumbnailStorage.ExistFile(itemFileHash) ) throw new FileNotFoundException("fileHash " + itemFileHash); + if ( string.IsNullOrWhiteSpace(itemFileHash) ) + throw new ArgumentNullException(nameof(itemFileHash)); + + if ( !_thumbnailStorage.ExistFile(itemFileHash) ) + { + throw new FileNotFoundException("fileHash " + itemFileHash); + } + + if ( _hostFileSystem.ExistFile(outputFullFilePath) ) + { + return Task.FromResult(false); + } - if ( _hostFileSystem.ExistFile(outputFullFilePath) ) return Task.FromResult(false); if ( !_hostFileSystem.ExistFile(profile.Path) ) { - throw new FileNotFoundException($"overlayImage is missing in profile.Path: {profile.Path}"); + throw new FileNotFoundException( + $"overlayImage is missing in profile.Path: {profile.Path}"); } + return ResizeOverlayImageThumbnailsInternal(itemFileHash, outputFullFilePath, profile); } @@ -69,13 +83,15 @@ private async Task ResizeOverlayImageThumbnailsInternal( string itemFilePath, string outputFullFilePath, AppSettingsPublishProfiles profile) { - using ( var sourceImageStream = _thumbnailStorage.ReadStream(itemFilePath)) + using ( var sourceImageStream = _thumbnailStorage.ReadStream(itemFilePath) ) using ( var sourceImage = await Image.LoadAsync(sourceImageStream) ) - using ( var overlayImageStream = _hostFileSystem.ReadStream(profile.Path)) // for example a logo + using ( var overlayImageStream = + _hostFileSystem.ReadStream(profile.Path) ) // for example a logo using ( var overlayImage = await Image.LoadAsync(overlayImageStream) ) - using ( var outputStream = new MemoryStream() ) + using ( var outputStream = new MemoryStream() ) { - return await ResizeOverlayImageShared(sourceImage, overlayImage, outputStream, profile, + return await ResizeOverlayImageShared(sourceImage, overlayImage, outputStream, + profile, outputFullFilePath); } } @@ -90,14 +106,17 @@ private async Task ResizeOverlayImageThumbnailsInternal( public Task ResizeOverlayImageLarge(string itemFilePath, string outputFullFilePath, AppSettingsPublishProfiles profile) { - if ( string.IsNullOrWhiteSpace(itemFilePath) ) throw new - ArgumentNullException(nameof(itemFilePath)); - if ( !_iStorage.ExistFile(itemFilePath) ) throw new FileNotFoundException("subPath " + itemFilePath); + if ( string.IsNullOrWhiteSpace(itemFilePath) ) + throw new + ArgumentNullException(nameof(itemFilePath)); + if ( !_iStorage.ExistFile(itemFilePath) ) + throw new FileNotFoundException("subPath " + itemFilePath); - if ( _hostFileSystem.ExistFile(outputFullFilePath) ) return Task.FromResult(false); + if ( _hostFileSystem.ExistFile(outputFullFilePath) ) return Task.FromResult(false); if ( !_hostFileSystem.ExistFile(profile.Path) ) { - throw new FileNotFoundException($"overlayImage is missing in profile.Path: {profile.Path}"); + throw new FileNotFoundException( + $"overlayImage is missing in profile.Path: {profile.Path}"); } return ResizeOverlayImageLargeInternal(itemFilePath, @@ -111,21 +130,23 @@ public Task ResizeOverlayImageLarge(string itemFilePath, /// input Image /// location where to store /// image profile that contains sizes - private async Task ResizeOverlayImageLargeInternal(string itemFilePath, + private async Task ResizeOverlayImageLargeInternal(string itemFilePath, string outputFullFilePath, AppSettingsPublishProfiles profile) { - using ( var sourceImageStream = _iStorage.ReadStream(itemFilePath)) + using ( var sourceImageStream = _iStorage.ReadStream(itemFilePath) ) using ( var sourceImage = await Image.LoadAsync(sourceImageStream) ) - using ( var overlayImageStream = _hostFileSystem.ReadStream(profile.Path)) + using ( var overlayImageStream = _hostFileSystem.ReadStream(profile.Path) ) using ( var overlayImage = await Image.LoadAsync(overlayImageStream) ) - using ( var outputStream = new MemoryStream() ) + using ( var outputStream = new MemoryStream() ) { - return await ResizeOverlayImageShared(sourceImage, overlayImage, outputStream, profile, + return await ResizeOverlayImageShared(sourceImage, overlayImage, outputStream, + profile, outputFullFilePath); } } - [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] + [SuppressMessage("Performance", + "CA1859:Use concrete types when possible for improved performance")] private async Task ResizeOverlayImageShared(Image sourceImage, Image overlayImage, Stream outputStream, AppSettingsPublishProfiles profile, string outputSubPath) { @@ -139,10 +160,10 @@ private async Task ResizeOverlayImageShared(Image sourceImage, Image overl .Resize(profile.OverlayMaxWidth, 0, KnownResamplers.Lanczos3) ); - int xPoint = sourceImage.Width - overlayImage.Width; - int yPoint = sourceImage.Height - overlayImage.Height; - - sourceImage.Mutate(x => x.DrawImage(overlayImage, + var xPoint = sourceImage.Width - overlayImage.Width; + var yPoint = sourceImage.Height - overlayImage.Height; + + sourceImage.Mutate(x => x.DrawImage(overlayImage, new Point(xPoint, yPoint), 1F)); await sourceImage.SaveAsJpegAsync(outputStream); diff --git a/starsky/starsky.feature.webhtmlpublish/Services/PublishPreflight.cs b/starsky/starsky.feature.webhtmlpublish/Services/PublishPreflight.cs index 92dd1afbfd..ceea9598e3 100644 --- a/starsky/starsky.feature.webhtmlpublish/Services/PublishPreflight.cs +++ b/starsky/starsky.feature.webhtmlpublish/Services/PublishPreflight.cs @@ -28,10 +28,10 @@ public PublishPreflight(AppSettings appSettings, IConsole console, ISelectorStor _logger = logger; _hostStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); } - - public List> GetPublishProfileNames() + + public List> GetPublishProfileNames() { - var returnList = new List>(); + var returnList = new List>(); if ( _appSettings.PublishProfiles == null || _appSettings.PublishProfiles.Count == 0 ) { return returnList; @@ -64,11 +64,11 @@ public Tuple> IsProfileValid( /// /// profile object /// (bool and list of errors) - internal Tuple> IsProfileValid( KeyValuePair> profiles) + internal Tuple> IsProfileValid(KeyValuePair> profiles) { if ( profiles.Key == null || profiles.Value == null ) { - return new Tuple>(false, new List {"Profile not found"}); + return new Tuple>(false, new List { "Profile not found" }); } var errors = new List(); @@ -79,21 +79,21 @@ internal Tuple> IsProfileValid( KeyValuePair>(errors.Count == 0, errors); } @@ -101,13 +101,13 @@ internal Tuple> IsProfileValid( KeyValuePair /// (string: name, bool: isValid) - public IEnumerable> GetAllPublishProfileNames() + public IEnumerable> GetAllPublishProfileNames() { return _appSettings.PublishProfiles!.Select(p => new KeyValuePair( p.Key, IsProfileValid(p).Item1)); } - + public List GetPublishProfileName(string publishProfileName) { return _appSettings.PublishProfiles! @@ -131,13 +131,13 @@ public string GetNameConsole(string inputPath, IReadOnlyList args) { var name = ArgsHelper.GetName(args); if ( !string.IsNullOrWhiteSpace(name) ) return name; - + var suggestedInput = Path.GetFileName(inputPath); - - _console.WriteLine("\nWhat is the name of the item? (for: "+ suggestedInput +" press Enter)\n "); + + _console.WriteLine("\nWhat is the name of the item? (for: " + suggestedInput + " press Enter)\n "); name = _console.ReadLine(); - - if (string.IsNullOrEmpty(name)) + + if ( string.IsNullOrEmpty(name) ) { name = suggestedInput; } diff --git a/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs b/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs index 0255760514..2787ad114a 100644 --- a/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs +++ b/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -22,401 +22,432 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; -using starsky.foundation.thumbnailgeneration.Helpers; using starsky.foundation.thumbnailgeneration.Interfaces; using starsky.foundation.writemeta.Helpers; using starsky.foundation.writemeta.Interfaces; [assembly: InternalsVisibleTo("starskytest")] + namespace starsky.feature.webhtmlpublish.Services { [Service(typeof(IWebHtmlPublishService), InjectionLifetime = InjectionLifetime.Scoped)] - public class WebHtmlPublishService : IWebHtmlPublishService - { - private readonly AppSettings _appSettings; - private readonly IExifTool _exifTool; - private readonly IStorage _subPathStorage; - private readonly IStorage _thumbnailStorage; - private readonly IStorage _hostFileSystemStorage; - private readonly IConsole _console; - private readonly IOverlayImage _overlayImage; - private readonly PublishManifest _publishManifest; - private readonly IPublishPreflight _publishPreflight; - private readonly CopyPublishedContent _copyPublishedContent; - private readonly ToCreateSubfolder _toCreateSubfolder; - private readonly IThumbnailService _thumbnailService; - private readonly IWebLogger _logger; - - [SuppressMessage("Usage", "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] - public WebHtmlPublishService(IPublishPreflight publishPreflight, ISelectorStorage - selectorStorage, AppSettings appSettings, IExifToolHostStorage exifTool, - IOverlayImage overlayImage, IConsole console, IWebLogger logger, IThumbnailService thumbnailService) - { - _publishPreflight = publishPreflight; - _subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); - _hostFileSystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); - _appSettings = appSettings; - _exifTool = exifTool; - _console = console; - _overlayImage = overlayImage; - _publishManifest = new PublishManifest(_hostFileSystemStorage); - _toCreateSubfolder = new ToCreateSubfolder(_hostFileSystemStorage); - _copyPublishedContent = new CopyPublishedContent(_toCreateSubfolder, - selectorStorage); - _logger = logger; - _thumbnailService = thumbnailService; - } - - public async Task> RenderCopy(List fileIndexItemsList, - string publishProfileName, string itemName, string outputParentFullFilePathFolder, - bool moveSourceFiles = false) - { - fileIndexItemsList = AddFileHashIfNotExist(fileIndexItemsList); - - await PreGenerateThumbnail(fileIndexItemsList, publishProfileName); - var base64ImageArray = await Base64DataUriList(fileIndexItemsList); - - var copyContent = await Render(fileIndexItemsList, base64ImageArray, - publishProfileName, itemName, outputParentFullFilePathFolder, moveSourceFiles); - - _publishManifest.ExportManifest(outputParentFullFilePathFolder, itemName, copyContent); - - return copyContent; - } - - internal List AddFileHashIfNotExist(List fileIndexItemsList) - { - foreach ( var item in fileIndexItemsList.Where(item => string.IsNullOrEmpty(item.FileHash)) ) - { - item.FileHash = new FileHash(_subPathStorage).GetHashCode(item.FilePath).Key; - } - return fileIndexItemsList; - } - - internal bool ShouldSkipExtraLarge(string publishProfileName) - { - var skipExtraLarge = _publishPreflight?.GetPublishProfileName(publishProfileName)? - .TrueForAll(p => p.SourceMaxWidth <= 1999); - return skipExtraLarge == true; - - } - - internal async Task PreGenerateThumbnail(IEnumerable fileIndexItemsList, string publishProfileName) - { - var skipExtraLarge = ShouldSkipExtraLarge(publishProfileName); - foreach ( var item in fileIndexItemsList ) - { - await _thumbnailService.CreateThumbAsync(item.FilePath, item.FileHash, skipExtraLarge); - } - } - - /// - /// Get base64 uri lists - /// - /// - private Task Base64DataUriList(IEnumerable fileIndexItemsList) - { - return new ToBase64DataUriList(_subPathStorage, - _thumbnailStorage,_logger, _appSettings).Create(fileIndexItemsList.ToList()); - } - - /// - /// Render output of Publish action - /// - /// which items need to be published - /// list of base64 hashes for html pages - /// name of profile - /// output publish item name - /// path on host disk where to publish to - /// include source files (false by default) - /// - private async Task> Render(List fileIndexItemsList, - string[] base64ImageArray, string publishProfileName, string itemName, - string outputParentFullFilePathFolder, bool moveSourceFiles = false) - { - if ( _appSettings.PublishProfiles.Count == 0 ) - { - _console.WriteLine("There are no config items"); - return null; - } - - if ( !_appSettings.PublishProfiles.ContainsKey(publishProfileName) ) - { - _console.WriteLine("Key not found"); - return null; - } - - if ( !_hostFileSystemStorage.ExistFolder(outputParentFullFilePathFolder) ) - { - _hostFileSystemStorage.CreateDirectory(outputParentFullFilePathFolder); - } - - base64ImageArray ??= new string[fileIndexItemsList.Count]; - - // Order alphabetically - // Ignore Items with Errors - fileIndexItemsList = fileIndexItemsList - .Where(p=> p.Status == FileIndexItem.ExifStatus.Ok || - p.Status == FileIndexItem.ExifStatus.ReadOnly) - .OrderBy(p => p.FileName).ToList(); - - var copyResult = new Dictionary(); - - var profiles = _publishPreflight.GetPublishProfileName(publishProfileName); - foreach (var currentProfile in profiles) - { - switch (currentProfile.ContentType) - { - case TemplateContentType.Html: - copyResult.AddRangeOverride(await GenerateWebHtml(profiles, currentProfile, itemName, - base64ImageArray, fileIndexItemsList, outputParentFullFilePathFolder)); - break; - case TemplateContentType.Jpeg: - copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, fileIndexItemsList, - outputParentFullFilePathFolder)); - break; - case TemplateContentType.MoveSourceFiles: - copyResult.AddRangeOverride(await GenerateMoveSourceFiles(currentProfile,fileIndexItemsList, - outputParentFullFilePathFolder, moveSourceFiles)); - break; - case TemplateContentType.PublishContent: - // Copy all items in the subFolder content for example JavaScripts - copyResult.AddRangeOverride(_copyPublishedContent.CopyContent(currentProfile, outputParentFullFilePathFolder)); - break; - case TemplateContentType.PublishManifest: - copyResult.Add( - _overlayImage.FilePathOverlayImage("_settings.json", currentProfile) - ,true); - break; - case TemplateContentType.OnlyFirstJpeg: - if ( fileIndexItemsList.Count == 0 ) - { - break; - } - var firstInList = new List{fileIndexItemsList.FirstOrDefault()}; - copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, firstInList, - outputParentFullFilePathFolder)); - break; - } - } - return copyResult; - } - - internal async Task> GenerateWebHtml(List profiles, - AppSettingsPublishProfiles currentProfile, string itemName, string[] base64ImageArray, - IEnumerable fileIndexItemsList, string outputParentFullFilePathFolder) - { - if ( string.IsNullOrEmpty(currentProfile.Template) ) - { - _console.WriteLine("CurrentProfile Template not configured"); - return new Dictionary(); - } - - // Generates html by razorLight - var viewModel = new WebHtmlViewModel - { - ItemName = itemName, - Profiles = profiles, - AppSettings = _appSettings, - CurrentProfile = currentProfile, - Base64ImageArray = base64ImageArray, - // apply slug to items, but use it only in the copy - FileIndexItems = fileIndexItemsList.Select(c => c.Clone()).ToList(), - }; - - // add to IClonable - foreach (var item in viewModel.FileIndexItems) - { - item.FileName = GenerateSlugHelper.GenerateSlug(item.FileCollectionName!, true) + - Path.GetExtension(item.FileName); - } - - // has a direct dependency on the filesystem - var embeddedResult = await new ParseRazor(_hostFileSystemStorage, _logger) - .EmbeddedViews(currentProfile.Template, viewModel); - - var stream = StringToStreamHelper.StringToStream(embeddedResult); - await _hostFileSystemStorage.WriteStreamAsync(stream, - Path.Combine(outputParentFullFilePathFolder, currentProfile.Path)); - - _console.Write(_appSettings.IsVerbose() ? embeddedResult +"\n" : "•"); - - return new Dictionary - { - { - currentProfile.Path.Replace(outputParentFullFilePathFolder, string.Empty), - currentProfile.Copy - } - }; - } - - /// - /// Generate loop of Jpeg images with overlay image - /// With Retry included - /// - /// contains sizes - /// list of items to generate jpeg for - /// outputParentFullFilePathFolder - /// when failed output, has default value - /// - internal async Task> GenerateJpeg(AppSettingsPublishProfiles profile, - IReadOnlyCollection fileIndexItemsList, string outputParentFullFilePathFolder, int delay = 6) - { - _toCreateSubfolder.Create(profile,outputParentFullFilePathFolder); - - foreach (var item in fileIndexItemsList) - { - var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, - item.FilePath, profile); - - async Task ResizerLocal() - { - return await Resizer(outputPath, profile, item); - } - - try - { - await RetryHelper.DoAsync(ResizerLocal, TimeSpan.FromSeconds(delay)); - } - catch ( AggregateException e ) - { - _logger.LogError($"[ResizerLocal] Skip due errors: (catch-ed exception) {item.FilePath} {item.FileHash}"); - foreach ( var exception in e.InnerExceptions ) - { - _logger.LogError("[ResizerLocal] " + exception.Message, exception); - } - } - } - - return fileIndexItemsList.ToDictionary(item => - _overlayImage.FilePathOverlayImage(item.FilePath, profile), - item => profile.Copy); - } - - /// - /// Resize image with overlay - /// - /// absolute path of output on host disk - /// size of output, overlay size, must contain metaData - /// database item with filePath - /// true when success - /// when output is not valid - private async Task Resizer(string outputPath, AppSettingsPublishProfiles profile, - FileIndexItem item) - { - // for less than 1000px - if (profile.SourceMaxWidth <= 1000 && _thumbnailStorage.ExistFile(ThumbnailNameHelper. - Combine(item.FileHash, ThumbnailSize.Large))) - { - await _overlayImage.ResizeOverlayImageThumbnails(item.FileHash, outputPath, profile); - } - else if ( profile.SourceMaxWidth <= 2000 && _thumbnailStorage.ExistFile(ThumbnailNameHelper. - Combine(item.FileHash, ThumbnailSize.ExtraLarge)) ) - { - await _overlayImage.ResizeOverlayImageThumbnails( - ThumbnailNameHelper.Combine(item.FileHash, ThumbnailSize.ExtraLarge), outputPath, profile); - } - else if ( _subPathStorage.ExistFile(item.FilePath)) - { - // Thumbs are 2000 px (and larger) - await _overlayImage.ResizeOverlayImageLarge(item.FilePath, outputPath, profile); - } - - if ( profile.MetaData ) - { - await MetaData(item, outputPath); - } - - var imageFormat = ExtensionRolesHelper.GetImageFormat(_hostFileSystemStorage.ReadStream(outputPath,160)); - if ( imageFormat == ExtensionRolesHelper.ImageFormat.jpg ) - return true; - - _hostFileSystemStorage.FileDelete(outputPath); - - throw new DecodingException("[WebHtmlPublishService] image output is not valid"); - } - - /// - /// Copy the metaData over the output path - /// - /// all the meta data - /// absolute path on host disk - private async Task MetaData(FileIndexItem item, string outputPath) - { - if ( !_subPathStorage.ExistFile(item.FilePath) ) return; - - // Write the metadata to the new created file - var comparedNames = FileIndexCompareHelper.Compare( - new FileIndexItem(), item); - - // Output has already rotated the image - var rotation = nameof(FileIndexItem.Orientation).ToLowerInvariant(); - - // should already check if it exists + public class WebHtmlPublishService : IWebHtmlPublishService + { + private readonly AppSettings _appSettings; + private readonly IExifTool _exifTool; + private readonly IStorage _subPathStorage; + private readonly IStorage _thumbnailStorage; + private readonly IStorage _hostFileSystemStorage; + private readonly IConsole _console; + private readonly IOverlayImage _overlayImage; + private readonly PublishManifest _publishManifest; + private readonly IPublishPreflight _publishPreflight; + private readonly CopyPublishedContent _copyPublishedContent; + private readonly ToCreateSubfolder _toCreateSubfolder; + private readonly IThumbnailService _thumbnailService; + private readonly IWebLogger _logger; + + [SuppressMessage("Usage", + "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] + public WebHtmlPublishService(IPublishPreflight publishPreflight, ISelectorStorage + selectorStorage, AppSettings appSettings, IExifToolHostStorage exifTool, + IOverlayImage overlayImage, IConsole console, IWebLogger logger, + IThumbnailService thumbnailService) + { + _publishPreflight = publishPreflight; + _subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + _hostFileSystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _appSettings = appSettings; + _exifTool = exifTool; + _console = console; + _overlayImage = overlayImage; + _publishManifest = new PublishManifest(_hostFileSystemStorage); + _toCreateSubfolder = new ToCreateSubfolder(_hostFileSystemStorage); + _copyPublishedContent = new CopyPublishedContent(_toCreateSubfolder, + selectorStorage); + _logger = logger; + _thumbnailService = thumbnailService; + } + + public async Task?> RenderCopy( + List fileIndexItemsList, + string publishProfileName, string itemName, string outputParentFullFilePathFolder, + bool moveSourceFiles = false) + { + fileIndexItemsList = AddFileHashIfNotExist(fileIndexItemsList); + + await PreGenerateThumbnail(fileIndexItemsList, publishProfileName); + var base64ImageArray = await Base64DataUriList(fileIndexItemsList); + + var copyContent = await Render(fileIndexItemsList, base64ImageArray, + publishProfileName, itemName, outputParentFullFilePathFolder, moveSourceFiles); + + _publishManifest.ExportManifest(outputParentFullFilePathFolder, itemName, copyContent); + + return copyContent; + } + + internal List AddFileHashIfNotExist(List fileIndexItemsList) + { + foreach ( var item in fileIndexItemsList.Where(item => + string.IsNullOrEmpty(item.FileHash)) ) + { + item.FileHash = new FileHash(_subPathStorage).GetHashCode(item.FilePath!).Key; + } + + return fileIndexItemsList; + } + + internal bool ShouldSkipExtraLarge(string publishProfileName) + { + var skipExtraLarge = _publishPreflight.GetPublishProfileName(publishProfileName) + .TrueForAll(p => p.SourceMaxWidth <= 1999); + return skipExtraLarge; + } + + internal async Task PreGenerateThumbnail(IEnumerable fileIndexItemsList, + string publishProfileName) + { + var skipExtraLarge = ShouldSkipExtraLarge(publishProfileName); + foreach ( var item in fileIndexItemsList ) + { + await _thumbnailService.CreateThumbAsync(item.FilePath, item.FileHash!, + skipExtraLarge); + } + } + + /// + /// Get base64 uri lists + /// + /// + private Task Base64DataUriList(IEnumerable fileIndexItemsList) + { + return new ToBase64DataUriList(_subPathStorage, + _thumbnailStorage, _logger, _appSettings).Create(fileIndexItemsList.ToList()); + } + + /// + /// Render output of Publish action + /// + /// which items need to be published + /// list of base64 hashes for html pages + /// name of profile + /// output publish item name + /// path on host disk where to publish to + /// include source files (false by default) + /// + private async Task?> Render(List fileIndexItemsList, + string[]? base64ImageArray, string publishProfileName, string itemName, + string outputParentFullFilePathFolder, bool moveSourceFiles = false) + { + if ( _appSettings.PublishProfiles?.Count == 0 ) + { + _console.WriteLine("There are no config items"); + return null; + } + + if ( _appSettings.PublishProfiles?.ContainsKey(publishProfileName) == false ) + { + _console.WriteLine("Key not found"); + return null; + } + + if ( !_hostFileSystemStorage.ExistFolder(outputParentFullFilePathFolder) ) + { + _hostFileSystemStorage.CreateDirectory(outputParentFullFilePathFolder); + } + + base64ImageArray ??= new string[fileIndexItemsList.Count]; + + // Order alphabetically + // Ignore Items with Errors + fileIndexItemsList = fileIndexItemsList + .Where(p => p.Status is FileIndexItem.ExifStatus.Ok + or FileIndexItem.ExifStatus.ReadOnly) + .OrderBy(p => p.FileName).ToList(); + + var copyResult = new Dictionary(); + + var profiles = _publishPreflight.GetPublishProfileName(publishProfileName); + foreach ( var currentProfile in profiles ) + { + switch ( currentProfile.ContentType ) + { + case TemplateContentType.Html: + copyResult.AddRangeOverride(await GenerateWebHtml(profiles, currentProfile, + itemName, + base64ImageArray, fileIndexItemsList, outputParentFullFilePathFolder)); + break; + case TemplateContentType.Jpeg: + copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, + fileIndexItemsList, + outputParentFullFilePathFolder)); + break; + case TemplateContentType.MoveSourceFiles: + copyResult.AddRangeOverride(await GenerateMoveSourceFiles(currentProfile, + fileIndexItemsList, + outputParentFullFilePathFolder, moveSourceFiles)); + break; + case TemplateContentType.PublishContent: + // Copy all items in the subFolder content for example JavaScripts + copyResult.AddRangeOverride( + _copyPublishedContent.CopyContent(currentProfile, + outputParentFullFilePathFolder)); + break; + case TemplateContentType.PublishManifest: + copyResult.Add( + _overlayImage.FilePathOverlayImage("_settings.json", currentProfile) + , true); + break; + case TemplateContentType.OnlyFirstJpeg: + var item = fileIndexItemsList.FirstOrDefault(); + if ( item == null ) + { + break; + } + + var firstInList = new List { item }; + copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, firstInList, + outputParentFullFilePathFolder)); + break; + } + } + + return copyResult; + } + + internal async Task> GenerateWebHtml( + List profiles, + AppSettingsPublishProfiles currentProfile, string itemName, string[] base64ImageArray, + IEnumerable fileIndexItemsList, string outputParentFullFilePathFolder) + { + if ( string.IsNullOrEmpty(currentProfile.Template) ) + { + _console.WriteLine("CurrentProfile Template not configured"); + return new Dictionary(); + } + + // Generates html by razorLight + var viewModel = new WebHtmlViewModel + { + ItemName = itemName, + Profiles = profiles, + AppSettings = _appSettings, + CurrentProfile = currentProfile, + Base64ImageArray = base64ImageArray, + // apply slug to items, but use it only in the copy + FileIndexItems = fileIndexItemsList.Select(c => c.Clone()).ToList(), + }; + + // add to IClonable + foreach ( var item in viewModel.FileIndexItems ) + { + item.FileName = GenerateSlugHelper.GenerateSlug(item.FileCollectionName!, true) + + Path.GetExtension(item.FileName); + } + + // has a direct dependency on the filesystem + var embeddedResult = await new ParseRazor(_hostFileSystemStorage, _logger) + .EmbeddedViews(currentProfile.Template, viewModel); + + var stream = StringToStreamHelper.StringToStream(embeddedResult); + await _hostFileSystemStorage.WriteStreamAsync(stream, + Path.Combine(outputParentFullFilePathFolder, currentProfile.Path)); + + _console.Write(_appSettings.IsVerbose() ? embeddedResult + "\n" : "•"); + + return new Dictionary + { + { + currentProfile.Path.Replace(outputParentFullFilePathFolder, string.Empty), + currentProfile.Copy + } + }; + } + + /// + /// Generate loop of Jpeg images with overlay image + /// With Retry included + /// + /// contains sizes + /// list of items to generate jpeg for + /// outputParentFullFilePathFolder + /// when failed output, has default value + /// + internal async Task> GenerateJpeg( + AppSettingsPublishProfiles profile, + IReadOnlyCollection fileIndexItemsList, + string outputParentFullFilePathFolder, int delay = 6) + { + _toCreateSubfolder.Create(profile, outputParentFullFilePathFolder); + + foreach ( var item in fileIndexItemsList ) + { + var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, + item.FilePath!, profile); + + async Task ResizerLocal() + { + return await Resizer(outputPath, profile, item); + } + + try + { + await RetryHelper.DoAsync(ResizerLocal, TimeSpan.FromSeconds(delay)); + } + catch ( AggregateException e ) + { + _logger.LogError( + $"[ResizerLocal] Skip due errors: (catch-ed exception) {item.FilePath} {item.FileHash}"); + foreach ( var exception in e.InnerExceptions ) + { + _logger.LogError("[ResizerLocal] " + exception.Message, exception); + } + } + } + + return fileIndexItemsList.ToDictionary(item => + _overlayImage.FilePathOverlayImage(item.FilePath!, profile), + _ => profile.Copy); + } + + /// + /// Resize image with overlay + /// + /// absolute path of output on host disk + /// size of output, overlay size, must contain metaData + /// database item with filePath + /// true when success + /// when output is not valid + private async Task Resizer(string outputPath, AppSettingsPublishProfiles profile, + FileIndexItem item) + { + // for less than 1000px + if ( profile.SourceMaxWidth <= 1000 && + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.Large)) ) + { + await _overlayImage.ResizeOverlayImageThumbnails(item.FileHash!, outputPath, + profile); + } + else if ( profile.SourceMaxWidth <= 2000 && + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.ExtraLarge)) ) + { + await _overlayImage.ResizeOverlayImageThumbnails( + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.ExtraLarge), + outputPath, profile); + } + else if ( _subPathStorage.ExistFile(item.FilePath!) ) + { + // Thumbs are 2000 px (and larger) + await _overlayImage.ResizeOverlayImageLarge(item.FilePath!, outputPath, profile); + } + + if ( profile.MetaData ) + { + await MetaData(item, outputPath); + } + + var imageFormat = + ExtensionRolesHelper.GetImageFormat( + _hostFileSystemStorage.ReadStream(outputPath, 160)); + if ( imageFormat == ExtensionRolesHelper.ImageFormat.jpg ) + return true; + + _hostFileSystemStorage.FileDelete(outputPath); + + throw new DecodingException("[WebHtmlPublishService] image output is not valid"); + } + + /// + /// Copy the metaData over the output path + /// + /// all the meta data + /// absolute path on host disk + private async Task MetaData(FileIndexItem item, string outputPath) + { + if ( !_subPathStorage.ExistFile(item.FilePath!) ) return; + + // Write the metadata to the new created file + var comparedNames = FileIndexCompareHelper.Compare( + new FileIndexItem(), item); + + // Output has already rotated the image + var rotation = nameof(FileIndexItem.Orientation).ToLowerInvariant(); + + // should already check if it exists comparedNames.Remove(rotation); // Write it back await new ExifToolCmdHelper(_exifTool, _hostFileSystemStorage, - _thumbnailStorage, null,null).UpdateAsync(item, - new List {outputPath}, comparedNames, - false, false); - } - - internal async Task> GenerateMoveSourceFiles( - AppSettingsPublishProfiles profile, IReadOnlyCollection fileIndexItemsList, - string outputParentFullFilePathFolder, bool moveSourceFiles) - { - _toCreateSubfolder.Create(profile,outputParentFullFilePathFolder); - - foreach (var subPath in fileIndexItemsList.Select(p => p.FilePath)) - { - // input: item.FilePath - var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, - subPath, profile); - - await _hostFileSystemStorage.WriteStreamAsync(_subPathStorage.ReadStream(subPath), - outputPath); - - // only delete when using in cli mode - if ( moveSourceFiles ) - { - _subPathStorage.FileDelete(subPath); - } - } - return fileIndexItemsList.ToDictionary(item => - _overlayImage.FilePathOverlayImage(item.FilePath, profile), - item => profile.Copy); - } - - /// - /// Generate Zip on Host - /// - /// One folder deeper than where the folder - /// blog item name - /// - /// - public async Task GenerateZip(string fullFileParentFolderPath, string itemName, - Dictionary renderCopyResult, bool deleteFolderAfterwards = false) - { - var fileNames = renderCopyResult.Where(p => p.Value).Select(p => p.Key).ToList(); - - var slugItemName = GenerateSlugHelper.GenerateSlug(itemName, true); - var filePaths = fileNames.Select(p => Path.Combine(fullFileParentFolderPath, slugItemName, p)).ToList(); - - new Zipper().CreateZip(fullFileParentFolderPath, filePaths, fileNames, - slugItemName); - - // Write a single file to be sure that writing is ready - var doneFileFullPath = Path.Combine(_appSettings.TempFolder, slugItemName) + ".done"; - await _hostFileSystemStorage. - WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), doneFileFullPath); - - if ( deleteFolderAfterwards ) - { - _hostFileSystemStorage.FolderDelete(Path.Combine(_appSettings.TempFolder, - slugItemName)); - } - } - } + _thumbnailStorage, null!, null!).UpdateAsync(item, + new List { outputPath }, comparedNames, + false, false); + } + + internal async Task> GenerateMoveSourceFiles( + AppSettingsPublishProfiles profile, + IReadOnlyCollection fileIndexItemsList, + string outputParentFullFilePathFolder, bool moveSourceFiles) + { + _toCreateSubfolder.Create(profile, outputParentFullFilePathFolder); + + foreach ( var subPath in fileIndexItemsList.Select(p => p.FilePath!) ) + { + // input: item.FilePath + var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, + subPath, profile); + + await _hostFileSystemStorage.WriteStreamAsync(_subPathStorage.ReadStream(subPath), + outputPath); + + // only delete when using in cli mode + if ( moveSourceFiles ) + { + _subPathStorage.FileDelete(subPath); + } + } + + return fileIndexItemsList.ToDictionary(item => + _overlayImage.FilePathOverlayImage(item.FilePath!, profile), + _ => profile.Copy); + } + + /// + /// Generate Zip on Host + /// + /// One folder deeper than where the folder + /// blog item name + /// + /// + public async Task GenerateZip(string fullFileParentFolderPath, string itemName, + Dictionary? renderCopyResult, + bool deleteFolderAfterwards = false) + { + ArgumentNullException.ThrowIfNull(renderCopyResult); + + var fileNames = renderCopyResult.Where(p => p.Value).Select(p => p.Key).ToList(); + + var slugItemName = GenerateSlugHelper.GenerateSlug(itemName, true); + var filePaths = fileNames + .Select(p => Path.Combine(fullFileParentFolderPath, slugItemName, p)).ToList(); + + new Zipper().CreateZip(fullFileParentFolderPath, filePaths, fileNames, + slugItemName); + + // Write a single file to be sure that writing is ready + var doneFileFullPath = Path.Combine(_appSettings.TempFolder, slugItemName) + ".done"; + await _hostFileSystemStorage.WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), + doneFileFullPath); + + if ( deleteFolderAfterwards ) + { + _hostFileSystemStorage.FolderDelete(Path.Combine(_appSettings.TempFolder, + slugItemName)); + } + } + } } diff --git a/starsky/starsky.feature.webhtmlpublish/ViewModels/WebHtmlViewModel.cs b/starsky/starsky.feature.webhtmlpublish/ViewModels/WebHtmlViewModel.cs index 566e29adbf..70c0717fdc 100644 --- a/starsky/starsky.feature.webhtmlpublish/ViewModels/WebHtmlViewModel.cs +++ b/starsky/starsky.feature.webhtmlpublish/ViewModels/WebHtmlViewModel.cs @@ -1,29 +1,29 @@ -using System.Collections.Generic; +using System.Collections.Generic; using starsky.foundation.database.Models; using starsky.foundation.platform.Models; -namespace starsky.feature.webhtmlpublish.ViewModels +namespace starsky.feature.webhtmlpublish.ViewModels; + +public class WebHtmlViewModel { - public class WebHtmlViewModel - { - public string ItemName { get; set; } + public string ItemName { get; set; } = string.Empty; + + /// + /// Used with helpers + /// + public AppSettings AppSettings { get; set; } = new(); + + /// + /// Current profile + /// + public AppSettingsPublishProfiles CurrentProfile { get; set; } = new(); + + /// + /// Other profiles within the same group + /// + public List Profiles { get; set; } = []; - /// - /// Used with helpers - /// - public AppSettings AppSettings { get; set; } - - /// - /// Current profile - /// - public AppSettingsPublishProfiles CurrentProfile { get; set; } - - /// - /// Other profiles within the same group - /// - public List Profiles { get; set; } + public string[] Base64ImageArray { get; set; } = []; - public string[] Base64ImageArray { get; set; } - public List FileIndexItems { get; set; } - } + public List FileIndexItems { get; set; } = []; } diff --git a/starsky/starsky.feature.webhtmlpublish/starsky.feature.webhtmlpublish.csproj b/starsky/starsky.feature.webhtmlpublish/starsky.feature.webhtmlpublish.csproj index cea89b3f9f..9a6da4a4f6 100644 --- a/starsky/starsky.feature.webhtmlpublish/starsky.feature.webhtmlpublish.csproj +++ b/starsky/starsky.feature.webhtmlpublish/starsky.feature.webhtmlpublish.csproj @@ -2,11 +2,12 @@ net8.0 - 8.0 + 12.0 {7f7fe502-31a8-409b-bd0b-92d7d1bfeb31} Full 0.6.0-beta.0 + enable @@ -14,25 +15,25 @@ true - + - - - + + + - + - - - + + + - + - - - - - + + + + + diff --git a/starsky/starsky.foundation.accountmanagement/Extensions/UseBasicAuthenticationExtensions.cs b/starsky/starsky.foundation.accountmanagement/Extensions/UseBasicAuthenticationExtensions.cs index e04aa74146..6cf55a6935 100644 --- a/starsky/starsky.foundation.accountmanagement/Extensions/UseBasicAuthenticationExtensions.cs +++ b/starsky/starsky.foundation.accountmanagement/Extensions/UseBasicAuthenticationExtensions.cs @@ -1,14 +1,14 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using starsky.foundation.accountmanagement.Middleware; // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Extensions { - public static class UseBasicAuthenticationExtensions - { - public static IApplicationBuilder UseBasicAuthentication(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } + public static class UseBasicAuthenticationExtensions + { + public static IApplicationBuilder UseBasicAuthentication(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Extensions/UseCheckIfAccountExistMiddlewareExtensions.cs b/starsky/starsky.foundation.accountmanagement/Extensions/UseCheckIfAccountExistMiddlewareExtensions.cs index 2393915f30..8305bd4dd4 100644 --- a/starsky/starsky.foundation.accountmanagement/Extensions/UseCheckIfAccountExistMiddlewareExtensions.cs +++ b/starsky/starsky.foundation.accountmanagement/Extensions/UseCheckIfAccountExistMiddlewareExtensions.cs @@ -1,14 +1,14 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using starsky.foundation.accountmanagement.Middleware; // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Extensions { - public static class UseCheckIfAccountExistMiddlewareExtensions - { - public static IApplicationBuilder UseCheckIfAccountExist(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } + public static class UseCheckIfAccountExistMiddlewareExtensions + { + public static IApplicationBuilder UseCheckIfAccountExist(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Extensions/UseNoAccountLocalhostExtensions.cs b/starsky/starsky.foundation.accountmanagement/Extensions/UseNoAccountLocalhostExtensions.cs index b4987c1c2a..a769829b14 100644 --- a/starsky/starsky.foundation.accountmanagement/Extensions/UseNoAccountLocalhostExtensions.cs +++ b/starsky/starsky.foundation.accountmanagement/Extensions/UseNoAccountLocalhostExtensions.cs @@ -1,14 +1,14 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using starsky.foundation.accountmanagement.Middleware; // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Extensions { - public static class UseNoAccountLocalhostExtensions - { - public static IApplicationBuilder UseNoAccount(this IApplicationBuilder builder, bool enable) - { - return !enable ? builder : builder.UseMiddleware(); - } - } + public static class UseNoAccountLocalhostExtensions + { + public static IApplicationBuilder UseNoAccount(this IApplicationBuilder builder, bool enable) + { + return !enable ? builder : builder.UseMiddleware(); + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Helpers/IsLocalhost.cs b/starsky/starsky.foundation.accountmanagement/Helpers/IsLocalhost.cs index 697819c383..feb7420917 100644 --- a/starsky/starsky.foundation.accountmanagement/Helpers/IsLocalhost.cs +++ b/starsky/starsky.foundation.accountmanagement/Helpers/IsLocalhost.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Net; namespace starsky.foundation.accountmanagement.Helpers @@ -12,15 +11,17 @@ public static class IsLocalhost /// context.Connection.LocalIpAddress /// /// - public static bool IsHostLocalHost(IPAddress? connectionLocalIpAddress, IPAddress? connectionRemoteIpAddress) + public static bool IsHostLocalHost(IPAddress? connectionLocalIpAddress, + IPAddress? connectionRemoteIpAddress) { if ( connectionLocalIpAddress == null || - connectionRemoteIpAddress == null ) + connectionRemoteIpAddress == null ) { return false; } - - return connectionRemoteIpAddress.Equals(connectionLocalIpAddress) || IPAddress.IsLoopback(connectionRemoteIpAddress); + + return connectionRemoteIpAddress.Equals(connectionLocalIpAddress) || + IPAddress.IsLoopback(connectionRemoteIpAddress); } } } diff --git a/starsky/starsky.foundation.accountmanagement/Helpers/Pbkdf2Hasher.cs b/starsky/starsky.foundation.accountmanagement/Helpers/Pbkdf2Hasher.cs index 78b0d1539a..be57fb95f3 100644 --- a/starsky/starsky.foundation.accountmanagement/Helpers/Pbkdf2Hasher.cs +++ b/starsky/starsky.foundation.accountmanagement/Helpers/Pbkdf2Hasher.cs @@ -1,4 +1,4 @@ -// Copyright © 2018 Dmitry Sikorsky. All rights reserved. +// Copyright © 2018 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -7,39 +7,39 @@ namespace starsky.foundation.accountmanagement.Helpers { - public static class Pbkdf2Hasher - { - /// - /// Get secured hash passwords based on a salt - /// - /// password - /// to decrypt - /// hased password - public static string ComputeHash(string password, byte[] salt) - { - return Convert.ToBase64String( - KeyDerivation.Pbkdf2( - password: password, - salt: salt, - prf: KeyDerivationPrf.HMACSHA1, - iterationCount: 10000, - numBytesRequested: 256 / 8 - ) - ); - } + public static class Pbkdf2Hasher + { + /// + /// Get secured hash passwords based on a salt + /// + /// password + /// to decrypt + /// hased password + public static string ComputeHash(string password, byte[] salt) + { + return Convert.ToBase64String( + KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA1, + iterationCount: 10000, + numBytesRequested: 256 / 8 + ) + ); + } - /// - /// Generate a random salt - /// - /// random salt - public static byte[] GenerateRandomSalt() - { - byte[] salt = new byte[128 / 8]; + /// + /// Generate a random salt + /// + /// random salt + public static byte[] GenerateRandomSalt() + { + byte[] salt = new byte[128 / 8]; - using (var rng = RandomNumberGenerator.Create()) - rng.GetBytes(salt); + using ( var rng = RandomNumberGenerator.Create() ) + rng.GetBytes(salt); - return salt; - } - } + return salt; + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs b/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs index 99f7e4623a..dc6477e72c 100644 --- a/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs +++ b/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs @@ -1,94 +1,94 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using starsky.foundation.accountmanagement.Models; using starsky.foundation.database.Models.Account; namespace starsky.foundation.accountmanagement.Interfaces { - public enum SignUpResultError - { - CredentialTypeNotFound, - NullString - } - - public class SignUpResult - { - public User? User { get; } - public bool Success { get; private set; } - public SignUpResultError? Error { get; } - - public SignUpResult(User? user = null, bool success = false, SignUpResultError? error = null) - { - User = user; - Success = success; - Error = error; - } - } - - public enum ValidateResultError - { - CredentialTypeNotFound, - CredentialNotFound, - SecretNotValid, - Lockout, - UserNotFound - } + public enum SignUpResultError + { + CredentialTypeNotFound, + NullString + } - public enum ChangeSecretResultError - { - CredentialTypeNotFound, - CredentialNotFound - } + public class SignUpResult + { + public User? User { get; } + public bool Success { get; private set; } + public SignUpResultError? Error { get; } - public class ChangeSecretResult - { - public bool Success { get; set; } + public SignUpResult(User? user = null, bool success = false, SignUpResultError? error = null) + { + User = user; + Success = success; + Error = error; + } + } - public ChangeSecretResultError? Error { get; set; } - - public ChangeSecretResult(bool success = false, ChangeSecretResultError? error = null) - { - Success = success; - Error = error; - } - } + public enum ValidateResultError + { + CredentialTypeNotFound, + CredentialNotFound, + SecretNotValid, + Lockout, + UserNotFound + } - public interface IUserManager - { - Task AllUsersAsync(); - - /// - /// Add a new user, including Roles and UserRoles - /// - /// Nice Name, default string.Emthy - /// default is: Email - /// an email address, e.g. dont@mail.us - /// Password - /// result object - Task SignUpAsync(string name, string credentialTypeCode, - string? identifier, string? secret); - - void AddToRole(User user, string roleCode); - void AddToRole(User user, Role role); - void RemoveFromRole(User user, string roleCode); - void RemoveFromRole(User user, Role role); - ChangeSecretResult ChangeSecret(string credentialTypeCode, string? identifier, string secret); - Task ValidateAsync(string credentialTypeCode, - string? identifier, string secret); + public enum ChangeSecretResultError + { + CredentialTypeNotFound, + CredentialNotFound + } - Task SignIn(HttpContext httpContext, User? user, - bool isPersistent = false); - void SignOut(HttpContext httpContext); - int GetCurrentUserId(HttpContext httpContext); - User? GetCurrentUser(HttpContext httpContext); - User? GetUser(string credentialTypeCode, string identifier); - Credential? GetCredentialsByUserId(int userId); - Task RemoveUser(string credentialTypeCode, - string identifier); - User? Exist(string identifier); + public class ChangeSecretResult + { + public bool Success { get; set; } - Task ExistAsync(int userTableId); - Role? GetRole(string credentialTypeCode, string identifier); - bool PreflightValidate(string userName, string password, string confirmPassword); - } + public ChangeSecretResultError? Error { get; set; } + + public ChangeSecretResult(bool success = false, ChangeSecretResultError? error = null) + { + Success = success; + Error = error; + } + } + + public interface IUserManager + { + Task AllUsersAsync(); + + /// + /// Add a new user, including Roles and UserRoles + /// + /// Nice Name, default string.Emthy + /// default is: Email + /// an email address, e.g. dont@mail.us + /// Password + /// result object + Task SignUpAsync(string name, string credentialTypeCode, + string? identifier, string? secret); + + void AddToRole(User user, string roleCode); + void AddToRole(User user, Role role); + void RemoveFromRole(User user, string roleCode); + void RemoveFromRole(User user, Role role); + ChangeSecretResult ChangeSecret(string credentialTypeCode, string? identifier, string secret); + Task ValidateAsync(string credentialTypeCode, + string? identifier, string secret); + + Task SignIn(HttpContext httpContext, User? user, + bool isPersistent = false); + void SignOut(HttpContext httpContext); + int GetCurrentUserId(HttpContext httpContext); + User? GetCurrentUser(HttpContext httpContext); + User? GetUser(string credentialTypeCode, string identifier); + Credential? GetCredentialsByUserId(int userId); + Task RemoveUser(string credentialTypeCode, + string identifier); + User? Exist(string identifier); + + Task ExistAsync(int userTableId); + Role? GetRole(string credentialTypeCode, string identifier); + bool PreflightValidate(string userName, string password, string confirmPassword); + } } diff --git a/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationHeaderValue.cs b/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationHeaderValue.cs index af07c2d03d..48ff3a1579 100644 --- a/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationHeaderValue.cs +++ b/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationHeaderValue.cs @@ -1,60 +1,60 @@ -using System; +using System; // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Middleware { - public sealed class BasicAuthenticationHeaderValue - { - public BasicAuthenticationHeaderValue(string? authenticationHeaderValue) - { - if (!string.IsNullOrWhiteSpace(authenticationHeaderValue)) - { - _authenticationHeaderValue = authenticationHeaderValue; - if (TryDecodeHeaderValue()) - { - ReadAuthenticationHeaderValue(); - } - } - } + public sealed class BasicAuthenticationHeaderValue + { + public BasicAuthenticationHeaderValue(string? authenticationHeaderValue) + { + if ( !string.IsNullOrWhiteSpace(authenticationHeaderValue) ) + { + _authenticationHeaderValue = authenticationHeaderValue; + if ( TryDecodeHeaderValue() ) + { + ReadAuthenticationHeaderValue(); + } + } + } - private readonly string _authenticationHeaderValue = string.Empty; - private string[] _splitDecodedCredentials = Array.Empty(); + private readonly string _authenticationHeaderValue = string.Empty; + private string[] _splitDecodedCredentials = Array.Empty(); - public bool IsValidBasicAuthenticationHeaderValue { get; private set; } - public string UserIdentifier { get; private set; } = string.Empty; - public string UserPassword { get; private set; } = string.Empty; + public bool IsValidBasicAuthenticationHeaderValue { get; private set; } + public string UserIdentifier { get; private set; } = string.Empty; + public string UserPassword { get; private set; } = string.Empty; - private bool TryDecodeHeaderValue() - { - const int headerSchemeLength = 6; - // The Length of the word "Basic " - if (_authenticationHeaderValue.Length <= headerSchemeLength) - { - return false; - } - var encodedCredentials = _authenticationHeaderValue.Substring(headerSchemeLength); - try - { - var decodedCredentials = Convert.FromBase64String(encodedCredentials); - _splitDecodedCredentials = System.Text.Encoding.ASCII.GetString(decodedCredentials).Split(':'); - return true; - } - catch (FormatException) - { - return false; - } - } + private bool TryDecodeHeaderValue() + { + const int headerSchemeLength = 6; + // The Length of the word "Basic " + if ( _authenticationHeaderValue.Length <= headerSchemeLength ) + { + return false; + } + var encodedCredentials = _authenticationHeaderValue.Substring(headerSchemeLength); + try + { + var decodedCredentials = Convert.FromBase64String(encodedCredentials); + _splitDecodedCredentials = System.Text.Encoding.ASCII.GetString(decodedCredentials).Split(':'); + return true; + } + catch ( FormatException ) + { + return false; + } + } - private void ReadAuthenticationHeaderValue() - { - IsValidBasicAuthenticationHeaderValue = _splitDecodedCredentials.Length == 2 - && !string.IsNullOrWhiteSpace(_splitDecodedCredentials[0]) - && !string.IsNullOrWhiteSpace(_splitDecodedCredentials[1]); - if (IsValidBasicAuthenticationHeaderValue) - { - UserIdentifier = _splitDecodedCredentials[0]; - UserPassword = _splitDecodedCredentials[1]; - } - } - } + private void ReadAuthenticationHeaderValue() + { + IsValidBasicAuthenticationHeaderValue = _splitDecodedCredentials.Length == 2 + && !string.IsNullOrWhiteSpace(_splitDecodedCredentials[0]) + && !string.IsNullOrWhiteSpace(_splitDecodedCredentials[1]); + if ( IsValidBasicAuthenticationHeaderValue ) + { + UserIdentifier = _splitDecodedCredentials[0]; + UserPassword = _splitDecodedCredentials[1]; + } + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationMiddleware.cs b/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationMiddleware.cs index 347e213ef3..c879d8d6fe 100644 --- a/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationMiddleware.cs +++ b/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationMiddleware.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -8,43 +8,43 @@ // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Middleware { - /// - /// Accepts either username or email as user identifier for sign in with Http Basic authentication - /// - public sealed class BasicAuthenticationMiddleware - { - - public BasicAuthenticationMiddleware(RequestDelegate next) - { - _next = next; - } + /// + /// Accepts either username or email as user identifier for sign in with Http Basic authentication + /// + public sealed class BasicAuthenticationMiddleware + { - private readonly RequestDelegate _next; + public BasicAuthenticationMiddleware(RequestDelegate next) + { + _next = next; + } - public async Task Invoke(HttpContext context) - { - if (context.User.Identity?.IsAuthenticated == false) - { - var basicAuthenticationHeader = GetBasicAuthenticationHeaderValue(context); - if (basicAuthenticationHeader.IsValidBasicAuthenticationHeaderValue) - { - var userManager = (IUserManager) context.RequestServices.GetRequiredService(typeof(IUserManager)); - - var authenticationManager = new BasicAuthenticationSignInManager( - context, basicAuthenticationHeader, userManager); - await authenticationManager.TrySignInUser(); - } - } - await _next.Invoke(context); - } + private readonly RequestDelegate _next; - private static BasicAuthenticationHeaderValue GetBasicAuthenticationHeaderValue(HttpContext context) - { - var basicAuthenticationHeader = context.Request.Headers.Authorization.FirstOrDefault(header => - header!.StartsWith("Basic", StringComparison.OrdinalIgnoreCase)); - - var decodedHeader = new BasicAuthenticationHeaderValue(basicAuthenticationHeader); - return decodedHeader; - } - } + public async Task Invoke(HttpContext context) + { + if ( context.User.Identity?.IsAuthenticated == false ) + { + var basicAuthenticationHeader = GetBasicAuthenticationHeaderValue(context); + if ( basicAuthenticationHeader.IsValidBasicAuthenticationHeaderValue ) + { + var userManager = ( IUserManager )context.RequestServices.GetRequiredService(typeof(IUserManager)); + + var authenticationManager = new BasicAuthenticationSignInManager( + context, basicAuthenticationHeader, userManager); + await authenticationManager.TrySignInUser(); + } + } + await _next.Invoke(context); + } + + private static BasicAuthenticationHeaderValue GetBasicAuthenticationHeaderValue(HttpContext context) + { + var basicAuthenticationHeader = context.Request.Headers.Authorization.FirstOrDefault(header => + header!.StartsWith("Basic", StringComparison.OrdinalIgnoreCase)); + + var decodedHeader = new BasicAuthenticationHeaderValue(basicAuthenticationHeader); + return decodedHeader; + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationSignInManager.cs b/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationSignInManager.cs index f22889e1bd..dee236e1c3 100644 --- a/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationSignInManager.cs +++ b/starsky/starsky.foundation.accountmanagement/Middleware/BasicAuthenticationSignInManager.cs @@ -1,47 +1,47 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using starsky.foundation.accountmanagement.Interfaces; // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Middleware { - public sealed class BasicAuthenticationSignInManager - { - private readonly IUserManager _userManager; + public sealed class BasicAuthenticationSignInManager + { + private readonly IUserManager _userManager; - public BasicAuthenticationSignInManager( - HttpContext context, - BasicAuthenticationHeaderValue authenticationHeaderValue, - IUserManager userManager) - { - _context = context; - _authenticationHeaderValue = authenticationHeaderValue; - _userManager = userManager; - } - - private readonly HttpContext _context; - private readonly BasicAuthenticationHeaderValue _authenticationHeaderValue; + public BasicAuthenticationSignInManager( + HttpContext context, + BasicAuthenticationHeaderValue authenticationHeaderValue, + IUserManager userManager) + { + _context = context; + _authenticationHeaderValue = authenticationHeaderValue; + _userManager = userManager; + } - public async Task TrySignInUser() - { - if (_authenticationHeaderValue.IsValidBasicAuthenticationHeaderValue) - { - var validateResult = await _userManager.ValidateAsync("email", - _authenticationHeaderValue.UserIdentifier, - _authenticationHeaderValue.UserPassword); - - if (!validateResult.Success) - { - _context.Response.StatusCode = 401; - if(!_context.Response.Headers.ContainsKey("WWW-Authenticate") ) + private readonly HttpContext _context; + private readonly BasicAuthenticationHeaderValue _authenticationHeaderValue; + + public async Task TrySignInUser() + { + if ( _authenticationHeaderValue.IsValidBasicAuthenticationHeaderValue ) + { + var validateResult = await _userManager.ValidateAsync("email", + _authenticationHeaderValue.UserIdentifier, + _authenticationHeaderValue.UserPassword); + + if ( !validateResult.Success ) + { + _context.Response.StatusCode = 401; + if ( !_context.Response.Headers.ContainsKey("WWW-Authenticate") ) { _context.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"Starsky " + validateResult.Error + " \""); } return; - } + } - await _userManager.SignIn(_context, validateResult.User); - } - } - } + await _userManager.SignIn(_context, validateResult.User); + } + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Middleware/CheckIfAccountExistMiddleware.cs b/starsky/starsky.foundation.accountmanagement/Middleware/CheckIfAccountExistMiddleware.cs index a20b2859c2..faa697bf09 100644 --- a/starsky/starsky.foundation.accountmanagement/Middleware/CheckIfAccountExistMiddleware.cs +++ b/starsky/starsky.foundation.accountmanagement/Middleware/CheckIfAccountExistMiddleware.cs @@ -13,7 +13,7 @@ namespace starsky.foundation.accountmanagement.Middleware /// public sealed class CheckIfAccountExistMiddleware { - + public CheckIfAccountExistMiddleware(RequestDelegate next) { _next = next; @@ -32,19 +32,19 @@ internal static int GetUserTableIdFromClaims(HttpContext httpContext) public async Task Invoke(HttpContext context) { var isApiCall = context.Request.Path.HasValue && ( - context.Request.Path.Value.EndsWith("api/health/details") || + context.Request.Path.Value.EndsWith("api/health/details") || context.Request.Path.Value.EndsWith("api/index") || context.Request.Path.Value.EndsWith("api/search") || context.Request.Path.Value.EndsWith("api/account/status") || - context.Request.Path.Value.EndsWith("api/env/")); + context.Request.Path.Value.EndsWith("api/env/") ); - if (context.User.Identity?.IsAuthenticated == true && isApiCall) + if ( context.User.Identity?.IsAuthenticated == true && isApiCall ) { - var userManager = (IUserManager) context.RequestServices.GetRequiredService(typeof(IUserManager)); + var userManager = ( IUserManager )context.RequestServices.GetRequiredService(typeof(IUserManager)); var id = GetUserTableIdFromClaims(context); var result = await userManager.ExistAsync(id); - if ( result == null) + if ( result == null ) { userManager.SignOut(context); context.Response.StatusCode = 401; diff --git a/starsky/starsky.foundation.accountmanagement/Middleware/NoAccountLocalhostMiddleware.cs b/starsky/starsky.foundation.accountmanagement/Middleware/NoAccountLocalhostMiddleware.cs index 970222b9c6..dcd898de4a 100644 --- a/starsky/starsky.foundation.accountmanagement/Middleware/NoAccountLocalhostMiddleware.cs +++ b/starsky/starsky.foundation.accountmanagement/Middleware/NoAccountLocalhostMiddleware.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -12,54 +11,54 @@ [assembly: InternalsVisibleTo("starskytest")] namespace starsky.foundation.accountmanagement.Middleware { - /// - /// Auto login when use is on localhost - /// - public sealed class NoAccountMiddleware - { - public NoAccountMiddleware(RequestDelegate next, AppSettings? appSettings = null) - { - _next = next; - _appSettings = appSettings; - } + /// + /// Auto login when use is on localhost + /// + public sealed class NoAccountMiddleware + { + public NoAccountMiddleware(RequestDelegate next, AppSettings? appSettings = null) + { + _next = next; + _appSettings = appSettings; + } - private readonly RequestDelegate _next; - private readonly AppSettings? _appSettings; + private readonly RequestDelegate _next; + private readonly AppSettings? _appSettings; - internal const string Identifier = "mail@localhost"; + internal const string Identifier = "mail@localhost"; - /// - /// Enable: app__NoAccountLocalhost - /// - /// - public async Task Invoke(HttpContext context) - { - var isHostAllowed = IsLocalhost.IsHostLocalHost(context.Connection.LocalIpAddress, - context.Connection.RemoteIpAddress) || _appSettings?.DemoUnsafeDeleteStorageFolder == true; + /// + /// Enable: app__NoAccountLocalhost + /// + /// + public async Task Invoke(HttpContext context) + { + var isHostAllowed = IsLocalhost.IsHostLocalHost(context.Connection.LocalIpAddress, + context.Connection.RemoteIpAddress) || _appSettings?.DemoUnsafeDeleteStorageFolder == true; - var isApiCall = context.Request.Path.HasValue && (context.Request.Path.Value.StartsWith("/api") || - context.Request.Path.Value.StartsWith("/realtime")); - - var isFromLogoutCall = context.Request.QueryString.HasValue && - context.Request.QueryString.Value!.Contains("fromLogout"); - - if ( isHostAllowed && context.User.Identity?.IsAuthenticated == false && !isApiCall && !isFromLogoutCall) - { - var userManager = (IUserManager) context.RequestServices.GetRequiredService(typeof(IUserManager)); + var isApiCall = context.Request.Path.HasValue && ( context.Request.Path.Value.StartsWith("/api") || + context.Request.Path.Value.StartsWith("/realtime") ); - var user = userManager.GetUser("email", Identifier); - if ( user == null ) - { - var newPassword = Convert.ToBase64String( - Pbkdf2Hasher.GenerateRandomSalt()); - await userManager.SignUpAsync(string.Empty, - "email", Identifier, newPassword+newPassword); - user = userManager.GetUser("email", Identifier); - } - - await userManager.SignIn(context, user, true); - } - await _next.Invoke(context); - } - } + var isFromLogoutCall = context.Request.QueryString.HasValue && + context.Request.QueryString.Value!.Contains("fromLogout"); + + if ( isHostAllowed && context.User.Identity?.IsAuthenticated == false && !isApiCall && !isFromLogoutCall ) + { + var userManager = ( IUserManager )context.RequestServices.GetRequiredService(typeof(IUserManager)); + + var user = userManager.GetUser("email", Identifier); + if ( user == null ) + { + var newPassword = Convert.ToBase64String( + Pbkdf2Hasher.GenerateRandomSalt()); + await userManager.SignUpAsync(string.Empty, + "email", Identifier, newPassword + newPassword); + user = userManager.GetUser("email", Identifier); + } + + await userManager.SignIn(context, user, true); + } + await _next.Invoke(context); + } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Models/Account/ChangePasswordViewModel.cs b/starsky/starsky.foundation.accountmanagement/Models/Account/ChangePasswordViewModel.cs index 91b2aa72f2..73998f03e7 100644 --- a/starsky/starsky.foundation.accountmanagement/Models/Account/ChangePasswordViewModel.cs +++ b/starsky/starsky.foundation.accountmanagement/Models/Account/ChangePasswordViewModel.cs @@ -1,32 +1,32 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; // ReSharper disable once IdentifierTypo namespace starsky.foundation.accountmanagement.Models.Account { - public sealed class ChangePasswordViewModel - { - /// - /// Password before change - /// - [StringLength(100, MinimumLength = 8)] - [DataType(DataType.Password)] - public string Password { get; set; } = string.Empty; - - /// - /// The new password - /// - [Required] - [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] - [DataType(DataType.Password)] - [Display(Name = "ChangedPassword")] - public string ChangedPassword { get; set; } = string.Empty; + public sealed class ChangePasswordViewModel + { + /// + /// Password before change + /// + [StringLength(100, MinimumLength = 8)] + [DataType(DataType.Password)] + public string Password { get; set; } = string.Empty; - /// - /// The new password confirmed - /// - [DataType(DataType.Password)] - [Display(Name = "Confirm password")] - [Compare("ChangedPassword", ErrorMessage = "The password and confirmation password do not match.")] - public string ChangedConfirmPassword { get; set; } = string.Empty; - } + /// + /// The new password + /// + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] + [DataType(DataType.Password)] + [Display(Name = "ChangedPassword")] + public string ChangedPassword { get; set; } = string.Empty; + + /// + /// The new password confirmed + /// + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("ChangedPassword", ErrorMessage = "The password and confirmation password do not match.")] + public string ChangedConfirmPassword { get; set; } = string.Empty; + } } diff --git a/starsky/starsky.foundation.accountmanagement/Models/Account/LoginViewModel.cs b/starsky/starsky.foundation.accountmanagement/Models/Account/LoginViewModel.cs index b1ef3d2d45..9752eeabab 100644 --- a/starsky/starsky.foundation.accountmanagement/Models/Account/LoginViewModel.cs +++ b/starsky/starsky.foundation.accountmanagement/Models/Account/LoginViewModel.cs @@ -1,18 +1,18 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace starsky.foundation.accountmanagement.Models.Account { - public sealed class LoginViewModel - { - [Required] - [EmailAddress] - public string Email { get; set; } = string.Empty; + public sealed class LoginViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = string.Empty; - [Required] - [DataType(DataType.Password)] - public string Password { get; set; } = string.Empty; + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = string.Empty; - [Display(Name = "Remember me?")] - public bool RememberMe { get; set; } = true; - } + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } = true; + } } diff --git a/starsky/starsky.foundation.accountmanagement/Models/Account/RegisterViewModel.cs b/starsky/starsky.foundation.accountmanagement/Models/Account/RegisterViewModel.cs index cb194b8180..1471b2823c 100644 --- a/starsky/starsky.foundation.accountmanagement/Models/Account/RegisterViewModel.cs +++ b/starsky/starsky.foundation.accountmanagement/Models/Account/RegisterViewModel.cs @@ -1,28 +1,28 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace starsky.foundation.accountmanagement.Models.Account { - public sealed class RegisterViewModel - { - [Required] - [EmailAddress] - [Display(Name = "Email")] - public string? Email { get; set; } + public sealed class RegisterViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string? Email { get; set; } - /// - /// Name - /// - public string Name { get; set; } = ""; + /// + /// Name + /// + public string Name { get; set; } = ""; - [Required] - [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] - [DataType(DataType.Password)] - [Display(Name = "Password")] - public string? Password { get; set; } + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string? Password { get; set; } - [DataType(DataType.Password)] - [Display(Name = "Confirm password")] - [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] - public string? ConfirmPassword { get; set; } - } + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string? ConfirmPassword { get; set; } + } } diff --git a/starsky/starsky.foundation.accountmanagement/Models/UserOverviewModel.cs b/starsky/starsky.foundation.accountmanagement/Models/UserOverviewModel.cs index 3fec2954a7..652eb1191b 100644 --- a/starsky/starsky.foundation.accountmanagement/Models/UserOverviewModel.cs +++ b/starsky/starsky.foundation.accountmanagement/Models/UserOverviewModel.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; using starsky.foundation.database.Models.Account; @@ -14,6 +13,6 @@ public UserOverviewModel(List? objectAllUsersResult = null) } public List Users { get; set; } = new List(); - + public bool IsSuccess { get; set; } } diff --git a/starsky/starsky.foundation.accountmanagement/Models/ValidateResult.cs b/starsky/starsky.foundation.accountmanagement/Models/ValidateResult.cs index 3a488c378e..dcd8aed5d6 100644 --- a/starsky/starsky.foundation.accountmanagement/Models/ValidateResult.cs +++ b/starsky/starsky.foundation.accountmanagement/Models/ValidateResult.cs @@ -8,7 +8,7 @@ public sealed class ValidateResult public User? User { get; set; } public bool Success { get; set; } public ValidateResultError? Error { get; set; } - + public ValidateResult(User? user = null, bool success = false, ValidateResultError? error = null) { User = user; diff --git a/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs b/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs index 1b5999f74f..fac0da02e7 100644 --- a/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs +++ b/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -37,14 +37,14 @@ public sealed class UserManager : IUserManager private readonly IWebLogger _logger; public UserManager(ApplicationDbContext dbContext, AppSettings appSettings, IWebLogger logger, - IMemoryCache? memoryCache = null ) + IMemoryCache? memoryCache = null) { _dbContext = dbContext; _cache = memoryCache; _appSettings = appSettings; _logger = logger; } - + private bool IsCacheEnabled() { // || _appSettings?.AddMemoryCache == false > disabled @@ -64,7 +64,7 @@ private List AddDefaultRoles() AccountRoles.AppAccountRoles.User.ToString(), AccountRoles.AppAccountRoles.Administrator.ToString(), }; - + var roles = new List(); foreach ( var roleName in existingRoleNames ) { @@ -85,10 +85,10 @@ private List AddDefaultRoles() // Get the Int Ids from the database role = _dbContext.Roles.FirstOrDefault(p => p.Code != null && p.Code.ToLower().Equals(roleName.ToLower())); - + roles.Add(role!); } - + return roles; } @@ -105,7 +105,7 @@ private List AddDefaultRoles() .Equals(credentialTypeCode.ToLower())); // When not exist add it - if (credentialType == null && credentialTypeCode.Equals("email", StringComparison.CurrentCultureIgnoreCase) ) + if ( credentialType == null && credentialTypeCode.Equals("email", StringComparison.CurrentCultureIgnoreCase) ) { credentialType = new CredentialType { @@ -123,16 +123,16 @@ private List AddDefaultRoles() private const string AllUsersCacheKey = "UserManager_AllUsers"; - + /// /// Return the number of users in the database /// /// public async Task AllUsersAsync() { - if (IsCacheEnabled() && _cache?.TryGetValue(AllUsersCacheKey, out var objectAllUsersResult) == true) + if ( IsCacheEnabled() && _cache?.TryGetValue(AllUsersCacheKey, out var objectAllUsersResult) == true ) { - return new UserOverviewModel(( List? ) objectAllUsersResult); + return new UserOverviewModel(( List? )objectAllUsersResult); } try @@ -140,12 +140,12 @@ public async Task AllUsersAsync() var allUsers = await _dbContext.Users.TagWith("AllUsersAsync").ToListAsync(); if ( IsCacheEnabled() ) { - _cache!.Set(AllUsersCacheKey, allUsers, - new TimeSpan(99,0,0)); + _cache!.Set(AllUsersCacheKey, allUsers, + new TimeSpan(99, 0, 0)); } return new UserOverviewModel(allUsers); } - catch ( RetryLimitExceededException exception) + catch ( RetryLimitExceededException exception ) { _logger.LogError(exception, "[User Manager] RetryLimitExceededException [catch-ed]"); @@ -160,7 +160,7 @@ public async Task AllUsersAsync() internal async Task AddUserToCache(User user) { if ( !IsCacheEnabled() ) return; - var allUsers = (await AllUsersAsync()).Users; + var allUsers = ( await AllUsersAsync() ).Users; var index = allUsers.Find(p => p.Id == user.Id); if ( allUsers.Exists(p => p.Id == user.Id) && index != null ) { @@ -171,20 +171,20 @@ internal async Task AddUserToCache(User user) { allUsers.Add(user); } - _cache!.Set(AllUsersCacheKey, allUsers, - new TimeSpan(99,0,0)); + _cache!.Set(AllUsersCacheKey, allUsers, + new TimeSpan(99, 0, 0)); } - + /// /// Remove one user from cache /// private async Task RemoveUserFromCacheAsync(User user) { if ( !IsCacheEnabled() ) return; - var allUsers = (await AllUsersAsync()).Users; + var allUsers = ( await AllUsersAsync() ).Users; allUsers.Remove(user); - _cache!.Set(AllUsersCacheKey, allUsers, - new TimeSpan(99,0,0)); + _cache!.Set(AllUsersCacheKey, allUsers, + new TimeSpan(99, 0, 0)); } /// @@ -207,8 +207,8 @@ private async Task RemoveUserFromCacheAsync(User user) { return await _dbContext.Users.FirstOrDefaultAsync(p => p.Id == userTableId); } - - var users = (await AllUsersAsync()).Users; + + var users = ( await AllUsersAsync() ).Users; return users.Find(p => p.Id == userTableId); } @@ -222,16 +222,16 @@ private async Task RemoveUserFromCacheAsync(User user) internal string GetRoleAddToUser(string identifier, User user) { var roleToAddToUser = _appSettings.AccountRegisterDefaultRole.ToString(); - - if (_appSettings.AccountRegisterFirstRoleAdmin == true && !_dbContext.Users.Any(p => p != user) ) + + if ( _appSettings.AccountRegisterFirstRoleAdmin == true && !_dbContext.Users.Any(p => p != user) ) { return AccountRoles.AppAccountRoles.Administrator.ToString(); } - if (_appSettings.AccountRolesByEmailRegisterOverwrite != null - && _appSettings.AccountRolesByEmailRegisterOverwrite - .TryGetValue(identifier, out var emailsForConfig) && - AccountRoles.GetAllRoles().Contains(emailsForConfig) ) + if ( _appSettings.AccountRolesByEmailRegisterOverwrite != null + && _appSettings.AccountRolesByEmailRegisterOverwrite + .TryGetValue(identifier, out var emailsForConfig) && + AccountRoles.GetAllRoles().Contains(emailsForConfig) ) { return emailsForConfig; } @@ -254,12 +254,12 @@ public async Task SignUpAsync(string name, var roles = AddDefaultRoles(); AddDefaultPermissions(); AddDefaultRolePermissions(); - - if ( string.IsNullOrEmpty(identifier) || string.IsNullOrEmpty(secret)) + + if ( string.IsNullOrEmpty(identifier) || string.IsNullOrEmpty(secret) ) { return new SignUpResult(success: false, error: SignUpResultError.NullString); } - + // The email is stored in the Credentials database var user = Exist(identifier); if ( user == null ) @@ -271,22 +271,22 @@ public async Task SignUpAsync(string name, Name = name, Created = createdDate }; - + await _dbContext.Users.AddAsync(user); await _dbContext.SaveChangesAsync(); await AddUserToCache(user); // to get the Id user = await _dbContext.Users.FirstOrDefaultAsync(p => p.Created == createdDate); - + if ( user == null ) throw new AggregateException("user should not be null"); } // Add a user role based on a user id - var roleToAdd = roles.Find(p => p.Code == GetRoleAddToUser(identifier,user)); + var roleToAdd = roles.Find(p => p.Code == GetRoleAddToUser(identifier, user)); AddToRole(user, roleToAdd); - if (credentialType == null) + if ( credentialType == null ) { return new SignUpResult(success: false, error: SignUpResultError.CredentialTypeNotFound); } @@ -303,7 +303,7 @@ public async Task SignUpAsync(string name, }; byte[] salt = Pbkdf2Hasher.GenerateRandomSalt(); string hash = Pbkdf2Hasher.ComputeHash(secret, salt); - + credential.Secret = hash; credential.Extra = Convert.ToBase64String(salt); await _dbContext.Credentials.AddAsync(credential); @@ -311,7 +311,7 @@ public async Task SignUpAsync(string name, return new SignUpResult(user: user, success: true); } - + /// /// Add a link between the user and the role (for example Admin) /// @@ -321,14 +321,14 @@ public void AddToRole(User user, string roleCode) { var role = _dbContext.Roles.TagWith("AddToRole").FirstOrDefault(r => r.Code == roleCode); - if (role == null) + if ( role == null ) { - return; + return; } - + AddToRole(user, role); } - + /// /// Add a link between the user and the role (for example Admin) /// @@ -337,10 +337,10 @@ public void AddToRole(User user, string roleCode) public void AddToRole(User user, Role? role) { var userRole = _dbContext.UserRoles.FirstOrDefault(p => p.User != null && p.User.Id == user.Id); - - if (userRole != null || role == null) + + if ( userRole != null || role == null ) { - return; + return; } // Add a user role based on a user id @@ -356,60 +356,60 @@ public void RemoveFromRole(User user, string roleCode) { var role = _dbContext.Roles.TagWith("RemoveFromRole").FirstOrDefault( r => string.Equals(r.Code, roleCode, StringComparison.OrdinalIgnoreCase)); - - if (role == null) + + if ( role == null ) { - return; + return; } - + RemoveFromRole(user, role); } - + public void RemoveFromRole(User user, Role role) { var userRole = _dbContext.UserRoles.Find(user.Id, role.Id); - - if (userRole == null) + + if ( userRole == null ) { return; } - + _dbContext.UserRoles.Remove(userRole); _dbContext.SaveChanges(); } - + public ChangeSecretResult ChangeSecret(string credentialTypeCode, string? identifier, string secret) { var credentialType = _dbContext.CredentialTypes.FirstOrDefault( ct => ct.Code != null && ct.Code.ToLower().Equals(credentialTypeCode.ToLower())); - - if (credentialType == null) + + if ( credentialType == null ) { return new ChangeSecretResult(success: false, error: ChangeSecretResultError.CredentialTypeNotFound); } - + var credential = _dbContext.Credentials.TagWith("ChangeSecret").FirstOrDefault( c => c.CredentialTypeId == credentialType.Id && c.Identifier == identifier); - - if (credential == null || identifier == null) + + if ( credential == null || identifier == null ) { return new ChangeSecretResult(success: false, error: ChangeSecretResultError.CredentialNotFound); } - + var salt = Pbkdf2Hasher.GenerateRandomSalt(); var hash = Pbkdf2Hasher.ComputeHash(secret, salt); - + credential.Secret = hash; credential.Extra = Convert.ToBase64String(salt); _dbContext.Credentials.Update(credential); _dbContext.SaveChanges(); - + if ( IsCacheEnabled() ) { - _cache!.Set(CredentialCacheKey(credentialType, identifier), - credential,new TimeSpan(99,0,0)); + _cache!.Set(CredentialCacheKey(credentialType, identifier), + credential, new TimeSpan(99, 0, 0)); } - + return new ChangeSecretResult(success: true); } @@ -430,25 +430,25 @@ internal static string CredentialCacheKey(CredentialType credentialType, string? { return null; } - + var key = CredentialCacheKey(credentialType, identifier); - + // Add caching for credentialType - if (IsCacheEnabled() && _cache?.TryGetValue(key, - out var objectCredentialTypeCode) == true) + if ( IsCacheEnabled() && _cache?.TryGetValue(key, + out var objectCredentialTypeCode) == true ) { - return ( Credential? ) objectCredentialTypeCode; + return ( Credential? )objectCredentialTypeCode; } - + var credentialSelect = _dbContext.Credentials.AsNoTracking().TagWith("Credential").Where( c => c.CredentialTypeId == credentialType.Id && c.Identifier == identifier).Select(x => new - { - x.Id, - x.UserId, - x.CredentialTypeId, - x.Secret, - x.Extra - }).FirstOrDefault(); + { + x.Id, + x.UserId, + x.CredentialTypeId, + x.Secret, + x.Extra + }).FirstOrDefault(); if ( credentialSelect == null ) { @@ -464,14 +464,14 @@ internal static string CredentialCacheKey(CredentialType credentialType, string? Extra = credentialSelect.Extra, }; - if ( IsCacheEnabled()) + if ( IsCacheEnabled() ) { - _cache!.Set(key, credential,new TimeSpan(99,0,0)); + _cache!.Set(key, credential, new TimeSpan(99, 0, 0)); } return credential; } - + /// /// Get the CredentialType by the credentialTypeCode @@ -482,20 +482,21 @@ internal static string CredentialCacheKey(CredentialType credentialType, string? { var cacheKey = "credentialTypeCode_" + credentialTypeCode; // Add caching for credentialType - if (IsCacheEnabled() && _cache?.TryGetValue(cacheKey, - out var objectCredentialTypeCode) == true) + if ( IsCacheEnabled() && _cache?.TryGetValue(cacheKey, + out var objectCredentialTypeCode) == true ) { - return ( CredentialType? ) objectCredentialTypeCode; + return ( CredentialType? )objectCredentialTypeCode; } - + var credentialTypeSelect = _dbContext.CredentialTypes.AsNoTracking().TagWith("CredentialType").Where( - ct => ct.Code != null && ct.Code.ToLower().Equals(credentialTypeCode.ToLower())).Select(x => new { - x.Id, - x.Code, - x.Name, - x.Position - }).FirstOrDefault(); - + ct => ct.Code != null && ct.Code.ToLower().Equals(credentialTypeCode.ToLower())).Select(x => new + { + x.Id, + x.Code, + x.Name, + x.Position + }).FirstOrDefault(); + if ( credentialTypeSelect == null ) return null; var credentialType = new CredentialType @@ -508,8 +509,8 @@ internal static string CredentialCacheKey(CredentialType credentialType, string? if ( IsCacheEnabled() ) { - _cache!.Set(cacheKey, credentialType, - new TimeSpan(99,0,0)); + _cache!.Set(cacheKey, credentialType, + new TimeSpan(99, 0, 0)); } return credentialType; } @@ -518,15 +519,15 @@ public bool PreflightValidate(string userName, string password, string confirmPa { var model = new RegisterViewModel { - Email = userName, - Password = password, + Email = userName, + Password = password, ConfirmPassword = confirmPassword }; - + var context = new ValidationContext(model, null, null); var results = new List(); return Validator.TryValidateObject( - model, context, results, + model, context, results, true ); } @@ -543,25 +544,25 @@ public async Task ValidateAsync(string credentialTypeCode, { var credentialType = CachedCredentialType(credentialTypeCode); - if (credentialType == null) + if ( credentialType == null ) { return new ValidateResult(success: false, error: ValidateResultError.CredentialTypeNotFound); } var credential = CachedCredential(credentialType, identifier); - - if (credential?.Extra == null) + + if ( credential?.Extra == null ) { return new ValidateResult(success: false, error: ValidateResultError.CredentialNotFound); } - + // No Password if ( string.IsNullOrWhiteSpace(secret) ) { return new ValidateResult(success: false, error: ValidateResultError.SecretNotValid); } - - var userData = (await AllUsersAsync()).Users.Find(p => p.Id == credential.UserId); + + var userData = ( await AllUsersAsync() ).Users.Find(p => p.Id == credential.UserId); if ( userData == null ) { return new ValidateResult(success: false, error: ValidateResultError.UserNotFound); @@ -571,7 +572,7 @@ public async Task ValidateAsync(string credentialTypeCode, { return new ValidateResult(success: false, error: ValidateResultError.Lockout); } - + // To compare the secret byte[] salt = Convert.FromBase64String(credential.Extra); string hashedPassword = Pbkdf2Hasher.ComputeHash(secret, salt); @@ -584,26 +585,26 @@ public async Task ValidateAsync(string credentialTypeCode, return await SetLockIfFailedCountIsToHigh(credential.UserId); } - internal async Task ResetAndSuccess(int accessFailedCount, int userId, User? userData ) + internal async Task ResetAndSuccess(int accessFailedCount, int userId, User? userData) { if ( accessFailedCount <= 0 ) { return new ValidateResult(userData, true); } - + userData = await _dbContext.Users.FindAsync(userId); if ( userData == null ) { return new ValidateResult(success: false, error: ValidateResultError.UserNotFound); } - + userData.LockoutEnabled = false; userData.AccessFailedCount = 0; userData.LockoutEnd = DateTime.MinValue; await _dbContext.SaveChangesAsync(); await AddUserToCache(userData); - + return new ValidateResult(userData, true); } @@ -628,7 +629,7 @@ internal async Task SetLockIfFailedCountIsToHigh(int userId) await AddUserToCache(userData); return new ValidateResult(success: false, error: errorReason); } - + public async Task SignIn(HttpContext httpContext, User? user, bool isPersistent = false) { if ( user == null ) @@ -640,16 +641,16 @@ public async Task SignIn(HttpContext httpContext, User? user, bool isPersi { return false; } - + ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); ClaimsPrincipal principal = new ClaimsPrincipal(identity); - + await httpContext.SignInAsync( - CookieAuthenticationDefaults.AuthenticationScheme, - principal, - new AuthenticationProperties() { IsPersistent = isPersistent} + CookieAuthenticationDefaults.AuthenticationScheme, + principal, + new AuthenticationProperties() { IsPersistent = isPersistent } ); - + // Required in the direct context; when using a REST like call httpContext.User = principal; return true; @@ -669,7 +670,7 @@ public async Task RemoveUser(string credentialTypeCode, { return new ValidateResult { Success = false, Error = ValidateResultError.CredentialTypeNotFound }; } - + var credential = _dbContext.Credentials.FirstOrDefault( c => c.CredentialTypeId == credentialType.Id && c.Identifier == identifier); @@ -677,7 +678,7 @@ public async Task RemoveUser(string credentialTypeCode, { return new ValidateResult { - Success = false, + Success = false, Error = ValidateResultError.CredentialNotFound }; } @@ -685,55 +686,57 @@ public async Task RemoveUser(string credentialTypeCode, var user = await _dbContext.Users.FirstOrDefaultAsync(p => p.Id == credential.UserId); var userRole = await _dbContext.UserRoles.FirstOrDefaultAsync(p => p.UserId == credential.UserId); - - if(userRole == null || user == null) { - return new ValidateResult{ - Success = false, - Error = ValidateResultError.CredentialNotFound - }; + + if ( userRole == null || user == null ) + { + return new ValidateResult + { + Success = false, + Error = ValidateResultError.CredentialNotFound + }; } _dbContext.Credentials.Remove(credential); _dbContext.Users.Remove(user); _dbContext.UserRoles.Remove(userRole); await _dbContext.SaveChangesAsync(); - + await RemoveUserFromCacheAsync(user); - - return new ValidateResult{Success = true}; + + return new ValidateResult { Success = true }; } - + public async void SignOut(HttpContext httpContext) { await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } - + public int GetCurrentUserId(HttpContext httpContext) { - if (httpContext.User.Identity?.IsAuthenticated == false) + if ( httpContext.User.Identity?.IsAuthenticated == false ) { return -1; } - + var claim = httpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); - - if (claim == null) + + if ( claim == null ) { return -1; } - if (!int.TryParse(claim.Value, out var currentUserId)) + if ( !int.TryParse(claim.Value, out var currentUserId) ) { return -1; } - + return currentUserId; } - + public User? GetCurrentUser(HttpContext httpContext) { var currentUserId = GetCurrentUserId(httpContext); - + return currentUserId == -1 ? null : _dbContext.Users.Find(currentUserId); } @@ -757,7 +760,7 @@ public int GetCurrentUserId(HttpContext httpContext) var role = _dbContext.UserRoles.FirstOrDefault(p => p.User != null && p.User.Id == user.Id); if ( role == null ) return new Role(); var roleId = role.RoleId; - return _dbContext.Roles.TagWith("GetRole").FirstOrDefault(p=> p.Id == roleId); + return _dbContext.Roles.TagWith("GetRole").FirstOrDefault(p => p.Id == roleId); } public Credential? GetCredentialsByUserId(int userId) @@ -777,7 +780,7 @@ internal IEnumerable GetUserClaims(User? user) var email = user.Credentials?.FirstOrDefault(p => !string.IsNullOrEmpty(p.Identifier))?.Identifier; - + var claims = new List { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), @@ -788,14 +791,14 @@ internal IEnumerable GetUserClaims(User? user) claims.AddRange(GetUserRoleClaims(user)); return claims; } - + private List GetUserRoleClaims(User user) { var claims = new List(); IEnumerable roleIds = _dbContext.UserRoles.TagWith("GetUserRoleClaims").Where( ur => ur.UserId == user.Id).Select(ur => ur.RoleId).ToList(); - foreach (var roleId in roleIds) + foreach ( var roleId in roleIds ) { var role = _dbContext.Roles.Find(roleId); if ( role?.Code == null ) @@ -807,7 +810,7 @@ private List GetUserRoleClaims(User user) } return claims; } - + internal IEnumerable GetUserPermissionClaims(Role role) { List claims = new List(); @@ -815,7 +818,7 @@ internal IEnumerable GetUserPermissionClaims(Role role) rp => rp.RoleId == role.Id); IEnumerable permissionIds = rolePermissions.Select(rp => rp.PermissionId).ToList(); - foreach (var permissionId in permissionIds) + foreach ( var permissionId in permissionIds ) { var permission = _dbContext.Permissions.Find(permissionId); if ( permission?.Code == null ) @@ -832,12 +835,12 @@ public enum AppPermissions { AppSettingsWrite = 10, } - + private static readonly List AllPermissions = new List { AppPermissions.AppSettingsWrite, }; - + private void AddDefaultPermissions() { foreach ( var permissionEnum in AllPermissions ) @@ -845,12 +848,12 @@ private void AddDefaultPermissions() var permission = _dbContext.Permissions.FirstOrDefault(p => p.Code == permissionEnum.ToString()); if ( permission != null ) continue; - + permission = new Permission() { Name = permissionEnum.ToString(), Code = permissionEnum.ToString(), - Position = (int) permissionEnum, + Position = ( int )permissionEnum, }; _dbContext.Permissions.Add(permission); _dbContext.SaveChanges(); @@ -859,16 +862,16 @@ private void AddDefaultPermissions() private void AddDefaultRolePermissions() { - var existingRolePermissions = new List> + var existingRolePermissions = new List> { new KeyValuePair( AccountRoles.AppAccountRoles.Administrator.ToString(), AppPermissions.AppSettingsWrite), }; - + foreach ( var rolePermissionsDictionary in existingRolePermissions ) { - var role = _dbContext.Roles.TagWith("AddDefaultRolePermissions").FirstOrDefault(p => p.Code == rolePermissionsDictionary.Key ); - var permission = _dbContext.Permissions.FirstOrDefault(p => + var role = _dbContext.Roles.TagWith("AddDefaultRolePermissions").FirstOrDefault(p => p.Code == rolePermissionsDictionary.Key); + var permission = _dbContext.Permissions.FirstOrDefault(p => p.Code == rolePermissionsDictionary.Value.ToString()); if ( permission == null || role == null ) continue; @@ -885,7 +888,7 @@ private void AddDefaultRolePermissions() RoleId = role.Id, PermissionId = permission.Id }; - + _dbContext.RolePermissions.Add(rolePermission); _dbContext.SaveChanges(); } diff --git a/starsky/starsky.foundation.consoletelemetry/Extensions/ApplicationInsightsWorkerExtension.cs b/starsky/starsky.foundation.consoletelemetry/Extensions/ApplicationInsightsWorkerExtension.cs deleted file mode 100644 index 07c50e502d..0000000000 --- a/starsky/starsky.foundation.consoletelemetry/Extensions/ApplicationInsightsWorkerExtension.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.ApplicationInsights.Extensibility; -using Microsoft.ApplicationInsights.WorkerService; -using Microsoft.Extensions.DependencyInjection; -using starsky.foundation.consoletelemetry.Initializers; -using starsky.foundation.platform.Models; - -namespace starsky.foundation.consoletelemetry.Extensions -{ - public static class ApplicationInsightsWorkerExtension - { - /// - /// Add Metrics & Monitoring for Application Insights - /// - /// collection service - /// to use for ApplicationInsights InstrumentationKey - /// application type - public static void AddMonitoringWorkerService(this IServiceCollection services, - AppSettings appSettings, AppSettings.StarskyAppType appType) - { - if ( string.IsNullOrWhiteSpace(appSettings - .ApplicationInsightsConnectionString) ) - { - return; - } - - appSettings.ApplicationType = appType; - services.AddSingleton(new CloudRoleNameInitializer($"{appType}")); - - services.AddApplicationInsightsTelemetryWorkerService(new ApplicationInsightsServiceOptions - { - ConnectionString = appSettings.ApplicationInsightsConnectionString, - ApplicationVersion = appSettings.AppVersion, - EnableDependencyTrackingTelemetryModule = true, - EnableHeartbeat = true - }); - - } - } -} diff --git a/starsky/starsky.foundation.consoletelemetry/Initializers/CloudRoleNameInitializer.cs b/starsky/starsky.foundation.consoletelemetry/Initializers/CloudRoleNameInitializer.cs deleted file mode 100644 index 7edcbc50f4..0000000000 --- a/starsky/starsky.foundation.consoletelemetry/Initializers/CloudRoleNameInitializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.Extensibility; - -namespace starsky.foundation.consoletelemetry.Initializers -{ - public sealed class CloudRoleNameInitializer : ITelemetryInitializer - { - private readonly string _roleName; - - public CloudRoleNameInitializer(string roleName) - { - _roleName = roleName ?? throw new ArgumentNullException(nameof(roleName)); - } - - public void Initialize(ITelemetry telemetry) - { - telemetry.Context.Cloud.RoleName = _roleName; - telemetry.Context.Cloud.RoleInstance = Environment.MachineName; - // @see: TelemetryConfigurationHelper - } - } -} diff --git a/starsky/starsky.foundation.consoletelemetry/starsky.foundation.consoletelemetry.csproj b/starsky/starsky.foundation.consoletelemetry/starsky.foundation.consoletelemetry.csproj deleted file mode 100644 index 5cf76d7dc3..0000000000 --- a/starsky/starsky.foundation.consoletelemetry/starsky.foundation.consoletelemetry.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net8.0 - - {9402eb61-ae4c-4e8b-8413-c47573d16c9d} - Full - 0.6.0-beta.0 - starsky.foundation.consoletelemetry - enable - - - - - - - - - - - - - diff --git a/starsky/starsky.foundation.database/Data/ApplicationDbContext.cs b/starsky/starsky.foundation.database/Data/ApplicationDbContext.cs index 73c2fd101f..40252662ee 100644 --- a/starsky/starsky.foundation.database/Data/ApplicationDbContext.cs +++ b/starsky/starsky.foundation.database/Data/ApplicationDbContext.cs @@ -7,14 +7,14 @@ using starsky.foundation.database.Models; using starsky.foundation.database.Models.Account; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + namespace starsky.foundation.database.Data { [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public class ApplicationDbContext : DbContext { -#pragma warning disable CS8618 public ApplicationDbContext(DbContextOptions options) : base(options) -#pragma warning restore CS8618 { } @@ -32,21 +32,21 @@ public ApplicationDbContext(DbContextOptions options) : base(options) public DbSet Notifications { get; set; } public DbSet Settings { get; set; } - + public DbSet Thumbnails { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Do nothing because of that in debug mode this only triggered -#if (DEBUG) +#if ( DEBUG ) optionsBuilder.EnableSensitiveDataLogging(); #endif } - + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - + // does not have direct effect modelBuilder.HasCharSet("utf8mb4", DelegationModes.ApplyToAll); @@ -56,11 +56,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(etb => { etb.HasAnnotation("MySql:CharSet", "utf8mb4"); - etb.HasIndex(x => new {x.FileName, x.ParentDirectory}); - + etb.HasIndex(x => new { x.FileName, x.ParentDirectory }); + etb.Property(p => p.Size).HasColumnType("bigint"); }); - + modelBuilder.Entity(etb => { etb.HasAnnotation("MySql:CharSet", "utf8mb4"); @@ -70,17 +70,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasAnnotation("MySql:ValueGeneratedOnAdd", true); etb.Property(e => e.Name).IsRequired().HasMaxLength(64); etb.ToTable("Users"); - + DateTime parsedDateTime; var converter = new ValueConverter( v => v.ToString(@"yyyy\-MM\-dd HH:mm:ss.fff", CultureInfo.InvariantCulture), - v => DateTime.TryParseExact(v, @"yyyy\-MM\-dd HH:mm:ss.fff", - CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedDateTime) + v => DateTime.TryParseExact(v, @"yyyy\-MM\-dd HH:mm:ss.fff", + CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, + out parsedDateTime) ? parsedDateTime : DateTime.MinValue ); - + etb.Property(e => e.LockoutEnd) .HasColumnType("TEXT") .HasConversion(converter); @@ -158,7 +159,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); modelBuilder.Entity() - .HasIndex(x => new {x.FileHash}) + .HasIndex(x => new { x.FileHash }) .HasAnnotation("MySql:CharSet", "utf8mb4"); modelBuilder.Entity(etb => @@ -173,54 +174,54 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) etb.Property(p => p.Content) .HasColumnType("mediumtext"); - + etb.Property(p => p.DateTime) .IsConcurrencyToken(); - + etb.Property(p => p.DateTimeEpoch).HasColumnType("bigint"); - etb.ToTable("Notifications"); etb.HasAnnotation("MySql:CharSet", "utf8mb4"); } ); - + modelBuilder.Entity(etb => { etb.Property(e => e.Key).IsRequired().HasMaxLength(150); etb.HasKey(e => e.Key); - + etb.ToTable("Settings"); etb.HasAnnotation("MySql:CharSet", "utf8mb4"); } ); - + modelBuilder.Entity(etb => - { - etb.HasAnnotation("MySql:CharSet", "utf8mb4"); - etb.HasKey(e => e.Id); - etb.Property(e => e.Id) - .ValueGeneratedOnAdd() - .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + { + etb.HasAnnotation("MySql:CharSet", "utf8mb4"); + etb.HasKey(e => e.Id); + etb.Property(e => e.Id) + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + etb.Property(e => e.Xml).HasMaxLength(1200); + etb.Property(e => e.FriendlyName).HasMaxLength(45); } ); - + modelBuilder.Entity(etb => { etb.Property(e => e.FileHash).IsRequired().HasMaxLength(190); etb.HasKey(e => e.FileHash); - + etb.ToTable("Thumbnails"); etb.HasAnnotation("MySql:CharSet", "utf8mb4"); } ); - } /// /// Store secure keys to generate cookies /// public virtual DbSet DataProtectionKeys { get; set; } - } } diff --git a/starsky/starsky.foundation.database/Data/DbContextFactory.cs b/starsky/starsky.foundation.database/Data/DbContextFactory.cs new file mode 100644 index 0000000000..18e145beb3 --- /dev/null +++ b/starsky/starsky.foundation.database/Data/DbContextFactory.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace starsky.foundation.database.Data; + +public class DbContextFactory : IDesignTimeDbContextFactory +{ + public ApplicationDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + +#if ENABLE_MYSQL_DATABASE + // dirty hack + + _services.AddDbContext(options => + options.UseMySql(_appSettings.DatabaseConnection, GetServerVersionMySql(), + b => + { + if (! string.IsNullOrWhiteSpace(foundationDatabaseName) ) + { + b.MigrationsAssembly(foundationDatabaseName); + } + })); +#endif + + optionsBuilder.UseSqlite("Data Source=blog.db"); + + return new ApplicationDbContext(optionsBuilder.Options); + } +} diff --git a/starsky/starsky.foundation.database/DataProtection/AddDataProtectionKeys.cs b/starsky/starsky.foundation.database/DataProtection/AddDataProtectionKeys.cs index 1cc2cb4031..b423b45c09 100644 --- a/starsky/starsky.foundation.database/DataProtection/AddDataProtectionKeys.cs +++ b/starsky/starsky.foundation.database/DataProtection/AddDataProtectionKeys.cs @@ -9,7 +9,7 @@ public static class AddDataProtectionKeys public static void SetupDataProtection(this IServiceCollection services) { services.AddDataProtection() - .AddKeyManagementOptions(options => options.XmlRepository = + .AddKeyManagementOptions(options => options.XmlRepository = services.BuildServiceProvider().GetService()) .SetApplicationName("Starsky"); } diff --git a/starsky/starsky.foundation.database/DataProtection/SqlXmlRepository.cs b/starsky/starsky.foundation.database/DataProtection/SqlXmlRepository.cs index 843a99c1ec..33b71002c2 100644 --- a/starsky/starsky.foundation.database/DataProtection/SqlXmlRepository.cs +++ b/starsky/starsky.foundation.database/DataProtection/SqlXmlRepository.cs @@ -28,7 +28,7 @@ public SqlXmlRepository(ApplicationDbContext dbContext, IServiceScopeFactory sco _scopeFactory = scopeFactory; _logger = logger; } - + public IReadOnlyCollection GetAllElements() { try @@ -36,28 +36,28 @@ public IReadOnlyCollection GetAllElements() var result = _dbContext.DataProtectionKeys .Where(p => p.Xml != null).AsEnumerable() .Select(x => XElement.Parse(x.Xml!)).ToList(); - + return result; } catch ( Exception exception ) { - if ( exception is not RetryLimitExceededException && - exception is not MySqlConnector.MySqlException && - exception is not Microsoft.Data.Sqlite.SqliteException ) throw; - + if ( exception is not RetryLimitExceededException && + exception is not MySqlConnector.MySqlException && + exception is not Microsoft.Data.Sqlite.SqliteException ) throw; + // MySqlConnector.MySqlException (0x80004005): Table 'starsky.DataProtectionKeys' doesn't exist // or Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 1: 'no such table: DataProtectionKeys if ( !exception.Message.Contains("0x80004005") && - !exception.Message.Contains( - "no such table: DataProtectionKeys") ) + !exception.Message.Contains( + "no such table: DataProtectionKeys") ) return new List(); - + _logger.LogInformation("run migration: dotnet ef database update"); _dbContext.Database.Migrate(); return new List(); } } - + /// /// This function crashes usually on the first run /// @@ -75,27 +75,27 @@ bool LocalDefault(ApplicationDbContext ctx) ctx.SaveChanges(); return true; } - + bool LocalDefaultQuery() { var context = new InjectServiceScope(_scopeFactory).Context(); return LocalDefault(context); } - + try { LocalDefault(_dbContext); } catch ( Exception exception ) { - if ( exception is not DbUpdateException && - exception is not RetryLimitExceededException && - exception is not MySqlConnector.MySqlException && - exception is not Microsoft.Data.Sqlite.SqliteException ) throw; - - var retryInterval = _dbContext.GetType().FullName?.Contains("test") == true ? + if ( exception is not DbUpdateException && + exception is not RetryLimitExceededException && + exception is not MySqlConnector.MySqlException && + exception is not Microsoft.Data.Sqlite.SqliteException ) throw; + + var retryInterval = _dbContext.GetType().FullName?.Contains("test") == true ? TimeSpan.FromSeconds(0) : TimeSpan.FromSeconds(5); - + try { RetryHelper.Do( diff --git a/starsky/starsky.foundation.database/Extensions/EntityFrameworkExtensions.cs b/starsky/starsky.foundation.database/Extensions/EntityFrameworkExtensions.cs index 8cdc77544c..a55714b109 100644 --- a/starsky/starsky.foundation.database/Extensions/EntityFrameworkExtensions.cs +++ b/starsky/starsky.foundation.database/Extensions/EntityFrameworkExtensions.cs @@ -23,19 +23,19 @@ public static bool TestConnection(this DbContext context, IWebLogger logger, IMe { return cacheValue; } - + try { if ( context?.Database == null ) return false; context.Database.CanConnect(); } - catch ( MySqlException e) + catch ( MySqlException e ) { logger.LogInformation($"[TestConnection] WARNING >>> \n{e}\n <<<"); return false; } cache?.Set(CacheKey, true, TimeSpan.FromMinutes(1)); - + return true; } } diff --git a/starsky/starsky.foundation.database/Helpers/Breadcrumbs.cs b/starsky/starsky.foundation.database/Helpers/Breadcrumbs.cs index e421766e88..dfced2697d 100644 --- a/starsky/starsky.foundation.database/Helpers/Breadcrumbs.cs +++ b/starsky/starsky.foundation.database/Helpers/Breadcrumbs.cs @@ -1,58 +1,58 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; using starsky.foundation.platform.Helpers; namespace starsky.foundation.database.Helpers { - public static class Breadcrumbs - { - /// - /// Breadcrumb returns a list of parent folders - /// it does not contain the current folder - /// - /// subPath (unix style) - /// list of parent folders - public static List BreadcrumbHelper(string? filePath) - { - if ( filePath == null ) - { - return new List(); - } + public static class Breadcrumbs + { + /// + /// Breadcrumb returns a list of parent folders + /// it does not contain the current folder + /// + /// subPath (unix style) + /// list of parent folders + public static List BreadcrumbHelper(string? filePath) + { + if ( filePath == null ) + { + return new List(); + } - // remove backslash from end - filePath = PathHelper.RemoveLatestBackslash(filePath); - if ( string.IsNullOrEmpty(filePath) ) filePath = "/"; + // remove backslash from end + filePath = PathHelper.RemoveLatestBackslash(filePath); + if ( string.IsNullOrEmpty(filePath) ) filePath = "/"; - var breadcrumb = new List(); - if (filePath[0].ToString() != "/") - { - filePath = "/" + filePath; - } - var filePathArray = filePath.Split("/".ToCharArray()); + var breadcrumb = new List(); + if ( filePath[0].ToString() != "/" ) + { + filePath = "/" + filePath; + } + var filePathArray = filePath.Split("/".ToCharArray()); - var dir = 0; - while (dir < filePathArray.Length - 1) - { - if (string.IsNullOrEmpty(filePathArray[dir])) - { - breadcrumb.Add("/"); - } - else - { - var itemStringBuilder = new StringBuilder(); - - for (int i = 0; i <= dir; i++) - { - if (!string.IsNullOrEmpty(filePathArray[i])) - { - itemStringBuilder.Append("/" + filePathArray[i]); - } - } - breadcrumb.Add(itemStringBuilder.ToString()); - } - dir++; - } - return breadcrumb; - } - } + var dir = 0; + while ( dir < filePathArray.Length - 1 ) + { + if ( string.IsNullOrEmpty(filePathArray[dir]) ) + { + breadcrumb.Add("/"); + } + else + { + var itemStringBuilder = new StringBuilder(); + + for ( int i = 0; i <= dir; i++ ) + { + if ( !string.IsNullOrEmpty(filePathArray[i]) ) + { + itemStringBuilder.Append("/" + filePathArray[i]); + } + } + breadcrumb.Add(itemStringBuilder.ToString()); + } + dir++; + } + return breadcrumb; + } + } } diff --git a/starsky/starsky.foundation.database/Helpers/DoubleModelBinder.cs b/starsky/starsky.foundation.database/Helpers/DoubleModelBinder.cs index caa79ac861..eb019e4ec7 100644 --- a/starsky/starsky.foundation.database/Helpers/DoubleModelBinder.cs +++ b/starsky/starsky.foundation.database/Helpers/DoubleModelBinder.cs @@ -23,7 +23,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) var value = valueProviderResult.FirstValue; - if (string.IsNullOrEmpty(value)) + if ( string.IsNullOrEmpty(value) ) { return Task.CompletedTask; } @@ -37,10 +37,10 @@ public Task BindModelAsync(ModelBindingContext bindingContext) bindingContext.Result = ModelBindingResult.Success(myValue); return Task.CompletedTask; } - catch (Exception) + catch ( Exception ) { - return Task.CompletedTask; + return Task.CompletedTask; } - + } } diff --git a/starsky/starsky.foundation.database/Helpers/Duplicate.cs b/starsky/starsky.foundation.database/Helpers/Duplicate.cs index 6f05714076..4811732eda 100644 --- a/starsky/starsky.foundation.database/Helpers/Duplicate.cs +++ b/starsky/starsky.foundation.database/Helpers/Duplicate.cs @@ -25,13 +25,13 @@ public async Task> RemoveDuplicateAsync(List // Get a list of duplicate items var duplicateItemsByFilePath = databaseSubFolderList.GroupBy(item => item.FilePath) .SelectMany(grp => grp.Skip(1).Take(1)).ToList(); - + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var duplicateItemByName in duplicateItemsByFilePath) + foreach ( var duplicateItemByName in duplicateItemsByFilePath ) { - var duplicateItems = databaseSubFolderList.Where(p => + var duplicateItems = databaseSubFolderList.Where(p => p.FilePath == duplicateItemByName.FilePath).ToList(); - for (var i = 1; i < duplicateItems.Count; i++) + for ( var i = 1; i < duplicateItems.Count; i++ ) { databaseSubFolderList.Remove(duplicateItems[i]); await _query.RemoveItemAsync(duplicateItems[i]); diff --git a/starsky/starsky.foundation.database/Helpers/FileIndexCompareHelper.cs b/starsky/starsky.foundation.database/Helpers/FileIndexCompareHelper.cs index d31c932bf0..d61927e1b0 100644 --- a/starsky/starsky.foundation.database/Helpers/FileIndexCompareHelper.cs +++ b/starsky/starsky.foundation.database/Helpers/FileIndexCompareHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -21,71 +21,71 @@ public static class FileIndexCompareHelper [SuppressMessage("Usage", "S6602: Find method should be used instead of the FirstOrDefault extension method.")] public static List Compare(FileIndexItem sourceIndexItem, FileIndexItem? updateObject = null, bool append = false) { - if (updateObject == null) updateObject = new FileIndexItem(); + if ( updateObject == null ) updateObject = new FileIndexItem(); var differenceList = new List(); PropertyInfo[] propertiesA = sourceIndexItem.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); PropertyInfo[] propertiesB = updateObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - for (int i = 0; i < propertiesA.Length; i++) + for ( int i = 0; i < propertiesA.Length; i++ ) { var propertyA = propertiesA[i]; var propertyB = propertiesB.FirstOrDefault(p => p.Name == propertyA.Name); - if (PropertyCanRead(propertyA, propertyB)) + if ( PropertyCanRead(propertyA, propertyB) ) continue; Type propertyType = propertyA.PropertyType; var oldValue = propertyA.GetValue(sourceIndexItem, null); var newValue = propertyB!.GetValue(updateObject, null); - if (propertyType == typeof(string)) + if ( propertyType == typeof(string) ) { - CompareString(propertyB.Name, sourceIndexItem, (string?) oldValue, (string?) newValue, differenceList, append); + CompareString(propertyB.Name, sourceIndexItem, ( string? )oldValue, ( string? )newValue, differenceList, append); } - else if (propertyType == typeof(bool?)) + else if ( propertyType == typeof(bool?) ) { - CompareNullableBool(propertyB.Name, sourceIndexItem, (bool?)oldValue, (bool?)newValue, differenceList); + CompareNullableBool(propertyB.Name, sourceIndexItem, ( bool? )oldValue, ( bool? )newValue, differenceList); } - else if (propertyType == typeof(ColorClassParser.Color)) + else if ( propertyType == typeof(ColorClassParser.Color) ) { - CompareColor(propertyB.Name, sourceIndexItem, (ColorClassParser.Color?)oldValue, - (ColorClassParser.Color?)newValue, differenceList); + CompareColor(propertyB.Name, sourceIndexItem, ( ColorClassParser.Color? )oldValue, + ( ColorClassParser.Color? )newValue, differenceList); } - else if (propertyType == typeof(DateTime)) + else if ( propertyType == typeof(DateTime) ) { - CompareDateTime(propertyB.Name, sourceIndexItem, (DateTime?)oldValue, - (DateTime?)newValue, differenceList); + CompareDateTime(propertyB.Name, sourceIndexItem, ( DateTime? )oldValue, + ( DateTime? )newValue, differenceList); } - else if (propertyType == typeof(FileIndexItem.Rotation)) + else if ( propertyType == typeof(FileIndexItem.Rotation) ) { - CompareRotation(propertyB.Name, sourceIndexItem, - (FileIndexItem.Rotation?)oldValue, - (FileIndexItem.Rotation?)newValue, differenceList); + CompareRotation(propertyB.Name, sourceIndexItem, + ( FileIndexItem.Rotation? )oldValue, + ( FileIndexItem.Rotation? )newValue, differenceList); } - else if (propertyType == typeof(ImageStabilisationType)) + else if ( propertyType == typeof(ImageStabilisationType) ) { - CompareImageStabilisationType(propertyB.Name, sourceIndexItem, - (ImageStabilisationType?)oldValue, - (ImageStabilisationType?)newValue, differenceList); + CompareImageStabilisationType(propertyB.Name, sourceIndexItem, + ( ImageStabilisationType? )oldValue, + ( ImageStabilisationType? )newValue, differenceList); } - else if (propertyType == typeof(double)) + else if ( propertyType == typeof(double) ) { - CompareDouble(propertyB.Name, sourceIndexItem, (double?)oldValue, (double?)(newValue), differenceList); + CompareDouble(propertyB.Name, sourceIndexItem, ( double? )oldValue, ( double? )( newValue ), differenceList); } - else if (propertyType == typeof(ushort)) + else if ( propertyType == typeof(ushort) ) { - CompareUshort(propertyB.Name, sourceIndexItem, (ushort?)oldValue, (ushort?)newValue, differenceList); + CompareUshort(propertyB.Name, sourceIndexItem, ( ushort? )oldValue, ( ushort? )newValue, differenceList); } - else if (propertyType == typeof(List)) + else if ( propertyType == typeof(List) ) { - CompareListString(propertyB.Name, sourceIndexItem, (List?)oldValue, (List?)newValue, differenceList); + CompareListString(propertyB.Name, sourceIndexItem, ( List? )oldValue, ( List? )newValue, differenceList); } - else if (propertyType == typeof(ExtensionRolesHelper.ImageFormat)) + else if ( propertyType == typeof(ExtensionRolesHelper.ImageFormat) ) { - CompareImageFormat(propertyB.Name, sourceIndexItem, - (ExtensionRolesHelper.ImageFormat?)oldValue, - (ExtensionRolesHelper.ImageFormat?)newValue, differenceList); + CompareImageFormat(propertyB.Name, sourceIndexItem, + ( ExtensionRolesHelper.ImageFormat? )oldValue, + ( ExtensionRolesHelper.ImageFormat? )newValue, differenceList); } } @@ -99,7 +99,7 @@ public static List Compare(FileIndexItem sourceIndexItem, FileIndexItem? private static bool PropertyCanRead(PropertyInfo propertyA, PropertyInfo? propertyB) { return propertyB == null || !propertyA.CanRead || - !propertyB.CanRead; + !propertyB.CanRead; } /// @@ -114,15 +114,15 @@ public static FileIndexItem Set(FileIndexItem? sourceIndexItem, string fieldName { sourceIndexItem ??= new FileIndexItem(); if ( !CheckIfPropertyExist(fieldName) ) return sourceIndexItem; - + // Compare input types, fieldType(object=string) fileIndexType(FileIndexItem.field=string) // wrong types are ignored by default - + PropertyInfo[] propertiesA = new FileIndexItem().GetType().GetProperties( BindingFlags.Public | BindingFlags.Instance); - var property = Array.Find(propertiesA,p => + var property = Array.Find(propertiesA, p => string.Equals(p.Name, fieldName, StringComparison.InvariantCultureIgnoreCase)); - + var fieldType = fieldContent.GetType(); var fileIndexType = property?.PropertyType; if ( fileIndexType == fieldType ) @@ -140,32 +140,32 @@ public static FileIndexItem Set(FileIndexItem? sourceIndexItem, string fieldName /// object type is defined in fileIndexItem public static object? Get(FileIndexItem? sourceIndexItem, string fieldName) { - if ( CheckIfPropertyExist(fieldName) && sourceIndexItem != null) + if ( CheckIfPropertyExist(fieldName) && sourceIndexItem != null ) { PropertyInfo[] propertiesA = new FileIndexItem().GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance); - return Array.Find(propertiesA,p => + return Array.Find(propertiesA, p => string.Equals(p.Name, fieldName, StringComparison.InvariantCultureIgnoreCase))? .GetValue(sourceIndexItem, null); } return null; } - + /// /// Check if property exist in FileIndexItem /// /// name e.g. Tags /// bool, true=exist [SuppressMessage("Usage", "S6605: Collection-specific Exists " + - "method should be used instead of the Any extension.")] + "method should be used instead of the Any extension.")] public static bool CheckIfPropertyExist(string fieldName) { PropertyInfo[] propertiesA = new FileIndexItem().GetType().GetProperties( BindingFlags.Public | BindingFlags.Instance); - return propertiesA.Any(p => string.Equals(p.Name, + return propertiesA.Any(p => string.Equals(p.Name, fieldName, StringComparison.InvariantCultureIgnoreCase)); } - + /// /// Update compared values /// @@ -178,18 +178,18 @@ public static FileIndexItem SetCompare(FileIndexItem sourceIndexItem, FileIndexI updateObject ??= new FileIndexItem(); PropertyInfo[] propertiesA = sourceIndexItem.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); PropertyInfo[] propertiesB = updateObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - + int count = propertiesA.Length; for ( int i = 0; i < count; i++ ) { if ( ( !propertiesA[i].CanRead ) || ( !propertiesB[i].CanRead ) ) continue; - if(!differenceList.Contains(propertiesA[i].Name)) continue; - - var newRotationValue = propertiesB [i].GetValue(updateObject, null); - + if ( !differenceList.Contains(propertiesA[i].Name) ) continue; + + var newRotationValue = propertiesB[i].GetValue(updateObject, null); + sourceIndexItem.GetType().GetProperty(propertiesA[i].Name)!.SetValue(sourceIndexItem, newRotationValue, null); } - + return sourceIndexItem; } @@ -201,14 +201,14 @@ public static FileIndexItem SetCompare(FileIndexItem sourceIndexItem, FileIndexI /// oldRotationValue to compare with newRotationValue /// oldRotationValue to compare with newRotationValue /// list of different values - internal static void CompareRotation(string propertyName, FileIndexItem sourceIndexItem, + internal static void CompareRotation(string propertyName, FileIndexItem sourceIndexItem, FileIndexItem.Rotation? oldRotationValue, FileIndexItem.Rotation? newRotationValue, List differenceList) { - if (oldRotationValue == newRotationValue || newRotationValue == null || newRotationValue == FileIndexItem.Rotation.DoNotChange) return; + if ( oldRotationValue == newRotationValue || newRotationValue == null || newRotationValue == FileIndexItem.Rotation.DoNotChange ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newRotationValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare double type /// @@ -217,15 +217,15 @@ internal static void CompareRotation(string propertyName, FileIndexItem sourceIn /// oldDoubleValue to compare with newDoubleValue /// oldDoubleValue to compare with newDoubleValue /// list of different values - internal static void CompareDouble(string propertyName, FileIndexItem sourceIndexItem, + internal static void CompareDouble(string propertyName, FileIndexItem sourceIndexItem, double? oldDoubleValue, double? newDoubleValue, List differenceList) { // Dont allow to overwrite with default 0 value - if (oldDoubleValue == null || newDoubleValue == null || Math.Abs(oldDoubleValue.Value - newDoubleValue.Value) < 0.0001 || newDoubleValue == 0) return; + if ( oldDoubleValue == null || newDoubleValue == null || Math.Abs(oldDoubleValue.Value - newDoubleValue.Value) < 0.0001 || newDoubleValue == 0 ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDoubleValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare ushort type /// @@ -234,15 +234,15 @@ internal static void CompareDouble(string propertyName, FileIndexItem sourceInde /// oldUshortValue to compare with newUshortValue /// oldUshortValue to compare with newUshortValue /// list of different values - internal static void CompareUshort(string propertyName, FileIndexItem sourceIndexItem, + internal static void CompareUshort(string propertyName, FileIndexItem sourceIndexItem, ushort? oldUshortValue, ushort? newUshortValue, List differenceList) { // Dont allow to overwrite with default 0 value - if (oldUshortValue == newUshortValue || newUshortValue == null || newUshortValue == 0) return; + if ( oldUshortValue == newUshortValue || newUshortValue == null || newUshortValue == 0 ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newUshortValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare imageFormat type /// @@ -251,15 +251,15 @@ internal static void CompareUshort(string propertyName, FileIndexItem sourceInde /// two values to compare with /// two values to compare with /// list of differences - internal static void CompareImageFormat(string propertyName, FileIndexItem sourceIndexItem, - ExtensionRolesHelper.ImageFormat? oldImageFormatValue, + internal static void CompareImageFormat(string propertyName, FileIndexItem sourceIndexItem, + ExtensionRolesHelper.ImageFormat? oldImageFormatValue, ExtensionRolesHelper.ImageFormat? newImageFormatValue, List differenceList) { - if (oldImageFormatValue == newImageFormatValue || newImageFormatValue == null || newImageFormatValue == ExtensionRolesHelper.ImageFormat.unknown) return; + if ( oldImageFormatValue == newImageFormatValue || newImageFormatValue == null || newImageFormatValue == ExtensionRolesHelper.ImageFormat.unknown ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newImageFormatValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare image stab value /// @@ -268,14 +268,14 @@ internal static void CompareImageFormat(string propertyName, FileIndexItem sourc /// two values to compare with /// two values to compare with /// list of differences - private static void CompareImageStabilisationType(string propertyName, FileIndexItem sourceIndexItem, + private static void CompareImageStabilisationType(string propertyName, FileIndexItem sourceIndexItem, ImageStabilisationType? oldImageStabValue, ImageStabilisationType? newImageStabValue, List differenceList) { - if (oldImageStabValue == newImageStabValue || newImageStabValue == null || newImageStabValue == ImageStabilisationType.Unknown ) return; + if ( oldImageStabValue == newImageStabValue || newImageStabValue == null || newImageStabValue == ImageStabilisationType.Unknown ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newImageStabValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare DateTime type /// @@ -284,15 +284,15 @@ private static void CompareImageStabilisationType(string propertyName, FileIndex /// oldDateValue to compare with newDateValue /// oldDateValue to compare with newDateValue /// list of different values - internal static void CompareDateTime(string propertyName, FileIndexItem sourceIndexItem, + internal static void CompareDateTime(string propertyName, FileIndexItem sourceIndexItem, DateTime? oldDateValue, DateTime? newDateValue, List differenceList) { // Dont allow to overwrite with default year 0001 - if (oldDateValue == newDateValue || newDateValue == null || newDateValue.Value.Year < 2) return; + if ( oldDateValue == newDateValue || newDateValue == null || newDateValue.Value.Year < 2 ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDateValue.Value, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare ColorClass type /// @@ -301,15 +301,15 @@ internal static void CompareDateTime(string propertyName, FileIndexItem sourceIn /// oldColorValue to compare with newColorValue /// oldColorValue to compare with newColorValue /// list of different values - internal static void CompareColor(string propertyName, FileIndexItem sourceIndexItem, ColorClassParser.Color? oldColorValue, + internal static void CompareColor(string propertyName, FileIndexItem sourceIndexItem, ColorClassParser.Color? oldColorValue, ColorClassParser.Color? newColorValue, List differenceList) { - if (oldColorValue == newColorValue || newColorValue == ColorClassParser.Color.DoNotChange || newColorValue == null) return; + if ( oldColorValue == newColorValue || newColorValue == ColorClassParser.Color.DoNotChange || newColorValue == null ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newColorValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - - + + /// /// Compare List String /// @@ -318,16 +318,16 @@ internal static void CompareColor(string propertyName, FileIndexItem sourceIndex /// oldListStringValue to compare with newListStringValue /// newListStringValue to compare with oldListStringValue /// list of different values - internal static void CompareListString(string propertyName, FileIndexItem sourceIndexItem, + internal static void CompareListString(string propertyName, FileIndexItem sourceIndexItem, List? oldListStringValue, List? newListStringValue, List differenceList) { if ( oldListStringValue == null || newListStringValue?.Count == 0 ) return; if ( oldListStringValue.Equals(newListStringValue) ) return; - + sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListStringValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare Nullable bool type /// @@ -336,10 +336,10 @@ internal static void CompareListString(string propertyName, FileIndexItem source /// oldBoolValue to compare with newBoolValue /// oldBoolValue to compare with newBoolValue /// list of different values - internal static void CompareNullableBool(string propertyName, FileIndexItem sourceIndexItem, + internal static void CompareNullableBool(string propertyName, FileIndexItem sourceIndexItem, bool? oldBoolValue, bool? newBoolValue, List differenceList) { - if ( newBoolValue == null || oldBoolValue == newBoolValue) return; + if ( newBoolValue == null || oldBoolValue == newBoolValue ) return; var property = sourceIndexItem.GetType().GetProperty(propertyName); if ( property == null ) return; property.SetValue(sourceIndexItem, newBoolValue, null); @@ -355,41 +355,41 @@ internal static void CompareNullableBool(string propertyName, FileIndexItem sour /// oldStringValue to compare with newStringValue /// list of different values /// to add after list (if tags) - internal static void CompareString(string propertyName, FileIndexItem sourceIndexItem, - string? oldStringValue, string? newStringValue, + internal static void CompareString(string propertyName, FileIndexItem sourceIndexItem, + string? oldStringValue, string? newStringValue, List differenceList, bool append) { // ignore capitals - if (string.IsNullOrEmpty(newStringValue) || - string.Equals(oldStringValue, newStringValue, StringComparison.InvariantCultureIgnoreCase)) return; + if ( string.IsNullOrEmpty(newStringValue) || + string.Equals(oldStringValue, newStringValue, StringComparison.InvariantCultureIgnoreCase) ) return; if ( propertyName is nameof(FileIndexItem.FileName) or nameof(FileIndexItem.FilePath) ) { return; } - + var propertyObject = sourceIndexItem.GetType().GetProperty(propertyName); if ( propertyObject == null || !propertyObject.CanWrite ) { return; } - - if (!append) + + if ( !append ) { newStringValue = StringHelper.AsciiNullReplacer(newStringValue); propertyObject.SetValue(sourceIndexItem, newStringValue, null); } // only for appending tags: ==> - else if (propertyName == nameof(FileIndexItem.Tags)) + else if ( propertyName == nameof(FileIndexItem.Tags) ) { - propertyObject.SetValue(sourceIndexItem, oldStringValue + ", " + newStringValue,null); + propertyObject.SetValue(sourceIndexItem, oldStringValue + ", " + newStringValue, null); } else { - propertyObject.SetValue(sourceIndexItem,oldStringValue + " " + newStringValue,null); + propertyObject.SetValue(sourceIndexItem, oldStringValue + " " + newStringValue, null); } - + differenceList.Add(propertyName.ToLowerInvariant()); } } diff --git a/starsky/starsky.foundation.database/Helpers/MySqlDatabaseFixes.cs b/starsky/starsky.foundation.database/Helpers/MySqlDatabaseFixes.cs index 2a659ad773..9fec305aec 100644 --- a/starsky/starsky.foundation.database/Helpers/MySqlDatabaseFixes.cs +++ b/starsky/starsky.foundation.database/Helpers/MySqlDatabaseFixes.cs @@ -1,5 +1,3 @@ -#nullable enable -using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -10,14 +8,14 @@ namespace starsky.foundation.database.Helpers { - public sealed class MySqlDatabaseFixes { private readonly MySqlConnection? _connection; private readonly AppSettings _appSettings; private readonly IWebLogger _logger; - public MySqlDatabaseFixes(MySqlConnection? connection, AppSettings appSettings, IWebLogger logger) + public MySqlDatabaseFixes(MySqlConnection? connection, AppSettings appSettings, + IWebLogger logger) { _connection = connection; _appSettings = appSettings; @@ -35,20 +33,21 @@ public MySqlDatabaseFixes(MySqlConnection? connection, AppSettings appSettings, public async Task FixUtf8Encoding(List tableNames) { var isUtf8 = await IsUtf8(); - if (isUtf8 != false ) return isUtf8; + if ( isUtf8 != false ) return isUtf8; await SetDatabaseSettingToUtf8(); foreach ( var tableName in tableNames ) { await SetTableToUtf8(tableName); } + return true; } internal async Task SetTableToUtf8(string? tableName) { - if ( _connection == null || tableName == null) return null; + if ( _connection == null || tableName == null ) return null; var query = "ALTER TABLE `" + tableName + "`" + - " CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"; + " CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"; await ExecuteNonQueryAsync(query); return true; } @@ -57,7 +56,7 @@ public MySqlDatabaseFixes(MySqlConnection? connection, AppSettings appSettings, { if ( _connection == null ) return null; var query = "ALTER DATABASE `" + _connection.Database + "` " + - "CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"; + "CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"; await ExecuteNonQueryAsync(query); return true; } @@ -66,10 +65,11 @@ public MySqlDatabaseFixes(MySqlConnection? connection, AppSettings appSettings, { var myCommand = new MySqlCommand(query); myCommand.Connection = _connection; - if (myCommand.Connection?.State != ConnectionState.Open ) + if ( myCommand.Connection?.State != ConnectionState.Open ) { return null; } + return await myCommand.ExecuteNonQueryAsync(); } @@ -78,18 +78,21 @@ public MySqlDatabaseFixes(MySqlConnection? connection, AppSettings appSettings, if ( _connection == null ) return null; var query = "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME " + - "FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '"+ _connection.Database + "'; "; + "FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '" + + _connection.Database + "'; "; var command = new MySqlCommand(query, _connection); - + var tableNames = await ReadCommand(command); - var isUtf8 = tableNames.FirstOrDefault()?.Contains( "utf8mb4,utf8mb4_general_ci") == true; + var isUtf8 = tableNames.FirstOrDefault()?.Contains("utf8mb4,utf8mb4_general_ci") == + true; return isUtf8; } public async Task OpenConnection() { - if (_connection == null || _appSettings.DatabaseType != AppSettings.DatabaseTypeList.Mysql ) + if ( _connection == null || + _appSettings.DatabaseType != AppSettings.DatabaseTypeList.Mysql ) { return; } @@ -100,22 +103,25 @@ public async Task OpenConnection() { await _connection.OpenAsync(); } - catch ( MySqlException exception) + catch ( MySqlException exception ) { - _logger.LogError($"[MySqlDatabaseFixes] OpenAsync MySqlException {exception.Message}", exception); + _logger.LogError( + $"[MySqlDatabaseFixes] OpenAsync MySqlException {exception.Message}", + exception); } } } - public async Task FixAutoIncrement( string tableName, bool dispose = false) + public async Task FixAutoIncrement(string tableName, bool dispose = false) { - if (_connection == null || _appSettings.DatabaseType != AppSettings.DatabaseTypeList.Mysql ) + if ( _connection == null || + _appSettings.DatabaseType != AppSettings.DatabaseTypeList.Mysql ) { return null; } var autoIncrementExist = await CheckAutoIncrementExist(tableName); - if (autoIncrementExist != false ) + if ( autoIncrementExist != false ) { if ( dispose ) await _connection.DisposeAsync(); return autoIncrementExist; @@ -132,17 +138,18 @@ public async Task DisposeAsync() await _connection.DisposeAsync(); } - internal async Task CheckAutoIncrementExist(string tableName, string columnName = "Id") + internal async Task CheckAutoIncrementExist(string tableName, + string columnName = "Id") { if ( _connection == null ) return null; var query = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS " + - "WHERE TABLE_NAME = '"+ tableName + "' " + - "AND COLUMN_NAME = '" + columnName + "' " + - "AND DATA_TYPE = 'int' " + - "AND COLUMN_DEFAULT IS NULL " + - "AND IS_NULLABLE = 'NO' " + - "AND EXTRA like '%auto_increment%'"; + "WHERE TABLE_NAME = '" + tableName + "' " + + "AND COLUMN_NAME = '" + columnName + "' " + + "AND DATA_TYPE = 'int' " + + "AND COLUMN_DEFAULT IS NULL " + + "AND IS_NULLABLE = 'NO' " + + "AND EXTRA like '%auto_increment%'"; var command = new MySqlCommand(query, _connection); var tableNames = await ReadCommand(command); @@ -152,28 +159,29 @@ public async Task DisposeAsync() private static async Task> ReadCommand(MySqlCommand command) { - if (command.Connection?.State != ConnectionState.Open ) + if ( command.Connection?.State != ConnectionState.Open ) { return new List(); } - + var tableNames = new List(); await using var reader = await command.ExecuteReaderAsync(); - while (reader.Read()) + while ( reader.Read() ) { // at least two columns tableNames.Add(reader.GetString(0) + "," + reader.GetString(1)); } + return tableNames; } - - internal async Task AlterTableAutoIncrement(string tableName, string columnName = "Id") + + internal async Task AlterTableAutoIncrement(string tableName, + string columnName = "Id") { if ( _connection == null ) return null; - var myInsertQuery = "ALTER TABLE `"+ tableName+ "` MODIFY " + columnName + " INTEGER NOT NULL AUTO_INCREMENT;"; + var myInsertQuery = "ALTER TABLE `" + tableName + "` MODIFY " + columnName + + " INTEGER NOT NULL AUTO_INCREMENT;"; return await ExecuteNonQueryAsync(myInsertQuery); } - } - } diff --git a/starsky/starsky.foundation.database/Helpers/PredicateBuilder.cs b/starsky/starsky.foundation.database/Helpers/PredicateBuilder.cs index 8db41b7927..930361a717 100644 --- a/starsky/starsky.foundation.database/Helpers/PredicateBuilder.cs +++ b/starsky/starsky.foundation.database/Helpers/PredicateBuilder.cs @@ -16,15 +16,15 @@ public static class PredicateBuilder /// /// type /// Expression - public static Expression> True () { return f => true; } - + public static Expression> True() { return f => true; } + /// /// Query setup negative /// /// type /// Expression - public static Expression> False () { return f => false; } - + public static Expression> False() { return f => false; } + /// /// Or Expression /// @@ -32,14 +32,14 @@ public static class PredicateBuilder /// second /// object type /// Expression - public static Expression> Or (this Expression> expr1, + public static Expression> Or(this Expression> expr1, Expression> expr2) { - var invokedExpr = Expression.Invoke (expr2, expr1.Parameters); + var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); return Expression.Lambda> - (Expression.OrElse (expr1.Body, invokedExpr), expr1.Parameters); + (Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); } - + /// /// And Expression /// @@ -47,12 +47,12 @@ public static Expression> Or (this Expression> ex /// second /// object type /// Expression - public static Expression> And (this Expression> expr1, + public static Expression> And(this Expression> expr1, Expression> expr2) { - var invokedExpr = Expression.Invoke (expr2, expr1.Parameters.Cast ()); + var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast()); return Expression.Lambda> - (Expression.AndAlso (expr1.Body, invokedExpr), expr1.Parameters); + (Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); } /// @@ -61,10 +61,10 @@ public static Expression> And (this Expression> e /// /// /// - public static Expression> OrLoop(List>> predicates) + public static Expression> OrLoop(List>> predicates) { var predicate = False(); - + for ( var i = 0; i < predicates.Count; i++ ) { if ( i == 0 ) @@ -75,13 +75,13 @@ public static Expression> OrLoop(List>> else { var item2 = predicates[i]; - predicate = predicate.Or(item2); + predicate = predicate.Or(item2); } } return predicate; } - + /// /// Combine two queries /// @see https://stackoverflow.com/a/457328 @@ -94,7 +94,7 @@ public static Expression> AndAlso( this Expression> expr1, Expression> expr2) { - var parameter = Expression.Parameter(typeof (T)); + var parameter = Expression.Parameter(typeof(T)); var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); var left = leftVisitor.Visit(expr1.Body); @@ -120,7 +120,7 @@ public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) public override Expression Visit(Expression? node) { - if (node == _oldValue) + if ( node == _oldValue ) return _newValue; return base.Visit(node)!; } diff --git a/starsky/starsky.foundation.database/Helpers/ReflectionValueHelper.cs b/starsky/starsky.foundation.database/Helpers/ReflectionValueHelper.cs index d651cd4526..6daab2c1bb 100644 --- a/starsky/starsky.foundation.database/Helpers/ReflectionValueHelper.cs +++ b/starsky/starsky.foundation.database/Helpers/ReflectionValueHelper.cs @@ -4,8 +4,10 @@ namespace starsky.foundation.database.Helpers; [SuppressMessage("Usage", "S3011:Make sure that this accessibility bypass is safe here", Justification = "Safe")] -public static class ReflectionExtensions { - public static T GetReflectionFieldValue(this object obj, string name) { +public static class ReflectionExtensions +{ + public static T GetReflectionFieldValue(this object obj, string name) + { // Set the flags so that private and public fields from instances will be found const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var field = obj.GetType().GetField(name, bindingFlags); diff --git a/starsky/starsky.foundation.database/Helpers/RunMigrations.cs b/starsky/starsky.foundation.database/Helpers/RunMigrations.cs index e76022aee4..d2e0552f83 100644 --- a/starsky/starsky.foundation.database/Helpers/RunMigrations.cs +++ b/starsky/starsky.foundation.database/Helpers/RunMigrations.cs @@ -1,5 +1,4 @@ using System; -using System.Data.Common; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -9,36 +8,36 @@ using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; -using starsky.foundation.platform.Services; namespace starsky.foundation.database.Helpers { public static class RunMigrations { - - internal static async Task MigrateAsync(AppSettings appSettings, ApplicationDbContext dbContext, IWebLogger logger) + internal static async Task MigrateAsync(AppSettings appSettings, + ApplicationDbContext dbContext, IWebLogger logger) { if ( appSettings.DatabaseType == AppSettings.DatabaseTypeList.InMemoryDatabase ) { return true; } - + await dbContext.Database.MigrateAsync(); if ( appSettings.DatabaseType != - AppSettings.DatabaseTypeList.Mysql ) return true; - + AppSettings.DatabaseTypeList.Mysql ) return true; + var connection = new MySqlConnection(appSettings.DatabaseConnection); await MysqlFixes(connection, appSettings, dbContext, logger); return true; } - internal static async Task MysqlFixes(MySqlConnection connection, AppSettings appSettings, ApplicationDbContext dbContext, IWebLogger logger) + internal static async Task MysqlFixes(MySqlConnection connection, + AppSettings appSettings, ApplicationDbContext dbContext, IWebLogger logger) { var databaseFixes = new MySqlDatabaseFixes(connection, appSettings, logger); await databaseFixes.OpenConnection(); - + var tableNames = dbContext.Model.GetEntityTypes() .Select(t => t.GetTableName()) .Distinct() @@ -56,21 +55,22 @@ public static async Task Run(IServiceScope serviceScope, int retryCount = 2) var logger = serviceScope.ServiceProvider.GetRequiredService(); var appSettings = serviceScope.ServiceProvider.GetRequiredService(); - await Run(dbContext,logger,appSettings,retryCount); + await Run(dbContext, logger, appSettings, retryCount); } - - public static async Task Run(ApplicationDbContext dbContext, IWebLogger logger, AppSettings appSettings, int retryCount = 2) + + public static async Task Run(ApplicationDbContext dbContext, IWebLogger logger, + AppSettings appSettings, int retryCount = 2) { async Task Migrate() { return await MigrateAsync(appSettings, dbContext, logger); } - + try { - await RetryHelper.DoAsync(Migrate, TimeSpan.FromSeconds(2),retryCount); + await RetryHelper.DoAsync(Migrate, TimeSpan.FromSeconds(2), retryCount); } - catch (AggregateException exception) + catch ( AggregateException exception ) { logger.LogInformation("[RunMigrations] start migration failed"); logger.LogError(exception.Message); diff --git a/starsky/starsky.foundation.database/Helpers/SetupDatebaseTypes.cs b/starsky/starsky.foundation.database/Helpers/SetupDatebaseTypes.cs index 3e39b2d9f5..b02cc12b2f 100644 --- a/starsky/starsky.foundation.database/Helpers/SetupDatebaseTypes.cs +++ b/starsky/starsky.foundation.database/Helpers/SetupDatebaseTypes.cs @@ -1,12 +1,9 @@ using System; -using Microsoft.ApplicationInsights; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using MySqlConnector; using starsky.foundation.database.Data; using starsky.foundation.platform.Models; -using starsky.foundation.databasetelemetry.Helpers; -using starsky.foundation.databasetelemetry.Services; using starsky.foundation.platform.Interfaces; namespace starsky.foundation.database.Helpers @@ -16,16 +13,15 @@ public sealed class SetupDatabaseTypes private readonly AppSettings _appSettings; private readonly IServiceCollection? _services; private readonly IWebLogger? _logger; - private readonly TelemetryClient? _telemetryClient; - public SetupDatabaseTypes(AppSettings appSettings, IServiceCollection? services = null, IWebLogger? logger = null) + public SetupDatabaseTypes(AppSettings appSettings, IServiceCollection? services = null, + IWebLogger? logger = null) { _appSettings = appSettings; _services = services; // if null get from service collection logger ??= _services?.BuildServiceProvider().GetService(); - _telemetryClient = _services?.BuildServiceProvider().GetService(); _logger = logger; } @@ -40,81 +36,62 @@ private ServerVersion GetServerVersionMySql() try { return ServerVersion.AutoDetect( - _appSettings.DatabaseConnection); + _appSettings.DatabaseConnection); } - catch ( MySqlException) + catch ( MySqlException ) { // nothing here } + return new MariaDbServerVersion("10.2"); } - + /// /// Setup database connection /// /// Assembly name, used for running migrations /// /// Missing arguments - internal DbContextOptions BuilderDbFactorySwitch(string? foundationDatabaseName = "") + internal DbContextOptions BuilderDbFactorySwitch( + string? foundationDatabaseName = "") { switch ( _appSettings.DatabaseType ) { case ( AppSettings.DatabaseTypeList.Mysql ): - + var mysql = new DbContextOptionsBuilder() - .UseMySql(_appSettings.DatabaseConnection, GetServerVersionMySql(), mySqlOptions => - { - mySqlOptions.EnableRetryOnFailure(2); - if ( !string.IsNullOrWhiteSpace(foundationDatabaseName) ) + .UseMySql(_appSettings.DatabaseConnection, GetServerVersionMySql(), + mySqlOptions => { - mySqlOptions.MigrationsAssembly(foundationDatabaseName); - } - }); - EnableDatabaseTracking(mysql); + mySqlOptions.EnableRetryOnFailure(2); + if ( !string.IsNullOrWhiteSpace(foundationDatabaseName) ) + { + mySqlOptions.MigrationsAssembly(foundationDatabaseName); + } + }); return mysql.Options; case AppSettings.DatabaseTypeList.InMemoryDatabase: var memoryDatabase = new DbContextOptionsBuilder() - .UseInMemoryDatabase(string.IsNullOrEmpty(_appSettings.DatabaseConnection) ? "starsky" : _appSettings.DatabaseConnection ); + .UseInMemoryDatabase(string.IsNullOrEmpty(_appSettings.DatabaseConnection) + ? "starsky" + : _appSettings.DatabaseConnection); return memoryDatabase.Options; case AppSettings.DatabaseTypeList.Sqlite: var sqlite = new DbContextOptionsBuilder() - .UseSqlite(_appSettings.DatabaseConnection, + .UseSqlite(_appSettings.DatabaseConnection, b => { - if (! string.IsNullOrWhiteSpace(foundationDatabaseName) ) + if ( !string.IsNullOrWhiteSpace(foundationDatabaseName) ) { b.MigrationsAssembly(foundationDatabaseName); } }); - EnableDatabaseTracking(sqlite); return sqlite.Options; default: throw new AggregateException(nameof(_appSettings.DatabaseType)); } } - private bool IsDatabaseTrackingEnabled() - { - return !string.IsNullOrEmpty(_appSettings - .ApplicationInsightsConnectionString) && _appSettings.ApplicationInsightsDatabaseTracking == true; - } - - internal bool EnableDatabaseTracking( DbContextOptionsBuilder databaseOptionsBuilder) - { - if (!IsDatabaseTrackingEnabled()) - { - return false; - } - databaseOptionsBuilder.AddInterceptors( - new DatabaseTelemetryInterceptor( - TelemetryConfigurationHelper.InitTelemetryClient( - _appSettings.ApplicationInsightsConnectionString, - _appSettings.ApplicationType.ToString(),_logger,_telemetryClient) - ) - ); - return true; - } - /// /// Setup database connection /// use boot parameters to run with EF Migrations and a direct connection @@ -130,37 +107,10 @@ public void BuilderDb(string? foundationDatabaseName = "") if ( _logger != null && _appSettings.IsVerbose() ) { _logger.LogInformation($"Database connection: {_appSettings.DatabaseConnection}"); - _logger.LogInformation($"Application Insights Database tracking is {IsDatabaseTrackingEnabled()}" ); } -#if ENABLE_DEFAULT_DATABASE - // dirty hack - _services.AddDbContext(options => - options.UseSqlite(_appSettings.DatabaseConnection, - b => - { - if (! string.IsNullOrWhiteSpace(foundationDatabaseName) ) - { - b.MigrationsAssembly(foundationDatabaseName); - } - })); -#endif -#if ENABLE_MYSQL_DATABASE - // dirty hack - - _services.AddDbContext(options => - options.UseMySql(_appSettings.DatabaseConnection, GetServerVersionMySql(), - b => - { - if (! string.IsNullOrWhiteSpace(foundationDatabaseName) ) - { - b.MigrationsAssembly(foundationDatabaseName); - } - })); -#endif - - _services.AddScoped(_ => new ApplicationDbContext(BuilderDbFactorySwitch(foundationDatabaseName))); + _services.AddScoped(_ => + new ApplicationDbContext(BuilderDbFactorySwitch(foundationDatabaseName))); } - } } diff --git a/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs b/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs index 39bad585b3..b43520434b 100644 --- a/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs +++ b/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs @@ -1,138 +1,138 @@ -using System; +using System; using System.Collections.Generic; using starsky.foundation.database.Models; using starsky.foundation.platform.Models; namespace starsky.foundation.database.Helpers { - public sealed class StatusCodesHelper - { - private readonly AppSettings _appSettings; - - public StatusCodesHelper(AppSettings appSettings) - { - _appSettings = appSettings; - } - - public FileIndexItem.ExifStatus IsReadOnlyStatus(FileIndexItem fileIndexItem) - { - if (fileIndexItem.IsDirectory == true && _appSettings.IsReadOnly(fileIndexItem.FilePath!)) - { - return FileIndexItem.ExifStatus.DirReadOnly; - } - - if ( _appSettings.IsReadOnly(fileIndexItem.ParentDirectory!) ) - { - return FileIndexItem.ExifStatus.ReadOnly; - } - - return FileIndexItem.ExifStatus.Default; - } - - public FileIndexItem.ExifStatus IsReadOnlyStatus(DetailView? detailView) - { - if(_appSettings == null) throw new DllNotFoundException("add app settings to ctor"); - - if ( detailView == null ) - { - return FileIndexItem.ExifStatus.Default; - } - - if (detailView.IsDirectory && _appSettings.IsReadOnly(detailView.SubPath)) - { - return FileIndexItem.ExifStatus.DirReadOnly; - } - - if ( _appSettings.IsReadOnly(detailView.FileIndexItem?.ParentDirectory!) ) - { - return FileIndexItem.ExifStatus.ReadOnly; - } - - return FileIndexItem.ExifStatus.Default; - } - - public static FileIndexItem.ExifStatus IsDeletedStatus(FileIndexItem? fileIndexItem) - { - return fileIndexItem?.Tags != null && fileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ? - FileIndexItem.ExifStatus.Deleted : FileIndexItem.ExifStatus.Default; - } - - public static FileIndexItem.ExifStatus IsDeletedStatus(DetailView? detailView) - { - if (!string.IsNullOrEmpty(detailView?.FileIndexItem?.Tags) && detailView.FileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString)) - { - return FileIndexItem.ExifStatus.Deleted; - } - - return FileIndexItem.ExifStatus.Default; - } - - /// - /// Does deside if the loop should be stopped, true = stop - /// Uses FileCollectionsCheck - /// Add for all types exept for OK/Readonly! - /// - /// the main object to return later - /// the status by FileCollectionsCheck - /// list of object that will be returned - /// If true skip the next code - public static bool ReturnExifStatusError(FileIndexItem statusModel, - FileIndexItem.ExifStatus statusResults, List fileIndexResultsList) - { - switch (statusResults) - { - case FileIndexItem.ExifStatus.DirReadOnly: - statusModel.IsDirectory = true; - statusModel.Status = FileIndexItem.ExifStatus.DirReadOnly; - fileIndexResultsList.Add(statusModel); - return true; - case FileIndexItem.ExifStatus.NotFoundNotInIndex: - statusModel.Status = FileIndexItem.ExifStatus.NotFoundNotInIndex; - fileIndexResultsList.Add(statusModel); - return true; - case FileIndexItem.ExifStatus.NotFoundSourceMissing: - statusModel.Status = FileIndexItem.ExifStatus.NotFoundSourceMissing; - fileIndexResultsList.Add(statusModel); - return true; - case FileIndexItem.ExifStatus.ReadOnly: - statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; - fileIndexResultsList.Add(statusModel); - return true; - case FileIndexItem.ExifStatus.OperationNotSupported: - statusModel.Status = FileIndexItem.ExifStatus.OperationNotSupported; - fileIndexResultsList.Add(statusModel); - return true; - case FileIndexItem.ExifStatus.ExifWriteNotSupported: - statusModel.Status = FileIndexItem.ExifStatus.ExifWriteNotSupported; - fileIndexResultsList.Add(statusModel); - return true; - } - return false; - } - - public static bool ReadonlyDenied(FileIndexItem statusModel, - FileIndexItem.ExifStatus statusResults, List fileIndexResultsList) - { - switch (statusResults) - { - case FileIndexItem.ExifStatus.ReadOnly: - statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; - fileIndexResultsList.Add(statusModel); - return true; - } - return false; - } - - public static void ReadonlyAllowed(FileIndexItem statusModel, - FileIndexItem.ExifStatus statusResults, List fileIndexResultsList) - { - // Readonly is allowed - if ( statusResults != FileIndexItem.ExifStatus.ReadOnly ) return; - - statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; - fileIndexResultsList.Add(statusModel); - } - - - } + public sealed class StatusCodesHelper + { + private readonly AppSettings _appSettings; + + public StatusCodesHelper(AppSettings appSettings) + { + _appSettings = appSettings; + } + + public FileIndexItem.ExifStatus IsReadOnlyStatus(FileIndexItem fileIndexItem) + { + if ( fileIndexItem.IsDirectory == true && _appSettings.IsReadOnly(fileIndexItem.FilePath!) ) + { + return FileIndexItem.ExifStatus.DirReadOnly; + } + + if ( _appSettings.IsReadOnly(fileIndexItem.ParentDirectory!) ) + { + return FileIndexItem.ExifStatus.ReadOnly; + } + + return FileIndexItem.ExifStatus.Default; + } + + public FileIndexItem.ExifStatus IsReadOnlyStatus(DetailView? detailView) + { + if ( _appSettings == null ) throw new DllNotFoundException("add app settings to ctor"); + + if ( detailView == null ) + { + return FileIndexItem.ExifStatus.Default; + } + + if ( detailView.IsDirectory && _appSettings.IsReadOnly(detailView.SubPath) ) + { + return FileIndexItem.ExifStatus.DirReadOnly; + } + + if ( _appSettings.IsReadOnly(detailView.FileIndexItem?.ParentDirectory!) ) + { + return FileIndexItem.ExifStatus.ReadOnly; + } + + return FileIndexItem.ExifStatus.Default; + } + + public static FileIndexItem.ExifStatus IsDeletedStatus(FileIndexItem? fileIndexItem) + { + return fileIndexItem?.Tags != null && fileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ? + FileIndexItem.ExifStatus.Deleted : FileIndexItem.ExifStatus.Default; + } + + public static FileIndexItem.ExifStatus IsDeletedStatus(DetailView? detailView) + { + if ( !string.IsNullOrEmpty(detailView?.FileIndexItem?.Tags) && detailView.FileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) + { + return FileIndexItem.ExifStatus.Deleted; + } + + return FileIndexItem.ExifStatus.Default; + } + + /// + /// Does deside if the loop should be stopped, true = stop + /// Uses FileCollectionsCheck + /// Add for all types exept for OK/Readonly! + /// + /// the main object to return later + /// the status by FileCollectionsCheck + /// list of object that will be returned + /// If true skip the next code + public static bool ReturnExifStatusError(FileIndexItem statusModel, + FileIndexItem.ExifStatus statusResults, List fileIndexResultsList) + { + switch ( statusResults ) + { + case FileIndexItem.ExifStatus.DirReadOnly: + statusModel.IsDirectory = true; + statusModel.Status = FileIndexItem.ExifStatus.DirReadOnly; + fileIndexResultsList.Add(statusModel); + return true; + case FileIndexItem.ExifStatus.NotFoundNotInIndex: + statusModel.Status = FileIndexItem.ExifStatus.NotFoundNotInIndex; + fileIndexResultsList.Add(statusModel); + return true; + case FileIndexItem.ExifStatus.NotFoundSourceMissing: + statusModel.Status = FileIndexItem.ExifStatus.NotFoundSourceMissing; + fileIndexResultsList.Add(statusModel); + return true; + case FileIndexItem.ExifStatus.ReadOnly: + statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; + fileIndexResultsList.Add(statusModel); + return true; + case FileIndexItem.ExifStatus.OperationNotSupported: + statusModel.Status = FileIndexItem.ExifStatus.OperationNotSupported; + fileIndexResultsList.Add(statusModel); + return true; + case FileIndexItem.ExifStatus.ExifWriteNotSupported: + statusModel.Status = FileIndexItem.ExifStatus.ExifWriteNotSupported; + fileIndexResultsList.Add(statusModel); + return true; + } + return false; + } + + public static bool ReadonlyDenied(FileIndexItem statusModel, + FileIndexItem.ExifStatus statusResults, List fileIndexResultsList) + { + switch ( statusResults ) + { + case FileIndexItem.ExifStatus.ReadOnly: + statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; + fileIndexResultsList.Add(statusModel); + return true; + } + return false; + } + + public static void ReadonlyAllowed(FileIndexItem statusModel, + FileIndexItem.ExifStatus statusResults, List fileIndexResultsList) + { + // Readonly is allowed + if ( statusResults != FileIndexItem.ExifStatus.ReadOnly ) return; + + statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; + fileIndexResultsList.Add(statusModel); + } + + + } } diff --git a/starsky/starsky.foundation.database/Import/ImportQuery.cs b/starsky/starsky.foundation.database/Import/ImportQuery.cs index 55f52e0e8d..f04b49a615 100644 --- a/starsky/starsky.foundation.database/Import/ImportQuery.cs +++ b/starsky/starsky.foundation.database/Import/ImportQuery.cs @@ -32,7 +32,7 @@ public sealed class ImportQuery : IImportQuery /// console output /// /// - public ImportQuery(IServiceScopeFactory? scopeFactory, IConsole console, IWebLogger logger, ApplicationDbContext? dbContext = null) + public ImportQuery(IServiceScopeFactory? scopeFactory, IConsole console, IWebLogger logger, ApplicationDbContext? dbContext = null) { _scopeFactory = scopeFactory; @@ -48,7 +48,7 @@ public ImportQuery(IServiceScopeFactory? scopeFactory, IConsole console, IWebLog /// database context private ApplicationDbContext GetDbContext() { - return (_scopeFactory != null ? new InjectServiceScope(_scopeFactory).Context() : _dbContext)!; + return ( _scopeFactory != null ? new InjectServiceScope(_scopeFactory).Context() : _dbContext )!; } /// @@ -64,8 +64,8 @@ public async Task IsHashInImportDbAsync(string fileHashCode) { if ( _isConnection ) { - var value = await GetDbContext().ImportIndex.CountAsync(p => - p.FileHash == fileHashCode) != 0; // there is no any in ef core + var value = await GetDbContext().ImportIndex.CountAsync(p => + p.FileHash == fileHashCode) != 0; // there is no any in ef core return value; } @@ -90,7 +90,7 @@ public async Task AddAsync(ImportIndexItem updateStatusContent, bool write // removed MySqlException catch return true; } - + /// /// Get imported items for today /// diff --git a/starsky/starsky.foundation.database/Import/ImportQueryFactory.cs b/starsky/starsky.foundation.database/Import/ImportQueryFactory.cs index 85e1193c8c..67d1fd214d 100644 --- a/starsky/starsky.foundation.database/Import/ImportQueryFactory.cs +++ b/starsky/starsky.foundation.database/Import/ImportQueryFactory.cs @@ -19,15 +19,15 @@ public ImportQueryFactory(SetupDatabaseTypes setupDatabaseTypes, IImportQuery im _console = console; _logger = logger; } - + public IImportQuery? ImportQuery() { var context = _setupDatabaseTypes.BuilderDbFactory(); if ( _importQuery is ImportQuery ) { - return new ImportQuery(null,_console,_logger,context); + return new ImportQuery(null, _console, _logger, context); } - return Activator.CreateInstance(_importQuery.GetType(), null,_console,_logger, context) as IImportQuery; + return Activator.CreateInstance(_importQuery.GetType(), null, _console, _logger, context) as IImportQuery; } } } diff --git a/starsky/starsky.foundation.database/Interfaces/INotificationQuery.cs b/starsky/starsky.foundation.database/Interfaces/INotificationQuery.cs index 4b03b95a0c..b952f65b5e 100644 --- a/starsky/starsky.foundation.database/Interfaces/INotificationQuery.cs +++ b/starsky/starsky.foundation.database/Interfaces/INotificationQuery.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq.Expressions; using System.Threading.Tasks; using starsky.foundation.database.Models; using starsky.foundation.platform.Models; @@ -13,7 +12,5 @@ public interface INotificationQuery Task> GetNewerThan(DateTime parsedDateTime); Task> GetOlderThan(DateTime parsedDateTime); Task RemoveAsync(IEnumerable content); - } } - diff --git a/starsky/starsky.foundation.database/Interfaces/IQuery.cs b/starsky/starsky.foundation.database/Interfaces/IQuery.cs index a44a98ecd3..c1a7791dd9 100644 --- a/starsky/starsky.foundation.database/Interfaces/IQuery.cs +++ b/starsky/starsky.foundation.database/Interfaces/IQuery.cs @@ -6,222 +6,214 @@ using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; -namespace starsky.foundation.database.Interfaces +namespace starsky.foundation.database.Interfaces; + +public interface IQuery { - public interface IQuery - { - List GetAllFiles(string subPath); - - /// - /// Get a list of all files inside an folder (NOT recursive) - /// But this uses a database as source - /// - /// relative database path - /// - /// list of FileIndex-objects - Task> GetAllFilesAsync(List filePaths, int timeout = 1000); - - /// - /// Get a list of all files inside an folder (NOT recursive) - /// But this uses a database as source - /// - /// relative database path - /// list of FileIndex-objects - Task> GetAllFilesAsync(string subPath); - - Task> GetAllRecursiveAsync(string subPath = "/"); - - Task> GetAllRecursiveAsync(List filePathList); - - /// - /// to do the query and return object - /// - /// subPath style - /// filter the colorClass - /// enable to show only one file with a base name - /// files that are marked as trash - /// - IEnumerable DisplayFileFolders( - string subPath = "/", - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true); - - // To make an object without any query - IEnumerable DisplayFileFolders( - List fileIndexItems, - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true); - - /// - /// to do the query and return object - /// - /// - /// - /// - /// - /// - /// - DetailView? SingleItem( - string singleItemDbPath, - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true, - SortType? sort = SortType.FileName); - - /// - /// To make an object without any query - /// - /// - /// - /// - /// - /// - /// - /// - DetailView? SingleItem( - List fileIndexItemsList, - string singleItemDbPath, - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true, - SortType? sort = SortType.FileName); - - /// - /// Get FirstOrDefault for that filePath - /// - /// subPath style - /// item - FileIndexItem? GetObjectByFilePath(string filePath); - - /// - /// Get FirstOrDefault for that filePath - /// - /// subPath style - /// time of cache - /// item - Task GetObjectByFilePathAsync(string filePath, TimeSpan? cacheTime = null); - - /// - /// - /// - /// - /// - /// - Task> GetObjectsByFilePathAsync(string inputFilePath, - bool collections); - - /// - /// Cached result that contain values - /// - /// List of filePaths - /// enable implicit raw files with the same base name - /// - Task> GetObjectsByFilePathAsync(List inputFilePaths, - bool collections); - - /// - /// Query direct by filePaths (without cache) - /// - /// List of filePaths - /// - Task> GetObjectsByFilePathQueryAsync( - List filePathList); - - Task RemoveItemAsync(FileIndexItem updateStatusContent); - - Task> RemoveItemAsync( - List updateStatusContentList); - - /// - /// Clear the directory name from the cache - /// - /// the path of the directory (there is no parent generation) - bool RemoveCacheParentItem(string directoryName); - - Task GetSubPathByHashAsync(string fileHash); - - Task> GetObjectsByFileHashAsync( - List fileHashesList, int retryCount = 2); - - void ResetItemByHash(string fileHash); - - /// - /// Only global search for all folder - /// - /// - List GetAllFolders(); - - Task> GetFoldersAsync(string subPath); - - Task> GetAllObjectsAsync(string subPath); - Task> GetAllObjectsAsync(List filePaths, - int fallbackDelay = 5000); - - Task AddItemAsync(FileIndexItem fileIndexItem); - - Task> AddRangeAsync(List fileIndexItemList); - - Task UpdateItemAsync(FileIndexItem updateStatusContent); - Task> UpdateItemAsync(List updateStatusContentList); - - RelativeObjects GetNextPrevInFolder(string currentFolder); - - - /// - /// Update parent item with all data from child items - /// - /// parent directory - /// all items that are in this folder - /// success or not - bool AddCacheParentItem(string directoryName, - List items); - - /// - /// Cache API within Query to update cached items - /// For DisplayFileFolders and SingleItem - /// - /// items to update - void CacheUpdateItem(List updateStatusContent); - - /// - /// And remove content from cache - /// - /// list of items - void RemoveCacheItem(List updateStatusContent); - - /// - /// Single remove content item from cache - /// - /// item - void RemoveCacheItem(FileIndexItem updateStatusContent); - - Tuple> CacheGetParentFolder(string subPath); - - - /// - /// Add Sub Path Folder - Parent Folders - /// root(/) - /// /2017 *= index only this folder - /// /2018 - /// If you use the cmd: $ starskycli -s "/2017" - /// the folder '2017' it self is not added - /// and all parent paths are not included - /// this class does add those parent folders - /// - /// subPath as input - /// void - Task> AddParentItemsAsync(string subPath); - IQuery Clone( ApplicationDbContext applicationDbContext); - void Invoke(ApplicationDbContext applicationDbContext); - - void SetGetObjectByFilePathCache(string filePath, - FileIndexItem result, - TimeSpan? cacheTime); - - Task DisposeAsync(); - Task CountAsync(Expression>? expression = null); - } -} + /// + /// Get a list of all files inside an folder (NOT recursive) + /// But this uses a database as source + /// + /// relative database path + /// + /// list of FileIndex-objects + Task> GetAllFilesAsync(List filePaths, int timeout = 1000); + + /// + /// Get a list of all files inside an folder (NOT recursive) + /// But this uses a database as source + /// + /// relative database path + /// list of FileIndex-objects + Task> GetAllFilesAsync(string subPath); + + Task> GetAllRecursiveAsync(string subPath = "/"); + + Task> GetAllRecursiveAsync(List filePathList); + + /// + /// to do the query and return object + /// + /// subPath style + /// filter the colorClass + /// enable to show only one file with a base name + /// files that are marked as trash + /// + IEnumerable DisplayFileFolders( + string subPath = "/", + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true); + + // To make an object without any query + IEnumerable DisplayFileFolders( + List fileIndexItems, + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true); + + /// + /// to do the query and return object + /// + /// + /// + /// + /// + /// + /// + DetailView? SingleItem( + string singleItemDbPath, + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true, + SortType? sort = SortType.FileName); + + /// + /// To make an object without any query + /// + /// + /// + /// + /// + /// + /// + /// + DetailView? SingleItem( + List fileIndexItemsList, + string singleItemDbPath, + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true, + SortType? sort = SortType.FileName); + + /// + /// Get FirstOrDefault for that filePath + /// + /// subPath style + /// item + FileIndexItem? GetObjectByFilePath(string filePath); + + /// + /// Get FirstOrDefault for that filePath + /// + /// subPath style + /// time of cache + /// item + Task GetObjectByFilePathAsync(string filePath, TimeSpan? cacheTime = null); + + /// + /// + /// + /// + /// + /// + Task> GetObjectsByFilePathAsync(string inputFilePath, + bool collections); + + /// + /// Cached result that contain values + /// + /// List of filePaths + /// enable implicit raw files with the same base name + /// + Task> GetObjectsByFilePathAsync(List inputFilePaths, + bool collections); + + /// + /// Query direct by filePaths (without cache) + /// + /// List of filePaths + /// + Task> GetObjectsByFilePathQueryAsync( + List filePathList); + + Task RemoveItemAsync(FileIndexItem updateStatusContent); + + Task> RemoveItemAsync( + List updateStatusContentList); + + /// + /// Clear the directory name from the cache + /// + /// the path of the directory (there is no parent generation) + bool RemoveCacheParentItem(string directoryName); + + Task GetSubPathByHashAsync(string fileHash); + + Task> GetObjectsByFileHashAsync( + List fileHashesList, int retryCount = 2); + + void ResetItemByHash(string fileHash); + + /// + /// Only global search for all folder + /// + /// + List GetAllFolders(); + + Task> GetFoldersAsync(string subPath); + + Task> GetAllObjectsAsync(string subPath); + + Task> GetAllObjectsAsync(List filePaths, + int fallbackDelay = 5000); + + Task AddItemAsync(FileIndexItem fileIndexItem); + + Task> AddRangeAsync(List fileIndexItemList); + + Task UpdateItemAsync(FileIndexItem updateStatusContent); + Task> UpdateItemAsync(List updateStatusContentList); + + RelativeObjects GetNextPrevInFolder(string currentFolder); + + + /// + /// Update parent item with all data from child items + /// + /// parent directory + /// all items that are in this folder + /// success or not + bool AddCacheParentItem(string directoryName, + List items); + + /// + /// Cache API within Query to update cached items + /// For DisplayFileFolders and SingleItem + /// + /// items to update + void CacheUpdateItem(List updateStatusContent); + + /// + /// And remove content from cache + /// + /// list of items + void RemoveCacheItem(List updateStatusContent); + + Tuple> CacheGetParentFolder(string subPath); + + + /// + /// Add Sub Path Folder - Parent Folders + /// root(/) + /// /2017 *= index only this folder + /// /2018 + /// If you use the cmd: $ starskycli -s "/2017" + /// the folder '2017' it self is not added + /// and all parent paths are not included + /// this class does add those parent folders + /// + /// subPath as input + /// void + Task> AddParentItemsAsync(string subPath); + + void Invoke(ApplicationDbContext applicationDbContext); + void SetGetObjectByFilePathCache(string filePath, + FileIndexItem result, + TimeSpan? cacheTime); + + Task DisposeAsync(); + + Task CountAsync(Expression>? expression = null); +} diff --git a/starsky/starsky.foundation.database/Migrations/20180308103923_initDatabase.cs b/starsky/starsky.foundation.database/Migrations/20180308103923_initDatabase.cs index 195f8f4c81..55e6b3a828 100644 --- a/starsky/starsky.foundation.database/Migrations/20180308103923_initDatabase.cs +++ b/starsky/starsky.foundation.database/Migrations/20180308103923_initDatabase.cs @@ -1,32 +1,32 @@ - + using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class initDatabase : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "FileIndex", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true), - FileName = table.Column(maxLength: 190, nullable: true), - FilePath = table.Column(maxLength: 380, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_FileIndex", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "FileIndex"); - } - } + public partial class initDatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "FileIndex", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true), + FileName = table.Column(maxLength: 190, nullable: true), + FilePath = table.Column(maxLength: 380, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FileIndex", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180308203924_FileHashTags.cs b/starsky/starsky.foundation.database/Migrations/20180308203924_FileHashTags.cs index 341b6baa7b..3cc35a209e 100644 --- a/starsky/starsky.foundation.database/Migrations/20180308203924_FileHashTags.cs +++ b/starsky/starsky.foundation.database/Migrations/20180308203924_FileHashTags.cs @@ -1,33 +1,33 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class FileHashTags : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "FileHash", - table: "FileIndex", - maxLength: 190, - nullable: true); + public partial class FileHashTags : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FileHash", + table: "FileIndex", + maxLength: 190, + nullable: true); - migrationBuilder.AddColumn( - name: "Tags", - table: "FileIndex", - maxLength: 1024, - nullable: true); - } + migrationBuilder.AddColumn( + name: "Tags", + table: "FileIndex", + maxLength: 1024, + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "FileHash", - table: "FileIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FileHash", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "Tags", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "Tags", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180308213306_DateTime_AddToDatabase.cs b/starsky/starsky.foundation.database/Migrations/20180308213306_DateTime_AddToDatabase.cs index 22597aba24..d516d93dd0 100644 --- a/starsky/starsky.foundation.database/Migrations/20180308213306_DateTime_AddToDatabase.cs +++ b/starsky/starsky.foundation.database/Migrations/20180308213306_DateTime_AddToDatabase.cs @@ -1,34 +1,34 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class DateTime_AddToDatabase : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AddToDatabase", - table: "FileIndex", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + public partial class DateTime_AddToDatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AddToDatabase", + table: "FileIndex", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - migrationBuilder.AddColumn( - name: "DateTime", - table: "FileIndex", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } + migrationBuilder.AddColumn( + name: "DateTime", + table: "FileIndex", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AddToDatabase", - table: "FileIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AddToDatabase", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "DateTime", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "DateTime", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180316113937_IsDirectoryAddDescriptionParentDirectory.cs b/starsky/starsky.foundation.database/Migrations/20180316113937_IsDirectoryAddDescriptionParentDirectory.cs index 84e6ca1c8f..066dc2ac96 100644 --- a/starsky/starsky.foundation.database/Migrations/20180316113937_IsDirectoryAddDescriptionParentDirectory.cs +++ b/starsky/starsky.foundation.database/Migrations/20180316113937_IsDirectoryAddDescriptionParentDirectory.cs @@ -1,48 +1,48 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class IsDirectoryAddDescriptionParentDirectory : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - - migrationBuilder.AddColumn( - name: "ParentDirectory", - table: "FileIndex", - maxLength: 190, - nullable: true); - - // SQLite does not support this migration operation ('RenameColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262. - - migrationBuilder.AddColumn( - name: "Description", - table: "FileIndex", - nullable: true); - - migrationBuilder.AddColumn( - name: "IsDirectory", - table: "FileIndex", - nullable: false, - defaultValue: false); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Description", - table: "FileIndex"); - - migrationBuilder.DropColumn( - name: "IsDirectory", - table: "FileIndex"); - - // SQLite does not support this migration operation ('RenameColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262. - - migrationBuilder.DropColumn( - name: "ParentDirectory", - table: "FileIndex"); - - } - } + public partial class IsDirectoryAddDescriptionParentDirectory : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + migrationBuilder.AddColumn( + name: "ParentDirectory", + table: "FileIndex", + maxLength: 190, + nullable: true); + + // SQLite does not support this migration operation ('RenameColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262. + + migrationBuilder.AddColumn( + name: "Description", + table: "FileIndex", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsDirectory", + table: "FileIndex", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + table: "FileIndex"); + + migrationBuilder.DropColumn( + name: "IsDirectory", + table: "FileIndex"); + + // SQLite does not support this migration operation ('RenameColumnOperation'). For more information, see http://go.microsoft.com/fwlink/?LinkId=723262. + + migrationBuilder.DropColumn( + name: "ParentDirectory", + table: "FileIndex"); + + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180322104602_ColorClassFeature.cs b/starsky/starsky.foundation.database/Migrations/20180322104602_ColorClassFeature.cs index d381c8313d..433ceee778 100644 --- a/starsky/starsky.foundation.database/Migrations/20180322104602_ColorClassFeature.cs +++ b/starsky/starsky.foundation.database/Migrations/20180322104602_ColorClassFeature.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class ColorClassFeature : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ColorClass", - table: "FileIndex", - nullable: false, - defaultValue: 0); - } + public partial class ColorClassFeature : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ColorClass", + table: "FileIndex", + nullable: false, + defaultValue: 0); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ColorClass", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ColorClass", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180407074506_Title.cs b/starsky/starsky.foundation.database/Migrations/20180407074506_Title.cs index bf287195b2..77f8bca15d 100644 --- a/starsky/starsky.foundation.database/Migrations/20180407074506_Title.cs +++ b/starsky/starsky.foundation.database/Migrations/20180407074506_Title.cs @@ -1,22 +1,22 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class Title : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Title", - table: "FileIndex", - nullable: true); - } + public partial class Title : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Title", + table: "FileIndex", + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Title", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Title", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180419182654_importdatabase.cs b/starsky/starsky.foundation.database/Migrations/20180419182654_importdatabase.cs index fce51e67af..107b777db8 100644 --- a/starsky/starsky.foundation.database/Migrations/20180419182654_importdatabase.cs +++ b/starsky/starsky.foundation.database/Migrations/20180419182654_importdatabase.cs @@ -1,33 +1,33 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. namespace starsky.foundation.database.Migrations { - public partial class importdatabase : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ImportIndex", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true), - AddToDatabase = table.Column(nullable: false), - FileHash = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ImportIndex", x => x.Id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ImportIndex"); - } - } + public partial class importdatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ImportIndex", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true), + AddToDatabase = table.Column(nullable: false), + FileHash = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ImportIndex", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ImportIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180423120037_LatitudeLongitudeGPS.cs b/starsky/starsky.foundation.database/Migrations/20180423120037_LatitudeLongitudeGPS.cs index 25b0ee7d3f..9c90aa3369 100644 --- a/starsky/starsky.foundation.database/Migrations/20180423120037_LatitudeLongitudeGPS.cs +++ b/starsky/starsky.foundation.database/Migrations/20180423120037_LatitudeLongitudeGPS.cs @@ -1,32 +1,32 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class LatitudeLongitudeGPS : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Latitude", - table: "FileIndex", - nullable: false, - defaultValue: 0.0); + public partial class LatitudeLongitudeGPS : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Latitude", + table: "FileIndex", + nullable: false, + defaultValue: 0.0); - migrationBuilder.AddColumn( - name: "Longitude", - table: "FileIndex", - nullable: false, - defaultValue: 0.0); - } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Latitude", - table: "FileIndex"); + migrationBuilder.AddColumn( + name: "Longitude", + table: "FileIndex", + nullable: false, + defaultValue: 0.0); + } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Latitude", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "Longitude", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "Longitude", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180622130045_DateTimeImportIndexItem.cs b/starsky/starsky.foundation.database/Migrations/20180622130045_DateTimeImportIndexItem.cs index 714856d756..934806ef6e 100644 --- a/starsky/starsky.foundation.database/Migrations/20180622130045_DateTimeImportIndexItem.cs +++ b/starsky/starsky.foundation.database/Migrations/20180622130045_DateTimeImportIndexItem.cs @@ -1,24 +1,24 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class DateTimeImportIndexItem : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DateTime", - table: "ImportIndex", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } + public partial class DateTimeImportIndexItem : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DateTime", + table: "ImportIndex", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DateTime", - table: "ImportIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DateTime", + table: "ImportIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180717164601_userAccountStorage.cs b/starsky/starsky.foundation.database/Migrations/20180717164601_userAccountStorage.cs index 738416b101..c529842c30 100644 --- a/starsky/starsky.foundation.database/Migrations/20180717164601_userAccountStorage.cs +++ b/starsky/starsky.foundation.database/Migrations/20180717164601_userAccountStorage.cs @@ -1,197 +1,197 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class userAccountStorage : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CredentialTypes", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true), - Code = table.Column(maxLength: 32, nullable: false), - Name = table.Column(maxLength: 64, nullable: false), - Position = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CredentialTypes", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Permissions", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) + public partial class userAccountStorage : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CredentialTypes", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) .Annotation("MySql:ValueGeneratedOnAdd", true), - Code = table.Column(maxLength: 32, nullable: false), - Name = table.Column(maxLength: 64, nullable: false), - Position = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Permissions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Roles", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true), - Code = table.Column(maxLength: 32, nullable: false), - Name = table.Column(maxLength: 64, nullable: false), - Position = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Roles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true), - Created = table.Column(nullable: false), - Name = table.Column(maxLength: 64, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "RolePermissions", - columns: table => new - { - RoleId = table.Column(nullable: false), - PermissionId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RolePermissions", x => new { x.RoleId, x.PermissionId }); - table.ForeignKey( - name: "FK_RolePermissions_Permissions_PermissionId", - column: x => x.PermissionId, - principalTable: "Permissions", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_RolePermissions_Roles_RoleId", - column: x => x.RoleId, - principalTable: "Roles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Credentials", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true), - CredentialTypeId = table.Column(nullable: false), - Extra = table.Column(nullable: true), - Identifier = table.Column(maxLength: 64, nullable: false), - Secret = table.Column(maxLength: 1024, nullable: true), - UserId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Credentials", x => x.Id); - table.ForeignKey( - name: "FK_Credentials_CredentialTypes_CredentialTypeId", - column: x => x.CredentialTypeId, - principalTable: "CredentialTypes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Credentials_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserRoles", - columns: table => new - { - UserId = table.Column(nullable: false), - RoleId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_UserRoles_Roles_RoleId", - column: x => x.RoleId, - principalTable: "Roles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_UserRoles_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Credentials_CredentialTypeId", - table: "Credentials", - column: "CredentialTypeId"); - - migrationBuilder.CreateIndex( - name: "IX_Credentials_UserId", - table: "Credentials", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_RolePermissions_PermissionId", - table: "RolePermissions", - column: "PermissionId"); - - migrationBuilder.CreateIndex( - name: "IX_UserRoles_RoleId", - table: "UserRoles", - column: "RoleId"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Credentials"); - - migrationBuilder.DropTable( - name: "RolePermissions"); - - migrationBuilder.DropTable( - name: "UserRoles"); - - migrationBuilder.DropTable( - name: "CredentialTypes"); - - migrationBuilder.DropTable( - name: "Permissions"); - - migrationBuilder.DropTable( - name: "Roles"); - - migrationBuilder.DropTable( - name: "Users"); - - } - } + Code = table.Column(maxLength: 32, nullable: false), + Name = table.Column(maxLength: 64, nullable: false), + Position = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CredentialTypes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true), + Code = table.Column(maxLength: 32, nullable: false), + Name = table.Column(maxLength: 64, nullable: false), + Position = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true), + Code = table.Column(maxLength: 32, nullable: false), + Name = table.Column(maxLength: 64, nullable: false), + Position = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true), + Created = table.Column(nullable: false), + Name = table.Column(maxLength: 64, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "RolePermissions", + columns: table => new + { + RoleId = table.Column(nullable: false), + PermissionId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RolePermissions", x => new { x.RoleId, x.PermissionId }); + table.ForeignKey( + name: "FK_RolePermissions_Permissions_PermissionId", + column: x => x.PermissionId, + principalTable: "Permissions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RolePermissions_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Credentials", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true), + CredentialTypeId = table.Column(nullable: false), + Extra = table.Column(nullable: true), + Identifier = table.Column(maxLength: 64, nullable: false), + Secret = table.Column(maxLength: 1024, nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Credentials", x => x.Id); + table.ForeignKey( + name: "FK_Credentials_CredentialTypes_CredentialTypeId", + column: x => x.CredentialTypeId, + principalTable: "CredentialTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Credentials_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_UserRoles_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Credentials_CredentialTypeId", + table: "Credentials", + column: "CredentialTypeId"); + + migrationBuilder.CreateIndex( + name: "IX_Credentials_UserId", + table: "Credentials", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_RolePermissions_PermissionId", + table: "RolePermissions", + column: "PermissionId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_RoleId", + table: "UserRoles", + column: "RoleId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Credentials"); + + migrationBuilder.DropTable( + name: "RolePermissions"); + + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.DropTable( + name: "CredentialTypes"); + + migrationBuilder.DropTable( + name: "Permissions"); + + migrationBuilder.DropTable( + name: "Roles"); + + migrationBuilder.DropTable( + name: "Users"); + + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180801084111_ImageFormatToFileIndexItem.cs b/starsky/starsky.foundation.database/Migrations/20180801084111_ImageFormatToFileIndexItem.cs index 66c9a33100..a08ef71fa9 100644 --- a/starsky/starsky.foundation.database/Migrations/20180801084111_ImageFormatToFileIndexItem.cs +++ b/starsky/starsky.foundation.database/Migrations/20180801084111_ImageFormatToFileIndexItem.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class ImageFormatToFileIndexItem : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ImageFormat", - table: "FileIndex", - nullable: false, - defaultValue: 0); - } + public partial class ImageFormatToFileIndexItem : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageFormat", + table: "FileIndex", + nullable: false, + defaultValue: 0); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ImageFormat", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageFormat", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180806181225_ExifOrientation.cs b/starsky/starsky.foundation.database/Migrations/20180806181225_ExifOrientation.cs index 515def3ba0..18b5634c37 100644 --- a/starsky/starsky.foundation.database/Migrations/20180806181225_ExifOrientation.cs +++ b/starsky/starsky.foundation.database/Migrations/20180806181225_ExifOrientation.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class ExifOrientation : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Orientation", - table: "FileIndex", - nullable: false, - defaultValue: 0); - } + public partial class ExifOrientation : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Orientation", + table: "FileIndex", + nullable: false, + defaultValue: 0); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Orientation", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Orientation", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180820145632_ImageWidthImageHeight.cs b/starsky/starsky.foundation.database/Migrations/20180820145632_ImageWidthImageHeight.cs index 0d296e43b7..383ea291eb 100644 --- a/starsky/starsky.foundation.database/Migrations/20180820145632_ImageWidthImageHeight.cs +++ b/starsky/starsky.foundation.database/Migrations/20180820145632_ImageWidthImageHeight.cs @@ -1,33 +1,33 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class ImageWidthImageHeight : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ImageHeight", - table: "FileIndex", - nullable: false, - defaultValue: (ushort)0); + public partial class ImageWidthImageHeight : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageHeight", + table: "FileIndex", + nullable: false, + defaultValue: ( ushort )0); - migrationBuilder.AddColumn( - name: "ImageWidth", - table: "FileIndex", - nullable: false, - defaultValue: (ushort)0); - } + migrationBuilder.AddColumn( + name: "ImageWidth", + table: "FileIndex", + nullable: false, + defaultValue: ( ushort )0); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ImageHeight", - table: "FileIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageHeight", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "ImageWidth", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "ImageWidth", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180919145305_LocationCityLocationStateLocationCountry.cs b/starsky/starsky.foundation.database/Migrations/20180919145305_LocationCityLocationStateLocationCountry.cs index c8c844c754..0982a704db 100644 --- a/starsky/starsky.foundation.database/Migrations/20180919145305_LocationCityLocationStateLocationCountry.cs +++ b/starsky/starsky.foundation.database/Migrations/20180919145305_LocationCityLocationStateLocationCountry.cs @@ -1,43 +1,43 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class LocationCityLocationStateLocationCountry : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LocationCity", - table: "FileIndex", - maxLength: 40, - nullable: true); + public partial class LocationCityLocationStateLocationCountry : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LocationCity", + table: "FileIndex", + maxLength: 40, + nullable: true); - migrationBuilder.AddColumn( - name: "LocationCountry", - table: "FileIndex", - maxLength: 40, - nullable: true); + migrationBuilder.AddColumn( + name: "LocationCountry", + table: "FileIndex", + maxLength: 40, + nullable: true); - migrationBuilder.AddColumn( - name: "LocationState", - table: "FileIndex", - maxLength: 40, - nullable: true); - } + migrationBuilder.AddColumn( + name: "LocationState", + table: "FileIndex", + maxLength: 40, + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LocationCity", - table: "FileIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LocationCity", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "LocationCountry", - table: "FileIndex"); + migrationBuilder.DropColumn( + name: "LocationCountry", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "LocationState", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "LocationState", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20180923145732_LocationAltitude.cs b/starsky/starsky.foundation.database/Migrations/20180923145732_LocationAltitude.cs index 4815fae1cd..fcfbc77fc3 100644 --- a/starsky/starsky.foundation.database/Migrations/20180923145732_LocationAltitude.cs +++ b/starsky/starsky.foundation.database/Migrations/20180923145732_LocationAltitude.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class LocationAltitude : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LocationAltitude", - table: "FileIndex", - nullable: false, - defaultValue: 0.0); - } + public partial class LocationAltitude : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LocationAltitude", + table: "FileIndex", + nullable: false, + defaultValue: 0.0); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LocationAltitude", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LocationAltitude", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20190103194709_AddApertureShutterSpeedIsoSpeed.cs b/starsky/starsky.foundation.database/Migrations/20190103194709_AddApertureShutterSpeedIsoSpeed.cs index ecc1ba7bc2..dee87a404e 100644 --- a/starsky/starsky.foundation.database/Migrations/20190103194709_AddApertureShutterSpeedIsoSpeed.cs +++ b/starsky/starsky.foundation.database/Migrations/20190103194709_AddApertureShutterSpeedIsoSpeed.cs @@ -1,43 +1,43 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class AddApertureShutterSpeedIsoSpeed : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Aperture", - table: "FileIndex", - nullable: false, - defaultValue: 0.0); + public partial class AddApertureShutterSpeedIsoSpeed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Aperture", + table: "FileIndex", + nullable: false, + defaultValue: 0.0); - migrationBuilder.AddColumn( - name: "IsoSpeed", - table: "FileIndex", - nullable: false, - defaultValue: (ushort)0); + migrationBuilder.AddColumn( + name: "IsoSpeed", + table: "FileIndex", + nullable: false, + defaultValue: ( ushort )0); - migrationBuilder.AddColumn( - name: "ShutterSpeed", - table: "FileIndex", - maxLength: 20, - nullable: true); - } + migrationBuilder.AddColumn( + name: "ShutterSpeed", + table: "FileIndex", + maxLength: 20, + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Aperture", - table: "FileIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Aperture", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "IsoSpeed", - table: "FileIndex"); + migrationBuilder.DropColumn( + name: "IsoSpeed", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "ShutterSpeed", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "ShutterSpeed", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20190111162308_MakeModel.cs b/starsky/starsky.foundation.database/Migrations/20190111162308_MakeModel.cs index ef0e319784..bbdce5dc54 100644 --- a/starsky/starsky.foundation.database/Migrations/20190111162308_MakeModel.cs +++ b/starsky/starsky.foundation.database/Migrations/20190111162308_MakeModel.cs @@ -1,22 +1,22 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class MakeModel : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "MakeModel", - table: "FileIndex", - nullable: true); - } + public partial class MakeModel : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MakeModel", + table: "FileIndex", + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "MakeModel", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MakeModel", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20190317154100_LastEditedField.cs b/starsky/starsky.foundation.database/Migrations/20190317154100_LastEditedField.cs index af47fbb2c5..340bec8dbd 100644 --- a/starsky/starsky.foundation.database/Migrations/20190317154100_LastEditedField.cs +++ b/starsky/starsky.foundation.database/Migrations/20190317154100_LastEditedField.cs @@ -1,24 +1,24 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class LastEditedField : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LastEdited", - table: "FileIndex", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } + public partial class LastEditedField : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastEdited", + table: "FileIndex", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LastEdited", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastEdited", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20190422190334_ImportIndexItemFilePath.cs b/starsky/starsky.foundation.database/Migrations/20190422190334_ImportIndexItemFilePath.cs index 52ff491fd6..641fa0a1f4 100644 --- a/starsky/starsky.foundation.database/Migrations/20190422190334_ImportIndexItemFilePath.cs +++ b/starsky/starsky.foundation.database/Migrations/20190422190334_ImportIndexItemFilePath.cs @@ -1,22 +1,22 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class ImportIndexItemFilePath : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "FilePath", - table: "ImportIndex", - nullable: true); - } + public partial class ImportIndexItemFilePath : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FilePath", + table: "ImportIndex", + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "FilePath", - table: "ImportIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FilePath", + table: "ImportIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20190819153950_FocalLengthAndIndexes.cs b/starsky/starsky.foundation.database/Migrations/20190819153950_FocalLengthAndIndexes.cs index f0f0d8bb3e..5c49d724f6 100644 --- a/starsky/starsky.foundation.database/Migrations/20190819153950_FocalLengthAndIndexes.cs +++ b/starsky/starsky.foundation.database/Migrations/20190819153950_FocalLengthAndIndexes.cs @@ -1,33 +1,33 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class FocalLengthAndIndexes : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "FocalLength", - table: "FileIndex", - nullable: false, - defaultValue: 0.0); + public partial class FocalLengthAndIndexes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FocalLength", + table: "FileIndex", + nullable: false, + defaultValue: 0.0); - migrationBuilder.CreateIndex( - name: "IX_FileIndex_FileName_ParentDirectory", - table: "FileIndex", - columns: new[] { "FileName", "ParentDirectory"}); - // Specified key was too long; max key length is 3072 bytes - } + migrationBuilder.CreateIndex( + name: "IX_FileIndex_FileName_ParentDirectory", + table: "FileIndex", + columns: new[] { "FileName", "ParentDirectory" }); + // Specified key was too long; max key length is 3072 bytes + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_FileIndex_FileName_ParentDirectory", - table: "FileIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_FileIndex_FileName_ParentDirectory", + table: "FileIndex"); - migrationBuilder.DropColumn( - name: "FocalLength", - table: "FileIndex"); - } - } + migrationBuilder.DropColumn( + name: "FocalLength", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20191123222156_MaxLength.cs b/starsky/starsky.foundation.database/Migrations/20191123222156_MaxLength.cs index dcdd37160a..3d72b18370 100644 --- a/starsky/starsky.foundation.database/Migrations/20191123222156_MaxLength.cs +++ b/starsky/starsky.foundation.database/Migrations/20191123222156_MaxLength.cs @@ -1,17 +1,17 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class MaxLength : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { + public partial class MaxLength : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { // migration has been undo - } + } - protected override void Down(MigrationBuilder migrationBuilder) - { - // migration has been undo - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + // migration has been undo + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20200206192905_addSoftwareField.cs b/starsky/starsky.foundation.database/Migrations/20200206192905_addSoftwareField.cs index 3584327e32..41c6fd03d0 100644 --- a/starsky/starsky.foundation.database/Migrations/20200206192905_addSoftwareField.cs +++ b/starsky/starsky.foundation.database/Migrations/20200206192905_addSoftwareField.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class addSoftwareField : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Software", - table: "FileIndex", - maxLength: 40, - nullable: true); - } + public partial class addSoftwareField : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Software", + table: "FileIndex", + maxLength: 40, + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Software", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Software", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20200730164439_addSize.cs b/starsky/starsky.foundation.database/Migrations/20200730164439_addSize.cs index 3ba15efe13..2f90e0c1ca 100644 --- a/starsky/starsky.foundation.database/Migrations/20200730164439_addSize.cs +++ b/starsky/starsky.foundation.database/Migrations/20200730164439_addSize.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class addSize : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Size", - table: "FileIndex", - nullable: false, - defaultValue: 0L); - } + public partial class addSize : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Size", + table: "FileIndex", + nullable: false, + defaultValue: 0L); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Size", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Size", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20201027151043_SidecarExtensionsAdded.cs b/starsky/starsky.foundation.database/Migrations/20201027151043_SidecarExtensionsAdded.cs index 2d2fcef21c..9805bc4071 100644 --- a/starsky/starsky.foundation.database/Migrations/20201027151043_SidecarExtensionsAdded.cs +++ b/starsky/starsky.foundation.database/Migrations/20201027151043_SidecarExtensionsAdded.cs @@ -1,22 +1,22 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class SidecarExtensionsAdded : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "SidecarExtensions", - table: "FileIndex", - nullable: true); - } + public partial class SidecarExtensionsAdded : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SidecarExtensions", + table: "FileIndex", + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "SidecarExtensions", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SidecarExtensions", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20210916112955_LockoutEnabled.cs b/starsky/starsky.foundation.database/Migrations/20210916112955_LockoutEnabled.cs index fd0aba5384..55d3a0cc3d 100644 --- a/starsky/starsky.foundation.database/Migrations/20210916112955_LockoutEnabled.cs +++ b/starsky/starsky.foundation.database/Migrations/20210916112955_LockoutEnabled.cs @@ -1,43 +1,43 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class LockoutEnabled : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AccessFailedCount", - table: "Users", - nullable: false, - defaultValue: 0); + public partial class LockoutEnabled : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccessFailedCount", + table: "Users", + nullable: false, + defaultValue: 0); - migrationBuilder.AddColumn( - name: "LockoutEnabled", - table: "Users", - nullable: false, - defaultValue: false); + migrationBuilder.AddColumn( + name: "LockoutEnabled", + table: "Users", + nullable: false, + defaultValue: false); - migrationBuilder.AddColumn( - name: "LockoutEnd", - table: "Users", - nullable: false); - } + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "Users", + nullable: false); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AccessFailedCount", - table: "Users"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AccessFailedCount", + table: "Users"); - migrationBuilder.DropColumn( - name: "LockoutEnabled", - table: "Users"); + migrationBuilder.DropColumn( + name: "LockoutEnabled", + table: "Users"); - migrationBuilder.DropColumn( - name: "LockoutEnd", - table: "Users"); - } - } + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "Users"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20211112185430_makeModelToImport.cs b/starsky/starsky.foundation.database/Migrations/20211112185430_makeModelToImport.cs index c15c061f2f..8c31f585c6 100644 --- a/starsky/starsky.foundation.database/Migrations/20211112185430_makeModelToImport.cs +++ b/starsky/starsky.foundation.database/Migrations/20211112185430_makeModelToImport.cs @@ -1,22 +1,22 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class makeModelToImport : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "MakeModel", - table: "ImportIndex", - nullable: true); - } + public partial class makeModelToImport : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MakeModel", + table: "ImportIndex", + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "MakeModel", - table: "ImportIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MakeModel", + table: "ImportIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20211214155311_IndexOnDatabaseSeed.cs b/starsky/starsky.foundation.database/Migrations/20211214155311_IndexOnDatabaseSeed.cs index ab301cfce8..269cc3ee19 100644 --- a/starsky/starsky.foundation.database/Migrations/20211214155311_IndexOnDatabaseSeed.cs +++ b/starsky/starsky.foundation.database/Migrations/20211214155311_IndexOnDatabaseSeed.cs @@ -1,31 +1,31 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class IndexOnDatabaseSeed : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateIndex( - name: "IX_ImportIndex_FileHash", - table: "ImportIndex", - column: "FileHash"); + public partial class IndexOnDatabaseSeed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_ImportIndex_FileHash", + table: "ImportIndex", + column: "FileHash"); - migrationBuilder.CreateIndex( - name: "IX_Credentials_Id_Identifier", - table: "Credentials", - columns: new[] { "Id", "Identifier" }); - } + migrationBuilder.CreateIndex( + name: "IX_Credentials_Id_Identifier", + table: "Credentials", + columns: new[] { "Id", "Identifier" }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_ImportIndex_FileHash", - table: "ImportIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ImportIndex_FileHash", + table: "ImportIndex"); - migrationBuilder.DropIndex( - name: "IX_Credentials_Id_Identifier", - table: "Credentials"); - } - } + migrationBuilder.DropIndex( + name: "IX_Credentials_Id_Identifier", + table: "Credentials"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20211228192706_ImageStabilisation.cs b/starsky/starsky.foundation.database/Migrations/20211228192706_ImageStabilisation.cs index 5c14a8dfc7..330b968b70 100644 --- a/starsky/starsky.foundation.database/Migrations/20211228192706_ImageStabilisation.cs +++ b/starsky/starsky.foundation.database/Migrations/20211228192706_ImageStabilisation.cs @@ -1,23 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; namespace starsky.foundation.database.Migrations { - public partial class ImageStabilisation : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ImageStabilisation", - table: "FileIndex", - nullable: false, - defaultValue: 0); - } + public partial class ImageStabilisation : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageStabilisation", + table: "FileIndex", + nullable: false, + defaultValue: 0); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ImageStabilisation", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageStabilisation", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20220408100352_NotificationTable.cs b/starsky/starsky.foundation.database/Migrations/20220408100352_NotificationTable.cs index 5ee6267949..16c721f369 100644 --- a/starsky/starsky.foundation.database/Migrations/20220408100352_NotificationTable.cs +++ b/starsky/starsky.foundation.database/Migrations/20220408100352_NotificationTable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; @@ -6,32 +6,32 @@ namespace starsky.foundation.database.Migrations { - public partial class NotificationTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Notifications", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true) - .Annotation("MySql:ValueGeneratedOnAdd", true) - .Annotation("MySql:ValueGenerationStrategy", - MySqlValueGenerationStrategy.IdentityColumn), - Content = table.Column(type: "TEXT", nullable: true), - DateTime = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notifications", x => x.Id); - }); - } + public partial class NotificationTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("MySql:ValueGeneratedOnAdd", true) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn), + Content = table.Column(type: "TEXT", nullable: true), + DateTime = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Notifications"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notifications"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20220409202146_DateTimeFromFileNameColorClassImport.cs b/starsky/starsky.foundation.database/Migrations/20220409202146_DateTimeFromFileNameColorClassImport.cs index efc15bacc7..3cd06fdacf 100644 --- a/starsky/starsky.foundation.database/Migrations/20220409202146_DateTimeFromFileNameColorClassImport.cs +++ b/starsky/starsky.foundation.database/Migrations/20220409202146_DateTimeFromFileNameColorClassImport.cs @@ -1,37 +1,37 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class DateTimeFromFileNameColorClassImport : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ColorClass", - table: "ImportIndex", - type: "INTEGER", - nullable: false, - defaultValue: 0); + public partial class DateTimeFromFileNameColorClassImport : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ColorClass", + table: "ImportIndex", + type: "INTEGER", + nullable: false, + defaultValue: 0); - migrationBuilder.AddColumn( - name: "DateTimeFromFileName", - table: "ImportIndex", - type: "INTEGER", - nullable: false, - defaultValue: false); - } + migrationBuilder.AddColumn( + name: "DateTimeFromFileName", + table: "ImportIndex", + type: "INTEGER", + nullable: false, + defaultValue: false); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ColorClass", - table: "ImportIndex"); + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ColorClass", + table: "ImportIndex"); - migrationBuilder.DropColumn( - name: "DateTimeFromFileName", - table: "ImportIndex"); - } - } + migrationBuilder.DropColumn( + name: "DateTimeFromFileName", + table: "ImportIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20220417170059_ContentSize.cs b/starsky/starsky.foundation.database/Migrations/20220417170059_ContentSize.cs index 0f7e7fb8e2..837e01ac57 100644 --- a/starsky/starsky.foundation.database/Migrations/20220417170059_ContentSize.cs +++ b/starsky/starsky.foundation.database/Migrations/20220417170059_ContentSize.cs @@ -1,33 +1,33 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class ContentSize : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Content", - table: "Notifications", - type: "mediumtext", - nullable: true, - oldClrType: typeof(string), - oldType: "TEXT", - oldNullable: true); - } + public partial class ContentSize : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Content", + table: "Notifications", + type: "mediumtext", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Content", - table: "Notifications", - type: "TEXT", - nullable: true, - oldClrType: typeof(string), - oldType: "mediumtext", - oldNullable: true); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Content", + table: "Notifications", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "mediumtext", + oldNullable: true); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20220417173513_bigintSize.cs b/starsky/starsky.foundation.database/Migrations/20220417173513_bigintSize.cs index cb529620b3..aa2a4b832a 100644 --- a/starsky/starsky.foundation.database/Migrations/20220417173513_bigintSize.cs +++ b/starsky/starsky.foundation.database/Migrations/20220417173513_bigintSize.cs @@ -1,31 +1,31 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class bigintSize : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Size", - table: "FileIndex", - type: "bigint", - nullable: false, - oldClrType: typeof(long), - oldType: "INTEGER"); - } + public partial class bigintSize : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Size", + table: "FileIndex", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Size", - table: "FileIndex", - type: "INTEGER", - nullable: false, - oldClrType: typeof(long), - oldType: "bigint"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Size", + table: "FileIndex", + type: "INTEGER", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20221008200947_settingsTable.cs b/starsky/starsky.foundation.database/Migrations/20221008200947_settingsTable.cs index 68e8085ed3..23fe160f78 100644 --- a/starsky/starsky.foundation.database/Migrations/20221008200947_settingsTable.cs +++ b/starsky/starsky.foundation.database/Migrations/20221008200947_settingsTable.cs @@ -1,32 +1,32 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class settingsTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Settings", - columns: table => new - { - Key = table.Column(type: "varchar(150)", maxLength: 150, nullable: false), - Value = table.Column(type: "TEXT", maxLength: 4096, nullable: false), - IsUserEditable = table.Column(type: "INTEGER", nullable: false), - UserId = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Settings", x => x.Key); - }); - } + public partial class settingsTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Settings", + columns: table => new + { + Key = table.Column(type: "varchar(150)", maxLength: 150, nullable: false), + Value = table.Column(type: "TEXT", maxLength: 4096, nullable: false), + IsUserEditable = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Settings", x => x.Key); + }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Settings"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Settings"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20221018125303_LocationCountryCode.cs b/starsky/starsky.foundation.database/Migrations/20221018125303_LocationCountryCode.cs index b011f31840..25d1096073 100644 --- a/starsky/starsky.foundation.database/Migrations/20221018125303_LocationCountryCode.cs +++ b/starsky/starsky.foundation.database/Migrations/20221018125303_LocationCountryCode.cs @@ -1,26 +1,26 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class LocationCountryCode : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LocationCountryCode", - table: "FileIndex", - type: "TEXT", - maxLength: 3, - nullable: true); - } + public partial class LocationCountryCode : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LocationCountryCode", + table: "FileIndex", + type: "TEXT", + maxLength: 3, + nullable: true); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LocationCountryCode", - table: "FileIndex"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LocationCountryCode", + table: "FileIndex"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20221025182759_data-protection-keys.cs b/starsky/starsky.foundation.database/Migrations/20221025182759_data-protection-keys.cs index 1ad19190a8..3f7f4fc0db 100644 --- a/starsky/starsky.foundation.database/Migrations/20221025182759_data-protection-keys.cs +++ b/starsky/starsky.foundation.database/Migrations/20221025182759_data-protection-keys.cs @@ -1,33 +1,33 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. #nullable disable namespace starsky.foundation.database.Migrations { - public partial class dataprotectionkeys : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "DataProtectionKeys", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - FriendlyName = table.Column(type: "TEXT", nullable: true), - Xml = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_DataProtectionKeys", x => x.Id); - }); - } + public partial class dataprotectionkeys : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DataProtectionKeys", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FriendlyName = table.Column(type: "TEXT", nullable: true), + Xml = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DataProtectionKeys", x => x.Id); + }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "DataProtectionKeys"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DataProtectionKeys"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20221229111418_ThumbnailTable.cs b/starsky/starsky.foundation.database/Migrations/20221229111418_ThumbnailTable.cs index 3aad0f10bf..2b8089e4c6 100644 --- a/starsky/starsky.foundation.database/Migrations/20221229111418_ThumbnailTable.cs +++ b/starsky/starsky.foundation.database/Migrations/20221229111418_ThumbnailTable.cs @@ -1,34 +1,34 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class ThumbnailTable : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Thumbnails", - columns: table => new - { - FileHash = table.Column(type: "varchar(190)", maxLength: 190, nullable: false), - TinyMeta = table.Column(type: "INTEGER", nullable: true), - Small = table.Column(type: "INTEGER", nullable: true), - Large = table.Column(type: "INTEGER", nullable: true), - ExtraLarge = table.Column(type: "INTEGER", nullable: true), - Reasons = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Thumbnails", x => x.FileHash); - }); - } + public partial class ThumbnailTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Thumbnails", + columns: table => new + { + FileHash = table.Column(type: "varchar(190)", maxLength: 190, nullable: false), + TinyMeta = table.Column(type: "INTEGER", nullable: true), + Small = table.Column(type: "INTEGER", nullable: true), + Large = table.Column(type: "INTEGER", nullable: true), + ExtraLarge = table.Column(type: "INTEGER", nullable: true), + Reasons = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Thumbnails", x => x.FileHash); + }); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Thumbnails"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Thumbnails"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20230105213937_epochNotifications.cs b/starsky/starsky.foundation.database/Migrations/20230105213937_epochNotifications.cs index d020b61e4f..cee9f0e139 100644 --- a/starsky/starsky.foundation.database/Migrations/20230105213937_epochNotifications.cs +++ b/starsky/starsky.foundation.database/Migrations/20230105213937_epochNotifications.cs @@ -1,26 +1,26 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace starsky.foundation.database.Migrations { - public partial class epochNotifications : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "DateTimeEpoch", - table: "Notifications", - type: "bigint", - nullable: false, - defaultValue: 0L); - } + public partial class epochNotifications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DateTimeEpoch", + table: "Notifications", + type: "bigint", + nullable: false, + defaultValue: 0L); + } - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "DateTimeEpoch", - table: "Notifications"); - } - } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DateTimeEpoch", + table: "Notifications"); + } + } } diff --git a/starsky/starsky.foundation.database/Migrations/20240209175725_limitdataprotectionkeyslength.Designer.cs b/starsky/starsky.foundation.database/Migrations/20240209175725_limitdataprotectionkeyslength.Designer.cs new file mode 100644 index 0000000000..976ac7fdb8 --- /dev/null +++ b/starsky/starsky.foundation.database/Migrations/20240209175725_limitdataprotectionkeyslength.Designer.cs @@ -0,0 +1,541 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using starsky.foundation.database.Data; +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. + +#nullable disable + +namespace starsky.foundation.database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240209175725_limitdataprotectionkeyslength")] + partial class limitdataprotectionkeyslength + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("MySql:CharSet", "utf8mb4") + .HasAnnotation("MySql:CharSetDelegation", DelegationModes.ApplyToAll) + .HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.Credential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + b.Property("CredentialTypeId") + .HasColumnType("INTEGER"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Secret") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CredentialTypeId"); + + b.HasIndex("UserId"); + + b.HasIndex("Id", "Identifier"); + + b.ToTable("Credentials", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.CredentialType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("CredentialTypes", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Permissions", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Roles", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("PermissionId") + .HasColumnType("INTEGER"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.ToTable("RolePermissions", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.UserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true); + + b.Property("FriendlyName") + .HasMaxLength(45) + .HasColumnType("TEXT"); + + b.Property("Xml") + .HasMaxLength(1200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.FileIndexItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddToDatabase") + .HasColumnType("TEXT"); + + b.Property("Aperture") + .HasColumnType("REAL"); + + b.Property("ColorClass") + .HasColumnType("INTEGER"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileHash") + .HasMaxLength(190) + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasMaxLength(190) + .HasColumnType("TEXT") + .HasColumnOrder(1); + + b.Property("FilePath") + .HasMaxLength(380) + .HasColumnType("TEXT") + .HasColumnOrder(2); + + b.Property("FocalLength") + .HasColumnType("REAL"); + + b.Property("ImageFormat") + .HasColumnType("INTEGER"); + + b.Property("ImageHeight") + .HasColumnType("INTEGER"); + + b.Property("ImageStabilisation") + .HasColumnType("INTEGER"); + + b.Property("ImageWidth") + .HasColumnType("INTEGER"); + + b.Property("IsDirectory") + .HasColumnType("INTEGER"); + + b.Property("IsoSpeed") + .HasColumnType("INTEGER"); + + b.Property("LastEdited") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("LocationAltitude") + .HasColumnType("REAL"); + + b.Property("LocationCity") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("LocationCountry") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("LocationCountryCode") + .HasMaxLength(3) + .HasColumnType("TEXT"); + + b.Property("LocationState") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("MakeModel") + .HasColumnType("TEXT"); + + b.Property("Orientation") + .HasColumnType("INTEGER"); + + b.Property("ParentDirectory") + .HasMaxLength(190) + .HasColumnType("TEXT"); + + b.Property("ShutterSpeed") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SidecarExtensions") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Software") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FileName", "ParentDirectory"); + + b.ToTable("FileIndex"); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.ImportIndexItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddToDatabase") + .HasColumnType("TEXT"); + + b.Property("ColorClass") + .HasColumnType("INTEGER"); + + b.Property("DateTime") + .HasColumnType("TEXT"); + + b.Property("DateTimeFromFileName") + .HasColumnType("INTEGER"); + + b.Property("FileHash") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("MakeModel") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FileHash") + .HasAnnotation("MySql:CharSet", "utf8mb4"); + + b.ToTable("ImportIndex"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.NotificationItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasAnnotation("MySql:ValueGeneratedOnAdd", true) + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) + .HasAnnotation("Sqlite:Autoincrement", true); + + b.Property("Content") + .HasColumnType("mediumtext"); + + b.Property("DateTime") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("DateTimeEpoch") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Notifications", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.SettingsItem", b => + { + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("IsUserEditable") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("Settings", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.ThumbnailItem", b => + { + b.Property("FileHash") + .HasMaxLength(190) + .HasColumnType("varchar(190)"); + + b.Property("ExtraLarge") + .HasColumnType("INTEGER"); + + b.Property("Large") + .HasColumnType("INTEGER"); + + b.Property("Reasons") + .HasColumnType("TEXT"); + + b.Property("Small") + .HasColumnType("INTEGER"); + + b.Property("TinyMeta") + .HasColumnType("INTEGER"); + + b.HasKey("FileHash"); + + b.ToTable("Thumbnails", (string)null); + + b.HasAnnotation("MySql:CharSet", "utf8mb4"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.Credential", b => + { + b.HasOne("starsky.foundation.database.Models.Account.CredentialType", "CredentialType") + .WithMany("Credentials") + .HasForeignKey("CredentialTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("starsky.foundation.database.Models.Account.User", "User") + .WithMany("Credentials") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CredentialType"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.RolePermission", b => + { + b.HasOne("starsky.foundation.database.Models.Account.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("starsky.foundation.database.Models.Account.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.UserRole", b => + { + b.HasOne("starsky.foundation.database.Models.Account.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("starsky.foundation.database.Models.Account.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.CredentialType", b => + { + b.Navigation("Credentials"); + }); + + modelBuilder.Entity("starsky.foundation.database.Models.Account.User", b => + { + b.Navigation("Credentials"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/starsky/starsky.foundation.database/Migrations/20240209175725_limitdataprotectionkeyslength.cs b/starsky/starsky.foundation.database/Migrations/20240209175725_limitdataprotectionkeyslength.cs new file mode 100644 index 0000000000..fb290ef8f2 --- /dev/null +++ b/starsky/starsky.foundation.database/Migrations/20240209175725_limitdataprotectionkeyslength.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Migrations; +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. +#pragma warning disable CS1587 // XML comment is not placed on a valid language element + +#nullable disable + +namespace starsky.foundation.database.Migrations +{ + [SuppressMessage("Usage", "S101: Rename class 'limitdataprotectionkeyslength' " + + "to match pascal case naming rules, consider using 'Limitdataprotectionkeyslength'.")] + /// + public partial class limitdataprotectionkeyslength : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // only used for the migration + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // only used for the migration + } + } +} diff --git a/starsky/starsky.foundation.database/Migrations/ApplicationDbContextModelSnapshot.cs b/starsky/starsky.foundation.database/Migrations/ApplicationDbContextModelSnapshot.cs index 0254043a48..594056de5f 100644 --- a/starsky/starsky.foundation.database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/starsky/starsky.foundation.database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder .HasAnnotation("MySql:CharSet", "utf8mb4") .HasAnnotation("MySql:CharSetDelegation", DelegationModes.ApplyToAll) - .HasAnnotation("ProductVersion", "6.0.12"); + .HasAnnotation("ProductVersion", "8.0.1"); modelBuilder.Entity("starsky.foundation.database.Models.Account.Credential", b => { @@ -214,9 +214,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("MySql:ValueGeneratedOnAdd", true); b.Property("FriendlyName") + .HasMaxLength(45) .HasColumnType("TEXT"); b.Property("Xml") + .HasMaxLength(1200) .HasColumnType("TEXT"); b.HasKey("Id"); diff --git a/starsky/starsky.foundation.database/Models/Account/Credential.cs b/starsky/starsky.foundation.database/Models/Account/Credential.cs index 99e3c99d01..49091bdccf 100755 --- a/starsky/starsky.foundation.database/Models/Account/Credential.cs +++ b/starsky/starsky.foundation.database/Models/Account/Credential.cs @@ -1,36 +1,36 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace starsky.foundation.database.Models.Account { - public class Credential - { - /// - /// Database Id - /// - public int Id { get; set; } - - /// - /// The user id - /// - public int UserId { get; set; } - public int CredentialTypeId { get; set; } - - /// - /// Email address - /// - public string? Identifier { get; set; } - - /// - /// Password - /// - public string? Secret { get; set; } - - /// - /// Some hash - /// - public string? Extra { get; set; } - public User? User { get; set; } - public CredentialType? CredentialType { get; set; } - } + public class Credential + { + /// + /// Database Id + /// + public int Id { get; set; } + + /// + /// The user id + /// + public int UserId { get; set; } + public int CredentialTypeId { get; set; } + + /// + /// Email address + /// + public string? Identifier { get; set; } + + /// + /// Password + /// + public string? Secret { get; set; } + + /// + /// Some hash + /// + public string? Extra { get; set; } + public User? User { get; set; } + public CredentialType? CredentialType { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/Account/CredentialType.cs b/starsky/starsky.foundation.database/Models/Account/CredentialType.cs index c523402cca..87b673d9b3 100755 --- a/starsky/starsky.foundation.database/Models/Account/CredentialType.cs +++ b/starsky/starsky.foundation.database/Models/Account/CredentialType.cs @@ -1,17 +1,17 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; namespace starsky.foundation.database.Models.Account { - public class CredentialType - { - public int Id { get; set; } - public string? Code { get; set; } - public string? Name { get; set; } - public int? Position { get; set; } - - public virtual ICollection? Credentials { get; set; } - } + public class CredentialType + { + public int Id { get; set; } + public string? Code { get; set; } + public string? Name { get; set; } + public int? Position { get; set; } + + public virtual ICollection? Credentials { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/Account/Permission.cs b/starsky/starsky.foundation.database/Models/Account/Permission.cs index c28dec85fd..3cb7e259b2 100755 --- a/starsky/starsky.foundation.database/Models/Account/Permission.cs +++ b/starsky/starsky.foundation.database/Models/Account/Permission.cs @@ -1,13 +1,13 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace starsky.foundation.database.Models.Account { - public class Permission - { - public int Id { get; set; } - public string? Code { get; set; } - public string? Name { get; set; } - public int? Position { get; set; } - } + public class Permission + { + public int Id { get; set; } + public string? Code { get; set; } + public string? Name { get; set; } + public int? Position { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/Account/Role.cs b/starsky/starsky.foundation.database/Models/Account/Role.cs index 8785857ad7..f498509d7a 100755 --- a/starsky/starsky.foundation.database/Models/Account/Role.cs +++ b/starsky/starsky.foundation.database/Models/Account/Role.cs @@ -1,4 +1,4 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.ComponentModel.DataAnnotations; @@ -6,13 +6,13 @@ namespace starsky.foundation.database.Models.Account { - public class Role - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - public string? Code { get; set; } - public string? Name { get; set; } - public int? Position { get; set; } - } + public class Role + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + public string? Code { get; set; } + public string? Name { get; set; } + public int? Position { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/Account/RolePermission.cs b/starsky/starsky.foundation.database/Models/Account/RolePermission.cs index 1d354c99b4..1939a35362 100755 --- a/starsky/starsky.foundation.database/Models/Account/RolePermission.cs +++ b/starsky/starsky.foundation.database/Models/Account/RolePermission.cs @@ -1,14 +1,14 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace starsky.foundation.database.Models.Account { - public class RolePermission - { - public int RoleId { get; set; } - public int PermissionId { get; set; } - - public virtual Role? Role { get; set; } - public virtual Permission? Permission { get; set; } - } + public class RolePermission + { + public int RoleId { get; set; } + public int PermissionId { get; set; } + + public virtual Role? Role { get; set; } + public virtual Permission? Permission { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/Account/User.cs b/starsky/starsky.foundation.database/Models/Account/User.cs index 2a5cf5d633..e6c6a48e22 100755 --- a/starsky/starsky.foundation.database/Models/Account/User.cs +++ b/starsky/starsky.foundation.database/Models/Account/User.cs @@ -1,4 +1,4 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -23,7 +23,7 @@ public class User public int AccessFailedCount { get; set; } // menu settings in the future - + public virtual ICollection? Credentials { get; set; } } diff --git a/starsky/starsky.foundation.database/Models/Account/UserRole.cs b/starsky/starsky.foundation.database/Models/Account/UserRole.cs index a42d710eab..05e04406ee 100755 --- a/starsky/starsky.foundation.database/Models/Account/UserRole.cs +++ b/starsky/starsky.foundation.database/Models/Account/UserRole.cs @@ -1,14 +1,14 @@ -// Copyright © 2017 Dmitry Sikorsky. All rights reserved. +// Copyright © 2017 Dmitry Sikorsky. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace starsky.foundation.database.Models.Account { - public class UserRole - { - public int UserId { get; set; } - public int RoleId { get; set; } - - public virtual User? User { get; set; } - public virtual Role? Role { get; set; } - } + public class UserRole + { + public int UserId { get; set; } + public int RoleId { get; set; } + + public virtual User? User { get; set; } + public virtual Role? Role { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/ApplicationUser.cs b/starsky/starsky.foundation.database/Models/ApplicationUser.cs index f95e5d6735..259bb515f6 100644 --- a/starsky/starsky.foundation.database/Models/ApplicationUser.cs +++ b/starsky/starsky.foundation.database/Models/ApplicationUser.cs @@ -1,11 +1,11 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Identity; namespace starsky.foundation.database.Models { - // Add profile data for application users by adding properties to the ApplicationUser class - [SuppressMessage("Usage", "S2094: Remove this empty class, write its code or make it an interface")] - public sealed class ApplicationUser : IdentityUser - { - } + // Add profile data for application users by adding properties to the ApplicationUser class + [SuppressMessage("Usage", "S2094: Remove this empty class, write its code or make it an interface")] + public sealed class ApplicationUser : IdentityUser + { + } } diff --git a/starsky/starsky.foundation.database/Models/DataProtectionKeyModel.cs b/starsky/starsky.foundation.database/Models/DataProtectionKeyModel.cs index 94c2d05bd1..4324e2862f 100644 --- a/starsky/starsky.foundation.database/Models/DataProtectionKeyModel.cs +++ b/starsky/starsky.foundation.database/Models/DataProtectionKeyModel.cs @@ -1,32 +1,34 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +// Warning for cref=EntityFrameworkCoreXmlRepository +#pragma warning disable CS1574, CS1584, CS1581, CS1580 + // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace starsky.foundation.database.Models -{ +namespace starsky.foundation.database.Models; +/// +/// Code first model used by . +/// +public class DataProtectionKey +{ /// - /// Code first model used by . + /// The entity identifier of the . /// - public class DataProtectionKey - { - /// - /// The entity identifier of the . - /// - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } - /// - /// The friendly name of the . - /// - public string? FriendlyName { get; set; } + /// + /// The friendly name of the . + /// + [MaxLength(45)] + public string? FriendlyName { get; set; } - /// - /// The XML representation of the . - /// - public string? Xml { get; set; } - } + /// + /// The XML representation of the . + /// + [MaxLength(1200)] + public string? Xml { get; set; } } - diff --git a/starsky/starsky.foundation.database/Models/DetailView.cs b/starsky/starsky.foundation.database/Models/DetailView.cs index 5d8c108916..c8815d4139 100644 --- a/starsky/starsky.foundation.database/Models/DetailView.cs +++ b/starsky/starsky.foundation.database/Models/DetailView.cs @@ -1,75 +1,75 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using starsky.foundation.platform.Helpers; namespace starsky.foundation.database.Models { - [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] - public sealed class DetailView - { - public FileIndexItem? FileIndexItem { get; set; } + [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")] + public sealed class DetailView + { + public FileIndexItem? FileIndexItem { get; set; } - public List Breadcrumb { get; set; } = new(); - - /// - /// List of selected Color Class's - /// - public List ColorClassActiveList { get; set; } = new(); - - /// - /// Used by react client - /// - [SuppressMessage("ReSharper", "CA1822")] - public string PageType => PageViewType.PageType.DetailView.ToString(); + public List Breadcrumb { get; set; } = new(); - /// - /// To return error codes in the json it is always false - /// - public bool IsDirectory { get; set; } + /// + /// List of selected Color Class's + /// + public List ColorClassActiveList { get; set; } = new(); - /// - /// Location of the path - /// - public string SubPath { get; set; } = string.Empty; + /// + /// Used by react client + /// + [SuppressMessage("ReSharper", "CA1822")] + public string PageType => PageViewType.PageType.DetailView.ToString(); - /// - /// Is collections enabled? - /// - public bool Collections { get; set; } + /// + /// To return error codes in the json it is always false + /// + public bool IsDirectory { get; set; } - /// - /// If collections is enabled return list of subPaths - /// Does NOT Fill the collection list - /// - /// the base fileIndexItem - /// bool, to enable - /// the file original requested in subPath style - /// - public static List GetCollectionSubPathList(FileIndexItem fileIndexItem, bool collections, string subPath) - { - // Paths that are used - var collectionSubPathList = fileIndexItem.CollectionPaths; - // when not running in collections mode only update one file - if (!collections) collectionSubPathList = new List {subPath}; - return collectionSubPathList; - } - - /// - /// Private field for next/prev - /// - private RelativeObjects _relativeObjects = new(); + /// + /// Location of the path + /// + public string SubPath { get; set; } = string.Empty; - public RelativeObjects RelativeObjects - { - get => _relativeObjects; - set - { - _relativeObjects = new RelativeObjects(Collections, ColorClassActiveList); - _relativeObjects = value; - } - } + /// + /// Is collections enabled? + /// + public bool Collections { get; set; } - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] - public bool IsReadOnly { get; set; } - } + /// + /// If collections is enabled return list of subPaths + /// Does NOT Fill the collection list + /// + /// the base fileIndexItem + /// bool, to enable + /// the file original requested in subPath style + /// + public static List GetCollectionSubPathList(FileIndexItem fileIndexItem, bool collections, string subPath) + { + // Paths that are used + var collectionSubPathList = fileIndexItem.CollectionPaths; + // when not running in collections mode only update one file + if ( !collections ) collectionSubPathList = new List { subPath }; + return collectionSubPathList; + } + + /// + /// Private field for next/prev + /// + private RelativeObjects _relativeObjects = new(); + + public RelativeObjects RelativeObjects + { + get => _relativeObjects; + set + { + _relativeObjects = new RelativeObjects(Collections, ColorClassActiveList); + _relativeObjects = value; + } + } + + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] + public bool IsReadOnly { get; set; } + } } diff --git a/starsky/starsky.foundation.database/Models/FileIndexItem.cs b/starsky/starsky.foundation.database/Models/FileIndexItem.cs index c20bfb37c9..b8ef4990f3 100644 --- a/starsky/starsky.foundation.database/Models/FileIndexItem.cs +++ b/starsky/starsky.foundation.database/Models/FileIndexItem.cs @@ -31,7 +31,7 @@ public FileIndexItem(string subPath) SetFilePath(subPath); IsDirectory = false; } - + /// /// Unique database Id, not used and Json Ignored due the fact that files that are moved could have a new Id /// @@ -61,7 +61,7 @@ public string? FilePath { get { - if ( string.IsNullOrEmpty(FilePathPrivate) ) + if ( string.IsNullOrEmpty(FilePathPrivate) ) { return "/"; } @@ -77,14 +77,14 @@ public string? FilePath public void SetFilePath(string? value) { var parentPath = FilenamesHelper.GetParentPath(value); - if ( string.IsNullOrEmpty(parentPath)) parentPath = "/"; + if ( string.IsNullOrEmpty(parentPath) ) parentPath = "/"; // Home has no parentDirectory and filename slash if ( value != "/" ) _parentDirectory = parentPath; - + _fileName = PathHelper.GetFileName(value); // filenames are without starting slash _fileName = PathHelper.RemovePrefixDbSlash(_fileName); - + FilePathPrivate = PathHelper.RemoveLatestSlash(ParentDirectory!) + PathHelper.PrefixDbSlash(FileName!); } @@ -109,14 +109,14 @@ public string? FileName get => _fileName ?? string.Empty; set { - if (string.IsNullOrWhiteSpace(value)) + if ( string.IsNullOrWhiteSpace(value) ) { _fileName = string.Empty; return; } _fileName = value; FilePathPrivate = PathHelper.RemoveLatestSlash(ParentDirectory!) + - PathHelper.PrefixDbSlash(value); + PathHelper.PrefixDbSlash(value); } } @@ -129,8 +129,8 @@ public string? FileName /// OZHCK4I47QPHOT53QBRE7Z4RLI [MaxLength(190)] // Index column size too large. The maximum column size is 767 bytes (767/4) public string? FileHash { get; set; } = string.Empty; - - + + /// /// GetFileNameWithoutExtension (only a getter) /// @@ -160,14 +160,14 @@ public string? ParentDirectory get => _parentDirectory ?? string.Empty; set { - if (string.IsNullOrWhiteSpace(value)) + if ( string.IsNullOrWhiteSpace(value) ) { _parentDirectory = string.Empty; return; } _parentDirectory = value; FilePathPrivate = PathHelper.RemoveLatestSlash(value) + - PathHelper.PrefixDbSlash(FileName!); + PathHelper.PrefixDbSlash(FileName!); } } @@ -188,15 +188,16 @@ public string? ParentDirectory /// [NotMapped] [JsonIgnore] // <== gives conversion errors with jsonParser - internal HashSet? Keywords { + internal HashSet? Keywords + { get => HashSetHelper.StringToHashSet(Tags?.Trim()!); set { - if (value == null) return; + if ( value == null ) return; _tags = HashSetHelper.HashSetToString(value); - } + } } - + /// /// Private API: Do not save null in database for tags /// @@ -217,7 +218,7 @@ public string? Tags get => _tags ?? string.Empty; set { - if (string.IsNullOrWhiteSpace(value)) + if ( string.IsNullOrWhiteSpace(value) ) { _tags = string.Empty; return; @@ -273,12 +274,12 @@ public enum ExifStatus /// File is in trash /// Deleted, - + /// /// Everything is good but not changed /// OkAndSame, - + /// /// File is in trash but not changed /// @@ -296,7 +297,7 @@ public enum ExifStatus [JsonConverter(typeof(JsonStringEnumConverter))] [NotMapped] public ExifStatus Status { get; set; } = ExifStatus.Default; - + /// /// Internal API: to store description /// @@ -314,7 +315,7 @@ public string? Description get => _description ?? string.Empty; set { - if (string.IsNullOrWhiteSpace(value)) + if ( string.IsNullOrWhiteSpace(value) ) { _description = string.Empty; return; @@ -322,7 +323,7 @@ public string? Description _description = value.Trim(); } } - + /// /// Private API: to store Title /// @@ -340,7 +341,7 @@ public string? Title get => _title ?? string.Empty; set { - if (string.IsNullOrWhiteSpace(value)) + if ( string.IsNullOrWhiteSpace(value) ) { _title = string.Empty; return; @@ -357,7 +358,7 @@ public string? Title /// The DateTime object /// public DateTime DateTime { get; set; } - + /// /// Datetime of when this item is added to the database /// @@ -365,7 +366,7 @@ public string? Title /// The add to database DateTime value /// public DateTime AddToDatabase { get; set; } - + /// /// Datetime of the last change to this object /// @@ -373,7 +374,7 @@ public string? Title /// The last edited DateTime value /// public DateTime LastEdited { get; set; } - + /// /// Update the add to Database Date /// @@ -431,10 +432,10 @@ public void SetAddToDatabase() /// /// The location country. /// - [MaxLength(40)] + [MaxLength(40)] public string? LocationCountry { get; set; } = string.Empty; - - + + /// /// ISO-3166-1 alpha-3 (for example NLD for The Netherlands) /// @see: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3 @@ -451,20 +452,21 @@ public void SetAddToDatabase() /// public static List GetColorClassList(string? colorClassString = null) { - if (string.IsNullOrWhiteSpace(colorClassString)) return new List(); + if ( string.IsNullOrWhiteSpace(colorClassString) ) return new List(); var colorClassStringList = new List(); - if (!colorClassString.Contains(',') ) + if ( !colorClassString.Contains(',') ) { - if (!int.TryParse(colorClassString, out var parsedInt)) return new List(); + if ( !int.TryParse(colorClassString, out var parsedInt) ) return new List(); colorClassStringList.Add(parsedInt.ToString()); } - if (colorClassString.Contains(',')) { + if ( colorClassString.Contains(',') ) + { colorClassStringList = colorClassString.Split(",".ToCharArray()).ToList(); } var colorClassList = new HashSet(); - foreach (var colorClassStringItem in colorClassStringList) + foreach ( var colorClassStringItem in colorClassStringList ) { colorClassList.Add(ColorClassParser.GetColorClass(colorClassStringItem)); } @@ -482,16 +484,16 @@ public static List FileIndexPropList() // only for types String in FileIndexItem() var allProperties = new FileIndexItem().GetType().GetProperties(); - foreach (var propertyInfo in allProperties) + foreach ( var propertyInfo in allProperties ) { - if (( - propertyInfo.PropertyType == typeof(bool) || - propertyInfo.PropertyType == typeof(bool?) || - propertyInfo.PropertyType == typeof(string) || + if ( ( + propertyInfo.PropertyType == typeof(bool) || + propertyInfo.PropertyType == typeof(bool?) || + propertyInfo.PropertyType == typeof(string) || propertyInfo.PropertyType == typeof(DateTime) || - propertyInfo.PropertyType == typeof(ExtensionRolesHelper.ImageFormat) || + propertyInfo.PropertyType == typeof(ExtensionRolesHelper.ImageFormat) || propertyInfo.PropertyType == typeof(ColorClassParser.Color) - ) && propertyInfo.CanRead) + ) && propertyInfo.CanRead ) { fileIndexPropList.Add(propertyInfo.Name); } @@ -517,25 +519,25 @@ public static List FileIndexPropList() [JsonConverter(typeof(JsonStringEnumConverter))] // newtonsoft uses: StringEnumConverter public Rotation Orientation { get; set; } = Rotation.DoNotChange; - + /// /// Exit Rotation values /// public enum Rotation { DoNotChange = -1, - + // There are more types: // https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ - - [Display(Name = "Horizontal (normal)")] + + [Display(Name = "Horizontal (normal)")] Horizontal = 1, [Display(Name = "Rotate 90 CW")] Rotate90Cw = 6, [Display(Name = "Rotate 180")] - Rotate180 = 3, + Rotate180 = 3, [Display(Name = "Rotate 270 CW")] Rotate270Cw = 8 @@ -580,18 +582,19 @@ public void SetRelativeOrientation(int relativeRotation = 0) /// enum value of the output rotation public Rotation RelativeOrientation(int relativeRotation = 0) { - if (Orientation == Rotation.DoNotChange) Orientation = Rotation.Horizontal; - + if ( Orientation == Rotation.DoNotChange ) Orientation = Rotation.Horizontal; + var currentOrientation = _orderRotation.FindIndex(i => i == Orientation); - - if (currentOrientation >= 0 && currentOrientation+relativeRotation < - _orderRotation.Count && currentOrientation+relativeRotation >= 0) + + if ( currentOrientation >= 0 && currentOrientation + relativeRotation < + _orderRotation.Count && currentOrientation + relativeRotation >= 0 ) { return _orderRotation[currentOrientation + relativeRotation]; } - if (currentOrientation + relativeRotation == -1) { + if ( currentOrientation + relativeRotation == -1 ) + { // ReSharper disable once UseIndexFromEndExpression - return _orderRotation[_orderRotation.Count-1]; //changed + return _orderRotation[_orderRotation.Count - 1]; //changed } return _orderRotation[0]; } @@ -604,7 +607,7 @@ public Rotation RelativeOrientation(int relativeRotation = 0) public Rotation SetAbsoluteOrientation(string orientationString = "0") { - switch (orientationString) + switch ( orientationString ) { case "1": Orientation = Rotation.Horizontal; @@ -660,8 +663,8 @@ public void SetImageWidth(string imageWidth) /// Width of the image. public void SetImageWidth(int imageWidth) { - if(imageWidth is >= 1 and <= ushort.MaxValue ) - ImageWidth = (ushort) imageWidth; + if ( imageWidth is >= 1 and <= ushort.MaxValue ) + ImageWidth = ( ushort )imageWidth; } /// @@ -682,10 +685,10 @@ public void SetImageHeight(string imageHeight) /// Height of the image. public void SetImageHeight(int imageHeight) { - if(imageHeight is >= 1 and <= ushort.MaxValue ) - ImageHeight = (ushort) imageHeight; + if ( imageHeight is >= 1 and <= ushort.MaxValue ) + ImageHeight = ( ushort )imageHeight; } - + /// /// Gets or sets the image format. (eg: jpg, tiff) @@ -722,13 +725,13 @@ public HashSet SidecarExtensionsList get { if ( string.IsNullOrWhiteSpace(SidecarExtensions) ) return new HashSet(); - return + return new HashSet( SidecarExtensions.Split('|') .Where(p => !string.IsNullOrWhiteSpace(p))); } } - + /// /// Add extensions without dot /// @@ -739,7 +742,7 @@ public void AddSidecarExtension(string ext) current.Add(ext); SidecarExtensions = string.Join("|", current); } - + /// /// Remove extensions without dot /// @@ -751,14 +754,14 @@ public void RemoveSidecarExtension(string ext) SidecarExtensions = string.Join("|", current); } - + /// /// Duplicate this item in memory. /// /// FileIndexItem duplicated public FileIndexItem Clone() { - return (FileIndexItem) MemberwiseClone(); + return ( FileIndexItem )MemberwiseClone(); } /// @@ -772,8 +775,9 @@ public FileIndexItem Clone() /// /// The aperture. /// - public double Aperture { - get => Math.Round(_aperture,5); + public double Aperture + { + get => Math.Round(_aperture, 5); set => _aperture = Math.Round(value, 5); } @@ -781,7 +785,7 @@ public double Aperture { /// Private field to store ShutterSpeed Data /// private string _shutterSpeed = string.Empty; - + /// /// Shutter speed as string so '1/2' or '1' /// @@ -789,7 +793,8 @@ public double Aperture { /// The shutter speed as string /// [MaxLength(20)] - public string? ShutterSpeed { + public string? ShutterSpeed + { get => string.IsNullOrEmpty(_shutterSpeed) ? string.Empty : _shutterSpeed; set { @@ -797,7 +802,7 @@ public string? ShutterSpeed { if ( value?.Length <= 20 ) _shutterSpeed = value; } } - + /// /// Gets or sets the iso speed used by cameras (saved as ushort 0-65535) /// @@ -824,10 +829,10 @@ public void SetIsoSpeed(string isoSpeed) /// The iso speed. public void SetIsoSpeed(int isoSpeed) { - if(isoSpeed is >= 1 and <= ushort.MaxValue ) - IsoSpeed = (ushort) isoSpeed; + if ( isoSpeed is >= 1 and <= ushort.MaxValue ) + IsoSpeed = ( ushort )isoSpeed; } - + /// /// Private field to store Software Data /// @@ -837,7 +842,8 @@ public void SetIsoSpeed(int isoSpeed) /// Edited with this program /// [MaxLength(40)] - public string? Software { + public string? Software + { get => string.IsNullOrEmpty(_softwareData) ? string.Empty : _softwareData; set => _softwareData = string.IsNullOrEmpty(value) ? string.Empty : value; } @@ -853,7 +859,8 @@ public string? Software { /// /// Camera brand and type (incl. lens) /// - public string? MakeModel { + public string? MakeModel + { get => string.IsNullOrEmpty(_makeModel) ? string.Empty : _makeModel; // ReSharper disable once PropertyCanBeMadeInitOnly.Global set => _makeModel = string.IsNullOrEmpty(value) ? string.Empty : value; @@ -893,7 +900,7 @@ public string Model return makeModelList?.Length != MakeModelFixedLength ? string.Empty : makeModelList[1]; } } - + /// /// Get the Lens info (from MakeModel) /// @@ -904,16 +911,16 @@ public string LensModel { if ( string.IsNullOrEmpty(_makeModel) ) return string.Empty; var makeModelList = MakeModel?.Split("|".ToCharArray()); - if( makeModelList?.Length != MakeModelFixedLength ) return string.Empty; + if ( makeModelList?.Length != MakeModelFixedLength ) return string.Empty; // ReSharper disable once ConvertIfStatementToReturnStatement if ( string.IsNullOrEmpty(Model) ) return makeModelList[2]; // It replaces the Camera Model in the lens - var lensModel = makeModelList[2].Replace(Model,string.Empty).Trim(); + var lensModel = makeModelList[2].Replace(Model, string.Empty).Trim(); return lensModel; } } - - + + /// /// The Zoom of the camera (that is currently used) /// @@ -923,21 +930,22 @@ public string LensModel /// Private field to store the ByteSize Data /// private long _size; - + /// /// Size of the file in bytes (BigInt) /// public long Size { get => _size; - set { + set + { if ( value < 0 ) { _size = 0; return; } _size = value; - } + } } /// @@ -975,7 +983,7 @@ public void SetMakeModel(string addedValue, int fieldIndex) makeModelList[fieldIndex] = titleValue; // Store Make: APPLE as Apple in the database - if ( fieldIndex == 0 ) makeModelList[fieldIndex] = + if ( fieldIndex == 0 ) makeModelList[fieldIndex] = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(titleValue.ToLowerInvariant()); _makeModel = FixedListToString(makeModelList); @@ -1013,7 +1021,7 @@ internal static string FixedListToString(IReadOnlyList? listKeywords) return toBeAddedKeywords; } - + /// /// Is ImageStabilisation unknown, on or off /// Currently Sony only @@ -1031,14 +1039,14 @@ internal static string FixedListToString(IReadOnlyList? listKeywords) } // end class - - - -// Make, -// CameraModelName, -// LensInfo, -// LensModel, -// Aperture, -// ShutterSpeed, -// ExposureMode + + + + // Make, + // CameraModelName, + // LensInfo, + // LensModel, + // Aperture, + // ShutterSpeed, + // ExposureMode } diff --git a/starsky/starsky.foundation.database/Models/ImportIndexItem.cs b/starsky/starsky.foundation.database/Models/ImportIndexItem.cs index a349868d06..f2029cd9e9 100644 --- a/starsky/starsky.foundation.database/Models/ImportIndexItem.cs +++ b/starsky/starsky.foundation.database/Models/ImportIndexItem.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -27,206 +27,208 @@ public enum ImportStatus ParentDirectoryNotFound, ReadOnlyFileSystem } - - [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] - public sealed class ImportIndexItem - { - /// - /// In order to create an instance of 'ImportIndexItem' - /// EF requires that a parameter-less constructor be declared. - /// - public ImportIndexItem() - { - } - - public ImportIndexItem(AppSettings appSettings) - { - Structure = appSettings.Structure; - } - - /// - /// Database Number (isn't used anywhere) - /// - [JsonIgnore] - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - /// - /// FileHash before importing - /// When using a -ColorClass=1 overwrite the fileHash changes during the import process - /// - public string? FileHash { get; set; } = string.Empty; - - public string GetFileHashWithUpdate() - { - if ( FileIndexItem == null && FileHash != null) - { - return FileHash; - } - return FileIndexItem?.FileHash ?? string.Empty; - } - - /// - /// The location where the image should be stored. - /// When the user move an item this field is NOT updated - /// - public string? FilePath { get; set; } = string.Empty; - - /// - /// UTC DateTime when the file is imported - /// - public DateTime AddToDatabase { get; set; } - - /// - /// DateTime of the photo/or when it is originally is made - /// - public DateTime DateTime{ get; set; } - - [NotMapped] + + [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] + public sealed class ImportIndexItem + { + /// + /// In order to create an instance of 'ImportIndexItem' + /// EF requires that a parameter-less constructor be declared. + /// + public ImportIndexItem() + { + } + + public ImportIndexItem(AppSettings appSettings) + { + Structure = appSettings.Structure; + } + + /// + /// Database Number (isn't used anywhere) + /// + [JsonIgnore] + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + /// FileHash before importing + /// When using a -ColorClass=1 overwrite the fileHash changes during the import process + /// + public string? FileHash { get; set; } = string.Empty; + + public string GetFileHashWithUpdate() + { + if ( FileIndexItem == null && FileHash != null ) + { + return FileHash; + } + return FileIndexItem?.FileHash ?? string.Empty; + } + + /// + /// The location where the image should be stored. + /// When the user move an item this field is NOT updated + /// + public string? FilePath { get; set; } = string.Empty; + + /// + /// UTC DateTime when the file is imported + /// + public DateTime AddToDatabase { get; set; } + + /// + /// DateTime of the photo/or when it is originally is made + /// + public DateTime DateTime { get; set; } + + [NotMapped] [JsonConverter(typeof(JsonStringEnumConverter))] - public ImportStatus Status { get; set; } - - [NotMapped] + public ImportStatus Status { get; set; } + + [NotMapped] public FileIndexItem? FileIndexItem { get; set; } - - [NotMapped] - [JsonIgnore] - public string SourceFullFilePath { get; set; } = string.Empty; - - // Defaults to _appSettings.Structure - // Feature to overwrite system structure by request - [NotMapped] - [JsonIgnore] - public string Structure { get; set; } = string.Empty; - - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] - public string? MakeModel { get; set; } = string.Empty; - - /// - /// Is the Exif DateTime parsed from the fileName - /// - public bool DateTimeFromFileName { get; set; } - - /// - /// ColorClass - /// - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] - public ColorClassParser.Color ColorClass { get; set; } - - public DateTime ParseDateTimeFromFileName() - { - // Depends on 'AppSettingsProvider.Structure' - // depends on SourceFullFilePath - if ( string.IsNullOrEmpty(SourceFullFilePath) ) - { - return new DateTime(0, DateTimeKind.Utc); - } - - var fileName = Path.GetFileNameWithoutExtension(SourceFullFilePath); - - // Replace asterisk > escape all options - var structuredFileName = Structure.Split("/".ToCharArray()).LastOrDefault(); - if ( structuredFileName == null || string.IsNullOrEmpty(fileName) ) { - return new DateTime(0, DateTimeKind.Utc); - } - structuredFileName = structuredFileName.Replace("*", ""); - structuredFileName = structuredFileName.Replace(".ext", string.Empty); - structuredFileName = structuredFileName.Replace("{filenamebase}", string.Empty); - - DateTime.TryParseExact(fileName, - structuredFileName, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out var dateTime); - - if (dateTime.Year >= 2) - { - DateTime = dateTime; - return dateTime; - } - - // Now retry it and replace special charaters from string - // For parsing files like: '2018-08-31 18.50.35' > '20180831185035' - Regex pattern = new Regex("-|_| |;|\\.|:", - RegexOptions.None, TimeSpan.FromMilliseconds(100)); - fileName = pattern.Replace(fileName,string.Empty); - structuredFileName = pattern.Replace(structuredFileName,string.Empty); - - DateTime.TryParseExact(fileName, - structuredFileName, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out dateTime); - - if (dateTime.Year >= 2) - { - DateTime = dateTime; - return dateTime; - } - - // when using /yyyymmhhss_{filenamebase}.jpg - // For the situation that the image has no exif date and there is an appendix used (in the config) - if(!string.IsNullOrWhiteSpace(fileName) && structuredFileName.Length >= fileName.Length) { - - structuredFileName = structuredFileName.Substring(0, fileName.Length-1); - - DateTime.TryParseExact(fileName, - structuredFileName, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out dateTime); - } - - if (dateTime.Year >= 2) - { - DateTime = dateTime; - return dateTime; - } - - // For the situation that the image has no exif date and there is an appendix - // used in the source filename AND the config - if ( !string.IsNullOrEmpty(fileName) && fileName.Length >= structuredFileName.Length ) - { - structuredFileName = RemoveEscapedCharacters(structuredFileName); - - // short the filename with structuredFileName - fileName = fileName.Substring(0, structuredFileName.Length); - - DateTime.TryParseExact(fileName, - structuredFileName, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out dateTime); - } - - // Return 0001-01-01 if everything fails - DateTime = dateTime; - return dateTime; - } - - /// - /// Removes the escaped characters and the first character after the backslash - /// - /// to input - /// the input string without those characters - public static string RemoveEscapedCharacters(string inputString) - { - var newString = new StringBuilder(); - for ( int i = 0; i < inputString.ToCharArray().Length; i++ ) - { - var structuredCharArray = inputString[i]; - var escapeChar = "\\"[0]; - if ( i != 0 && structuredCharArray != escapeChar && inputString[i - 1] != escapeChar ) - { - newString.Append(structuredCharArray); - } - - // add the first one - if ( i == 0 && structuredCharArray != escapeChar) newString.Append(structuredCharArray); - - } - return newString.ToString(); - } - } + + [NotMapped] + [JsonIgnore] + public string SourceFullFilePath { get; set; } = string.Empty; + + // Defaults to _appSettings.Structure + // Feature to overwrite system structure by request + [NotMapped] + [JsonIgnore] + public string Structure { get; set; } = string.Empty; + + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] + public string? MakeModel { get; set; } = string.Empty; + + /// + /// Is the Exif DateTime parsed from the fileName + /// + public bool DateTimeFromFileName { get; set; } + + /// + /// ColorClass + /// + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] + public ColorClassParser.Color ColorClass { get; set; } + + public DateTime ParseDateTimeFromFileName() + { + // Depends on 'AppSettingsProvider.Structure' + // depends on SourceFullFilePath + if ( string.IsNullOrEmpty(SourceFullFilePath) ) + { + return new DateTime(0, DateTimeKind.Utc); + } + + var fileName = Path.GetFileNameWithoutExtension(SourceFullFilePath); + + // Replace asterisk > escape all options + var structuredFileName = Structure.Split("/".ToCharArray()).LastOrDefault(); + if ( structuredFileName == null || string.IsNullOrEmpty(fileName) ) + { + return new DateTime(0, DateTimeKind.Utc); + } + structuredFileName = structuredFileName.Replace("*", ""); + structuredFileName = structuredFileName.Replace(".ext", string.Empty); + structuredFileName = structuredFileName.Replace("{filenamebase}", string.Empty); + + DateTime.TryParseExact(fileName, + structuredFileName, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var dateTime); + + if ( dateTime.Year >= 2 ) + { + DateTime = dateTime; + return dateTime; + } + + // Now retry it and replace special charaters from string + // For parsing files like: '2018-08-31 18.50.35' > '20180831185035' + Regex pattern = new Regex("-|_| |;|\\.|:", + RegexOptions.None, TimeSpan.FromMilliseconds(100)); + fileName = pattern.Replace(fileName, string.Empty); + structuredFileName = pattern.Replace(structuredFileName, string.Empty); + + DateTime.TryParseExact(fileName, + structuredFileName, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out dateTime); + + if ( dateTime.Year >= 2 ) + { + DateTime = dateTime; + return dateTime; + } + + // when using /yyyymmhhss_{filenamebase}.jpg + // For the situation that the image has no exif date and there is an appendix used (in the config) + if ( !string.IsNullOrWhiteSpace(fileName) && structuredFileName.Length >= fileName.Length ) + { + + structuredFileName = structuredFileName.Substring(0, fileName.Length - 1); + + DateTime.TryParseExact(fileName, + structuredFileName, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out dateTime); + } + + if ( dateTime.Year >= 2 ) + { + DateTime = dateTime; + return dateTime; + } + + // For the situation that the image has no exif date and there is an appendix + // used in the source filename AND the config + if ( !string.IsNullOrEmpty(fileName) && fileName.Length >= structuredFileName.Length ) + { + structuredFileName = RemoveEscapedCharacters(structuredFileName); + + // short the filename with structuredFileName + fileName = fileName.Substring(0, structuredFileName.Length); + + DateTime.TryParseExact(fileName, + structuredFileName, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out dateTime); + } + + // Return 0001-01-01 if everything fails + DateTime = dateTime; + return dateTime; + } + + /// + /// Removes the escaped characters and the first character after the backslash + /// + /// to input + /// the input string without those characters + public static string RemoveEscapedCharacters(string inputString) + { + var newString = new StringBuilder(); + for ( int i = 0; i < inputString.ToCharArray().Length; i++ ) + { + var structuredCharArray = inputString[i]; + var escapeChar = "\\"[0]; + if ( i != 0 && structuredCharArray != escapeChar && inputString[i - 1] != escapeChar ) + { + newString.Append(structuredCharArray); + } + + // add the first one + if ( i == 0 && structuredCharArray != escapeChar ) newString.Append(structuredCharArray); + + } + return newString.ToString(); + } + } } diff --git a/starsky/starsky.foundation.database/Models/MetadataContainer.cs b/starsky/starsky.foundation.database/Models/MetadataContainer.cs index 8d6346ee85..7d3fc2f572 100644 --- a/starsky/starsky.foundation.database/Models/MetadataContainer.cs +++ b/starsky/starsky.foundation.database/Models/MetadataContainer.cs @@ -10,7 +10,7 @@ public class MetadataContainer /// [JsonPropertyName("$id")] public string Id { get; set; } = "https://docs.qdraw.nl/schema/meta-data-container.json"; - + [JsonPropertyName("$schema")] public string Schema { get; set; } = "https://json-schema.org/draft/2020-12/schema"; diff --git a/starsky/starsky.foundation.database/Models/PageViewType.cs b/starsky/starsky.foundation.database/Models/PageViewType.cs index 4277160523..3e698f779f 100644 --- a/starsky/starsky.foundation.database/Models/PageViewType.cs +++ b/starsky/starsky.foundation.database/Models/PageViewType.cs @@ -1,14 +1,14 @@ -namespace starsky.foundation.database.Models +namespace starsky.foundation.database.Models { - public sealed class PageViewType - { - public enum PageType - { - Archive = 1, // index - DetailView = 2, - Search = 3, - Trash = 4, - Unknown = -1 - } - } + public sealed class PageViewType + { + public enum PageType + { + Archive = 1, // index + DetailView = 2, + Search = 3, + Trash = 4, + Unknown = -1 + } + } } diff --git a/starsky/starsky.foundation.database/Models/RelativeObjects.cs b/starsky/starsky.foundation.database/Models/RelativeObjects.cs index 0ce795fb25..ee5b38e999 100644 --- a/starsky/starsky.foundation.database/Models/RelativeObjects.cs +++ b/starsky/starsky.foundation.database/Models/RelativeObjects.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; using starsky.foundation.platform.Helpers; @@ -9,69 +9,69 @@ namespace starsky.foundation.database.Models /// /// In DetailView there are values added for handling Args /// - public sealed class RelativeObjects - { - /// - /// Set Args based on Collections and ColorClass Settings - /// - /// - /// - public RelativeObjects(bool collections, List? colorClassActiveList) - { - if ( !collections ) - { - Args.Add(nameof(collections).ToLowerInvariant(),"false"); - } - - if (colorClassActiveList is { Count: >= 1 } ) - { - var colorClassArg = new StringBuilder(); - for ( int i = 0; i < colorClassActiveList.Count; i++ ) - { - var colorClass = colorClassActiveList[i]; - if (i == colorClassActiveList.Count-1) - { - colorClassArg.Append(colorClass.GetHashCode()); - } - else - { - colorClassArg.Append(colorClass.GetHashCode()+ ","); - } - } - Args.Add(nameof(FileIndexItem.ColorClass).ToLowerInvariant(),colorClassArg.ToString()); - } - } - - public RelativeObjects() - { - } - - public string NextFilePath { get; set; } - public string PrevFilePath { get; set; } + public sealed class RelativeObjects + { + /// + /// Set Args based on Collections and ColorClass Settings + /// + /// + /// + public RelativeObjects(bool collections, List? colorClassActiveList) + { + if ( !collections ) + { + Args.Add(nameof(collections).ToLowerInvariant(), "false"); + } - public string NextHash { get; set; } + if ( colorClassActiveList is { Count: >= 1 } ) + { + var colorClassArg = new StringBuilder(); + for ( int i = 0; i < colorClassActiveList.Count; i++ ) + { + var colorClass = colorClassActiveList[i]; + if ( i == colorClassActiveList.Count - 1 ) + { + colorClassArg.Append(colorClass.GetHashCode()); + } + else + { + colorClassArg.Append(colorClass.GetHashCode() + ","); + } + } + Args.Add(nameof(FileIndexItem.ColorClass).ToLowerInvariant(), colorClassArg.ToString()); + } + } - public string PrevHash { get; set; } - - /// - /// Private field - /// - private Dictionary ArgsPrivate { get; set; } = new Dictionary(); + public RelativeObjects() + { + } - /// - /// Prevent overwrites with null args - /// - [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] - public Dictionary Args - { - get { return ArgsPrivate; } - set - { - if ( value == null ) return; - ArgsPrivate = value; - } - } - + public string NextFilePath { get; set; } + public string PrevFilePath { get; set; } - } + public string NextHash { get; set; } + + public string PrevHash { get; set; } + + /// + /// Private field + /// + private Dictionary ArgsPrivate { get; set; } = new Dictionary(); + + /// + /// Prevent overwrites with null args + /// + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] + public Dictionary Args + { + get { return ArgsPrivate; } + set + { + if ( value == null ) return; + ArgsPrivate = value; + } + } + + + } } diff --git a/starsky/starsky.foundation.database/Models/SettingsItem.cs b/starsky/starsky.foundation.database/Models/SettingsItem.cs index a637ff45a8..709a990bf4 100644 --- a/starsky/starsky.foundation.database/Models/SettingsItem.cs +++ b/starsky/starsky.foundation.database/Models/SettingsItem.cs @@ -7,7 +7,7 @@ namespace starsky.foundation.database.Models; [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public sealed class SettingsItem { - [Key] + [Key] [Column(TypeName = "varchar(150)")] [MaxLength(150)] [Required] diff --git a/starsky/starsky.foundation.database/Models/ThumbnailItem.cs b/starsky/starsky.foundation.database/Models/ThumbnailItem.cs index bc72775742..4101e17db2 100644 --- a/starsky/starsky.foundation.database/Models/ThumbnailItem.cs +++ b/starsky/starsky.foundation.database/Models/ThumbnailItem.cs @@ -1,8 +1,6 @@ -using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; -using starsky.foundation.platform.Enums; namespace starsky.foundation.database.Models; @@ -15,7 +13,7 @@ public ThumbnailItem() // used be EF Core FileHash = string.Empty; } - + public ThumbnailItem(string fileHash, bool? tinyMeta, bool? small, bool? large, bool? extraLarge, string? reasons = null) { @@ -26,7 +24,7 @@ public ThumbnailItem(string fileHash, bool? tinyMeta, bool? small, if ( extraLarge != null ) ExtraLarge = extraLarge; if ( reasons != null ) Reasons = reasons; } - + [Key] [Column(TypeName = "varchar(190)")] [MaxLength(190)] @@ -57,11 +55,12 @@ public ThumbnailItem(string fileHash, bool? tinyMeta, bool? small, /// Private field to avoid null issues /// private string ReasonsPrivate { get; set; } = string.Empty; - + /// /// When something went wrong add message here /// - public string? Reasons { + public string? Reasons + { get => ReasonsPrivate; set { @@ -69,7 +68,8 @@ public string? Reasons { { return; } + ReasonsPrivate = value; - } + } } } diff --git a/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs b/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs index 9be19a1035..ce3da2c9f9 100644 --- a/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs +++ b/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs @@ -5,7 +5,7 @@ namespace starsky.foundation.database.Models; public class ThumbnailResultDataTransferModel { - public ThumbnailResultDataTransferModel(string fileHash, bool? tinyMeta = null, bool? small= null, bool? large= null, bool? extraLarge = null) + public ThumbnailResultDataTransferModel(string fileHash, bool? tinyMeta = null, bool? small = null, bool? large = null, bool? extraLarge = null) { FileHash = fileHash; if ( tinyMeta != null ) TinyMeta = tinyMeta; @@ -13,7 +13,7 @@ public ThumbnailResultDataTransferModel(string fileHash, bool? tinyMeta = null, if ( large != null ) Large = large; if ( extraLarge != null ) ExtraLarge = extraLarge; } - + /// /// Null is to-do /// True is done @@ -44,7 +44,7 @@ public void Change(ThumbnailSize? thumbnailSize = null, bool? setStatus = null) throw new ArgumentOutOfRangeException(nameof(thumbnailSize), thumbnailSize, null); } } - + public string? FileHash { get; set; } /// @@ -66,7 +66,7 @@ public void Change(ThumbnailSize? thumbnailSize = null, bool? setStatus = null) /// 2000px, null is to-do, false is error, true, is done /// public bool? ExtraLarge { get; set; } - + /// /// When something went wrong add message here /// diff --git a/starsky/starsky.foundation.database/Notifications/NotificationQuery.cs b/starsky/starsky.foundation.database/Notifications/NotificationQuery.cs index 370b2cd659..6a95be7907 100644 --- a/starsky/starsky.foundation.database/Notifications/NotificationQuery.cs +++ b/starsky/starsky.foundation.database/Notifications/NotificationQuery.cs @@ -38,7 +38,7 @@ public async Task AddNotification(string content) DateTimeEpoch = DateTimeOffset.Now.ToUnixTimeSeconds(), Content = content }; - + async Task LocalAdd(ApplicationDbContext context) { await context.Notifications.AddAsync(item); @@ -58,7 +58,7 @@ async Task LocalAdd(ApplicationDbContext context) { await _context.SaveChangesAsync(); } - catch ( DbUpdateConcurrencyException e) + catch ( DbUpdateConcurrencyException e ) { _logger.LogInformation(e, "[AddNotification] save failed after DbUpdateConcurrencyException"); } @@ -68,26 +68,26 @@ async Task LocalAdd(ApplicationDbContext context) var context = new InjectServiceScope(_scopeFactory).Context(); return await LocalAdd(context); } - + return item; } public Task AddNotification(ApiNotificationResponseModel content) { var stringMessage = JsonSerializer.Serialize(content, - DefaultJsonSerializer.CamelCaseNoEnters); + DefaultJsonSerializer.CamelCaseNoEnters); return AddNotification(stringMessage); } public Task> GetNewerThan(DateTime parsedDateTime) { - var unixTime = ((DateTimeOffset)parsedDateTime).ToUnixTimeSeconds() -1; + var unixTime = ( ( DateTimeOffset )parsedDateTime ).ToUnixTimeSeconds() - 1; return _context.Notifications.Where(x => x.DateTimeEpoch > unixTime).ToListAsync(); } public Task> GetOlderThan(DateTime parsedDateTime) { - var unixTime = ((DateTimeOffset)parsedDateTime).ToUnixTimeSeconds(); + var unixTime = ( ( DateTimeOffset )parsedDateTime ).ToUnixTimeSeconds(); return _context.Notifications.Where(x => x.DateTimeEpoch < unixTime).ToListAsync(); } diff --git a/starsky/starsky.foundation.database/Query/InjectServiceScope.cs b/starsky/starsky.foundation.database/Query/InjectServiceScope.cs index 45aa177260..229a684883 100644 --- a/starsky/starsky.foundation.database/Query/InjectServiceScope.cs +++ b/starsky/starsky.foundation.database/Query/InjectServiceScope.cs @@ -4,8 +4,8 @@ using starsky.foundation.database.Data; #pragma warning disable CS8618 -[assembly:InternalsVisibleTo("starskytest"), - InternalsVisibleTo("starsky.foundation.settings")] +[assembly: InternalsVisibleTo("starskytest"), + InternalsVisibleTo("starsky.foundation.settings")] namespace starsky.foundation.database.Query { public class InjectServiceScope @@ -15,11 +15,11 @@ public class InjectServiceScope [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] public InjectServiceScope(IServiceScopeFactory? scopeFactory) { - if (scopeFactory == null) return; + if ( scopeFactory == null ) return; var scope = scopeFactory.CreateScope(); _dbContext = scope.ServiceProvider.GetRequiredService(); } - + /// /// Dependency injection, used in background tasks /// diff --git a/starsky/starsky.foundation.database/Query/Query.cs b/starsky/starsky.foundation.database/Query/Query.cs index c9af3fa994..bb6d88d588 100644 --- a/starsky/starsky.foundation.database/Query/Query.cs +++ b/starsky/starsky.foundation.database/Query/Query.cs @@ -19,59 +19,59 @@ namespace starsky.foundation.database.Query { - + [Service(typeof(IQuery), InjectionLifetime = InjectionLifetime.Scoped)] // ReSharper disable once RedundantExtendsListEntry public partial class Query : IQuery - { - private ApplicationDbContext _context; - private readonly IServiceScopeFactory? _scopeFactory; - private readonly IMemoryCache? _cache; - private readonly AppSettings _appSettings; - private readonly IWebLogger _logger; - - public Query(ApplicationDbContext context, - AppSettings appSettings, - IServiceScopeFactory? scopeFactory, - IWebLogger logger, IMemoryCache? memoryCache = null) - { - _context = context; - _cache = memoryCache; - _appSettings = appSettings; - _scopeFactory = scopeFactory; - _logger = logger; - } + { + private ApplicationDbContext _context; + private readonly IServiceScopeFactory? _scopeFactory; + private readonly IMemoryCache? _cache; + private readonly AppSettings _appSettings; + private readonly IWebLogger _logger; + + public Query(ApplicationDbContext context, + AppSettings appSettings, + IServiceScopeFactory? scopeFactory, + IWebLogger logger, IMemoryCache? memoryCache = null) + { + _context = context; + _cache = memoryCache; + _appSettings = appSettings; + _scopeFactory = scopeFactory; + _logger = logger; + } /// /// Returns a database object file or folder /// /// relative database path /// FileIndex-objects with database data - public FileIndexItem? GetObjectByFilePath(string filePath) + public FileIndexItem? GetObjectByFilePath(string filePath) { if ( filePath != "/" ) { filePath = PathHelper.RemoveLatestSlash(filePath); } - - FileIndexItem? LocalQuery(ApplicationDbContext context) - { - var item = context.FileIndex.FirstOrDefault(p => p.FilePath == filePath); - if ( item != null ) item.Status = FileIndexItem.ExifStatus.Ok; - return item; - } - - try - { - return LocalQuery(_context); - } - catch (ObjectDisposedException e) - { - _logger.LogInformation("[GetObjectByFilePath] catch-ed ObjectDisposedException", e); - return LocalQuery(new InjectServiceScope(_scopeFactory).Context()); - } - } - + + FileIndexItem? LocalQuery(ApplicationDbContext context) + { + var item = context.FileIndex.FirstOrDefault(p => p.FilePath == filePath); + if ( item != null ) item.Status = FileIndexItem.ExifStatus.Ok; + return item; + } + + try + { + return LocalQuery(_context); + } + catch ( ObjectDisposedException e ) + { + _logger.LogInformation("[GetObjectByFilePath] catch-ed ObjectDisposedException", e); + return LocalQuery(new InjectServiceScope(_scopeFactory).Context()); + } + } + internal static string GetObjectByFilePathAsyncCacheName(string subPath) { return $"_{nameof(GetObjectByFilePathAsyncCacheName)}~{subPath}"; @@ -87,13 +87,13 @@ internal static string GetObjectByFilePathAsyncCacheName(string subPath) string filePath, TimeSpan? cacheTime = null) { // cache code: - if ( cacheTime != null && - _appSettings.AddMemoryCache == true && - _cache != null && - _cache.TryGetValue( - GetObjectByFilePathAsyncCacheName(filePath), out var data) ) + if ( cacheTime != null && + _appSettings.AddMemoryCache == true && + _cache != null && + _cache.TryGetValue( + GetObjectByFilePathAsyncCacheName(filePath), out var data) ) { - if ( !(data is FileIndexItem fileIndexItem) ) return null; + if ( !( data is FileIndexItem fileIndexItem ) ) return null; fileIndexItem.Status = FileIndexItem.ExifStatus.OkAndSame; return fileIndexItem; } @@ -102,7 +102,7 @@ internal static string GetObjectByFilePathAsyncCacheName(string subPath) var result = ( await GetObjectByFilePathQueryAsync(filePath) ); // cache code: - if ( cacheTime == null || _appSettings.AddMemoryCache != true || result == null) + if ( cacheTime == null || _appSettings.AddMemoryCache != true || result == null ) return result; SetGetObjectByFilePathCache(filePath, result.Clone(), cacheTime); @@ -110,7 +110,7 @@ internal static string GetObjectByFilePathAsyncCacheName(string subPath) return result; } - public void SetGetObjectByFilePathCache(string filePath, + public void SetGetObjectByFilePathCache(string filePath, FileIndexItem result, TimeSpan? cacheTime) { @@ -121,14 +121,14 @@ public void SetGetObjectByFilePathCache(string filePath, return; } _cache.Set(GetObjectByFilePathAsyncCacheName(filePath), - result, cacheTime.Value ); + result, cacheTime.Value); } private async Task GetObjectByFilePathQueryAsync( string filePath) { if ( filePath != "/" ) filePath = PathHelper.RemoveLatestSlash(filePath); - var paths = new List {filePath}; + var paths = new List { filePath }; return ( await GetObjectsByFilePathQueryAsync(paths) ) .FirstOrDefault(); } @@ -136,575 +136,575 @@ public void SetGetObjectByFilePathCache(string filePath, public async Task GetSubPathByHashAsync(string fileHash) { // The CLI programs uses no cache - if( !IsCacheEnabled() || _cache == null) return await QueryGetItemByHashAsync(fileHash); - + if ( !IsCacheEnabled() || _cache == null ) return await QueryGetItemByHashAsync(fileHash); + // Return values from IMemoryCache var queryHashListCacheName = CachingDbName("hashList", fileHash); // if result is not null return cached value - if ( _cache.TryGetValue(queryHashListCacheName, out var cachedSubPath) - && !string.IsNullOrEmpty((string?)cachedSubPath)) return ( string ) cachedSubPath; + if ( _cache.TryGetValue(queryHashListCacheName, out var cachedSubPath) + && !string.IsNullOrEmpty(( string? )cachedSubPath) ) return ( string )cachedSubPath; cachedSubPath = await QueryGetItemByHashAsync(fileHash); - - _cache.Set(queryHashListCacheName, cachedSubPath, new TimeSpan(48,0,0)); - return (string?) cachedSubPath; + + _cache.Set(queryHashListCacheName, cachedSubPath, new TimeSpan(48, 0, 0)); + return ( string? )cachedSubPath; } /// /// Remove fileHash from hash-list-cache /// /// base32 fileHash - public void ResetItemByHash(string? fileHash) - { - if( _cache == null || _appSettings.AddMemoryCache == false) return; - + public void ResetItemByHash(string? fileHash) + { + if ( _cache == null || _appSettings.AddMemoryCache == false ) return; + var queryCacheName = CachingDbName("hashList", fileHash); - + if ( _cache.TryGetValue(queryCacheName, out _) ) { _cache.Remove(queryCacheName); } - } - - private async Task QueryGetItemByHashAsync(string fileHash) - { - async Task LocalQueryGetItemByHashAsync(ApplicationDbContext context) - { - return (await context.FileIndex.TagWith("QueryGetItemByHashAsync").FirstOrDefaultAsync( - p => p.FileHash == fileHash - && p.IsDirectory != true - ))?.FilePath; - } - - try - { - return await LocalQueryGetItemByHashAsync(_context); - } - catch ( ObjectDisposedException ) - { - var context = new InjectServiceScope(_scopeFactory).Context(); - return await LocalQueryGetItemByHashAsync(context); - } - } - - /// - /// Get the name of Key in the cache db - /// - /// how is the function called - /// the path - /// an unique key - [SuppressMessage("Performance", "CA1822:Mark members as static")] - // ReSharper disable once MemberCanBeMadeStatic.Global - internal string CachingDbName(string functionName, string? singleItemDbPath) - { - // when is nothing assume its the home item - if ( string.IsNullOrWhiteSpace(singleItemDbPath) ) singleItemDbPath = "/"; - // For creating an unique name: DetailView_/2018/01/1.jpg - var uniqueSingleDbCacheNameBuilder = new StringBuilder(); - uniqueSingleDbCacheNameBuilder.Append(functionName + "_" + singleItemDbPath); - return uniqueSingleDbCacheNameBuilder.ToString(); - } - - /// - /// Update one single item in the database - /// For the API/update endpoint - /// - /// content to updated - /// this item - public async Task UpdateItemAsync(FileIndexItem updateStatusContent) - { - async Task LocalQuery(ApplicationDbContext context, FileIndexItem fileIndexItem) - { - context.Attach(fileIndexItem).State = EntityState.Modified; - await context.SaveChangesAsync(); - context.Attach(fileIndexItem).State = EntityState.Detached; - // Clone to avoid reference when cache exists - CacheUpdateItem(new List{updateStatusContent.Clone()}); - if ( _appSettings.Verbose == true ) - { - // Ef core changes debug - _logger.LogDebug(context.ChangeTracker.DebugView.LongView); - } - - // object cache path is used to avoid updates - SetGetObjectByFilePathCache(fileIndexItem.FilePath!, updateStatusContent, TimeSpan.FromMinutes(1)); - } - - try - { - await LocalQuery(_context, updateStatusContent); - } - catch ( ObjectDisposedException e ) - { - await RetryQueryUpdateSaveChangesAsync(updateStatusContent, e,"UpdateItemAsync ObjectDisposedException"); - } - catch ( InvalidOperationException e) - { - // System.InvalidOperationException: Can't replace active reader. - await RetryQueryUpdateSaveChangesAsync(updateStatusContent, e, $"UpdateItemAsync InvalidOperationException {updateStatusContent.FilePath}" ,2000); - } - catch ( DbUpdateConcurrencyException concurrencyException) - { - SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); - try - { - await _context.SaveChangesAsync(); - } - catch ( DbUpdateConcurrencyException e) - { - _logger.LogInformation(e, "[UpdateItemAsync] save failed after DbUpdateConcurrencyException"); - } - } - catch ( MySqlException exception ) - { - // Skip if Duplicate entry - // MySqlConnector.MySqlException (0x80004005): Duplicate entry for key 'PRIMARY' - if ( !exception.Message.Contains("Duplicate") ) - { - throw; - } - _logger.LogError(exception, $"[UpdateItemAsync] Skipped MySqlException Duplicate entry for key {updateStatusContent.FilePath}"); - } - - return updateStatusContent; - } - - /// - /// Update item in Database Async - /// You should update the cache yourself (so this is NOT done) - /// - /// content to update - /// same item - public async Task> UpdateItemAsync(List updateStatusContentList) - { - if ( updateStatusContentList.Count == 0 ) - { - return new List(); - } - - async Task> LocalQuery(DbContext context, List fileIndexItems) - { - foreach ( var item in fileIndexItems ) - { - try - { - context.Attach(item).State = EntityState.Modified; - } - catch ( InvalidOperationException) - { - // System.InvalidOperationException: The property 'FileIndexItem.Id' has a temporary value while attempting to change the entity's state to 'Modified' - // Issue #994 - } - } - - await context.SaveChangesAsync(); - - foreach ( var item in fileIndexItems ) - { - context.Attach(item).State = EntityState.Detached; - } - - CacheUpdateItem(fileIndexItems); - return fileIndexItems; - } - - try - { - return await LocalQuery(_context, updateStatusContentList); - } - catch ( ObjectDisposedException ) - { - var context = new InjectServiceScope(_scopeFactory).Context(); - try - { - return await LocalQuery(context, updateStatusContentList); - } - catch ( DbUpdateConcurrencyException concurrencyException) - { - SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); - return await LocalQuery(context, updateStatusContentList); - } - } - catch ( DbUpdateConcurrencyException concurrencyException) - { - SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); - try - { - return await LocalQuery(_context, updateStatusContentList); - } - catch ( DbUpdateConcurrencyException e) - { - var items = await GetObjectsByFilePathQueryAsync(updateStatusContentList - .Where(p => p.FilePath != null) - .Select(p => p.FilePath).ToList()!); - _logger.LogInformation($"double error UCL:{updateStatusContentList.Count} Count: {items.Count}", e); - return updateStatusContentList; - } - } - } - - /// - /// Retry when an Exception has occured - /// - /// - /// Exception - /// Where from is this called, this helps to debug the code better - /// retry delay in milliseconds, 1000 = 1 second - internal async Task RetryQueryUpdateSaveChangesAsync(FileIndexItem updateStatusContent, Exception e, string source, int delay = 50) - { - if ( updateStatusContent.Id == 0 ) - { - _logger.LogError(e,$"[RetrySaveChangesAsync] skipped due 0 id: {source}"); - return null; - } - - _logger.LogInformation(e,$"[RetrySaveChangesAsync] retry catch-ed exception from {source} {updateStatusContent.FileName}"); - - async Task LocalRetrySaveChangesAsyncQuery() - { - // InvalidOperationException: A second operation started on this context before a previous operation completed. - // https://go.microsoft.com/fwlink/?linkid=2097913 - await Task.Delay(delay); - var context = new InjectServiceScope(_scopeFactory).Context(); - if ( context == null! ) - { - throw new AggregateException("Query Context is null"); - } - context.Attach(updateStatusContent).State = EntityState.Modified; - await context.SaveChangesAsync(); - context.Attach(updateStatusContent).State = EntityState.Unchanged; - await context.DisposeAsync(); - } - - try - { - await LocalRetrySaveChangesAsyncQuery(); - } - catch ( MySqlException mySqlException) - { - _logger.LogInformation(mySqlException,$"[RetrySaveChangesAsync] MySqlException catch-ed and retry again, from {source}"); - await LocalRetrySaveChangesAsyncQuery(); - } - catch ( DbUpdateConcurrencyException concurrencyException) - { - SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); - try - { - _logger.LogInformation("[RetrySaveChangesAsync] SolveConcurrencyExceptionLoop disposed item"); - var context = new InjectServiceScope(_scopeFactory).Context(); - await context.SaveChangesAsync(); - } - catch ( DbUpdateConcurrencyException retry2Exception) - { - _logger.LogError(retry2Exception, - "[RetrySaveChangesAsync] save failed after DbUpdateConcurrencyException"); - } - } - - _logger.LogInformation($"[RetrySaveChangesAsync] done saved from {source} {updateStatusContent.FileName}"); - return true; - } - - /// - /// Is Cache enabled, null object or feature toggle disabled - /// - /// true when enabled - internal bool IsCacheEnabled() - { - if( _cache == null || _appSettings.AddMemoryCache == false) return false; - return true; - } - - /// - /// Add child item to parent cache - /// Private api within Query to add cached items - /// Assumes that the parent directory already exist in the cache - /// @see: AddCacheParentItem to add parent item - /// - /// the content to add - internal void AddCacheItem(FileIndexItem updateStatusContent) - { - // If cache is turned of - if( _cache == null || _appSettings.AddMemoryCache == false) return; - - var queryCacheName = CachingDbName(nameof(FileIndexItem), - updateStatusContent.ParentDirectory!); - - if (!_cache.TryGetValue(queryCacheName, out var objectFileFolders)) return; - - objectFileFolders ??= new List(); - var displayFileFolders = (List) objectFileFolders; - - if ( updateStatusContent.FilePath == "/" ) - { - return; - } - - displayFileFolders.Add(updateStatusContent); - // Order by filename - displayFileFolders = displayFileFolders.OrderBy(p => p.FileName).ToList(); - - _cache.Remove(queryCacheName); - _cache.Set(queryCacheName, displayFileFolders, new TimeSpan(1,0,0)); - } - - /// - /// Cache API within Query to update cached items and implicit add items to list - /// - /// items to update - public void CacheUpdateItem(List updateStatusContent) - { - if( _cache == null || _appSettings.AddMemoryCache == false) return; - - var skippedCacheItems = new HashSet(); - foreach (var item in updateStatusContent.ToList()) - { - if ( item.Status == FileIndexItem.ExifStatus.OkAndSame || item.Status == FileIndexItem.ExifStatus.Default ) - { - item.Status = FileIndexItem.ExifStatus.Ok; - } - - // ToList() > Collection was modified; enumeration operation may not execute. - var queryCacheName = CachingDbName(nameof(FileIndexItem), - item.ParentDirectory!); - - if ( !_cache.TryGetValue(queryCacheName, - out var objectFileFolders) ) - { - skippedCacheItems.Add(item.ParentDirectory!); - continue; - } - - objectFileFolders ??= new List(); - var displayFileFolders = (List) objectFileFolders; - - // make it a list to avoid enum errors - displayFileFolders = displayFileFolders.ToList(); - - var obj = displayFileFolders.Find(p => p.FilePath == item.FilePath); - if ( obj != null ) - { - // remove add again - displayFileFolders.Remove(obj); - } - - if ( item.Status == FileIndexItem.ExifStatus.Ok) // ExifStatus.default is already changed - { - // Add here item to cached index - displayFileFolders.Add(item); - } - - // make it a list to avoid enum errors - displayFileFolders = displayFileFolders.ToList(); - // Order by filename - displayFileFolders = displayFileFolders.OrderBy(p => p.FileName).ToList(); - - _cache.Remove(queryCacheName); - _cache.Set(queryCacheName, displayFileFolders, new TimeSpan(1,0,0)); - } - - if ( skippedCacheItems.Count >= 1 && _appSettings.Verbose == true ) - { - _logger.LogInformation($"[CacheUpdateItem] skipped: {string.Join(", ", skippedCacheItems)}"); - } - - } - - /// - /// Cache Only! Private api within Query to remove cached items - /// This Does remove a SINGLE item from the cache NOT from the database - /// - /// - public void RemoveCacheItem(List updateStatusContent) - { - if( _cache == null || _appSettings.AddMemoryCache == false) return; - - foreach ( var item in updateStatusContent.ToList() ) - { - RemoveCacheItem(item); - } - } - - /// - /// Cache Only! Private api within Query to remove cached items - /// This Does remove a SINGLE item from the cache NOT from the database - /// - /// - public void RemoveCacheItem(FileIndexItem updateStatusContent) - { - // Add protection for disabled caching - if( _cache == null || _appSettings.AddMemoryCache == false) return; - - var queryCacheName = CachingDbName(nameof(FileIndexItem), - updateStatusContent.ParentDirectory!); - - if (!_cache.TryGetValue(queryCacheName, out var objectFileFolders)) return; - - objectFileFolders ??= new List(); - var displayFileFolders = (List) objectFileFolders; - - // Order by filename - displayFileFolders = displayFileFolders - .Where(p => p.FilePath != updateStatusContent.FilePath) - .OrderBy(p => p.FileName).ToList(); - - _cache.Remove(queryCacheName); - // generate list again - _cache.Set(queryCacheName, displayFileFolders, new TimeSpan(1,0,0)); - } - - /// - /// Clear the directory name from the cache - /// - /// the path of the directory (there is no parent generation) - public bool RemoveCacheParentItem(string directoryName) - { - // Add protection for disabled caching - if( _cache == null || _appSettings.AddMemoryCache == false) return false; - - var queryCacheName = CachingDbName(nameof(FileIndexItem), - PathHelper.RemoveLatestSlash(directoryName.Clone().ToString()!)); - if (!_cache.TryGetValue(queryCacheName, out _)) return false; - - _cache.Remove(queryCacheName); - return true; - } - - /// - /// Add an new Parent Item - /// - /// the path of the directory (there is no parent generation) - /// the items in the folder - public bool AddCacheParentItem(string directoryName, List items) - { - // Add protection for disabled caching - if( _cache == null || _appSettings.AddMemoryCache == false) return false; - - var queryCacheName = CachingDbName(nameof(FileIndexItem), - PathHelper.RemoveLatestSlash(directoryName.Clone().ToString()!)); - - _cache.Set(queryCacheName, items, - new TimeSpan(1,0,0)); - return true; - } - - /// - /// Add a new item to the database - /// - /// the item - /// item with id - [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] - public virtual async Task AddItemAsync(FileIndexItem fileIndexItem) - { - async Task LocalDefaultQuery() - { - var context = new InjectServiceScope(_scopeFactory).Context(); - return await LocalQuery(context); - } - - async Task LocalQuery(ApplicationDbContext context) - { - // only in test case fileIndex is null - if ( context.FileIndex != null ) await context.FileIndex.AddAsync(fileIndexItem); - await context.SaveChangesAsync(); - // Fix for: The instance of entity type 'Item' cannot be tracked because - // another instance with the same key value for {'Id'} is already being tracked - context.Entry(fileIndexItem).State = EntityState.Detached; - AddCacheItem(fileIndexItem); - return fileIndexItem; - } - - try - { - return await LocalQuery(_context); - } - catch ( Microsoft.Data.Sqlite.SqliteException e) - { - _logger.LogInformation(e, $"[AddItemAsync] catch-ed SqliteException going to retry 2 times {fileIndexItem.FilePath}"); - return await RetryHelper.DoAsync( - LocalDefaultQuery, TimeSpan.FromSeconds(2), 2); - } - catch ( DbUpdateException e) - { - _logger.LogInformation(e, $"[AddItemAsync] catch-ed DbUpdateException going to retry 2 times {fileIndexItem.FilePath}"); - return await RetryHelper.DoAsync( - LocalDefaultQuery, TimeSpan.FromSeconds(2), 2); - } - catch ( InvalidOperationException e) // or ObjectDisposedException - { - _logger.LogInformation(e, $"[AddItemAsync] catch-ed InvalidOperationException going to retry 2 times {fileIndexItem.FilePath}"); - return await RetryHelper.DoAsync( - LocalDefaultQuery, TimeSpan.FromSeconds(2), 2); - } - } - - private async Task> GetParentItems(List pathListShouldExist) - { - async Task> LocalQuery(ApplicationDbContext context) - { - return await context.FileIndex.Where(p => - pathListShouldExist.Any(f => f == p.FilePath)).ToListAsync(); - } - - try - { - return await LocalQuery(_context); - - } - catch ( ObjectDisposedException) - { - return await LocalQuery(new InjectServiceScope( _scopeFactory).Context()); - } - } - - /// - /// Add Sub Path Folder - Parent Folders - /// root(/) - /// /2017 *= index only this folder - /// /2018 - /// If you use the cmd: $ starskycli -s "/2017" - /// the folder '2017' it self is not added - /// and all parent paths are not included - /// this class does add those parent folders - /// - /// subPath as input - /// List - public async Task> AddParentItemsAsync(string subPath) - { - var path = subPath == "/" || string.IsNullOrEmpty(subPath) ? "/" : PathHelper.RemoveLatestSlash(subPath); - var pathListShouldExist = Breadcrumbs.BreadcrumbHelper(path).ToList(); - - var indexItems = await GetParentItems(pathListShouldExist); - - var toAddList = new List(); - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach ( var pathShouldExist in pathListShouldExist ) - { - if ( !indexItems.Select(p => p.FilePath).Contains(pathShouldExist) ) - { - toAddList.Add(new FileIndexItem(pathShouldExist) - { - IsDirectory = true, - AddToDatabase = DateTime.UtcNow, - ColorClass = ColorClassParser.Color.None, - Software = pathShouldExist == "/" ? "Root object" : string.Empty, - Status = FileIndexItem.ExifStatus.Ok - }); - } - } - - await AddRangeAsync(toAddList); - return toAddList; - } - - /// - /// Use only when new Context item is created manually, otherwise there is only 1 context - /// - public async Task DisposeAsync() - { - await _context.DisposeAsync(); - } - - } + } + + private async Task QueryGetItemByHashAsync(string fileHash) + { + async Task LocalQueryGetItemByHashAsync(ApplicationDbContext context) + { + return ( await context.FileIndex.TagWith("QueryGetItemByHashAsync").FirstOrDefaultAsync( + p => p.FileHash == fileHash + && p.IsDirectory != true + ) )?.FilePath; + } + + try + { + return await LocalQueryGetItemByHashAsync(_context); + } + catch ( ObjectDisposedException ) + { + var context = new InjectServiceScope(_scopeFactory).Context(); + return await LocalQueryGetItemByHashAsync(context); + } + } + + /// + /// Get the name of Key in the cache db + /// + /// how is the function called + /// the path + /// an unique key + [SuppressMessage("Performance", "CA1822:Mark members as static")] + // ReSharper disable once MemberCanBeMadeStatic.Global + internal string CachingDbName(string functionName, string? singleItemDbPath) + { + // when is nothing assume its the home item + if ( string.IsNullOrWhiteSpace(singleItemDbPath) ) singleItemDbPath = "/"; + // For creating an unique name: DetailView_/2018/01/1.jpg + var uniqueSingleDbCacheNameBuilder = new StringBuilder(); + uniqueSingleDbCacheNameBuilder.Append(functionName + "_" + singleItemDbPath); + return uniqueSingleDbCacheNameBuilder.ToString(); + } + + /// + /// Update one single item in the database + /// For the API/update endpoint + /// + /// content to updated + /// this item + public async Task UpdateItemAsync(FileIndexItem updateStatusContent) + { + async Task LocalQuery(ApplicationDbContext context, FileIndexItem fileIndexItem) + { + context.Attach(fileIndexItem).State = EntityState.Modified; + await context.SaveChangesAsync(); + context.Attach(fileIndexItem).State = EntityState.Detached; + // Clone to avoid reference when cache exists + CacheUpdateItem(new List { updateStatusContent.Clone() }); + if ( _appSettings.Verbose == true ) + { + // Ef core changes debug + _logger.LogDebug(context.ChangeTracker.DebugView.LongView); + } + + // object cache path is used to avoid updates + SetGetObjectByFilePathCache(fileIndexItem.FilePath!, updateStatusContent, TimeSpan.FromMinutes(1)); + } + + try + { + await LocalQuery(_context, updateStatusContent); + } + catch ( ObjectDisposedException e ) + { + await RetryQueryUpdateSaveChangesAsync(updateStatusContent, e, "UpdateItemAsync ObjectDisposedException"); + } + catch ( InvalidOperationException e ) + { + // System.InvalidOperationException: Can't replace active reader. + await RetryQueryUpdateSaveChangesAsync(updateStatusContent, e, $"UpdateItemAsync InvalidOperationException {updateStatusContent.FilePath}", 2000); + } + catch ( DbUpdateConcurrencyException concurrencyException ) + { + SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); + try + { + await _context.SaveChangesAsync(); + } + catch ( DbUpdateConcurrencyException e ) + { + _logger.LogInformation(e, "[UpdateItemAsync] save failed after DbUpdateConcurrencyException"); + } + } + catch ( MySqlException exception ) + { + // Skip if Duplicate entry + // MySqlConnector.MySqlException (0x80004005): Duplicate entry for key 'PRIMARY' + if ( !exception.Message.Contains("Duplicate") ) + { + throw; + } + _logger.LogError(exception, $"[UpdateItemAsync] Skipped MySqlException Duplicate entry for key {updateStatusContent.FilePath}"); + } + + return updateStatusContent; + } + + /// + /// Update item in Database Async + /// You should update the cache yourself (so this is NOT done) + /// + /// content to update + /// same item + public async Task> UpdateItemAsync(List updateStatusContentList) + { + if ( updateStatusContentList.Count == 0 ) + { + return new List(); + } + + async Task> LocalQuery(DbContext context, List fileIndexItems) + { + foreach ( var item in fileIndexItems ) + { + try + { + context.Attach(item).State = EntityState.Modified; + } + catch ( InvalidOperationException ) + { + // System.InvalidOperationException: The property 'FileIndexItem.Id' has a temporary value while attempting to change the entity's state to 'Modified' + // Issue #994 + } + } + + await context.SaveChangesAsync(); + + foreach ( var item in fileIndexItems ) + { + context.Attach(item).State = EntityState.Detached; + } + + CacheUpdateItem(fileIndexItems); + return fileIndexItems; + } + + try + { + return await LocalQuery(_context, updateStatusContentList); + } + catch ( ObjectDisposedException ) + { + var context = new InjectServiceScope(_scopeFactory).Context(); + try + { + return await LocalQuery(context, updateStatusContentList); + } + catch ( DbUpdateConcurrencyException concurrencyException ) + { + SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); + return await LocalQuery(context, updateStatusContentList); + } + } + catch ( DbUpdateConcurrencyException concurrencyException ) + { + SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); + try + { + return await LocalQuery(_context, updateStatusContentList); + } + catch ( DbUpdateConcurrencyException e ) + { + var items = await GetObjectsByFilePathQueryAsync(updateStatusContentList + .Where(p => p.FilePath != null) + .Select(p => p.FilePath).ToList()!); + _logger.LogInformation($"double error UCL:{updateStatusContentList.Count} Count: {items.Count}", e); + return updateStatusContentList; + } + } + } + + /// + /// Retry when an Exception has occured + /// + /// + /// Exception + /// Where from is this called, this helps to debug the code better + /// retry delay in milliseconds, 1000 = 1 second + internal async Task RetryQueryUpdateSaveChangesAsync(FileIndexItem updateStatusContent, Exception e, string source, int delay = 50) + { + if ( updateStatusContent.Id == 0 ) + { + _logger.LogError(e, $"[RetrySaveChangesAsync] skipped due 0 id: {source}"); + return null; + } + + _logger.LogInformation(e, $"[RetrySaveChangesAsync] retry catch-ed exception from {source} {updateStatusContent.FileName}"); + + async Task LocalRetrySaveChangesAsyncQuery() + { + // InvalidOperationException: A second operation started on this context before a previous operation completed. + // https://go.microsoft.com/fwlink/?linkid=2097913 + await Task.Delay(delay); + var context = new InjectServiceScope(_scopeFactory).Context(); + if ( context == null! ) + { + throw new AggregateException("Query Context is null"); + } + context.Attach(updateStatusContent).State = EntityState.Modified; + await context.SaveChangesAsync(); + context.Attach(updateStatusContent).State = EntityState.Unchanged; + await context.DisposeAsync(); + } + + try + { + await LocalRetrySaveChangesAsyncQuery(); + } + catch ( MySqlException mySqlException ) + { + _logger.LogInformation(mySqlException, $"[RetrySaveChangesAsync] MySqlException catch-ed and retry again, from {source}"); + await LocalRetrySaveChangesAsyncQuery(); + } + catch ( DbUpdateConcurrencyException concurrencyException ) + { + SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); + try + { + _logger.LogInformation("[RetrySaveChangesAsync] SolveConcurrencyExceptionLoop disposed item"); + var context = new InjectServiceScope(_scopeFactory).Context(); + await context.SaveChangesAsync(); + } + catch ( DbUpdateConcurrencyException retry2Exception ) + { + _logger.LogError(retry2Exception, + "[RetrySaveChangesAsync] save failed after DbUpdateConcurrencyException"); + } + } + + _logger.LogInformation($"[RetrySaveChangesAsync] done saved from {source} {updateStatusContent.FileName}"); + return true; + } + + /// + /// Is Cache enabled, null object or feature toggle disabled + /// + /// true when enabled + internal bool IsCacheEnabled() + { + if ( _cache == null || _appSettings.AddMemoryCache == false ) return false; + return true; + } + + /// + /// Add child item to parent cache + /// Private api within Query to add cached items + /// Assumes that the parent directory already exist in the cache + /// @see: AddCacheParentItem to add parent item + /// + /// the content to add + internal void AddCacheItem(FileIndexItem updateStatusContent) + { + // If cache is turned of + if ( _cache == null || _appSettings.AddMemoryCache == false ) return; + + var queryCacheName = CachingDbName(nameof(FileIndexItem), + updateStatusContent.ParentDirectory!); + + if ( !_cache.TryGetValue(queryCacheName, out var objectFileFolders) ) return; + + objectFileFolders ??= new List(); + var displayFileFolders = ( List )objectFileFolders; + + if ( updateStatusContent.FilePath == "/" ) + { + return; + } + + displayFileFolders.Add(updateStatusContent); + // Order by filename + displayFileFolders = displayFileFolders.OrderBy(p => p.FileName).ToList(); + + _cache.Remove(queryCacheName); + _cache.Set(queryCacheName, displayFileFolders, new TimeSpan(1, 0, 0)); + } + + /// + /// Cache API within Query to update cached items and implicit add items to list + /// + /// items to update + public void CacheUpdateItem(List updateStatusContent) + { + if ( _cache == null || _appSettings.AddMemoryCache == false ) return; + + var skippedCacheItems = new HashSet(); + foreach ( var item in updateStatusContent.ToList() ) + { + if ( item.Status == FileIndexItem.ExifStatus.OkAndSame || item.Status == FileIndexItem.ExifStatus.Default ) + { + item.Status = FileIndexItem.ExifStatus.Ok; + } + + // ToList() > Collection was modified; enumeration operation may not execute. + var queryCacheName = CachingDbName(nameof(FileIndexItem), + item.ParentDirectory!); + + if ( !_cache.TryGetValue(queryCacheName, + out var objectFileFolders) ) + { + skippedCacheItems.Add(item.ParentDirectory!); + continue; + } + + objectFileFolders ??= new List(); + var displayFileFolders = ( List )objectFileFolders; + + // make it a list to avoid enum errors + displayFileFolders = displayFileFolders.ToList(); + + var obj = displayFileFolders.Find(p => p.FilePath == item.FilePath); + if ( obj != null ) + { + // remove add again + displayFileFolders.Remove(obj); + } + + if ( item.Status == FileIndexItem.ExifStatus.Ok ) // ExifStatus.default is already changed + { + // Add here item to cached index + displayFileFolders.Add(item); + } + + // make it a list to avoid enum errors + displayFileFolders = displayFileFolders.ToList(); + // Order by filename + displayFileFolders = displayFileFolders.OrderBy(p => p.FileName).ToList(); + + _cache.Remove(queryCacheName); + _cache.Set(queryCacheName, displayFileFolders, new TimeSpan(1, 0, 0)); + } + + if ( skippedCacheItems.Count >= 1 && _appSettings.Verbose == true ) + { + _logger.LogInformation($"[CacheUpdateItem] skipped: {string.Join(", ", skippedCacheItems)}"); + } + + } + + /// + /// Cache Only! Private api within Query to remove cached items + /// This Does remove a SINGLE item from the cache NOT from the database + /// + /// + public void RemoveCacheItem(List updateStatusContent) + { + if ( _cache == null || _appSettings.AddMemoryCache == false ) return; + + foreach ( var item in updateStatusContent.ToList() ) + { + RemoveCacheItem(item); + } + } + + /// + /// Cache Only! Private api within Query to remove cached items + /// This Does remove a SINGLE item from the cache NOT from the database + /// + /// + public void RemoveCacheItem(FileIndexItem updateStatusContent) + { + // Add protection for disabled caching + if ( _cache == null || _appSettings.AddMemoryCache == false ) return; + + var queryCacheName = CachingDbName(nameof(FileIndexItem), + updateStatusContent.ParentDirectory!); + + if ( !_cache.TryGetValue(queryCacheName, out var objectFileFolders) ) return; + + objectFileFolders ??= new List(); + var displayFileFolders = ( List )objectFileFolders; + + // Order by filename + displayFileFolders = displayFileFolders + .Where(p => p.FilePath != updateStatusContent.FilePath) + .OrderBy(p => p.FileName).ToList(); + + _cache.Remove(queryCacheName); + // generate list again + _cache.Set(queryCacheName, displayFileFolders, new TimeSpan(1, 0, 0)); + } + + /// + /// Clear the directory name from the cache + /// + /// the path of the directory (there is no parent generation) + public bool RemoveCacheParentItem(string directoryName) + { + // Add protection for disabled caching + if ( _cache == null || _appSettings.AddMemoryCache == false ) return false; + + var queryCacheName = CachingDbName(nameof(FileIndexItem), + PathHelper.RemoveLatestSlash(directoryName.Clone().ToString()!)); + if ( !_cache.TryGetValue(queryCacheName, out _) ) return false; + + _cache.Remove(queryCacheName); + return true; + } + + /// + /// Add an new Parent Item + /// + /// the path of the directory (there is no parent generation) + /// the items in the folder + public bool AddCacheParentItem(string directoryName, List items) + { + // Add protection for disabled caching + if ( _cache == null || _appSettings.AddMemoryCache == false ) return false; + + var queryCacheName = CachingDbName(nameof(FileIndexItem), + PathHelper.RemoveLatestSlash(directoryName.Clone().ToString()!)); + + _cache.Set(queryCacheName, items, + new TimeSpan(1, 0, 0)); + return true; + } + + /// + /// Add a new item to the database + /// + /// the item + /// item with id + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] + public virtual async Task AddItemAsync(FileIndexItem fileIndexItem) + { + async Task LocalDefaultQuery() + { + var context = new InjectServiceScope(_scopeFactory).Context(); + return await LocalQuery(context); + } + + async Task LocalQuery(ApplicationDbContext context) + { + // only in test case fileIndex is null + if ( context.FileIndex != null ) await context.FileIndex.AddAsync(fileIndexItem); + await context.SaveChangesAsync(); + // Fix for: The instance of entity type 'Item' cannot be tracked because + // another instance with the same key value for {'Id'} is already being tracked + context.Entry(fileIndexItem).State = EntityState.Detached; + AddCacheItem(fileIndexItem); + return fileIndexItem; + } + + try + { + return await LocalQuery(_context); + } + catch ( Microsoft.Data.Sqlite.SqliteException e ) + { + _logger.LogInformation(e, $"[AddItemAsync] catch-ed SqliteException going to retry 2 times {fileIndexItem.FilePath}"); + return await RetryHelper.DoAsync( + LocalDefaultQuery, TimeSpan.FromSeconds(2), 2); + } + catch ( DbUpdateException e ) + { + _logger.LogInformation(e, $"[AddItemAsync] catch-ed DbUpdateException going to retry 2 times {fileIndexItem.FilePath}"); + return await RetryHelper.DoAsync( + LocalDefaultQuery, TimeSpan.FromSeconds(2), 2); + } + catch ( InvalidOperationException e ) // or ObjectDisposedException + { + _logger.LogInformation(e, $"[AddItemAsync] catch-ed InvalidOperationException going to retry 2 times {fileIndexItem.FilePath}"); + return await RetryHelper.DoAsync( + LocalDefaultQuery, TimeSpan.FromSeconds(2), 2); + } + } + + private async Task> GetParentItems(List pathListShouldExist) + { + async Task> LocalQuery(ApplicationDbContext context) + { + return await context.FileIndex.Where(p => + pathListShouldExist.Any(f => f == p.FilePath)).ToListAsync(); + } + + try + { + return await LocalQuery(_context); + + } + catch ( ObjectDisposedException ) + { + return await LocalQuery(new InjectServiceScope(_scopeFactory).Context()); + } + } + + /// + /// Add Sub Path Folder - Parent Folders + /// root(/) + /// /2017 *= index only this folder + /// /2018 + /// If you use the cmd: $ starskycli -s "/2017" + /// the folder '2017' it self is not added + /// and all parent paths are not included + /// this class does add those parent folders + /// + /// subPath as input + /// List + public async Task> AddParentItemsAsync(string subPath) + { + var path = subPath == "/" || string.IsNullOrEmpty(subPath) ? "/" : PathHelper.RemoveLatestSlash(subPath); + var pathListShouldExist = Breadcrumbs.BreadcrumbHelper(path).ToList(); + + var indexItems = await GetParentItems(pathListShouldExist); + + var toAddList = new List(); + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach ( var pathShouldExist in pathListShouldExist ) + { + if ( !indexItems.Select(p => p.FilePath).Contains(pathShouldExist) ) + { + toAddList.Add(new FileIndexItem(pathShouldExist) + { + IsDirectory = true, + AddToDatabase = DateTime.UtcNow, + ColorClass = ColorClassParser.Color.None, + Software = pathShouldExist == "/" ? "Root object" : string.Empty, + Status = FileIndexItem.ExifStatus.Ok + }); + } + } + + await AddRangeAsync(toAddList); + return toAddList; + } + + /// + /// Use only when new Context item is created manually, otherwise there is only 1 context + /// + public async Task DisposeAsync() + { + await _context.DisposeAsync(); + } + + } } diff --git a/starsky/starsky.foundation.database/Query/QueryAddRange.cs b/starsky/starsky.foundation.database/Query/QueryAddRange.cs index 12b2846a9f..c844949ca3 100644 --- a/starsky/starsky.foundation.database/Query/QueryAddRange.cs +++ b/starsky/starsky.foundation.database/Query/QueryAddRange.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using starsky.foundation.database.Data; -using starsky.foundation.database.Helpers; using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; @@ -36,10 +34,11 @@ async Task LocalQuery(ApplicationDbContext context, context.Attach(item).State = EntityState.Detached; } } - + async Task LocalRemoveDefaultQuery() { - await LocalQuery(new InjectServiceScope(_scopeFactory).Context(), fileIndexItemList); + await LocalQuery(new InjectServiceScope(_scopeFactory).Context(), + fileIndexItemList); return true; } @@ -92,6 +91,5 @@ await RetryHelper.DoAsync(LocalRemoveDefaultQuery, return fileIndexItemList; } - } } diff --git a/starsky/starsky.foundation.database/Query/QueryCollections.cs b/starsky/starsky.foundation.database/Query/QueryCollections.cs index a0075764f8..34a63acd58 100644 --- a/starsky/starsky.foundation.database/Query/QueryCollections.cs +++ b/starsky/starsky.foundation.database/Query/QueryCollections.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using starsky.foundation.database.Models; @@ -6,57 +6,57 @@ namespace starsky.foundation.database.Query { - public partial class Query // QueryCollections - { - internal static List StackCollections(List databaseSubFolderList) - { - // Get a list of duplicate items - var stackItemsByFileCollectionName = databaseSubFolderList - .GroupBy(item => item.FileCollectionName) - .SelectMany(grp => grp.Skip(1).Take(1)).ToList(); + public partial class Query // QueryCollections + { + internal static List StackCollections(List databaseSubFolderList) + { + // Get a list of duplicate items + var stackItemsByFileCollectionName = databaseSubFolderList + .GroupBy(item => item.FileCollectionName) + .SelectMany(grp => grp.Skip(1).Take(1)).ToList(); // databaseSubFolderList.ToList() > Collection was modified; enumeration operation may not execute. - - // duplicateItemsByFilePath > - // If you have 3 item with the same name it will include 1 name - // So we do a linq query to search simalar items - // We keep the first item - // And Delete duplicate items - - var querySubFolderList = new List(); - // Do not remove it from: databaseSubFolderList otherwise it will be deleted from cache - - foreach (var stackItemByName in stackItemsByFileCollectionName) - { - var duplicateItems = databaseSubFolderList.Where(p => - p.FileCollectionName == stackItemByName.FileCollectionName).ToList(); - - // The idea to pick thumbnail based images first, followed by non-thumb supported - // when not pick alphabetically - - querySubFolderList.AddRange(duplicateItems.Where(item => - ExtensionRolesHelper.IsExtensionThumbnailSupported(item.FileName))); - } - - return AddNonDuplicateBackToList(databaseSubFolderList,stackItemsByFileCollectionName,querySubFolderList); - } - - [SuppressMessage("Usage", "S3267:Loops should be simplified with LINQ expressions")] - [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] - private static List AddNonDuplicateBackToList(IEnumerable databaseSubFolderList, - IReadOnlyCollection stackItemsByFileCollectionName, ICollection querySubFolderList) - { - // Then add the items that are non duplicate back to the list - foreach (var dbItem in databaseSubFolderList.ToList()) - { - // check if any item is duplicate - if (stackItemsByFileCollectionName.All(p => - p.FileCollectionName != dbItem.FileCollectionName)) - { - querySubFolderList.Add(dbItem); - } - } - - return querySubFolderList.OrderBy(p => p.FileName).ToList(); - } - } + + // duplicateItemsByFilePath > + // If you have 3 item with the same name it will include 1 name + // So we do a linq query to search simalar items + // We keep the first item + // And Delete duplicate items + + var querySubFolderList = new List(); + // Do not remove it from: databaseSubFolderList otherwise it will be deleted from cache + + foreach ( var stackItemByName in stackItemsByFileCollectionName ) + { + var duplicateItems = databaseSubFolderList.Where(p => + p.FileCollectionName == stackItemByName.FileCollectionName).ToList(); + + // The idea to pick thumbnail based images first, followed by non-thumb supported + // when not pick alphabetically + + querySubFolderList.AddRange(duplicateItems.Where(item => + ExtensionRolesHelper.IsExtensionThumbnailSupported(item.FileName))); + } + + return AddNonDuplicateBackToList(databaseSubFolderList, stackItemsByFileCollectionName, querySubFolderList); + } + + [SuppressMessage("Usage", "S3267:Loops should be simplified with LINQ expressions")] + [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] + private static List AddNonDuplicateBackToList(IEnumerable databaseSubFolderList, + IReadOnlyCollection stackItemsByFileCollectionName, ICollection querySubFolderList) + { + // Then add the items that are non duplicate back to the list + foreach ( var dbItem in databaseSubFolderList.ToList() ) + { + // check if any item is duplicate + if ( stackItemsByFileCollectionName.All(p => + p.FileCollectionName != dbItem.FileCollectionName) ) + { + querySubFolderList.Add(dbItem); + } + } + + return querySubFolderList.OrderBy(p => p.FileName).ToList(); + } + } } diff --git a/starsky/starsky.foundation.database/Query/QueryCount.cs b/starsky/starsky.foundation.database/Query/QueryCount.cs index b957a5c407..21e9a83cc0 100644 --- a/starsky/starsky.foundation.database/Query/QueryCount.cs +++ b/starsky/starsky.foundation.database/Query/QueryCount.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Linq.Expressions; using System.Threading.Tasks; @@ -14,9 +13,9 @@ public partial class Query { public Task CountAsync(Expression>? expression = null) { - return expression == null ? _context.FileIndex.CountAsync() : _context.FileIndex.CountAsync(expression); + return expression == null + ? _context.FileIndex.CountAsync() + : _context.FileIndex.CountAsync(expression); } } - } - diff --git a/starsky/starsky.foundation.database/Query/QueryFactory.cs b/starsky/starsky.foundation.database/Query/QueryFactory.cs index c1ae997cd1..d066830445 100644 --- a/starsky/starsky.foundation.database/Query/QueryFactory.cs +++ b/starsky/starsky.foundation.database/Query/QueryFactory.cs @@ -1,7 +1,5 @@ -#nullable enable using System; using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using starsky.foundation.database.Helpers; @@ -21,35 +19,38 @@ public class QueryFactory private readonly IServiceScopeFactory? _serviceScopeFactory; private readonly IWebLogger? _logger; - public QueryFactory(SetupDatabaseTypes? setupDatabaseTypes, IQuery? query, - IMemoryCache? cache, AppSettings? appSettings, IServiceScopeFactory? serviceScopeFactory, IWebLogger? logger) + public QueryFactory(SetupDatabaseTypes? setupDatabaseTypes, IQuery? query, + IMemoryCache? cache, AppSettings? appSettings, + IServiceScopeFactory? serviceScopeFactory, IWebLogger? logger) { _setupDatabaseTypes = setupDatabaseTypes; _query = query; - _cache = cache; + _cache = cache; _appSettings = appSettings; _serviceScopeFactory = serviceScopeFactory; _logger = logger; } - + public IQuery? Query() { if ( _query == null ) return null!; var context = _setupDatabaseTypes?.BuilderDbFactory(); - if ( _query.GetType() == typeof(Query) && context != null && _appSettings != null && _logger != null) + if ( _query.GetType() == typeof(Query) && context != null && _appSettings != null && + _logger != null ) { return new Query(context, _appSettings, _serviceScopeFactory!, _logger, _cache); } // FakeIQuery should skip creation - var isAnyContentIncluded = _query.GetReflectionFieldValue?>("_content")?.Count >= 1; + var isAnyContentIncluded = + _query.GetReflectionFieldValue?>("_content")?.Count >= 1; if ( !isAnyContentIncluded ) { // ApplicationDbContext context, // AppSettings appSettings, // IServiceScopeFactory scopeFactory, // IWebLogger logger, IMemoryCache memoryCache = null - + return Activator.CreateInstance(_query.GetType(), context, _appSettings, _serviceScopeFactory, _logger, _cache) as IQuery; diff --git a/starsky/starsky.foundation.database/Query/QueryFolder.cs b/starsky/starsky.foundation.database/Query/QueryFolder.cs index 43165fdac8..f2e0e82c5b 100644 --- a/starsky/starsky.foundation.database/Query/QueryFolder.cs +++ b/starsky/starsky.foundation.database/Query/QueryFolder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -9,225 +9,226 @@ namespace starsky.foundation.database.Query { - public partial class Query // For folder displays only - { - - /// - /// Query all FileIndexItems with the type folder - /// - /// List of all folders in database, including content - public List GetAllFolders() - { - try - { - return _context.FileIndex.Where(p => p.IsDirectory == true).ToList(); - } - catch ( ObjectDisposedException ) - { - var context = new InjectServiceScope( _scopeFactory).Context(); - return context.FileIndex.Where(p => p.IsDirectory == true).ToList(); - } - } - - - // Class for displaying folder content - // This is the query part - public IEnumerable DisplayFileFolders( - string subPath = "/", - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true) - { - if ( subPath != "/" ) PathHelper.RemoveLatestSlash(subPath); - - var fileIndexItems = CacheQueryDisplayFileFolders(subPath); - - return DisplayFileFolders(fileIndexItems, - colorClassActiveList, - enableCollections, - hideDeleted); - } - - // Display File folder displays content of the folder - // without any query in this method - public IEnumerable DisplayFileFolders( - List fileIndexItems, - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true) - { - colorClassActiveList ??= new List(); - - // Hide meta files in list - fileIndexItems = fileIndexItems.Where(p => - p.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp && - p.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json).ToList(); - - if (colorClassActiveList.Count != 0 ) - { - fileIndexItems = fileIndexItems.Where(p => colorClassActiveList.Contains(p.ColorClass)).ToList(); - } - - if (fileIndexItems.Count == 0 ) - { - return new List(); - } - - if (enableCollections) - { - // Query Collections - fileIndexItems = StackCollections(fileIndexItems); - } - - return hideDeleted ? HideDeletedFileFolderList(fileIndexItems) : fileIndexItems; - } - - public Tuple> CacheGetParentFolder(string subPath) - { - var fallbackResult = new Tuple>(false, - new List()); - if ( _cache == null || _appSettings?.AddMemoryCache == false ) - return fallbackResult; - - // Return values from IMemoryCache - var queryCacheName = CachingDbName(nameof(FileIndexItem), - subPath); - - if ( !_cache.TryGetValue(queryCacheName, - out var objectFileFolders) ) return fallbackResult; - - var result = objectFileFolders as List ?? - new List(); - return new Tuple>(true,result); - } - - private List CacheQueryDisplayFileFolders(string subPath) - { - // The CLI programs uses no cache - if( _cache == null || _appSettings.AddMemoryCache == false) return QueryDisplayFileFolders(subPath); - - var (isSuccess, objectFileFolders) = CacheGetParentFolder(subPath); - - if ( isSuccess ) return objectFileFolders; - - objectFileFolders = QueryDisplayFileFolders(subPath); - - AddCacheParentItem(subPath, objectFileFolders); - return objectFileFolders; - } - - internal List QueryDisplayFileFolders(string subPath = "/") - { - List QueryItems(ApplicationDbContext context) - { - var queryItems = context.FileIndex - .TagWith("QueryDisplayFileFolders") - .Where(p => p.ParentDirectory == subPath && p.FileName != "/") - .OrderBy(p => p.FileName).AsEnumerable() - // remove duplicates from list - .GroupBy(t => t.FileName).Select(g => g.First()); - return queryItems.OrderBy(p => p.FileName, StringComparer.InvariantCulture).ToList(); - } - - try - { - return QueryItems(_context); - } - catch ( NotSupportedException ) - { - // System.NotSupportedException: The ReadAsync method cannot be called when another read operation is pending. - var context = new InjectServiceScope(_scopeFactory).Context(); - return QueryItems(context); - } - catch ( InvalidOperationException ) // or ObjectDisposedException - { - var context = new InjectServiceScope(_scopeFactory).Context(); - return QueryItems(context); - } - } - - /// - /// Hide Deleted items in folder - /// - /// list of items - /// list without deleted items - private static List HideDeletedFileFolderList(List queryItems){ - // temp feature to hide deleted items - var displayItems = new List(); - foreach (var item in queryItems) - { - if (item.Tags != null && !item.Tags.Contains(TrashKeyword.TrashKeywordString)) - { - displayItems.Add(item); - } - } - return displayItems; - // temp feature to hide deleted items - } - - private List QueryGetNextPrevInFolder( - string parentFolderPath) - { - List LocalQuery(ApplicationDbContext context) - { - // sort by alphabet - return context.FileIndex.Where( - p => p.ParentDirectory == parentFolderPath) - .OrderBy(p => p.FileName).AsEnumerable() - .GroupBy(i => i.FilePath).Select(g => g.First()).ToList(); - } - - try - { - return LocalQuery(_context); - } - catch ( MySqlProtocolException ) - { - var context = new InjectServiceScope(_scopeFactory).Context(); - return LocalQuery(context); - } - catch ( ObjectDisposedException ) - { - var context = new InjectServiceScope(_scopeFactory).Context(); - return LocalQuery(context); - } - } - - /// - /// Show previous en next items in the folder view. - /// There is equivalent class for prev next in the display view - /// - /// subPath style - /// relative object - public RelativeObjects GetNextPrevInFolder(string currentFolder) - { - if ( currentFolder != "/" ) - { - PathHelper.RemoveLatestSlash(currentFolder); - } - - // We use breadcrumbs to get the parent folder - var parentFolderPath = FilenamesHelper.GetParentPath(currentFolder); - - var itemsInSubFolder = QueryGetNextPrevInFolder(parentFolderPath); - - var photoIndexOfSubFolder = itemsInSubFolder.FindIndex(p => p.FilePath == currentFolder); - - var relativeObject = new RelativeObjects(); - if (photoIndexOfSubFolder != itemsInSubFolder.Count - 1 && currentFolder != "/") - { - // currentFolder != "/" >= on the home folder you will automatically go to a subfolder - relativeObject.NextFilePath = itemsInSubFolder[photoIndexOfSubFolder + 1].FilePath!; - relativeObject.NextHash = itemsInSubFolder[photoIndexOfSubFolder + 1].FileHash!; - } - - if (photoIndexOfSubFolder >= 1) - { - relativeObject.PrevFilePath = itemsInSubFolder[photoIndexOfSubFolder - 1].FilePath!; - relativeObject.PrevHash = itemsInSubFolder[photoIndexOfSubFolder - 1].FileHash!; - } - - return relativeObject; - } - } + public partial class Query // For folder displays only + { + + /// + /// Query all FileIndexItems with the type folder + /// + /// List of all folders in database, including content + public List GetAllFolders() + { + try + { + return _context.FileIndex.Where(p => p.IsDirectory == true).ToList(); + } + catch ( ObjectDisposedException ) + { + var context = new InjectServiceScope(_scopeFactory).Context(); + return context.FileIndex.Where(p => p.IsDirectory == true).ToList(); + } + } + + + // Class for displaying folder content + // This is the query part + public IEnumerable DisplayFileFolders( + string subPath = "/", + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true) + { + if ( subPath != "/" ) PathHelper.RemoveLatestSlash(subPath); + + var fileIndexItems = CacheQueryDisplayFileFolders(subPath); + + return DisplayFileFolders(fileIndexItems, + colorClassActiveList, + enableCollections, + hideDeleted); + } + + // Display File folder displays content of the folder + // without any query in this method + public IEnumerable DisplayFileFolders( + List fileIndexItems, + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true) + { + colorClassActiveList ??= new List(); + + // Hide meta files in list + fileIndexItems = fileIndexItems.Where(p => + p.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp && + p.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json).ToList(); + + if ( colorClassActiveList.Count != 0 ) + { + fileIndexItems = fileIndexItems.Where(p => colorClassActiveList.Contains(p.ColorClass)).ToList(); + } + + if ( fileIndexItems.Count == 0 ) + { + return new List(); + } + + if ( enableCollections ) + { + // Query Collections + fileIndexItems = StackCollections(fileIndexItems); + } + + return hideDeleted ? HideDeletedFileFolderList(fileIndexItems) : fileIndexItems; + } + + public Tuple> CacheGetParentFolder(string subPath) + { + var fallbackResult = new Tuple>(false, + new List()); + if ( _cache == null || _appSettings?.AddMemoryCache == false ) + return fallbackResult; + + // Return values from IMemoryCache + var queryCacheName = CachingDbName(nameof(FileIndexItem), + subPath); + + if ( !_cache.TryGetValue(queryCacheName, + out var objectFileFolders) ) return fallbackResult; + + var result = objectFileFolders as List ?? + new List(); + return new Tuple>(true, result); + } + + private List CacheQueryDisplayFileFolders(string subPath) + { + // The CLI programs uses no cache + if ( _cache == null || _appSettings.AddMemoryCache == false ) return QueryDisplayFileFolders(subPath); + + var (isSuccess, objectFileFolders) = CacheGetParentFolder(subPath); + + if ( isSuccess ) return objectFileFolders; + + objectFileFolders = QueryDisplayFileFolders(subPath); + + AddCacheParentItem(subPath, objectFileFolders); + return objectFileFolders; + } + + internal List QueryDisplayFileFolders(string subPath = "/") + { + List QueryItems(ApplicationDbContext context) + { + var queryItems = context.FileIndex + .TagWith("QueryDisplayFileFolders") + .Where(p => p.ParentDirectory == subPath && p.FileName != "/") + .OrderBy(p => p.FileName).AsEnumerable() + // remove duplicates from list + .GroupBy(t => t.FileName).Select(g => g.First()); + return queryItems.OrderBy(p => p.FileName, StringComparer.InvariantCulture).ToList(); + } + + try + { + return QueryItems(_context); + } + catch ( NotSupportedException ) + { + // System.NotSupportedException: The ReadAsync method cannot be called when another read operation is pending. + var context = new InjectServiceScope(_scopeFactory).Context(); + return QueryItems(context); + } + catch ( InvalidOperationException ) // or ObjectDisposedException + { + var context = new InjectServiceScope(_scopeFactory).Context(); + return QueryItems(context); + } + } + + /// + /// Hide Deleted items in folder + /// + /// list of items + /// list without deleted items + private static List HideDeletedFileFolderList(List queryItems) + { + // temp feature to hide deleted items + var displayItems = new List(); + foreach ( var item in queryItems ) + { + if ( item.Tags != null && !item.Tags.Contains(TrashKeyword.TrashKeywordString) ) + { + displayItems.Add(item); + } + } + return displayItems; + // temp feature to hide deleted items + } + + private List QueryGetNextPrevInFolder( + string parentFolderPath) + { + List LocalQuery(ApplicationDbContext context) + { + // sort by alphabet + return context.FileIndex.Where( + p => p.ParentDirectory == parentFolderPath) + .OrderBy(p => p.FileName).AsEnumerable() + .GroupBy(i => i.FilePath).Select(g => g.First()).ToList(); + } + + try + { + return LocalQuery(_context); + } + catch ( MySqlProtocolException ) + { + var context = new InjectServiceScope(_scopeFactory).Context(); + return LocalQuery(context); + } + catch ( ObjectDisposedException ) + { + var context = new InjectServiceScope(_scopeFactory).Context(); + return LocalQuery(context); + } + } + + /// + /// Show previous en next items in the folder view. + /// There is equivalent class for prev next in the display view + /// + /// subPath style + /// relative object + public RelativeObjects GetNextPrevInFolder(string currentFolder) + { + if ( currentFolder != "/" ) + { + PathHelper.RemoveLatestSlash(currentFolder); + } + + // We use breadcrumbs to get the parent folder + var parentFolderPath = FilenamesHelper.GetParentPath(currentFolder); + + var itemsInSubFolder = QueryGetNextPrevInFolder(parentFolderPath); + + var photoIndexOfSubFolder = itemsInSubFolder.FindIndex(p => p.FilePath == currentFolder); + + var relativeObject = new RelativeObjects(); + if ( photoIndexOfSubFolder != itemsInSubFolder.Count - 1 && currentFolder != "/" ) + { + // currentFolder != "/" >= on the home folder you will automatically go to a subfolder + relativeObject.NextFilePath = itemsInSubFolder[photoIndexOfSubFolder + 1].FilePath!; + relativeObject.NextHash = itemsInSubFolder[photoIndexOfSubFolder + 1].FileHash!; + } + + if ( photoIndexOfSubFolder >= 1 ) + { + relativeObject.PrevFilePath = itemsInSubFolder[photoIndexOfSubFolder - 1].FilePath!; + relativeObject.PrevHash = itemsInSubFolder[photoIndexOfSubFolder - 1].FileHash!; + } + + return relativeObject; + } + } } diff --git a/starsky/starsky.foundation.database/Query/QueryGetAllFiles.cs b/starsky/starsky.foundation.database/Query/QueryGetAllFiles.cs index 709f8347e3..c5b0f0381b 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetAllFiles.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetAllFiles.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -15,7 +14,7 @@ namespace starsky.foundation.database.Query // QueryGetAllFiles public partial class Query { - + /// /// Get a list of all files inside an folder (NOT recursive) /// But this uses a database as source @@ -26,29 +25,29 @@ public List GetAllFiles(string subPath) { try { - return GetAllFilesQuery(_context,new List{subPath}).ToList()!; + return GetAllFilesQuery(_context, new List { subPath }).ToList()!; } catch ( ObjectDisposedException ) { - return GetAllFilesQuery(new InjectServiceScope(_scopeFactory).Context(),new List{subPath}).ToList()!; + return GetAllFilesQuery(new InjectServiceScope(_scopeFactory).Context(), new List { subPath }).ToList()!; } } - - internal static List FormatOk(IReadOnlyCollection? input, + + internal static List FormatOk(IReadOnlyCollection? input, FileIndexItem.ExifStatus fromStatus = FileIndexItem.ExifStatus.Default) { if ( input == null ) return new List(); return input.Where(p => p != null).Select(p => { // status check for some referenced based code - if (p!.Status == fromStatus ) + if ( p!.Status == fromStatus ) { p.Status = FileIndexItem.ExifStatus.Ok; } return p; }).ToList(); } - + /// /// Get a list of all files inside an folder (NOT recursive) /// But this uses a database as source @@ -57,7 +56,7 @@ internal static List FormatOk(IReadOnlyCollection /// list of FileIndex-objects public async Task> GetAllFilesAsync(string subPath) { - return await GetAllFilesAsync(new List {subPath}); + return await GetAllFilesAsync(new List { subPath }); } /// @@ -75,17 +74,17 @@ public async Task> GetAllFilesAsync(List filePaths, .ToListAsync()); } // InvalidOperationException can also be disposed - catch (InvalidOperationException invalidOperationException) + catch ( InvalidOperationException invalidOperationException ) { _logger.LogInformation($"[GetAllFilesAsync] catch-ed and retry after {timeout}", invalidOperationException); await Task.Delay(timeout); - return FormatOk(await GetAllFilesQuery(new InjectServiceScope(_scopeFactory).Context(),filePaths).ToListAsync()); + return FormatOk(await GetAllFilesQuery(new InjectServiceScope(_scopeFactory).Context(), filePaths).ToListAsync()); } } private static IOrderedQueryable GetAllFilesQuery(ApplicationDbContext context, List filePathList) { - var predicates = new List>>(); + var predicates = new List>>(); // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach ( var filePath in filePathList ) @@ -94,9 +93,9 @@ public async Task> GetAllFilesAsync(List filePaths, if ( filePath == "/" ) subPath = "/"; predicates.Add(p => p.IsDirectory == false && p.ParentDirectory == subPath); } - + var predicate = PredicateBuilder.OrLoop(predicates); - + return context.FileIndex.Where(predicate).OrderBy(r => r.FileName); } diff --git a/starsky/starsky.foundation.database/Query/QueryGetAllObjects.cs b/starsky/starsky.foundation.database/Query/QueryGetAllObjects.cs index 0f5d41f0cf..3ea2059c8b 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetAllObjects.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetAllObjects.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -24,7 +23,7 @@ public partial class Query /// public async Task> GetAllObjectsAsync(string subPath) { - return await GetAllObjectsAsync(new List {subPath}); + return await GetAllObjectsAsync(new List { subPath }); } /// @@ -45,15 +44,15 @@ async Task> LocalGetAllObjectsAsync() var dbContext = new InjectServiceScope(_scopeFactory).Context(); var result = FormatOk( - (await GetAllObjectsQuery(dbContext, filePaths)?.ToListAsync()!)!); + ( await GetAllObjectsQuery(dbContext, filePaths)?.ToListAsync()! )!); await dbContext.DisposeAsync(); return result; } try { - return FormatOk((await GetAllObjectsQuery(_context, filePaths)! - .ToListAsync())!); + return FormatOk(( await GetAllObjectsQuery(_context, filePaths)! + .ToListAsync() )!); } catch ( ObjectDisposedException ) { @@ -64,14 +63,14 @@ async Task> LocalGetAllObjectsAsync() // System.InvalidOperationException: ExecuteReader can only be called when the connection is open. return await LocalGetAllObjectsAsync(); } - catch ( MySqlConnector.MySqlException exception) + catch ( MySqlConnector.MySqlException exception ) { _logger.LogError(exception, $"catch-ed mysql error [next delay {fallbackDelay}]"); await Task.Delay(fallbackDelay); return await LocalGetAllObjectsAsync(); } } - + /// /// QueryFolder Async without cache /// @@ -80,7 +79,7 @@ async Task> LocalGetAllObjectsAsync() /// private static IOrderedQueryable? GetAllObjectsQuery(ApplicationDbContext context, List filePathList) { - var predicates = new List>>(); + var predicates = new List>>(); // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach ( var filePath in filePathList ) @@ -89,9 +88,9 @@ async Task> LocalGetAllObjectsAsync() if ( filePath == "/" ) subPath = "/"; predicates.Add(p => p.ParentDirectory == subPath); } - + var predicate = PredicateBuilder.OrLoop(predicates); - + return context.FileIndex .TagWith("GetAllObjectsQuery") .Where(predicate).OrderBy(r => r.FileName); diff --git a/starsky/starsky.foundation.database/Query/QueryGetAllRecursive.cs b/starsky/starsky.foundation.database/Query/QueryGetAllRecursive.cs index a337add7f7..675344d314 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetAllRecursive.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetAllRecursive.cs @@ -24,9 +24,9 @@ public partial class Query /// results public async Task> GetAllRecursiveAsync(string subPath = "/") { - return await GetAllRecursiveAsync(new List {subPath}); + return await GetAllRecursiveAsync(new List { subPath }); } - + /// /// Includes sub Items /// @@ -36,7 +36,7 @@ public async Task> GetAllRecursiveAsync(List filePat { async Task> LocalQuery(ApplicationDbContext context) { - var predicates = new List>>(); + var predicates = new List>>(); // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach ( var filePath in filePathList ) @@ -44,9 +44,9 @@ async Task> LocalQuery(ApplicationDbContext context) var subPath = PathHelper.RemoveLatestSlash(filePath); predicates.Add(p => p.ParentDirectory!.StartsWith(subPath)); } - + var predicate = PredicateBuilder.OrLoop(predicates); - + return await context.FileIndex.Where(predicate).OrderBy(r => r.FilePath).ToListAsync(); } @@ -64,7 +64,7 @@ async Task> LocalQuery(ApplicationDbContext context) return await LocalQuery(new InjectServiceScope(_scopeFactory) .Context()); } - catch ( MySqlException exception) + catch ( MySqlException exception ) { // https://github.com/qdraw/starsky/issues/1243 if ( exception.Message.Contains("Timeout") ) diff --git a/starsky/starsky.foundation.database/Query/QueryGetFoldersAsync.cs b/starsky/starsky.foundation.database/Query/QueryGetFoldersAsync.cs index ca8ffffa18..9ae8f3ea28 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetFoldersAsync.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetFoldersAsync.cs @@ -19,7 +19,7 @@ public partial class Query : IQuery { public async Task> GetFoldersAsync(string subPath) { - return await GetFoldersAsync(new List {subPath}); + return await GetFoldersAsync(new List { subPath }); } public async Task> GetFoldersAsync(List filePaths) @@ -30,13 +30,13 @@ public async Task> GetFoldersAsync(List filePaths) } catch ( ObjectDisposedException ) { - return FormatOk(await GetAllFoldersQuery(new InjectServiceScope(_scopeFactory).Context(),filePaths).ToListAsync()); + return FormatOk(await GetAllFoldersQuery(new InjectServiceScope(_scopeFactory).Context(), filePaths).ToListAsync()); } } - + private static IOrderedQueryable GetAllFoldersQuery(ApplicationDbContext context, List filePathList) { - var predicates = new List>>(); + var predicates = new List>>(); // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach ( var filePath in filePathList ) @@ -45,9 +45,9 @@ private static IOrderedQueryable GetAllFoldersQuery(ApplicationDb if ( filePath == "/" ) subPath = "/"; predicates.Add(p => p.ParentDirectory == subPath && p.IsDirectory == true); } - + var predicate = PredicateBuilder.OrLoop(predicates); - + return context.FileIndex.Where(predicate).OrderBy(r => r.FileName); } } diff --git a/starsky/starsky.foundation.database/Query/QueryGetObjectsByFileHashAsync.cs b/starsky/starsky.foundation.database/Query/QueryGetObjectsByFileHashAsync.cs index 6a94edd654..5738883e76 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetObjectsByFileHashAsync.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetObjectsByFileHashAsync.cs @@ -15,14 +15,14 @@ namespace starsky.foundation.database.Query /// public partial class Query : IQuery { - + public async Task> GetObjectsByFileHashAsync(List fileHashesList, int retryCount = 2) { if ( fileHashesList.Count == 0 ) { return new List(); } - + async Task> LocalQuery(ApplicationDbContext context) { var result = await context. @@ -32,8 +32,11 @@ async Task> LocalQuery(ApplicationDbContext context) { if ( result.Find(p => p.FileHash == fileHash) == null ) { - result.Add(new FileIndexItem(){FileHash = fileHash, - Status = FileIndexItem.ExifStatus.NotFoundNotInIndex}); + result.Add(new FileIndexItem() + { + FileHash = fileHash, + Status = FileIndexItem.ExifStatus.NotFoundNotInIndex + }); } } return FormatOk(result); @@ -43,7 +46,7 @@ async Task> LocalDefaultQuery() { return await LocalQuery(_context); } - + return await RetryHelper.DoAsync(LocalDefaultQuery, TimeSpan.FromSeconds(3), retryCount); } } diff --git a/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathAsync.cs b/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathAsync.cs index 341cbdfb36..fe8b58e955 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathAsync.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathAsync.cs @@ -18,7 +18,7 @@ public partial class Query : IQuery { public async Task> GetObjectsByFilePathAsync(string inputFilePath, bool collections) { - return await GetObjectsByFilePathAsync(new List{inputFilePath}, collections); + return await GetObjectsByFilePathAsync(new List { inputFilePath }, collections); } /// @@ -31,10 +31,10 @@ public async Task> GetObjectsByFilePathAsync(List in { var resultFileIndexItemsList = new List(); var toQueryPaths = new List(); - foreach ( var path in inputFilePaths) + foreach ( var path in inputFilePaths ) { var parentPath = FilenamesHelper.GetParentPath(path); - + var (success, cachedResult) = CacheGetParentFolder(parentPath); List? item = null; @@ -43,13 +43,13 @@ public async Task> GetObjectsByFilePathAsync(List in case false: if ( !success ) break; item = cachedResult.Where(p => - p.ParentDirectory == parentPath && + p.ParentDirectory == parentPath && p.FileName == FilenamesHelper.GetFileName(path)).ToList(); break; case true: if ( !success ) break; item = cachedResult.Where(p => - p.ParentDirectory == parentPath && + p.ParentDirectory == parentPath && p.FileCollectionName == FilenamesHelper.GetFileNameWithoutExtension(path)).ToList(); break; } @@ -66,7 +66,7 @@ public async Task> GetObjectsByFilePathAsync(List in resultFileIndexItemsList.AddRange(fileIndexItemsList); return resultFileIndexItemsList; } - + /// /// Switch between collections and non-collections /// @@ -79,15 +79,15 @@ internal async Task> GetObjectsByFilePathQuery(string[] inpu { return new List(); } - + if ( collections ) { return await GetObjectsByFilePathCollectionQueryAsync(inputFilePaths.ToList()); } return await GetObjectsByFilePathQueryAsync(inputFilePaths.ToList()); } - - + + /// /// Skip cache /// @@ -106,9 +106,9 @@ async Task> LocalQuery(ApplicationDbContext context) { return await LocalQuery(_context); } - catch ( NullReferenceException ex1) + catch ( NullReferenceException ex1 ) { - _logger.LogInformation($"catch-ed null ref exception: {string.Join( ",", filePathList.ToArray() )} {ex1.StackTrace}", ex1); + _logger.LogInformation($"catch-ed null ref exception: {string.Join(",", filePathList.ToArray())} {ex1.StackTrace}", ex1); await Task.Delay(10); // System.NullReferenceException: Object reference not set to an instance of an object. // at MySql.Data.MySqlClient.MySqlDataReader.ActivateResultSet() @@ -123,9 +123,9 @@ async Task> LocalQuery(ApplicationDbContext context) return await LocalQuery( new InjectServiceScope(_scopeFactory).Context()); } - catch ( NullReferenceException ex2) + catch ( NullReferenceException ex2 ) { - _logger.LogInformation($"catch-ed null ref exception 2: {string.Join( ",", filePathList.ToArray() )} {ex2.StackTrace}", ex2); + _logger.LogInformation($"catch-ed null ref exception 2: {string.Join(",", filePathList.ToArray())} {ex2.StackTrace}", ex2); throw; } } diff --git a/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathCollectionAsync.cs b/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathCollectionAsync.cs index 67e153bb78..0cd2ef366e 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathCollectionAsync.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetObjectsByFilePathCollectionAsync.cs @@ -24,7 +24,7 @@ public partial class Query : IQuery /// internal async Task> GetObjectsByFilePathCollectionAsync(string subPath) { - return await GetObjectsByFilePathCollectionQueryAsync(new List {subPath}); + return await GetObjectsByFilePathCollectionQueryAsync(new List { subPath }); } internal async Task> GetObjectsByFilePathCollectionQueryAsync(List filePathList) @@ -36,14 +36,14 @@ internal async Task> GetObjectsByFilePathCollectionQueryAsyn catch ( ObjectDisposedException ) { return FormatOk(await GetObjectsByFilePathCollectionQuery( - new InjectServiceScope(_scopeFactory).Context(),filePathList).ToListAsync()); + new InjectServiceScope(_scopeFactory).Context(), filePathList).ToListAsync()); } } - - private static IOrderedQueryable GetObjectsByFilePathCollectionQuery(ApplicationDbContext context, + + private static IOrderedQueryable GetObjectsByFilePathCollectionQuery(ApplicationDbContext context, IEnumerable filePathList) { - var predicates = new List>>(); + var predicates = new List>>(); // ReSharper disable once LoopCanBeConvertedToQuery foreach ( var path in filePathList ) @@ -51,15 +51,15 @@ private static IOrderedQueryable GetObjectsByFilePathCollectionQu var fileNameWithoutExtension = FilenamesHelper.GetFileNameWithoutExtension(path); if ( string.IsNullOrEmpty(FilenamesHelper.GetFileExtensionWithoutDot(path)) ) { - predicates.Add(p => p.ParentDirectory == FilenamesHelper.GetParentPath(path) - && p.FileName == fileNameWithoutExtension); + predicates.Add(p => p.ParentDirectory == FilenamesHelper.GetParentPath(path) + && p.FileName == fileNameWithoutExtension); continue; } - predicates.Add(p => p.ParentDirectory == FilenamesHelper.GetParentPath(path) - && p.FileName != null - && p.FileName.StartsWith(fileNameWithoutExtension + ".") ); + predicates.Add(p => p.ParentDirectory == FilenamesHelper.GetParentPath(path) + && p.FileName != null + && p.FileName.StartsWith(fileNameWithoutExtension + ".")); } - + var predicate = PredicateBuilder.OrLoop(predicates); return context.FileIndex.Where(predicate).OrderBy(r => r.FileName); diff --git a/starsky/starsky.foundation.database/Query/QueryInvokeClone.cs b/starsky/starsky.foundation.database/Query/QueryInvokeClone.cs index 27b8852fb5..8589b7960a 100644 --- a/starsky/starsky.foundation.database/Query/QueryInvokeClone.cs +++ b/starsky/starsky.foundation.database/Query/QueryInvokeClone.cs @@ -7,7 +7,7 @@ public partial class Query // For invoke & clone only { public IQuery Clone(ApplicationDbContext applicationDbContext) { - var query = (IQuery) MemberwiseClone(); + var query = ( IQuery )MemberwiseClone(); query.Invoke(applicationDbContext); return query; } diff --git a/starsky/starsky.foundation.database/Query/QueryRemoveItemAsync.cs b/starsky/starsky.foundation.database/Query/QueryRemoveItemAsync.cs index 436e1d7de9..e7a4b6a7ab 100644 --- a/starsky/starsky.foundation.database/Query/QueryRemoveItemAsync.cs +++ b/starsky/starsky.foundation.database/Query/QueryRemoveItemAsync.cs @@ -9,7 +9,7 @@ namespace starsky.foundation.database.Query { - + /// /// QueryRemoveItemAsync /// @@ -27,23 +27,23 @@ async Task LocalRemoveDefaultQuery() await LocalRemoveQuery(new InjectServiceScope(_scopeFactory).Context()); return true; } - + async Task LocalRemoveQuery(ApplicationDbContext context) { // Detach first https://stackoverflow.com/a/42475617 var local = context.Set() .Local .FirstOrDefault(entry => entry.Id.Equals(updateStatusContent.Id)); - if (local != null) + if ( local != null ) { context.Entry(local).State = EntityState.Detached; } - + // keep conditional marker for test context.FileIndex?.Remove(updateStatusContent); await context.SaveChangesAsync(); } - + try { await LocalRemoveQuery(_context); @@ -62,20 +62,20 @@ await RetryHelper.DoAsync(LocalRemoveDefaultQuery, { await LocalRemoveDefaultQuery(); } - catch ( DbUpdateConcurrencyException e) + catch ( DbUpdateConcurrencyException e ) { - _logger.LogInformation(e,"[RemoveItemAsync] catch-ed " + - "DbUpdateConcurrencyException (do nothing)"); + _logger.LogInformation(e, "[RemoveItemAsync] catch-ed " + + "DbUpdateConcurrencyException (do nothing)"); } - + // remove parent directory cache RemoveCacheItem(updateStatusContent); - + // remove getFileHash Cache ResetItemByHash(updateStatusContent.FileHash); return updateStatusContent; } - + /// /// Remove a new item from the database (NOT from the file system) /// @@ -88,7 +88,7 @@ async Task LocalRemoveDefaultQuery() await LocalRemoveQuery(new InjectServiceScope(_scopeFactory).Context()); return true; } - + async Task LocalRemoveQuery(ApplicationDbContext context) { // Detach first https://stackoverflow.com/a/42475617 @@ -97,7 +97,7 @@ async Task LocalRemoveQuery(ApplicationDbContext context) var local = context.Set() .Local .FirstOrDefault(entry => entry.Id.Equals(updateStatusContent.Id)); - if (local != null) + if ( local != null ) { context.Entry(local).State = EntityState.Detached; } @@ -106,7 +106,7 @@ async Task LocalRemoveQuery(ApplicationDbContext context) context.FileIndex?.RemoveRange(updateStatusContentList); await context.SaveChangesAsync(); } - + try { await LocalRemoveQuery(_context); @@ -125,15 +125,15 @@ await RetryHelper.DoAsync(LocalRemoveDefaultQuery, { await LocalRemoveDefaultQuery(); } - catch ( DbUpdateConcurrencyException e) + catch ( DbUpdateConcurrencyException e ) { - _logger.LogInformation(e,"[RemoveItemAsync:List] catch-ed " + - "DbUpdateConcurrencyException (do nothing)"); + _logger.LogInformation(e, "[RemoveItemAsync:List] catch-ed " + + "DbUpdateConcurrencyException (do nothing)"); } - + // remove parent directory cache RemoveCacheItem(updateStatusContentList); - + // remove getFileHash Cache foreach ( var updateStatusContent in updateStatusContentList ) { diff --git a/starsky/starsky.foundation.database/Query/QuerySingleItem.cs b/starsky/starsky.foundation.database/Query/QuerySingleItem.cs index 538957c8f9..964475a218 100644 --- a/starsky/starsky.foundation.database/Query/QuerySingleItem.cs +++ b/starsky/starsky.foundation.database/Query/QuerySingleItem.cs @@ -1,181 +1,193 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using starsky.foundation.database.Helpers; using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; -namespace starsky.foundation.database.Query +namespace starsky.foundation.database.Query; + +/// +/// QuerySingleItem +/// +public partial class Query { + // For displaying single photo's + // Display feature only?! + // input: Name of item by db style path + // With Caching feature :) + + /// + /// SingleItemPath do the query for singleItem + return detailView object + /// + /// + /// list of colorClasses to show, default show all + /// enable collections feature > default true + /// do not show deleted files > default true + /// how to sort + /// view object to show on the page (if not exists its null) + public DetailView? SingleItem( + string singleItemDbPath, + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true, + SortType? sort = SortType.FileName) + { + if ( string.IsNullOrWhiteSpace(singleItemDbPath) ) + { + return null; + } + + var parentFolder = FilenamesHelper.GetParentPath(singleItemDbPath); + var fileIndexItemsList = DisplayFileFolders( + parentFolder, null, false, false).ToList(); + + return SingleItem( + fileIndexItemsList, + singleItemDbPath, + colorClassActiveList, + enableCollections, + hideDeleted, + sort); + } + /// - /// QuerySingleItem + /// fileIndexItemsList, Create an detailView object /// - public partial class Query - { - // For displaying single photo's - // Display feature only?! - // input: Name of item by db style path - // With Caching feature :) - - /// - /// SingleItemPath do the query for singleitem + return detailview object - /// - /// - /// list of colorclasses to show, default show all - /// enable collections feature > default true - /// do not show deleted files > default true - /// how to sort - /// view object to show on the page - public DetailView? SingleItem( - string singleItemDbPath, - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true, - SortType? sort = SortType.FileName) - { - if (string.IsNullOrWhiteSpace(singleItemDbPath) ) return null; - var parentFolder = FilenamesHelper.GetParentPath(singleItemDbPath); - var fileIndexItemsList = DisplayFileFolders( - parentFolder,null,false,false).ToList(); - - return SingleItem( - fileIndexItemsList, - singleItemDbPath, - colorClassActiveList, - enableCollections, - hideDeleted, - sort); - } - - /// - /// fileIndexItemsList, Create an detailView object - /// - /// list of fileIndexItems - /// database style path - /// list of colorClasses to show, default show all - /// enable collections feature > default true - /// do not show deleted files > default true - /// how to sort - /// view object to show on the page - public DetailView? SingleItem( - List fileIndexItemsList, - string singleItemDbPath, - List? colorClassActiveList = null, - bool enableCollections = true, - bool hideDeleted = true, - SortType? sort = SortType.FileName) - { - // reject empty requests - if (string.IsNullOrWhiteSpace(singleItemDbPath) ) return null; - var parentFolder = FilenamesHelper.GetParentPath(singleItemDbPath); - - // RemoveLatestSlash is for '/' folder - var fileName = singleItemDbPath.Replace( - PathHelper.RemoveLatestSlash(parentFolder) + "/", string.Empty); - - // Home has no parent, so return a value - var objectByFilePath = GetObjectByFilePath("/"); - if ( fileName == string.Empty && parentFolder == "/" && objectByFilePath != null) - { - // This is for HOME only - return new DetailView - { - FileIndexItem = objectByFilePath, - RelativeObjects = new RelativeObjects(), - Breadcrumb = new List{"/"}, - ColorClassActiveList = colorClassActiveList ?? new List(), - IsDirectory = true, - SubPath = "/", - Collections = enableCollections - }; - } - - var currentFileIndexItem = fileIndexItemsList.Find(p => p.FileName == fileName); - - // Could be not found or not in directory cache - if ( currentFileIndexItem == null ) - { - // retry - currentFileIndexItem = GetObjectByFilePath(singleItemDbPath); - if ( currentFileIndexItem == null ) return null; - AddCacheItem(currentFileIndexItem); - } - - // To know when a file is deleted - if ( currentFileIndexItem.Tags != null && currentFileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) - { - currentFileIndexItem.Status = FileIndexItem.ExifStatus.Deleted; - } - - if(currentFileIndexItem.IsDirectory == true) { - currentFileIndexItem.CollectionPaths = new List{singleItemDbPath}; - return new DetailView - { - IsDirectory = true, - SubPath = singleItemDbPath, - FileIndexItem = currentFileIndexItem, - Collections = enableCollections, - }; - } - - if ( currentFileIndexItem.Tags != null && - currentFileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) - { - hideDeleted = false; - } - - var fileIndexItemsForPrevNextList = DisplayFileFolders( - parentFolder,colorClassActiveList,enableCollections,hideDeleted).ToList(); - - var itemResult = new DetailView - { - FileIndexItem = currentFileIndexItem, - RelativeObjects = GetNextPrevInSubFolder(currentFileIndexItem,fileIndexItemsForPrevNextList, sort ?? SortType.FileName), - Breadcrumb = Breadcrumbs.BreadcrumbHelper(singleItemDbPath), - ColorClassActiveList = colorClassActiveList ?? new List(), - IsDirectory = false, - SubPath = singleItemDbPath, - Collections = enableCollections - }; - - // First item is current item - var collectionPaths = new List {singleItemDbPath}; - collectionPaths.AddRange(fileIndexItemsList - .Where(p => p.FileCollectionName == currentFileIndexItem.FileCollectionName) - .Select(p => p.FilePath)!); - - var collectionPathsHashSet = new HashSet(collectionPaths); - itemResult.FileIndexItem.CollectionPaths = collectionPathsHashSet.ToList(); - - return itemResult; - } - - private static RelativeObjects GetNextPrevInSubFolder( - FileIndexItem? currentFileIndexItem, - List fileIndexItemsList, SortType sortType) - { - // Check if this is item is not !deleted! yet - if (currentFileIndexItem == null) return new RelativeObjects(); - - fileIndexItemsList = SortHelper.Helper(fileIndexItemsList, sortType).ToList(); - - var currentIndex = fileIndexItemsList.FindIndex(p => p.FilePath == currentFileIndexItem.FilePath); - var relativeObject = new RelativeObjects(); - - if (currentIndex != fileIndexItemsList.Count - 1) - { - relativeObject.NextFilePath = fileIndexItemsList[currentIndex + 1].FilePath!; - relativeObject.NextHash = fileIndexItemsList[currentIndex + 1].FileHash!; - } - - if ( currentIndex >= 1 ) - { - relativeObject.PrevFilePath = fileIndexItemsList[currentIndex - 1].FilePath!; - relativeObject.PrevHash = fileIndexItemsList[currentIndex - 1].FileHash!; - } - - return relativeObject; - } - - - } + /// list of fileIndexItems + /// database style path + /// list of colorClasses to show, default show all + /// enable collections feature > default true + /// do not show deleted files > default true + /// how to sort + /// view object to show on the page (if not exists its null) + public DetailView? SingleItem( + List fileIndexItemsList, + string singleItemDbPath, + List? colorClassActiveList = null, + bool enableCollections = true, + bool hideDeleted = true, + SortType? sort = SortType.FileName) + { + // reject empty requests + if ( string.IsNullOrWhiteSpace(singleItemDbPath) ) return null; + var parentFolder = FilenamesHelper.GetParentPath(singleItemDbPath); + + // RemoveLatestSlash is for '/' folder + var fileName = singleItemDbPath.Replace( + PathHelper.RemoveLatestSlash(parentFolder) + "/", string.Empty); + + // Home has no parent, so return a value + var objectByFilePath = GetObjectByFilePath("/"); + if ( fileName == string.Empty && parentFolder == "/" && objectByFilePath != null ) + { + // This is for HOME only + return new DetailView + { + FileIndexItem = objectByFilePath, + RelativeObjects = new RelativeObjects(), + Breadcrumb = new List { "/" }, + ColorClassActiveList = + colorClassActiveList ?? new List(), + IsDirectory = true, + SubPath = "/", + Collections = enableCollections + }; + } + + var currentFileIndexItem = fileIndexItemsList.Find(p => p.FileName == fileName); + + // Could be not found or not in directory cache + if ( currentFileIndexItem == null ) + { + // retry + currentFileIndexItem = GetObjectByFilePath(singleItemDbPath); + if ( currentFileIndexItem == null ) + { + return null; + } + + AddCacheItem(currentFileIndexItem); + } + + // To know when a file is deleted + if ( currentFileIndexItem.Tags != null && + currentFileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) + { + currentFileIndexItem.Status = FileIndexItem.ExifStatus.Deleted; + } + + if ( currentFileIndexItem.IsDirectory == true ) + { + currentFileIndexItem.CollectionPaths = new List { singleItemDbPath }; + return new DetailView + { + IsDirectory = true, + SubPath = singleItemDbPath, + FileIndexItem = currentFileIndexItem, + Collections = enableCollections, + }; + } + + if ( currentFileIndexItem.Tags != null && + currentFileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) + { + hideDeleted = false; + } + + var fileIndexItemsForPrevNextList = DisplayFileFolders( + parentFolder, colorClassActiveList, enableCollections, hideDeleted).ToList(); + + var itemResult = new DetailView + { + FileIndexItem = currentFileIndexItem, + RelativeObjects = + GetNextPrevInSubFolder(currentFileIndexItem, fileIndexItemsForPrevNextList, + sort ?? SortType.FileName), + Breadcrumb = Breadcrumbs.BreadcrumbHelper(singleItemDbPath), + ColorClassActiveList = + colorClassActiveList ?? new List(), + IsDirectory = false, + SubPath = singleItemDbPath, + Collections = enableCollections + }; + + // First item is current item + var collectionPaths = new List { singleItemDbPath }; + collectionPaths.AddRange(fileIndexItemsList + .Where(p => p.FileCollectionName == currentFileIndexItem.FileCollectionName) + .Select(p => p.FilePath)!); + + var collectionPathsHashSet = new HashSet(collectionPaths); + itemResult.FileIndexItem.CollectionPaths = collectionPathsHashSet.ToList(); + + return itemResult; + } + + private static RelativeObjects GetNextPrevInSubFolder( + FileIndexItem? currentFileIndexItem, + List fileIndexItemsList, SortType sortType) + { + // Check if this is item is not !deleted! yet + if ( currentFileIndexItem == null ) return new RelativeObjects(); + + fileIndexItemsList = SortHelper.Helper(fileIndexItemsList, sortType).ToList(); + + var currentIndex = + fileIndexItemsList.FindIndex(p => p.FilePath == currentFileIndexItem.FilePath); + var relativeObject = new RelativeObjects(); + + if ( currentIndex != fileIndexItemsList.Count - 1 ) + { + relativeObject.NextFilePath = fileIndexItemsList[currentIndex + 1].FilePath!; + relativeObject.NextHash = fileIndexItemsList[currentIndex + 1].FileHash!; + } + + if ( currentIndex >= 1 ) + { + relativeObject.PrevFilePath = fileIndexItemsList[currentIndex - 1].FilePath!; + relativeObject.PrevHash = fileIndexItemsList[currentIndex - 1].FileHash!; + } + + return relativeObject; + } } diff --git a/starsky/starsky.foundation.database/Query/SolveConcurrency.cs b/starsky/starsky.foundation.database/Query/SolveConcurrency.cs index cff70f1f48..28e8caa175 100644 --- a/starsky/starsky.foundation.database/Query/SolveConcurrency.cs +++ b/starsky/starsky.foundation.database/Query/SolveConcurrency.cs @@ -10,15 +10,15 @@ public static class SolveConcurrency internal static void SolveConcurrencyExceptionLoop( IReadOnlyList concurrencyExceptionEntries) { - foreach (var entry in concurrencyExceptionEntries) + foreach ( var entry in concurrencyExceptionEntries ) { SolveConcurrencyException(entry.Entity, entry.CurrentValues, - entry.GetDatabaseValues(), entry.Metadata.Name, + entry.GetDatabaseValues(), entry.Metadata.Name, // former values from database entry.CurrentValues.SetValues); } } - + /// /// Delegate to abstract OriginalValues Setter /// @@ -35,16 +35,16 @@ internal static void SolveConcurrencyExceptionLoop( /// meta name /// entry item /// unknown how to fix - internal static void SolveConcurrencyException(object entryEntity, - PropertyValues proposedValues, PropertyValues? databaseValues, string entryMetadataName, + internal static void SolveConcurrencyException(object entryEntity, + PropertyValues proposedValues, PropertyValues? databaseValues, string entryMetadataName, OriginalValuesSetValuesDelegate entryOriginalValuesSetValues) { if ( !( entryEntity is FileIndexItem ) ) throw new NotSupportedException( "Don't know how to handle concurrency conflicts for " + entryMetadataName); - - foreach (var property in proposedValues.Properties) + + foreach ( var property in proposedValues.Properties ) { var proposedValue = proposedValues[property]; proposedValues[property] = proposedValue; @@ -57,6 +57,6 @@ internal static void SolveConcurrencyException(object entryEntity, } } } - + } diff --git a/starsky/starsky.foundation.database/Thumbnails/ThumbnailQuery.cs b/starsky/starsky.foundation.database/Thumbnails/ThumbnailQuery.cs index 1aef97195b..7d130f5381 100644 --- a/starsky/starsky.foundation.database/Thumbnails/ThumbnailQuery.cs +++ b/starsky/starsky.foundation.database/Thumbnails/ThumbnailQuery.cs @@ -28,10 +28,10 @@ public ThumbnailQuery(ApplicationDbContext context, IServiceScopeFactory? scopeF _scopeFactory = scopeFactory; _logger = logger; } - + public Task?> AddThumbnailRangeAsync(List thumbnailItems) { - if ( thumbnailItems.Exists(p => string.IsNullOrEmpty(p.FileHash) ) ) + if ( thumbnailItems.Exists(p => string.IsNullOrEmpty(p.FileHash)) ) { throw new ArgumentNullException(nameof(thumbnailItems), "[AddThumbnailRangeAsync] FileHash is null or empty"); } @@ -45,7 +45,7 @@ public ThumbnailQuery(ApplicationDbContext context, IServiceScopeFactory? scopeF return await AddThumbnailRangeInternalAsync(_context, thumbnailItems); } // InvalidOperationException can also be disposed - catch (InvalidOperationException) + catch ( InvalidOperationException ) { if ( _scopeFactory == null ) throw; return await AddThumbnailRangeInternalAsync(new InjectServiceScope(_scopeFactory).Context(), thumbnailItems); @@ -53,32 +53,32 @@ public ThumbnailQuery(ApplicationDbContext context, IServiceScopeFactory? scopeF } private static async Task?> AddThumbnailRangeInternalAsync( - ApplicationDbContext dbContext, + ApplicationDbContext dbContext, IReadOnlyCollection thumbnailItems) { if ( thumbnailItems.Count == 0 ) { return new List(); } - + var updateThumbnailNewItemsList = new List(); foreach ( var item in thumbnailItems - .Where(p => p.FileHash != null).DistinctBy(p => p.FileHash) ) + .Where(p => p.FileHash != null).DistinctBy(p => p.FileHash) ) { - updateThumbnailNewItemsList.Add(new ThumbnailItem(item.FileHash!,item.TinyMeta, item.Small, item.Large, item.ExtraLarge, item.Reasons)); + updateThumbnailNewItemsList.Add(new ThumbnailItem(item.FileHash!, item.TinyMeta, item.Small, item.Large, item.ExtraLarge, item.Reasons)); } - - var (newThumbnailItems, - alreadyExistingThumbnailItems, - equalThumbnailItems) = + + var (newThumbnailItems, + alreadyExistingThumbnailItems, + equalThumbnailItems) = await CheckForDuplicates(dbContext, updateThumbnailNewItemsList); - + if ( newThumbnailItems.Count != 0 ) { await dbContext.Thumbnails.AddRangeAsync(newThumbnailItems); await SaveChangesDuplicate(dbContext); } - + if ( alreadyExistingThumbnailItems.Count != 0 ) { dbContext.Thumbnails.UpdateRange(alreadyExistingThumbnailItems); @@ -86,17 +86,17 @@ public ThumbnailQuery(ApplicationDbContext context, IServiceScopeFactory? scopeF await dbContext.SaveChangesAsync(); await SaveChangesDuplicate(dbContext); } - + var allResults = alreadyExistingThumbnailItems .Concat(newThumbnailItems) .Concat(equalThumbnailItems) .ToList(); - + foreach ( var item in allResults ) { dbContext.Attach(item).State = EntityState.Detached; } - + return allResults; } @@ -125,7 +125,7 @@ public async Task> Get(string? fileHash = null) return await GetInternalAsync(_context, fileHash); } // InvalidOperationException can also be disposed - catch (InvalidOperationException) + catch ( InvalidOperationException ) { if ( _scopeFactory == null ) throw; return await GetInternalAsync(new InjectServiceScope(_scopeFactory).Context(), fileHash); @@ -137,7 +137,7 @@ private static async Task> GetInternalAsync( string? fileHash = null) { return fileHash == null ? await context - .Thumbnails.ToListAsync() : await context + .Thumbnails.ToListAsync() : await context .Thumbnails.Where(p => p.FileHash == fileHash) .ToListAsync(); } @@ -148,13 +148,13 @@ public async Task RemoveThumbnailsAsync(List deletedFileHashes) { return; } - + try { await RemoveThumbnailsInternalAsync(_context, deletedFileHashes); } // InvalidOperationException can also be disposed - catch (InvalidOperationException) + catch ( InvalidOperationException ) { if ( _scopeFactory == null ) throw; await RemoveThumbnailsInternalAsync(new InjectServiceScope(_scopeFactory).Context(), deletedFileHashes); @@ -169,7 +169,7 @@ internal static async Task RemoveThumbnailsInternalAsync( { return false; } - + foreach ( var fileNamesInChunk in deletedFileHashes.ChunkyEnumerable(100) ) { var thumbnailItems = await context.Thumbnails.Where(p => fileNamesInChunk.Contains(p.FileHash)).ToListAsync(); @@ -178,7 +178,7 @@ internal static async Task RemoveThumbnailsInternalAsync( } return true; } - + public async Task RenameAsync(string beforeFileHash, string newFileHash) { try @@ -186,12 +186,12 @@ public async Task RenameAsync(string beforeFileHash, string newFileHash) return await RenameInternalAsync(_context, beforeFileHash, newFileHash); } // InvalidOperationException can also be disposed - catch (InvalidOperationException) + catch ( InvalidOperationException ) { if ( _scopeFactory == null ) throw; return await RenameInternalAsync(new InjectServiceScope(_scopeFactory).Context(), beforeFileHash, newFileHash); } - catch (DbUpdateConcurrencyException concurrencyException) + catch ( DbUpdateConcurrencyException concurrencyException ) { _logger.LogInformation("[ThumbnailQuery] try to fix DbUpdateConcurrencyException", concurrencyException); SolveConcurrency.SolveConcurrencyExceptionLoop(concurrencyException.Entries); @@ -199,7 +199,7 @@ public async Task RenameAsync(string beforeFileHash, string newFileHash) { await _context.SaveChangesAsync(); } - catch ( DbUpdateConcurrencyException e) + catch ( DbUpdateConcurrencyException e ) { _logger.LogInformation(e, "[ThumbnailQuery] save failed after DbUpdateConcurrencyException"); return false; @@ -207,16 +207,16 @@ public async Task RenameAsync(string beforeFileHash, string newFileHash) return true; } } - + private static async Task RenameInternalAsync(ApplicationDbContext dbContext, string beforeFileHash, string newFileHash) { var beforeOrNewItems = await dbContext.Thumbnails.Where(p => p.FileHash == beforeFileHash || p.FileHash == newFileHash).ToListAsync(); - + var beforeItem = beforeOrNewItems.Find(p => p.FileHash == beforeFileHash); var newItem = beforeOrNewItems.Find(p => p.FileHash == newFileHash); - if ( beforeItem == null) return false; + if ( beforeItem == null ) return false; dbContext.Thumbnails.Remove(beforeItem); @@ -224,20 +224,20 @@ private static async Task RenameInternalAsync(ApplicationDbContext dbConte { dbContext.Thumbnails.Remove(newItem); } - - await dbContext.Thumbnails.AddRangeAsync(new ThumbnailItem(newFileHash, + + await dbContext.Thumbnails.AddRangeAsync(new ThumbnailItem(newFileHash, beforeItem.TinyMeta, beforeItem.Small, beforeItem.Large, beforeItem.ExtraLarge, beforeItem.Reasons)); - + await dbContext.SaveChangesAsync(); - + return true; } public async Task> UnprocessedGeneratedThumbnails() { - return await _context.Thumbnails.Where(p => ( p.ExtraLarge == null - || p.Large == null || p.Small == null) - && !string.IsNullOrEmpty(p.FileHash)).ToListAsync(); + return await _context.Thumbnails.Where(p => ( p.ExtraLarge == null + || p.Large == null || p.Small == null ) + && !string.IsNullOrEmpty(p.FileHash)).ToListAsync(); } public async Task UpdateAsync(ThumbnailItem item) @@ -247,7 +247,7 @@ public async Task UpdateAsync(ThumbnailItem item) return await UpdateInternalAsync(_context, item); } // InvalidOperationException can also be disposed - catch (InvalidOperationException) + catch ( InvalidOperationException ) { if ( _scopeFactory == null ) throw; return await UpdateInternalAsync(new InjectServiceScope(_scopeFactory).Context(), item); @@ -268,28 +268,28 @@ internal static async Task UpdateInternalAsync(ApplicationDbContext dbCont /// /// internal static async Task<(List newThumbnailItems, - List updateThumbnailItems, List equalThumbnailItems)> - CheckForDuplicates(ApplicationDbContext context, + List updateThumbnailItems, List equalThumbnailItems)> + CheckForDuplicates(ApplicationDbContext context, IEnumerable updateThumbnailNewItemsList) { - var nonNullItems = updateThumbnailNewItemsList.Where(item => item != null && + var nonNullItems = updateThumbnailNewItemsList.Where(item => item != null && item.FileHash != null!).Distinct().ToList(); - + var dbThumbnailItems = await context.Thumbnails .Where(p => nonNullItems.Select(x => x!.FileHash) .Contains(p.FileHash)).ToListAsync(); var alreadyExistingThumbnails = dbThumbnailItems.Select(p => p.FileHash).Distinct(); - + var newThumbnailItems = nonNullItems.Where(p => !alreadyExistingThumbnails. Contains(p!.FileHash)).Cast().DistinctBy(p => p.FileHash).ToList(); - + var alreadyExistingThumbnailItems = nonNullItems .Where(p => alreadyExistingThumbnails.Contains(p!.FileHash)) .Cast().DistinctBy(p => p.FileHash).ToList(); var updateThumbnailItems = new List(); var equalThumbnailItems = new List(); - + // merge two items together foreach ( var item in dbThumbnailItems ) { @@ -310,19 +310,19 @@ internal static async Task UpdateInternalAsync(ApplicationDbContext dbCont reasons.Append($",{item.Reasons}"); alreadyExistingThumbnailItems[indexOfAlreadyExists].Reasons = reasons.ToString(); } - - if ( item.TinyMeta == alreadyExists.TinyMeta && - item.Large == alreadyExists.Large && - item.Small == alreadyExists.Small && - item.ExtraLarge == alreadyExists.ExtraLarge) + + if ( item.TinyMeta == alreadyExists.TinyMeta && + item.Large == alreadyExists.Large && + item.Small == alreadyExists.Small && + item.ExtraLarge == alreadyExists.ExtraLarge ) { equalThumbnailItems.Add(alreadyExists); continue; } updateThumbnailItems.Add(alreadyExists); } - - return ( newThumbnailItems, updateThumbnailItems, equalThumbnailItems ); + + return (newThumbnailItems, updateThumbnailItems, equalThumbnailItems); } } diff --git a/starsky/starsky.foundation.database/Thumbnails/ThumbnailQueryFactory.cs b/starsky/starsky.foundation.database/Thumbnails/ThumbnailQueryFactory.cs index 233be6f2b5..79fd6ca73c 100644 --- a/starsky/starsky.foundation.database/Thumbnails/ThumbnailQueryFactory.cs +++ b/starsky/starsky.foundation.database/Thumbnails/ThumbnailQueryFactory.cs @@ -22,25 +22,25 @@ public ThumbnailQueryFactory(SetupDatabaseTypes? setupDatabaseTypes, IServiceSco _thumbnailQuery = thumbnailQuery; _logger = logger; } - + public IThumbnailQuery? ThumbnailQuery() { if ( _thumbnailQuery == null ) return null; var context = _setupDatabaseTypes?.BuilderDbFactory(); - if ( _thumbnailQuery.GetType() == typeof(ThumbnailQuery) && context != null) + if ( _thumbnailQuery.GetType() == typeof(ThumbnailQuery) && context != null ) { return new ThumbnailQuery(context, _serviceScopeFactory, _logger); } // FakeIQuery should skip creation var isAnyContentIncluded = _thumbnailQuery.GetReflectionFieldValue?>("_content")?.Count != 0; - + if ( !isAnyContentIncluded ) { return Activator.CreateInstance(_thumbnailQuery.GetType(), - context, _serviceScopeFactory,_logger) as IThumbnailQuery; + context, _serviceScopeFactory, _logger) as IThumbnailQuery; } - + _logger.LogInformation("FakeIThumbnailQuery _content detected"); return _thumbnailQuery; } diff --git a/starsky/starsky.foundation.database/starsky.foundation.database.csproj b/starsky/starsky.foundation.database/starsky.foundation.database.csproj index b844ba1d89..f211c059f6 100644 --- a/starsky/starsky.foundation.database/starsky.foundation.database.csproj +++ b/starsky/starsky.foundation.database/starsky.foundation.database.csproj @@ -11,80 +11,79 @@ - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - + - - migrations.md - + + migrations.md + diff --git a/starsky/starsky.foundation.databasetelemetry/Helpers/TelemetryConfigurationHelper.cs b/starsky/starsky.foundation.databasetelemetry/Helpers/TelemetryConfigurationHelper.cs deleted file mode 100644 index 83b26ac9b8..0000000000 --- a/starsky/starsky.foundation.databasetelemetry/Helpers/TelemetryConfigurationHelper.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.DependencyCollector; -using Microsoft.ApplicationInsights.Extensibility; -using starsky.foundation.databasetelemetry.Processor; -using starsky.foundation.platform.Interfaces; - -namespace starsky.foundation.databasetelemetry.Helpers -{ - public static class TelemetryConfigurationHelper - { - public static TelemetryClient? InitTelemetryClient(string appInsightsConnectionString, string roleName, IWebLogger? logger, TelemetryClient? telemetryClient) - { - try - { - // Should skip to avoid memory issues - if ( telemetryClient == null ) - { - var telemetryConfiguration = - CreateTelemetryConfiguration(appInsightsConnectionString); - if ( telemetryConfiguration == null ) return null; - telemetryClient = - new TelemetryClient(telemetryConfiguration); - telemetryClient.Context.Cloud.RoleName = roleName; - telemetryClient.Context.Cloud.RoleInstance = - Environment.MachineName; - logger?.LogInformation("Added TelemetryClient [should avoid due memory issues]"); - } - - var module = CreateDatabaseDependencyTrackingTelemetryModule(); - module.Initialize(telemetryClient.TelemetryConfiguration); - return telemetryClient; - } - catch ( OutOfMemoryException ) - { - return null; - } - catch (System.Threading.Tasks.TaskSchedulerException ) - { - return null; - } - } - - private static TelemetryConfiguration? CreateTelemetryConfiguration(string appInsightsConnectionString) - { - var telemetryConfiguration = TelemetryConfiguration.CreateDefault(); - telemetryConfiguration.ConnectionString = appInsightsConnectionString; - telemetryConfiguration.TelemetryProcessorChainBuilder.Use(next => new FilterWebsocketsTelemetryProcessor(next)); - telemetryConfiguration.TelemetryProcessorChainBuilder.Build(); - return telemetryConfiguration; - } - - private static DependencyTrackingTelemetryModule CreateDatabaseDependencyTrackingTelemetryModule() - { - var module = new DependencyTrackingTelemetryModule(); - module.IncludeDiagnosticSourceActivities.Add("Database"); - module.EnableSqlCommandTextInstrumentation = true; - return module; - } - } -} diff --git a/starsky/starsky.foundation.databasetelemetry/Helpers/TrackDependency.cs b/starsky/starsky.foundation.databasetelemetry/Helpers/TrackDependency.cs deleted file mode 100644 index 64654cb0e1..0000000000 --- a/starsky/starsky.foundation.databasetelemetry/Helpers/TrackDependency.cs +++ /dev/null @@ -1,42 +0,0 @@ -#nullable enable -using System; -using System.Data.Common; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.DataContracts; - -namespace starsky.foundation.databasetelemetry.Helpers -{ - public class TrackDependency - { - private readonly TelemetryClient? _telemetryClient; - - public TrackDependency(TelemetryClient? telemetryClient) - { - _telemetryClient = telemetryClient; - } - - public bool Track(DbCommand command, DateTimeOffset? startTime, string name, string telemetryType, bool success = true) - { - if ( startTime == null ) return false; - var duration = TimeSpan.Zero; - if (startTime.Value != default) - { - duration = DateTimeOffset.UtcNow - startTime.Value; - } - - var commandName = command.CommandText; - _telemetryClient?.TrackDependency(new DependencyTelemetry() - { - Name = name, - Data = commandName, - Type = telemetryType, - Duration = duration, - Timestamp = startTime.Value, - Success = success - }); - - return _telemetryClient != null; - } - - } -} diff --git a/starsky/starsky.foundation.databasetelemetry/Processor/FilterWebsocketsTelemetryProcessor.cs b/starsky/starsky.foundation.databasetelemetry/Processor/FilterWebsocketsTelemetryProcessor.cs deleted file mode 100644 index d5120c374a..0000000000 --- a/starsky/starsky.foundation.databasetelemetry/Processor/FilterWebsocketsTelemetryProcessor.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.ApplicationInsights.Extensibility; - -namespace starsky.foundation.databasetelemetry.Processor -{ - public sealed class FilterWebsocketsTelemetryProcessor : ITelemetryProcessor - { - private readonly ITelemetryProcessor _next; - - public FilterWebsocketsTelemetryProcessor(ITelemetryProcessor next) - { - // Next TelemetryProcessor in the chain - _next = next; - } - - public void Process(ITelemetry item) - { - if (item is RequestTelemetry request && request.ResponseCode == "101") - { - return; - } - - // Send the item to the next TelemetryProcessor - _next.Process(item); - } - } -} diff --git a/starsky/starsky.foundation.databasetelemetry/Services/DatabaseTelemetryInterceptor.cs b/starsky/starsky.foundation.databasetelemetry/Services/DatabaseTelemetryInterceptor.cs deleted file mode 100644 index 021775b2f5..0000000000 --- a/starsky/starsky.foundation.databasetelemetry/Services/DatabaseTelemetryInterceptor.cs +++ /dev/null @@ -1,149 +0,0 @@ -#nullable enable -using System.Data.Common; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.ApplicationInsights; -using Microsoft.EntityFrameworkCore.Diagnostics; -using starsky.foundation.databasetelemetry.Helpers; - -[assembly: InternalsVisibleTo("starskytest")] -namespace starsky.foundation.databasetelemetry.Services -{ - /// - /// @see: https://amuratgencay.medium.com/how-to-track-postgresql-queries-using-entityframework-core-in-application-insights-2f173d6c636d - /// - public sealed class DatabaseTelemetryInterceptor : IDbCommandInterceptor - { - private readonly TelemetryClient? _telemetryClient; - - private const string TelemetryType = "Database"; - - public DatabaseTelemetryInterceptor(TelemetryClient? telemetryClient) - { - _telemetryClient = telemetryClient; - } - - internal static string GetSqlName(DbCommand command) - { - var name = "SQLDatabase"; - if (command.Connection != null) - { - name = $"{command.Connection.DataSource} | {command.Connection.Database}"; - } - return name; - } - - public InterceptionResult CommandCreating(CommandCorrelatedEventData eventData, - InterceptionResult result) - { - return result; - } - - public DbCommand CommandCreated(CommandEndEventData eventData, DbCommand result) - { - return result; - } - - public InterceptionResult ReaderExecuting(DbCommand command, - CommandEventData eventData, InterceptionResult result) - { - return result; - } - - public InterceptionResult ScalarExecuting(DbCommand command, - CommandEventData eventData, InterceptionResult result) - { - return result; - } - - public InterceptionResult NonQueryExecuting(DbCommand command, - CommandEventData eventData, InterceptionResult result) - { - return result; - } - - public ValueTask> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, - InterceptionResult result, - CancellationToken cancellationToken = new CancellationToken()) - { - return ValueTask.FromResult(result); - } - - public ValueTask> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, - InterceptionResult result, - CancellationToken cancellationToken = new CancellationToken()) - { - return ValueTask.FromResult(result); - } - - public ValueTask> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, - InterceptionResult result, - CancellationToken cancellationToken = new CancellationToken()) - { - return ValueTask.FromResult(result); - } - - public DbDataReader ReaderExecuted(DbCommand command, - CommandExecutedEventData eventData, DbDataReader result) - { - return result; - } - - public object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, - object? result) - { - return result; - } - - public int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, - int result) - { - return result; - } - - public ValueTask ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, - DbDataReader result, - CancellationToken cancellationToken = new CancellationToken()) - { - return ValueTask.FromResult(result); - } - - public ValueTask ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, - object? result, - CancellationToken cancellationToken = new CancellationToken()) - { - return ValueTask.FromResult(result); - } - - public ValueTask NonQueryExecutedAsync(DbCommand command, - CommandExecutedEventData eventData, int result, - CancellationToken cancellationToken = new CancellationToken()) - { - return ValueTask.FromResult(result); - } - - public void CommandFailed(DbCommand command, CommandErrorEventData eventData) - { - // should not contain anything - } - - public Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, - CancellationToken cancellationToken = new CancellationToken()) - { - return Task.CompletedTask; - } - - public InterceptionResult DataReaderDisposing(DbCommand command, - DataReaderDisposingEventData? eventData, InterceptionResult result) - { - if ( command.CommandText.Contains("__EFMigrationsHistory") || command.CommandText.Contains("SearchSuggestionsService")) - { - return result; - } - - new TrackDependency(_telemetryClient).Track(command, eventData?.StartTime, GetSqlName(command), TelemetryType); - return result; - } - } -} diff --git a/starsky/starsky.foundation.databasetelemetry/starsky.foundation.databasetelemetry.csproj b/starsky/starsky.foundation.databasetelemetry/starsky.foundation.databasetelemetry.csproj deleted file mode 100644 index d039789f3b..0000000000 --- a/starsky/starsky.foundation.databasetelemetry/starsky.foundation.databasetelemetry.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - - {0697d621-f50c-430b-9a41-9a9992d63a2b} - Full - 0.6.0-beta.0 - enable - - - - - - - - - - - - - diff --git a/starsky/starsky.foundation.http/Interfaces/IHttpClientHelper.cs b/starsky/starsky.foundation.http/Interfaces/IHttpClientHelper.cs index 36d4facfe4..d62aa1d4d8 100644 --- a/starsky/starsky.foundation.http/Interfaces/IHttpClientHelper.cs +++ b/starsky/starsky.foundation.http/Interfaces/IHttpClientHelper.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; diff --git a/starsky/starsky.foundation.http/Interfaces/IHttpProvider.cs b/starsky/starsky.foundation.http/Interfaces/IHttpProvider.cs index 1ddbe365b0..b7c0f88e25 100644 --- a/starsky/starsky.foundation.http/Interfaces/IHttpProvider.cs +++ b/starsky/starsky.foundation.http/Interfaces/IHttpProvider.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Net.Http; using System.Threading.Tasks; diff --git a/starsky/starsky.foundation.http/Services/HttpClientHelper.cs b/starsky/starsky.foundation.http/Services/HttpClientHelper.cs index d7273b9a36..8292f10cf0 100644 --- a/starsky/starsky.foundation.http/Services/HttpClientHelper.cs +++ b/starsky/starsky.foundation.http/Services/HttpClientHelper.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.IO; @@ -17,45 +16,45 @@ namespace starsky.foundation.http.Services { [Service(typeof(IHttpClientHelper), InjectionLifetime = InjectionLifetime.Singleton)] - public sealed class HttpClientHelper : IHttpClientHelper - { - private readonly IStorage? _storage; - - /// - /// Set Http Provider - /// - /// IHttpProvider - /// ScopeFactory contains a IStorageSelector - /// WebLogger - public HttpClientHelper(IHttpProvider httpProvider, - IServiceScopeFactory? serviceScopeFactory, IWebLogger logger) - { - _httpProvider = httpProvider; - _logger = logger; - if ( serviceScopeFactory == null ) return; - - using ( var scope = serviceScopeFactory.CreateScope() ) - { - // ISelectorStorage is a scoped service - var selectorStorage = scope.ServiceProvider.GetRequiredService(); - _storage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); - } - } - - /// - /// Http Provider - /// - private readonly IHttpProvider _httpProvider; - - private readonly IWebLogger _logger; + public sealed class HttpClientHelper : IHttpClientHelper + { + private readonly IStorage? _storage; + + /// + /// Set Http Provider + /// + /// IHttpProvider + /// ScopeFactory contains a IStorageSelector + /// WebLogger + public HttpClientHelper(IHttpProvider httpProvider, + IServiceScopeFactory? serviceScopeFactory, IWebLogger logger) + { + _httpProvider = httpProvider; + _logger = logger; + if ( serviceScopeFactory == null ) return; + + using ( var scope = serviceScopeFactory.CreateScope() ) + { + // ISelectorStorage is a scoped service + var selectorStorage = scope.ServiceProvider.GetRequiredService(); + _storage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + } + } + + /// + /// Http Provider + /// + private readonly IHttpProvider _httpProvider; + + private readonly IWebLogger _logger; /// /// This domains are only allowed domains to download from (and https only) /// private readonly List _allowedDomains = new List - { - "dl.dropboxusercontent.com", - "qdraw.nl", // < used by test + { + "dl.dropboxusercontent.com", + "qdraw.nl", // < used by test "media.qdraw.nl", // < used by demo "locker.ifttt.com", "download.geonames.org", @@ -63,56 +62,56 @@ public HttpClientHelper(IHttpProvider httpProvider, "api.github.com" }; - public async Task> ReadString(string sourceHttpUrl) + public async Task> ReadString(string sourceHttpUrl) { Uri sourceUri = new Uri(sourceHttpUrl); - _logger.LogInformation("[ReadString] HttpClientHelper > " - + sourceUri.Host + " ~ " + sourceHttpUrl); + _logger.LogInformation("[ReadString] HttpClientHelper > " + + sourceUri.Host + " ~ " + sourceHttpUrl); // allow whitelist and https only - if (!_allowedDomains.Contains(sourceUri.Host) || sourceUri.Scheme != "https") return - new KeyValuePair(false,string.Empty); + if ( !_allowedDomains.Contains(sourceUri.Host) || sourceUri.Scheme != "https" ) return + new KeyValuePair(false, string.Empty); try { - using (HttpResponseMessage response = await _httpProvider.GetAsync(sourceHttpUrl)) - using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) + using ( HttpResponseMessage response = await _httpProvider.GetAsync(sourceHttpUrl) ) + using ( Stream streamToReadFrom = await response.Content.ReadAsStreamAsync() ) { var reader = new StreamReader(streamToReadFrom, Encoding.UTF8); var result = await reader.ReadToEndAsync(); - return new KeyValuePair(response.StatusCode == HttpStatusCode.OK,result); + return new KeyValuePair(response.StatusCode == HttpStatusCode.OK, result); } } - catch (HttpRequestException exception) + catch ( HttpRequestException exception ) { return new KeyValuePair(false, exception.Message); } } - - public async Task> PostString(string sourceHttpUrl, + + public async Task> PostString(string sourceHttpUrl, HttpContent? httpContent, bool verbose = true) { Uri sourceUri = new Uri(sourceHttpUrl); - if ( verbose ) _logger.LogInformation("[PostString] HttpClientHelper > " - + sourceUri.Host + " ~ " + sourceHttpUrl); + if ( verbose ) _logger.LogInformation("[PostString] HttpClientHelper > " + + sourceUri.Host + " ~ " + sourceHttpUrl); // // allow whitelist and https only - if (!_allowedDomains.Contains(sourceUri.Host) || sourceUri.Scheme != "https") return - new KeyValuePair(false,string.Empty); + if ( !_allowedDomains.Contains(sourceUri.Host) || sourceUri.Scheme != "https" ) return + new KeyValuePair(false, string.Empty); try { - using (HttpResponseMessage response = await _httpProvider.PostAsync(sourceHttpUrl, httpContent)) - using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) + using ( HttpResponseMessage response = await _httpProvider.PostAsync(sourceHttpUrl, httpContent) ) + using ( Stream streamToReadFrom = await response.Content.ReadAsStreamAsync() ) { var reader = new StreamReader(streamToReadFrom, Encoding.UTF8); var result = await reader.ReadToEndAsync(); - return new KeyValuePair(response.StatusCode == HttpStatusCode.OK,result); + return new KeyValuePair(response.StatusCode == HttpStatusCode.OK, result); } } - catch (HttpRequestException exception) + catch ( HttpRequestException exception ) { return new KeyValuePair(false, exception.Message); } @@ -129,53 +128,53 @@ public async Task Download(string sourceHttpUrl, string fullLocalPath, int { if ( _storage == null ) { - throw new EndOfStreamException("is null " + nameof(_storage) ); + throw new EndOfStreamException("is null " + nameof(_storage)); } - Uri sourceUri = new Uri(sourceHttpUrl); - - _logger.LogInformation("[Download] HttpClientHelper > " - + sourceUri.Host + " ~ " + sourceHttpUrl); - - // allow whitelist and https only - if ( !_allowedDomains.Contains(sourceUri.Host) || - sourceUri.Scheme != "https" ) - { - _logger.LogInformation("[Download] HttpClientHelper > " - + "skip: domain not whitelisted " + " ~ " + sourceHttpUrl); - return false; - } - - async Task DownloadAsync() - { - using var response = await _httpProvider.GetAsync(sourceHttpUrl); - await using var streamToReadFrom = await response.Content.ReadAsStreamAsync(); - if ( response.StatusCode != HttpStatusCode.OK ) - { - _logger.LogInformation("[Download] HttpClientHelper > " + - response.StatusCode + " ~ " + sourceHttpUrl); - return false; - } - - await _storage!.WriteStreamAsync(streamToReadFrom, fullLocalPath); - return true; - } - - try - { - return await RetryHelper.DoAsync(DownloadAsync, - TimeSpan.FromSeconds(retryAfterInSeconds), 2); - } - catch (AggregateException exception) - { - foreach ( var innerException in exception.InnerExceptions ) - { - _logger.LogError(innerException, $"[Download] InnerException: {exception.Message}"); - } - _logger.LogError(exception, $"[Download] Exception: {exception.Message}"); - return false; - } - } - } + Uri sourceUri = new Uri(sourceHttpUrl); + + _logger.LogInformation("[Download] HttpClientHelper > " + + sourceUri.Host + " ~ " + sourceHttpUrl); + + // allow whitelist and https only + if ( !_allowedDomains.Contains(sourceUri.Host) || + sourceUri.Scheme != "https" ) + { + _logger.LogInformation("[Download] HttpClientHelper > " + + "skip: domain not whitelisted " + " ~ " + sourceHttpUrl); + return false; + } + + async Task DownloadAsync() + { + using var response = await _httpProvider.GetAsync(sourceHttpUrl); + await using var streamToReadFrom = await response.Content.ReadAsStreamAsync(); + if ( response.StatusCode != HttpStatusCode.OK ) + { + _logger.LogInformation("[Download] HttpClientHelper > " + + response.StatusCode + " ~ " + sourceHttpUrl); + return false; + } + + await _storage!.WriteStreamAsync(streamToReadFrom, fullLocalPath); + return true; + } + + try + { + return await RetryHelper.DoAsync(DownloadAsync, + TimeSpan.FromSeconds(retryAfterInSeconds), 2); + } + catch ( AggregateException exception ) + { + foreach ( var innerException in exception.InnerExceptions ) + { + _logger.LogError(innerException, $"[Download] InnerException: {exception.Message}"); + } + _logger.LogError(exception, $"[Download] Exception: {exception.Message}"); + return false; + } + } + } } diff --git a/starsky/starsky.foundation.http/Services/HttpProvider.cs b/starsky/starsky.foundation.http/Services/HttpProvider.cs index 019ab01552..154af5267a 100644 --- a/starsky/starsky.foundation.http/Services/HttpProvider.cs +++ b/starsky/starsky.foundation.http/Services/HttpProvider.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Net; using System.Net.Http; @@ -35,7 +34,7 @@ public HttpProvider(HttpClient httpClient) /// Task with Response public Task GetAsync(string requestUri) { - _httpClient.DefaultRequestHeaders.Add("User-Agent",UserAgent); + _httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); return _httpClient.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead); } @@ -55,21 +54,21 @@ public Task PostAsync(string requestUri, HttpContent? conte Content = new StringContent("http content is null") }); } - - _httpClient.DefaultRequestHeaders.Add("User-Agent",UserAgent); + + _httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); var request = new HttpRequestMessage { Method = HttpMethod.Post, Content = content, - RequestUri = new Uri(requestUri) + RequestUri = new Uri(requestUri) }; - + if ( typeof(FormUrlEncodedContent) == content.GetType() ) { request.Headers.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"); } - + return _httpClient.SendAsync(request); } } diff --git a/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs b/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs index aab727219a..9d0f4e974c 100644 --- a/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs +++ b/starsky/starsky.foundation.http/Streaming/FileStreamingHelper.cs @@ -15,117 +15,117 @@ namespace starsky.foundation.http.Streaming { - public static class FileStreamingHelper - { - private static readonly FormOptions DefaultFormOptions = new FormOptions(); - - /// - /// Support for plain text input and base64 strings - /// Use for single files only - /// - /// HttpRequest - /// - public static string HeaderFileName(HttpRequest request) - { - // > when you do nothing - if (string.IsNullOrEmpty(request.Headers["filename"])) - return Base32.Encode(FileHash.GenerateRandomBytes(8)) + ".unknown"; - - // file without base64 encoding; return slug based url - if (Base64Helper.TryParse(request.Headers["filename"]).Length == 0) - return GenerateSlugHelper.GenerateSlug(Path.GetFileNameWithoutExtension(request.Headers["filename"]), - true, false, true) + Path.GetExtension(request.Headers["filename"]); - - var requestHeadersBytes = Base64Helper.TryParse(request.Headers["filename"]); - var requestHeaders = Encoding.ASCII.GetString(requestHeadersBytes); - return GenerateSlugHelper.GenerateSlug(Path.GetFileNameWithoutExtension(requestHeaders), - true, false, true) + Path.GetExtension(requestHeaders); - } - - public static async Task> StreamFile(this HttpRequest request, - AppSettings appSettings, ISelectorStorage selectorStorage) - { - // The Header 'filename' is for uploading on file without a form. - return await StreamFile(request.ContentType, request.Body, - appSettings, - selectorStorage, HeaderFileName(request)); - } - - [SuppressMessage("Usage", "S125:Remove this commented out code")] - [SuppressMessage("Usage", "S2589:contentDisposition null")] - public static async Task> StreamFile(string? contentType, Stream requestBody, AppSettings appSettings, - ISelectorStorage selectorStorage, string? headerFileName = null) - { - // headerFileName is for uploading on a single file without a multi part form. - - // fallback - headerFileName ??= Base32.Encode(FileHash.GenerateRandomBytes(8)) + ".unknown"; - - var tempPaths = new List(); - - if (!MultipartRequestHelper.IsMultipartContentType(contentType)) - { - if (contentType != "image/jpeg" && contentType != "application/octet-stream") - throw new FileLoadException($"Expected a multipart request, but got {contentType}; add the header 'content-type' "); - - var randomFolderName = "stream_" + - Base32.Encode(FileHash.GenerateRandomBytes(4)); - var fullFilePath = Path.Combine(appSettings.TempFolder, randomFolderName, headerFileName); - - // Write to disk - var hostFileSystemStorage = - selectorStorage.Get(SelectorStorage.StorageServices - .HostFilesystem); - hostFileSystemStorage.CreateDirectory(Path.Combine(appSettings.TempFolder, randomFolderName)); - await hostFileSystemStorage - .WriteStreamAsync(requestBody, fullFilePath); - - tempPaths.Add(fullFilePath); - - return tempPaths; - } - - // Used to accumulate all the form url encoded key value pairs in the - // request. - - var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(contentType), - DefaultFormOptions.MultipartBoundaryLengthLimit); - var reader = new MultipartReader(boundary, requestBody); - - var section = await reader.ReadNextSectionAsync(); - - while (section != null) - { - var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse( - section.ContentDisposition, out var contentDisposition); - - if (hasContentDispositionHeader && contentDisposition != null && MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) - { - var sourceFileName = contentDisposition.FileName.ToString().Replace("\"", string.Empty); - var inputExtension = Path.GetExtension(sourceFileName).Replace("\n",string.Empty); - - var tempHash = GenerateSlugHelper.GenerateSlug(Path.GetFileNameWithoutExtension(sourceFileName), - true, false, true); // underscore allowed - var randomFolderName = "stream_" + - Base32.Encode(FileHash.GenerateRandomBytes(4)); - var fullFilePath = Path.Combine(appSettings.TempFolder, randomFolderName, tempHash + inputExtension); - tempPaths.Add(fullFilePath); - - var hostFileSystemStorage = - selectorStorage.Get(SelectorStorage.StorageServices - .HostFilesystem); - hostFileSystemStorage.CreateDirectory(Path.Combine(appSettings.TempFolder, randomFolderName)); - - await hostFileSystemStorage - .WriteStreamAsync(section.Body, fullFilePath); - } - - // Drains any remaining section body that has not been consumed and - // reads the headers for the next section. - section = await reader.ReadNextSectionAsync(); - } - - return tempPaths; - } - } + public static class FileStreamingHelper + { + private static readonly FormOptions DefaultFormOptions = new FormOptions(); + + /// + /// Support for plain text input and base64 strings + /// Use for single files only + /// + /// HttpRequest + /// + public static string HeaderFileName(HttpRequest request) + { + // > when you do nothing + if ( string.IsNullOrEmpty(request.Headers["filename"]) ) + return Base32.Encode(FileHash.GenerateRandomBytes(8)) + ".unknown"; + + // file without base64 encoding; return slug based url + if ( Base64Helper.TryParse(request.Headers["filename"]).Length == 0 ) + return GenerateSlugHelper.GenerateSlug(Path.GetFileNameWithoutExtension(request.Headers["filename"]), + true, false, true) + Path.GetExtension(request.Headers["filename"]); + + var requestHeadersBytes = Base64Helper.TryParse(request.Headers["filename"]); + var requestHeaders = Encoding.ASCII.GetString(requestHeadersBytes); + return GenerateSlugHelper.GenerateSlug(Path.GetFileNameWithoutExtension(requestHeaders), + true, false, true) + Path.GetExtension(requestHeaders); + } + + public static async Task> StreamFile(this HttpRequest request, + AppSettings appSettings, ISelectorStorage selectorStorage) + { + // The Header 'filename' is for uploading on file without a form. + return await StreamFile(request.ContentType, request.Body, + appSettings, + selectorStorage, HeaderFileName(request)); + } + + [SuppressMessage("Usage", "S125:Remove this commented out code")] + [SuppressMessage("Usage", "S2589:contentDisposition null")] + public static async Task> StreamFile(string? contentType, Stream requestBody, AppSettings appSettings, + ISelectorStorage selectorStorage, string? headerFileName = null) + { + // headerFileName is for uploading on a single file without a multi part form. + + // fallback + headerFileName ??= Base32.Encode(FileHash.GenerateRandomBytes(8)) + ".unknown"; + + var tempPaths = new List(); + + if ( !MultipartRequestHelper.IsMultipartContentType(contentType) ) + { + if ( contentType != "image/jpeg" && contentType != "application/octet-stream" ) + throw new FileLoadException($"Expected a multipart request, but got {contentType}; add the header 'content-type' "); + + var randomFolderName = "stream_" + + Base32.Encode(FileHash.GenerateRandomBytes(4)); + var fullFilePath = Path.Combine(appSettings.TempFolder, randomFolderName, headerFileName); + + // Write to disk + var hostFileSystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices + .HostFilesystem); + hostFileSystemStorage.CreateDirectory(Path.Combine(appSettings.TempFolder, randomFolderName)); + await hostFileSystemStorage + .WriteStreamAsync(requestBody, fullFilePath); + + tempPaths.Add(fullFilePath); + + return tempPaths; + } + + // Used to accumulate all the form url encoded key value pairs in the + // request. + + var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(contentType), + DefaultFormOptions.MultipartBoundaryLengthLimit); + var reader = new MultipartReader(boundary, requestBody); + + var section = await reader.ReadNextSectionAsync(); + + while ( section != null ) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse( + section.ContentDisposition, out var contentDisposition); + + if ( hasContentDispositionHeader && contentDisposition != null && MultipartRequestHelper.HasFileContentDisposition(contentDisposition) ) + { + var sourceFileName = contentDisposition.FileName.ToString().Replace("\"", string.Empty); + var inputExtension = Path.GetExtension(sourceFileName).Replace("\n", string.Empty); + + var tempHash = GenerateSlugHelper.GenerateSlug(Path.GetFileNameWithoutExtension(sourceFileName), + true, false, true); // underscore allowed + var randomFolderName = "stream_" + + Base32.Encode(FileHash.GenerateRandomBytes(4)); + var fullFilePath = Path.Combine(appSettings.TempFolder, randomFolderName, tempHash + inputExtension); + tempPaths.Add(fullFilePath); + + var hostFileSystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices + .HostFilesystem); + hostFileSystemStorage.CreateDirectory(Path.Combine(appSettings.TempFolder, randomFolderName)); + + await hostFileSystemStorage + .WriteStreamAsync(section.Body, fullFilePath); + } + + // Drains any remaining section body that has not been consumed and + // reads the headers for the next section. + section = await reader.ReadNextSectionAsync(); + } + + return tempPaths; + } + } } diff --git a/starsky/starsky.foundation.http/Streaming/MultipartRequestHelper.cs b/starsky/starsky.foundation.http/Streaming/MultipartRequestHelper.cs index e505c84a74..911d76415e 100644 --- a/starsky/starsky.foundation.http/Streaming/MultipartRequestHelper.cs +++ b/starsky/starsky.foundation.http/Streaming/MultipartRequestHelper.cs @@ -1,53 +1,53 @@ -using System; +using System; using System.IO; using Microsoft.Net.Http.Headers; using MediaTypeHeaderValue = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; namespace starsky.foundation.http.Streaming { - public static class MultipartRequestHelper - { - // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" - // The spec says 70 characters is a reasonable limit. - public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) - { - var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0 - if (string.IsNullOrWhiteSpace(boundary)) - { - throw new InvalidDataException("Missing content-type boundary."); - } + public static class MultipartRequestHelper + { + // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" + // The spec says 70 characters is a reasonable limit. + public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + { + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0 + if ( string.IsNullOrWhiteSpace(boundary) ) + { + throw new InvalidDataException("Missing content-type boundary."); + } - if (boundary.Length > lengthLimit) - { - throw new InvalidDataException( - $"Multipart boundary length limit {lengthLimit} exceeded."); - } + if ( boundary.Length > lengthLimit ) + { + throw new InvalidDataException( + $"Multipart boundary length limit {lengthLimit} exceeded."); + } - return boundary; - } + return boundary; + } - public static bool IsMultipartContentType(string? contentType) - { - return !string.IsNullOrEmpty(contentType) - && contentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase); - } + public static bool IsMultipartContentType(string? contentType) + { + return !string.IsNullOrEmpty(contentType) + && contentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase); + } - public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) - { - // Content-Disposition: form-data; name="key" dot comma - return contentDisposition != null - && contentDisposition.DispositionType.Equals("form-data") - && string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value" - && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value" - } + public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="key" dot comma + return contentDisposition != null + && contentDisposition.DispositionType.Equals("form-data") + && string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value" + && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value" + } - public static bool HasFileContentDisposition(ContentDispositionHeaderValue? contentDisposition) - { - // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" - return contentDisposition != null - && contentDisposition.DispositionType.Equals("form-data") - && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value" - || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value" - } - } + public static bool HasFileContentDisposition(ContentDispositionHeaderValue? contentDisposition) + { + // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" + return contentDisposition != null + && contentDisposition.DispositionType.Equals("form-data") + && ( !string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value" + || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value) ); // For .NET Core <2.0 remove ".Value" + } + } } diff --git a/starsky/starsky.foundation.injection/InjectionLifetime.cs b/starsky/starsky.foundation.injection/InjectionLifetime.cs index 698dedec44..f57f3d8643 100644 --- a/starsky/starsky.foundation.injection/InjectionLifetime.cs +++ b/starsky/starsky.foundation.injection/InjectionLifetime.cs @@ -1,4 +1,4 @@ -namespace starsky.foundation.injection +namespace starsky.foundation.injection { public enum InjectionLifetime { @@ -6,12 +6,12 @@ public enum InjectionLifetime /// It's opposite to singleton. You'll get as many object as you call Resolve /// Transient, - + /// /// It's mean "one instance for all". All times when you call Resolve (even implicitly) you got the same object /// Singleton, - + /// /// Scoped lifetime services are created once per client request (connection). but different across different requests. /// diff --git a/starsky/starsky.foundation.injection/ServiceCollectionExtensions.cs b/starsky/starsky.foundation.injection/ServiceCollectionExtensions.cs index 127abeffb3..90ad9bb221 100644 --- a/starsky/starsky.foundation.injection/ServiceCollectionExtensions.cs +++ b/starsky/starsky.foundation.injection/ServiceCollectionExtensions.cs @@ -11,210 +11,214 @@ [assembly: InternalsVisibleTo("starskytest")] namespace starsky.foundation.injection { - public static class ServiceCollectionExtensions - { - public static void AddClassesWithServiceAttribute(this IServiceCollection serviceCollection, - params string[] assemblyFilters) - { - var assemblies = GetAssemblies(assemblyFilters.ToList()); - serviceCollection.AddClassesWithServiceAttribute(assemblies); - } - - /// - /// - /// - /// - /// - public static void AddClassesWithServiceAttribute(this IServiceCollection serviceCollection, - params Assembly[] assemblies) - { - var typesWithAttributes = assemblies - .Where(assembly => !assembly.IsDynamic) - .SelectMany(GetExportedTypes) - .Where(type => type?.IsAbstract == false && !type.IsGenericTypeDefinition) - .Select(type => new { Lifetime = type?.GetCustomAttribute()?.InjectionLifetime, ServiceType = type, - ImplementationType = type?.GetCustomAttribute()?.ServiceType }) - .Where(t => t.Lifetime != null); - - foreach (var type in typesWithAttributes) - { - if (type.ImplementationType == null) - serviceCollection.Add(type.ServiceType!, type.Lifetime!.Value); - else - serviceCollection.Add(type.ImplementationType, type.ServiceType!, type.Lifetime!.Value); - } - } - - - public static void Add(this IServiceCollection serviceCollection, InjectionLifetime ioCLifetime, params Type[] types) - { - if ( types == null ) - { - throw new ArgumentNullException(types + nameof(types)); - } - - foreach (var type in types) - { - serviceCollection.Add(type, ioCLifetime); - } - } - - public static void Add(this IServiceCollection serviceCollection, InjectionLifetime ioCLifetime) - { - serviceCollection.Add(typeof(T), ioCLifetime); - } - - public static void Add(this IServiceCollection serviceCollection, Type type, InjectionLifetime ioCLifetime) - { - switch (ioCLifetime) - { - case InjectionLifetime.Singleton: - serviceCollection.AddSingleton(type); - break; - case InjectionLifetime.Transient: - serviceCollection.AddTransient(type); - break; - case InjectionLifetime.Scoped: - serviceCollection.AddScoped(type); - break; - default: - throw new ArgumentOutOfRangeException(nameof(ioCLifetime), ioCLifetime, null); - } - } - - public static void Add(this IServiceCollection serviceCollection, Type serviceType, - Type implementationType, InjectionLifetime ioCLifetime) - { - switch (ioCLifetime) - { - case InjectionLifetime.Singleton: - serviceCollection.AddSingleton(serviceType, implementationType); - break; - case InjectionLifetime.Transient: - serviceCollection.AddTransient(serviceType, implementationType); - break; - case InjectionLifetime.Scoped: - serviceCollection.AddScoped(serviceType, implementationType); - break; - default: - throw new ArgumentOutOfRangeException(nameof(ioCLifetime), ioCLifetime, null); - } - } - - private static Assembly[] GetAssemblies(List assemblyFilters) - { - var assemblies = new List(); - foreach (var assemblyFilter in assemblyFilters) - { - assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(assembly => - IsWildcardMatch(assembly.GetName().Name!, assemblyFilter)).ToArray()); - } - - assemblies = GetEntryAssemblyReferencedAssemblies(assemblies, - assemblyFilters); - - return GetReferencedAssemblies(assemblies, assemblyFilters); - } - - /// - /// Load Assemblies with filter - /// @see: https://dotnetcoretutorials.com/2020/07/03/getting-assemblies-is-harder-than-you-think-in-c/ - /// - /// Current Assemblies - /// filter used - /// list of assemblies used - private static List GetEntryAssemblyReferencedAssemblies(List assemblies, List assemblyFilters) - { - var assemblyNames = Assembly - .GetEntryAssembly()?.GetReferencedAssemblies(); - if ( assemblyNames == null ) - { - return assemblies; - } - - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach ( var assemblyName in assemblyNames ) - { - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach ( var assemblyFilter in assemblyFilters ) - { - var isThere = assemblies.Exists(p => p.FullName == assemblyName.FullName); - if (IsWildcardMatch(assemblyName.Name!, assemblyFilter) && !isThere ) - { - assemblies.Add(AppDomain.CurrentDomain.Load(assemblyName)); - } - } - } - - return assemblies.OrderBy(p => p.FullName).ToList(); - } - - /// - /// Get the assembly files that are referenced and match the pattern - /// - /// current referenced assemblies - /// filters that need to be checked - /// - private static Assembly[] GetReferencedAssemblies(List assemblies, IEnumerable assemblyFilters) - { - // assemblies.ToList() to avoid Collection was modified; enumeration operation may not execute - foreach (var assemblyFilter in assemblyFilters.ToList()) - { - foreach ( var assembly in assemblies.ToList()) - { - foreach ( var referencedAssembly in assembly.GetReferencedAssemblies() ) - { - if ( IsWildcardMatch(referencedAssembly.Name!, assemblyFilter) - && assemblies.TrueForAll(p => p.FullName != referencedAssembly.FullName) ) - { - assemblies.Add(Assembly.Load(referencedAssembly)); - } - } - } - } - return assemblies.ToArray(); - } - - internal static IEnumerable GetExportedTypes(Assembly assembly) - { - try - { - return assembly.GetExportedTypes(); - } - catch (NotSupportedException) - { - // A type load exception would typically happen on an Anonymously Hosted DynamicMethods - // Assembly and it would be safe to skip this exception. - return Type.EmptyTypes; - } - catch (FileLoadException) - { - // The assembly points to a not found assembly - ignore and continue - return Type.EmptyTypes; - } - catch (ReflectionTypeLoadException ex) - { - // Return the types that could be loaded. Types can contain null values. - return ex.Types.Where(type => type != null); - } - catch (Exception ex) - { - // Throw a more descriptive message containing the name of the assembly. - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, - "Unable to load types from assembly {0}. {1}", assembly.FullName, ex.Message), ex); - } - } - - /// - /// Checks if a string matches a wildcard argument (using regex) - /// - private static bool IsWildcardMatch(string input, string wildcard) - { - return input == wildcard || Regex.IsMatch(input, "^" + - Regex.Escape(wildcard).Replace("\\*", ".*") - .Replace("\\?", ".") + "$", RegexOptions.IgnoreCase, - TimeSpan.FromMilliseconds(100)); - } - - } + public static class ServiceCollectionExtensions + { + public static void AddClassesWithServiceAttribute(this IServiceCollection serviceCollection, + params string[] assemblyFilters) + { + var assemblies = GetAssemblies(assemblyFilters.ToList()); + serviceCollection.AddClassesWithServiceAttribute(assemblies); + } + + /// + /// + /// + /// + /// + public static void AddClassesWithServiceAttribute(this IServiceCollection serviceCollection, + params Assembly[] assemblies) + { + var typesWithAttributes = assemblies + .Where(assembly => !assembly.IsDynamic) + .SelectMany(GetExportedTypes) + .Where(type => type?.IsAbstract == false && !type.IsGenericTypeDefinition) + .Select(type => new + { + Lifetime = type?.GetCustomAttribute()?.InjectionLifetime, + ServiceType = type, + ImplementationType = type?.GetCustomAttribute()?.ServiceType + }) + .Where(t => t.Lifetime != null); + + foreach ( var type in typesWithAttributes ) + { + if ( type.ImplementationType == null ) + serviceCollection.Add(type.ServiceType!, type.Lifetime!.Value); + else + serviceCollection.Add(type.ImplementationType, type.ServiceType!, type.Lifetime!.Value); + } + } + + + public static void Add(this IServiceCollection serviceCollection, InjectionLifetime ioCLifetime, params Type[] types) + { + if ( types == null ) + { + throw new ArgumentNullException(types + nameof(types)); + } + + foreach ( var type in types ) + { + serviceCollection.Add(type, ioCLifetime); + } + } + + public static void Add(this IServiceCollection serviceCollection, InjectionLifetime ioCLifetime) + { + serviceCollection.Add(typeof(T), ioCLifetime); + } + + public static void Add(this IServiceCollection serviceCollection, Type type, InjectionLifetime ioCLifetime) + { + switch ( ioCLifetime ) + { + case InjectionLifetime.Singleton: + serviceCollection.AddSingleton(type); + break; + case InjectionLifetime.Transient: + serviceCollection.AddTransient(type); + break; + case InjectionLifetime.Scoped: + serviceCollection.AddScoped(type); + break; + default: + throw new ArgumentOutOfRangeException(nameof(ioCLifetime), ioCLifetime, null); + } + } + + public static void Add(this IServiceCollection serviceCollection, Type serviceType, + Type implementationType, InjectionLifetime ioCLifetime) + { + switch ( ioCLifetime ) + { + case InjectionLifetime.Singleton: + serviceCollection.AddSingleton(serviceType, implementationType); + break; + case InjectionLifetime.Transient: + serviceCollection.AddTransient(serviceType, implementationType); + break; + case InjectionLifetime.Scoped: + serviceCollection.AddScoped(serviceType, implementationType); + break; + default: + throw new ArgumentOutOfRangeException(nameof(ioCLifetime), ioCLifetime, null); + } + } + + private static Assembly[] GetAssemblies(List assemblyFilters) + { + var assemblies = new List(); + foreach ( var assemblyFilter in assemblyFilters ) + { + assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Where(assembly => + IsWildcardMatch(assembly.GetName().Name!, assemblyFilter)).ToArray()); + } + + assemblies = GetEntryAssemblyReferencedAssemblies(assemblies, + assemblyFilters); + + return GetReferencedAssemblies(assemblies, assemblyFilters); + } + + /// + /// Load Assemblies with filter + /// @see: https://dotnetcoretutorials.com/2020/07/03/getting-assemblies-is-harder-than-you-think-in-c/ + /// + /// Current Assemblies + /// filter used + /// list of assemblies used + private static List GetEntryAssemblyReferencedAssemblies(List assemblies, List assemblyFilters) + { + var assemblyNames = Assembly + .GetEntryAssembly()?.GetReferencedAssemblies(); + if ( assemblyNames == null ) + { + return assemblies; + } + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach ( var assemblyName in assemblyNames ) + { + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach ( var assemblyFilter in assemblyFilters ) + { + var isThere = assemblies.Exists(p => p.FullName == assemblyName.FullName); + if ( IsWildcardMatch(assemblyName.Name!, assemblyFilter) && !isThere ) + { + assemblies.Add(AppDomain.CurrentDomain.Load(assemblyName)); + } + } + } + + return assemblies.OrderBy(p => p.FullName).ToList(); + } + + /// + /// Get the assembly files that are referenced and match the pattern + /// + /// current referenced assemblies + /// filters that need to be checked + /// + private static Assembly[] GetReferencedAssemblies(List assemblies, IEnumerable assemblyFilters) + { + // assemblies.ToList() to avoid Collection was modified; enumeration operation may not execute + foreach ( var assemblyFilter in assemblyFilters.ToList() ) + { + foreach ( var assembly in assemblies.ToList() ) + { + foreach ( var referencedAssembly in assembly.GetReferencedAssemblies() ) + { + if ( IsWildcardMatch(referencedAssembly.Name!, assemblyFilter) + && assemblies.TrueForAll(p => p.FullName != referencedAssembly.FullName) ) + { + assemblies.Add(Assembly.Load(referencedAssembly)); + } + } + } + } + return assemblies.ToArray(); + } + + internal static IEnumerable GetExportedTypes(Assembly assembly) + { + try + { + return assembly.GetExportedTypes(); + } + catch ( NotSupportedException ) + { + // A type load exception would typically happen on an Anonymously Hosted DynamicMethods + // Assembly and it would be safe to skip this exception. + return Type.EmptyTypes; + } + catch ( FileLoadException ) + { + // The assembly points to a not found assembly - ignore and continue + return Type.EmptyTypes; + } + catch ( ReflectionTypeLoadException ex ) + { + // Return the types that could be loaded. Types can contain null values. + return ex.Types.Where(type => type != null); + } + catch ( Exception ex ) + { + // Throw a more descriptive message containing the name of the assembly. + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, + "Unable to load types from assembly {0}. {1}", assembly.FullName, ex.Message), ex); + } + } + + /// + /// Checks if a string matches a wildcard argument (using regex) + /// + private static bool IsWildcardMatch(string input, string wildcard) + { + return input == wildcard || Regex.IsMatch(input, "^" + + Regex.Escape(wildcard).Replace("\\*", ".*") + .Replace("\\?", ".") + "$", RegexOptions.IgnoreCase, + TimeSpan.FromMilliseconds(100)); + } + + } } diff --git a/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs b/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs index d5a34878ef..efccca4dfc 100644 --- a/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs +++ b/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs @@ -11,7 +11,8 @@ public static OSPlatform GetPlatform() internal delegate bool IsOsPlatformDelegate(OSPlatform osPlatform); - internal static OSPlatform GetPlatformInternal(IsOsPlatformDelegate isOsPlatformDelegate) { + internal static OSPlatform GetPlatformInternal(IsOsPlatformDelegate isOsPlatformDelegate) + { if ( isOsPlatformDelegate(OSPlatform.Windows) ) { return OSPlatform.Windows; diff --git a/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs b/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs index fc815707eb..93e54f568c 100644 --- a/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs +++ b/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs @@ -6,144 +6,146 @@ [assembly: InternalsVisibleTo("starskytest")] namespace starsky.foundation.native.Trash.Helpers { - + /// /// Trash the file on Mac OS /// There is NO check if the file exists /// /// @see: https://stackoverflow.com/a/44669560 /// - [SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' instead of \'DllImportAttribute\' " + - "to generate P/Invoke marshalling code at compile time")] + [SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' instead of \'DllImportAttribute\' " + + "to generate P/Invoke marshalling code at compile time")] public static class MacOsTrashBindingHelper - { - /// - /// Trash endpoint - /// - /// - /// - /// - internal static bool? Trash(string fullPath, - OSPlatform platform) - { - return Trash(new List{fullPath}, platform); - } - - /// - /// Trash endpoint - /// - /// list of paths - /// current os - /// operation succeed - internal static bool? Trash(List filesFullPath, OSPlatform platform) - { - if ( platform != OSPlatform.OSX ) - { - return null; - } - - TrashInternal(filesFullPath); - return true; - } - - internal static IntPtr[] GetUrls(List filesFullPath) - { - var urls = new List(); - foreach ( var filePath in filesFullPath ) - { - var cfStrTestFile = CreateCfString(filePath); - var nsUrl = objc_getClass("NSURL"); - var fileUrl = objc_msgSend_retIntPtr_IntPtr(nsUrl, GetSelector("fileURLWithPath:"), cfStrTestFile); - CFRelease(cfStrTestFile); - urls.Add(fileUrl); - } - return urls.ToArray(); - } - - internal static void TrashInternal(List filesFullPath) - { - var urls = GetUrls(filesFullPath); - - var urlArray = CreateCfArray(urls); - - var nsWorkspace = objc_getClass("NSWorkspace"); - var sharedWorkspace = objc_msgSend_retIntPtr(nsWorkspace, GetSelector("sharedWorkspace")); - var completionHandler = GetSelector("recycleURLs:completionHandler:") ; - - // https://developer.apple.com/documentation/appkit/nsworkspace/1530465-recycle - objc_msgSend_retVoid_IntPtr_IntPtr(sharedWorkspace, - completionHandler, - urlArray, IntPtr.Zero); - - CFRelease(urlArray); - // CFRelease the fileUrl, sharedWorkspace, nsWorkspace gives a crash (error 139) - } - - internal static IntPtr GetSelector(string name) - { - var cfStrSelector = CreateCfString(name); - var selector = NSSelectorFromString(cfStrSelector); - CFRelease(cfStrSelector); - return selector; - } - - private const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation"; - private const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit"; - - [SuppressMessage("Usage", "S6640: Make sure that using \"unsafe\" is safe here")] - internal static unsafe IntPtr CreateCfString(string aString) - { - var bytes = Encoding.Unicode.GetBytes(aString); - fixed (byte* b = bytes) { - var cfStr = CFStringCreateWithBytes(IntPtr.Zero, (IntPtr)b, bytes.Length, - CfStringEncoding.UTF16, false); - return cfStr; - } - } - - [SuppressMessage("Usage", "S6640: Make sure that using \"unsafe\" is safe here")] - - internal static unsafe IntPtr CreateCfArray(IntPtr[] objects) - { - // warning: this doesn't call retain/release on the elements in the array - fixed(IntPtr* values = objects) { - return CFArrayCreate(IntPtr.Zero, (IntPtr)values, objects.Length, IntPtr.Zero); - } - } - - [DllImport(FoundationFramework)] - private static extern IntPtr CFStringCreateWithBytes(IntPtr allocator, IntPtr buffer, - long bufferLength, CfStringEncoding encoding, bool isExternalRepresentation); - - [DllImport(FoundationFramework)] - private static extern IntPtr CFArrayCreate(IntPtr allocator, IntPtr values, long numValues, IntPtr callbackStruct); - - [DllImport(FoundationFramework)] - private static extern void CFRelease(IntPtr handle); - - [SuppressMessage("Usage", "CA2101: Specify marshaling for P/Invoke string arguments")] - [DllImport(AppKitFramework, CharSet = CharSet.Ansi)] - private static extern IntPtr objc_getClass(string name); - - [DllImport(AppKitFramework)] - private static extern IntPtr NSSelectorFromString(IntPtr cfstr); - - [DllImport(FoundationFramework, EntryPoint="objc_msgSend")] - private static extern IntPtr objc_msgSend_retIntPtr(IntPtr target, IntPtr selector); - - [DllImport(FoundationFramework, EntryPoint="objc_msgSend")] - private static extern void objc_msgSend_retVoid_IntPtr_IntPtr(IntPtr target, IntPtr selector, IntPtr param1, IntPtr param2); - - [DllImport(FoundationFramework, EntryPoint="objc_msgSend")] - private static extern IntPtr objc_msgSend_retIntPtr_IntPtr(IntPtr target, IntPtr selector, IntPtr param); - - [SuppressMessage("ReSharper", "InconsistentNaming")] - public enum CfStringEncoding : uint - { - UTF16 = 0x0100, - UTF16BE = 0x10000100, - UTF16LE = 0x14000100, - ASCII = 0x0600 - } - } + { + /// + /// Trash endpoint + /// + /// + /// + /// + internal static bool? Trash(string fullPath, + OSPlatform platform) + { + return Trash(new List { fullPath }, platform); + } + + /// + /// Trash endpoint + /// + /// list of paths + /// current os + /// operation succeed + internal static bool? Trash(List filesFullPath, OSPlatform platform) + { + if ( platform != OSPlatform.OSX ) + { + return null; + } + + TrashInternal(filesFullPath); + return true; + } + + internal static IntPtr[] GetUrls(List filesFullPath) + { + var urls = new List(); + foreach ( var filePath in filesFullPath ) + { + var cfStrTestFile = CreateCfString(filePath); + var nsUrl = objc_getClass("NSURL"); + var fileUrl = objc_msgSend_retIntPtr_IntPtr(nsUrl, GetSelector("fileURLWithPath:"), cfStrTestFile); + CFRelease(cfStrTestFile); + urls.Add(fileUrl); + } + return urls.ToArray(); + } + + internal static void TrashInternal(List filesFullPath) + { + var urls = GetUrls(filesFullPath); + + var urlArray = CreateCfArray(urls); + + var nsWorkspace = objc_getClass("NSWorkspace"); + var sharedWorkspace = objc_msgSend_retIntPtr(nsWorkspace, GetSelector("sharedWorkspace")); + var completionHandler = GetSelector("recycleURLs:completionHandler:"); + + // https://developer.apple.com/documentation/appkit/nsworkspace/1530465-recycle + objc_msgSend_retVoid_IntPtr_IntPtr(sharedWorkspace, + completionHandler, + urlArray, IntPtr.Zero); + + CFRelease(urlArray); + // CFRelease the fileUrl, sharedWorkspace, nsWorkspace gives a crash (error 139) + } + + internal static IntPtr GetSelector(string name) + { + var cfStrSelector = CreateCfString(name); + var selector = NSSelectorFromString(cfStrSelector); + CFRelease(cfStrSelector); + return selector; + } + + private const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation"; + private const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit"; + + [SuppressMessage("Usage", "S6640: Make sure that using \"unsafe\" is safe here")] + internal static unsafe IntPtr CreateCfString(string aString) + { + var bytes = Encoding.Unicode.GetBytes(aString); + fixed ( byte* b = bytes ) + { + var cfStr = CFStringCreateWithBytes(IntPtr.Zero, ( IntPtr )b, bytes.Length, + CfStringEncoding.UTF16, false); + return cfStr; + } + } + + [SuppressMessage("Usage", "S6640: Make sure that using \"unsafe\" is safe here")] + + internal static unsafe IntPtr CreateCfArray(IntPtr[] objects) + { + // warning: this doesn't call retain/release on the elements in the array + fixed ( IntPtr* values = objects ) + { + return CFArrayCreate(IntPtr.Zero, ( IntPtr )values, objects.Length, IntPtr.Zero); + } + } + + [DllImport(FoundationFramework)] + private static extern IntPtr CFStringCreateWithBytes(IntPtr allocator, IntPtr buffer, + long bufferLength, CfStringEncoding encoding, bool isExternalRepresentation); + + [DllImport(FoundationFramework)] + private static extern IntPtr CFArrayCreate(IntPtr allocator, IntPtr values, long numValues, IntPtr callbackStruct); + + [DllImport(FoundationFramework)] + private static extern void CFRelease(IntPtr handle); + + [SuppressMessage("Usage", "CA2101: Specify marshaling for P/Invoke string arguments")] + [DllImport(AppKitFramework, CharSet = CharSet.Ansi)] + private static extern IntPtr objc_getClass(string name); + + [DllImport(AppKitFramework)] + private static extern IntPtr NSSelectorFromString(IntPtr cfstr); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern IntPtr objc_msgSend_retIntPtr(IntPtr target, IntPtr selector); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern void objc_msgSend_retVoid_IntPtr_IntPtr(IntPtr target, IntPtr selector, IntPtr param1, IntPtr param2); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern IntPtr objc_msgSend_retIntPtr_IntPtr(IntPtr target, IntPtr selector, IntPtr param); + + [SuppressMessage("ReSharper", "InconsistentNaming")] + public enum CfStringEncoding : uint + { + UTF16 = 0x0100, + UTF16BE = 0x10000100, + UTF16LE = 0x14000100, + ASCII = 0x0600 + } + } } diff --git a/starsky/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelper.cs b/starsky/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelper.cs index f5b6dba3f8..0631bc7de4 100644 --- a/starsky/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelper.cs +++ b/starsky/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelper.cs @@ -20,10 +20,10 @@ public static class WindowsShellTrashBindingHelper /// Location of directory or file to recycle /// should be windows /// FileOperationFlags to add in addition to FOF_ALLOWUNDO - internal static (bool?, string) Trash(string path, - OSPlatform platform, + internal static (bool?, string) Trash(string path, + OSPlatform platform, ShFileOperations flags = ShFileOperations.FOF_NOCONFIRMATION | - ShFileOperations.FOF_WANTNUKEWARNING) + ShFileOperations.FOF_WANTNUKEWARNING) { if ( platform != OSPlatform.Windows ) { @@ -42,7 +42,7 @@ internal static (bool?, string) Trash(string path, internal static (bool?, string) Trash(IEnumerable filesFullPath, OSPlatform platform, ShFileOperations flags = ShFileOperations.FOF_NOCONFIRMATION | - ShFileOperations.FOF_WANTNUKEWARNING) + ShFileOperations.FOF_WANTNUKEWARNING) { var results = filesFullPath.Select(path => Trash(path, platform, flags)).ToList(); return results.FirstOrDefault(); @@ -127,7 +127,7 @@ internal struct SHFILEOPSTRUCT [DllImport("shell32.dll", CharSet = CharSet.Unicode)] [SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' instead of \'DllImportAttribute\' " + - "to generate P/Invoke marshalling code at compile time")] + "to generate P/Invoke marshalling code at compile time")] private static extern int SHFileOperation(ref SHFILEOPSTRUCT FileOp); @@ -147,7 +147,7 @@ public static (bool, string) TrashInternal(string path, ShFileOperations flags = var result = SHFileOperation(ref fs).ToString(); return (true, result); } - catch ( Exception ex) + catch ( Exception ex ) { return (false, ex.Message); } @@ -185,7 +185,7 @@ internal struct SHQUERYRBINFO /// [DllImport("shell32.dll", CharSet = CharSet.Unicode)] [SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' " + - "instead of \'DllImportAttribute\' to generate P/Invoke marshalling code at compile time")] + "instead of \'DllImportAttribute\' to generate P/Invoke marshalling code at compile time")] private static extern int SHQueryRecycleBin(string pszRootPath, ref SHQUERYRBINFO pSHQueryRBInfo); @@ -215,15 +215,15 @@ internal static string SHQueryRecycleBinInfo(int? hResult, string drivePath, SHQ var successStatus = hResult == 0 ? "Success!" : "Fail!"; return $"{successStatus} Drive {drivePath} contains {pSHQueryRBInfo.i64NumItems} " + - $"item(s) in {pSHQueryRBInfo.i64Size:#,##0} bytes"; + $"item(s) in {pSHQueryRBInfo.i64Size:#,##0} bytes"; } public static (bool, long, string) DriveHasRecycleBin(string drivePath = @"C:\") { var (hResult, info, pSHQueryRBInfo) = SHQueryRecycleBinWrapper(drivePath); info ??= SHQueryRecycleBinInfo(hResult, drivePath, pSHQueryRBInfo); - + return (hResult == 0, pSHQueryRBInfo.i64NumItems, info); } - + } diff --git a/starsky/starsky.foundation.native/Trash/TrashService.cs b/starsky/starsky.foundation.native/Trash/TrashService.cs index d98a13c3ee..55a3734776 100644 --- a/starsky/starsky.foundation.native/Trash/TrashService.cs +++ b/starsky/starsky.foundation.native/Trash/TrashService.cs @@ -16,11 +16,11 @@ public class TrashService : ITrashService /// true if supported, false if not supported public bool DetectToUseSystemTrash() { - return DetectToUseSystemTrashInternal(RuntimeInformation.IsOSPlatform, - Environment.UserInteractive, + return DetectToUseSystemTrashInternal(RuntimeInformation.IsOSPlatform, + Environment.UserInteractive, Environment.UserName); } - + /// /// Use to overwrite the RuntimeInformation.IsOSPlatform /// @@ -33,14 +33,14 @@ public bool DetectToUseSystemTrash() /// Environment.UserInteractive /// Environment.UserName /// true if supported, false if not supported - internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtimeInformationIsOsPlatform, - bool environmentUserInteractive, + internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtimeInformationIsOsPlatform, + bool environmentUserInteractive, string environmentUserName) { // ReSharper disable once ConvertIfStatementToReturnStatement - if (runtimeInformationIsOsPlatform(OSPlatform.Linux) || - runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) || - environmentUserName == "root" || !environmentUserInteractive ) + if ( runtimeInformationIsOsPlatform(OSPlatform.Linux) || + runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) || + environmentUserName == "root" || !environmentUserInteractive ) { return false; } @@ -48,10 +48,10 @@ internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtime // ReSharper disable once InvertIf if ( runtimeInformationIsOsPlatform(OSPlatform.Windows) ) { - var (driveHasBin,_,_) = WindowsShellTrashBindingHelper.DriveHasRecycleBin(); + var (driveHasBin, _, _) = WindowsShellTrashBindingHelper.DriveHasRecycleBin(); return driveHasBin; } - + return true; } @@ -65,7 +65,7 @@ internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtime { var currentPlatform = OperatingSystemHelper.GetPlatform(); var macOsTrash = MacOsTrashBindingHelper.Trash(fullPath, currentPlatform); - var (windowsTrash,_) = WindowsShellTrashBindingHelper.Trash(fullPath, currentPlatform); + var (windowsTrash, _) = WindowsShellTrashBindingHelper.Trash(fullPath, currentPlatform); return macOsTrash ?? windowsTrash; } @@ -73,7 +73,7 @@ internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtime { var currentPlatform = OperatingSystemHelper.GetPlatform(); var macOsTrash = MacOsTrashBindingHelper.Trash(fullPaths, currentPlatform); - var (windowsTrash,_) = WindowsShellTrashBindingHelper.Trash(fullPaths, currentPlatform); + var (windowsTrash, _) = WindowsShellTrashBindingHelper.Trash(fullPaths, currentPlatform); return macOsTrash ?? windowsTrash; } } diff --git a/starsky/starsky.foundation.platform/Attributes/PackageTelemetryAttribute.cs b/starsky/starsky.foundation.platform/Attributes/PackageTelemetryAttribute.cs index 1fa03a6168..662103aa13 100644 --- a/starsky/starsky.foundation.platform/Attributes/PackageTelemetryAttribute.cs +++ b/starsky/starsky.foundation.platform/Attributes/PackageTelemetryAttribute.cs @@ -4,7 +4,7 @@ namespace starsky.foundation.platform.Attributes { [SuppressMessage("Design", "CA1018:Mark attributes with AttributeUsageAttribute")] - public sealed class PackageTelemetryAttribute: Attribute + public sealed class PackageTelemetryAttribute : Attribute { // Attribute is used to known if this property is used in the Telemetry // nothing here diff --git a/starsky/starsky.foundation.platform/Enums/ApiNotificationType.cs b/starsky/starsky.foundation.platform/Enums/ApiNotificationType.cs index 597b74ebde..fab123a2d3 100644 --- a/starsky/starsky.foundation.platform/Enums/ApiNotificationType.cs +++ b/starsky/starsky.foundation.platform/Enums/ApiNotificationType.cs @@ -12,12 +12,12 @@ public enum ApiNotificationType /// Welcome, - + /// /// Uses HeartbeatModel as payload /// Heartbeat, - + /// /// Uses List<FileIndexItem> as payload /// @@ -26,47 +26,47 @@ public enum ApiNotificationType /// Uses List<FileIndexItem> as payload /// ManualBackgroundSync, - + /// /// Uses List<FileIndexItem> as payload /// SyncWatcherConnector, - + /// /// Uses List<FileIndexItem> as payload /// Mkdir, - + /// /// Uses List<FileIndexItem> as payload /// Rename, - + /// /// Uses List<FileIndexItem> as payload /// UploadFile, - + /// /// Uses List<FileIndexItem> as payload /// MetaUpdate, - + /// /// Uses List<FileIndexItem> as payload /// Replace, - + /// /// On Startup Sync /// OnStartupSyncBackgroundSync, - + /// /// When clean and adding demo data in application /// CleanDemoData, - + /// /// Move to trash /// diff --git a/starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs b/starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs index 7d6c22de84..a629053526 100644 --- a/starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs +++ b/starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs @@ -9,7 +9,7 @@ public enum ThumbnailSize /// Should not use this one /// Unknown = 0, - + /// /// 150px /// @@ -18,13 +18,13 @@ public enum ThumbnailSize /// /// 300px /// - Small = 20, - + Small = 20, + /// /// 1000px /// Large = 30, - + /// /// 2000px /// diff --git a/starsky/starsky.foundation.platform/Exceptions/TelemetryServiceException.cs b/starsky/starsky.foundation.platform/Exceptions/TelemetryServiceException.cs index 9296069232..8379ddd3ca 100644 --- a/starsky/starsky.foundation.platform/Exceptions/TelemetryServiceException.cs +++ b/starsky/starsky.foundation.platform/Exceptions/TelemetryServiceException.cs @@ -10,13 +10,13 @@ public TelemetryServiceException(string message) : base(message) { } - + /// /// Without this constructor, deserialization will fail /// /// /// - protected TelemetryServiceException(SerializationInfo info, StreamingContext context) + protected TelemetryServiceException(SerializationInfo info, StreamingContext context) #pragma warning disable SYSLIB0051 : base(info, context) #pragma warning restore SYSLIB0051 diff --git a/starsky/starsky.foundation.platform/Extensions/AppSettingsExtensions.cs b/starsky/starsky.foundation.platform/Extensions/AppSettingsExtensions.cs index 0257993462..431e8a53c8 100644 --- a/starsky/starsky.foundation.platform/Extensions/AppSettingsExtensions.cs +++ b/starsky/starsky.foundation.platform/Extensions/AppSettingsExtensions.cs @@ -1,21 +1,21 @@ -using System; +using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace starsky.foundation.platform.Extensions { - public static class AppSettingsExtensions - { - public static TConfig ConfigurePoCo( - this IServiceCollection services, IConfiguration configuration) where TConfig : class, new() - { + public static class AppSettingsExtensions + { + public static TConfig ConfigurePoCo( + this IServiceCollection services, IConfiguration configuration) where TConfig : class, new() + { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); var config = new TConfig(); - configuration.Bind(config); - services.AddSingleton(config); - return config; - } - } + configuration.Bind(config); + services.AddSingleton(config); + return config; + } + } } diff --git a/starsky/starsky.foundation.platform/Extensions/ChunkHelper.cs b/starsky/starsky.foundation.platform/Extensions/ChunkHelper.cs index 705faead2b..cf1a3fd186 100644 --- a/starsky/starsky.foundation.platform/Extensions/ChunkHelper.cs +++ b/starsky/starsky.foundation.platform/Extensions/ChunkHelper.cs @@ -12,7 +12,7 @@ public static IEnumerable> ChunkyEnumerable(this IEnumerable(); - } - } + public static class ContentSecurityPolicyExtensions + { + public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/starsky/starsky.foundation.platform/Extensions/EnumerableExtensions.cs b/starsky/starsky.foundation.platform/Extensions/EnumerableExtensions.cs index 0575fb0087..8c1c6bc29a 100644 --- a/starsky/starsky.foundation.platform/Extensions/EnumerableExtensions.cs +++ b/starsky/starsky.foundation.platform/Extensions/EnumerableExtensions.cs @@ -29,9 +29,9 @@ public static class EnumerableExtensions var bufferBlock = new BufferBlock(); - using (transformBlock.LinkTo(bufferBlock, new DataflowLinkOptions {PropagateCompletion = true})) + using ( transformBlock.LinkTo(bufferBlock, new DataflowLinkOptions { PropagateCompletion = true }) ) { - foreach (var item in items) + foreach ( var item in items ) { transformBlock.Post(item); } diff --git a/starsky/starsky.foundation.platform/Extensions/MemoryCacheExtensions.cs b/starsky/starsky.foundation.platform/Extensions/MemoryCacheExtensions.cs index e15427d9b6..dbd0a8fa7c 100644 --- a/starsky/starsky.foundation.platform/Extensions/MemoryCacheExtensions.cs +++ b/starsky/starsky.foundation.platform/Extensions/MemoryCacheExtensions.cs @@ -34,14 +34,14 @@ private static Func CreateGetter(FieldInfo fie ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Ldfld, field); ilGen.Emit(OpCodes.Ret); - return (Func)method.CreateDelegate(typeof(Func)); + return ( Func )method.CreateDelegate(typeof(Func)); } - - private static readonly Func GetEntries = + + private static readonly Func GetEntries = cache => GetEntries7.Value(GetCoherentState.Value(cache)); private static ICollection GetKeys(this IMemoryCache memoryCache) => - GetEntries((MemoryCache)memoryCache).Keys; + GetEntries(( MemoryCache )memoryCache).Keys; /// /// Get Keys @@ -49,7 +49,8 @@ private static ICollection GetKeys(this IMemoryCache memoryCache) => /// memory cache /// bind as /// list of items - public static IEnumerable GetKeys(this IMemoryCache memoryCache) { + public static IEnumerable GetKeys(this IMemoryCache memoryCache) + { try { return GetKeys(memoryCache).OfType(); @@ -59,7 +60,7 @@ public static IEnumerable GetKeys(this IMemoryCache memoryCache) { return new List(); } } - - + + } } diff --git a/starsky/starsky.foundation.platform/Extensions/TimeoutTaskAfter.cs b/starsky/starsky.foundation.platform/Extensions/TimeoutTaskAfter.cs index df6ee67ec6..a8cf55ff3c 100644 --- a/starsky/starsky.foundation.platform/Extensions/TimeoutTaskAfter.cs +++ b/starsky/starsky.foundation.platform/Extensions/TimeoutTaskAfter.cs @@ -31,13 +31,14 @@ public static Task TimeoutAfter(this Task task, public static async Task TimeoutAfter(this Task task, TimeSpan timeout) { if ( timeout <= TimeSpan.Zero ) throw new TimeoutException("timeout less than 0"); - - using (var timeoutCancellationTokenSource = new CancellationTokenSource()) { + + using ( var timeoutCancellationTokenSource = new CancellationTokenSource() ) + { var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)); if ( completedTask != task ) throw new TimeoutException($"[TimeoutTaskAfter] {nameof(task)} operation has timed out"); - + timeoutCancellationTokenSource.Cancel(); return await task; // Very important in order to propagate exceptions } diff --git a/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs b/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs index 887e22c27c..dcf80bef8c 100644 --- a/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -24,14 +23,14 @@ public static List Compare(AppSettings sourceIndexItem, object? updateOb updateObject ??= new AppSettings(); var propertiesA = sourceIndexItem.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); var propertiesB = updateObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - + var differenceList = new List(); foreach ( var propertyB in propertiesB ) { // only for when TransferObject is Nullable and AppSettings is bool var propertyInfoFromA = Array.Find(propertiesA, p => p.Name == propertyB.Name); if ( propertyInfoFromA == null ) continue; - + CompareMultipleSingleItems(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList); CompareMultipleListDictionary(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList); CompareMultipleObjects(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList); @@ -42,27 +41,27 @@ public static List Compare(AppSettings sourceIndexItem, object? updateOb private static void CompareMultipleObjects(PropertyInfo propertyB, PropertyInfo propertyInfoFromA, AppSettings sourceIndexItem, object updateObject, List differenceList) { - if (propertyInfoFromA.PropertyType == typeof(OpenTelemetrySettings) && propertyB.PropertyType == typeof(OpenTelemetrySettings)) + if ( propertyInfoFromA.PropertyType == typeof(OpenTelemetrySettings) && propertyB.PropertyType == typeof(OpenTelemetrySettings) ) { - var oldObjectValue = (OpenTelemetrySettings?)propertyInfoFromA.GetValue(sourceIndexItem, null); - var newObjectValue = (OpenTelemetrySettings?)propertyB.GetValue(updateObject, null); + var oldObjectValue = ( OpenTelemetrySettings? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newObjectValue = ( OpenTelemetrySettings? )propertyB.GetValue(updateObject, null); CompareOpenTelemetrySettingsObject(propertyB.Name, sourceIndexItem, oldObjectValue, newObjectValue, differenceList); } } - + [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] - private static void CompareOpenTelemetrySettingsObject(string propertyName, AppSettings? sourceIndexItem, - OpenTelemetrySettings? oldKeyValuePairStringStringValue, + private static void CompareOpenTelemetrySettingsObject(string propertyName, AppSettings? sourceIndexItem, + OpenTelemetrySettings? oldKeyValuePairStringStringValue, OpenTelemetrySettings? newKeyValuePairStringStringValue, ICollection differenceList) { if ( oldKeyValuePairStringStringValue == null || - newKeyValuePairStringStringValue == null || - // compare lists - JsonSerializer.Serialize(oldKeyValuePairStringStringValue) == - JsonSerializer.Serialize(newKeyValuePairStringStringValue) || - // default options - JsonSerializer.Serialize(newKeyValuePairStringStringValue) == - JsonSerializer.Serialize(new OpenTelemetrySettings())) + newKeyValuePairStringStringValue == null || + // compare lists + JsonSerializer.Serialize(oldKeyValuePairStringStringValue) == + JsonSerializer.Serialize(newKeyValuePairStringStringValue) || + // default options + JsonSerializer.Serialize(newKeyValuePairStringStringValue) == + JsonSerializer.Serialize(new OpenTelemetrySettings()) ) { return; } @@ -76,106 +75,106 @@ private static void CompareMultipleSingleItems(PropertyInfo propertyB, AppSettings sourceIndexItem, object updateObject, List differenceList) { - if (propertyInfoFromA.PropertyType == typeof(bool?) && propertyB.PropertyType == typeof(bool?)) + if ( propertyInfoFromA.PropertyType == typeof(bool?) && propertyB.PropertyType == typeof(bool?) ) { - var oldBoolValue = (bool?)propertyInfoFromA.GetValue(sourceIndexItem, null); - var newBoolValue = (bool?)propertyB.GetValue(updateObject, null); + var oldBoolValue = ( bool? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newBoolValue = ( bool? )propertyB.GetValue(updateObject, null); CompareBool(propertyB.Name, sourceIndexItem, oldBoolValue, newBoolValue, differenceList); } if ( propertyB.PropertyType == typeof(string) ) { - var oldStringValue = (string?)propertyInfoFromA.GetValue(sourceIndexItem, null); - var newStringValue = (string?)propertyB.GetValue(updateObject, null); + var oldStringValue = ( string? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newStringValue = ( string? )propertyB.GetValue(updateObject, null); CompareString(propertyB.Name, sourceIndexItem, oldStringValue!, newStringValue!, differenceList); } - + if ( propertyB.PropertyType == typeof(int) ) { - var oldIntValue = (int)propertyInfoFromA.GetValue(sourceIndexItem, null)!; - var newIntValue = (int)propertyB.GetValue(updateObject, null)!; + var oldIntValue = ( int )propertyInfoFromA.GetValue(sourceIndexItem, null)!; + var newIntValue = ( int )propertyB.GetValue(updateObject, null)!; CompareInt(propertyB.Name, sourceIndexItem, oldIntValue, newIntValue, differenceList); } - + if ( propertyB.PropertyType == typeof(AppSettings.DatabaseTypeList) ) { - var oldListStringValue = ( AppSettings.DatabaseTypeList? ) propertyInfoFromA.GetValue(sourceIndexItem, null); - var newListStringValue = ( AppSettings.DatabaseTypeList? ) propertyB.GetValue(updateObject, null); + var oldListStringValue = ( AppSettings.DatabaseTypeList? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newListStringValue = ( AppSettings.DatabaseTypeList? )propertyB.GetValue(updateObject, null); CompareDatabaseTypeList(propertyB.Name, sourceIndexItem, oldListStringValue, newListStringValue, differenceList); } } - private static void CompareMultipleListDictionary(PropertyInfo propertyB, - PropertyInfo propertyInfoFromA, + private static void CompareMultipleListDictionary(PropertyInfo propertyB, + PropertyInfo propertyInfoFromA, AppSettings sourceIndexItem, object updateObject, List differenceList) { if ( propertyB.PropertyType == typeof(List) ) { - var oldListStringValue = ( List? ) propertyInfoFromA.GetValue(sourceIndexItem, null); - var newListStringValue = ( List? ) propertyB.GetValue(updateObject, null); + var oldListStringValue = ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newListStringValue = ( List? )propertyB.GetValue(updateObject, null); CompareListString(propertyB.Name, sourceIndexItem, oldListStringValue, newListStringValue, differenceList); } - + if ( propertyB.PropertyType == typeof(List) ) { - var oldKeyValuePairStringStringValue = (List?)propertyInfoFromA.GetValue(sourceIndexItem, null); - var newKeyValuePairStringStringValue = (List?)propertyB.GetValue(updateObject, null); - CompareKeyValuePairStringString(propertyB.Name, sourceIndexItem, oldKeyValuePairStringStringValue!, + var oldKeyValuePairStringStringValue = ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newKeyValuePairStringStringValue = ( List? )propertyB.GetValue(updateObject, null); + CompareKeyValuePairStringString(propertyB.Name, sourceIndexItem, oldKeyValuePairStringStringValue!, newKeyValuePairStringStringValue!, differenceList); } if ( propertyB.PropertyType == typeof(Dictionary>) ) { - var oldListPublishProfilesValue = ( Dictionary> ?) + var oldListPublishProfilesValue = ( Dictionary>? ) propertyInfoFromA.GetValue(sourceIndexItem, null); - var newListPublishProfilesValue = ( Dictionary> ?) + var newListPublishProfilesValue = ( Dictionary>? ) propertyB.GetValue(updateObject, null); CompareListPublishProfiles(propertyB.Name, sourceIndexItem, oldListPublishProfilesValue, newListPublishProfilesValue, differenceList); } - + if ( propertyB.PropertyType == typeof(Dictionary) ) { - var oldDictionaryValue = ( Dictionary ?) + var oldDictionaryValue = ( Dictionary? ) propertyInfoFromA.GetValue(sourceIndexItem, null); - var newDictionaryValue = ( Dictionary ?) + var newDictionaryValue = ( Dictionary? ) propertyB.GetValue(updateObject, null); CompareStringDictionary(propertyB.Name, sourceIndexItem, oldDictionaryValue, newDictionaryValue, differenceList); } } - private static void CompareStringDictionary(string propertyName, AppSettings sourceIndexItem, - Dictionary? oldDictionaryValue, + private static void CompareStringDictionary(string propertyName, AppSettings sourceIndexItem, + Dictionary? oldDictionaryValue, Dictionary? newDictionaryValue, List differenceList) { if ( oldDictionaryValue == null || newDictionaryValue?.Count == 0 ) return; if ( JsonSerializer.Serialize(oldDictionaryValue, - DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newDictionaryValue, - DefaultJsonSerializer.CamelCase) ) + DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newDictionaryValue, + DefaultJsonSerializer.CamelCase) ) { return; } - + sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDictionaryValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - private static void CompareKeyValuePairStringString(string propertyName, AppSettings sourceIndexItem, - List? oldKeyValuePairStringStringValue, + private static void CompareKeyValuePairStringString(string propertyName, AppSettings sourceIndexItem, + List? oldKeyValuePairStringStringValue, List? newKeyValuePairStringStringValue, List differenceList) { if ( oldKeyValuePairStringStringValue == null || - newKeyValuePairStringStringValue == null || - newKeyValuePairStringStringValue.Count == 0 ) + newKeyValuePairStringStringValue == null || + newKeyValuePairStringStringValue.Count == 0 ) { return; } if ( oldKeyValuePairStringStringValue.Equals( - newKeyValuePairStringStringValue) ) + newKeyValuePairStringStringValue) ) { return; } @@ -192,16 +191,16 @@ private static void CompareKeyValuePairStringString(string propertyName, AppSett /// oldDatabaseTypeList to compare with newDatabaseTypeList /// newDatabaseTypeList to compare with oldDatabaseTypeList /// list of different values - internal static void CompareDatabaseTypeList(string propertyName, AppSettings sourceIndexItem, - AppSettings.DatabaseTypeList? oldDatabaseTypeList, + internal static void CompareDatabaseTypeList(string propertyName, AppSettings sourceIndexItem, + AppSettings.DatabaseTypeList? oldDatabaseTypeList, AppSettings.DatabaseTypeList? newDatabaseTypeList, List differenceList) { - if (oldDatabaseTypeList == newDatabaseTypeList || newDatabaseTypeList == new AppSettings().DatabaseType) return; + if ( oldDatabaseTypeList == newDatabaseTypeList || newDatabaseTypeList == new AppSettings().DatabaseType ) return; sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDatabaseTypeList, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare List String /// @@ -210,21 +209,21 @@ internal static void CompareDatabaseTypeList(string propertyName, AppSettings so /// oldListStringValue to compare with newListStringValue /// newListStringValue to compare with oldListStringValue /// list of different values - internal static void CompareListString(string propertyName, AppSettings sourceIndexItem, + internal static void CompareListString(string propertyName, AppSettings sourceIndexItem, List? oldListStringValue, List? newListStringValue, List differenceList) { if ( oldListStringValue == null || newListStringValue?.Count == 0 ) return; if ( JsonSerializer.Serialize(oldListStringValue, - DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newListStringValue, - DefaultJsonSerializer.CamelCase) ) + DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newListStringValue, + DefaultJsonSerializer.CamelCase) ) { return; } - + sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListStringValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare List AppSettingsPublishProfiles /// @@ -233,18 +232,18 @@ internal static void CompareListString(string propertyName, AppSettings sourceIn /// oldListPublishValue to compare with newListPublishValue /// newListPublishValue to compare with oldListPublishValue /// list of different values - internal static void CompareListPublishProfiles(string propertyName, AppSettings sourceIndexItem, - Dictionary>? oldListPublishValue, + internal static void CompareListPublishProfiles(string propertyName, AppSettings sourceIndexItem, + Dictionary>? oldListPublishValue, Dictionary>? newListPublishValue, List differenceList) { if ( oldListPublishValue == null || newListPublishValue?.Count == 0 ) return; if ( oldListPublishValue.Equals(newListPublishValue) ) return; - + sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListPublishValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// Compare bool type /// @@ -253,7 +252,7 @@ internal static void CompareListPublishProfiles(string propertyName, AppSettings /// oldBoolValue to compare with newBoolValue /// oldBoolValue to compare with newBoolValue /// list of different values - internal static void CompareBool(string propertyName, AppSettings sourceIndexItem, bool? oldBoolValue, + internal static void CompareBool(string propertyName, AppSettings sourceIndexItem, bool? oldBoolValue, bool? newBoolValue, List differenceList) { if ( newBoolValue == null ) @@ -276,8 +275,8 @@ internal static void CompareBool(string propertyName, AppSettings sourceIndexIte /// oldStringValue to compare with newStringValue /// oldStringValue to compare with newStringValue /// list of different values - internal static void CompareString(string propertyName, AppSettings sourceIndexItem, - string oldStringValue, string newStringValue, + internal static void CompareString(string propertyName, AppSettings sourceIndexItem, + string oldStringValue, string newStringValue, List differenceList) { var newAppSettings = new AppSettings(); @@ -289,19 +288,19 @@ internal static void CompareString(string propertyName, AppSettings sourceIndexI } if ( oldStringValue == newStringValue || - ( string.IsNullOrEmpty(newStringValue) ) ) + ( string.IsNullOrEmpty(newStringValue) ) ) { return; } - + var propertyObject = sourceIndexItem.GetType().GetProperty(propertyName); - + propertyObject?.SetValue(sourceIndexItem, newStringValue, null); - + differenceList.Add(propertyName.ToLowerInvariant()); } - - + + /// /// Compare string type /// @@ -310,24 +309,24 @@ internal static void CompareString(string propertyName, AppSettings sourceIndexI /// oldStringValue to compare with newStringValue /// oldStringValue to compare with newStringValue /// list of different values - internal static void CompareInt(string propertyName, AppSettings sourceIndexItem, - int oldStringValue, int newStringValue, + internal static void CompareInt(string propertyName, AppSettings sourceIndexItem, + int oldStringValue, int newStringValue, List differenceList) { var newAppSettings = new AppSettings(); var defaultValue = GetPropertyValue(newAppSettings, propertyName) as int?; if ( newStringValue == defaultValue ) return; - - if (oldStringValue == newStringValue || newStringValue == 0) return; - + + if ( oldStringValue == newStringValue || newStringValue == 0 ) return; + var propertyObject = sourceIndexItem.GetType().GetProperty(propertyName); - + propertyObject?.SetValue(sourceIndexItem, newStringValue, null); - + differenceList.Add(propertyName.ToLowerInvariant()); } - + /// /// @see: https://stackoverflow.com/a/5508068 /// diff --git a/starsky/starsky.foundation.platform/Helpers/ArgsHelper.cs b/starsky/starsky.foundation.platform/Helpers/ArgsHelper.cs index d7ddd3995c..19562994de 100644 --- a/starsky/starsky.foundation.platform/Helpers/ArgsHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/ArgsHelper.cs @@ -15,7 +15,7 @@ namespace starsky.foundation.platform.Helpers public sealed class ArgsHelper { // Table of Content - + // -j > free // -k > free // -l > free @@ -45,7 +45,7 @@ public sealed class ArgsHelper // -n --name // -x --clean // --colorclass (no shorthand) - + /// /// Simple injection /// @@ -66,7 +66,7 @@ public ArgsHelper(AppSettings appSettings, IConsole? console = null) _console = console; } } - + /// /// Console abstraction, use this instead of Console /// @@ -76,7 +76,7 @@ public ArgsHelper(AppSettings appSettings, IConsole? console = null) /// AppSettings /// private readonly AppSettings _appSettings = new AppSettings(); - + /// /// Show debug information /// @@ -85,17 +85,17 @@ public ArgsHelper(AppSettings appSettings, IConsole? console = null) public static bool NeedVerbose(IReadOnlyList args) { var needDebug = false; - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--verbose", StringComparison.CurrentCultureIgnoreCase) - || args[arg].Equals("-v", StringComparison.CurrentCultureIgnoreCase) ) - && (arg + 1) != args.Count - && bool.TryParse(args[arg + 1], out var needDebugParsed)) + if ( ( args[arg].Equals("--verbose", StringComparison.CurrentCultureIgnoreCase) + || args[arg].Equals("-v", StringComparison.CurrentCultureIgnoreCase) ) + && ( arg + 1 ) != args.Count + && bool.TryParse(args[arg + 1], out var needDebugParsed) ) { needDebug = needDebugParsed; } - if ((args[arg].Equals("--verbose", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-v", StringComparison.CurrentCultureIgnoreCase) ) ) + if ( ( args[arg].Equals("--verbose", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-v", StringComparison.CurrentCultureIgnoreCase) ) ) { needDebug = true; } @@ -128,7 +128,7 @@ public static bool NeedVerbose(IReadOnlyList args) public readonly IEnumerable EnvNameList = new List { "app__DatabaseType","app__DatabaseConnection","app__StorageFolder","app__ThumbnailTempFolder", - "app__ExifToolPath", "app__Structure", "app__subpathrelative", "app__ExifToolImportXmpCreate", + "app__ExifToolPath", "app__Structure", "app__subpathrelative", "app__ExifToolImportXmpCreate", "app__TempFolder", "app__DependenciesFolder" }.AsReadOnly(); @@ -141,14 +141,14 @@ public void SetEnvironmentByArgs(IReadOnlyList args) var shortNameList = ShortNameList.ToArray(); var longNameList = LongNameList.ToArray(); var envNameList = EnvNameList.ToArray(); - for (var i = 0; i < ShortNameList.Count(); i++) + for ( var i = 0; i < ShortNameList.Count(); i++ ) { - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals(longNameList[i], StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals(shortNameList[i], StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count) + if ( ( args[arg].Equals(longNameList[i], StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals(shortNameList[i], StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { - Environment.SetEnvironmentVariable(envNameList[i],args[arg+1]); + Environment.SetEnvironmentVariable(envNameList[i], args[arg + 1]); } } } @@ -161,7 +161,7 @@ public void SetEnvironmentByArgs(IReadOnlyList args) public void SetEnvironmentToAppSettings() { if ( _appSettings == null ) throw new FieldAccessException("use with _appsettings"); - + var envNameList = EnvNameList.ToArray(); foreach ( var envUnderscoreName in envNameList ) { @@ -170,9 +170,9 @@ public void SetEnvironmentToAppSettings() if ( !string.IsNullOrEmpty(envValue) ) { var propertyObject = _appSettings.GetType().GetProperty(envName); - if(propertyObject == null) continue; + if ( propertyObject == null ) continue; var type = propertyObject.PropertyType; - + // for enums if ( propertyObject.PropertyType.IsEnum ) { @@ -180,14 +180,14 @@ public void SetEnvironmentToAppSettings() propertyObject.SetValue(_appSettings, envTypedObject, null); continue; } - + dynamic envTypedDynamic = Convert.ChangeType(envValue, type); propertyObject.SetValue(_appSettings, envTypedDynamic, null); } } } - - + + /// /// Based on args get the -h or --help commandline input /// @@ -196,23 +196,23 @@ public void SetEnvironmentToAppSettings() public static bool NeedHelp(IReadOnlyList args) { var needHelp = false; - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--help", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-h", StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count - && bool.TryParse(args[arg + 1], out var needHelp2) && needHelp2) + if ( ( args[arg].Equals("--help", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-h", StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count + && bool.TryParse(args[arg + 1], out var needHelp2) && needHelp2 ) { needHelp = true; } - if (args[arg].Equals("--help", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-h", StringComparison.CurrentCultureIgnoreCase) ) + if ( args[arg].Equals("--help", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-h", StringComparison.CurrentCultureIgnoreCase) ) { needHelp = true; } } return needHelp; } - + /// /// Show Help dialog /// @@ -224,34 +224,34 @@ public void NeedHelpShowDialog() { throw new FieldAccessException("use with _appSettings"); } - + _console.WriteLine("Starsky " + _appSettings.ApplicationType + " Cli ~ Help:"); _console.WriteLine("--help or -h == help (this window)"); - - switch (_appSettings.ApplicationType) + + switch ( _appSettings.ApplicationType ) { case AppSettings.StarskyAppType.Thumbnail: - + _console.WriteLine("-t == enable thumbnail (default true)"); _console.WriteLine("--path or -p == parameter: (string) ; " + - "'full path', only child items of the database folder are supported," + - "search and replace first part of the filename, '/', use '-p' for current directory "); + "'full path', only child items of the database folder are supported," + + "search and replace first part of the filename, '/', use '-p' for current directory "); _console.WriteLine("--subpath or -s == parameter: (string) ; relative path in the database"); _console.WriteLine("--subpathrelative or -g == Overwrite sub-path to use relative days to select a folder" + - ", use for example '1' to select yesterday. (structure is required)"); + ", use for example '1' to select yesterday. (structure is required)"); _console.WriteLine("-p, -s, -g == you need to select one of those tags"); - + _console.WriteLine("recursive is enabled by default"); break; case AppSettings.StarskyAppType.MetaThumbnail: _console.WriteLine("--path or -p == parameter: (string) ; " + - "'full path', only child items of the database folder are supported," + - "search and replace first part of the filename, '/', use '-p' for current directory "); + "'full path', only child items of the database folder are supported," + + "search and replace first part of the filename, '/', use '-p' for current directory "); _console.WriteLine("--subpath or -s == parameter: (string) ; relative path in the database"); _console.WriteLine("--subpathrelative or -g == Overwrite sub-path to use relative days to select a folder" + - ", use for example '1' to select yesterday. (structure is required)"); + ", use for example '1' to select yesterday. (structure is required)"); _console.WriteLine("-p, -s, -g == you need to select one of those tags"); - + _console.WriteLine("recursive is enabled by default"); break; case AppSettings.StarskyAppType.Admin: @@ -261,119 +261,113 @@ public void NeedHelpShowDialog() case AppSettings.StarskyAppType.Geo: // When this change please update ./readme.md _console.WriteLine("--path or -p == parameter: (string) ; " + - "without addition is current directory, full path (all locations are supported) "); + "without addition is current directory, full path (all locations are supported) "); _console.WriteLine("--subpath or -s == parameter: (string) ; relative path in the database "); _console.WriteLine("--subpathrelative or -g == Overwrite subpath to use relative days to select a folder" + - ", use for example '1' to select yesterday. (structure is required)"); + ", use for example '1' to select yesterday. (structure is required)"); _console.WriteLine("-p, -s, -g == you need to select one of those tags"); _console.WriteLine("--all or -a == overwrite reverse geotag location tags " + - "(default: false / ignore already taged files) "); + "(default: false / ignore already taged files) "); _console.WriteLine("--index or -i == parameter: (bool) ; gpx feature to index geo location, default true"); - break; + break; case AppSettings.StarskyAppType.WebHtml: // When this change please update ./readme.md _console.WriteLine("--path or -p == parameter: (string) ; full path (select a folder), " + - "use '-p' for current directory"); + "use '-p' for current directory"); _console.WriteLine("--name or -n == parameter: (string) ; name of blog item "); - break; + break; case AppSettings.StarskyAppType.Importer: // When this change please update ./readme.md _console.WriteLine("--path or -p == parameter: (string) ; full path"); _console.WriteLine(" can be an folder or file, use '-p' for current directory"); _console.WriteLine(" for multiple items use dot comma (;) " + - "to split and quotes (\") around the input string"); + "to split and quotes (\") around the input string"); _console.WriteLine("--move or -m == delete file after importing (default false / copy file)"); _console.WriteLine("--recursive or -r == Import Directory recursive " + - "(default: false / only the selected folder) "); - _console.WriteLine("--structure == overwrite app-settings with file-directory structure "+ - "based on exif and filename create datetime"); + "(default: false / only the selected folder) "); + _console.WriteLine("--structure == overwrite app-settings with file-directory structure " + + "based on exif and filename create datetime"); _console.WriteLine("--index or -i == parameter: (bool) ; indexing, false is always copy," + - " true is check if exist in db, default true"); + " true is check if exist in db, default true"); _console.WriteLine("--clean or -x == true is to add a xmp sidecar file for raws, default true"); _console.WriteLine("--colorclass == update color-class to this number value, default don't change"); - break; + break; case AppSettings.StarskyAppType.Sync: // When this change please update ./readme.md _console.WriteLine("--path or -p == parameter: (string) ; " + - "'full path', only child items of the database folder are supported," + - "search and replace first part of the filename, '/', use '-p' for current directory "); + "'full path', only child items of the database folder are supported," + + "search and replace first part of the filename, '/', use '-p' for current directory "); _console.WriteLine("--subpath or -s == parameter: (string) ; relative path in the database"); _console.WriteLine("--subpathrelative or -g == Overwrite sub-path to use relative days to select a folder" + - ", use for example '1' to select yesterday. (structure is required)"); + ", use for example '1' to select yesterday. (structure is required)"); _console.WriteLine("-p, -s, -g == you need to select one of those tags"); _console.WriteLine("--index or -i == parameter: (bool) ; enable indexing, default true"); _console.WriteLine("--thumbnail or -t == parameter: (bool) ; enable thumbnail, default false"); _console.WriteLine("--clean or -x == parameter: (bool) ; enable checks in thumbnail-temp-folder" + - " if thumbnails are needed, delete unused files"); + " if thumbnails are needed, delete unused files"); _console.WriteLine("--orphanfolder or -o == To delete files without a parent folder " + - "(heavy cpu usage), default false"); + "(heavy cpu usage), default false"); _console.WriteLine("--verbose or -v == verbose, more detailed info"); _console.WriteLine("--databasetype or -d == Overwrite EnvironmentVariable for DatabaseType"); _console.WriteLine("--basepath or -b == Overwrite EnvironmentVariable for StorageFolder"); _console.WriteLine("--connection or -c == Overwrite EnvironmentVariable for DatabaseConnection"); _console.WriteLine("--thumbnailtempfolder or -f == Overwrite EnvironmentVariable for ThumbnailTempFolder"); _console.WriteLine("--exiftoolpath or -e == Overwrite EnvironmentVariable for ExifToolPath"); - break; + break; } - + _console.WriteLine("--verbose or -v == verbose, more detailed info"); _console.WriteLine(" use -v -help to show settings: "); - - if (!_appSettings.IsVerbose()) return; - + + if ( !_appSettings.IsVerbose() ) return; + _console.WriteLine(string.Empty); _console.WriteLine("AppSettings: " + _appSettings.ApplicationType); - _console.WriteLine("Database Type (-d --databasetype) "+ _appSettings.DatabaseType); + _console.WriteLine("Database Type (-d --databasetype) " + _appSettings.DatabaseType); _console.WriteLine("DatabaseConnection (-c --connection) " + _appSettings.DatabaseConnection); _console.WriteLine($"StorageFolder (-b --basepath) {_appSettings.StorageFolder} "); _console.WriteLine($"ThumbnailTempFolder (-f --thumbnailtempfolder) {_appSettings.ThumbnailTempFolder} "); _console.WriteLine($"ExifToolPath (-e --exiftoolpath) {_appSettings.ExifToolPath} "); - _console.WriteLine("Structure (-u --structure) "+ _appSettings.Structure); - _console.WriteLine("CameraTimeZone "+ _appSettings.CameraTimeZone); + _console.WriteLine("Structure (-u --structure) " + _appSettings.Structure); + _console.WriteLine("CameraTimeZone " + _appSettings.CameraTimeZone); _console.WriteLine("Name " + _appSettings.Name); _console.WriteLine($"TempFolder {_appSettings.TempFolder} "); _console.WriteLine($"BaseDirectoryProject {_appSettings.BaseDirectoryProject} "); - _console.WriteLine("ExiftoolSkipDownloadOnStartup "+ _appSettings.ExiftoolSkipDownloadOnStartup); - _console.WriteLine("GeoFilesSkipDownloadOnStartup "+ _appSettings.GeoFilesSkipDownloadOnStartup); - - if ( !string.IsNullOrEmpty(_appSettings.ApplicationInsightsConnectionString) ) - { - _console.WriteLine($"ApplicationInsightsConnectionString {_appSettings.ApplicationInsightsConnectionString} "); - _console.WriteLine($"ApplicationInsightsDatabaseTracking {_appSettings.ApplicationInsightsDatabaseTracking} \n" + - $"ApplicationInsightsLog {_appSettings.ApplicationInsightsLog} "); - } + _console.WriteLine("ExiftoolSkipDownloadOnStartup " + _appSettings.ExiftoolSkipDownloadOnStartup); + _console.WriteLine("GeoFilesSkipDownloadOnStartup " + _appSettings.GeoFilesSkipDownloadOnStartup); + _console.WriteLine($"MaxDegreesOfParallelism {_appSettings.MaxDegreesOfParallelism} "); _console.Write("SyncIgnore "); foreach ( var rule in _appSettings.SyncIgnore ) _console.Write($"{rule}, "); _console.Write("\n"); - + _console.Write("ImportIgnore "); foreach ( var rule in _appSettings.ImportIgnore ) _console.Write($"{rule}, "); _console.Write("\n"); - - if ( _appSettings.ApplicationType == AppSettings.StarskyAppType.Importer) + + if ( _appSettings.ApplicationType == AppSettings.StarskyAppType.Importer ) _console.WriteLine("Create xmp on import (ExifToolImportXmpCreate): " + _appSettings.ExifToolImportXmpCreate); - - if ( _appSettings.ApplicationType == AppSettings.StarskyAppType.WebFtp) + + if ( _appSettings.ApplicationType == AppSettings.StarskyAppType.WebFtp ) _console.WriteLine("WebFtp " + _appSettings.WebFtp); if ( _appSettings.ApplicationType == AppSettings.StarskyAppType.Admin ) { _console.WriteLine("NoAccountLocalhost " + _appSettings.NoAccountLocalhost); } - + _console.WriteLine("-- Appsettings.json locations -- "); - + var machineName = Environment.MachineName.ToLowerInvariant(); - + _console.WriteLine("Config is read in this order: (latest is applied over lower numbers)"); - _console.WriteLine( $"1. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings.json")}"); - _console.WriteLine( $"2. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings.default.json")}"); - _console.WriteLine( $"3. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings.patch.json")}"); - _console.WriteLine( $"4. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings." + machineName + ".json")}"); - _console.WriteLine( $"5. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings." + machineName + ".patch.json")}"); - _console.WriteLine( $"6. Environment variable: app__appsettingspath: {Environment.GetEnvironmentVariable("app__appsettingspath")}"); + _console.WriteLine($"1. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings.json")}"); + _console.WriteLine($"2. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings.default.json")}"); + _console.WriteLine($"3. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings.patch.json")}"); + _console.WriteLine($"4. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings." + machineName + ".json")}"); + _console.WriteLine($"5. {Path.Combine(_appSettings.BaseDirectoryProject, "appsettings." + machineName + ".patch.json")}"); + _console.WriteLine($"6. Environment variable: app__appsettingspath: {Environment.GetEnvironmentVariable("app__appsettingspath")}"); _console.WriteLine("7. Specific environment variables for example app__storageFolder"); AppSpecificHelp(); @@ -385,29 +379,29 @@ private void AppSpecificHelp() { switch ( _appSettings.ApplicationType ) { - case AppSettings.StarskyAppType.WebHtml: + case AppSettings.StarskyAppType.WebHtml: _console.WriteLine($"Config for {_appSettings.ApplicationType}"); if ( _appSettings.PublishProfiles == null ) { break; } - + foreach ( var publishProfiles in _appSettings.PublishProfiles ) { - _console.WriteLine($"ID: {publishProfiles.Key}" ); + _console.WriteLine($"ID: {publishProfiles.Key}"); foreach ( var publishProfile in publishProfiles.Value ) { _console.WriteLine("--- " + - $"Path: {publishProfile.Path} " + - $"Append: {publishProfile.Append} " + - $"Copy: {publishProfile.Copy} " + - $"Folder: {publishProfile.Folder} " + - $"Prepend: {publishProfile.Prepend} " + - $"Template: {publishProfile.Template} " + - $"ContentType: {publishProfile.ContentType} " + - $"MetaData: {publishProfile.MetaData} " + - $"OverlayMaxWidth: {publishProfile.OverlayMaxWidth} " + - $"SourceMaxWidth: {publishProfile.SourceMaxWidth} "); + $"Path: {publishProfile.Path} " + + $"Append: {publishProfile.Append} " + + $"Copy: {publishProfile.Copy} " + + $"Folder: {publishProfile.Folder} " + + $"Prepend: {publishProfile.Prepend} " + + $"Template: {publishProfile.Template} " + + $"ContentType: {publishProfile.ContentType} " + + $"MetaData: {publishProfile.MetaData} " + + $"OverlayMaxWidth: {publishProfile.OverlayMaxWidth} " + + $"SourceMaxWidth: {publishProfile.SourceMaxWidth} "); } } break; @@ -423,11 +417,11 @@ private void ShowVersions() var version = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; _console.WriteLine($".NET Version - {version}"); _console.WriteLine($"Starsky Version - {_appSettings.AppVersion} " + - "- build at: " + - DateAssembly.GetBuildDate(Assembly.GetExecutingAssembly()).ToString( - new CultureInfo("nl-NL"))); + "- build at: " + + DateAssembly.GetBuildDate(Assembly.GetExecutingAssembly()).ToString( + new CultureInfo("nl-NL"))); } - + /// /// Default On /// Based on args get the -i or --index commandline input @@ -437,21 +431,21 @@ private void ShowVersions() public static bool GetIndexMode(IReadOnlyList args) { var isIndexMode = true; - - for (var arg = 0; arg < args.Count; arg++) + + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--index", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-i", StringComparison.CurrentCultureIgnoreCase) ) - && (arg + 1) != args.Count - && bool.TryParse(args[arg + 1], out var isIndexMode2)) + if ( ( args[arg].Equals("--index", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-i", StringComparison.CurrentCultureIgnoreCase) ) + && ( arg + 1 ) != args.Count + && bool.TryParse(args[arg + 1], out var isIndexMode2) ) { isIndexMode = isIndexMode2; } } - + return isIndexMode; } - + /// /// Get multiple path from args /// @@ -463,21 +457,21 @@ public List GetPathListFormArgs(IReadOnlyList args) { if ( _appSettings == null ) throw new FieldAccessException("use with _appSettings"); var path = GetUserInputPathFromArg(args); - + // To use only with -p or --path > current directory - if ( (args.Contains("-p") || args.Contains("--path") ) && (path == string.Empty || path[0] == "-"[0])) + if ( ( args.Contains("-p") || args.Contains("--path") ) && ( path == string.Empty || path[0] == "-"[0] ) ) { path = Directory.GetCurrentDirectory(); } // Ignore quotes at beginning: unescaped ^"|"$ - path = new Regex("^\"|\"$", + path = new Regex("^\"|\"$", RegexOptions.None, TimeSpan.FromMilliseconds(100)) .Replace(path, string.Empty); - + // split every dot comma but ignore escaped // non escaped: (? !string.IsNullOrWhiteSpace(p)).ToList(); } @@ -490,17 +484,17 @@ public List GetPathListFormArgs(IReadOnlyList args) private static string GetUserInputPathFromArg(IReadOnlyList args) { var path = string.Empty; - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--path", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-p", StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count ) + if ( ( args[arg].Equals("--path", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-p", StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { path = args[arg + 1]; } } return path; } - + /// /// Get the user input from -p or --password /// @@ -509,17 +503,17 @@ private static string GetUserInputPathFromArg(IReadOnlyList args) public static string GetUserInputPassword(IReadOnlyList args) { var path = string.Empty; - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--password", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-p", StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count ) + if ( ( args[arg].Equals("--password", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-p", StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { path = args[arg + 1]; } } return path; } - + /// /// Get output mode /// @@ -528,16 +522,16 @@ public static string GetUserInputPassword(IReadOnlyList args) public static ConsoleOutputMode GetConsoleOutputMode(IReadOnlyList args) { var outputMode = ConsoleOutputMode.Default; - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { if ( ( !args[arg].Equals("--output", StringComparison.CurrentCultureIgnoreCase) ) || - ( arg + 1 ) == args.Count ) continue; + ( arg + 1 ) == args.Count ) continue; var outputModeItem = args[arg + 1]; Enum.TryParse(outputModeItem, true, out outputMode); } return outputMode; } - + /// /// Get the user input from -n or --name /// @@ -546,17 +540,17 @@ public static ConsoleOutputMode GetConsoleOutputMode(IReadOnlyList args) public static string GetName(IReadOnlyList args) { var name = string.Empty; - for (var arg = 0; arg < args.Count; arg++) + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--name", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-n", StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count ) + if ( ( args[arg].Equals("--name", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-n", StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { name = args[arg + 1]; } } return name; } - + /// /// Get path from args /// @@ -569,9 +563,9 @@ public string GetPathFormArgs(IReadOnlyList args, bool dbStyle = true) if ( _appSettings == null ) throw new FieldAccessException("use with _appSettings"); var path = GetUserInputPathFromArg(args); - + // To use only with -p or --path > current directory - if ( (args.Contains("-p") || args.Contains("--path") ) && (path == string.Empty || path[0] == "-"[0])) + if ( ( args.Contains("-p") || args.Contains("--path") ) && ( path == string.Empty || path[0] == "-"[0] ) ) { var currentDirectory = Directory.GetCurrentDirectory(); if ( currentDirectory != _appSettings.BaseDirectoryProject ) @@ -580,15 +574,15 @@ public string GetPathFormArgs(IReadOnlyList args, bool dbStyle = true) if ( _appSettings.IsVerbose() ) Console.WriteLine($">> currentDirectory: {currentDirectory}"); } } - - if ( dbStyle) + + if ( dbStyle ) { path = _appSettings.FullPathToDatabaseStyle(path); } - + return path; } - + /// /// Get --subpath from args /// @@ -597,11 +591,11 @@ public string GetPathFormArgs(IReadOnlyList args, bool dbStyle = true) public static string GetSubPathFormArgs(IReadOnlyList args) { var subPath = "/"; - - for (var arg = 0; arg < args.Count; arg++) + + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--subpath", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-s", StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count) + if ( ( args[arg].Equals("--subpath", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-s", StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { subPath = args[arg + 1]; } @@ -617,30 +611,30 @@ public static string GetSubPathFormArgs(IReadOnlyList args) /// missing appSettings public int? GetRelativeValue(IReadOnlyList args) { - if (_appSettings == null) throw new FieldAccessException("use with _appSettings"); + if ( _appSettings == null ) throw new FieldAccessException("use with _appSettings"); var subPathRelative = string.Empty; - - for (int arg = 0; arg < args.Count; arg++) + + for ( int arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--subpathrelative", StringComparison.InvariantCultureIgnoreCase) || - args[arg].Equals("-g", StringComparison.InvariantCultureIgnoreCase) ) && (arg + 1) != args.Count) + if ( ( args[arg].Equals("--subpathrelative", StringComparison.InvariantCultureIgnoreCase) || + args[arg].Equals("-g", StringComparison.InvariantCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { subPathRelative = args[arg + 1]; } } - - if (string.IsNullOrWhiteSpace(subPathRelative)) return null; // null + + if ( string.IsNullOrWhiteSpace(subPathRelative) ) return null; // null if ( int.TryParse(subPathRelative, out var subPathInt) && subPathInt >= 1 ) { subPathInt *= -1; // always in the past } - + // Fallback for dates older than 24-11-1854 to avoid a exception. if ( subPathInt < -60000 ) return null; return subPathInt; } - + /// /// Know if --subpath or --path /// @@ -649,17 +643,17 @@ public static string GetSubPathFormArgs(IReadOnlyList args) public static bool IsSubPathOrPath(IReadOnlyList args) { // To use only with -p or --path > current directory - if ( args.Any(arg => (arg.Equals("--path", StringComparison.CurrentCultureIgnoreCase) || - arg.Equals("-p", StringComparison.CurrentCultureIgnoreCase) )) ) + if ( args.Any(arg => ( arg.Equals("--path", StringComparison.CurrentCultureIgnoreCase) || + arg.Equals("-p", StringComparison.CurrentCultureIgnoreCase) )) ) { return false; } - + // Detect if a input is a fullPath or a subPath. - for (int arg = 0; arg < args.Count; arg++) + for ( int arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--subpath", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-s", StringComparison.CurrentCultureIgnoreCase) ) && (arg + 1) != args.Count) + if ( ( args[arg].Equals("--subpath", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-s", StringComparison.CurrentCultureIgnoreCase) ) && ( arg + 1 ) != args.Count ) { return true; } @@ -677,7 +671,7 @@ public string SubPathOrPathValue(IReadOnlyList args) { return IsSubPathOrPath(args) ? GetSubPathFormArgs(args) : GetPathFormArgs(args); } - + /// /// --thumbnail bool /// @@ -686,20 +680,20 @@ public string SubPathOrPathValue(IReadOnlyList args) public static bool GetThumbnail(IReadOnlyList args) { var isThumbnail = true; - - for (var arg = 0; arg < args.Count; arg++) + + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--thumbnail", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-t", StringComparison.CurrentCultureIgnoreCase) ) - && (arg + 1) != args.Count && bool.TryParse(args[arg + 1], out var isThumbnail2)) + if ( ( args[arg].Equals("--thumbnail", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-t", StringComparison.CurrentCultureIgnoreCase) ) + && ( arg + 1 ) != args.Count && bool.TryParse(args[arg + 1], out var isThumbnail2) ) { isThumbnail = isThumbnail2; } } - + return isThumbnail; } - + /// /// Check for parent/sub items feature /// @@ -708,21 +702,21 @@ public static bool GetThumbnail(IReadOnlyList args) public bool GetOrphanFolderCheck(IReadOnlyList args) { var isOrphanFolderCheck = false; - - for (var arg = 0; arg < args.Count; arg++) + + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--orphanfolder", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-o", StringComparison.CurrentCultureIgnoreCase) ) - && (arg + 1) != args.Count && bool.TryParse(args[arg + 1], out var isOrphanFolderCheck2)) + if ( ( args[arg].Equals("--orphanfolder", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-o", StringComparison.CurrentCultureIgnoreCase) ) + && ( arg + 1 ) != args.Count && bool.TryParse(args[arg + 1], out var isOrphanFolderCheck2) ) { isOrphanFolderCheck = isOrphanFolderCheck2; } } - - if (_appSettings.IsVerbose()) Console.WriteLine(">> isOrphanFolderCheck " + isOrphanFolderCheck); + + if ( _appSettings.IsVerbose() ) Console.WriteLine(">> isOrphanFolderCheck " + isOrphanFolderCheck); return isOrphanFolderCheck; } - + /// /// Move files /// @@ -731,26 +725,26 @@ public bool GetOrphanFolderCheck(IReadOnlyList args) public static bool GetMove(IReadOnlyList args) { var getMove = false; - - for (var arg = 0; arg < args.Count; arg++) + + for ( var arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--move", StringComparison.CurrentCultureIgnoreCase) - || args[arg].Equals("-m", StringComparison.CurrentCultureIgnoreCase) ) - && (arg + 1) != args.Count && bool.TryParse(args[arg + 1], out var getMove2)) + if ( ( args[arg].Equals("--move", StringComparison.CurrentCultureIgnoreCase) + || args[arg].Equals("-m", StringComparison.CurrentCultureIgnoreCase) ) + && ( arg + 1 ) != args.Count && bool.TryParse(args[arg + 1], out var getMove2) ) { getMove = getMove2; continue; } - - if ((args[arg].Equals("--move", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-m", StringComparison.CurrentCultureIgnoreCase) ) ) + + if ( ( args[arg].Equals("--move", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-m", StringComparison.CurrentCultureIgnoreCase) ) ) { getMove = true; } } return getMove; } - + /// /// Get all --all true /// @@ -760,33 +754,33 @@ public static bool GetAll(IReadOnlyList args) { // default false var getAll = false; - - for (int arg = 0; arg < args.Count; arg++) + + for ( int arg = 0; arg < args.Count; arg++ ) { - if ((args[arg].Equals("--all", StringComparison.CurrentCultureIgnoreCase) || - args[arg].Equals("-a", StringComparison.CurrentCultureIgnoreCase) ) ) + if ( ( args[arg].Equals("--all", StringComparison.CurrentCultureIgnoreCase) || + args[arg].Equals("-a", StringComparison.CurrentCultureIgnoreCase) ) ) { getAll = true; } if ( ( !args[arg].Equals("--all", - StringComparison.CurrentCultureIgnoreCase) && - !args[arg].Equals("-a", - StringComparison.CurrentCultureIgnoreCase) ) || - ( arg + 1 ) == args.Count ) + StringComparison.CurrentCultureIgnoreCase) && + !args[arg].Equals("-a", + StringComparison.CurrentCultureIgnoreCase) ) || + ( arg + 1 ) == args.Count ) { continue; } if ( args[arg + 1].Equals("false", - StringComparison.CurrentCultureIgnoreCase) ) + StringComparison.CurrentCultureIgnoreCase) ) { getAll = false; } } return getAll; } - + /// /// Recursive scan for folders /// @@ -795,18 +789,18 @@ public static bool GetAll(IReadOnlyList args) public static bool NeedRecursive(IReadOnlyList args) { bool needRecursive = false; - + foreach ( var arg in args ) { - if ((arg.Equals("--recursive", StringComparison.CurrentCultureIgnoreCase) || - arg.Equals("-r", StringComparison.CurrentCultureIgnoreCase) ) ) + if ( ( arg.Equals("--recursive", StringComparison.CurrentCultureIgnoreCase) || + arg.Equals("-r", StringComparison.CurrentCultureIgnoreCase) ) ) { needRecursive = true; } } return needRecursive; } - + /// /// Need to remove caches /// @@ -816,11 +810,11 @@ public static bool NeedCleanup(IReadOnlyList args) { // -x --clean bool needCacheCleanup = false; - + foreach ( var arg in args ) { - if ((arg.Equals("--clean", StringComparison.CurrentCultureIgnoreCase) || - arg.Equals("-x", StringComparison.CurrentCultureIgnoreCase) ) ) + if ( ( arg.Equals("--clean", StringComparison.CurrentCultureIgnoreCase) || + arg.Equals("-x", StringComparison.CurrentCultureIgnoreCase) ) ) { needCacheCleanup = true; } @@ -834,22 +828,22 @@ public static bool NeedCleanup(IReadOnlyList args) /// input args /// number, but valid with colorClass public static int GetColorClass(IReadOnlyList args) - { + { // --colorclass var colorClass = -1; - - for (var arg = 0; arg < args.Count; arg++) + + for ( var arg = 0; arg < args.Count; arg++ ) { if ( !args[arg].Equals("--colorclass", - StringComparison.CurrentCultureIgnoreCase) || - ( arg + 1 ) == args.Count ) + StringComparison.CurrentCultureIgnoreCase) || + ( arg + 1 ) == args.Count ) { continue; } - + var colorClassString = args[arg + 1]; - var color = ColorClassParser.GetColorClass(colorClassString); - colorClass = (int) color; + var color = ColorClassParser.GetColorClass(colorClassString); + colorClass = ( int )color; } return colorClass; } diff --git a/starsky/starsky.foundation.platform/Helpers/Base64Helper.cs b/starsky/starsky.foundation.platform/Helpers/Base64Helper.cs index 3958276dbb..d80f046669 100644 --- a/starsky/starsky.foundation.platform/Helpers/Base64Helper.cs +++ b/starsky/starsky.foundation.platform/Helpers/Base64Helper.cs @@ -16,7 +16,7 @@ public static byte[] TryParse(string? inputString) { inputString ??= string.Empty; if ( inputString.Length % 4 != 0 || - !Base64Regex().IsMatch(inputString) ) + !Base64Regex().IsMatch(inputString) ) { return Array.Empty(); } diff --git a/starsky/starsky.foundation.platform/Helpers/ColorClassParser.cs b/starsky/starsky.foundation.platform/Helpers/ColorClassParser.cs index 6a8014bf98..f6823ae5fe 100644 --- a/starsky/starsky.foundation.platform/Helpers/ColorClassParser.cs +++ b/starsky/starsky.foundation.platform/Helpers/ColorClassParser.cs @@ -67,12 +67,12 @@ public enum Color public static Color GetColorClass(string colorclassString = "0") { - switch (colorclassString) + switch ( colorclassString ) { case "0": return Color.None; case "8": - return Color.Trash; + return Color.Trash; case "7": return Color.Extras; case "6": diff --git a/starsky/starsky.foundation.platform/Helpers/DateAssembly.cs b/starsky/starsky.foundation.platform/Helpers/DateAssembly.cs index a34536ebe9..ec416c53cd 100644 --- a/starsky/starsky.foundation.platform/Helpers/DateAssembly.cs +++ b/starsky/starsky.foundation.platform/Helpers/DateAssembly.cs @@ -24,7 +24,7 @@ public static DateTime GetBuildDate(Assembly assembly) { return new DateTime(0, DateTimeKind.Utc); } - + var value = attribute.InformationalVersion; return ParseBuildTime(value); } @@ -38,9 +38,9 @@ internal static DateTime ParseBuildTime(string value) return new DateTime(0, DateTimeKind.Utc); } value = value.Substring(index + buildVersionMetadataPrefix.Length); - return DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal, out var result) ? - result : + return DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal, out var result) ? + result : new DateTime(0, DateTimeKind.Utc); } } diff --git a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs index 0fb351c04d..165fec4c2f 100644 --- a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs @@ -14,53 +14,53 @@ public static class ExtensionRolesHelper /// /// Xmp sidecar file /// - private static readonly List ExtensionXmp = new List {"xmp"}; - + private static readonly List ExtensionXmp = new List { "xmp" }; + /// /// Meta.json sidecar files /// - private static readonly List ExtensionJsonSidecar = new List {"meta.json"}; + private static readonly List ExtensionJsonSidecar = new List { "meta.json" }; + - /// /// List of .jpg,.jpeg extensions /// - private static readonly List ExtensionJpg = new List {"jpg", "jpeg"}; + private static readonly List ExtensionJpg = new List { "jpg", "jpeg" }; /// /// Tiff based, tiff, including raws /// tiff, arw:sony, dng:adobe, nef:nikon, raf:fuji, cr2:canon, orf:olympus, rw2:panasonic, pef:pentax, /// Not supported due Image Processing Error x3f:sigma, crw:canon /// - private static readonly List ExtensionTiff = new List {"tiff", "arw", "dng", "nef", + private static readonly List ExtensionTiff = new List {"tiff", "arw", "dng", "nef", "raf", "cr2", "orf", "rw2", "pef"}; /// /// Bitmaps /// - private static readonly List ExtensionBmp = new List {"bmp"}; - + private static readonly List ExtensionBmp = new List { "bmp" }; + /// /// Gif based images /// - private static readonly List ExtensionGif = new List {"gif"}; - + private static readonly List ExtensionGif = new List { "gif" }; + /// /// PNG /// - private static readonly List ExtensionPng = new List {"png"}; - + private static readonly List ExtensionPng = new List { "png" }; + /// /// GPX, list of geo locations /// - private static readonly List ExtensionGpx = new List {"gpx"}; + private static readonly List ExtensionGpx = new List { "gpx" }; /// /// Mp4 Videos in h264 codex /// - private static readonly List ExtensionMp4 = new List {"mp4", "mov"}; + private static readonly List ExtensionMp4 = new List { "mp4", "mov" }; - private static readonly Dictionary> MapFileTypesToExtensionDictionary = + private static readonly Dictionary> MapFileTypesToExtensionDictionary = new Dictionary> { { @@ -89,7 +89,7 @@ public static class ExtensionRolesHelper }, }; - + public static ImageFormat MapFileTypesToExtension(string filename) { if ( string.IsNullOrEmpty(filename) ) return ImageFormat.unknown; @@ -103,7 +103,7 @@ public static ImageFormat MapFileTypesToExtension(string filename) { return ImageFormat.unknown; } - + var imageFormat = ImageFormat.unknown; foreach ( var matchValue in matchCollection.Select(p => p.Value) ) { @@ -205,7 +205,7 @@ public static bool IsExtensionSyncSupported(string filename) { return IsExtensionForce(filename.ToLowerInvariant(), ExtensionSyncSupportedList); } - + /// /// is this filename with extension a filetype that imageSharp can read/write /// @@ -215,7 +215,7 @@ public static bool IsExtensionThumbnailSupported(string? filename) { return IsExtensionForce(filename?.ToLowerInvariant(), ExtensionThumbSupportedList); } - + /// /// List of extension that are forced to use site car xmp files /// @@ -262,7 +262,7 @@ public static bool IsExtensionForceGpx(string? filename) { return IsExtensionForce(filename?.ToLowerInvariant(), ExtensionGpx); } - + /// /// Is the current file a sidecar file or not /// @@ -273,7 +273,7 @@ public static bool IsExtensionSidecar(string? filename) var sidecars = ExtensionXmp.Concat(ExtensionJsonSidecar).ToList(); return IsExtensionForce(filename?.ToLowerInvariant(), sidecars); } - + /// /// is this filename with extension a fileType that needs a item that is in the list /// @@ -287,9 +287,9 @@ private static bool IsExtensionForce(string? filename, List checkThisLis // without escaped values: // \.([0-9a-z]+)(?=[?#])|(\.)(?:[\w]+)$ - var matchCollection = new Regex("\\.([0-9a-z]+)(?=[?#])|(\\.)(?:[\\w]+)$", + var matchCollection = new Regex("\\.([0-9a-z]+)(?=[?#])|(\\.)(?:[\\w]+)$", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(200)).Matches(filename); - + if ( matchCollection.Count == 0 ) return false; foreach ( var matchValue in matchCollection.Select(p => p.Value) ) { @@ -303,10 +303,10 @@ private static bool IsExtensionForce(string? filename, List checkThisLis { return true; } - + return false; } - + /// /// Extension must be three letters /// @@ -318,24 +318,24 @@ public static string ReplaceExtensionWithXmp(string? filename) { return string.Empty; } - + // without escaped values: // \.([0-9a-z]+)(?=[?#])|(\.)(?:[\w]+)$ - var matchCollection = new Regex("\\.([0-9a-z]+)(?=[?#])|(\\.)(?:[\\w]+)$", + var matchCollection = new Regex("\\.([0-9a-z]+)(?=[?#])|(\\.)(?:[\\w]+)$", RegexOptions.None, TimeSpan.FromMilliseconds(100)).Matches(filename); - + if ( matchCollection.Count == 0 ) return string.Empty; foreach ( Match match in matchCollection ) { if ( match.Value.Length < 2 ) continue; // Extension must be three letters // removed: ExtensionForceXmpUseList.Contains(match.Value.Remove(0, 1).ToLowerInvariant()) && - if ( filename.Length >= match.Index + 4 ) + if ( filename.Length >= match.Index + 4 ) { var matchValue = filename.Substring(0, match.Index + 4).ToCharArray(); - matchValue[match.Index+1] = 'x'; - matchValue[match.Index+2] = 'm'; - matchValue[match.Index+3] = 'p'; + matchValue[match.Index + 1] = 'x'; + matchValue[match.Index + 2] = 'm'; + matchValue[match.Index + 3] = 'p'; return new string(matchValue); } } @@ -360,24 +360,24 @@ public enum ImageFormat // Sidecar files xmp = 30, - + /// /// Extension: .meta.json /// - meta_json = 31, - + meta_json = 31, + // documents gpx = 40, pdf = 41, // video mp4 = 50, - + // archives zip = 60 } - + [SuppressMessage("ReSharper", "MustUseReturnValue")] private static byte[] ReadBuffer(Stream stream, int size) { @@ -395,7 +395,7 @@ private static byte[] ReadBuffer(Stream stream, int size) return buffer; } - + /// /// Get the format of the image by looking the first bytes /// @@ -420,8 +420,8 @@ public static ImageFormat GetImageFormat(byte[] bytes) // on posix: 'od -t x1 -N 10 file.mp4' var bmp = Encoding.ASCII.GetBytes("BM"); // BMP var gif = Encoding.ASCII.GetBytes("GIF"); // GIF - var png = new byte[] {137, 80, 78, 71}; // PNG - var pdf = new byte[] {37, 80, 68, 70, 45}; // pdf + var png = new byte[] { 137, 80, 78, 71 }; // PNG + var pdf = new byte[] { 37, 80, 68, 70, 45 }; // pdf if ( bmp.SequenceEqual(bytes.Take(bmp.Length)) ) return ImageFormat.bmp; @@ -435,18 +435,18 @@ public static ImageFormat GetImageFormat(byte[] bytes) if ( GetImageFormatTiff(bytes) != null ) return ImageFormat.tiff; if ( GetImageFormatJpeg(bytes) != null ) return ImageFormat.jpg; - + if ( GetImageFormatXmp(bytes) != null ) return ImageFormat.xmp; if ( GetImageFormatGpx(bytes) != null ) return ImageFormat.gpx; if ( GetImageFormatMpeg4(bytes) != null ) return ImageFormat.mp4; - + if ( pdf.SequenceEqual(bytes.Take(pdf.Length)) ) return ImageFormat.pdf; - + if ( GetImageFormatZip(bytes) != null ) return ImageFormat.zip; - + if ( GetImageFormatMetaJson(bytes) != null ) return ImageFormat.meta_json; return ImageFormat.unknown; @@ -455,42 +455,42 @@ public static ImageFormat GetImageFormat(byte[] bytes) private static ImageFormat? GetImageFormatMetaJson(byte[] bytes) { var metaJsonUnix = new byte[] { - 123, 10, 32, 32, 34, 36, 105, 100, 34, 58, 32, 34, 104, 116, 116, - 112, 115, 58, 47, 47, 100, 111, 99, 115, 46, 113, 100, 114, 97, - 119, 46, 110, 108, 47, 115, 99, 104, 101, 109, 97, 47, 109, 101, - 116, 97, 45, 100, 97, 116, 97, 45, 99, 111, 110, 116, 97, 105, 110, + 123, 10, 32, 32, 34, 36, 105, 100, 34, 58, 32, 34, 104, 116, 116, + 112, 115, 58, 47, 47, 100, 111, 99, 115, 46, 113, 100, 114, 97, + 119, 46, 110, 108, 47, 115, 99, 104, 101, 109, 97, 47, 109, 101, + 116, 97, 45, 100, 97, 116, 97, 45, 99, 111, 110, 116, 97, 105, 110, 101, 114, 46, 106, 115, 111, 110, 34, 44 }; // or : { \n "$id": "https://docs.qdraw.nl/schema/meta-data-container.json", - + var metaJsonWindows = new byte[] { // 13 is CR - 123, 13, 10, 32, 32, 34, 36, 105, 100, 34, 58, 32, 34, 104, 116, - 116, 112, 115, 58, 47, 47, 100, 111, 99, 115, 46, 113, 100, 114, - 97, 119, 46, 110, 108, 47, 115, 99, 104, 101, 109, 97, 47, 109, 101, - 116, 97, 45, 100, 97, 116, 97, 45, 99, 111, 110, 116, 97, 105, 110, + 123, 13, 10, 32, 32, 34, 36, 105, 100, 34, 58, 32, 34, 104, 116, + 116, 112, 115, 58, 47, 47, 100, 111, 99, 115, 46, 113, 100, 114, + 97, 119, 46, 110, 108, 47, 115, 99, 104, 101, 109, 97, 47, 109, 101, + 116, 97, 45, 100, 97, 116, 97, 45, 99, 111, 110, 116, 97, 105, 110, 101, 114, 46, 106, 115, 111, 110, 34 }; - + if ( metaJsonUnix.SequenceEqual(bytes.Take(metaJsonUnix.Length)) ) return ImageFormat.meta_json; - + if ( metaJsonWindows.SequenceEqual(bytes.Take(metaJsonWindows.Length)) ) return ImageFormat.meta_json; - + return null; } private static ImageFormat? GetImageFormatTiff(byte[] bytes) { - var tiff = new byte[] {73, 73, 42}; // TIFF - var tiff2 = new byte[] {77, 77, 42}; // TIFF - var dng = new byte[] {77, 77, 0}; // DNG? //0 - var olympusRaw = new byte[] {73, 73, 82}; - var fujiFilmRaw = new byte[] {70, 85, 74}; - var panasonicRaw = new byte[] {73, 73, 85, 0}; - + var tiff = new byte[] { 73, 73, 42 }; // TIFF + var tiff2 = new byte[] { 77, 77, 42 }; // TIFF + var dng = new byte[] { 77, 77, 0 }; // DNG? //0 + var olympusRaw = new byte[] { 73, 73, 82 }; + var fujiFilmRaw = new byte[] { 70, 85, 74 }; + var panasonicRaw = new byte[] { 73, 73, 85, 0 }; + if ( tiff.SequenceEqual(bytes.Take(tiff.Length)) ) return ImageFormat.tiff; @@ -499,103 +499,103 @@ public static ImageFormat GetImageFormat(byte[] bytes) if ( dng.SequenceEqual(bytes.Take(dng.Length)) ) return ImageFormat.tiff; - + if ( olympusRaw.SequenceEqual(bytes.Take(olympusRaw.Length)) ) return ImageFormat.tiff; - + if ( fujiFilmRaw.SequenceEqual(bytes.Take(fujiFilmRaw.Length)) ) return ImageFormat.tiff; - + if ( panasonicRaw.SequenceEqual(bytes.Take(panasonicRaw.Length)) ) return ImageFormat.tiff; - + return null; } private static ImageFormat? GetImageFormatMpeg4(byte[] bytes) { - var fTypMp4 = new byte[] {102, 116, 121, 112}; // 00 00 00 [skip this byte] - // 66 74 79 70 QuickTime Container 3GG, 3GP, 3G2 FLV - + var fTypMp4 = new byte[] { 102, 116, 121, 112 }; // 00 00 00 [skip this byte] + // 66 74 79 70 QuickTime Container 3GG, 3GP, 3G2 FLV + if ( fTypMp4.SequenceEqual(bytes.Skip(4).Take(fTypMp4.Length)) ) return ImageFormat.mp4; - - var fTypIsoM = new byte[] {102, 116, 121, 112, 105, 115, 111, 109}; + + var fTypIsoM = new byte[] { 102, 116, 121, 112, 105, 115, 111, 109 }; if ( fTypIsoM.SequenceEqual(bytes.Take(fTypIsoM.Length)) ) return ImageFormat.xmp; - + return null; } private static ImageFormat? GetImageFormatGpx(byte[] bytes) { - var gpx = new byte[] {60, 103, 112}; // bytes) { - var zip = new byte[] {80, 75, 3, 4}; - + var zip = new byte[] { 80, 75, 3, 4 }; + if ( zip.SequenceEqual(bytes.Take(zip.Length)) ) return ImageFormat.zip; - + return null; } - + private static ImageFormat? GetImageFormatXmp(byte[] bytes) { var xmp = Encoding.ASCII.GetBytes(" /// Convert Hex Value to byte array /// diff --git a/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelper.cs b/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelper.cs index d79373817f..d415a5297e 100644 --- a/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelper.cs @@ -16,7 +16,7 @@ public static string GenerateSlug(string phrase, bool allowUnderScore = false, var text = toLowerCase ? phrase.ToLowerInvariant() : phrase; text = GenerateSlugHelperStaticRegex.CleanReplaceInvalidCharacters(text, allowAtSign, allowUnderScore); text = GenerateSlugHelperStaticRegex.CleanSpace(text); - + text = text.Substring(0, text.Length <= 65 ? text.Length : 65).Trim(); // cut and trim text = GenerateSlugHelperStaticRegex.ReplaceSpaceWithHyphen(text); text = text.Trim('-'); // remove trailing hyphens diff --git a/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelperStaticRegex.cs b/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelperStaticRegex.cs index be188b9c05..dc2ae600df 100644 --- a/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelperStaticRegex.cs +++ b/starsky/starsky.foundation.platform/Helpers/GenerateSlugHelperStaticRegex.cs @@ -14,7 +14,7 @@ public static partial class GenerateSlugHelperStaticRegex RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 200)] private static partial Regex CleanIncludingLowercaseAndAtSignRegex(); - + /// /// Clean including _ regex (no @) /// Regex.Replace (pre compiled regex) @@ -25,7 +25,7 @@ public static partial class GenerateSlugHelperStaticRegex RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 200)] private static partial Regex CleanIncludingLowercaseRegex(); - + /// /// Clean including _ regex (no @) /// Regex.Replace (pre compiled regex) @@ -36,14 +36,14 @@ public static partial class GenerateSlugHelperStaticRegex RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 200)] private static partial Regex CleanIncludingAtRegex(); - + /// /// Clean default regex (without _ and @) /// Regex.Replace (pre compiled regex) /// /// Regex object [GeneratedRegex( - @"[^a-zA-Z0-9\s-]", + @"[^a-zA-Z0-9\s-]", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 200)] private static partial Regex CleanDefaultRegex(); @@ -65,18 +65,18 @@ public static string CleanReplaceInvalidCharacters(string text, bool allowAtSign return CleanDefaultRegex().Replace(text, string.Empty); } - + /// /// Space + regex /// Regex.Replace (pre compiled regex) /// /// Regex object [GeneratedRegex( - @"\s+", + @"\s+", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 200)] private static partial Regex SpacePlusRegex(); - + /// /// Remove multiple spaces and trim spaces at begin and end /// @@ -84,20 +84,20 @@ public static string CleanReplaceInvalidCharacters(string text, bool allowAtSign /// cleaned text public static string CleanSpace(string text) { - return SpacePlusRegex().Replace(text, " ").Trim(); + return SpacePlusRegex().Replace(text, " ").Trim(); } - + /// /// Space regex /// Regex.Replace (pre compiled regex) /// /// Regex object [GeneratedRegex( - @"\s", + @"\s", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 200)] private static partial Regex SpaceRegex(); - + /// /// Replace space with hyphen /// --- replace is for example: "test[space]-[space]test"; @@ -108,6 +108,6 @@ public static string ReplaceSpaceWithHyphen(string text) { return SpaceRegex() .Replace(text, "-") - .Replace("---", "-"); + .Replace("---", "-"); } } diff --git a/starsky/starsky.foundation.platform/Helpers/HashSetHelper.cs b/starsky/starsky.foundation.platform/Helpers/HashSetHelper.cs index 89271c7ce6..83193aa7e8 100644 --- a/starsky/starsky.foundation.platform/Helpers/HashSetHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/HashSetHelper.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -41,12 +40,12 @@ public static HashSet StringToHashSet(string inputKeywords) keywordList = keywordList.Select(t => t.Trim()).ToArray(); var keywordsHashSet = new HashSet(from x in keywordList - select x); + select x); return keywordsHashSet; } - + /// /// To replace: 'test,fake' with 'test, fake' - Regex /// unescaped regex: (,(?=\S)|:) @@ -58,7 +57,7 @@ public static HashSet StringToHashSet(string inputKeywords) RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 200)] private static partial Regex SingleCommaWithCommaWithSpaceRegex(); - + /// /// To replace: 'test,fake' with 'test, fake' /// @@ -116,7 +115,7 @@ internal static string ListToString(List? listKeywords) var toBeAddedKeywordsStringBuilder = new StringBuilder(); foreach ( var keyword in listKeywords.Where(keyword => - !string.IsNullOrWhiteSpace(keyword)) ) + !string.IsNullOrWhiteSpace(keyword)) ) { if ( keyword != listKeywords.LastOrDefault() ) { diff --git a/starsky/starsky.foundation.platform/Helpers/JsonSidecarLocation.cs b/starsky/starsky.foundation.platform/Helpers/JsonSidecarLocation.cs index d9d9a6d76d..84774e8047 100644 --- a/starsky/starsky.foundation.platform/Helpers/JsonSidecarLocation.cs +++ b/starsky/starsky.foundation.platform/Helpers/JsonSidecarLocation.cs @@ -8,7 +8,7 @@ public static string JsonLocation(string subPath) var parentDirectory = subPath.Replace(fileName, string.Empty); return JsonLocation(parentDirectory, fileName); } - + /// /// Get the jsonSubPath `parentDir/.starsky.filename.ext.json` /// @@ -18,7 +18,7 @@ public static string JsonLocation(string subPath) public static string JsonLocation(string parentDirectory, string fileName) { return PathHelper.AddSlash(parentDirectory) + ".starsky." + fileName - + ".json"; + + ".json"; } } } diff --git a/starsky/starsky.foundation.platform/Helpers/PathHelper.cs b/starsky/starsky.foundation.platform/Helpers/PathHelper.cs index 2559d0f776..018b3cfeb7 100644 --- a/starsky/starsky.foundation.platform/Helpers/PathHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/PathHelper.cs @@ -52,7 +52,7 @@ public static string GetFileName(string? filePath) // remove latest backslash if ( basePath.Substring(basePath.Length - 1, 1) == - Path.DirectorySeparatorChar.ToString() ) + Path.DirectorySeparatorChar.ToString() ) { basePath = basePath.Substring(0, basePath.Length - 1); } @@ -94,7 +94,7 @@ public static string AddBackslash(string thumbnailTempFolder) if ( string.IsNullOrWhiteSpace(thumbnailTempFolder) ) return thumbnailTempFolder; if ( thumbnailTempFolder.Substring(thumbnailTempFolder.Length - 1, - 1) != Path.DirectorySeparatorChar.ToString() ) + 1) != Path.DirectorySeparatorChar.ToString() ) { thumbnailTempFolder += Path.DirectorySeparatorChar.ToString(); } diff --git a/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs b/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs index 0a2993f6a4..292e1e5b75 100644 --- a/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs @@ -14,23 +14,23 @@ public static async Task SetEnvPortAspNetUrlsAndSetDefault(string[] args, string { return; } - + SetEnvPortAspNetUrls(args); SetDefaultAspNetCoreUrls(args); } - + internal static async Task SkipForAppSettingsJsonFile(string appSettingsPath) { var appContainer = await ReadAppSettings.Read(appSettingsPath); if ( appContainer?.Kestrel?.Endpoints?.Http?.Url == null && - appContainer?.Kestrel?.Endpoints?.Https?.Url == null ) + appContainer?.Kestrel?.Endpoints?.Https?.Url == null ) { return false; } - + Console.WriteLine("Kestrel Endpoints are set in appsettings.json, " + - "this results in skip setting the PORT and default " + - "ASPNETCORE_URLS environment variable"); + "this results in skip setting the PORT and default " + + "ASPNETCORE_URLS environment variable"); return true; } @@ -38,9 +38,9 @@ internal static void SetEnvPortAspNetUrls(IEnumerable args) { // Set port from environment variable var portString = Environment.GetEnvironmentVariable("PORT"); - - if (args.Contains("--urls") || string.IsNullOrEmpty(portString) - || !int.TryParse(portString, out var port)) return; + + if ( args.Contains("--urls") || string.IsNullOrEmpty(portString) + || !int.TryParse(portString, out var port) ) return; SetEnvironmentVariableForPort(port); } @@ -49,14 +49,14 @@ internal static void SetEnvPortAspNetUrls(IEnumerable args) private static void SetEnvironmentVariableForPort(int port) { Console.WriteLine($"Set port from environment variable: {port} " + - $"\nPro tip: Its recommended to use a https proxy like nginx or traefik"); + $"\nPro tip: Its recommended to use a https proxy like nginx or traefik"); Environment.SetEnvironmentVariable("ASPNETCORE_URLS", $"http://*:{port}"); } internal static void SetDefaultAspNetCoreUrls(IEnumerable args) { var aspNetCoreUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); - if (args.Contains("--urls") || !string.IsNullOrEmpty(aspNetCoreUrls)) return; + if ( args.Contains("--urls") || !string.IsNullOrEmpty(aspNetCoreUrls) ) return; Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "http://localhost:4000;https://localhost:4001"); } } diff --git a/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs b/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs index e0ed2ad63f..664f357657 100644 --- a/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs +++ b/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs @@ -14,12 +14,12 @@ public static class ReadAppSettings { return new AppContainerAppSettings(); } - + using ( var openStream = File.OpenRead(path) ) { var result = await JsonSerializer.DeserializeAsync( - openStream, DefaultJsonSerializer.NoNamingPolicy); - + openStream, DefaultJsonSerializer.NoNamingPolicyBoolAsString); + return result; } } diff --git a/starsky/starsky.foundation.platform/Helpers/RetryHelper.cs b/starsky/starsky.foundation.platform/Helpers/RetryHelper.cs index eb8837a53d..5b76ace849 100644 --- a/starsky/starsky.foundation.platform/Helpers/RetryHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/RetryHelper.cs @@ -29,24 +29,24 @@ public static T Do( var exceptions = new List(); - for (int attempted = 0; attempted < maxAttemptCount; attempted++) + for ( int attempted = 0; attempted < maxAttemptCount; attempted++ ) { try { - if (attempted > 0) + if ( attempted > 0 ) { Thread.Sleep(retryInterval); } return action(); } - catch (Exception ex) + catch ( Exception ex ) { exceptions.Add(ex); } } throw new AggregateException(exceptions); } - + /// /// Retry when Exception happens with the async await pattern /// @@ -57,7 +57,7 @@ public static T Do( /// value of function /// when lower or eq than 0 public static Task DoAsync( - Func> operation, TimeSpan delay, int maxAttemptCount = 3 ) + Func> operation, TimeSpan delay, int maxAttemptCount = 3) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxAttemptCount); @@ -72,14 +72,14 @@ private static async Task DoAsyncWorker( { try { - if (attempted > 0) + if ( attempted > 0 ) { // Thread.Sleep await Task.Delay(delay); } return await operation(); } - catch (Exception ex) + catch ( Exception ex ) { exceptions.Add(ex); } diff --git a/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs b/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs index 786dd79dda..26db9cce03 100644 --- a/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs +++ b/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs @@ -22,7 +22,7 @@ public static async Task FirstStepToAddSingleton(ServiceColle services.ConfigurePoCo(configurationRoot.GetSection("App")); return services; } - + /// /// Default appSettings.json to builder /// @@ -33,21 +33,21 @@ public static async Task AppSettingsToBuilder(string[]? args var builder = new ConfigurationBuilder(); var settings = await MergeJsonFiles(appSettings.BaseDirectoryProject); - + // Make sure is wrapped in a AppContainer app - var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new AppContainerAppSettings{ App = settings}); + var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new AppContainerAppSettings { App = settings }); builder .AddJsonStream(new MemoryStream(utf8Bytes)) // overwrite envs // current dir gives problems on linux arm .AddEnvironmentVariables(); - + if ( args != null ) { builder.AddCommandLine(args); - } - + } + return builder.Build(); } @@ -88,9 +88,9 @@ internal static async Task MergeJsonFiles(string baseDirectoryProje { return new AppSettings(); } - + var appSetting = appSettingsList.FirstOrDefault()!; - + for ( var i = 1; i < appSettingsList.Count; i++ ) { var currentAppSetting = appSettingsList[i]; @@ -111,10 +111,10 @@ public static AppSettings ConfigurePoCoAppSettings(IServiceCollection services, { // configs services.ConfigurePoCo(configuration.GetSection("App")); - + // Need to rebuild for AppSettings var serviceProvider = services.BuildServiceProvider(); - + return serviceProvider.GetRequiredService(); } diff --git a/starsky/starsky.foundation.platform/Helpers/Sha256.cs b/starsky/starsky.foundation.platform/Helpers/Sha256.cs index 9ff0f886cd..518de07951 100644 --- a/starsky/starsky.foundation.platform/Helpers/Sha256.cs +++ b/starsky/starsky.foundation.platform/Helpers/Sha256.cs @@ -10,7 +10,7 @@ public static string ComputeSha256(string input) { return string.IsNullOrEmpty(input) ? string.Empty : ComputeSha256(Encoding.UTF8.GetBytes(input)); } - + public static string ComputeSha256(byte[]? input) { if ( input == null || input.Length == 0 ) return string.Empty; diff --git a/starsky/starsky.foundation.platform/Helpers/StopWatchLogger.cs b/starsky/starsky.foundation.platform/Helpers/StopWatchLogger.cs index 8bc598ab85..1257975d34 100644 --- a/starsky/starsky.foundation.platform/Helpers/StopWatchLogger.cs +++ b/starsky/starsky.foundation.platform/Helpers/StopWatchLogger.cs @@ -12,7 +12,7 @@ public StopWatchLogger(IWebLogger logger) { _logger = logger; } - + public static Stopwatch StartUpdateReplaceStopWatch() { var stopWatch = new Stopwatch(); @@ -28,10 +28,10 @@ public void StopUpdateReplaceStopWatch(string name, string f, bool collections, stopwatch.Stop(); } _logger.LogInformation($"[{name}] f: {f} Stopwatch response collections: " + - $"{collections} {DateTime.UtcNow} duration: {stopwatch.Elapsed.TotalMilliseconds} ms or:" + - $" {stopwatch.Elapsed.TotalSeconds} sec"); + $"{collections} {DateTime.UtcNow} duration: {stopwatch.Elapsed.TotalMilliseconds} ms or:" + + $" {stopwatch.Elapsed.TotalSeconds} sec"); } } - + } diff --git a/starsky/starsky.foundation.platform/Helpers/StringHelper.cs b/starsky/starsky.foundation.platform/Helpers/StringHelper.cs index 06e0bdb08d..eeec46af9c 100644 --- a/starsky/starsky.foundation.platform/Helpers/StringHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/StringHelper.cs @@ -4,10 +4,10 @@ public static class StringHelper { public static string AsciiNullReplacer(string newStringValue) { - return (newStringValue == "\\0" || newStringValue == "\\\\0") ? string.Empty : newStringValue; + return ( newStringValue == "\\0" || newStringValue == "\\\\0" ) ? string.Empty : newStringValue; } public static readonly string AsciiNullChar = "\\\\0"; - + } } diff --git a/starsky/starsky.foundation.platform/Helpers/ValidateLocation.cs b/starsky/starsky.foundation.platform/Helpers/ValidateLocation.cs index 4aadf1af45..2b6b592e8c 100644 --- a/starsky/starsky.foundation.platform/Helpers/ValidateLocation.cs +++ b/starsky/starsky.foundation.platform/Helpers/ValidateLocation.cs @@ -8,23 +8,23 @@ public static class ValidateLocation { public static bool ValidateLatitudeLongitude(double latitude, double longitude) { - var latitudeValue = Math.Round(latitude,6).ToString(CultureInfo.InvariantCulture); - var longitudeValue = Math.Round(longitude,6).ToString(CultureInfo.InvariantCulture); + var latitudeValue = Math.Round(latitude, 6).ToString(CultureInfo.InvariantCulture); + var longitudeValue = Math.Round(longitude, 6).ToString(CultureInfo.InvariantCulture); // un-escaped: ^[+-]?(([1-8]?[0-9])(\.[0-9]{1,6})?|90(\.0{1,6})?)$ - var latitudeRegex = + var latitudeRegex = new Regex( - "^[+-]?(([1-8]?[0-9])(\\.[0-9]{1,6})?|90(\\.0{1,6})?)$", + "^[+-]?(([1-8]?[0-9])(\\.[0-9]{1,6})?|90(\\.0{1,6})?)$", RegexOptions.None, TimeSpan.FromMilliseconds(100)); - + // un-escaped ^[+-]?((([1-9]?[0-9]|1[0-7][0-9])(\.[0-9]{1,6})?)|180(\.0{1,6})?)$ var longitudeRegex = new Regex( - "^[+-]?((([1-9]?[0-9]|1[0-7][0-9])(\\.[0-9]{1,6})?)|180(\\.0{1,6})?)$", + "^[+-]?((([1-9]?[0-9]|1[0-7][0-9])(\\.[0-9]{1,6})?)|180(\\.0{1,6})?)$", RegexOptions.None, TimeSpan.FromMilliseconds(100)); return latitudeRegex.IsMatch(latitudeValue) && - longitudeRegex.IsMatch(longitudeValue); + longitudeRegex.IsMatch(longitudeValue); } } diff --git a/starsky/starsky.foundation.platform/JsonConverter/DefaultJsonSerializer.cs b/starsky/starsky.foundation.platform/JsonConverter/DefaultJsonSerializer.cs index 4da9542e03..cee19d4505 100644 --- a/starsky/starsky.foundation.platform/JsonConverter/DefaultJsonSerializer.cs +++ b/starsky/starsky.foundation.platform/JsonConverter/DefaultJsonSerializer.cs @@ -16,7 +16,7 @@ public static class DefaultJsonSerializer AllowTrailingCommas = true, WriteIndented = false }; - + /// /// Write with enters in output /// @@ -28,12 +28,12 @@ public static class DefaultJsonSerializer AllowTrailingCommas = true, WriteIndented = true }; - + /// - /// Please use CamelCase if possible - /// Bool is written as quoted string + /// PascalCase (No Naming policy) + /// Bool is written as quoted string "true" or "false" /// - public static JsonSerializerOptions NoNamingPolicy => + public static JsonSerializerOptions NoNamingPolicyBoolAsString => new JsonSerializerOptions { PropertyNamingPolicy = null, @@ -41,10 +41,21 @@ public static class DefaultJsonSerializer PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, WriteIndented = true, - Converters = - { - new JsonBoolQuotedConverter(), - }, + Converters = { new JsonBoolQuotedConverter(), }, + }; + + /// + /// PascalCase (No Naming policy) + /// Bool as normal bool + /// + public static JsonSerializerOptions NoNamingPolicy => + new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + WriteIndented = true }; } } diff --git a/starsky/starsky.foundation.platform/JsonConverter/JsonClone.cs b/starsky/starsky.foundation.platform/JsonConverter/JsonClone.cs index 50e78668ee..dc5252c14d 100644 --- a/starsky/starsky.foundation.platform/JsonConverter/JsonClone.cs +++ b/starsky/starsky.foundation.platform/JsonConverter/JsonClone.cs @@ -17,5 +17,5 @@ public static class JsonClone var result = JsonSerializer.Deserialize(serialized, DefaultJsonSerializer.CamelCase); return result ?? default; } - } + } } diff --git a/starsky/starsky.foundation.platform/JsonConverter/TimeSpanConverter.cs b/starsky/starsky.foundation.platform/JsonConverter/TimeSpanConverter.cs index a1bab41801..b239116263 100644 --- a/starsky/starsky.foundation.platform/JsonConverter/TimeSpanConverter.cs +++ b/starsky/starsky.foundation.platform/JsonConverter/TimeSpanConverter.cs @@ -21,7 +21,7 @@ public override bool CanConvert(Type typeToConvert) { // Don't perform a typeToConvert == null check for performance. Trust our callers will be nice. return typeToConvert == typeof(TimeSpan) - || (typeToConvert.IsGenericType && IsNullableTimeSpan(typeToConvert)); + || ( typeToConvert.IsGenericType && IsNullableTimeSpan(typeToConvert) ); } /// diff --git a/starsky/starsky.foundation.platform/MetricsNamespaces/ActivitySourceMeter.cs b/starsky/starsky.foundation.platform/MetricsNamespaces/ActivitySourceMeter.cs new file mode 100644 index 0000000000..c36b4bfeca --- /dev/null +++ b/starsky/starsky.foundation.platform/MetricsNamespaces/ActivitySourceMeter.cs @@ -0,0 +1,7 @@ +namespace starsky.foundation.platform.MetricsNamespaces; + +public static class ActivitySourceMeter +{ + public const string SyncNameSpace = "starsky.foundation.sync"; + public const string WorkerNameSpace = "starsky.foundation.worker"; +} diff --git a/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs b/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs index d3c8f613db..eb8ddfa2ce 100644 --- a/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs +++ b/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -7,7 +6,6 @@ namespace starsky.foundation.platform.Middleware { public sealed class ContentSecurityPolicyMiddleware { - private readonly RequestDelegate _next; public ContentSecurityPolicyMiddleware(RequestDelegate next) @@ -20,15 +18,12 @@ public async Task Invoke(HttpContext httpContext) { // For Error pages (for example 404) this middleware will be executed double, // so Adding a Header that already exist give an Error 500 - if (string.IsNullOrEmpty(httpContext.Response.Headers.ContentSecurityPolicy) ) + if ( string.IsNullOrEmpty(httpContext.Response.Headers.ContentSecurityPolicy) ) { - // CSP 2.0 nonce // used in ApplicationInsightsJsHelper - var nonce = Guid.NewGuid().ToString("N"); - httpContext.Items["csp-nonce"] = nonce; - // only needed for safari and old firefox - var socketUrl = httpContext.Request.Scheme == "http" - ? $"ws://{httpContext.Request.Host.Host}" : $"wss://{httpContext.Request.Host.Host}"; + var socketUrl = httpContext.Request.Scheme == "http" + ? $"ws://{httpContext.Request.Host.Host}" + : $"wss://{httpContext.Request.Host.Host}"; // For Safari localhost var socketUrlWithPort = string.Empty; @@ -38,11 +33,12 @@ public async Task Invoke(HttpContext httpContext) $"{socketUrl}:{httpContext.Request.Host.Port}"; } + // When change also update in Electron var cspHeader = - "default-src 'none'; img-src 'self' https://*.tile.openstreetmap.org; script-src 'self' " + - $"https://js.monitor.azure.com/scripts/b/ai.2.min.js https://az416426.vo.msecnd.net \'nonce-{nonce}\'; " + - $"connect-src 'self' {socketUrl} {socketUrlWithPort} " + - "https://*.in.applicationinsights.azure.com https://dc.services.visualstudio.com/v2/track; " + + "default-src 'none'; img-src 'self' https://a.tile.openstreetmap.org/ " + + "https://b.tile.openstreetmap.org/ " + + "https://c.tile.openstreetmap.org/; script-src 'self'; " + + $"connect-src 'self' {socketUrl} {socketUrlWithPort};" + "style-src 'self'; " + "font-src 'self'; " + "frame-ancestors 'none'; " + @@ -55,54 +51,55 @@ public async Task Invoke(HttpContext httpContext) "block-all-mixed-content; "; // Currently not supported in Firefox and Safari (Edge user agent also includes the word Chrome) - if (httpContext.Request.Headers.UserAgent.Contains("Chrome") || httpContext.Request.Headers.UserAgent.Contains("csp-evaluator")) + if ( httpContext.Request.Headers.UserAgent.Contains("Chrome") || + httpContext.Request.Headers.UserAgent.Contains("csp-evaluator") ) { cspHeader += "require-trusted-types-for 'script'; "; } - + // When change also update in Electron httpContext.Response.Headers - .Append("Content-Security-Policy",cspHeader); + .Append("Content-Security-Policy", cspHeader); } // @see: https://www.permissionspolicy.com/ if ( string.IsNullOrEmpty( - httpContext.Response.Headers["Permissions-Policy"]) ) + httpContext.Response.Headers["Permissions-Policy"]) ) { httpContext.Response.Headers .Append("Permissions-Policy", "autoplay=(self), " + - "fullscreen=(self), " + - "geolocation=(self), " + - "picture-in-picture=(self), " + - "clipboard-read=(self), " + - "clipboard-write=(self), " + - "window-placement=(self)"); + "fullscreen=(self), " + + "geolocation=(self), " + + "picture-in-picture=(self), " + + "clipboard-read=(self), " + + "clipboard-write=(self), " + + "window-placement=(self)"); } - if (string.IsNullOrEmpty(httpContext.Response.Headers["Referrer-Policy"]) ) + if ( string.IsNullOrEmpty(httpContext.Response.Headers["Referrer-Policy"]) ) { httpContext.Response.Headers .Append("Referrer-Policy", "no-referrer"); } - - if (string.IsNullOrEmpty(httpContext.Response.Headers.XFrameOptions) ) + + if ( string.IsNullOrEmpty(httpContext.Response.Headers.XFrameOptions) ) { httpContext.Response.Headers .Append("X-Frame-Options", "DENY"); } - - if (string.IsNullOrEmpty(httpContext.Response.Headers.XXSSProtection) ) + + if ( string.IsNullOrEmpty(httpContext.Response.Headers.XXSSProtection) ) { httpContext.Response.Headers .Append("X-Xss-Protection", "1; mode=block"); } - - if (string.IsNullOrEmpty(httpContext.Response.Headers.XContentTypeOptions) ) + + if ( string.IsNullOrEmpty(httpContext.Response.Headers.XContentTypeOptions) ) { httpContext.Response.Headers .Append("X-Content-Type-Options", "nosniff"); } - + await _next(httpContext); } } diff --git a/starsky/starsky.foundation.platform/Models/AccountRoles.cs b/starsky/starsky.foundation.platform/Models/AccountRoles.cs index 9807857cf1..881e6d44e3 100644 --- a/starsky/starsky.foundation.platform/Models/AccountRoles.cs +++ b/starsky/starsky.foundation.platform/Models/AccountRoles.cs @@ -10,7 +10,7 @@ public enum AppAccountRoles User, Administrator } - + public static IEnumerable GetAllRoles() { return Enum.GetNames(typeof(AppAccountRoles)); diff --git a/starsky/starsky.foundation.platform/Models/ApiNotificationResponseModel.cs b/starsky/starsky.foundation.platform/Models/ApiNotificationResponseModel.cs index 2193bbf262..6c6850008d 100644 --- a/starsky/starsky.foundation.platform/Models/ApiNotificationResponseModel.cs +++ b/starsky/starsky.foundation.platform/Models/ApiNotificationResponseModel.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using starsky.foundation.platform.Enums; diff --git a/starsky/starsky.foundation.platform/Models/AppSettings.cs b/starsky/starsky.foundation.platform/Models/AppSettings.cs index 43a2266d79..83c1730b31 100644 --- a/starsky/starsky.foundation.platform/Models/AppSettings.cs +++ b/starsky/starsky.foundation.platform/Models/AppSettings.cs @@ -381,7 +381,7 @@ public static void StructureCheck(string? structure) } throw new ArgumentException("(StructureCheck) Structure is not confirm regex - " + - structure); + structure); } /// @@ -587,8 +587,8 @@ public string WebFtp if ( string.IsNullOrEmpty(value) ) return; Uri uriAddress = new Uri(value); if ( uriAddress.UserInfo.Split(":".ToCharArray()).Length == 2 - && uriAddress.Scheme == "ftp" - && uriAddress.LocalPath.Length >= 1 ) + && uriAddress.Scheme == "ftp" + && uriAddress.LocalPath.Length >= 1 ) { _webFtp = value; } @@ -650,7 +650,8 @@ public Dictionary>? PublishProfiles /// Value for AccountRolesDefaultByEmailRegisterOverwrite /// private Dictionary - AccountRolesByEmailRegisterOverwritePrivate { get; set; } = + AccountRolesByEmailRegisterOverwritePrivate + { get; set; } = new Dictionary(); /// @@ -668,7 +669,7 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite { if ( value == null ) return; foreach ( var singleValue in value.Where(singleValue => - AccountRoles.GetAllRoles().Contains(singleValue.Value)) ) + AccountRoles.GetAllRoles().Contains(singleValue.Value)) ) { AccountRolesByEmailRegisterOverwritePrivate.TryAdd( singleValue.Key, singleValue.Value); @@ -682,37 +683,6 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite [PackageTelemetry] public bool? AccountRegisterFirstRoleAdmin { get; set; } = true; - /// - /// Private storage for Application Insights InstrumentationKey - /// - private string ApplicationInsightsConnectionStringPrivate { get; set; } = ""; - - /// - /// Insert the Application Insights Connection String here or use environment variable: APPLICATIONINSIGHTS_CONNECTION_STRING - /// - public string ApplicationInsightsConnectionString - { - get - { - if ( string.IsNullOrWhiteSpace(ApplicationInsightsConnectionStringPrivate) ) - { - var connectionString = - Environment.GetEnvironmentVariable( - "APPLICATIONINSIGHTS_CONNECTION_STRING"); - return !string.IsNullOrEmpty(connectionString) - ? connectionString - : string.Empty; - } - - return ApplicationInsightsConnectionStringPrivate; - } - set => ApplicationInsightsConnectionStringPrivate = value; - } - - [PackageTelemetry] public bool? ApplicationInsightsLog { get; set; } = true; - - [PackageTelemetry] public bool? ApplicationInsightsDatabaseTracking { get; set; } = false; - [PackageTelemetry] public int MaxDegreesOfParallelism { get; set; } = 6; [PackageTelemetry] public int MaxDegreesOfParallelismThumbnail { get; set; } = 3; @@ -808,7 +778,7 @@ public bool? EnablePackageTelemetry if ( EnablePackageTelemetryPrivate == null ) { #pragma warning disable CS0162 -#if(DEBUG) +#if ( DEBUG ) return false; #endif // ReSharper disable once HeuristicUnreachableCode @@ -914,24 +884,19 @@ public AppSettings CloneToDisplay() } if ( appSettings.DatabaseType == DatabaseTypeList.Sqlite && - !string.IsNullOrEmpty(userProfileFolder) ) + !string.IsNullOrEmpty(userProfileFolder) ) { appSettings.DatabaseConnection = appSettings.DatabaseConnection.Replace(userProfileFolder, "~"); } - if ( !string.IsNullOrEmpty(appSettings.ApplicationInsightsConnectionString) ) - { - appSettings.ApplicationInsightsConnectionString = CloneToDisplaySecurityWarning; - } - if ( !string.IsNullOrEmpty(appSettings.WebFtp) ) { appSettings._webFtp = CloneToDisplaySecurityWarning; } if ( !string.IsNullOrEmpty(appSettings.AppSettingsPath) && - !string.IsNullOrEmpty(userProfileFolder) ) + !string.IsNullOrEmpty(userProfileFolder) ) { appSettings.AppSettingsPath = appSettings.AppSettingsPath.Replace(userProfileFolder, "~"); @@ -940,7 +905,7 @@ public AppSettings CloneToDisplay() if ( appSettings.PublishProfiles != null ) { foreach ( var value in appSettings.PublishProfiles.SelectMany(profile => - profile.Value) ) + profile.Value) ) { ReplaceAppSettingsPublishProfilesCloneToDisplay(value); } @@ -987,7 +952,7 @@ private static void ReplaceAppSettingsPublishProfilesCloneToDisplay( AppSettingsPublishProfiles value) { if ( !string.IsNullOrEmpty(value.Path) && - value.Path != AppSettingsPublishProfiles.GetDefaultPath() ) + value.Path != AppSettingsPublishProfiles.GetDefaultPath() ) { value.Path = CloneToDisplaySecurityWarning; } @@ -1106,7 +1071,7 @@ internal static string ReplaceEnvironmentVariable(string input) public string SqLiteFullPath(string connectionString, string baseDirectoryProject) { if ( DatabaseType == DatabaseTypeList.Mysql && - string.IsNullOrWhiteSpace(connectionString) ) + string.IsNullOrWhiteSpace(connectionString) ) throw new ArgumentException("The 'DatabaseConnection' field is null or empty"); if ( DatabaseType != DatabaseTypeList.Sqlite ) @@ -1127,7 +1092,7 @@ public string SqLiteFullPath(string connectionString, string baseDirectoryProjec if ( baseDirectoryProject.Contains("entityframeworkcore") ) return connectionString; var dataSource = "Data Source=" + baseDirectoryProject + - Path.DirectorySeparatorChar + databaseFileName; + Path.DirectorySeparatorChar + databaseFileName; return dataSource; } @@ -1156,7 +1121,7 @@ internal static void CopyProperties(object source, object destination) var destinationProperty = destinationType.GetProperty(sourceProperty.Name); if ( destinationProperty == null || - !destinationProperty.CanWrite ) + !destinationProperty.CanWrite ) { continue; } diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsKeyValue.cs b/starsky/starsky.foundation.platform/Models/AppSettingsKeyValue.cs index 2b7f637d02..3dd888e876 100644 --- a/starsky/starsky.foundation.platform/Models/AppSettingsKeyValue.cs +++ b/starsky/starsky.foundation.platform/Models/AppSettingsKeyValue.cs @@ -4,7 +4,7 @@ public sealed class AppSettingsKeyValue { public string Key { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; - + public void Deconstruct(out string key, out string value) { key = Key; diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsPublishProfiles.cs b/starsky/starsky.foundation.platform/Models/AppSettingsPublishProfiles.cs index a75d4c6e99..edb32628e6 100644 --- a/starsky/starsky.foundation.platform/Models/AppSettingsPublishProfiles.cs +++ b/starsky/starsky.foundation.platform/Models/AppSettingsPublishProfiles.cs @@ -5,235 +5,235 @@ namespace starsky.foundation.platform.Models { - //"ContentType": "html", - //"SourceMaxWidth": null, - //"OverlayMaxWidth": null, - //"OverlayFullPath": null, - //"Path": "index.html", - //"Template": "index", - //"Append": "_kl1k" - - public sealed class AppSettingsPublishProfiles - { - - /// - /// Type of template - /// - [JsonConverter(typeof(JsonStringEnumConverter))] - // newtonsoft uses: StringEnumConverter - public TemplateContentType ContentType { get; set; } = TemplateContentType.None; - - /// - /// Get the extension of the new file based on content type - /// - /// path for fallback - /// extension with dot as prefix e.g. `.jpg` - public string GetExtensionWithDot(string sourceFilePath) - { - return ContentType switch - { - TemplateContentType.Jpeg => ".jpg", - _ => System.IO.Path.GetExtension(sourceFilePath).ToLowerInvariant() - }; - } - - /// - /// Private name of SourceMaxWidth - /// - private int _sourceMaxWidth; - - /// - /// The size of the main image after resizing - /// - public int SourceMaxWidth - { - get - { - if (_sourceMaxWidth >= 100) return _sourceMaxWidth; - return 100; - } - set => _sourceMaxWidth = value; - } - - /// - /// Private name for Overlay Image - /// - private int _overlayMaxWidth; - - /// - /// Size of the overlay Image / logo - /// - public int OverlayMaxWidth - { - get - { - if (_overlayMaxWidth >= 100) return _overlayMaxWidth; - return 100; - } - set => _overlayMaxWidth = value; - } - - - /// - /// private: used for template url or overlay image - /// - private string PathPrivate { get; set; } = string.Empty; - - private const string PathDefault = "{AssemblyDirectory}/WebHtmlPublish/EmbeddedViews/default.png"; - - /// - /// Get the path to the overlay image and replace the {AssemblyDirectory} - /// - /// input value, can be null - /// system path with replaced {AssemblyDirectory} - public static string GetDefaultPath(string? value = null) - { - if ( string.IsNullOrEmpty(value) ) - { - value = PathDefault; - } - - // get current dir - var assemblyDirectory = PathHelper.RemoveLatestBackslash(AppDomain.CurrentDomain.BaseDirectory); - // replace value -- ignore this case - var subPath = Regex.Replace(value, "{AssemblyDirectory}", - string.Empty, RegexOptions.IgnoreCase, - TimeSpan.FromMilliseconds(100)); - - // append and replace - return assemblyDirectory + subPath - .Replace("starskywebftpcli", "starskywebhtmlcli"); - } - - /// - /// Reset the path to string.Empty - /// - public void ResetPath() - { - PathPrivate = string.Empty; - } - - /// - /// used for template url or overlay image - /// - public string Path - { - get - { - // return: if null > string.Empty - return string.IsNullOrEmpty(PathPrivate) ? string.Empty : PathPrivate; - } - set - { - if ( string.IsNullOrEmpty(value) ) - { - value = PathDefault; - } - - if ( !value.Contains("{AssemblyDirectory}") ) - { - PathPrivate = value; - return; - } - - // append and replace - PathPrivate = GetDefaultPath(value); - } - } - - /// - /// Private Name for folder - /// - private string _folder = string.Empty; - - /// - /// To copy folder - /// - public string Folder - { - get { return _folder; } - set - { + //"ContentType": "html", + //"SourceMaxWidth": null, + //"OverlayMaxWidth": null, + //"OverlayFullPath": null, + //"Path": "index.html", + //"Template": "index", + //"Append": "_kl1k" + + public sealed class AppSettingsPublishProfiles + { + + /// + /// Type of template + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + // newtonsoft uses: StringEnumConverter + public TemplateContentType ContentType { get; set; } = TemplateContentType.None; + + /// + /// Get the extension of the new file based on content type + /// + /// path for fallback + /// extension with dot as prefix e.g. `.jpg` + public string GetExtensionWithDot(string sourceFilePath) + { + return ContentType switch + { + TemplateContentType.Jpeg => ".jpg", + _ => System.IO.Path.GetExtension(sourceFilePath).ToLowerInvariant() + }; + } + + /// + /// Private name of SourceMaxWidth + /// + private int _sourceMaxWidth; + + /// + /// The size of the main image after resizing + /// + public int SourceMaxWidth + { + get + { + if ( _sourceMaxWidth >= 100 ) return _sourceMaxWidth; + return 100; + } + set => _sourceMaxWidth = value; + } + + /// + /// Private name for Overlay Image + /// + private int _overlayMaxWidth; + + /// + /// Size of the overlay Image / logo + /// + public int OverlayMaxWidth + { + get + { + if ( _overlayMaxWidth >= 100 ) return _overlayMaxWidth; + return 100; + } + set => _overlayMaxWidth = value; + } + + + /// + /// private: used for template url or overlay image + /// + private string PathPrivate { get; set; } = string.Empty; + + private const string PathDefault = "{AssemblyDirectory}/WebHtmlPublish/EmbeddedViews/default.png"; + + /// + /// Get the path to the overlay image and replace the {AssemblyDirectory} + /// + /// input value, can be null + /// system path with replaced {AssemblyDirectory} + public static string GetDefaultPath(string? value = null) + { + if ( string.IsNullOrEmpty(value) ) + { + value = PathDefault; + } + + // get current dir + var assemblyDirectory = PathHelper.RemoveLatestBackslash(AppDomain.CurrentDomain.BaseDirectory); + // replace value -- ignore this case + var subPath = Regex.Replace(value, "{AssemblyDirectory}", + string.Empty, RegexOptions.IgnoreCase, + TimeSpan.FromMilliseconds(100)); + + // append and replace + return assemblyDirectory + subPath + .Replace("starskywebftpcli", "starskywebhtmlcli"); + } + + /// + /// Reset the path to string.Empty + /// + public void ResetPath() + { + PathPrivate = string.Empty; + } + + /// + /// used for template url or overlay image + /// + public string Path + { + get + { + // return: if null > string.Empty + return string.IsNullOrEmpty(PathPrivate) ? string.Empty : PathPrivate; + } + set + { + if ( string.IsNullOrEmpty(value) ) + { + value = PathDefault; + } + + if ( !value.Contains("{AssemblyDirectory}") ) + { + PathPrivate = value; + return; + } + + // append and replace + PathPrivate = GetDefaultPath(value); + } + } + + /// + /// Private Name for folder + /// + private string _folder = string.Empty; + + /// + /// To copy folder + /// + public string Folder + { + get { return _folder; } + set + { // Append slash after if ( string.IsNullOrEmpty(value) ) { _folder = PathHelper.AddSlash(string.Empty); return; } - _folder = PathHelper.AddSlash(value); + _folder = PathHelper.AddSlash(value); } } - /// - /// do not add slash check, used for _kl - /// - public string Append { get; set; } = string.Empty; - - /// - /// index.cshtml for example - /// - public string Template { get; set; } = string.Empty; - - /// - /// To add before - /// - public string Prepend { get; set; } = string.Empty; - - /// - /// Include Exif Data - /// - public bool MetaData { get; set; } = true; - - /// - /// For the ftp client to ignore some directories - /// - public bool Copy { get; set; } = true; - - public override string ToString() - { - return $"ContentType:{ContentType}," + - $"SourceMaxWidth:{SourceMaxWidth}," + - $"OverlayMaxWidth:{OverlayMaxWidth}," + - $"Path:{Path}," + - $"Folder:{Folder}," + - $"Append:{Append}," + - $"Template:{Template}," + - $"Prepend:{Prepend}," + - $"MetaData:{MetaData}," + - $"Copy:{Copy}"; - } - } - - public enum TemplateContentType - { - /// - /// Default, should pick one of the other options - /// - None = 0, - /// - /// Generate Html lists - /// - Html = 1, - /// - /// Create a Jpeg Image - /// - Jpeg = 2, - /// - /// To move the source images to a folder, when using the web ui, this means copying - /// - MoveSourceFiles = 3, - /// - /// Content to be copied from WebHtmlPublish/PublishedContent to include - /// For example javaScript files - /// - PublishContent = 4, - /// - /// Include manifest file _settings.json in Copy list - /// - PublishManifest = 6, - /// - /// Only the first image, useful for og:image in template - /// - OnlyFirstJpeg = 7, - } + /// + /// do not add slash check, used for _kl + /// + public string Append { get; set; } = string.Empty; + + /// + /// index.cshtml for example + /// + public string Template { get; set; } = string.Empty; + + /// + /// To add before + /// + public string Prepend { get; set; } = string.Empty; + + /// + /// Include Exif Data + /// + public bool MetaData { get; set; } = true; + + /// + /// For the ftp client to ignore some directories + /// + public bool Copy { get; set; } = true; + + public override string ToString() + { + return $"ContentType:{ContentType}," + + $"SourceMaxWidth:{SourceMaxWidth}," + + $"OverlayMaxWidth:{OverlayMaxWidth}," + + $"Path:{Path}," + + $"Folder:{Folder}," + + $"Append:{Append}," + + $"Template:{Template}," + + $"Prepend:{Prepend}," + + $"MetaData:{MetaData}," + + $"Copy:{Copy}"; + } + } + + public enum TemplateContentType + { + /// + /// Default, should pick one of the other options + /// + None = 0, + /// + /// Generate Html lists + /// + Html = 1, + /// + /// Create a Jpeg Image + /// + Jpeg = 2, + /// + /// To move the source images to a folder, when using the web ui, this means copying + /// + MoveSourceFiles = 3, + /// + /// Content to be copied from WebHtmlPublish/PublishedContent to include + /// For example javaScript files + /// + PublishContent = 4, + /// + /// Include manifest file _settings.json in Copy list + /// + PublishManifest = 6, + /// + /// Only the first image, useful for og:image in template + /// + OnlyFirstJpeg = 7, + } } diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs b/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs index ac39480fe3..483cc02ddb 100644 --- a/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs +++ b/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs @@ -6,7 +6,7 @@ namespace starsky.foundation.platform.Models public sealed class AppSettingsTransferObject { public bool? Verbose { get; set; } - + public string? StorageFolder { get; set; } public bool? UseSystemTrash { get; set; } diff --git a/starsky/starsky.foundation.platform/Models/CameraMakeModel.cs b/starsky/starsky.foundation.platform/Models/CameraMakeModel.cs index db085b6115..bb5dc26edf 100644 --- a/starsky/starsky.foundation.platform/Models/CameraMakeModel.cs +++ b/starsky/starsky.foundation.platform/Models/CameraMakeModel.cs @@ -14,6 +14,6 @@ public CameraMakeModel(string make, string model) } public string Make { get; set; } = string.Empty; - public string Model { get; set; }= string.Empty; + public string Model { get; set; } = string.Empty; } } diff --git a/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainer.cs b/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainer.cs index 6c30a076d1..25f1a8aecb 100644 --- a/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainer.cs +++ b/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainer.cs @@ -1,4 +1,3 @@ -#nullable enable namespace starsky.foundation.platform.Models.Kestrel; public sealed class KestrelContainer diff --git a/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpoints.cs b/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpoints.cs index 5434d2c920..cbe0781c4a 100644 --- a/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpoints.cs +++ b/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpoints.cs @@ -1,4 +1,3 @@ -#nullable enable namespace starsky.foundation.platform.Models.Kestrel; public sealed class KestrelContainerEndpoints diff --git a/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpointsUrl.cs b/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpointsUrl.cs index dab9feacbc..9defefc426 100644 --- a/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpointsUrl.cs +++ b/starsky/starsky.foundation.platform/Models/Kestrel/KestrelContainerEndpointsUrl.cs @@ -1,4 +1,3 @@ -#nullable enable namespace starsky.foundation.platform.Models.Kestrel; public sealed class KestrelContainerEndpointsUrl diff --git a/starsky/starsky.foundation.platform/Models/OpenTelemetrySettings.cs b/starsky/starsky.foundation.platform/Models/OpenTelemetrySettings.cs index e197131b22..e6aa4b6930 100644 --- a/starsky/starsky.foundation.platform/Models/OpenTelemetrySettings.cs +++ b/starsky/starsky.foundation.platform/Models/OpenTelemetrySettings.cs @@ -18,7 +18,7 @@ public string GetServiceName() { return string.IsNullOrWhiteSpace(ServiceName) ? "Starsky" - : ServiceName ; + : ServiceName; } public string? GetLogsHeader() @@ -27,7 +27,7 @@ public string GetServiceName() ? Header : LogsHeader; } - + public string? GetMetricsHeader() { return string.IsNullOrWhiteSpace(MetricsHeader) diff --git a/starsky/starsky.foundation.platform/Services/WebLogger.cs b/starsky/starsky.foundation.platform/Services/WebLogger.cs index 11d9ee22f7..66b2065901 100644 --- a/starsky/starsky.foundation.platform/Services/WebLogger.cs +++ b/starsky/starsky.foundation.platform/Services/WebLogger.cs @@ -8,7 +8,7 @@ namespace starsky.foundation.platform.Services { [SuppressMessage("Usage", "CA2254:The logging message template should not vary between calls to " + - "'LoggerExtensions.LogInformation(ILogger, string?, params object?[])'")] + "'LoggerExtensions.LogInformation(ILogger, string?, params object?[])'")] [Service(typeof(IWebLogger), InjectionLifetime = InjectionLifetime.Singleton)] public sealed class WebLogger : IWebLogger { @@ -34,8 +34,8 @@ public void LogDebug(string? message, params object[] args) { return; } - - if ( _logger == null) + + if ( _logger == null ) { _console?.WriteLine(message); return; @@ -49,7 +49,7 @@ public void LogInformation(string? message, params object[] args) { return; } - + if ( _logger == null ) { _console?.WriteLine(message); @@ -75,13 +75,13 @@ public void LogError(string? message, params object[] args) { return; } - + if ( _logger == null ) { _console?.WriteLine(message); return; } - _logger.LogError(message,args); + _logger.LogError(message, args); } public void LogError(Exception exception, string message, params object[] args) { @@ -90,7 +90,7 @@ public void LogError(Exception exception, string message, params object[] args) _console?.WriteLine($"{exception.Message} {message}"); return; } - _logger.LogError(exception, message,args); + _logger.LogError(exception, message, args); } } } diff --git a/starsky/starsky.foundation.platform/VersionHelpers/SemVersion.cs b/starsky/starsky.foundation.platform/VersionHelpers/SemVersion.cs index f90f66546e..d98447700f 100644 --- a/starsky/starsky.foundation.platform/VersionHelpers/SemVersion.cs +++ b/starsky/starsky.foundation.platform/VersionHelpers/SemVersion.cs @@ -30,10 +30,10 @@ public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = " } private static readonly Regex ParseEx = new Regex(@"^(?\d+)" + - @"(?>\.(?\d+))?" + - @"(?>\.(?\d+))?" + - @"(?>\-(?
[0-9A-Za-z\-\.]+))?" +
-		                                                  @"(?>\+(?[0-9A-Za-z\-\.]+))?$",
+														  @"(?>\.(?\d+))?" +
+														  @"(?>\.(?\d+))?" +
+														  @"(?>\-(?
[0-9A-Za-z\-\.]+))?" +
+														  @"(?>\+(?[0-9A-Za-z\-\.]+))?$",
 			RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
 			TimeSpan.FromSeconds(0.5));
 
@@ -84,7 +84,7 @@ public static SemVersion Parse(string version)
 		/// 
 		/// The minor version.
 		/// 
-		public int Minor { get; set;}
+		public int Minor { get; set; }
 
 		/// 
 		/// Gets the patch version.
@@ -92,7 +92,7 @@ public static SemVersion Parse(string version)
 		/// 
 		/// The patch version.
 		/// 
-		public int Patch { get; set;}
+		public int Patch { get; set; }
 
 		/// 
 		/// Gets the prerelease version.
@@ -100,7 +100,7 @@ public static SemVersion Parse(string version)
 		/// 
 		/// The prerelease version. Empty string if this is a release version.
 		/// 
-		public string Prerelease { get; set;}
+		public string Prerelease { get; set; }
 
 		/// 
 		/// Gets the build metadata.
@@ -108,7 +108,7 @@ public static SemVersion Parse(string version)
 		/// 
 		/// The build metadata. Empty string if there is no build metadata.
 		/// 
-		public string Build { get; set;}
+		public string Build { get; set; }
 
 		/// 
 		/// Compares the current instance with another object of the same type and returns an integer that indicates
@@ -201,12 +201,12 @@ private static int CompareComponentCompareLoop(string[] aComps, string[] bComps)
 					if ( value != int.MaxValue )
 					{
 						return value;
-					}	
+					}
 				}
 			}
-			return  aComps.Length.CompareTo(bComps.Length);
+			return aComps.Length.CompareTo(bComps.Length);
 		}
-		
+
 		/// 
 		/// Returns a hash code for this instance.
 		/// 
@@ -226,7 +226,7 @@ public override int GetHashCode()
 				return result;
 			}
 		}
-		
+
 		/// 
 		/// Determines whether the specified  is equal to this instance.
 		/// 
@@ -238,21 +238,21 @@ public override int GetHashCode()
 		/// The  is not a .
 		public override bool Equals(object? obj)
 		{
-			if (obj is null)
+			if ( obj is null )
 				return false;
 
-			if (ReferenceEquals(this, obj))
+			if ( ReferenceEquals(this, obj) )
 				return true;
 
-			var other = (SemVersion)obj;
+			var other = ( SemVersion )obj;
 
 			return Major == other.Major
-			       && Minor == other.Minor
-			       && Patch == other.Patch
-			       && string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal)
-			       && string.Equals(Build, other.Build, StringComparison.Ordinal);
+				   && Minor == other.Minor
+				   && Patch == other.Patch
+				   && string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal)
+				   && string.Equals(Build, other.Build, StringComparison.Ordinal);
 		}
-		
+
 		/// 
 		/// Checks whether two semantic versions are equal.
 		/// 
@@ -261,8 +261,8 @@ public override bool Equals(object? obj)
 		///  if the two values are equal, otherwise .
 		public static bool Equals(SemVersion? versionA, SemVersion? versionB)
 		{
-			if (ReferenceEquals(versionA, versionB)) return true;
-			if (versionA is null || versionB is null) return false;
+			if ( ReferenceEquals(versionA, versionB) ) return true;
+			if ( versionA is null || versionB is null ) return false;
 			return versionA.Equals(versionB);
 		}
 
@@ -346,7 +346,7 @@ public override string ToString()
 		{
 			return Equals(left, right) || Compare(left, right) > 0;
 		}
-		
+
 		/// 
 		/// Compares two semantic versions.
 		/// 
diff --git a/starsky/starsky.foundation.readmeta/Helpers/GeoDistanceTo.cs b/starsky/starsky.foundation.readmeta/Helpers/GeoDistanceTo.cs
index 29bbb54a69..8703eda45d 100644
--- a/starsky/starsky.foundation.readmeta/Helpers/GeoDistanceTo.cs
+++ b/starsky/starsky.foundation.readmeta/Helpers/GeoDistanceTo.cs
@@ -1,42 +1,42 @@
-using System;
+using System;
 
 namespace starsky.foundation.readmeta.Helpers
 {
-    public static class GeoDistanceTo
-    {
-        /// 
-        /// Calculate straight line distance in kilometers
-        /// 
-        /// latitude in decimal degrees
-        /// longitude in decimal degrees
-        /// latitude in decimal degrees
-        /// longitude in decimal degrees
-        /// in kilometers
-        public static double GetDistance(double latitudeFrom, double longitudeFrom, double latitudeTo, double longitudeTo)
-        {
-            double dlon = Radians(longitudeTo - longitudeFrom);
-            double dlat = Radians(latitudeTo - latitudeFrom);
+	public static class GeoDistanceTo
+	{
+		/// 
+		/// Calculate straight line distance in kilometers
+		/// 
+		/// latitude in decimal degrees
+		/// longitude in decimal degrees
+		/// latitude in decimal degrees
+		/// longitude in decimal degrees
+		/// in kilometers
+		public static double GetDistance(double latitudeFrom, double longitudeFrom, double latitudeTo, double longitudeTo)
+		{
+			double dlon = Radians(longitudeTo - longitudeFrom);
+			double dlat = Radians(latitudeTo - latitudeFrom);
 
-            double a = (Math.Sin(dlat / 2) * Math.Sin(dlat / 2)) + 
-                       Math.Cos(Radians(latitudeFrom)) * Math.Cos(Radians(latitudeTo)) * (Math.Sin(dlon / 2) * Math.Sin(dlon / 2));
-            double angle = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
-            return angle * Radius;
-        }
-        
-	    /// 
-	    /// Radius of the earth
-	    /// 
-        private const double Radius = 6378.16;
+			double a = ( Math.Sin(dlat / 2) * Math.Sin(dlat / 2) ) +
+					   Math.Cos(Radians(latitudeFrom)) * Math.Cos(Radians(latitudeTo)) * ( Math.Sin(dlon / 2) * Math.Sin(dlon / 2) );
+			double angle = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
+			return angle * Radius;
+		}
 
-        /// 
-        /// Convert degrees to Radians
-        /// 
-        /// Degrees
-        /// The equivalent in radians
-        private static double Radians(double x)
-        {
-            return x * Math.PI / 180;
-        }
+		/// 
+		/// Radius of the earth
+		/// 
+		private const double Radius = 6378.16;
 
-    }
+		/// 
+		/// Convert degrees to Radians
+		/// 
+		/// Degrees
+		/// The equivalent in radians
+		private static double Radians(double x)
+		{
+			return x * Math.PI / 180;
+		}
+
+	}
 }
diff --git a/starsky/starsky.foundation.readmeta/Helpers/GeoParser.cs b/starsky/starsky.foundation.readmeta/Helpers/GeoParser.cs
index 7b1325185b..04b16f2fca 100644
--- a/starsky/starsky.foundation.readmeta/Helpers/GeoParser.cs
+++ b/starsky/starsky.foundation.readmeta/Helpers/GeoParser.cs
@@ -7,61 +7,61 @@ namespace starsky.foundation.readmeta.Helpers
 {
 	public static class GeoParser
 	{
-			    
-	    /// 
-	    /// Convert 17.21.18S / DD°MM’SS.s” usage to double
-	    /// 
-	    /// 
-	    /// 
-	    /// 
-	    public static double ConvertDegreeMinutesSecondsToDouble(string point, string refGps)
-	    {
-		    //Example: 17.21.18S
-		    // DD°MM’SS.s” usage
-            
-		    var multiplier = (refGps.Contains('S') || refGps.Contains('W') ) ? -1 : 1; //handle south and west
-
-		    point = Regex.Replace(point, "[^0-9\\., ]", "", RegexOptions.CultureInvariant, 
-			    TimeSpan.FromMilliseconds(100)); //remove the characters
-
-		    // When you use an localisation where commas are used instead of a dot
-		    point = point.Replace(",", ".");
-
-		    var pointArray = point.Split(' '); //split the string.
-
-		    //Decimal degrees = 
-		    //   whole number of degrees, 
-		    //   plus minutes divided by 60, 
-		    //   plus seconds divided by 3600
-
-		    var degrees = double.Parse(pointArray[0], CultureInfo.InvariantCulture);
-		    var minutes = double.Parse(pointArray[1], CultureInfo.InvariantCulture) / 60;
-		    var seconds = double.Parse(pointArray[2],CultureInfo.InvariantCulture) / 3600;
-
-		    return (degrees + minutes + seconds) * multiplier;
-	    }
-
-	    /// 
-	    /// Convert "5,55.840,E" to double
-	    /// 
-	    /// 
-	    /// 
-	    /// 
-	    public static double ConvertDegreeMinutesToDouble(string point, string refGps)
-	    {
-		    // "5,55.840E"
-		    var multiplier = (refGps.Contains('S') || refGps.Contains('W')) ? -1 : 1; // handle south and west
-
-		    point = point.Replace(",", " ");
-		    point = Regex.Replace(point, "[^0-9\\., ]", "", RegexOptions.CultureInvariant, 
-			     TimeSpan.FromMilliseconds(100)); //remove the characters
-
-		    var pointArray = point.Split(' '); //split the string.
-		    var degrees = double.Parse(pointArray[0], CultureInfo.InvariantCulture);
-		    var minutes = double.Parse(pointArray[1], CultureInfo.InvariantCulture) / 60;
-            
-		    return (degrees + minutes) * multiplier;
-	    }
+
+		/// 
+		/// Convert 17.21.18S / DD°MM’SS.s” usage to double
+		/// 
+		/// 
+		/// 
+		/// 
+		public static double ConvertDegreeMinutesSecondsToDouble(string point, string refGps)
+		{
+			//Example: 17.21.18S
+			// DD°MM’SS.s” usage
+
+			var multiplier = ( refGps.Contains('S') || refGps.Contains('W') ) ? -1 : 1; //handle south and west
+
+			point = Regex.Replace(point, "[^0-9\\., ]", "", RegexOptions.CultureInvariant,
+				TimeSpan.FromMilliseconds(100)); //remove the characters
+
+			// When you use an localisation where commas are used instead of a dot
+			point = point.Replace(",", ".");
+
+			var pointArray = point.Split(' '); //split the string.
+
+			//Decimal degrees = 
+			//   whole number of degrees, 
+			//   plus minutes divided by 60, 
+			//   plus seconds divided by 3600
+
+			var degrees = double.Parse(pointArray[0], CultureInfo.InvariantCulture);
+			var minutes = double.Parse(pointArray[1], CultureInfo.InvariantCulture) / 60;
+			var seconds = double.Parse(pointArray[2], CultureInfo.InvariantCulture) / 3600;
+
+			return ( degrees + minutes + seconds ) * multiplier;
+		}
+
+		/// 
+		/// Convert "5,55.840,E" to double
+		/// 
+		/// 
+		/// 
+		/// 
+		public static double ConvertDegreeMinutesToDouble(string point, string refGps)
+		{
+			// "5,55.840E"
+			var multiplier = ( refGps.Contains('S') || refGps.Contains('W') ) ? -1 : 1; // handle south and west
+
+			point = point.Replace(",", " ");
+			point = Regex.Replace(point, "[^0-9\\., ]", "", RegexOptions.CultureInvariant,
+				 TimeSpan.FromMilliseconds(100)); //remove the characters
+
+			var pointArray = point.Split(' '); //split the string.
+			var degrees = double.Parse(pointArray[0], CultureInfo.InvariantCulture);
+			var minutes = double.Parse(pointArray[1], CultureInfo.InvariantCulture) / 60;
+
+			return ( degrees + minutes ) * multiplier;
+		}
 
 		private static readonly char[] Separator = ['+', '-'];
 
@@ -70,108 +70,108 @@ public static double ConvertDegreeMinutesToDouble(string point, string refGps)
 		/// 
 		/// 
 		public static GeoListItem ParseIsoString(string isoStr)
-        {
-	        var geoListItem = new GeoListItem();
-	        
-            // Parse coordinate in the following ISO 6709 formats:
-            // source: https://github.com/jaime-olivares/coordinate/blob/master/Coordinate.cs
-            // Latitude and Longitude in Degrees:
-            // �DD.DDDD�DDD.DDDD/         (eg +12.345-098.765/)
-            // Latitude and Longitude in Degrees and Minutes:
-            // �DDMM.MMMM�DDDMM.MMMM/     (eg +1234.56-09854.321/)
-            // Latitude and Longitude in Degrees, Minutes and Seconds:
-            // �DDMMSS.SSSS�DDDMMSS.SSSS/ (eg +123456.7-0985432.1/)
-            // Latitude, Longitude (in Degrees) and Altitude:
-            // �DD.DDDD�DDD.DDDD�AAA.AAA/         (eg +12.345-098.765+15.9/)
-            // Latitude, Longitude (in Degrees and Minutes) and Altitude:
-            // �DDMM.MMMM�DDDMM.MMMM�AAA.AAA/     (eg +1234.56-09854.321+15.9/)
-            // Latitude, Longitude (in Degrees, Minutes and Seconds) and Altitude:
-            // �DDMMSS.SSSS�DDDMMSS.SSSS�AAA.AAA/ (eg +123456.7-0985432.1+15.9/)
-
-            // Check for minimum length
-            // Check for trailing slash
-            if ( isoStr.Length < 18 || !isoStr.EndsWith('/') )
-            {
-	            return geoListItem;
-            }
-
-            isoStr = isoStr.Remove(isoStr.Length - 1); // Remove trailing slash
-
-            var parts = isoStr.Split(Separator, StringSplitOptions.None);
-            if ( parts.Length < 3 || parts.Length > 4 )
-            {
-	            // Check for parts count
-	            return geoListItem;
-            }  
-            
-            if ( parts[0].Length != 0 )
-            {
-	            // Check if first part is empty
-	            return geoListItem;
-            } 
-
-            var point = parts[1].IndexOf('.');
-            if ( point != 2 && point != 4 && point != 6 )
-            {
-	            // Check for valid length for lat/lon
-	            return geoListItem;
-            }
-
-            if ( point != parts[2].IndexOf('.') - 1 )
-            {
-	            // Check for lat/lon decimal positions
-	            return geoListItem;
-            }
-            
-            var numberFormatInfo = NumberFormatInfo.InvariantInfo;
-
-            switch ( point )
-            {
-	            // Parse latitude and longitude values, according to format
-	            case 2:
-		            geoListItem.Latitude = float.Parse(parts[1], numberFormatInfo) * 3600;
-		            geoListItem.Longitude = float.Parse(parts[2], numberFormatInfo) * 3600;
-		            break;
-	            case 4:
-		            geoListItem.Latitude = float.Parse(parts[1].AsSpan(0, 2), numberFormatInfo) * 3600 + 
-		                                   float.Parse(parts[1].AsSpan(2), numberFormatInfo) * 60;
-		            geoListItem.Longitude = float.Parse(parts[2].AsSpan(0, 3), numberFormatInfo) * 3600 + 
-		                                    float.Parse(parts[2].AsSpan(3), numberFormatInfo) * 60;
-		            break;
-	            // point==8 / 6
-	            default:
-		            geoListItem.Latitude = float.Parse(parts[1].AsSpan(0, 2), numberFormatInfo) * 3600 + 
-		                                   float.Parse(parts[1].AsSpan(2, 2), numberFormatInfo) * 60 + 
-		                                   float.Parse(parts[1].AsSpan(4), numberFormatInfo);
-		            geoListItem.Longitude = float.Parse(parts[2].AsSpan(0, 3), numberFormatInfo) * 3600 + 
-		                                    float.Parse(parts[2].AsSpan(3, 2), numberFormatInfo) * 60 + 
-		                                    float.Parse(parts[2].AsSpan(5), numberFormatInfo);
-		            break;
-            }
-            
-            // Parse altitude, just to check if it is valid
-            if ( parts.Length == 4 && !float.TryParse(parts[3],
-	                NumberStyles.Float, numberFormatInfo, out _) )
-            {
-	            return geoListItem;
-            }
-
-            // Add proper sign to lat/lon
-            if ( isoStr[0] == '-' )
-            {
-	            geoListItem.Latitude = - geoListItem.Latitude;
-            }
-
-            if ( isoStr[parts[1].Length + 1] == '-' )
-            {
-	            geoListItem.Longitude = - geoListItem.Longitude;
-            }
-
-            // and calc back to degrees
-            geoListItem.Latitude /= 3600.0f;
-            geoListItem.Longitude /= 3600.0f;
-
-            return geoListItem;
-        }
+		{
+			var geoListItem = new GeoListItem();
+
+			// Parse coordinate in the following ISO 6709 formats:
+			// source: https://github.com/jaime-olivares/coordinate/blob/master/Coordinate.cs
+			// Latitude and Longitude in Degrees:
+			// �DD.DDDD�DDD.DDDD/         (eg +12.345-098.765/)
+			// Latitude and Longitude in Degrees and Minutes:
+			// �DDMM.MMMM�DDDMM.MMMM/     (eg +1234.56-09854.321/)
+			// Latitude and Longitude in Degrees, Minutes and Seconds:
+			// �DDMMSS.SSSS�DDDMMSS.SSSS/ (eg +123456.7-0985432.1/)
+			// Latitude, Longitude (in Degrees) and Altitude:
+			// �DD.DDDD�DDD.DDDD�AAA.AAA/         (eg +12.345-098.765+15.9/)
+			// Latitude, Longitude (in Degrees and Minutes) and Altitude:
+			// �DDMM.MMMM�DDDMM.MMMM�AAA.AAA/     (eg +1234.56-09854.321+15.9/)
+			// Latitude, Longitude (in Degrees, Minutes and Seconds) and Altitude:
+			// �DDMMSS.SSSS�DDDMMSS.SSSS�AAA.AAA/ (eg +123456.7-0985432.1+15.9/)
+
+			// Check for minimum length
+			// Check for trailing slash
+			if ( isoStr.Length < 18 || !isoStr.EndsWith('/') )
+			{
+				return geoListItem;
+			}
+
+			isoStr = isoStr.Remove(isoStr.Length - 1); // Remove trailing slash
+
+			var parts = isoStr.Split(Separator, StringSplitOptions.None);
+			if ( parts.Length < 3 || parts.Length > 4 )
+			{
+				// Check for parts count
+				return geoListItem;
+			}
+
+			if ( parts[0].Length != 0 )
+			{
+				// Check if first part is empty
+				return geoListItem;
+			}
+
+			var point = parts[1].IndexOf('.');
+			if ( point != 2 && point != 4 && point != 6 )
+			{
+				// Check for valid length for lat/lon
+				return geoListItem;
+			}
+
+			if ( point != parts[2].IndexOf('.') - 1 )
+			{
+				// Check for lat/lon decimal positions
+				return geoListItem;
+			}
+
+			var numberFormatInfo = NumberFormatInfo.InvariantInfo;
+
+			switch ( point )
+			{
+				// Parse latitude and longitude values, according to format
+				case 2:
+					geoListItem.Latitude = float.Parse(parts[1], numberFormatInfo) * 3600;
+					geoListItem.Longitude = float.Parse(parts[2], numberFormatInfo) * 3600;
+					break;
+				case 4:
+					geoListItem.Latitude = float.Parse(parts[1].AsSpan(0, 2), numberFormatInfo) * 3600 +
+										   float.Parse(parts[1].AsSpan(2), numberFormatInfo) * 60;
+					geoListItem.Longitude = float.Parse(parts[2].AsSpan(0, 3), numberFormatInfo) * 3600 +
+											float.Parse(parts[2].AsSpan(3), numberFormatInfo) * 60;
+					break;
+				// point==8 / 6
+				default:
+					geoListItem.Latitude = float.Parse(parts[1].AsSpan(0, 2), numberFormatInfo) * 3600 +
+										   float.Parse(parts[1].AsSpan(2, 2), numberFormatInfo) * 60 +
+										   float.Parse(parts[1].AsSpan(4), numberFormatInfo);
+					geoListItem.Longitude = float.Parse(parts[2].AsSpan(0, 3), numberFormatInfo) * 3600 +
+											float.Parse(parts[2].AsSpan(3, 2), numberFormatInfo) * 60 +
+											float.Parse(parts[2].AsSpan(5), numberFormatInfo);
+					break;
+			}
+
+			// Parse altitude, just to check if it is valid
+			if ( parts.Length == 4 && !float.TryParse(parts[3],
+					NumberStyles.Float, numberFormatInfo, out _) )
+			{
+				return geoListItem;
+			}
+
+			// Add proper sign to lat/lon
+			if ( isoStr[0] == '-' )
+			{
+				geoListItem.Latitude = -geoListItem.Latitude;
+			}
+
+			if ( isoStr[parts[1].Length + 1] == '-' )
+			{
+				geoListItem.Longitude = -geoListItem.Longitude;
+			}
+
+			// and calc back to degrees
+			geoListItem.Latitude /= 3600.0f;
+			geoListItem.Longitude /= 3600.0f;
+
+			return geoListItem;
+		}
 	}
 }
diff --git a/starsky/starsky.foundation.readmeta/Helpers/MathFraction.cs b/starsky/starsky.foundation.readmeta/Helpers/MathFraction.cs
index bd68e27d9c..ba24a97141 100644
--- a/starsky/starsky.foundation.readmeta/Helpers/MathFraction.cs
+++ b/starsky/starsky.foundation.readmeta/Helpers/MathFraction.cs
@@ -13,7 +13,7 @@ public static class MathFraction
 		public static double Fraction(string value)
 		{
 			var gpsAltitudeValues = value.Split("/".ToCharArray());
-			if(gpsAltitudeValues.Length != 2) return 0f;
+			if ( gpsAltitudeValues.Length != 2 ) return 0f;
 			var numerator = double.Parse(gpsAltitudeValues[0], CultureInfo.InvariantCulture);
 			var denominator = double.Parse(gpsAltitudeValues[1], CultureInfo.InvariantCulture);
 			return numerator / denominator;
diff --git a/starsky/starsky.foundation.readmeta/Interfaces/IReadMeta.cs b/starsky/starsky.foundation.readmeta/Interfaces/IReadMeta.cs
index 9d86e4ef41..cc15cb474e 100644
--- a/starsky/starsky.foundation.readmeta/Interfaces/IReadMeta.cs
+++ b/starsky/starsky.foundation.readmeta/Interfaces/IReadMeta.cs
@@ -1,20 +1,19 @@
-#nullable enable
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using starsky.foundation.database.Models;
 
 namespace starsky.foundation.readmeta.Interfaces
 {
-    public interface IReadMeta
-    {
+	public interface IReadMeta
+	{
 		/// 
 		/// this returns only meta data > so no fileName or fileHash
 		/// 
 		/// subPath
 		/// 
-	    Task ReadExifAndXmpFromFileAsync(string subPath);
-	    Task> ReadExifAndXmpFromFileAddFilePathHashAsync(List subPathList, List? fileHashes = null);
-        bool? RemoveReadMetaCache(string fullFilePath);
-        void UpdateReadMetaCache(IEnumerable objectExifToolModel);
-    }
+		Task ReadExifAndXmpFromFileAsync(string subPath);
+		Task> ReadExifAndXmpFromFileAddFilePathHashAsync(List subPathList, List? fileHashes = null);
+		bool? RemoveReadMetaCache(string fullFilePath);
+		void UpdateReadMetaCache(IEnumerable objectExifToolModel);
+	}
 }
diff --git a/starsky/starsky.foundation.readmeta/Models/GeoListItem.cs b/starsky/starsky.foundation.readmeta/Models/GeoListItem.cs
index 10acc8f8d6..3a79b57642 100644
--- a/starsky/starsky.foundation.readmeta/Models/GeoListItem.cs
+++ b/starsky/starsky.foundation.readmeta/Models/GeoListItem.cs
@@ -1,13 +1,13 @@
-using System;
+using System;
 
 namespace starsky.foundation.readmeta.Models
 {
-    public sealed class GeoListItem
-    {
-	    public string Title { get; set; } = string.Empty;
-        public double Latitude { get; set; }
-        public double Longitude { get; set; }
-        public double Altitude { get; set; }
-        public DateTime DateTime { get; set; }
-    }
+	public sealed class GeoListItem
+	{
+		public string Title { get; set; } = string.Empty;
+		public double Latitude { get; set; }
+		public double Longitude { get; set; }
+		public double Altitude { get; set; }
+		public DateTime DateTime { get; set; }
+	}
 }
diff --git a/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaExif.cs b/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaExif.cs
index 72010e7d9d..1092d1151d 100644
--- a/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaExif.cs
+++ b/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaExif.cs
@@ -25,8 +25,8 @@
 namespace starsky.foundation.readmeta.ReadMetaHelpers
 {
 	[SuppressMessage("Usage", "S3966: Resource '_iStorage.ReadStream' has " +
-	                          "already been disposed explicitly or through a using statement implicitly. " +
-	                          "Remove the redundant disposal.")]
+							  "already been disposed explicitly or through a using statement implicitly. " +
+							  "Remove the redundant disposal.")]
 	public sealed class ReadMetaExif
 	{
 		private readonly IStorage _iStorage;
@@ -39,359 +39,359 @@ public ReadMetaExif(IStorage iStorage, AppSettings appSettings, IWebLogger logge
 			_appSettings = appSettings;
 			_logger = logger;
 		}
-		public FileIndexItem ReadExifFromFile(string subPath, 
+		public FileIndexItem ReadExifFromFile(string subPath,
 			FileIndexItem? existingFileIndexItem = null) // use null to create an object
-        {
-            List allExifItems;
-
-	        // Used to overwrite feature
-	        if (existingFileIndexItem == null)
-	        {
-		        existingFileIndexItem = new FileIndexItem(subPath);
-	        }
-
-	        var defaultErrorResult = new FileIndexItem(subPath)
-	        {
-		        ColorClass = ColorClassParser.Color.None,
-		        ImageFormat = ExtensionRolesHelper.ImageFormat.unknown,
-		        Status = FileIndexItem.ExifStatus.OperationNotSupported,
-		        Tags = string.Empty,
-		        Orientation = FileIndexItem.Rotation.Horizontal
-	        };
-	        
-	        using ( var stream = _iStorage.ReadStream(subPath) )
-	        {
-		        if ( stream == Stream.Null )
-		        {
-			        return defaultErrorResult;
-		        }
+		{
+			List allExifItems;
+
+			// Used to overwrite feature
+			if ( existingFileIndexItem == null )
+			{
+				existingFileIndexItem = new FileIndexItem(subPath);
+			}
+
+			var defaultErrorResult = new FileIndexItem(subPath)
+			{
+				ColorClass = ColorClassParser.Color.None,
+				ImageFormat = ExtensionRolesHelper.ImageFormat.unknown,
+				Status = FileIndexItem.ExifStatus.OperationNotSupported,
+				Tags = string.Empty,
+				Orientation = FileIndexItem.Rotation.Horizontal
+			};
+
+			using ( var stream = _iStorage.ReadStream(subPath) )
+			{
+				if ( stream == Stream.Null )
+				{
+					return defaultErrorResult;
+				}
 				try
 				{
 					allExifItems = ImageMetadataReader.ReadMetadata(stream).ToList();
 					DisplayAllExif(allExifItems);
 				}
-				catch (Exception)
+				catch ( Exception )
 				{
 					// ImageProcessing or System.Exception: Handler moved stream beyond end of atom
 					stream.Close();
 					return defaultErrorResult;
 				}
-	        }
-	        
-            return ParseExifDirectory(allExifItems, existingFileIndexItem);
-        }
-
-        internal FileIndexItem ParseExifDirectory(List allExifItems, FileIndexItem? item)
-        {
-            // Used to overwrite feature
-            if (item == null)
-            {
-                throw new ArgumentException("need to fill item with filepath");
-            }
-            
-            // Set the default value
-            item.ColorClass = ColorClassParser.GetColorClass();
-            
-            // Set the default value
-            item.Orientation = item.SetAbsoluteOrientation("1");
-
-            item.Latitude = GetGeoLocationLatitude(allExifItems);
-            item.Longitude = GetGeoLocationLongitude(allExifItems);
-            item.LocationAltitude = GetGeoLocationAltitude(allExifItems);
-            
-            item.SetImageWidth(GetImageWidthHeight(allExifItems,true));
-            item.SetImageHeight(GetImageWidthHeight(allExifItems,false));
-
-	        // Update imageFormat based on Exif data
-	        var imageFormat = GetFileSpecificTags(allExifItems);
-	        if ( imageFormat != ExtensionRolesHelper.ImageFormat.unknown )
-	        {
-		        item.ImageFormat = imageFormat;
-	        }
-	        
-            SetArrayBasedItemsTagsDescriptionTitle(allExifItems, item);
+			}
+
+			return ParseExifDirectory(allExifItems, existingFileIndexItem);
+		}
+
+		internal FileIndexItem ParseExifDirectory(List allExifItems, FileIndexItem? item)
+		{
+			// Used to overwrite feature
+			if ( item == null )
+			{
+				throw new ArgumentException("need to fill item with filepath");
+			}
+
+			// Set the default value
+			item.ColorClass = ColorClassParser.GetColorClass();
+
+			// Set the default value
+			item.Orientation = item.SetAbsoluteOrientation("1");
+
+			item.Latitude = GetGeoLocationLatitude(allExifItems);
+			item.Longitude = GetGeoLocationLongitude(allExifItems);
+			item.LocationAltitude = GetGeoLocationAltitude(allExifItems);
+
+			item.SetImageWidth(GetImageWidthHeight(allExifItems, true));
+			item.SetImageHeight(GetImageWidthHeight(allExifItems, false));
+
+			// Update imageFormat based on Exif data
+			var imageFormat = GetFileSpecificTags(allExifItems);
+			if ( imageFormat != ExtensionRolesHelper.ImageFormat.unknown )
+			{
+				item.ImageFormat = imageFormat;
+			}
+
+			SetArrayBasedItemsTagsDescriptionTitle(allExifItems, item);
 			SetArrayBasedItemsApertureShutterSpeedIso(allExifItems, item);
-            SetArrayBasedItemsLocationPlaces(allExifItems, item);
-            SetArrayBasedItemsOrientation(allExifItems, item);
-            SetArrayBasedItemsLens(allExifItems, item);
-            SetArrayBasedItemsMakeModel(allExifItems, item);
-            SetArrayBasedItemsSoftwareStabilization(allExifItems, item);
-
-            return item;
-        }
-        
-        /// 
-        /// Combination setter Tags Description Title ColorClass
-        /// 
-        /// list of items
-        /// output item
-        private static void SetArrayBasedItemsTagsDescriptionTitle(
-	        List allExifItems, FileIndexItem item)
-        {
-	        //  exifItem.Tags
-	        var tags = GetExifKeywords(allExifItems);
-	        if(!string.IsNullOrEmpty(tags)) // null = is not the right tag or empty tag
-	        {
-		        item.Tags = tags;
-	        }
-	        // Colour Class => ratings
-	        var colorClassString = GetColorClassString(allExifItems);
-	        if(!string.IsNullOrEmpty(colorClassString)) // null = is not the right tag or empty tag
-	        {
-		        item.ColorClass = ColorClassParser.GetColorClass(colorClassString);
-	        }
-                
-	        // [IPTC] Caption/Abstract
-	        var caption = GetCaptionAbstract(allExifItems);
-	        if(!string.IsNullOrEmpty(caption)) // null = is not the right tag or empty tag
-	        {
-		        item.Description = caption;
-	        }    
-                
-	        // [IPTC] Object Name = Title
-	        var title = GetObjectName(allExifItems);
-	        if(!string.IsNullOrEmpty(title)) // null = is not the right tag or empty tag
-	        {
-		        item.Title = title;
-	        }
-        }
-        
-
-        /// 
-        /// Combination setter Orientation
-        /// 
-        /// list of items
-        /// output item
-        private static void SetArrayBasedItemsOrientation(
-	        IEnumerable allExifItems, FileIndexItem item)
-        {
-	        // Orientation of image
-	        var orientation = GetOrientationFromExifItem(allExifItems);
-	        if (orientation != FileIndexItem.Rotation.DoNotChange)
-	        {
-		        item.Orientation = orientation;
-	        }
-        }
-
-        /// 
-        /// Combination setter Aperture Shutter SpeedIso
-        /// 
-        /// list of items
-        /// output item
-        private static void SetArrayBasedItemsApertureShutterSpeedIso(List allExifItems, FileIndexItem item)
-        {
-	        //    [Exif SubIFD] Aperture Value = f/2.2
-	        var aperture = GetAperture(allExifItems);
-	        if(Math.Abs(aperture) > 0) // 0f = is not the right tag or empty tag
-	        {
-		        item.Aperture = aperture;
-	        }
-	            
-	        // [Exif SubIFD] Shutter Speed Value = 1/2403 sec
-	        var shutterSpeed = GetShutterSpeedValue(allExifItems);
-	        if(shutterSpeed != string.Empty) // string.Empty = is not the right tag or empty tag
-	        {
-		        item.ShutterSpeed = shutterSpeed;
-	        }
-	            
-	        // [Exif SubIFD] ISO Speed Ratings = 25
-	        var isoSpeed = GetIsoSpeedValue(allExifItems);
-	        if(isoSpeed != 0) // 0 = is not the right tag or empty tag
-	        {
-		        item.SetIsoSpeed(isoSpeed);
-	        }
-        }
-
-        /// 
-        /// Combination setter Location Places
-        /// 
-        /// list of items
-        /// output item
-        private static void SetArrayBasedItemsLocationPlaces(
-	        List allExifItems, FileIndexItem item)
-        {
-	        //    [IPTC] City = Diepenveen
-	        var locationCity = GetLocationPlaces(allExifItems, "City","photoshop:City");
-	        if(!string.IsNullOrEmpty(locationCity)) // null = is not the right tag or empty tag
-	        {
-		        item.LocationCity = locationCity;
-	        }
-                
-	        //    [IPTC] Province/State = Overijssel
-	        var locationState = GetLocationPlaces(allExifItems, 
-		        "Province/State","photoshop:State");
-	        if(!string.IsNullOrEmpty(locationState)) // null = is not the right tag or empty tag
-	        {
-		        item.LocationState = locationState;
-	        }
-                
-	        //    [IPTC] Country/Primary Location Name = Nederland
-	        var locationCountry = GetLocationPlaces(allExifItems, 
-		        "Country/Primary Location Name","photoshop:Country");
-	        if(!string.IsNullOrEmpty(locationCountry)) // null = is not the right tag or empty tag
-	        {
-		        item.LocationCountry = locationCountry;
-	        }
-        }
-
-        /// 
-        /// Combination setter Focal Length and lens model
-        /// 
-        /// list of items
-        /// output item
-        private static void SetArrayBasedItemsLens(
-	        List allExifItems, FileIndexItem item)
-        {
-	        // [Exif SubIFD] Focal Length = 200 mm
-	        var focalLength = GetFocalLength(allExifItems);
-	        if (Math.Abs(focalLength) > 0.00001) 
-	        {
-		        item.FocalLength = focalLength;
-	        }
-	        
-	        var lensModel = GetMakeLensModel(allExifItems);
-	        if (lensModel != string.Empty)
-	        {
-		        item.SetMakeModel(lensModel,2);
-	        }
-        }
-
-        /// 
-        /// Combination setter for Make and Model
-        /// 
-        /// list of items
-        /// output item
-        private static void SetArrayBasedItemsMakeModel(
-	        List allExifItems, FileIndexItem item)
-        {
-	        var make = GetMakeModel(allExifItems,true);
-	        if (make != string.Empty) // string.Empty = is not the right tag or empty tag
-	        {
-		        item.SetMakeModel(make,0);
-	        }
-	            
-	        var model = GetMakeModel(allExifItems,false);
-	        if (model != string.Empty) // string.Empty = is not the right tag or empty tag
-	        {
-		        item.SetMakeModel(model,1);
-	        }
-	        
-	        // last & out of the loop
-	        var sonyLensModel = GetSonyMakeLensModel(allExifItems, item.LensModel);
-	        if ( !string.IsNullOrEmpty(sonyLensModel) )
-	        {
-		        item.SetMakeModel(sonyLensModel,2);
-	        }
-        }
-
-        /// 
-        /// Combination setter Software Stabilization
-        /// 
-        /// list of meta data
-        /// single item
-        private void SetArrayBasedItemsSoftwareStabilization(List allExifItems, FileIndexItem item)
-        {
-	        item.Software = GetSoftware(allExifItems);
-            
-	        item.ImageStabilisation = GetImageStabilisation(allExifItems);
-	        item.LocationCountryCode = GetLocationCountryCode(allExifItems);
-
-	        // DateTime of image
-	        var dateTime = GetExifDateTime(allExifItems, new CameraMakeModel(item.Make, item.Model));
-	        if ( dateTime != null )
-	        {
-		        item.DateTime = (DateTime)dateTime;
-	        }
-        }
-        
-        /// 
-        /// Currently only for Sony cameras
-        /// 
-        /// all items
-        /// Enum
-        private static ImageStabilisationType GetImageStabilisation(IEnumerable allExifItems)
-        {
-	        var sonyDirectory = allExifItems.OfType().FirstOrDefault();
-	        var imageStabilisation = sonyDirectory?.GetDescription(SonyType1MakernoteDirectory.TagImageStabilisation);
-	        // 0 	0x0000	Off
-	        // 1 	0x0001	On
-	        switch ( imageStabilisation )
-	        {
-		        case "Off":
-			        return ImageStabilisationType.Off;
-		        case "On":
-			        return ImageStabilisationType.On;
-	        }
-	        return ImageStabilisationType.Unknown;
-        }
-        
-        internal static string GetLocationCountryCode(List allExifItems)
-        {
-	        var iptcDirectory = allExifItems.OfType().FirstOrDefault();
-	        var countryCodeIptc = iptcDirectory?.GetDescription(IptcDirectory.TagCountryOrPrimaryLocationCode);
-
-	        if ( !string.IsNullOrEmpty(countryCodeIptc) )
-	        {
-		        return countryCodeIptc;
-	        }
-	        
-	        // XMP,http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/,Iptc4xmpCore:CountryCode,NLD
-	        var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-	        var countryCodeXmp =  GetXmpData(xmpDirectory, "Iptc4xmpCore:CountryCode");
-	        
-	        return countryCodeXmp;
-        }
-
-        private static string GetSonyMakeLensModel(IEnumerable allExifItems, string lensModel)
-        {
-	        // only if there is nothing yet
-	        if ( !string.IsNullOrEmpty(lensModel) ) return string.Empty;
-	        var sonyDirectory = allExifItems.OfType().FirstOrDefault();
-	        var lensId = sonyDirectory?.GetDescription(SonyType1MakernoteDirectory.TagLensId);
-	        
-	        return string.IsNullOrEmpty(lensId) ? string.Empty : SonyLensIdConverter.GetById(lensId);
-        }
+			SetArrayBasedItemsLocationPlaces(allExifItems, item);
+			SetArrayBasedItemsOrientation(allExifItems, item);
+			SetArrayBasedItemsLens(allExifItems, item);
+			SetArrayBasedItemsMakeModel(allExifItems, item);
+			SetArrayBasedItemsSoftwareStabilization(allExifItems, item);
+
+			return item;
+		}
+
+		/// 
+		/// Combination setter Tags Description Title ColorClass
+		/// 
+		/// list of items
+		/// output item
+		private static void SetArrayBasedItemsTagsDescriptionTitle(
+			List allExifItems, FileIndexItem item)
+		{
+			//  exifItem.Tags
+			var tags = GetExifKeywords(allExifItems);
+			if ( !string.IsNullOrEmpty(tags) ) // null = is not the right tag or empty tag
+			{
+				item.Tags = tags;
+			}
+			// Colour Class => ratings
+			var colorClassString = GetColorClassString(allExifItems);
+			if ( !string.IsNullOrEmpty(colorClassString) ) // null = is not the right tag or empty tag
+			{
+				item.ColorClass = ColorClassParser.GetColorClass(colorClassString);
+			}
+
+			// [IPTC] Caption/Abstract
+			var caption = GetCaptionAbstract(allExifItems);
+			if ( !string.IsNullOrEmpty(caption) ) // null = is not the right tag or empty tag
+			{
+				item.Description = caption;
+			}
+
+			// [IPTC] Object Name = Title
+			var title = GetObjectName(allExifItems);
+			if ( !string.IsNullOrEmpty(title) ) // null = is not the right tag or empty tag
+			{
+				item.Title = title;
+			}
+		}
+
+
+		/// 
+		/// Combination setter Orientation
+		/// 
+		/// list of items
+		/// output item
+		private static void SetArrayBasedItemsOrientation(
+			IEnumerable allExifItems, FileIndexItem item)
+		{
+			// Orientation of image
+			var orientation = GetOrientationFromExifItem(allExifItems);
+			if ( orientation != FileIndexItem.Rotation.DoNotChange )
+			{
+				item.Orientation = orientation;
+			}
+		}
+
+		/// 
+		/// Combination setter Aperture Shutter SpeedIso
+		/// 
+		/// list of items
+		/// output item
+		private static void SetArrayBasedItemsApertureShutterSpeedIso(List allExifItems, FileIndexItem item)
+		{
+			//    [Exif SubIFD] Aperture Value = f/2.2
+			var aperture = GetAperture(allExifItems);
+			if ( Math.Abs(aperture) > 0 ) // 0f = is not the right tag or empty tag
+			{
+				item.Aperture = aperture;
+			}
+
+			// [Exif SubIFD] Shutter Speed Value = 1/2403 sec
+			var shutterSpeed = GetShutterSpeedValue(allExifItems);
+			if ( shutterSpeed != string.Empty ) // string.Empty = is not the right tag or empty tag
+			{
+				item.ShutterSpeed = shutterSpeed;
+			}
+
+			// [Exif SubIFD] ISO Speed Ratings = 25
+			var isoSpeed = GetIsoSpeedValue(allExifItems);
+			if ( isoSpeed != 0 ) // 0 = is not the right tag or empty tag
+			{
+				item.SetIsoSpeed(isoSpeed);
+			}
+		}
+
+		/// 
+		/// Combination setter Location Places
+		/// 
+		/// list of items
+		/// output item
+		private static void SetArrayBasedItemsLocationPlaces(
+			List allExifItems, FileIndexItem item)
+		{
+			//    [IPTC] City = Diepenveen
+			var locationCity = GetLocationPlaces(allExifItems, "City", "photoshop:City");
+			if ( !string.IsNullOrEmpty(locationCity) ) // null = is not the right tag or empty tag
+			{
+				item.LocationCity = locationCity;
+			}
+
+			//    [IPTC] Province/State = Overijssel
+			var locationState = GetLocationPlaces(allExifItems,
+				"Province/State", "photoshop:State");
+			if ( !string.IsNullOrEmpty(locationState) ) // null = is not the right tag or empty tag
+			{
+				item.LocationState = locationState;
+			}
+
+			//    [IPTC] Country/Primary Location Name = Nederland
+			var locationCountry = GetLocationPlaces(allExifItems,
+				"Country/Primary Location Name", "photoshop:Country");
+			if ( !string.IsNullOrEmpty(locationCountry) ) // null = is not the right tag or empty tag
+			{
+				item.LocationCountry = locationCountry;
+			}
+		}
+
+		/// 
+		/// Combination setter Focal Length and lens model
+		/// 
+		/// list of items
+		/// output item
+		private static void SetArrayBasedItemsLens(
+			List allExifItems, FileIndexItem item)
+		{
+			// [Exif SubIFD] Focal Length = 200 mm
+			var focalLength = GetFocalLength(allExifItems);
+			if ( Math.Abs(focalLength) > 0.00001 )
+			{
+				item.FocalLength = focalLength;
+			}
+
+			var lensModel = GetMakeLensModel(allExifItems);
+			if ( lensModel != string.Empty )
+			{
+				item.SetMakeModel(lensModel, 2);
+			}
+		}
+
+		/// 
+		/// Combination setter for Make and Model
+		/// 
+		/// list of items
+		/// output item
+		private static void SetArrayBasedItemsMakeModel(
+			List allExifItems, FileIndexItem item)
+		{
+			var make = GetMakeModel(allExifItems, true);
+			if ( make != string.Empty ) // string.Empty = is not the right tag or empty tag
+			{
+				item.SetMakeModel(make, 0);
+			}
+
+			var model = GetMakeModel(allExifItems, false);
+			if ( model != string.Empty ) // string.Empty = is not the right tag or empty tag
+			{
+				item.SetMakeModel(model, 1);
+			}
+
+			// last & out of the loop
+			var sonyLensModel = GetSonyMakeLensModel(allExifItems, item.LensModel);
+			if ( !string.IsNullOrEmpty(sonyLensModel) )
+			{
+				item.SetMakeModel(sonyLensModel, 2);
+			}
+		}
+
+		/// 
+		/// Combination setter Software Stabilization
+		/// 
+		/// list of meta data
+		/// single item
+		private void SetArrayBasedItemsSoftwareStabilization(List allExifItems, FileIndexItem item)
+		{
+			item.Software = GetSoftware(allExifItems);
+
+			item.ImageStabilisation = GetImageStabilisation(allExifItems);
+			item.LocationCountryCode = GetLocationCountryCode(allExifItems);
+
+			// DateTime of image
+			var dateTime = GetExifDateTime(allExifItems, new CameraMakeModel(item.Make, item.Model));
+			if ( dateTime != null )
+			{
+				item.DateTime = ( DateTime )dateTime;
+			}
+		}
+
+		/// 
+		/// Currently only for Sony cameras
+		/// 
+		/// all items
+		/// Enum
+		private static ImageStabilisationType GetImageStabilisation(IEnumerable allExifItems)
+		{
+			var sonyDirectory = allExifItems.OfType().FirstOrDefault();
+			var imageStabilisation = sonyDirectory?.GetDescription(SonyType1MakernoteDirectory.TagImageStabilisation);
+			// 0 	0x0000	Off
+			// 1 	0x0001	On
+			switch ( imageStabilisation )
+			{
+				case "Off":
+					return ImageStabilisationType.Off;
+				case "On":
+					return ImageStabilisationType.On;
+			}
+			return ImageStabilisationType.Unknown;
+		}
+
+		internal static string GetLocationCountryCode(List allExifItems)
+		{
+			var iptcDirectory = allExifItems.OfType().FirstOrDefault();
+			var countryCodeIptc = iptcDirectory?.GetDescription(IptcDirectory.TagCountryOrPrimaryLocationCode);
+
+			if ( !string.IsNullOrEmpty(countryCodeIptc) )
+			{
+				return countryCodeIptc;
+			}
+
+			// XMP,http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/,Iptc4xmpCore:CountryCode,NLD
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+			var countryCodeXmp = GetXmpData(xmpDirectory, "Iptc4xmpCore:CountryCode");
+
+			return countryCodeXmp;
+		}
+
+		private static string GetSonyMakeLensModel(IEnumerable allExifItems, string lensModel)
+		{
+			// only if there is nothing yet
+			if ( !string.IsNullOrEmpty(lensModel) ) return string.Empty;
+			var sonyDirectory = allExifItems.OfType().FirstOrDefault();
+			var lensId = sonyDirectory?.GetDescription(SonyType1MakernoteDirectory.TagLensId);
+
+			return string.IsNullOrEmpty(lensId) ? string.Empty : SonyLensIdConverter.GetById(lensId);
+		}
 
 		private static ExtensionRolesHelper.ImageFormat GetFileSpecificTags(List allExifItems)
 		{
 			if ( allExifItems.Exists(p => p.Name == "JPEG") )
 				return ExtensionRolesHelper.ImageFormat.jpg;
-				
+
 			if ( allExifItems.Exists(p => p.Name == "PNG-IHDR") )
 				return ExtensionRolesHelper.ImageFormat.png;
-			
+
 			if ( allExifItems.Exists(p => p.Name == "BMP Header") )
-				return ExtensionRolesHelper.ImageFormat.bmp;	
-			
+				return ExtensionRolesHelper.ImageFormat.bmp;
+
 			if ( allExifItems.Exists(p => p.Name == "GIF Header") )
-				return ExtensionRolesHelper.ImageFormat.gif;	
-				
+				return ExtensionRolesHelper.ImageFormat.gif;
+
 			return ExtensionRolesHelper.ImageFormat.unknown;
 		}
 
 		public static FileIndexItem.Rotation GetOrientationFromExifItem(IEnumerable allExifItems)
 		{
 			var exifItem = allExifItems.OfType().FirstOrDefault();
-			
+
 			var caption = exifItem?.Tags
 				.FirstOrDefault(p => p.Type == ExifDirectoryBase.TagOrientation)
 				?.Description;
-            if (caption == null) return FileIndexItem.Rotation.DoNotChange;
-            
-            switch (caption)
-            {
-                case "Top, left side (Horizontal / normal)":
-                    return FileIndexItem.Rotation.Horizontal;
-                case "Right side, top (Rotate 90 CW)":
-                    return FileIndexItem.Rotation.Rotate90Cw;
-                case "Bottom, right side (Rotate 180)":
-                    return FileIndexItem.Rotation.Rotate180;
-                case "Left side, bottom (Rotate 270 CW)":
-                    return FileIndexItem.Rotation.Rotate270Cw;
-                default:
-                    return FileIndexItem.Rotation.Horizontal;
-            }
-        }
+			if ( caption == null ) return FileIndexItem.Rotation.DoNotChange;
+
+			switch ( caption )
+			{
+				case "Top, left side (Horizontal / normal)":
+					return FileIndexItem.Rotation.Horizontal;
+				case "Right side, top (Rotate 90 CW)":
+					return FileIndexItem.Rotation.Rotate90Cw;
+				case "Bottom, right side (Rotate 180)":
+					return FileIndexItem.Rotation.Rotate180;
+				case "Left side, bottom (Rotate 270 CW)":
+					return FileIndexItem.Rotation.Rotate270Cw;
+				default:
+					return FileIndexItem.Rotation.Horizontal;
+			}
+		}
 
 		private static string GetSoftware(IEnumerable allExifItems)
 		{
@@ -402,731 +402,733 @@ private static string GetSoftware(IEnumerable allExifItems)
 			return tagSoftware;
 		}
 
-	    private static string GetMakeModel(List allExifItems, bool isMake)
-	    {
-		    // [Exif IFD0] Make = SONY
-		    // [Exif IFD0] Model = SLT-A58
-
-		    var exifIfd0Directory = allExifItems.OfType().FirstOrDefault();
-		    var tagMakeModelExif = isMake ? ExifDirectoryBase.TagMake : ExifDirectoryBase.TagModel;
-		    
-		    var captionExifIfd0 = exifIfd0Directory?.GetDescription(tagMakeModelExif);
-		    if ( !string.IsNullOrEmpty(captionExifIfd0) )
-		    {
-			    return captionExifIfd0;
-		    }
-		    
-		    var quickTimeMetaDataDirectory = allExifItems.OfType().FirstOrDefault();
-		    var tagMakeModelQuickTime = isMake ? 
-			    QuickTimeMetadataHeaderDirectory.TagMake : QuickTimeMetadataHeaderDirectory.TagModel;
-		    
-		    var captionQuickTime = quickTimeMetaDataDirectory?.GetDescription(tagMakeModelQuickTime);
-		    return !string.IsNullOrEmpty(captionQuickTime) ? captionQuickTime : string.Empty;
-	    }
-
-	    /// 
-	    /// [Exif SubIFD] Lens Model = E 18-200mm F3.5-6.3 OSS LE
-	    /// 
-	    /// 
-	    /// 
-	    private static string GetMakeLensModel(IEnumerable allExifItems)
-	    {
-		    var exifIfd0Directory = allExifItems.OfType().FirstOrDefault();
-		    var lensModel = exifIfd0Directory?.GetDescription(ExifDirectoryBase.TagLensModel);
-		    
-		    lensModel ??= string.Empty;
-		    return lensModel == "----" ? string.Empty : lensModel;
-	    }
-
-
-	    private void DisplayAllExif(IEnumerable allExifItems)
-        {
-	        if ( _appSettings == null || !_appSettings.IsVerbose() )
-	        {
-		        return;
-	        }
-	        
-            foreach (var exifItem in allExifItems) {
-	            foreach ( var tag in exifItem.Tags )
-	            {
-		            _logger.LogDebug($"[{exifItem.Name}] {tag.Name} = {tag.Description}");
-	            }
-	            
-                // for xmp notes
-                if ( exifItem is not XmpDirectory xmpDirectory ||
-                     xmpDirectory.XmpMeta == null ) continue;
-                
-                foreach (var property in xmpDirectory.XmpMeta.Properties.Where(
-	                         p => !string.IsNullOrEmpty(p.Path)))
-                {
-	                _logger.LogDebug($"{exifItem.Name},{property.Namespace},{property.Path},{property.Value}");
-                }
-            }
-        }
-
-	    /// 
-	    /// Read "dc:subject" values from XMP
-	    /// 
-	    /// item
-	    /// 
-	    private static string GetXmpDataSubject(Directory? exifItem)
-	    {
-		    if ( !( exifItem is XmpDirectory xmpDirectory ) || xmpDirectory.XmpMeta == null )
-			    return string.Empty;
-		    
-		    var tagsList = new HashSet();
-		    foreach (var property in xmpDirectory.XmpMeta.Properties.Where(p => !string.IsNullOrEmpty(p.Value) 
-			             && p.Path.StartsWith("dc:subject[")))
-		    {
-			    tagsList.Add(property.Value);
-		    }
-		    return HashSetHelper.HashSetToString(tagsList);
-	    }
-
-	    private static string GetXmpData(Directory? exifItem, string propertyPath)
-	    {
-		    // for xmp notes
-		    if ( exifItem is not XmpDirectory xmpDirectory || xmpDirectory.XmpMeta == null )
-			    return string.Empty;
-		    
-		    var result = ( from property in xmpDirectory.XmpMeta.
-				    Properties.Where(p => !string.IsNullOrEmpty(p.Value)) 
-			    where property.Path == propertyPath select property.Value ).FirstOrDefault();
-		    result ??= string.Empty;
-		    return result;
-	    }
-
-        public static string GetObjectName (List allExifItems)
-        {
-	        var iptcDirectory = allExifItems.OfType().FirstOrDefault();
-
-            var objectName = iptcDirectory?.Tags.FirstOrDefault(
-	            p => p.Name == "Object Name")?.Description;
-            
-            if ( ! string.IsNullOrEmpty(objectName) )
-            {
-	            return objectName;
-            }
-            
-            // Xmp readings
-            var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-            return GetXmpData(xmpDirectory, "dc:title[1]");
-        }
-        
-        public static string GetCaptionAbstract(List allExifItems)
-        {
-	        var iptcDirectory = allExifItems.OfType().FirstOrDefault();
-            var caption = iptcDirectory?.GetDescription(IptcDirectory.TagCaption);
-
-            if ( !string.IsNullOrEmpty(caption) ) return caption;
-            
-            var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-            return GetXmpData(xmpDirectory, "dc:description[1]");
-        }
-        
-        public static string GetExifKeywords(List allExifItems)
-        {
-	        var iptcDirectory = allExifItems.OfType().FirstOrDefault();
-	        var keyWords = iptcDirectory?.GetDescription(IptcDirectory.TagKeywords);
-
-            if ( string.IsNullOrEmpty(keyWords) )
-            {
-	            var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-	            return GetXmpDataSubject(xmpDirectory);
-            }
-            
-            if (!string.IsNullOrWhiteSpace(keyWords))
-            {
-	            keyWords = keyWords.Replace(";", ", ");
-            }
-
-            return keyWords;
-        }
-
-        private static string GetColorClassString(List allExifItems)
+		private static string GetMakeModel(List allExifItems, bool isMake)
+		{
+			// [Exif IFD0] Make = SONY
+			// [Exif IFD0] Model = SLT-A58
+
+			var exifIfd0Directory = allExifItems.OfType().FirstOrDefault();
+			var tagMakeModelExif = isMake ? ExifDirectoryBase.TagMake : ExifDirectoryBase.TagModel;
+
+			var captionExifIfd0 = exifIfd0Directory?.GetDescription(tagMakeModelExif);
+			if ( !string.IsNullOrEmpty(captionExifIfd0) )
+			{
+				return captionExifIfd0;
+			}
+
+			var quickTimeMetaDataDirectory = allExifItems.OfType().FirstOrDefault();
+			var tagMakeModelQuickTime = isMake ?
+				QuickTimeMetadataHeaderDirectory.TagMake : QuickTimeMetadataHeaderDirectory.TagModel;
+
+			var captionQuickTime = quickTimeMetaDataDirectory?.GetDescription(tagMakeModelQuickTime);
+			return !string.IsNullOrEmpty(captionQuickTime) ? captionQuickTime : string.Empty;
+		}
+
+		/// 
+		/// [Exif SubIFD] Lens Model = E 18-200mm F3.5-6.3 OSS LE
+		/// 
+		/// 
+		/// 
+		private static string GetMakeLensModel(IEnumerable allExifItems)
+		{
+			var exifIfd0Directory = allExifItems.OfType().FirstOrDefault();
+			var lensModel = exifIfd0Directory?.GetDescription(ExifDirectoryBase.TagLensModel);
+
+			lensModel ??= string.Empty;
+			return lensModel == "----" ? string.Empty : lensModel;
+		}
+
+
+		private void DisplayAllExif(IEnumerable allExifItems)
+		{
+			if ( _appSettings == null || !_appSettings.IsVerbose() )
+			{
+				return;
+			}
+
+			foreach ( var exifItem in allExifItems )
+			{
+				foreach ( var tag in exifItem.Tags )
+				{
+					_logger.LogDebug($"[{exifItem.Name}] {tag.Name} = {tag.Description}");
+				}
+
+				// for xmp notes
+				if ( exifItem is not XmpDirectory xmpDirectory ||
+					 xmpDirectory.XmpMeta == null ) continue;
+
+				foreach ( var property in xmpDirectory.XmpMeta.Properties.Where(
+							 p => !string.IsNullOrEmpty(p.Path)) )
+				{
+					_logger.LogDebug($"{exifItem.Name},{property.Namespace},{property.Path},{property.Value}");
+				}
+			}
+		}
+
+		/// 
+		/// Read "dc:subject" values from XMP
+		/// 
+		/// item
+		/// 
+		private static string GetXmpDataSubject(Directory? exifItem)
+		{
+			if ( !( exifItem is XmpDirectory xmpDirectory ) || xmpDirectory.XmpMeta == null )
+				return string.Empty;
+
+			var tagsList = new HashSet();
+			foreach ( var property in xmpDirectory.XmpMeta.Properties.Where(p => !string.IsNullOrEmpty(p.Value)
+						 && p.Path.StartsWith("dc:subject[")) )
+			{
+				tagsList.Add(property.Value);
+			}
+			return HashSetHelper.HashSetToString(tagsList);
+		}
+
+		private static string GetXmpData(Directory? exifItem, string propertyPath)
+		{
+			// for xmp notes
+			if ( exifItem is not XmpDirectory xmpDirectory || xmpDirectory.XmpMeta == null )
+				return string.Empty;
+
+			var result = ( from property in xmpDirectory.XmpMeta.
+					Properties.Where(p => !string.IsNullOrEmpty(p.Value))
+						   where property.Path == propertyPath
+						   select property.Value ).FirstOrDefault();
+			result ??= string.Empty;
+			return result;
+		}
+
+		public static string GetObjectName(List allExifItems)
+		{
+			var iptcDirectory = allExifItems.OfType().FirstOrDefault();
+
+			var objectName = iptcDirectory?.Tags.FirstOrDefault(
+				p => p.Name == "Object Name")?.Description;
+
+			if ( !string.IsNullOrEmpty(objectName) )
+			{
+				return objectName;
+			}
+
+			// Xmp readings
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+			return GetXmpData(xmpDirectory, "dc:title[1]");
+		}
+
+		public static string GetCaptionAbstract(List allExifItems)
+		{
+			var iptcDirectory = allExifItems.OfType().FirstOrDefault();
+			var caption = iptcDirectory?.GetDescription(IptcDirectory.TagCaption);
+
+			if ( !string.IsNullOrEmpty(caption) ) return caption;
+
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+			return GetXmpData(xmpDirectory, "dc:description[1]");
+		}
+
+		public static string GetExifKeywords(List allExifItems)
+		{
+			var iptcDirectory = allExifItems.OfType().FirstOrDefault();
+			var keyWords = iptcDirectory?.GetDescription(IptcDirectory.TagKeywords);
+
+			if ( string.IsNullOrEmpty(keyWords) )
+			{
+				var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+				return GetXmpDataSubject(xmpDirectory);
+			}
+
+			if ( !string.IsNullOrWhiteSpace(keyWords) )
+			{
+				keyWords = keyWords.Replace(";", ", ");
+			}
+
+			return keyWords;
+		}
+
+		private static string GetColorClassString(List allExifItems)
 		{
-	        var exifItem = allExifItems.OfType().FirstOrDefault();
-	        var colorClassSting = string.Empty;
-            var ratingCounts = exifItem?.Tags.Count(p => p.DirectoryName == "IPTC" && p.Name.Contains("0x02dd"));
-            if (ratingCounts >= 1)
-            {
-                var prefsTag = exifItem!.Tags.FirstOrDefault(p => 
-	                p.DirectoryName == "IPTC" && p.Name.Contains("0x02dd"))?.Description;
-    
-                // Results for example
-                //     0:1:0:-00001
-                //     ~~~~~~
-                //     0:8:0:-00001
-                        
-                if (!string.IsNullOrWhiteSpace(prefsTag))
-                {
-                    var prefsTagSplit = prefsTag.Split(":".ToCharArray());
-                    colorClassSting = prefsTagSplit[1];     
-                }
-                return colorClassSting;
-            }
-            
-            // Xmp readings
-            var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-            colorClassSting = GetXmpData(xmpDirectory, "photomechanic:ColorClass");
-            return colorClassSting;
-        }
-
-        /// 
-        /// Get the EXIF/SubIFD or PNG Created Datetime
-        /// 
-        /// Directory
-        /// cameraMakeModel
-        /// Datetime
-        internal DateTime? GetExifDateTime(List allExifItems, CameraMakeModel? cameraMakeModel = null)
-        {
-	        var provider = CultureInfo.InvariantCulture;
-
-	        var itemDateTimeSubIfd = ParseSubIfdDateTime(allExifItems, provider);
-	        if ( itemDateTimeSubIfd.Year >= 2 )
-	        {
-		        return itemDateTimeSubIfd;
-	        }
-			
-	        var itemDateTimeQuickTime = ParseQuickTimeDateTime(cameraMakeModel, allExifItems);
-
-	        // to avoid errors scanning gpx files (with this it would be Local)
-
-	        if ( itemDateTimeQuickTime.Year >= 1970 )
-	        {
-		        return itemDateTimeQuickTime;
-	        }
-	        
-	        // 1970-01-01T02:00:03 formatted
-	        var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-
-	        var photoShopDateCreated = GetXmpData(xmpDirectory, "photoshop:DateCreated");
-	        DateTime.TryParseExact(photoShopDateCreated, "yyyy-MM-ddTHH:mm:ss", provider, 
-		        DateTimeStyles.AdjustToUniversal, out var xmpItemDateTime);
-	        
-	        if ( xmpItemDateTime.Year >= 2 )
-	        {
-		        return xmpItemDateTime;
-	        }
-	        
-	        return null;
-        }
-
-        internal static DateTime ParseSubIfdDateTime(IEnumerable allExifItems, IFormatProvider provider)
-        {
-	        var pattern = "yyyy:MM:dd HH:mm:ss";
-
-	        // Raw files have multiple ExifSubIfdDirectories
-	        var exifSubIfdList = allExifItems.OfType().ToList();
-	        foreach ( var exifSubIfd in exifSubIfdList )
-	        {
-		        // https://odedcoster.com/blog/2011/12/13/date-and-time-format-strings-in-net-understanding-format-strings/
-		        // 2018:01:01 11:29:36
-		        var tagDateTimeDigitized = exifSubIfd.GetDescription(ExifDirectoryBase.TagDateTimeDigitized);
-		        DateTime.TryParseExact(tagDateTimeDigitized, 
-			        pattern, provider, DateTimeStyles.AdjustToUniversal, out var itemDateTimeDigitized);
-		        if ( itemDateTimeDigitized.Year >= 2 )
-		        {
-			        return itemDateTimeDigitized;
-		        }
-	        
-		        var tagDateTimeOriginal = exifSubIfd.GetDescription(ExifDirectoryBase.TagDateTimeOriginal);
-		        DateTime.TryParseExact(tagDateTimeOriginal, 
-			        pattern, provider, DateTimeStyles.AdjustToUniversal, out var itemDateTimeOriginal);
-		        if ( itemDateTimeOriginal.Year >= 2 )
-		        {
-			        return itemDateTimeOriginal;
-		        }
-	        }
-
-	        return new DateTime(0, DateTimeKind.Utc);
-        }
-
-        internal DateTime ParseQuickTimeDateTime(CameraMakeModel? cameraMakeModel,
-	        IEnumerable allExifItems)
-        {
-	        if ( _appSettings == null ) Console.WriteLine("[ParseQuickTimeDateTime] app settings is null");
-	        cameraMakeModel ??= new CameraMakeModel();
-
-	        if ( _appSettings is { VideoUseLocalTime: null } )
-	        {
-		        _appSettings.VideoUseLocalTime = new List();
-	        }
-	        
-	        var useUseLocalTime = _appSettings?.VideoUseLocalTime.Exists(p =>
-		        string.Equals(p.Make, cameraMakeModel.Make, StringComparison.InvariantCultureIgnoreCase) && (
-			        string.Equals(p.Model, cameraMakeModel.Model, StringComparison.InvariantCultureIgnoreCase) ||
-			        string.IsNullOrEmpty(p.Model) ));
-	        
-	        
-	        var quickTimeDirectory = allExifItems.OfType().FirstOrDefault();
-
-	        var quickTimeCreated = quickTimeDirectory?.GetDescription(QuickTimeMovieHeaderDirectory.TagCreated);
-
-	        var dateTimeStyle = useUseLocalTime == true
-		        ? DateTimeStyles.AdjustToUniversal
-		        : DateTimeStyles.AssumeLocal;
-	        
-	        // [QuickTime Movie Header] Created = Tue Oct 11 09:40:04 2011 or Sat Mar 20 21:29:11 2010 // time is in UTC
-	        // Or Dutch (NL-nl) "zo mrt. 29 13:10:07 2020"
-	        DateTime.TryParseExact(quickTimeCreated, "ddd MMM dd HH:mm:ss yyyy", CultureInfo.CurrentCulture, 
-		        dateTimeStyle, out var itemDateTimeQuickTime);
-
-	        // ReSharper disable once InvertIf
-	        if ( useUseLocalTime != true && _appSettings?.CameraTimeZoneInfo != null)
-	        {
-		        itemDateTimeQuickTime = DateTime.SpecifyKind(itemDateTimeQuickTime, DateTimeKind.Utc);
-		        itemDateTimeQuickTime =  TimeZoneInfo.ConvertTime(itemDateTimeQuickTime, 
-			        TimeZoneInfo.Utc, _appSettings.CameraTimeZoneInfo); 
-	        }
-
-	        return itemDateTimeQuickTime;
-        }
-        
-        /// 
-        /// 51° 57' 2.31"
-        /// 
-        /// 
-        /// 
-        private static double GetGeoLocationLatitude(List allExifItems)
-        {
-            var latitudeString = string.Empty;
-            var latitudeRef = string.Empty;
-            
-            foreach (var exifItemTag in allExifItems.Select(p => p.Tags))
-            {
-                var latitudeRefLocal = exifItemTag.FirstOrDefault(
-                    p => p.DirectoryName == "GPS" 
-                    && p.Name == "GPS Latitude Ref")?.Description;
-                
-                if (latitudeRefLocal != null)
-                {
-                    latitudeRef = latitudeRefLocal;
-                }
-                
-                var latitudeLocal = exifItemTag.FirstOrDefault(
-                    p => p.DirectoryName == "GPS" 
-                         && p.Name == "GPS Latitude")?.Description;
-
-                if (latitudeLocal != null)
-                {
-                    latitudeString = latitudeLocal;
-                    continue;
-                }
-
-                var locationQuickTime = exifItemTag.FirstOrDefault(
-	                p => p.DirectoryName == "QuickTime Metadata Header" 
-	                     && p.Name == "GPS Location")?.Description;
-                if ( locationQuickTime != null)
-                {
-	                return GeoParser.ParseIsoString(locationQuickTime).Latitude;
-                }
-            }
-
-            if ( string.IsNullOrWhiteSpace(latitudeString) )
-	            return GetXmpGeoData(allExifItems, "exif:GPSLatitude");
-            
-            var latitude = GeoParser.ConvertDegreeMinutesSecondsToDouble(latitudeString, latitudeRef);
-            return  Math.Floor(latitude * 10000000000) / 10000000000;
-
-        }
-
-        internal static double GetXmpGeoData(List allExifItems, string propertyPath)
-        {
-	        var latitudeString = string.Empty;
-	        var latitudeRef = string.Empty;
-	        
-	        foreach ( var exifItem in allExifItems )
-	        {
-		        // exif:GPSLatitude,45,33.615N
-		        var latitudeLocal = GetXmpData(exifItem, propertyPath);
-		        if(string.IsNullOrEmpty(latitudeLocal)) continue;
-		        var split = Regex.Split(latitudeLocal, "[NSWE]", 
-			        RegexOptions.None, TimeSpan.FromMilliseconds(100));
-		        if(split.Length != 2) continue;
-		        latitudeString = split[0];
-		        latitudeRef = latitudeLocal[^1].ToString();
-	        }
-
-	        if ( string.IsNullOrWhiteSpace(latitudeString) ) return 0;
-            
-	        var latitudeDegreeMinutes = GeoParser.ConvertDegreeMinutesToDouble(latitudeString, latitudeRef);
-	        return Math.Floor(latitudeDegreeMinutes * 10000000000) / 10000000000; 
-        }
-        
-        private static double GetGeoLocationAltitude(List allExifItems)
-        {
-            //    [GPS] GPS Altitude Ref = Below sea level
-            //    [GPS] GPS Altitude = 2 metres
-
-            var altitudeString = string.Empty;
-            var altitudeRef = string.Empty;
-            
-            foreach (var exifItemTags in allExifItems.Select(p => p.Tags))
-            {
-                var longitudeRefLocal = exifItemTags.FirstOrDefault(
-                    p => p.DirectoryName == "GPS" 
-                         && p.Name == "GPS Altitude Ref")?.Description;
-                
-                if (longitudeRefLocal != null)
-                {
-                    altitudeRef = longitudeRefLocal;
-                }
-                
-                var altitudeLocal = exifItemTags.FirstOrDefault(
-                    p => p.DirectoryName == "GPS" 
-                         && p.Name == "GPS Altitude")?.Description;
-
-                if (altitudeLocal != null)
-                {
-	                // space metres
-                    altitudeString = altitudeLocal.Replace(" metres",string.Empty);
-                }
-            }
-
-            // this value is always an int but current culture formatted
-            var parsedAltitudeString = double.TryParse(altitudeString, 
-	            NumberStyles.Number, CultureInfo.CurrentCulture, out var altitude);
-            
-            if (altitudeRef == "Below sea level" ) altitude *= -1;
-
-            // Read xmp if altitudeString is string.Empty
-            if (!parsedAltitudeString)
-            {
-	            return GetXmpGeoAlt(allExifItems);
-            }
-
-            return (int) altitude;
-        }
-
-        internal static double GetXmpGeoAlt(List allExifItems)
-        {
-	        var altitudeRef = true;
-	        var altitude = 0d;
-	        
-	        // example:
-	        // +1
-	        // XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitude,1/1
-	        // XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitudeRef,0
-
-	        // -10
-	        // XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitude,10/1
-	        // XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitudeRef,1
-	        
-	        foreach ( var exifItem in allExifItems )
-	        {
-		        if ( !( exifItem is XmpDirectory xmpDirectory ) || xmpDirectory.XmpMeta == null )
-			        continue;
-
-		        foreach (var property in xmpDirectory.XmpMeta.Properties.Where(p =>
-			        !string.IsNullOrEmpty(p.Value)) )
-		        {
-			        switch ( property.Path )
-			        {
-				        case "exif:GPSAltitude":
-					        altitude = MathFraction.Fraction(property.Value);
-					        break;
-				        case "exif:GPSAltitudeRef":
-					        altitudeRef = property.Value == "1";
-					        break;
-			        }
-		        }
-	        }
-	        
-	        // no -0 as result
-	        if(Math.Abs(altitude) < 0.001) return 0;
-	        
-	        if (altitudeRef) altitude *= -1;
-	        return altitude;
-        }
-        
-        
-        private static double GetGeoLocationLongitude(List allExifItems)
-        {
-            var longitudeString = string.Empty;
-            var longitudeRef = string.Empty;
-            
-            foreach (var exifItemTags in allExifItems.Select(p => p.Tags))
-            {
-                var longitudeRefLocal = exifItemTags.FirstOrDefault(
-                    p => p.DirectoryName == "GPS" 
-                         && p.Name == "GPS Longitude Ref")?.Description;
-                
-                if (longitudeRefLocal != null)
-                {
-                    longitudeRef = longitudeRefLocal;
-                }
-                
-                var longitudeLocal = exifItemTags.FirstOrDefault(
-                    p => p.DirectoryName == "GPS" 
-                         && p.Name == "GPS Longitude")?.Description;
-
-                if (longitudeLocal != null)
-                {
-                    longitudeString = longitudeLocal;
-                    continue;
-                }
-
-                var locationQuickTime = exifItemTags.FirstOrDefault(
-	                p => p.DirectoryName == "QuickTime Metadata Header" 
-	                     && p.Name == "GPS Location")?.Description;
-                if ( locationQuickTime != null)
-                {
-	                return GeoParser.ParseIsoString(locationQuickTime).Longitude;
-                }
-            }
-
-            if (!string.IsNullOrWhiteSpace(longitudeString))
-            {
-                var longitude = GeoParser.
-	                ConvertDegreeMinutesSecondsToDouble(longitudeString, longitudeRef);
-                longitude = Math.Floor(longitude * 10000000000) / 10000000000; 
-                return longitude;
-            }
-            
-            return GetXmpGeoData(allExifItems, "exif:GPSLongitude");
-        }
-
-        private static int GetImageWidthHeightMaxCount(string dirName, ICollection allExifItems)
-        {
-            var maxCount =  6;
-            if(dirName == "Exif SubIFD") maxCount = 30; // on header place 17&18
-            if (allExifItems.Count <= 5) maxCount = allExifItems.Count;
-            return maxCount;
-        }
-
-        private static string GetImageWidthTypeName(string dirName,
-	        bool isWidth)
-        {
-	        var typeName = "Image Height";
-	        if(dirName == "QuickTime Track Header") typeName = "Height";
-                
-	        if (isWidth) typeName = "Image Width";
-	        if(isWidth && dirName == "QuickTime Track Header") typeName = "Width";
-	        return typeName;
-        }
-            
-        public static int GetImageWidthHeight(List allExifItems, bool isWidth)
-        {
-            // The size lives normally in the first 5 headers
-            // > "Exif IFD0" .dng
-            // [Exif SubIFD] > arw; on header place 17&18
-            var directoryNames = new[] {"JPEG", "PNG-IHDR", "BMP Header", "GIF Header", 
-	            "QuickTime Track Header", "Exif IFD0", "Exif SubIFD"};
-            foreach (var dirName in directoryNames)
-            {
-	            var typeName = GetImageWidthTypeName(dirName,isWidth);
-                var maxCount = GetImageWidthHeightMaxCount(dirName, allExifItems);
-                
-                for (var i = 0; i < maxCount; i++)
-                {
-	                if ( i >= allExifItems.Count )
-	                {
-		                continue;
-	                }
-                    var exifItem = allExifItems[i];
-                    var value = GetImageSizeInsideLoop(exifItem, dirName, typeName);
-                    if ( value != 0 ) return value;
-                }
-            }
-            return 0;
-        }
-
-        private static int GetImageSizeInsideLoop( Directory exifItem, string dirName, string typeName)
-        {
-	        var ratingCountsJpeg =
-		        exifItem.Tags.Count(p => p.DirectoryName == dirName 
-		                                 && p.Name.Contains(typeName) && p.Description != "0");
-	        if (ratingCountsJpeg >= 1)
-	        {
-		        var widthTag = exifItem.Tags
-			        .FirstOrDefault(p => p.DirectoryName == dirName 
-			                             && p.Name.Contains(typeName) && p.Description != "0")
-			        ?.Description;
-		        widthTag = widthTag?.Replace(" pixels", string.Empty);
-		        if ( int.TryParse(widthTag, out var widthInt) )
-		        {
-			        return widthInt >= 1 ? widthInt : 0; // (widthInt >= 1) return widthInt)
-		        }
-	        }
-	        return 0;
-        }
-        
-
-        /// 
-        ///     For the location element
-        ///    [IPTC] City = Diepenveen
-        ///    [IPTC] Province/State = Overijssel
-        ///    [IPTC] Country/Primary Location Name = Nederland
-        /// 
-        /// 
-        /// City, State or Country
-        /// photoshop:State
-        /// 
-        private static string GetLocationPlaces(List allExifItems, string iptcName, string xmpPropertyPath)
-        {
-	        var iptcDirectoryDirectory = allExifItems.OfType().FirstOrDefault();
-	        
-            var tCounts = iptcDirectoryDirectory?.Tags.Count(p => p.DirectoryName == "IPTC" && p.Name == iptcName);
-            if ( tCounts < 1 )
-            {
-	            var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-	            return GetXmpData(xmpDirectory, xmpPropertyPath);
-            }
-            
-            var locationCity = iptcDirectoryDirectory?.Tags
-	            .FirstOrDefault(p => p.Name == iptcName)?.Description;
-            locationCity ??= string.Empty;
-            return locationCity;
-        }
-
-        /// 
-        /// [Exif SubIFD] Focal Length
-        /// 
-        /// 
-        private static double GetFocalLength(List allExifItems)
-        {
-	        var exifSubIfdDirectory = allExifItems.OfType().FirstOrDefault();
-	        var focalLengthString = exifSubIfdDirectory?.GetDescription(ExifDirectoryBase.TagFocalLength);
-
-	        var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-
-	        // XMP,http://ns.adobe.com/exif/1.0/,exif:FocalLength,11/1
-	        var focalLengthXmp = GetXmpData(xmpDirectory, "exif:FocalLength");
-	        if (string.IsNullOrEmpty(focalLengthString) && !string.IsNullOrEmpty(focalLengthXmp))
-	        {
-		        return Math.Round(MathFraction.Fraction(focalLengthXmp), 5);
-	        }
-	        
-	        if ( string.IsNullOrWhiteSpace(focalLengthString) ) return 0d;
-
-	        focalLengthString = focalLengthString.Replace(" mm", string.Empty);
-	        
-	        // Note: focalLengthString: (Dutch) 2,2 or (English) 2.2 based CultureInfo.CurrentCulture
-	        float.TryParse(focalLengthString, NumberStyles.Number, 
-		        CultureInfo.CurrentCulture, out var focalLength);
-	        
-	        return Math.Round(focalLength, 5);
-        }
-
-
-        private static double GetAperture(List allExifItems)
+			var exifItem = allExifItems.OfType().FirstOrDefault();
+			var colorClassSting = string.Empty;
+			var ratingCounts = exifItem?.Tags.Count(p => p.DirectoryName == "IPTC" && p.Name.Contains("0x02dd"));
+			if ( ratingCounts >= 1 )
+			{
+				var prefsTag = exifItem!.Tags.FirstOrDefault(p =>
+					p.DirectoryName == "IPTC" && p.Name.Contains("0x02dd"))?.Description;
+
+				// Results for example
+				//     0:1:0:-00001
+				//     ~~~~~~
+				//     0:8:0:-00001
+
+				if ( !string.IsNullOrWhiteSpace(prefsTag) )
+				{
+					var prefsTagSplit = prefsTag.Split(":".ToCharArray());
+					colorClassSting = prefsTagSplit[1];
+				}
+				return colorClassSting;
+			}
+
+			// Xmp readings
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+			colorClassSting = GetXmpData(xmpDirectory, "photomechanic:ColorClass");
+			return colorClassSting;
+		}
+
+		/// 
+		/// Get the EXIF/SubIFD or PNG Created Datetime
+		/// 
+		/// Directory
+		/// cameraMakeModel
+		/// Datetime
+		internal DateTime? GetExifDateTime(List allExifItems, CameraMakeModel? cameraMakeModel = null)
+		{
+			var provider = CultureInfo.InvariantCulture;
+
+			var itemDateTimeSubIfd = ParseSubIfdDateTime(allExifItems, provider);
+			if ( itemDateTimeSubIfd.Year >= 2 )
+			{
+				return itemDateTimeSubIfd;
+			}
+
+			var itemDateTimeQuickTime = ParseQuickTimeDateTime(cameraMakeModel, allExifItems);
+
+			// to avoid errors scanning gpx files (with this it would be Local)
+
+			if ( itemDateTimeQuickTime.Year >= 1970 )
+			{
+				return itemDateTimeQuickTime;
+			}
+
+			// 1970-01-01T02:00:03 formatted
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+
+			var photoShopDateCreated = GetXmpData(xmpDirectory, "photoshop:DateCreated");
+			DateTime.TryParseExact(photoShopDateCreated, "yyyy-MM-ddTHH:mm:ss", provider,
+				DateTimeStyles.AdjustToUniversal, out var xmpItemDateTime);
+
+			if ( xmpItemDateTime.Year >= 2 )
+			{
+				return xmpItemDateTime;
+			}
+
+			return null;
+		}
+
+		internal static DateTime ParseSubIfdDateTime(IEnumerable allExifItems, IFormatProvider provider)
 		{
-	        var exifItem = allExifItems.OfType().FirstOrDefault();
-
-	        // "Exif SubIFD"
-		    var apertureString = exifItem?.Tags.FirstOrDefault(p => 
-			    p.Name == "Aperture Value")?.Description;
-
-		    if (string.IsNullOrEmpty(apertureString))
-		    {
-			    apertureString = exifItem?.Tags.FirstOrDefault(p => 
-				    p.Name == "F-Number")?.Description;
-		    }
-		    
-		    // XMP,http://ns.adobe.com/exif/1.0/,exif:FNumber,9/1
-		    var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-		    var fNumberXmp = GetXmpData(xmpDirectory, "exif:FNumber");
-
-		    if (string.IsNullOrEmpty(apertureString) && !string.IsNullOrEmpty(fNumberXmp))
-		    {
-			    return MathFraction.Fraction(fNumberXmp);
-		    }
-		    
-		    if(apertureString == null) return 0d; 
-		    
-		    apertureString = apertureString.Replace("f/", string.Empty);
-		    // Note: apertureString: (Dutch) 2,2 or (English) 2.2 based CultureInfo.CurrentCulture
-		    float.TryParse(apertureString, NumberStyles.Number, CultureInfo.CurrentCulture, out var aperture);
-		    
-		    return aperture;
-	    }
-	    
-	    /// 
-	    /// [Exif SubIFD] Shutter Speed Value = 1/2403 sec
-	    /// 
-	    /// item to look in
-	    /// value
-	    private static string GetShutterSpeedValue(List allExifItems)
-	    {
-		    var exifItem = allExifItems.OfType().FirstOrDefault();
-
-		    // Exif SubIFD
-		    var shutterSpeedString = exifItem?.Tags.FirstOrDefault(p => 
-			    p.Name == "Shutter Speed Value")?.Description;
-
-		    if (string.IsNullOrEmpty(shutterSpeedString))
-		    {
-			    // Exif SubIFD
-			    shutterSpeedString = exifItem?.Tags.FirstOrDefault(p => 
-				    p.Name == "Exposure Time")?.Description;
-		    }
-		    
-		    // XMP,http://ns.adobe.com/exif/1.0/,exif:ExposureTime,1/20
-		    var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-
-		    var exposureTimeXmp = GetXmpData(xmpDirectory, "exif:ExposureTime");
-		    if (string.IsNullOrEmpty(shutterSpeedString) && 
-		        !string.IsNullOrEmpty(exposureTimeXmp) && exposureTimeXmp.Length <= 20)
-		    {
-			    return exposureTimeXmp;
-		    }
-		    
-		    if(shutterSpeedString == null) return string.Empty; 
-		    // the database has a 20 char limit
-		    if(shutterSpeedString.Length >= 20) return string.Empty;
-		    
-		    // in xmp there is only a field with for example: 1/33
-		    shutterSpeedString = shutterSpeedString.Replace(" sec", string.Empty);
-		    return shutterSpeedString;
-	    }
-
-	    internal static int GetIsoSpeedValue(List allExifItems)
-	    {
-		    var subIfdItem = allExifItems.OfType().FirstOrDefault();
-
-		    // Exif SubIFD
-		    var isoSpeedString = subIfdItem?.Tags.FirstOrDefault(p => 
-			    p.Name == "ISO Speed Ratings")?.Description;
-
-		    if ( string.IsNullOrEmpty(isoSpeedString) )
-		    {
-			    var canonMakerNoteDirectory = allExifItems.OfType().FirstOrDefault();
-
-			    // Canon Makernote
-			    isoSpeedString = canonMakerNoteDirectory?.Tags.FirstOrDefault(p => 
-				    p.Name == "Iso")?.Description;
-			    if ( isoSpeedString == "Auto" )
-			    {
-				    // src: https://github.com/exiftool/exiftool/blob/
-				    // 6b994069d52302062b9d7a462dc27082c4196d95/lib/Image/ExifTool/Canon.pm#L8882
-				    var autoIso = canonMakerNoteDirectory!.Tags.FirstOrDefault(p => 
-					    p.Name == "Auto ISO")?.Description;
-				    var baseIso = canonMakerNoteDirectory.Tags.FirstOrDefault(p => 
-					    p.Name == "Base ISO")?.Description;
-				    if ( !string.IsNullOrEmpty(autoIso) && !string.IsNullOrEmpty(baseIso) )
-				    {
-					    int.TryParse(autoIso, NumberStyles.Number, 
-						    CultureInfo.InvariantCulture, out var autoIsoSpeed);
-					    int.TryParse(baseIso, NumberStyles.Number, 
-						    CultureInfo.InvariantCulture, out var baseIsoSpeed);
-					    return baseIsoSpeed * autoIsoSpeed / 100;
-				    }
-			    }
-		    }
-		    
-		    // XMP,http://ns.adobe.com/exif/1.0/,exif:ISOSpeedRatings,
-		    // XMP,,exif:ISOSpeedRatings[1],101
-		    // XMP,,exif:ISOSpeedRatings[2],101
-		    var xmpDirectory = allExifItems.OfType().FirstOrDefault();
-		    var isoSpeedXmp = GetXmpData(xmpDirectory, "exif:ISOSpeedRatings[1]");
-		    if (string.IsNullOrEmpty(isoSpeedString) && !string.IsNullOrEmpty(isoSpeedXmp))
-		    {
-			    isoSpeedString = isoSpeedXmp;
-		    }
-		    
-		    int.TryParse(isoSpeedString, NumberStyles.Number, CultureInfo.InvariantCulture, out var isoSpeed);
-		    return isoSpeed;
-	    }
+			var pattern = "yyyy:MM:dd HH:mm:ss";
+
+			// Raw files have multiple ExifSubIfdDirectories
+			var exifSubIfdList = allExifItems.OfType().ToList();
+			foreach ( var exifSubIfd in exifSubIfdList )
+			{
+				// https://odedcoster.com/blog/2011/12/13/date-and-time-format-strings-in-net-understanding-format-strings/
+				// 2018:01:01 11:29:36
+				var tagDateTimeDigitized = exifSubIfd.GetDescription(ExifDirectoryBase.TagDateTimeDigitized);
+				DateTime.TryParseExact(tagDateTimeDigitized,
+					pattern, provider, DateTimeStyles.AdjustToUniversal, out var itemDateTimeDigitized);
+				if ( itemDateTimeDigitized.Year >= 2 )
+				{
+					return itemDateTimeDigitized;
+				}
+
+				var tagDateTimeOriginal = exifSubIfd.GetDescription(ExifDirectoryBase.TagDateTimeOriginal);
+				DateTime.TryParseExact(tagDateTimeOriginal,
+					pattern, provider, DateTimeStyles.AdjustToUniversal, out var itemDateTimeOriginal);
+				if ( itemDateTimeOriginal.Year >= 2 )
+				{
+					return itemDateTimeOriginal;
+				}
+			}
+
+			return new DateTime(0, DateTimeKind.Utc);
+		}
+
+		internal DateTime ParseQuickTimeDateTime(CameraMakeModel? cameraMakeModel,
+			IEnumerable allExifItems)
+		{
+			if ( _appSettings == null ) Console.WriteLine("[ParseQuickTimeDateTime] app settings is null");
+			cameraMakeModel ??= new CameraMakeModel();
+
+			if ( _appSettings is { VideoUseLocalTime: null } )
+			{
+				_appSettings.VideoUseLocalTime = new List();
+			}
+
+			var useUseLocalTime = _appSettings?.VideoUseLocalTime.Exists(p =>
+				string.Equals(p.Make, cameraMakeModel.Make, StringComparison.InvariantCultureIgnoreCase) && (
+					string.Equals(p.Model, cameraMakeModel.Model, StringComparison.InvariantCultureIgnoreCase) ||
+					string.IsNullOrEmpty(p.Model) ));
+
+
+			var quickTimeDirectory = allExifItems.OfType().FirstOrDefault();
+
+			var quickTimeCreated = quickTimeDirectory?.GetDescription(QuickTimeMovieHeaderDirectory.TagCreated);
+
+			var dateTimeStyle = useUseLocalTime == true
+				? DateTimeStyles.AdjustToUniversal
+				: DateTimeStyles.AssumeLocal;
+
+			// [QuickTime Movie Header] Created = Tue Oct 11 09:40:04 2011 or Sat Mar 20 21:29:11 2010 // time is in UTC
+			// Or Dutch (NL-nl) "zo mrt. 29 13:10:07 2020"
+			DateTime.TryParseExact(quickTimeCreated, "ddd MMM dd HH:mm:ss yyyy", CultureInfo.CurrentCulture,
+				dateTimeStyle, out var itemDateTimeQuickTime);
+
+			// ReSharper disable once InvertIf
+			if ( useUseLocalTime != true && _appSettings?.CameraTimeZoneInfo != null )
+			{
+				itemDateTimeQuickTime = DateTime.SpecifyKind(itemDateTimeQuickTime, DateTimeKind.Utc);
+				itemDateTimeQuickTime = TimeZoneInfo.ConvertTime(itemDateTimeQuickTime,
+					TimeZoneInfo.Utc, _appSettings.CameraTimeZoneInfo);
+			}
+
+			return itemDateTimeQuickTime;
+		}
+
+		/// 
+		/// 51° 57' 2.31"
+		/// 
+		/// 
+		/// 
+		private static double GetGeoLocationLatitude(List allExifItems)
+		{
+			var latitudeString = string.Empty;
+			var latitudeRef = string.Empty;
+
+			foreach ( var exifItemTag in allExifItems.Select(p => p.Tags) )
+			{
+				var latitudeRefLocal = exifItemTag.FirstOrDefault(
+					p => p.DirectoryName == "GPS"
+					&& p.Name == "GPS Latitude Ref")?.Description;
+
+				if ( latitudeRefLocal != null )
+				{
+					latitudeRef = latitudeRefLocal;
+				}
+
+				var latitudeLocal = exifItemTag.FirstOrDefault(
+					p => p.DirectoryName == "GPS"
+						 && p.Name == "GPS Latitude")?.Description;
+
+				if ( latitudeLocal != null )
+				{
+					latitudeString = latitudeLocal;
+					continue;
+				}
+
+				var locationQuickTime = exifItemTag.FirstOrDefault(
+					p => p.DirectoryName == "QuickTime Metadata Header"
+						 && p.Name == "GPS Location")?.Description;
+				if ( locationQuickTime != null )
+				{
+					return GeoParser.ParseIsoString(locationQuickTime).Latitude;
+				}
+			}
+
+			if ( string.IsNullOrWhiteSpace(latitudeString) )
+				return GetXmpGeoData(allExifItems, "exif:GPSLatitude");
+
+			var latitude = GeoParser.ConvertDegreeMinutesSecondsToDouble(latitudeString, latitudeRef);
+			return Math.Floor(latitude * 10000000000) / 10000000000;
+
+		}
+
+		internal static double GetXmpGeoData(List allExifItems, string propertyPath)
+		{
+			var latitudeString = string.Empty;
+			var latitudeRef = string.Empty;
+
+			foreach ( var exifItem in allExifItems )
+			{
+				// exif:GPSLatitude,45,33.615N
+				var latitudeLocal = GetXmpData(exifItem, propertyPath);
+				if ( string.IsNullOrEmpty(latitudeLocal) ) continue;
+				var split = Regex.Split(latitudeLocal, "[NSWE]",
+					RegexOptions.None, TimeSpan.FromMilliseconds(100));
+				if ( split.Length != 2 ) continue;
+				latitudeString = split[0];
+				latitudeRef = latitudeLocal[^1].ToString();
+			}
+
+			if ( string.IsNullOrWhiteSpace(latitudeString) ) return 0;
+
+			var latitudeDegreeMinutes = GeoParser.ConvertDegreeMinutesToDouble(latitudeString, latitudeRef);
+			return Math.Floor(latitudeDegreeMinutes * 10000000000) / 10000000000;
+		}
+
+		private static double GetGeoLocationAltitude(List allExifItems)
+		{
+			//    [GPS] GPS Altitude Ref = Below sea level
+			//    [GPS] GPS Altitude = 2 metres
+
+			var altitudeString = string.Empty;
+			var altitudeRef = string.Empty;
+
+			foreach ( var exifItemTags in allExifItems.Select(p => p.Tags) )
+			{
+				var longitudeRefLocal = exifItemTags.FirstOrDefault(
+					p => p.DirectoryName == "GPS"
+						 && p.Name == "GPS Altitude Ref")?.Description;
+
+				if ( longitudeRefLocal != null )
+				{
+					altitudeRef = longitudeRefLocal;
+				}
+
+				var altitudeLocal = exifItemTags.FirstOrDefault(
+					p => p.DirectoryName == "GPS"
+						 && p.Name == "GPS Altitude")?.Description;
+
+				if ( altitudeLocal != null )
+				{
+					// space metres
+					altitudeString = altitudeLocal.Replace(" metres", string.Empty);
+				}
+			}
+
+			// this value is always an int but current culture formatted
+			var parsedAltitudeString = double.TryParse(altitudeString,
+				NumberStyles.Number, CultureInfo.CurrentCulture, out var altitude);
+
+			if ( altitudeRef == "Below sea level" ) altitude *= -1;
+
+			// Read xmp if altitudeString is string.Empty
+			if ( !parsedAltitudeString )
+			{
+				return GetXmpGeoAlt(allExifItems);
+			}
+
+			return ( int )altitude;
+		}
+
+		internal static double GetXmpGeoAlt(List allExifItems)
+		{
+			var altitudeRef = true;
+			var altitude = 0d;
+
+			// example:
+			// +1
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitude,1/1
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitudeRef,0
+
+			// -10
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitude,10/1
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:GPSAltitudeRef,1
+
+			foreach ( var exifItem in allExifItems )
+			{
+				if ( !( exifItem is XmpDirectory xmpDirectory ) || xmpDirectory.XmpMeta == null )
+					continue;
+
+				foreach ( var property in xmpDirectory.XmpMeta.Properties.Where(p =>
+					!string.IsNullOrEmpty(p.Value)) )
+				{
+					switch ( property.Path )
+					{
+						case "exif:GPSAltitude":
+							altitude = MathFraction.Fraction(property.Value);
+							break;
+						case "exif:GPSAltitudeRef":
+							altitudeRef = property.Value == "1";
+							break;
+					}
+				}
+			}
+
+			// no -0 as result
+			if ( Math.Abs(altitude) < 0.001 ) return 0;
+
+			if ( altitudeRef ) altitude *= -1;
+			return altitude;
+		}
+
+
+		private static double GetGeoLocationLongitude(List allExifItems)
+		{
+			var longitudeString = string.Empty;
+			var longitudeRef = string.Empty;
+
+			foreach ( var exifItemTags in allExifItems.Select(p => p.Tags) )
+			{
+				var longitudeRefLocal = exifItemTags.FirstOrDefault(
+					p => p.DirectoryName == "GPS"
+						 && p.Name == "GPS Longitude Ref")?.Description;
+
+				if ( longitudeRefLocal != null )
+				{
+					longitudeRef = longitudeRefLocal;
+				}
+
+				var longitudeLocal = exifItemTags.FirstOrDefault(
+					p => p.DirectoryName == "GPS"
+						 && p.Name == "GPS Longitude")?.Description;
+
+				if ( longitudeLocal != null )
+				{
+					longitudeString = longitudeLocal;
+					continue;
+				}
+
+				var locationQuickTime = exifItemTags.FirstOrDefault(
+					p => p.DirectoryName == "QuickTime Metadata Header"
+						 && p.Name == "GPS Location")?.Description;
+				if ( locationQuickTime != null )
+				{
+					return GeoParser.ParseIsoString(locationQuickTime).Longitude;
+				}
+			}
+
+			if ( !string.IsNullOrWhiteSpace(longitudeString) )
+			{
+				var longitude = GeoParser.
+					ConvertDegreeMinutesSecondsToDouble(longitudeString, longitudeRef);
+				longitude = Math.Floor(longitude * 10000000000) / 10000000000;
+				return longitude;
+			}
+
+			return GetXmpGeoData(allExifItems, "exif:GPSLongitude");
+		}
+
+		private static int GetImageWidthHeightMaxCount(string dirName, ICollection allExifItems)
+		{
+			var maxCount = 6;
+			if ( dirName == "Exif SubIFD" ) maxCount = 30; // on header place 17&18
+			if ( allExifItems.Count <= 5 ) maxCount = allExifItems.Count;
+			return maxCount;
+		}
+
+		private static string GetImageWidthTypeName(string dirName,
+			bool isWidth)
+		{
+			var typeName = "Image Height";
+			if ( dirName == "QuickTime Track Header" ) typeName = "Height";
+
+			if ( isWidth ) typeName = "Image Width";
+			if ( isWidth && dirName == "QuickTime Track Header" ) typeName = "Width";
+			return typeName;
+		}
+
+		public static int GetImageWidthHeight(List allExifItems, bool isWidth)
+		{
+			// The size lives normally in the first 5 headers
+			// > "Exif IFD0" .dng
+			// [Exif SubIFD] > arw; on header place 17&18
+			var directoryNames = new[] {"JPEG", "PNG-IHDR", "BMP Header", "GIF Header",
+				"QuickTime Track Header", "Exif IFD0", "Exif SubIFD"};
+			foreach ( var dirName in directoryNames )
+			{
+				var typeName = GetImageWidthTypeName(dirName, isWidth);
+				var maxCount = GetImageWidthHeightMaxCount(dirName, allExifItems);
+
+				for ( var i = 0; i < maxCount; i++ )
+				{
+					if ( i >= allExifItems.Count )
+					{
+						continue;
+					}
+					var exifItem = allExifItems[i];
+					var value = GetImageSizeInsideLoop(exifItem, dirName, typeName);
+					if ( value != 0 ) return value;
+				}
+			}
+			return 0;
+		}
+
+		private static int GetImageSizeInsideLoop(Directory exifItem, string dirName, string typeName)
+		{
+			var ratingCountsJpeg =
+				exifItem.Tags.Count(p => p.DirectoryName == dirName
+										 && p.Name.Contains(typeName) && p.Description != "0");
+			if ( ratingCountsJpeg >= 1 )
+			{
+				var widthTag = exifItem.Tags
+					.FirstOrDefault(p => p.DirectoryName == dirName
+										 && p.Name.Contains(typeName) && p.Description != "0")
+					?.Description;
+				widthTag = widthTag?.Replace(" pixels", string.Empty);
+				if ( int.TryParse(widthTag, out var widthInt) )
+				{
+					return widthInt >= 1 ? widthInt : 0; // (widthInt >= 1) return widthInt)
+				}
+			}
+			return 0;
+		}
+
+
+		/// 
+		///     For the location element
+		///    [IPTC] City = Diepenveen
+		///    [IPTC] Province/State = Overijssel
+		///    [IPTC] Country/Primary Location Name = Nederland
+		/// 
+		/// 
+		/// City, State or Country
+		/// photoshop:State
+		/// 
+		private static string GetLocationPlaces(List allExifItems, string iptcName, string xmpPropertyPath)
+		{
+			var iptcDirectoryDirectory = allExifItems.OfType().FirstOrDefault();
+
+			var tCounts = iptcDirectoryDirectory?.Tags.Count(p => p.DirectoryName == "IPTC" && p.Name == iptcName);
+			if ( tCounts < 1 )
+			{
+				var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+				return GetXmpData(xmpDirectory, xmpPropertyPath);
+			}
+
+			var locationCity = iptcDirectoryDirectory?.Tags
+				.FirstOrDefault(p => p.Name == iptcName)?.Description;
+			locationCity ??= string.Empty;
+			return locationCity;
+		}
+
+		/// 
+		/// [Exif SubIFD] Focal Length
+		/// 
+		/// 
+		private static double GetFocalLength(List allExifItems)
+		{
+			var exifSubIfdDirectory = allExifItems.OfType().FirstOrDefault();
+			var focalLengthString = exifSubIfdDirectory?.GetDescription(ExifDirectoryBase.TagFocalLength);
+
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:FocalLength,11/1
+			var focalLengthXmp = GetXmpData(xmpDirectory, "exif:FocalLength");
+			if ( string.IsNullOrEmpty(focalLengthString) && !string.IsNullOrEmpty(focalLengthXmp) )
+			{
+				return Math.Round(MathFraction.Fraction(focalLengthXmp), 5);
+			}
+
+			if ( string.IsNullOrWhiteSpace(focalLengthString) ) return 0d;
+
+			focalLengthString = focalLengthString.Replace(" mm", string.Empty);
+
+			// Note: focalLengthString: (Dutch) 2,2 or (English) 2.2 based CultureInfo.CurrentCulture
+			float.TryParse(focalLengthString, NumberStyles.Number,
+				CultureInfo.CurrentCulture, out var focalLength);
+
+			return Math.Round(focalLength, 5);
+		}
+
+
+		private static double GetAperture(List allExifItems)
+		{
+			var exifItem = allExifItems.OfType().FirstOrDefault();
+
+			// "Exif SubIFD"
+			var apertureString = exifItem?.Tags.FirstOrDefault(p =>
+				p.Name == "Aperture Value")?.Description;
+
+			if ( string.IsNullOrEmpty(apertureString) )
+			{
+				apertureString = exifItem?.Tags.FirstOrDefault(p =>
+					p.Name == "F-Number")?.Description;
+			}
+
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:FNumber,9/1
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+			var fNumberXmp = GetXmpData(xmpDirectory, "exif:FNumber");
+
+			if ( string.IsNullOrEmpty(apertureString) && !string.IsNullOrEmpty(fNumberXmp) )
+			{
+				return MathFraction.Fraction(fNumberXmp);
+			}
+
+			if ( apertureString == null ) return 0d;
+
+			apertureString = apertureString.Replace("f/", string.Empty);
+			// Note: apertureString: (Dutch) 2,2 or (English) 2.2 based CultureInfo.CurrentCulture
+			float.TryParse(apertureString, NumberStyles.Number, CultureInfo.CurrentCulture, out var aperture);
+
+			return aperture;
+		}
+
+		/// 
+		/// [Exif SubIFD] Shutter Speed Value = 1/2403 sec
+		/// 
+		/// item to look in
+		/// value
+		private static string GetShutterSpeedValue(List allExifItems)
+		{
+			var exifItem = allExifItems.OfType().FirstOrDefault();
+
+			// Exif SubIFD
+			var shutterSpeedString = exifItem?.Tags.FirstOrDefault(p =>
+				p.Name == "Shutter Speed Value")?.Description;
+
+			if ( string.IsNullOrEmpty(shutterSpeedString) )
+			{
+				// Exif SubIFD
+				shutterSpeedString = exifItem?.Tags.FirstOrDefault(p =>
+					p.Name == "Exposure Time")?.Description;
+			}
+
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:ExposureTime,1/20
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+
+			var exposureTimeXmp = GetXmpData(xmpDirectory, "exif:ExposureTime");
+			if ( string.IsNullOrEmpty(shutterSpeedString) &&
+				!string.IsNullOrEmpty(exposureTimeXmp) && exposureTimeXmp.Length <= 20 )
+			{
+				return exposureTimeXmp;
+			}
+
+			if ( shutterSpeedString == null ) return string.Empty;
+			// the database has a 20 char limit
+			if ( shutterSpeedString.Length >= 20 ) return string.Empty;
+
+			// in xmp there is only a field with for example: 1/33
+			shutterSpeedString = shutterSpeedString.Replace(" sec", string.Empty);
+			return shutterSpeedString;
+		}
+
+		internal static int GetIsoSpeedValue(List allExifItems)
+		{
+			var subIfdItem = allExifItems.OfType().FirstOrDefault();
+
+			// Exif SubIFD
+			var isoSpeedString = subIfdItem?.Tags.FirstOrDefault(p =>
+				p.Name == "ISO Speed Ratings")?.Description;
+
+			if ( string.IsNullOrEmpty(isoSpeedString) )
+			{
+				var canonMakerNoteDirectory = allExifItems.OfType().FirstOrDefault();
+
+				// Canon Makernote
+				isoSpeedString = canonMakerNoteDirectory?.Tags.FirstOrDefault(p =>
+					p.Name == "Iso")?.Description;
+				if ( isoSpeedString == "Auto" )
+				{
+					// src: https://github.com/exiftool/exiftool/blob/
+					// 6b994069d52302062b9d7a462dc27082c4196d95/lib/Image/ExifTool/Canon.pm#L8882
+					var autoIso = canonMakerNoteDirectory!.Tags.FirstOrDefault(p =>
+						p.Name == "Auto ISO")?.Description;
+					var baseIso = canonMakerNoteDirectory.Tags.FirstOrDefault(p =>
+						p.Name == "Base ISO")?.Description;
+					if ( !string.IsNullOrEmpty(autoIso) && !string.IsNullOrEmpty(baseIso) )
+					{
+						int.TryParse(autoIso, NumberStyles.Number,
+							CultureInfo.InvariantCulture, out var autoIsoSpeed);
+						int.TryParse(baseIso, NumberStyles.Number,
+							CultureInfo.InvariantCulture, out var baseIsoSpeed);
+						return baseIsoSpeed * autoIsoSpeed / 100;
+					}
+				}
+			}
+
+			// XMP,http://ns.adobe.com/exif/1.0/,exif:ISOSpeedRatings,
+			// XMP,,exif:ISOSpeedRatings[1],101
+			// XMP,,exif:ISOSpeedRatings[2],101
+			var xmpDirectory = allExifItems.OfType().FirstOrDefault();
+			var isoSpeedXmp = GetXmpData(xmpDirectory, "exif:ISOSpeedRatings[1]");
+			if ( string.IsNullOrEmpty(isoSpeedString) && !string.IsNullOrEmpty(isoSpeedXmp) )
+			{
+				isoSpeedString = isoSpeedXmp;
+			}
+
+			int.TryParse(isoSpeedString, NumberStyles.Number, CultureInfo.InvariantCulture, out var isoSpeed);
+			return isoSpeed;
+		}
 	}
 }
diff --git a/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaGpx.cs b/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaGpx.cs
index 86c0e23430..8672ad8f60 100644
--- a/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaGpx.cs
+++ b/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaGpx.cs
@@ -1,4 +1,3 @@
-#nullable enable
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -16,197 +15,197 @@
 
 namespace starsky.foundation.readmeta.ReadMetaHelpers
 {
-    public sealed class ReadMetaGpx
-    {
-	    private readonly IWebLogger _logger;
-
-	    public ReadMetaGpx(IWebLogger logger)
-	    {
-		    _logger = logger;
-	    }
-	    
-	    private const string GpxXmlNameSpaceName = "http://www.topografix.com/GPX/1/1"; 
-	    
-        public async Task ReadGpxFromFileReturnAfterFirstFieldAsync(Stream? stream, 
-	        string subPath, bool useLocal = true)
-        {
-	        if ( stream == null )
-	        {
-		        var returnItem = new FileIndexItem(subPath)
-		        {
-			        Status = FileIndexItem.ExifStatus.OperationNotSupported
-		        };
-		        return returnItem;
-	        }
-
-	        var readGpxFile = await ReadGpxFileAsync(stream, null, 1);
-
-	        if ( readGpxFile.Count == 0 )
-	        {
-		        _logger.LogInformation($"[ReadMetaGpx] SystemXmlXmlException for {subPath}");
-		        return new FileIndexItem(subPath)
-		        {
-			        Tags = "SystemXmlXmlException",
-			        ColorClass = ColorClassParser.Color.None
-		        };
-	        }
-
-	        var title = readGpxFile.FirstOrDefault()?.Title ?? string.Empty;
-	        var dateTime = readGpxFile.FirstOrDefault()?.DateTime ?? new DateTime(0, DateTimeKind.Utc);
-	        var latitude = readGpxFile.FirstOrDefault()?.Latitude ?? 0d;
-	        var longitude = readGpxFile.FirstOrDefault()?.Longitude ?? 0d;
-	        var altitude = readGpxFile.FirstOrDefault()?.Altitude ?? 0d;
-
-	        return new FileIndexItem(subPath)
-	        {
-		        Title = title,
-		        DateTime = ConvertDateTime(dateTime, useLocal, latitude, longitude),
-		        Latitude = latitude,
-		        Longitude = longitude,
-		        LocationAltitude = altitude,
-		        ColorClass = ColorClassParser.Color.None,
-		        ImageFormat = ExtensionRolesHelper.ImageFormat.gpx
-	        };
-        }
-
-        internal static DateTime ConvertDateTime(DateTime dateTime, bool useLocal, 
-	        double latitude, double longitude)
-        {
-	        if ( !useLocal ) return dateTime;
-	        var localTimeZoneNameResult = TimeZoneLookup
-		        .GetTimeZone(latitude, longitude).Result;
-	        var localTimeZone =
-		        TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneNameResult);
-	        return TimeZoneInfo.ConvertTimeFromUtc(dateTime, localTimeZone);
-        }
-
-        private static string GetTrkName(XmlNode? gpxDoc, XmlNamespaceManager namespaceManager)
-        {
-	        var trkNodeList = gpxDoc?.SelectNodes("//x:trk",  namespaceManager);
-            if ( trkNodeList == null ) return string.Empty;
-            var trkName = new StringBuilder();
-            foreach (XmlElement node in trkNodeList)
-            {
-                foreach (XmlElement childNode in node.ChildNodes)
-                {
-	                if ( childNode.Name != "name" ) continue;
-	                trkName.Append(childNode.InnerText);
-	                return trkName.ToString();
-                }
-            }
-            return string.Empty;
-        }
-
-	    /// 
-	    /// Read full gpx file, or return after trackPoint
-	    /// 
-	    /// 
-	    /// 
-	    /// default complete file, but can be used to read only the first point
-	    /// 
-	    public async Task> ReadGpxFileAsync(Stream stream, 
-		    List? geoList = null, int returnAfter = int.MaxValue)
-        {
-            geoList ??= new List();
-
-	        // Some files are having problems with gpxDoc.Load()
-	        var fileString = await StreamToStringHelper.StreamToStringAsync(stream);
-	        
-	        try
-	        {
-		        return ParseGpxString(fileString, geoList, returnAfter);
-	        }
-	        catch ( XmlException e )
-	        {
-		        _logger.LogInformation($"XmlException for {e}");
-		        return geoList;
-	        }
-
-        }
-
-	    /// 
-	    /// Parse XML as XmlDocument
-	    /// 
-	    /// input as string
-	    /// parsed xml document
-	    internal static XmlDocument ParseXml(string fileString)
-	    {
-		    XmlDocument gpxDoc = new XmlDocument();
-		    gpxDoc.LoadXml(fileString);
-		    return gpxDoc;
-	    }
-
-	    /// 
-	    /// Parse the gpx string
-	    /// 
-	    /// string with xml
-	    /// object to add
-	    /// return after number of values; default return all
-	    /// 
-	    private static List ParseGpxString(string fileString, List? geoList = null, 
-		    int returnAfter = int.MaxValue)
-	    {
-		    var gpxDoc = ParseXml(fileString);
-            
-            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(gpxDoc.NameTable);
+	public sealed class ReadMetaGpx
+	{
+		private readonly IWebLogger _logger;
+
+		public ReadMetaGpx(IWebLogger logger)
+		{
+			_logger = logger;
+		}
+
+		private const string GpxXmlNameSpaceName = "http://www.topografix.com/GPX/1/1";
+
+		public async Task ReadGpxFromFileReturnAfterFirstFieldAsync(Stream? stream,
+			string subPath, bool useLocal = true)
+		{
+			if ( stream == null )
+			{
+				var returnItem = new FileIndexItem(subPath)
+				{
+					Status = FileIndexItem.ExifStatus.OperationNotSupported
+				};
+				return returnItem;
+			}
+
+			var readGpxFile = await ReadGpxFileAsync(stream, null, 1);
+
+			if ( readGpxFile.Count == 0 )
+			{
+				_logger.LogInformation($"[ReadMetaGpx] SystemXmlXmlException for {subPath}");
+				return new FileIndexItem(subPath)
+				{
+					Tags = "SystemXmlXmlException",
+					ColorClass = ColorClassParser.Color.None
+				};
+			}
+
+			var title = readGpxFile.FirstOrDefault()?.Title ?? string.Empty;
+			var dateTime = readGpxFile.FirstOrDefault()?.DateTime ?? new DateTime(0, DateTimeKind.Utc);
+			var latitude = readGpxFile.FirstOrDefault()?.Latitude ?? 0d;
+			var longitude = readGpxFile.FirstOrDefault()?.Longitude ?? 0d;
+			var altitude = readGpxFile.FirstOrDefault()?.Altitude ?? 0d;
+
+			return new FileIndexItem(subPath)
+			{
+				Title = title,
+				DateTime = ConvertDateTime(dateTime, useLocal, latitude, longitude),
+				Latitude = latitude,
+				Longitude = longitude,
+				LocationAltitude = altitude,
+				ColorClass = ColorClassParser.Color.None,
+				ImageFormat = ExtensionRolesHelper.ImageFormat.gpx
+			};
+		}
+
+		internal static DateTime ConvertDateTime(DateTime dateTime, bool useLocal,
+			double latitude, double longitude)
+		{
+			if ( !useLocal ) return dateTime;
+			var localTimeZoneNameResult = TimeZoneLookup
+				.GetTimeZone(latitude, longitude).Result;
+			var localTimeZone =
+				TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneNameResult);
+			return TimeZoneInfo.ConvertTimeFromUtc(dateTime, localTimeZone);
+		}
+
+		private static string GetTrkName(XmlNode? gpxDoc, XmlNamespaceManager namespaceManager)
+		{
+			var trkNodeList = gpxDoc?.SelectNodes("//x:trk", namespaceManager);
+			if ( trkNodeList == null ) return string.Empty;
+			var trkName = new StringBuilder();
+			foreach ( XmlElement node in trkNodeList )
+			{
+				foreach ( XmlElement childNode in node.ChildNodes )
+				{
+					if ( childNode.Name != "name" ) continue;
+					trkName.Append(childNode.InnerText);
+					return trkName.ToString();
+				}
+			}
+			return string.Empty;
+		}
+
+		/// 
+		/// Read full gpx file, or return after trackPoint
+		/// 
+		/// 
+		/// 
+		/// default complete file, but can be used to read only the first point
+		/// 
+		public async Task> ReadGpxFileAsync(Stream stream,
+			List? geoList = null, int returnAfter = int.MaxValue)
+		{
+			geoList ??= new List();
+
+			// Some files are having problems with gpxDoc.Load()
+			var fileString = await StreamToStringHelper.StreamToStringAsync(stream);
+
+			try
+			{
+				return ParseGpxString(fileString, geoList, returnAfter);
+			}
+			catch ( XmlException e )
+			{
+				_logger.LogInformation($"XmlException for {e}");
+				return geoList;
+			}
+
+		}
+
+		/// 
+		/// Parse XML as XmlDocument
+		/// 
+		/// input as string
+		/// parsed xml document
+		internal static XmlDocument ParseXml(string fileString)
+		{
+			XmlDocument gpxDoc = new XmlDocument();
+			gpxDoc.LoadXml(fileString);
+			return gpxDoc;
+		}
+
+		/// 
+		/// Parse the gpx string
+		/// 
+		/// string with xml
+		/// object to add
+		/// return after number of values; default return all
+		/// 
+		private static List ParseGpxString(string fileString, List? geoList = null,
+			int returnAfter = int.MaxValue)
+		{
+			var gpxDoc = ParseXml(fileString);
+
+			XmlNamespaceManager namespaceManager = new XmlNamespaceManager(gpxDoc.NameTable);
 			namespaceManager.AddNamespace("x", GpxXmlNameSpaceName);
-            
-            XmlNodeList? nodeList = gpxDoc.SelectNodes("//x:trkpt", namespaceManager);
-            if ( nodeList == null ) return new List();
-            geoList ??= new List();
-
-            var title = GetTrkName(gpxDoc, namespaceManager);
-
-            var count = 0;
-            foreach (XmlElement node in nodeList)
-            {
-                var longitudeString = node.GetAttribute("lon");
-                var latitudeString = node.GetAttribute("lat");
-
-                var longitude = double.Parse(longitudeString, 
-                    NumberStyles.Currency, CultureInfo.InvariantCulture);
-                var latitude = double.Parse(latitudeString, 
-                    NumberStyles.Currency, CultureInfo.InvariantCulture);
-
-                DateTime dateTime = DateTime.MinValue;
-
-                var elevation = 0d;
-
-                foreach (XmlElement childNode in node.ChildNodes)
-                {
-                    if (childNode.Name == "ele")
-                    {
-                        elevation = double.Parse(childNode.InnerText, CultureInfo.InvariantCulture);
-                    }
-                    
-                    if (childNode.Name != "time") continue;
-                    var datetimeString = childNode.InnerText;
-                    
-                    // 2018-08-21T19:15:41Z
-                    DateTime.TryParseExact(datetimeString, 
-                        "yyyy-MM-ddTHH:mm:ssZ", 
-                        CultureInfo.InvariantCulture, 
-                        DateTimeStyles.AdjustToUniversal, 
-                        out dateTime);
-
-                    dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
-                }
-                
-
-                geoList.Add(new GeoListItem
-                {
-                    Title = title,
-                    DateTime = dateTime,
-                    Latitude = latitude,
-                    Longitude = longitude,
-                    Altitude = elevation
-                });
-                
-                if(returnAfter == count) return geoList;
-                count++;
-                
-            }
-            return geoList;
-	    }
-    }
+
+			XmlNodeList? nodeList = gpxDoc.SelectNodes("//x:trkpt", namespaceManager);
+			if ( nodeList == null ) return new List();
+			geoList ??= new List();
+
+			var title = GetTrkName(gpxDoc, namespaceManager);
+
+			var count = 0;
+			foreach ( XmlElement node in nodeList )
+			{
+				var longitudeString = node.GetAttribute("lon");
+				var latitudeString = node.GetAttribute("lat");
+
+				var longitude = double.Parse(longitudeString,
+					NumberStyles.Currency, CultureInfo.InvariantCulture);
+				var latitude = double.Parse(latitudeString,
+					NumberStyles.Currency, CultureInfo.InvariantCulture);
+
+				DateTime dateTime = DateTime.MinValue;
+
+				var elevation = 0d;
+
+				foreach ( XmlElement childNode in node.ChildNodes )
+				{
+					if ( childNode.Name == "ele" )
+					{
+						elevation = double.Parse(childNode.InnerText, CultureInfo.InvariantCulture);
+					}
+
+					if ( childNode.Name != "time" ) continue;
+					var datetimeString = childNode.InnerText;
+
+					// 2018-08-21T19:15:41Z
+					DateTime.TryParseExact(datetimeString,
+						"yyyy-MM-ddTHH:mm:ssZ",
+						CultureInfo.InvariantCulture,
+						DateTimeStyles.AdjustToUniversal,
+						out dateTime);
+
+					dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
+				}
+
+
+				geoList.Add(new GeoListItem
+				{
+					Title = title,
+					DateTime = dateTime,
+					Latitude = latitude,
+					Longitude = longitude,
+					Altitude = elevation
+				});
+
+				if ( returnAfter == count ) return geoList;
+				count++;
+
+			}
+			return geoList;
+		}
+	}
 }
diff --git a/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaXmp.cs b/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaXmp.cs
index e5af882938..6156a3e815 100644
--- a/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaXmp.cs
+++ b/starsky/starsky.foundation.readmeta/ReadMetaHelpers/ReadMetaXmp.cs
@@ -23,352 +23,352 @@ public ReadMetaXmp(IStorage iStorage, IWebLogger logger)
 			_iStorage = iStorage;
 			_logger = logger;
 		}
-		
-        public async Task XmpGetSidecarFileAsync(FileIndexItem? databaseItem)
-        {
-	        databaseItem ??= new FileIndexItem();
-
-	        // Parse an xmp file for this location
-	        var xmpSubPath =
-		        ExtensionRolesHelper.ReplaceExtensionWithXmp(databaseItem.FilePath);
-	        // also add when the file is a jpeg, we are not writing to it then
-	        if ( _iStorage.ExistFile(xmpSubPath) ) databaseItem.AddSidecarExtension("xmp");
-
-	        // Read content from sidecar xmp file
-	        if ( !ExtensionRolesHelper.IsExtensionForceXmp(databaseItem.FilePath) ||
-	             !_iStorage.ExistFile(xmpSubPath) ) return databaseItem;
-	        
-	        // Read the text-content of the xmp file.
-            var xmp = await StreamToStringHelper.StreamToStringAsync(_iStorage.ReadStream(xmpSubPath));
-            // Get the data from the xmp
-            databaseItem = GetDataFromString(xmp,databaseItem);
-            return databaseItem;
-        }
-        
-        public FileIndexItem GetDataFromString(string xmpDataAsString, FileIndexItem? databaseItem = null)
-        {
-            // Does not require appSettings
-            
-            databaseItem ??= new FileIndexItem();
-		        
-	        try
-	        {
-		        var xmp = XmpMetaFactory.ParseFromString(xmpDataAsString);
-		        // ContentNameSpace is for example : Namespace=http://...
-		        databaseItem = GetDataContentNameSpaceTypes(xmp, databaseItem);
-		        // NullNameSpace is for example : string.Empty
-		        databaseItem = GetDataNullNameSpaceTypes(xmp, databaseItem);
-	        }
-	        catch ( XmpException e )
-	        {
-		        _logger.LogInformation($"XmpException {databaseItem.FilePath} >>\n{e}\n <
-        /// Get the value for items with the name without namespace
-        /// 
-        /// IXmpPropertyInfo read from string
-        /// xmpName, for example dc:subject[1]
-        /// value or null
-        private static string? GetNullNameSpace(IXmpPropertyInfo property, string xmpName)
-        {
-            if (property.Path == xmpName && !string.IsNullOrEmpty(property.Value) 
-                                                     && string.IsNullOrEmpty(property.Namespace) )
-            {
-               return property.Value;
-            }
-            return null;
-        }
-
-        /// 
-        /// Get the value for items with the name with a namespace
-        /// 
-        /// IXmpPropertyInfo read from string
-        /// xmpName, for example dc:subject[1]
-        /// value or null
-        private static string? GetContentNameSpace(IXmpPropertyInfo property, string xmpName)
-        {
-            if (property.Path == xmpName && !string.IsNullOrEmpty(property.Value) 
-                                         && !string.IsNullOrEmpty(property.Namespace) )
-                // the difference it this ^^!^^
-            {
-                return property.Value;
-            }
-            return null;
-        }
-
-        private static double GpsPreParseAndConvertDegreeAngleToDouble(string gpsLatOrLong)
-        {
-            // get ref North, South, East West
-            var refGps = gpsLatOrLong.Substring(gpsLatOrLong.Length-1, 1);
-            return GeoParser.ConvertDegreeMinutesToDouble(gpsLatOrLong, refGps);
-        }
-        
-        [SuppressMessage("Usage", "S125:Remove this commented out code")]
-        private static FileIndexItem GetDataNullNameSpaceTypes(IXmpMeta xmp, FileIndexItem item)
-        {
-            foreach (var property in xmp.Properties)
-            {
-	            // dc:description[1] and dc:subject 
-	            SetCombinedDescriptionSubject(property, item);
-                
-                // Path=dc:title Namespace=http://purl.org/dc/elements/1.1/ Value=
-                // Path=dc:title[1] Namespace= Value=The object name
-                //    Path=dc:title[1]/xml:lang Namespace=http://www.w3...
-                var title = GetNullNameSpace(property, "dc:title[1]");
-                if (title != null) item.Title = title;
-	            
-	            // Path=exif:ISOSpeedRatings Namespace=http://ns.adobe.com/exif/1.0/ Value=
-	            // Path=exif:ISOSpeedRatings[1] Namespace= Value=25
-	            var isoSpeed = GetNullNameSpace(property, "exif:ISOSpeedRatings[1]");
-	            if ( isoSpeed != null ) item.SetIsoSpeed(isoSpeed);
-	            
-	            var height = GetContentNameSpace(property, "exif:PixelXDimension");
-	            if (height != null) item.SetImageHeight(height);
-
-	            var width = GetContentNameSpace(property, "exif:PixelYDimension");
-	            if (width != null) item.SetImageWidth(width);
-	            
-	            var lensModel = GetContentNameSpace(property, "exifEX:LensModel");
-	            if (lensModel != null) item.SetMakeModel(lensModel,2);
-	            
-	            var countryCode = GetContentNameSpace(property, "Iptc4xmpCore:CountryCode");
-	            if ( countryCode != null ) item.LocationCountryCode = countryCode;
-	            
-	            // ImageStabilisation is not found in XMP
-	            
-	            // dont show in production 
+
+		public async Task XmpGetSidecarFileAsync(FileIndexItem? databaseItem)
+		{
+			databaseItem ??= new FileIndexItem();
+
+			// Parse an xmp file for this location
+			var xmpSubPath =
+				ExtensionRolesHelper.ReplaceExtensionWithXmp(databaseItem.FilePath);
+			// also add when the file is a jpeg, we are not writing to it then
+			if ( _iStorage.ExistFile(xmpSubPath) ) databaseItem.AddSidecarExtension("xmp");
+
+			// Read content from sidecar xmp file
+			if ( !ExtensionRolesHelper.IsExtensionForceXmp(databaseItem.FilePath) ||
+				 !_iStorage.ExistFile(xmpSubPath) ) return databaseItem;
+
+			// Read the text-content of the xmp file.
+			var xmp = await StreamToStringHelper.StreamToStringAsync(_iStorage.ReadStream(xmpSubPath));
+			// Get the data from the xmp
+			databaseItem = GetDataFromString(xmp, databaseItem);
+			return databaseItem;
+		}
+
+		public FileIndexItem GetDataFromString(string xmpDataAsString, FileIndexItem? databaseItem = null)
+		{
+			// Does not require appSettings
+
+			databaseItem ??= new FileIndexItem();
+
+			try
+			{
+				var xmp = XmpMetaFactory.ParseFromString(xmpDataAsString);
+				// ContentNameSpace is for example : Namespace=http://...
+				databaseItem = GetDataContentNameSpaceTypes(xmp, databaseItem);
+				// NullNameSpace is for example : string.Empty
+				databaseItem = GetDataNullNameSpaceTypes(xmp, databaseItem);
+			}
+			catch ( XmpException e )
+			{
+				_logger.LogInformation($"XmpException {databaseItem.FilePath} >>\n{e}\n <
+		/// Get the value for items with the name without namespace
+		/// 
+		/// IXmpPropertyInfo read from string
+		/// xmpName, for example dc:subject[1]
+		/// value or null
+		private static string? GetNullNameSpace(IXmpPropertyInfo property, string xmpName)
+		{
+			if ( property.Path == xmpName && !string.IsNullOrEmpty(property.Value)
+													 && string.IsNullOrEmpty(property.Namespace) )
+			{
+				return property.Value;
+			}
+			return null;
+		}
+
+		/// 
+		/// Get the value for items with the name with a namespace
+		/// 
+		/// IXmpPropertyInfo read from string
+		/// xmpName, for example dc:subject[1]
+		/// value or null
+		private static string? GetContentNameSpace(IXmpPropertyInfo property, string xmpName)
+		{
+			if ( property.Path == xmpName && !string.IsNullOrEmpty(property.Value)
+										 && !string.IsNullOrEmpty(property.Namespace) )
+			// the difference it this ^^!^^
+			{
+				return property.Value;
+			}
+			return null;
+		}
+
+		private static double GpsPreParseAndConvertDegreeAngleToDouble(string gpsLatOrLong)
+		{
+			// get ref North, South, East West
+			var refGps = gpsLatOrLong.Substring(gpsLatOrLong.Length - 1, 1);
+			return GeoParser.ConvertDegreeMinutesToDouble(gpsLatOrLong, refGps);
+		}
+
+		[SuppressMessage("Usage", "S125:Remove this commented out code")]
+		private static FileIndexItem GetDataNullNameSpaceTypes(IXmpMeta xmp, FileIndexItem item)
+		{
+			foreach ( var property in xmp.Properties )
+			{
+				// dc:description[1] and dc:subject 
+				SetCombinedDescriptionSubject(property, item);
+
+				// Path=dc:title Namespace=http://purl.org/dc/elements/1.1/ Value=
+				// Path=dc:title[1] Namespace= Value=The object name
+				//    Path=dc:title[1]/xml:lang Namespace=http://www.w3...
+				var title = GetNullNameSpace(property, "dc:title[1]");
+				if ( title != null ) item.Title = title;
+
+				// Path=exif:ISOSpeedRatings Namespace=http://ns.adobe.com/exif/1.0/ Value=
+				// Path=exif:ISOSpeedRatings[1] Namespace= Value=25
+				var isoSpeed = GetNullNameSpace(property, "exif:ISOSpeedRatings[1]");
+				if ( isoSpeed != null ) item.SetIsoSpeed(isoSpeed);
+
+				var height = GetContentNameSpace(property, "exif:PixelXDimension");
+				if ( height != null ) item.SetImageHeight(height);
+
+				var width = GetContentNameSpace(property, "exif:PixelYDimension");
+				if ( width != null ) item.SetImageWidth(width);
+
+				var lensModel = GetContentNameSpace(property, "exifEX:LensModel");
+				if ( lensModel != null ) item.SetMakeModel(lensModel, 2);
+
+				var countryCode = GetContentNameSpace(property, "Iptc4xmpCore:CountryCode");
+				if ( countryCode != null ) item.LocationCountryCode = countryCode;
+
+				// ImageStabilisation is not found in XMP
+
+				// dont show in production 
 				// Console.WriteLine($"Path={property.Path} Namespace={property.Namespace} Value={property.Value}");
 
-            }
-
-            return item;
-        }
-
-        private static void SetCombinedDescriptionSubject(IXmpPropertyInfo property, FileIndexItem item)
-        {
-	        //   Path=dc:description Namespace=http://purl.org/dc/elements/1.1/ Value=
-	        //   Path=dc:description[1] Namespace= Value=caption
-	        //   Path=dc:description[1]/xml:lang Namespace=http...
-	        var description = GetNullNameSpace(property, "dc:description[1]");
-	        if (description != null) item.Description = description;
-                
-	        // Path=dc:subject Namespace=http://purl.org/dc/elements/1.1/ Value=
-	        // Path=dc:subject[1] Namespace= Value=keyword
-	        var tags = GetNullNameSpace(property, "dc:subject[1]");
-	        if (tags != null) item.Tags = tags;
-                
-	        // Path=dc:subject[2] Namespace= Value=keyword2
-	        if ( !string.IsNullOrEmpty(property.Path) && 
-	             property.Path.Contains("dc:subject[") && 
-	             property.Path != "dc:subject[1]" && 
-	             !string.IsNullOrEmpty(property.Value) && 
-	             string.IsNullOrEmpty(property.Namespace) )
-	        {
-		        var tagsStringBuilder = new StringBuilder();
-		        tagsStringBuilder.Append(item.Tags);
-		        tagsStringBuilder.Append(", ");
-		        tagsStringBuilder.Append(property.Value);
-		        item.Tags = tagsStringBuilder.ToString();
-	        }
-        }
-
-        private static void GpsAltitudeRef(IXmpMeta xmp, FileIndexItem item)
-        {
-            string? gpsAltitude = null;
-            string? gpsAltitudeRef = null;
-            foreach (var property in xmp.Properties)
-            {
-                // Path=exif:GPSAltitude Namespace=http://ns.adobe.com/exif/1.0/ Value=627/10
-                // Path=exif:GPSAltitudeRef Namespace=http://ns.adobe.com/exif/1.0/ Value=0
-                var gpsAltitudeLocal = GetContentNameSpace(property, "exif:GPSAltitude");
-                if (gpsAltitudeLocal != null)
-                {
-                    gpsAltitude = gpsAltitudeLocal;
-                }
-                
-                var gpsAltitudeRefLocal = GetContentNameSpace(property, "exif:GPSAltitudeRef");
-                if (gpsAltitudeRefLocal != null)
-                {
-                    gpsAltitudeRef = gpsAltitudeRefLocal;
-                }
-            }
-            if(gpsAltitude == null || gpsAltitudeRef == null) return;
-            if(!gpsAltitude.Contains('/')) return;
+			}
+
+			return item;
+		}
+
+		private static void SetCombinedDescriptionSubject(IXmpPropertyInfo property, FileIndexItem item)
+		{
+			//   Path=dc:description Namespace=http://purl.org/dc/elements/1.1/ Value=
+			//   Path=dc:description[1] Namespace= Value=caption
+			//   Path=dc:description[1]/xml:lang Namespace=http...
+			var description = GetNullNameSpace(property, "dc:description[1]");
+			if ( description != null ) item.Description = description;
+
+			// Path=dc:subject Namespace=http://purl.org/dc/elements/1.1/ Value=
+			// Path=dc:subject[1] Namespace= Value=keyword
+			var tags = GetNullNameSpace(property, "dc:subject[1]");
+			if ( tags != null ) item.Tags = tags;
+
+			// Path=dc:subject[2] Namespace= Value=keyword2
+			if ( !string.IsNullOrEmpty(property.Path) &&
+				 property.Path.Contains("dc:subject[") &&
+				 property.Path != "dc:subject[1]" &&
+				 !string.IsNullOrEmpty(property.Value) &&
+				 string.IsNullOrEmpty(property.Namespace) )
+			{
+				var tagsStringBuilder = new StringBuilder();
+				tagsStringBuilder.Append(item.Tags);
+				tagsStringBuilder.Append(", ");
+				tagsStringBuilder.Append(property.Value);
+				item.Tags = tagsStringBuilder.ToString();
+			}
+		}
+
+		private static void GpsAltitudeRef(IXmpMeta xmp, FileIndexItem item)
+		{
+			string? gpsAltitude = null;
+			string? gpsAltitudeRef = null;
+			foreach ( var property in xmp.Properties )
+			{
+				// Path=exif:GPSAltitude Namespace=http://ns.adobe.com/exif/1.0/ Value=627/10
+				// Path=exif:GPSAltitudeRef Namespace=http://ns.adobe.com/exif/1.0/ Value=0
+				var gpsAltitudeLocal = GetContentNameSpace(property, "exif:GPSAltitude");
+				if ( gpsAltitudeLocal != null )
+				{
+					gpsAltitude = gpsAltitudeLocal;
+				}
+
+				var gpsAltitudeRefLocal = GetContentNameSpace(property, "exif:GPSAltitudeRef");
+				if ( gpsAltitudeRefLocal != null )
+				{
+					gpsAltitudeRef = gpsAltitudeRefLocal;
+				}
+			}
+			if ( gpsAltitude == null || gpsAltitudeRef == null ) return;
+			if ( !gpsAltitude.Contains('/') ) return;
 
 			var locationAltitude = MathFraction.Fraction(gpsAltitude);
-	        if(Math.Abs(locationAltitude) < 0) return;
-	        item.LocationAltitude = locationAltitude;
-	        
-            //For items under the sea level
-            if (gpsAltitudeRef == "1") item.LocationAltitude = item.LocationAltitude * -1;
-        }
-
-
-
-
-	    /// 
-	    /// ContentNameSpace is for example : Namespace=http://...
-	    /// 
-	    /// 
-	    /// 
-	    /// 
-	    private static FileIndexItem GetDataContentNameSpaceTypes(IXmpMeta xmp, FileIndexItem item)
-	    {
-     
-            GpsAltitudeRef(xmp, item);
-                
-            foreach (var property in xmp.Properties)
-            {
-	            SetCombinedLatLong(property, item);
-	            SetCombinedDateTime(property,item);
-	            SetCombinedColorClass(property,item);
-	            SetCombinedOrientation(property,item);
-                SetCombinedImageHeightWidth(property,item);
-                SetCombinedCityStateCountry(property,item);
-	            
-	            // exif:ExposureTime http://ns.adobe.com/exif/1.0/
-	            var shutterSpeed = GetContentNameSpace(property, "exif:ExposureTime");
-	            if (shutterSpeed != null) item.ShutterSpeed = shutterSpeed;
-	            
-	            // exif:FNumber http://ns.adobe.com/exif/1.0/
-	            var aperture = GetContentNameSpace(property, "exif:FNumber");
-	            if (aperture != null) item.Aperture =  MathFraction.Fraction(aperture);
-	            
-	            // Path=tiff:Make Namespace=http://ns.adobe.com/tiff/1.0/ Value=SONY
-	            var make = GetContentNameSpace(property, "tiff:Make");
-	            if (make != null) item.SetMakeModel(make,0);
+			if ( Math.Abs(locationAltitude) < 0 ) return;
+			item.LocationAltitude = locationAltitude;
+
+			//For items under the sea level
+			if ( gpsAltitudeRef == "1" ) item.LocationAltitude = item.LocationAltitude * -1;
+		}
+
+
+
+
+		/// 
+		/// ContentNameSpace is for example : Namespace=http://...
+		/// 
+		/// 
+		/// 
+		/// 
+		private static FileIndexItem GetDataContentNameSpaceTypes(IXmpMeta xmp, FileIndexItem item)
+		{
+
+			GpsAltitudeRef(xmp, item);
+
+			foreach ( var property in xmp.Properties )
+			{
+				SetCombinedLatLong(property, item);
+				SetCombinedDateTime(property, item);
+				SetCombinedColorClass(property, item);
+				SetCombinedOrientation(property, item);
+				SetCombinedImageHeightWidth(property, item);
+				SetCombinedCityStateCountry(property, item);
+
+				// exif:ExposureTime http://ns.adobe.com/exif/1.0/
+				var shutterSpeed = GetContentNameSpace(property, "exif:ExposureTime");
+				if ( shutterSpeed != null ) item.ShutterSpeed = shutterSpeed;
+
+				// exif:FNumber http://ns.adobe.com/exif/1.0/
+				var aperture = GetContentNameSpace(property, "exif:FNumber");
+				if ( aperture != null ) item.Aperture = MathFraction.Fraction(aperture);
+
+				// Path=tiff:Make Namespace=http://ns.adobe.com/tiff/1.0/ Value=SONY
+				var make = GetContentNameSpace(property, "tiff:Make");
+				if ( make != null ) item.SetMakeModel(make, 0);
 
 				// Path=tiff:Model Namespace=http://ns.adobe.com/tiff/1.0/ Value=SLT-A58
-	            var model = GetContentNameSpace(property, "tiff:Model");
-	            if (model != null) item.SetMakeModel(model,1);
+				var model = GetContentNameSpace(property, "tiff:Model");
+				if ( model != null ) item.SetMakeModel(model, 1);
 
 				// Path=exif:FocalLength Namespace=http://ns.adobe.com/exif/1.0/ Value=200/1
 				// Path=exif:FocalLength Namespace=http://ns.adobe.com/exif/1.0/ Value=18/1
 				var focalLength = GetContentNameSpace(property, "exif:FocalLength");
-				if ( focalLength != null ) item.FocalLength =  MathFraction.Fraction(focalLength);
-	            
+				if ( focalLength != null ) item.FocalLength = MathFraction.Fraction(focalLength);
+
 				// Path=xmp:CreatorTool Namespace=http://ns.adobe.com/xap/1.0/ Value=SLT-A58 v1.00
 				var software = GetContentNameSpace(property, "xmp:CreatorTool");
 				if ( software != null ) item.Software = software;
-            }
-	        
-            return item;
-        }
-
-	    private static void SetCombinedOrientation(IXmpPropertyInfo property, FileIndexItem item)
-	    {
-		    // Path=tiff:Orientation Namespace=http://ns.adobe.com/tiff/1.0/ Value=6
-		    var rotation = GetContentNameSpace(property, "tiff:Orientation");
-		    if (rotation != null)
-		    {
-			    item.SetAbsoluteOrientation(rotation);
-		    }
-	    }
-
-	    private static void SetCombinedColorClass(IXmpPropertyInfo property, FileIndexItem item)
-	    {
-		    //   Path=photomechanic:ColorClass Namespace=http://ns.camerabits.com/photomechanic/1.0/ Value=1
-		    var colorClass = GetContentNameSpace(property, "photomechanic:ColorClass");
-		    if (colorClass != null)
-		    {
-			    item.ColorClass = ColorClassParser.GetColorClass(colorClass);
-		    }
-	    }
-
-	    private static void SetCombinedImageHeightWidth(IXmpPropertyInfo property, FileIndexItem item)
-	    {
-		    //  Path=tiff:ImageLength Namespace=http://ns.adobe.com/tiff/1.0/ Value=13656
-		    var height = GetContentNameSpace(property, "tiff:ImageLength");
-		    if (height != null) item.SetImageHeight(height);
-
-		    //  Path=tiff:ImageWidth Namespace=http://ns.adobe.com/tiff/1.0/ Value=15504
-		    var width = GetContentNameSpace(property, "tiff:ImageWidth");
-		    if (width != null) item.SetImageWidth(width);
-	    }
-
-	    private static void SetCombinedLatLong(
-		    IXmpPropertyInfo property, FileIndexItem item)
-	    {
-		    // Path=exif:GPSLatitude Namespace=http://ns.adobe.com/exif/1.0/ Value=52,20.708N
-		    var gpsLatitude = GetContentNameSpace(property, "exif:GPSLatitude");
-		    if (gpsLatitude != null)
-		    {
-			    item.Latitude = GpsPreParseAndConvertDegreeAngleToDouble(gpsLatitude);
-		    }
-
-		    // Path=exif:GPSLongitude Namespace=http://ns.adobe.com/exif/1.0/ Value=5,55.840E
-		    var gpsLongitude = GetContentNameSpace(property, "exif:GPSLongitude");
-		    if (gpsLongitude != null)
-		    {
-			    item.Longitude = GpsPreParseAndConvertDegreeAngleToDouble(gpsLongitude);
-		    }
-	    }
-
-	    private static void SetCombinedDateTime(
-		    IXmpPropertyInfo property, FileIndexItem item)
-	    {
-		    // Option 1 (Datetime)
-		    // Path=exif:DateTimeOriginal Namespace=http://ns.adobe.com/exif/1.0/ Value=2018-07-18T19:44:27
-		    var dateTimeOriginal = GetContentNameSpace(property, "exif:DateTimeOriginal");
-		    if ( dateTimeOriginal != null )
-		    {
-			    DateTime.TryParseExact(dateTimeOriginal,
-				    "yyyy-MM-dd\\THH:mm:ss",
-				    CultureInfo.InvariantCulture,
-				    DateTimeStyles.None,
-				    out var dateTime);
-			    if ( dateTime.Year >= 3 ) item.DateTime = dateTime;
-		    }
-
-		    // Option 2 (Datetime)
-		    // Path=xmp:CreateDate Namespace=http://ns.adobe.com/xap/1.0/ Value=2019-03-02T11:29:18+01:00
-		    // Path=xmp:CreateDate Namespace=http://ns.adobe.com/xap/1.0/ Value=2019-03-02T11:29:18
-		    var createDate = GetContentNameSpace(property, "xmp:CreateDate");
-		    if (createDate != null)
-		    {
-			    DateTime.TryParseExact(createDate,
-				    "yyyy-MM-dd\\THH:mm:sszzz",
-				    CultureInfo.InvariantCulture,
-				    DateTimeStyles.None,
-				    out var dateTime);
-		            
-			    // The other option
-			    if ( dateTime.Year <= 3 )
-			    {
-				    DateTime.TryParseExact(createDate,
-					    "yyyy-MM-dd\\THH:mm:ss",
-					    CultureInfo.InvariantCulture,
-					    DateTimeStyles.None,
-					    out dateTime);
-			    }
-			    // and use this value
-			    item.DateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
-		    }
-	    }
-
-	    private static void SetCombinedCityStateCountry(
-		    IXmpPropertyInfo property, FileIndexItem item)
-	    {
-		    // Path=photoshop:City Namespace=http://ns.adobe.com/photoshop/1.0/ Value=Epe
-		    var locationCity = GetContentNameSpace(property, "photoshop:City");
-		    if (locationCity != null) item.LocationCity = locationCity;
-
-		    // Path=photoshop:State Namespace=http://ns.adobe.com/photoshop/1.0/ Value=Gelderland
-		    var locationState = GetContentNameSpace(property, "photoshop:State");
-		    if (locationState != null) item.LocationState = locationState;
-                
-		    // Path=photoshop:Country Namespace=http://ns.adobe.com/photoshop/1.0/ Value=Nederland
-		    var locationCountry = GetContentNameSpace(property, "photoshop:Country");
-		    if (locationCountry != null) item.LocationCountry = locationCountry;
-	    }
+			}
+
+			return item;
+		}
+
+		private static void SetCombinedOrientation(IXmpPropertyInfo property, FileIndexItem item)
+		{
+			// Path=tiff:Orientation Namespace=http://ns.adobe.com/tiff/1.0/ Value=6
+			var rotation = GetContentNameSpace(property, "tiff:Orientation");
+			if ( rotation != null )
+			{
+				item.SetAbsoluteOrientation(rotation);
+			}
+		}
+
+		private static void SetCombinedColorClass(IXmpPropertyInfo property, FileIndexItem item)
+		{
+			//   Path=photomechanic:ColorClass Namespace=http://ns.camerabits.com/photomechanic/1.0/ Value=1
+			var colorClass = GetContentNameSpace(property, "photomechanic:ColorClass");
+			if ( colorClass != null )
+			{
+				item.ColorClass = ColorClassParser.GetColorClass(colorClass);
+			}
+		}
+
+		private static void SetCombinedImageHeightWidth(IXmpPropertyInfo property, FileIndexItem item)
+		{
+			//  Path=tiff:ImageLength Namespace=http://ns.adobe.com/tiff/1.0/ Value=13656
+			var height = GetContentNameSpace(property, "tiff:ImageLength");
+			if ( height != null ) item.SetImageHeight(height);
+
+			//  Path=tiff:ImageWidth Namespace=http://ns.adobe.com/tiff/1.0/ Value=15504
+			var width = GetContentNameSpace(property, "tiff:ImageWidth");
+			if ( width != null ) item.SetImageWidth(width);
+		}
+
+		private static void SetCombinedLatLong(
+			IXmpPropertyInfo property, FileIndexItem item)
+		{
+			// Path=exif:GPSLatitude Namespace=http://ns.adobe.com/exif/1.0/ Value=52,20.708N
+			var gpsLatitude = GetContentNameSpace(property, "exif:GPSLatitude");
+			if ( gpsLatitude != null )
+			{
+				item.Latitude = GpsPreParseAndConvertDegreeAngleToDouble(gpsLatitude);
+			}
+
+			// Path=exif:GPSLongitude Namespace=http://ns.adobe.com/exif/1.0/ Value=5,55.840E
+			var gpsLongitude = GetContentNameSpace(property, "exif:GPSLongitude");
+			if ( gpsLongitude != null )
+			{
+				item.Longitude = GpsPreParseAndConvertDegreeAngleToDouble(gpsLongitude);
+			}
+		}
+
+		private static void SetCombinedDateTime(
+			IXmpPropertyInfo property, FileIndexItem item)
+		{
+			// Option 1 (Datetime)
+			// Path=exif:DateTimeOriginal Namespace=http://ns.adobe.com/exif/1.0/ Value=2018-07-18T19:44:27
+			var dateTimeOriginal = GetContentNameSpace(property, "exif:DateTimeOriginal");
+			if ( dateTimeOriginal != null )
+			{
+				DateTime.TryParseExact(dateTimeOriginal,
+					"yyyy-MM-dd\\THH:mm:ss",
+					CultureInfo.InvariantCulture,
+					DateTimeStyles.None,
+					out var dateTime);
+				if ( dateTime.Year >= 3 ) item.DateTime = dateTime;
+			}
+
+			// Option 2 (Datetime)
+			// Path=xmp:CreateDate Namespace=http://ns.adobe.com/xap/1.0/ Value=2019-03-02T11:29:18+01:00
+			// Path=xmp:CreateDate Namespace=http://ns.adobe.com/xap/1.0/ Value=2019-03-02T11:29:18
+			var createDate = GetContentNameSpace(property, "xmp:CreateDate");
+			if ( createDate != null )
+			{
+				DateTime.TryParseExact(createDate,
+					"yyyy-MM-dd\\THH:mm:sszzz",
+					CultureInfo.InvariantCulture,
+					DateTimeStyles.None,
+					out var dateTime);
+
+				// The other option
+				if ( dateTime.Year <= 3 )
+				{
+					DateTime.TryParseExact(createDate,
+						"yyyy-MM-dd\\THH:mm:ss",
+						CultureInfo.InvariantCulture,
+						DateTimeStyles.None,
+						out dateTime);
+				}
+				// and use this value
+				item.DateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
+			}
+		}
+
+		private static void SetCombinedCityStateCountry(
+			IXmpPropertyInfo property, FileIndexItem item)
+		{
+			// Path=photoshop:City Namespace=http://ns.adobe.com/photoshop/1.0/ Value=Epe
+			var locationCity = GetContentNameSpace(property, "photoshop:City");
+			if ( locationCity != null ) item.LocationCity = locationCity;
+
+			// Path=photoshop:State Namespace=http://ns.adobe.com/photoshop/1.0/ Value=Gelderland
+			var locationState = GetContentNameSpace(property, "photoshop:State");
+			if ( locationState != null ) item.LocationState = locationState;
+
+			// Path=photoshop:Country Namespace=http://ns.adobe.com/photoshop/1.0/ Value=Nederland
+			var locationCountry = GetContentNameSpace(property, "photoshop:Country");
+			if ( locationCountry != null ) item.LocationCountry = locationCountry;
+		}
 	}
 }
diff --git a/starsky/starsky.foundation.readmeta/Services/ReadMeta.cs b/starsky/starsky.foundation.readmeta/Services/ReadMeta.cs
index ececa5624c..cb424307f8 100644
--- a/starsky/starsky.foundation.readmeta/Services/ReadMeta.cs
+++ b/starsky/starsky.foundation.readmeta/Services/ReadMeta.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading.Tasks;
@@ -36,22 +36,22 @@ public ReadMeta(IStorage iStorage, AppSettings appSettings, IMemoryCache? memory
 			_appSettings = appSettings;
 			_cache = memoryCache;
 			_iStorage = iStorage;
-			_readExif = new ReadMetaExif(_iStorage, appSettings,logger);
+			_readExif = new ReadMetaExif(_iStorage, appSettings, logger);
 			_readXmp = new ReadMetaXmp(_iStorage, logger);
 			_readMetaGpx = new ReadMetaGpx(logger);
 		}
 
 		private async Task ReadExifAndXmpFromFileDirectAsync(string subPath)
 		{
-			if ( _iStorage.ExistFile(subPath) 
-			     && ExtensionRolesHelper.IsExtensionForceGpx(subPath) )
+			if ( _iStorage.ExistFile(subPath)
+				 && ExtensionRolesHelper.IsExtensionForceGpx(subPath) )
 			{
 				// Get the item back with DateTime as Camera local datetime
 				return await _readMetaGpx.ReadGpxFromFileReturnAfterFirstFieldAsync(
-					_iStorage.ReadStream(subPath), 
+					_iStorage.ReadStream(subPath),
 					subPath); // use local
 			}
-	        
+
 			var fileIndexItemWithPath = new FileIndexItem(subPath);
 
 			// Read first the sidecar file
@@ -63,16 +63,16 @@ private async Task ReadExifAndXmpFromFileDirectAsync(string subPa
 				return xmpFileIndexItem;
 
 			if ( xmpFileIndexItem.IsoSpeed != 0
-			     && !string.IsNullOrEmpty(xmpFileIndexItem.Make)
-			     && xmpFileIndexItem.DateTime.Year != 0
-			     && !string.IsNullOrEmpty(xmpFileIndexItem.ShutterSpeed) )
+				 && !string.IsNullOrEmpty(xmpFileIndexItem.Make)
+				 && xmpFileIndexItem.DateTime.Year != 0
+				 && !string.IsNullOrEmpty(xmpFileIndexItem.ShutterSpeed) )
 			{
 				return xmpFileIndexItem;
 			}
-			
+
 			// so the sidecar file is not used to store the most important tags
-			var fileExifItemFile = _readExif.ReadExifFromFile(subPath,fileIndexItemWithPath);
-		        
+			var fileExifItemFile = _readExif.ReadExifFromFile(subPath, fileIndexItemWithPath);
+
 			// overwrite content with incomplete sidecar file (this file can contain tags)
 			FileIndexCompareHelper.Compare(fileExifItemFile, xmpFileIndexItem);
 			return fileExifItemFile;
@@ -86,9 +86,9 @@ public async Task> ReadExifAndXmpFromFileAddFilePathHashAsyn
 			for ( int i = 0; i < subPathList.Count; i++ )
 			{
 				var subPath = subPathList[i];
-		        
+
 				var returnItem = await ReadExifAndXmpFromFileAsync(subPath);
-				var imageFormat = ExtensionRolesHelper.GetImageFormat(_iStorage.ReadStream(subPath, 50)); 
+				var imageFormat = ExtensionRolesHelper.GetImageFormat(_iStorage.ReadStream(subPath, 50));
 
 				returnItem!.ImageFormat = imageFormat;
 				returnItem.FileName = Path.GetFileName(subPath);
@@ -98,7 +98,7 @@ public async Task> ReadExifAndXmpFromFileAddFilePathHashAsyn
 
 				if ( fileHashes == null || fileHashes.Count <= i )
 				{
-					returnItem.FileHash = (await new FileHash(_iStorage).GetHashCodeAsync(subPath)).Key;
+					returnItem.FileHash = ( await new FileHash(_iStorage).GetHashCodeAsync(subPath) ).Key;
 				}
 				else
 				{
@@ -122,25 +122,25 @@ public async Task> ReadExifAndXmpFromFileAddFilePathHashAsyn
 		public async Task ReadExifAndXmpFromFileAsync(string subPath)
 		{
 			// The CLI programs uses no cache
-			if( _cache == null || _appSettings?.AddMemoryCache == false) 
+			if ( _cache == null || _appSettings?.AddMemoryCache == false )
 				return await ReadExifAndXmpFromFileDirectAsync(subPath);
-            
+
 			// Return values from IMemoryCache
 			var queryReadMetaCacheName = CachePrefix + subPath;
-            
+
 			// Return Cached object if it exist
-			if (_cache.TryGetValue(queryReadMetaCacheName, out var objectExifToolModel))
+			if ( _cache.TryGetValue(queryReadMetaCacheName, out var objectExifToolModel) )
 				return objectExifToolModel as FileIndexItem;
-            
+
 			// Try to catch a new object
 			objectExifToolModel = await ReadExifAndXmpFromFileDirectAsync(subPath);
-			
-			_cache.Set(queryReadMetaCacheName, objectExifToolModel, 
-				new TimeSpan(0,1,0));
-			return (FileIndexItem?) objectExifToolModel!;
+
+			_cache.Set(queryReadMetaCacheName, objectExifToolModel,
+				new TimeSpan(0, 1, 0));
+			return ( FileIndexItem? )objectExifToolModel!;
 		}
 
-        
+
 		/// 
 		/// Update Cache only for ReadMeta!
 		/// To 15 minutes
@@ -149,13 +149,13 @@ public async Task> ReadExifAndXmpFromFileAddFilePathHashAsyn
 		/// the item
 		public void UpdateReadMetaCache(string fullFilePath, FileIndexItem objectExifToolModel)
 		{
-			if (_cache == null || _appSettings?.AddMemoryCache == false) return;
+			if ( _cache == null || _appSettings?.AddMemoryCache == false ) return;
 
 			var toUpdateObject = objectExifToolModel.Clone();
 			var queryReadMetaCacheName = CachePrefix + fullFilePath;
 			RemoveReadMetaCache(fullFilePath);
-			_cache.Set(queryReadMetaCacheName, toUpdateObject, 
-				new TimeSpan(0,15,0));
+			_cache.Set(queryReadMetaCacheName, toUpdateObject,
+				new TimeSpan(0, 15, 0));
 		}
 
 		/// 
@@ -179,10 +179,10 @@ public void UpdateReadMetaCache(IEnumerable objectExifToolModel)
 		/// can also be a subPath
 		public bool? RemoveReadMetaCache(string fullFilePath)
 		{
-			if (_cache == null || _appSettings?.AddMemoryCache == false) return null;
+			if ( _cache == null || _appSettings?.AddMemoryCache == false ) return null;
 			var queryCacheName = CachePrefix + fullFilePath;
 
-			if (!_cache.TryGetValue(queryCacheName, out _)) return false; 
+			if ( !_cache.TryGetValue(queryCacheName, out _) ) return false;
 			// continue = go to the next item in the list
 			_cache.Remove(queryCacheName);
 			return true;
diff --git a/starsky/starsky.foundation.readmeta/Services/ReadMetaSubPathStorage.cs b/starsky/starsky.foundation.readmeta/Services/ReadMetaSubPathStorage.cs
index b9f79d8d8f..85d7f0f8a3 100644
--- a/starsky/starsky.foundation.readmeta/Services/ReadMetaSubPathStorage.cs
+++ b/starsky/starsky.foundation.readmeta/Services/ReadMetaSubPathStorage.cs
@@ -1,4 +1,3 @@
-#nullable enable
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Caching.Memory;
@@ -20,13 +19,13 @@ public sealed class ReadMetaSubPathStorage : IReadMetaSubPathStorage
 		public ReadMetaSubPathStorage(ISelectorStorage selectorStorage, AppSettings appSettings, IMemoryCache memoryCache, IWebLogger logger)
 		{
 			var storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
-			_readMeta = new ReadMeta(storage, appSettings, memoryCache,logger);
+			_readMeta = new ReadMeta(storage, appSettings, memoryCache, logger);
 		}
-		
+
 		public async Task> ReadExifAndXmpFromFileAddFilePathHashAsync(List subPathList,
 			List? fileHashes = null)
 		{
-			return await _readMeta.ReadExifAndXmpFromFileAddFilePathHashAsync(subPathList,fileHashes);
+			return await _readMeta.ReadExifAndXmpFromFileAddFilePathHashAsync(subPathList, fileHashes);
 		}
 
 		public async Task ReadExifAndXmpFromFileAsync(string subPath)
diff --git a/starsky/starsky.foundation.realtime/Extentions/WebSocketConnectionsMiddlewareExtensions.cs b/starsky/starsky.foundation.realtime/Extentions/WebSocketConnectionsMiddlewareExtensions.cs
index ae0b6f38b4..a3bbf56ce9 100644
--- a/starsky/starsky.foundation.realtime/Extentions/WebSocketConnectionsMiddlewareExtensions.cs
+++ b/starsky/starsky.foundation.realtime/Extentions/WebSocketConnectionsMiddlewareExtensions.cs
@@ -8,8 +8,8 @@ namespace starsky.foundation.realtime.Extentions
 {
 	public static class WebSocketConnectionsMiddlewareExtensions
 	{
-		public static IApplicationBuilder MapWebSocketConnections(this IApplicationBuilder app, 
-			PathString pathMatch, WebSocketConnectionsOptions options, 
+		public static IApplicationBuilder MapWebSocketConnections(this IApplicationBuilder app,
+			PathString pathMatch, WebSocketConnectionsOptions options,
 			bool? featureToggleEnabled = true)
 		{
 			ArgumentNullException.ThrowIfNull(app);
diff --git a/starsky/starsky.foundation.realtime/Helpers/WebSocketConnection.cs b/starsky/starsky.foundation.realtime/Helpers/WebSocketConnection.cs
index 3d75865485..10d1af7cb2 100644
--- a/starsky/starsky.foundation.realtime/Helpers/WebSocketConnection.cs
+++ b/starsky/starsky.foundation.realtime/Helpers/WebSocketConnection.cs
@@ -10,7 +10,7 @@ namespace starsky.foundation.realtime.Helpers
 	public sealed class WebSocketConnection
 	{
 		private readonly WebSocket _webSocket;
-		
+
 		private readonly int _receivePayloadBufferSize;
 
 		public Guid Id { get; } = Guid.NewGuid();
@@ -28,7 +28,7 @@ public WebSocketConnection(WebSocket webSocket, int receivePayloadBufferSize = 4
 			_webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket));
 			_receivePayloadBufferSize = receivePayloadBufferSize;
 		}
-		
+
 		/// 
 		/// Need to check for WebSocketException
 		/// 
@@ -65,7 +65,7 @@ public Task SendAsync(string message, CancellationToken cancellationToken)
 						// skip the offset of the message and check the length
 						var messageBytes = message.Skip(message.Offset).Take(result.Count).ToArray();
 						receivedMessageStringBuilder.Append(Encoding.UTF8.GetString(messageBytes));
-					} 
+					}
 					while ( !result.EndOfMessage );
 
 					var receivedMessage = receivedMessageStringBuilder.ToString();
@@ -78,7 +78,7 @@ public Task SendAsync(string message, CancellationToken cancellationToken)
 
 			}
 			catch ( WebSocketException wsex ) when ( wsex.WebSocketErrorCode ==
-			                                         WebSocketError.InvalidState )
+													 WebSocketError.InvalidState )
 			{
 				return WebSocketCloseStatus.NormalClosure;
 			}
diff --git a/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs b/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs
index 65941ac384..caa5a788f1 100644
--- a/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs
+++ b/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs
@@ -19,7 +19,7 @@ public async Task Invoke(HttpContext context)
 			{
 				var webSocket = await context.WebSockets.AcceptWebSocketAsync();
 				// StatusCode MessageTooBig = 1009
-				await webSocket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig, 
+				await webSocket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig,
 					"Feature toggle disabled", CancellationToken.None);
 				return;
 			}
diff --git a/starsky/starsky.foundation.realtime/Middleware/WebSocketConnectionsMiddleware.cs b/starsky/starsky.foundation.realtime/Middleware/WebSocketConnectionsMiddleware.cs
index 94d7fd2955..a912a4a89c 100644
--- a/starsky/starsky.foundation.realtime/Middleware/WebSocketConnectionsMiddleware.cs
+++ b/starsky/starsky.foundation.realtime/Middleware/WebSocketConnectionsMiddleware.cs
@@ -1,8 +1,6 @@
 using System;
-using System.Globalization;
 using System.Net.WebSockets;
 using System.Text.Json;
-using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
@@ -17,50 +15,50 @@ namespace starsky.foundation.realtime.Middleware
 {
 	public sealed class WebSocketConnectionsMiddleware
 	{
-		#region Fields
 		private readonly WebSocketConnectionsOptions _options;
 		private readonly IWebSocketConnectionsService _connectionsService;
-		#endregion
 
-		#region Constructor
-		public WebSocketConnectionsMiddleware(RequestDelegate _, WebSocketConnectionsOptions options, 
+
+		public WebSocketConnectionsMiddleware(RequestDelegate _,
+			WebSocketConnectionsOptions options,
 			IWebSocketConnectionsService connectionsService)
 		{
 			_options = options ?? throw new ArgumentNullException(nameof(options));
-			_connectionsService = connectionsService ?? throw new ArgumentNullException(nameof(connectionsService));
+			_connectionsService = connectionsService ??
+								  throw new ArgumentNullException(nameof(connectionsService));
 		}
-		#endregion
-		
-		#region Methods
+
+
 		public async Task Invoke(HttpContext context)
 		{
-			if (ValidateOrigin(context))
+			if ( ValidateOrigin(context) )
 			{
-				if (context.WebSockets.IsWebSocketRequest)
+				if ( context.WebSockets.IsWebSocketRequest )
 				{
 					WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
-                    
-					if ( context.User.Identity?.IsAuthenticated == false)
+
+					if ( context.User.Identity?.IsAuthenticated == false )
 					{
 						// Status Code 1008 PolicyViolation
-						await webSocket.CloseOutputAsync(WebSocketCloseStatus.PolicyViolation, 
+						await webSocket.CloseOutputAsync(WebSocketCloseStatus.PolicyViolation,
 							"Please login first", CancellationToken.None);
 						return;
 					}
-                    
-					WebSocketConnection webSocketConnection = new WebSocketConnection(webSocket, _options.ReceivePayloadBufferSize);
 
-					async void OnWebSocketConnectionOnNewConnection(object? sender, EventArgs message)
+					WebSocketConnection webSocketConnection =
+						new WebSocketConnection(webSocket, _options.ReceivePayloadBufferSize);
+
+					async void OnWebSocketConnectionOnNewConnection(object? sender,
+						EventArgs message)
 					{
 						await Task.Delay(150);
 						try
 						{
 							var welcomeMessage = new ApiNotificationResponseModel(
 								new HeartbeatModel(null))
-							{
-								Type =  ApiNotificationType.Welcome,
-							};
-							await webSocketConnection.SendAsync(JsonSerializer.Serialize(welcomeMessage,
+							{ Type = ApiNotificationType.Welcome, };
+							await webSocketConnection.SendAsync(JsonSerializer.Serialize(
+								welcomeMessage,
 								DefaultJsonSerializer.CamelCaseNoEnters), CancellationToken.None);
 						}
 						catch ( WebSocketException )
@@ -75,9 +73,9 @@ await webSocketConnection.SendAsync(JsonSerializer.Serialize(welcomeMessage,
 
 					await webSocketConnection.ReceiveMessagesUntilCloseAsync();
 
-					if (webSocketConnection.CloseStatus.HasValue)
+					if ( webSocketConnection.CloseStatus.HasValue )
 					{
-						await webSocket.CloseOutputAsync(webSocketConnection.CloseStatus.Value, 
+						await webSocket.CloseOutputAsync(webSocketConnection.CloseStatus.Value,
 							webSocketConnection.CloseStatusDescription, CancellationToken.None);
 					}
 
@@ -96,10 +94,10 @@ await webSocket.CloseOutputAsync(webSocketConnection.CloseStatus.Value,
 
 		private bool ValidateOrigin(HttpContext context)
 		{
-			return (_options.AllowedOrigins == null) || (_options.AllowedOrigins.Count == 0) || (
-				_options.AllowedOrigins.Contains(context.Request.Headers.Origin.ToString()));
+			return ( _options.AllowedOrigins == null ) || ( _options.AllowedOrigins.Count == 0 ) ||
+				   (
+					   _options.AllowedOrigins.Contains(context.Request.Headers.Origin
+						   .ToString()) );
 		}
-
-		#endregion
 	}
 }
diff --git a/starsky/starsky.foundation.realtime/Model/HeartbeatModel.cs b/starsky/starsky.foundation.realtime/Model/HeartbeatModel.cs
index fcac774ad5..a73832abcd 100644
--- a/starsky/starsky.foundation.realtime/Model/HeartbeatModel.cs
+++ b/starsky/starsky.foundation.realtime/Model/HeartbeatModel.cs
@@ -11,10 +11,10 @@ public HeartbeatModel(int? speedInSeconds)
 		{
 			SpeedInSeconds = speedInSeconds;
 		}
-		
+
 		[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 		public int? SpeedInSeconds { get; set; }
-		
+
 		public DateTime DateTime { get; set; } = DateTime.UtcNow;
 	}
 }
diff --git a/starsky/starsky.foundation.realtime/Services/HeartBeatService.cs b/starsky/starsky.foundation.realtime/Services/HeartBeatService.cs
index 752081e373..e748794353 100644
--- a/starsky/starsky.foundation.realtime/Services/HeartBeatService.cs
+++ b/starsky/starsky.foundation.realtime/Services/HeartBeatService.cs
@@ -30,18 +30,18 @@ public Task StartAsync(CancellationToken cancellationToken)
 			_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 
 			_heartbeatTask = HeartbeatAsync(_cancellationTokenSource.Token);
-			
+
 			if ( _heartbeatTask.IsCompleted )
 			{
 				_cancellationTokenSource.Dispose();
 			}
-			
+
 			return _heartbeatTask.IsCompleted ? _heartbeatTask : Task.CompletedTask;
 		}
 
 		public async Task StopAsync(CancellationToken cancellationToken)
 		{
-			if (_heartbeatTask != null)
+			if ( _heartbeatTask != null )
 			{
 				if ( _cancellationTokenSource != null )
 				{
@@ -54,7 +54,7 @@ public async Task StopAsync(CancellationToken cancellationToken)
 				{
 					cancellationToken.ThrowIfCancellationRequested();
 				}
-				catch (OperationCanceledException)
+				catch ( OperationCanceledException )
 				{
 					// do nothing
 				}
@@ -63,10 +63,10 @@ public async Task StopAsync(CancellationToken cancellationToken)
 
 		private async Task HeartbeatAsync(CancellationToken cancellationToken)
 		{
-			while (!cancellationToken.IsCancellationRequested)
+			while ( !cancellationToken.IsCancellationRequested )
 			{
 				var webSocketResponse =
-					new ApiNotificationResponseModel(new HeartbeatModel(SpeedInSeconds), 
+					new ApiNotificationResponseModel(new HeartbeatModel(SpeedInSeconds),
 						ApiNotificationType.Heartbeat);
 				await _connectionsService.SendToAllAsync(webSocketResponse, cancellationToken);
 				await Task.Delay(TimeSpan.FromSeconds(SpeedInSeconds), cancellationToken);
diff --git a/starsky/starsky.foundation.realtime/Services/WebSocketConnectionsService.cs b/starsky/starsky.foundation.realtime/Services/WebSocketConnectionsService.cs
index 06d3b5a5fa..81625a0236 100644
--- a/starsky/starsky.foundation.realtime/Services/WebSocketConnectionsService.cs
+++ b/starsky/starsky.foundation.realtime/Services/WebSocketConnectionsService.cs
@@ -21,7 +21,7 @@ public WebSocketConnectionsService(IWebLogger logger)
 		{
 			_logger = logger;
 		}
-		
+
 		private readonly ConcurrentDictionary _connections = new ConcurrentDictionary();
 		private readonly IWebLogger _logger;
 
@@ -38,13 +38,13 @@ public void RemoveConnection(Guid connectionId)
 		public Task SendToAllAsync(string message, CancellationToken cancellationToken)
 		{
 			List connectionsTasks = new List();
-			foreach (WebSocketConnection connection in _connections.Values)
+			foreach ( WebSocketConnection connection in _connections.Values )
 			{
 				try
 				{
 					connectionsTasks.Add(connection.SendAsync(message, cancellationToken));
 				}
-				catch ( WebSocketException exception)
+				catch ( WebSocketException exception )
 				{
 					// if the client is closing the socket the wrong way
 					_logger.LogInformation(exception, "catch-ed exception socket");
diff --git a/starsky/starsky.foundation.settings/Services/SettingsService.cs b/starsky/starsky.foundation.settings/Services/SettingsService.cs
index c1a8358e63..2825070676 100644
--- a/starsky/starsky.foundation.settings/Services/SettingsService.cs
+++ b/starsky/starsky.foundation.settings/Services/SettingsService.cs
@@ -32,7 +32,7 @@ public SettingsService(ApplicationDbContext dbContext, IServiceScopeFactory scop
 			return await context.Settings.AsNoTracking()
 				.FirstOrDefaultAsync(p => p.Key == Enum.GetName(key));
 		}
-		
+
 		try
 		{
 			return await GetSettingLocal(_context);
@@ -52,15 +52,15 @@ public SettingsService(ApplicationDbContext dbContext, IServiceScopeFactory scop
 
 	public static T? CastSetting(SettingsItem? data)
 	{
-		if ( data?.Value == null) return default;
-		
-		if (typeof(T) == typeof(DateTime) && DateTime.TryParseExact(data.Value, 
-			    SettingsFormats.LastSyncBackgroundDateTime, 
-			    CultureInfo.InvariantCulture, 
-			    DateTimeStyles.AssumeUniversal, 
-			    out var expectDateTime) )
+		if ( data?.Value == null ) return default;
+
+		if ( typeof(T) == typeof(DateTime) && DateTime.TryParseExact(data.Value,
+				SettingsFormats.LastSyncBackgroundDateTime,
+				CultureInfo.InvariantCulture,
+				DateTimeStyles.AssumeUniversal,
+				out var expectDateTime) )
 		{
-			return (T)(object) expectDateTime;
+			return ( T )( object )expectDateTime;
 		}
 
 		try
@@ -77,7 +77,7 @@ public SettingsService(ApplicationDbContext dbContext, IServiceScopeFactory scop
 	{
 		return await AddOrUpdateSetting(new SettingsItem
 		{
-			Key = Enum.GetName(key) ?? string.Empty, 
+			Key = Enum.GetName(key) ?? string.Empty,
 			Value = value
 		});
 	}
@@ -90,31 +90,31 @@ public SettingsService(ApplicationDbContext dbContext, IServiceScopeFactory scop
 		}
 
 		var existingItem = ( await GetSetting(settingsType) )?.Value;
-		if (string.IsNullOrEmpty(existingItem))
+		if ( string.IsNullOrEmpty(existingItem) )
 		{
 
 			try
 			{
-				return await AddItem(_context,item);
+				return await AddItem(_context, item);
 			}
 			catch ( ObjectDisposedException )
 			{
 				var context = new InjectServiceScope(_scopeFactory).Context();
-				return await AddItem(context,item);
+				return await AddItem(context, item);
 			}
 		}
-		
+
 		try
 		{
-			return await UpdateItem(_context,item);
+			return await UpdateItem(_context, item);
 		}
 		catch ( ObjectDisposedException )
 		{
 			var context = new InjectServiceScope(_scopeFactory).Context();
-			return await UpdateItem(context,item);
+			return await UpdateItem(context, item);
 		}
 	}
-		
+
 	private static async Task AddItem(ApplicationDbContext context, SettingsItem item)
 	{
 		context.Settings.Add(item);
@@ -122,7 +122,7 @@ private static async Task AddItem(ApplicationDbContext context, Se
 		context.Attach(item).State = EntityState.Detached;
 		return item;
 	}
-	
+
 	private static async Task UpdateItem(ApplicationDbContext context, SettingsItem item)
 	{
 		context.Attach(item).State = EntityState.Modified;
diff --git a/starsky/starsky.foundation.settings/starsky.foundation.settings.csproj b/starsky/starsky.foundation.settings/starsky.foundation.settings.csproj
index c186745d31..6b60969f78 100644
--- a/starsky/starsky.foundation.settings/starsky.foundation.settings.csproj
+++ b/starsky/starsky.foundation.settings/starsky.foundation.settings.csproj
@@ -1,20 +1,19 @@
 
+    
+        net8.0
+        starsky.foundation.settings
+        
+        {67e301f1-e700-4ca5-81ae-696abab1bb0f}
+        0.6.0-beta.0
+        enable
+    
 
-
-    net8.0
-    starsky.foundation.settings
-    
-    {67e301f1-e700-4ca5-81ae-696abab1bb0f}
-    0.6.0-beta.0
-    enable
-
+    
+        
+        
+    
 
-
-  
-  
-
-
-
-    true
-
+    
+        true
+    
 
diff --git a/starsky/starsky.foundation.storage/ArchiveFormats/TarBal.cs b/starsky/starsky.foundation.storage/ArchiveFormats/TarBal.cs
index a7dcfce089..ffcafb6856 100644
--- a/starsky/starsky.foundation.storage/ArchiveFormats/TarBal.cs
+++ b/starsky/starsky.foundation.storage/ArchiveFormats/TarBal.cs
@@ -36,12 +36,12 @@ public async Task ExtractTarGz(Stream stream, string outputDir, CancellationToke
 			using var memStr = new MemoryStream();
 			int read;
 			var buffer = new byte[chunk];
-                    
-			while ((read = await gzip.ReadAsync(buffer, 0, buffer.Length,cancellationToken)) > 0)
+
+			while ( ( read = await gzip.ReadAsync(buffer, 0, buffer.Length, cancellationToken) ) > 0 )
 			{
 				await memStr.WriteAsync(buffer, 0, read, cancellationToken);
 			}
-			
+
 			gzip.Close();
 			memStr.Seek(0, SeekOrigin.Begin);
 			await ExtractTar(memStr, outputDir, cancellationToken);
@@ -57,11 +57,11 @@ public async Task ExtractTar(Stream stream, string outputDir, CancellationToken
 		{
 			var buffer = new byte[100];
 			var longFileName = string.Empty;
-			while (true)
+			while ( true )
 			{
 				await stream.ReadAsync(buffer, 0, 100, cancellationToken);
 				var name = string.IsNullOrEmpty(longFileName) ? Encoding.ASCII.GetString(buffer).Trim('\0') : longFileName; //Use longFileName if we have one read
-				if (string.IsNullOrWhiteSpace(name))
+				if ( string.IsNullOrWhiteSpace(name) )
 					break;
 				stream.Seek(24, SeekOrigin.Current);
 				await stream.ReadAsync(buffer, 0, 12, cancellationToken);
@@ -70,7 +70,7 @@ public async Task ExtractTar(Stream stream, string outputDir, CancellationToken
 				var typeTag = stream.ReadByte();
 				stream.Seek(355L, SeekOrigin.Current); //Move head to beginning of data (byte 512)
 
-				if (typeTag == 'L')
+				if ( typeTag == 'L' )
 				{
 					//We have a long file name
 					longFileName = await CreateLongFileName(size, stream, cancellationToken);
@@ -84,8 +84,8 @@ public async Task ExtractTar(Stream stream, string outputDir, CancellationToken
 
 				//Move head to next 512 byte block 
 				var pos = stream.Position;
-				var offset = 512 - (pos % 512);
-				if (offset == 512)
+				var offset = 512 - ( pos % 512 );
+				if ( offset == 512 )
 					offset = 0;
 
 				stream.Seek(offset, SeekOrigin.Current);
@@ -104,13 +104,13 @@ private static async Task CreateLongFileName(long size, Stream stream, C
 		private async Task CreateFileOrDirectory(string outputDir, string name, long size, Stream stream, CancellationToken cancellationToken)
 		{
 			var output = $"{outputDir}{_pathSeparator}{name}";
-			
+
 			if ( !_storage.ExistFolder(FilenamesHelper.GetParentPath(output)) )
 			{
 				_storage.CreateDirectory(FilenamesHelper.GetParentPath(output));
 			}
 
-			if (!name.EndsWith('/')) // Directories are zero size and don't need anything written
+			if ( !name.EndsWith('/') ) // Directories are zero size and don't need anything written
 			{
 				var str = new MemoryStream();
 				var buf = new byte[size];
@@ -119,6 +119,6 @@ private async Task CreateFileOrDirectory(string outputDir, string name, long siz
 				_storage.WriteStreamOpenOrCreate(str, output);
 			}
 		}
-		
+
 	}
 }
diff --git a/starsky/starsky.foundation.storage/ArchiveFormats/Zipper.cs b/starsky/starsky.foundation.storage/ArchiveFormats/Zipper.cs
index 9f9f193de9..90c7011bcf 100644
--- a/starsky/starsky.foundation.storage/ArchiveFormats/Zipper.cs
+++ b/starsky/starsky.foundation.storage/ArchiveFormats/Zipper.cs
@@ -19,7 +19,7 @@ public sealed class Zipper
 		/// output e.g. /folder/
 		/// 
 		[SuppressMessage("Usage", "S5042:Make sure that decompressing this archive file is safe")]
-		public bool ExtractZip( string zipInputFullPath, string storeZipFolderFullPath)
+		public bool ExtractZip(string zipInputFullPath, string storeZipFolderFullPath)
 		{
 			if ( !File.Exists(zipInputFullPath) ) return false;
 			// Ensures that the last character on the extraction path
@@ -36,25 +36,25 @@ public bool ExtractZip( string zipInputFullPath, string storeZipFolderFullPath)
 
 					// Ordinal match is safest, case-sensitive volumes can be mounted within volumes that
 					// are case-insensitive.
-					if (destinationPath.StartsWith(storeZipFolderFullPath, StringComparison.Ordinal))
-						entry.ExtractToFile(destinationPath,true);
+					if ( destinationPath.StartsWith(storeZipFolderFullPath, StringComparison.Ordinal) )
+						entry.ExtractToFile(destinationPath, true);
 				}
 			}
 
 			return true;
 		}
-		
-		public static Dictionary ExtractZip(byte[] zipped)
+
+		public static Dictionary ExtractZip(byte[] zipped)
 		{
 			using var memoryStream = new MemoryStream(zipped);
 			using var archive = new ZipArchive(memoryStream);
 			var result = new Dictionary();
-			foreach (var entry in archive.Entries)
+			foreach ( var entry in archive.Entries )
 			{
 				// only the first item
 				using var entryStream = entry.Open();
 				using var reader = new BinaryReader(entryStream);
-				result.Add(entry.FullName, reader.ReadBytes((int)entry.Length));
+				result.Add(entry.FullName, reader.ReadBytes(( int )entry.Length));
 			}
 			return result;
 		}
@@ -69,18 +69,18 @@ public static Dictionary ExtractZip(byte[] zipped)
 		/// list of filenames
 		/// to name of the zip file (zipHash)
 		/// a zip in the temp folder
-		public string CreateZip(string storeZipFolderFullPath, List filePaths, 
+		public string CreateZip(string storeZipFolderFullPath, List filePaths,
 			List fileNames, string zipOutputFilename)
 		{
 
-			var tempFileFullPath = Path.Combine(storeZipFolderFullPath,zipOutputFilename) + ".zip";
+			var tempFileFullPath = Path.Combine(storeZipFolderFullPath, zipOutputFilename) + ".zip";
 
 			// Has a direct dependency on the filesystem to avoid large content in memory
-			if(File.Exists(tempFileFullPath))
+			if ( File.Exists(tempFileFullPath) )
 			{
 				return tempFileFullPath;
 			}
-			
+
 			ZipArchive zip = ZipFile.Open(tempFileFullPath, ZipArchiveMode.Create);
 
 			for ( int i = 0; i < filePaths.Count; i++ )
diff --git a/starsky/starsky.foundation.storage/Exceptions/DecodingException.cs b/starsky/starsky.foundation.storage/Exceptions/DecodingException.cs
index ef3183ef99..f65fa66c9f 100644
--- a/starsky/starsky.foundation.storage/Exceptions/DecodingException.cs
+++ b/starsky/starsky.foundation.storage/Exceptions/DecodingException.cs
@@ -13,13 +13,13 @@ public class DecodingException : Exception
 		public DecodingException(string message) : base(message)
 		{
 		}
-		
+
 		/// 
 		/// Without this constructor, deserialization will fail
 		/// 
 		/// 
 		/// 
-		protected DecodingException(SerializationInfo info, StreamingContext context) 
+		protected DecodingException(SerializationInfo info, StreamingContext context)
 #pragma warning disable SYSLIB0051
 			: base(info, context)
 #pragma warning restore SYSLIB0051
diff --git a/starsky/starsky.foundation.storage/Helpers/DeserializeJson.cs b/starsky/starsky.foundation.storage/Helpers/DeserializeJson.cs
index 20b9fc64d7..8c0ea61c74 100644
--- a/starsky/starsky.foundation.storage/Helpers/DeserializeJson.cs
+++ b/starsky/starsky.foundation.storage/Helpers/DeserializeJson.cs
@@ -13,8 +13,8 @@ public sealed class DeserializeJson
 		public DeserializeJson(IStorage iStorage)
 		{
 			_iStorage = iStorage;
-		}	
-		
+		}
+
 		/// 
 		/// Read Json
 		/// 
diff --git a/starsky/starsky.foundation.storage/Helpers/StreamToStringHelper.cs b/starsky/starsky.foundation.storage/Helpers/StreamToStringHelper.cs
index ecbecbde6b..cd9389e15a 100644
--- a/starsky/starsky.foundation.storage/Helpers/StreamToStringHelper.cs
+++ b/starsky/starsky.foundation.storage/Helpers/StreamToStringHelper.cs
@@ -24,7 +24,7 @@ public static async Task StreamToStringAsync(Stream stream, bool dispose
 		{
 			await stream.DisposeAsync();
 		}
-		return result;  
+		return result;
 	}
 
 }
diff --git a/starsky/starsky.foundation.storage/Interfaces/IStorage.cs b/starsky/starsky.foundation.storage/Interfaces/IStorage.cs
index fcadab5b3a..9965c1eb40 100644
--- a/starsky/starsky.foundation.storage/Interfaces/IStorage.cs
+++ b/starsky/starsky.foundation.storage/Interfaces/IStorage.cs
@@ -18,7 +18,7 @@ public interface IStorage
 
 	void CreateDirectory(string path);
 	bool FolderDelete(string path);
-		
+
 	/// 
 	/// Returns a list of Files in a directory (non-Recursive)
 	/// to filter use:
@@ -28,7 +28,7 @@ public interface IStorage
 	/// filePath
 	/// 
 	IEnumerable GetAllFilesInDirectory(string path);
-		
+
 	/// 
 	/// Returns a list of Files in a directory (Recursive)
 	/// to filter use:
@@ -38,7 +38,7 @@ public interface IStorage
 	/// subPath, path relative to the database
 	/// list of files
 	IEnumerable GetAllFilesInDirectoryRecursive(string path);
-		
+
 	/// 
 	/// Returns a NON-Recursive list of child directories
 	/// 
@@ -52,7 +52,7 @@ public interface IStorage
 	/// 
 	/// directory
 	/// list
-	IEnumerable> GetDirectoryRecursive(string path);
+	IEnumerable> GetDirectoryRecursive(string path);
 
 	/// 
 	/// Read Stream (and keep open)
@@ -61,7 +61,7 @@ public interface IStorage
 	/// how many bytes are read (default all or -1)
 	/// Stream with data (non-disposed)
 	Stream ReadStream(string path, int maxRead = -1);
-		
+
 	bool WriteStream(Stream stream, string path);
 	bool WriteStreamOpenOrCreate(Stream stream, string path);
 
@@ -72,7 +72,7 @@ public interface IStorage
 	/// where to write to
 	/// is Success
 	Task WriteStreamAsync(Stream stream, string path);
-		
+
 	StorageInfo Info(string path);
 
 	DateTime SetLastWriteTime(string path, DateTime? dateTime = null);
diff --git a/starsky/starsky.foundation.storage/Models/FolderOrFileModel.cs b/starsky/starsky.foundation.storage/Models/FolderOrFileModel.cs
index 78aa423a1c..0a8c5c3b41 100644
--- a/starsky/starsky.foundation.storage/Models/FolderOrFileModel.cs
+++ b/starsky/starsky.foundation.storage/Models/FolderOrFileModel.cs
@@ -1,20 +1,20 @@
-namespace starsky.foundation.storage.Models
+namespace starsky.foundation.storage.Models
 {
-    public sealed class FolderOrFileModel
-    {
-	    /// 
-	    /// To Store output if file exist, folder or deleted
-	    /// 
-        public FolderOrFileTypeList IsFolderOrFile { get; set; }
-	    
-	    /// 
-	    /// Enum FolderOrFileTypeList
-	    /// 
-        public enum FolderOrFileTypeList
-        {
-            Folder = 1,
-            File = 2,
-            Deleted = 0
-        }
-    }
+	public sealed class FolderOrFileModel
+	{
+		/// 
+		/// To Store output if file exist, folder or deleted
+		/// 
+		public FolderOrFileTypeList IsFolderOrFile { get; set; }
+
+		/// 
+		/// Enum FolderOrFileTypeList
+		/// 
+		public enum FolderOrFileTypeList
+		{
+			Folder = 1,
+			File = 2,
+			Deleted = 0
+		}
+	}
 }
diff --git a/starsky/starsky.foundation.storage/Models/StorageInfo.cs b/starsky/starsky.foundation.storage/Models/StorageInfo.cs
index ae2d7897a0..24db2aa7d8 100644
--- a/starsky/starsky.foundation.storage/Models/StorageInfo.cs
+++ b/starsky/starsky.foundation.storage/Models/StorageInfo.cs
@@ -5,7 +5,7 @@ namespace starsky.foundation.storage.Models
 	public sealed class StorageInfo
 	{
 		public FolderOrFileModel.FolderOrFileTypeList IsFolderOrFile { get; set; }
-		
+
 		/// 
 		/// Size in bytes
 		/// 
@@ -20,7 +20,7 @@ public sealed class StorageInfo
 		/// Is the object a directory
 		/// 
 		public bool? IsDirectory { get; set; }
-		
+
 		/// 
 		/// Is the filesystem readonly (**NOT** the setting in appSettings)
 		/// 
diff --git a/starsky/starsky.foundation.storage/Services/Base32.cs b/starsky/starsky.foundation.storage/Services/Base32.cs
index a623d6ac4d..7d7bc2d0ee 100644
--- a/starsky/starsky.foundation.storage/Services/Base32.cs
+++ b/starsky/starsky.foundation.storage/Services/Base32.cs
@@ -25,166 +25,166 @@
 
 namespace starsky.foundation.storage.Services
 {
-    /// 
-    /// Create a Base32 string based on byte max arrond 30
-    /// 
-    [SuppressMessage("Usage", "S3963:Initialize all 'static fields' inline and remove the 'static",
-	    Justification = "as designed")]
-    public static class Base32
-    {
-
-        private static readonly char[] Digits;
-        private static readonly int Mask;
-        private static readonly int Shift;
-        private static readonly Dictionary CharMap = new Dictionary();
-        private const string Separator = "-";
-
-        static Base32()
-        {
-            Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
-            Mask = Digits.Length - 1;
-            Shift = NumberOfTrailingZeros(Digits.Length);
-            for (int i = 0; i < Digits.Length; i++) CharMap[Digits[i]] = i;
-        }
-
-        private static int NumberOfTrailingZeros(int i)
-        {
-            // HD, Figure 5-14
-            int y;
-            if (i == 0) return 32;
-            int n = 31;
-            y = i << 16;
-            if (y != 0)
-            {
-                n = n - 16;
-                i = y;
-            }
-
-            y = i << 8;
-            if (y != 0)
-            {
-                n = n - 8;
-                i = y;
-            }
-
-            y = i << 4;
-            if (y != 0)
-            {
-                n = n - 4;
-                i = y;
-            }
-
-            y = i << 2;
-            if (y != 0)
-            {
-                n = n - 2;
-                i = y;
-            }
-
-            return n - (int) ((uint) (i << 1) >> 31);
-        }
-
-        public static byte[] Decode(string encoded)
-        {
-            // Remove whitespace and separators
-            encoded = encoded.Trim().Replace(Separator, "");
-
-            // Remove padding. Note: the padding is used as hint to determine how many
-            // bits to decode from the last incomplete chunk (which is commented out
-            // below, so this may have been wrong to start with).
-            encoded = Regex.Replace(encoded, "[=]*$", "", 
-	            RegexOptions.None, TimeSpan.FromMilliseconds(100));
-
-            // Canonicalize to all upper case
-            encoded = encoded.ToUpper();
-            if (encoded.Length == 0)
-            {
-                return Array.Empty();
-            }
-
-            var encodedLength = encoded.Length;
-            var outLength = encodedLength * Shift / 8;
-            var result = new byte[outLength];
-            var buffer = 0;
-            var next = 0;
-            var bitsLeft = 0;
-            
-            foreach (var c in encoded)
-            {
-                if (!CharMap.TryGetValue(c, out var value) )
-                {
-                    throw new DecodingException("Illegal character: " + c);
-                }
-
-                buffer <<= Shift;
-                buffer |= value & Mask;
-                bitsLeft += Shift;
-                if ( bitsLeft < 8 )
-                {
-	                continue;
-                }
-                result[next++] = (byte) (buffer >> (bitsLeft - 8));
-                bitsLeft -= 8;
-            }
-
-            return result;
-        }
-
-        public static string Encode(byte[] data, bool padOutput = false)
-        {
-            if (data.Length == 0)
-            {
-                return "";
-            }
-
-            // SHIFT is the number of bits per output character, so the length of the
-            // output is the length of the input multiplied by 8/SHIFT, rounded up.
-	        // so: 268435456
-            if (data.Length >= (1 << 28))
-            {
-                // The computation below will fail, so don't do it.
-                throw new ArgumentOutOfRangeException(nameof(data));
-            }
-
-            int outputLength = (data.Length * 8 + Shift - 1) / Shift;
-            StringBuilder result = new StringBuilder(outputLength);
-
-            int buffer = data[0];
-            int next = 1;
-            int bitsLeft = 8;
-            while (bitsLeft > 0 || next < data.Length)
-            {
-                if (bitsLeft < Shift)
-                {
-                    if (next < data.Length)
-                    {
-                        buffer <<= 8;
-                        buffer |= (data[next++] & 0xff);
-                        bitsLeft += 8;
-                    }
-                    else
-                    {
-                        int pad = Shift - bitsLeft;
-                        buffer <<= pad;
-                        bitsLeft += pad;
-                    }
-                }
-
-                int index = Mask & (buffer >> (bitsLeft - Shift));
-                bitsLeft -= Shift;
-                result.Append(Digits[index]);
-            }
-
-            return Base32ReturnPadOutput(padOutput, result);
-        }
-
-        private static string Base32ReturnPadOutput(bool padOutput, StringBuilder result)
-        {
-            if (!padOutput) return result.ToString();
-            var padding = 8 - (result.Length % 8);
-            if (padding > 0) result.Append(new string('=', padding == 8 ? 0 : padding));
-            return result.ToString();
-        }
-       
-    }
+	/// 
+	/// Create a Base32 string based on byte max arrond 30
+	/// 
+	[SuppressMessage("Usage", "S3963:Initialize all 'static fields' inline and remove the 'static",
+		Justification = "as designed")]
+	public static class Base32
+	{
+
+		private static readonly char[] Digits;
+		private static readonly int Mask;
+		private static readonly int Shift;
+		private static readonly Dictionary CharMap = new Dictionary();
+		private const string Separator = "-";
+
+		static Base32()
+		{
+			Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
+			Mask = Digits.Length - 1;
+			Shift = NumberOfTrailingZeros(Digits.Length);
+			for ( int i = 0; i < Digits.Length; i++ ) CharMap[Digits[i]] = i;
+		}
+
+		private static int NumberOfTrailingZeros(int i)
+		{
+			// HD, Figure 5-14
+			int y;
+			if ( i == 0 ) return 32;
+			int n = 31;
+			y = i << 16;
+			if ( y != 0 )
+			{
+				n = n - 16;
+				i = y;
+			}
+
+			y = i << 8;
+			if ( y != 0 )
+			{
+				n = n - 8;
+				i = y;
+			}
+
+			y = i << 4;
+			if ( y != 0 )
+			{
+				n = n - 4;
+				i = y;
+			}
+
+			y = i << 2;
+			if ( y != 0 )
+			{
+				n = n - 2;
+				i = y;
+			}
+
+			return n - ( int )( ( uint )( i << 1 ) >> 31 );
+		}
+
+		public static byte[] Decode(string encoded)
+		{
+			// Remove whitespace and separators
+			encoded = encoded.Trim().Replace(Separator, "");
+
+			// Remove padding. Note: the padding is used as hint to determine how many
+			// bits to decode from the last incomplete chunk (which is commented out
+			// below, so this may have been wrong to start with).
+			encoded = Regex.Replace(encoded, "[=]*$", "",
+				RegexOptions.None, TimeSpan.FromMilliseconds(100));
+
+			// Canonicalize to all upper case
+			encoded = encoded.ToUpper();
+			if ( encoded.Length == 0 )
+			{
+				return Array.Empty();
+			}
+
+			var encodedLength = encoded.Length;
+			var outLength = encodedLength * Shift / 8;
+			var result = new byte[outLength];
+			var buffer = 0;
+			var next = 0;
+			var bitsLeft = 0;
+
+			foreach ( var c in encoded )
+			{
+				if ( !CharMap.TryGetValue(c, out var value) )
+				{
+					throw new DecodingException("Illegal character: " + c);
+				}
+
+				buffer <<= Shift;
+				buffer |= value & Mask;
+				bitsLeft += Shift;
+				if ( bitsLeft < 8 )
+				{
+					continue;
+				}
+				result[next++] = ( byte )( buffer >> ( bitsLeft - 8 ) );
+				bitsLeft -= 8;
+			}
+
+			return result;
+		}
+
+		public static string Encode(byte[] data, bool padOutput = false)
+		{
+			if ( data.Length == 0 )
+			{
+				return "";
+			}
+
+			// SHIFT is the number of bits per output character, so the length of the
+			// output is the length of the input multiplied by 8/SHIFT, rounded up.
+			// so: 268435456
+			if ( data.Length >= ( 1 << 28 ) )
+			{
+				// The computation below will fail, so don't do it.
+				throw new ArgumentOutOfRangeException(nameof(data));
+			}
+
+			int outputLength = ( data.Length * 8 + Shift - 1 ) / Shift;
+			StringBuilder result = new StringBuilder(outputLength);
+
+			int buffer = data[0];
+			int next = 1;
+			int bitsLeft = 8;
+			while ( bitsLeft > 0 || next < data.Length )
+			{
+				if ( bitsLeft < Shift )
+				{
+					if ( next < data.Length )
+					{
+						buffer <<= 8;
+						buffer |= ( data[next++] & 0xff );
+						bitsLeft += 8;
+					}
+					else
+					{
+						int pad = Shift - bitsLeft;
+						buffer <<= pad;
+						bitsLeft += pad;
+					}
+				}
+
+				int index = Mask & ( buffer >> ( bitsLeft - Shift ) );
+				bitsLeft -= Shift;
+				result.Append(Digits[index]);
+			}
+
+			return Base32ReturnPadOutput(padOutput, result);
+		}
+
+		private static string Base32ReturnPadOutput(bool padOutput, StringBuilder result)
+		{
+			if ( !padOutput ) return result.ToString();
+			var padding = 8 - ( result.Length % 8 );
+			if ( padding > 0 ) result.Append(new string('=', padding == 8 ? 0 : padding));
+			return result.ToString();
+		}
+
+	}
 }
diff --git a/starsky/starsky.foundation.storage/Services/FileHash.cs b/starsky/starsky.foundation.storage/Services/FileHash.cs
index e738d13b0e..cee6ec99ba 100644
--- a/starsky/starsky.foundation.storage/Services/FileHash.cs
+++ b/starsky/starsky.foundation.storage/Services/FileHash.cs
@@ -192,8 +192,8 @@ public static async Task CalculateHashAsync(Stream stream, bool dispose
 			{
 				int length;
 				while ( ( length = await stream
-					       .ReadAsync(block, cancellationToken)
-					       .ConfigureAwait(false) ) > 0 )
+						   .ReadAsync(block, cancellationToken)
+						   .ConfigureAwait(false) ) > 0 )
 				{
 					md5.TransformBlock(block, 0, length, null, 0);
 				}
diff --git a/starsky/starsky.foundation.storage/Services/StructureService.cs b/starsky/starsky.foundation.storage/Services/StructureService.cs
index 5c96bc4788..60d71aa6cc 100644
--- a/starsky/starsky.foundation.storage/Services/StructureService.cs
+++ b/starsky/starsky.foundation.storage/Services/StructureService.cs
@@ -29,8 +29,8 @@ public StructureService(IStorage storage, string structure)
 		/// include fileName if requested in structure
 		/// fileExtension without dot
 		/// filename without starting slash
-		public string ParseFileName(DateTime dateTime, 
-			string fileNameBase = "", 
+		public string ParseFileName(DateTime dateTime,
+			string fileNameBase = "",
 			string extensionWithoutDot = "")
 		{
 			CheckStructureFormat();
@@ -51,7 +51,7 @@ public string ParseFileName(DateTime dateTime,
 			string extensionWithoutDot = "")
 		{
 			if ( getSubPathRelative == null ) return null;
-			var dateTime = DateTime.Now.AddDays(( double ) getSubPathRelative);
+			var dateTime = DateTime.Now.AddDays(( double )getSubPathRelative);
 			return ParseSubfolders(dateTime, fileNameBase, extensionWithoutDot);
 		}
 
@@ -62,7 +62,7 @@ public string ParseFileName(DateTime dateTime,
 		/// include fileName if requested in structure
 		/// include parentFolder if requested in structure (not recommend)
 		/// sub Path including folders
-		public string ParseSubfolders(DateTime dateTime, string fileNameBase = "", 
+		public string ParseSubfolders(DateTime dateTime, string fileNameBase = "",
 			string extensionWithoutDot = "")
 		{
 			CheckStructureFormat();
@@ -71,7 +71,7 @@ public string ParseSubfolders(DateTime dateTime, string fileNameBase = "",
 			return ApplyStructureRangeToStorage(
 				parsedStructuredList.GetRange(0, parsedStructuredList.Count - 1));
 		}
-		
+
 		/// 
 		/// Get the output of structure applied on the storage
 		/// With the DateTime and fileNameBase applied
@@ -83,7 +83,7 @@ private string ApplyStructureRangeToStorage(List> parsedStr
 			var parentFolderBuilder = new StringBuilder();
 			foreach ( var subStructureItem in parsedStructuredList )
 			{
-				
+
 				var currentChildFolderBuilder = new StringBuilder();
 				currentChildFolderBuilder.Append('/');
 
@@ -94,9 +94,9 @@ private string ApplyStructureRangeToStorage(List> parsedStr
 
 				var parentFolderSubPath = FilenamesHelper.GetParentPath(parentFolderBuilder.ToString());
 				var existParentFolder = _storage.ExistFolder(parentFolderSubPath);
-				
+
 				// default situation without asterisk or child directory is NOT found
-				if ( !currentChildFolderBuilder.ToString().Contains('*') || !existParentFolder)
+				if ( !currentChildFolderBuilder.ToString().Contains('*') || !existParentFolder )
 				{
 					var currentChildFolderRemovedAsterisk = RemoveAsteriskFromString(currentChildFolderBuilder);
 					parentFolderBuilder.Append(currentChildFolderRemovedAsterisk);
@@ -118,13 +118,13 @@ private string ApplyStructureRangeToStorage(List> parsedStr
 		/// the current folder name with asterisk 
 		/// other child folder items (item in loop of childDirectories)
 		/// is match
-		private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, StringBuilder currentChildFolderBuilder, string p )
+		private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, StringBuilder currentChildFolderBuilder, string p)
 		{
 			var matchDirectFolderName = RemoveAsteriskFromString(currentChildFolderBuilder);
 			if ( matchDirectFolderName != "/" && p == parentFolderBuilder + matchDirectFolderName ) return true;
-			
+
 			var matchRegex = new Regex(
-				parentFolderBuilder + currentChildFolderBuilder.ToString().Replace("*", ".+"), 
+				parentFolderBuilder + currentChildFolderBuilder.ToString().Replace("*", ".+"),
 				RegexOptions.None, TimeSpan.FromMilliseconds(100)
 			);
 			return matchRegex.IsMatch(p);
@@ -141,10 +141,10 @@ private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, S
 			// should return a list of: 
 			var childDirectories = _storage.GetDirectories(parentFolderBuilder.ToString()).ToList();
 
-			var matchingFoldersPath= childDirectories.Find(p => 
-				MatchChildFolderSearch(parentFolderBuilder,currentChildFolderBuilder,p) 
+			var matchingFoldersPath = childDirectories.Find(p =>
+				MatchChildFolderSearch(parentFolderBuilder, currentChildFolderBuilder, p)
 				);
-			
+
 			// When a new folder with asterisk is created
 			if ( matchingFoldersPath == null )
 			{
@@ -157,11 +157,11 @@ private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, S
 				parentFolderBuilder.Append(defaultValue);
 				return parentFolderBuilder;
 			}
-			
+
 			// When a regex folder is matched
-			var childFolderName = 
+			var childFolderName =
 				PathHelper.PrefixDbSlash(FilenamesHelper.GetFileName(matchingFoldersPath));
-			
+
 			parentFolderBuilder.Append(childFolderName);
 			return parentFolderBuilder;
 		}
@@ -171,10 +171,10 @@ private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, S
 		/// 
 		/// 
 		/// 
-		private static string RemoveAsteriskFromString(StringBuilder input )
+		private static string RemoveAsteriskFromString(StringBuilder input)
 		{
 			return input.ToString().Replace("*", string.Empty);
-		} 
+		}
 
 		/// 
 		/// Check if the structure is right formatted
@@ -183,15 +183,15 @@ private static string RemoveAsteriskFromString(StringBuilder input )
 		private void CheckStructureFormat()
 		{
 			if ( !string.IsNullOrEmpty(_structure) &&
-			     _structure.StartsWith('/') && _structure.EndsWith(".ext") &&
-			     _structure != "/.ext" )
+				 _structure.StartsWith('/') && _structure.EndsWith(".ext") &&
+				 _structure != "/.ext" )
 			{
 				return;
 			}
-			
+
 			throw new FieldAccessException("Structure is not right formatted, please read the documentation");
 		}
-		
+
 		/// 
 		/// Find 'Custom date and time format strings'
 		/// @see: https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
@@ -217,11 +217,11 @@ private static List> ParseStructure(string structure, DateT
 			{
 				if ( string.IsNullOrWhiteSpace(structureItem) ) continue;
 
-				var matchCollection = new 
-						Regex(DateRegexPattern + "|{filenamebase}|\\*|.ext|.", 
+				var matchCollection = new
+						Regex(DateRegexPattern + "|{filenamebase}|\\*|.ext|.",
 							RegexOptions.None, TimeSpan.FromMilliseconds(100))
 							.Matches(structureItem);
-				
+
 				var matchList = new List();
 				foreach ( Match match in matchCollection )
 				{
@@ -234,7 +234,7 @@ private static List> ParseStructure(string structure, DateT
 					});
 				}
 
-				parsedStructuredList.Add( matchList.OrderBy(p => p.Start).ToList());
+				parsedStructuredList.Add(matchList.OrderBy(p => p.Start).ToList());
 			}
 
 			return parsedStructuredList;
@@ -252,9 +252,9 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da
 			string fileNameBase, string extensionWithoutDot = "")
 		{
 			// allow only full word matches (so .ext is no match)
-			MatchCollection matchCollection = new Regex(DateRegexPattern, 
+			MatchCollection matchCollection = new Regex(DateRegexPattern,
 				RegexOptions.None, TimeSpan.FromMilliseconds(100)).Matches(pattern);
-			
+
 			foreach ( Match match in matchCollection )
 			{
 				// Ignore escaped items
@@ -263,7 +263,7 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da
 					return dateTime.ToString(pattern, CultureInfo.InvariantCulture);
 				}
 			}
-			
+
 			// the other options
 			switch ( pattern )
 			{
@@ -272,7 +272,7 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da
 				case ".ext":
 					return string.IsNullOrEmpty(extensionWithoutDot) ? ".unknown" : $".{extensionWithoutDot}";
 				default:
-					return pattern.Replace("\\",string.Empty);
+					return pattern.Replace("\\", string.Empty);
 			}
 		}
 	}
diff --git a/starsky/starsky.foundation.storage/Storage/RetryStream.cs b/starsky/starsky.foundation.storage/Storage/RetryStream.cs
index f94c24150f..a5d7f28d12 100644
--- a/starsky/starsky.foundation.storage/Storage/RetryStream.cs
+++ b/starsky/starsky.foundation.storage/Storage/RetryStream.cs
@@ -12,7 +12,7 @@ public RetryStream(int waitTime = 2000)
 		{
 			_waitTime = waitTime;
 		}
-		
+
 		public delegate Stream RetryStreamDelegate();
 
 		/// 
@@ -34,7 +34,7 @@ public Stream Retry(RetryStreamDelegate localGet)
 				{
 					if ( retry < maxRetry - 1 )
 					{
-						Console.WriteLine($"catch-ed > {retry}/{maxRetry-1} {error}");
+						Console.WriteLine($"catch-ed > {retry}/{maxRetry - 1} {error}");
 						Thread.Sleep(_waitTime);
 						continue;
 					}
diff --git a/starsky/starsky.foundation.storage/Storage/SelectorStorage.cs b/starsky/starsky.foundation.storage/Storage/SelectorStorage.cs
index 8395f1d5b9..10953d4c05 100644
--- a/starsky/starsky.foundation.storage/Storage/SelectorStorage.cs
+++ b/starsky/starsky.foundation.storage/Storage/SelectorStorage.cs
@@ -15,7 +15,7 @@ public SelectorStorage(IServiceProvider serviceProvider)
 		{
 			_serviceProvider = serviceProvider;
 		}
-		
+
 		public enum StorageServices
 		{
 			SubPath,
@@ -23,9 +23,9 @@ public enum StorageServices
 			/// 
 			/// Use only to import from
 			/// 
-			HostFilesystem 
+			HostFilesystem
 		}
-		
+
 		public IStorage Get(StorageServices storageServices)
 		{
 			var services = _serviceProvider.GetServices();
diff --git a/starsky/starsky.foundation.storage/Storage/StorageHostFullPathFilesystem.cs b/starsky/starsky.foundation.storage/Storage/StorageHostFullPathFilesystem.cs
index 98d5d28acb..1cb9c3db77 100644
--- a/starsky/starsky.foundation.storage/Storage/StorageHostFullPathFilesystem.cs
+++ b/starsky/starsky.foundation.storage/Storage/StorageHostFullPathFilesystem.cs
@@ -40,12 +40,12 @@ public StorageInfo Info(string path)
 				};
 			}
 
-			var lastWrite = type == FolderOrFileModel.FolderOrFileTypeList.File ? 
+			var lastWrite = type == FolderOrFileModel.FolderOrFileTypeList.File ?
 				File.GetLastWriteTime(path) : Directory.GetLastWriteTime(path);
-			
-			var size = type == FolderOrFileModel.FolderOrFileTypeList.File ? 
+
+			var size = type == FolderOrFileModel.FolderOrFileTypeList.File ?
 				new FileInfo(path).Length : -1;
-			
+
 			return new StorageInfo
 			{
 				IsFolderOrFile = type,
@@ -56,7 +56,7 @@ public StorageInfo Info(string path)
 			};
 		}
 
-		internal static bool? TestIfFileSystemIsReadOnly(string folderPath, FolderOrFileModel.FolderOrFileTypeList type )
+		internal static bool? TestIfFileSystemIsReadOnly(string folderPath, FolderOrFileModel.FolderOrFileTypeList type)
 		{
 			if ( type != FolderOrFileModel.FolderOrFileTypeList.Folder )
 			{
@@ -71,14 +71,14 @@ public StorageInfo Info(string path)
 				myFileStream.Dispose();
 				File.Delete(testFilePath);
 			}
-			catch ( IOException  )
+			catch ( IOException )
 			{
 				return true;
 			}
 
 			return false;
 		}
-		
+
 		public void CreateDirectory(string path)
 		{
 			Directory.CreateDirectory(path);
@@ -86,9 +86,9 @@ public void CreateDirectory(string path)
 
 		public bool FolderDelete(string path)
 		{
-			if	( !Directory.Exists(path) ) return false;
-			
-			foreach (var directory in Directory.GetDirectories(path))
+			if ( !Directory.Exists(path) ) return false;
+
+			foreach ( var directory in Directory.GetDirectories(path) )
 			{
 				FolderDelete(directory);
 			}
@@ -122,27 +122,27 @@ public IEnumerable GetAllFilesInDirectory(string path)
 			string[] allFiles;
 			try
 			{
-				 allFiles = Directory.GetFiles(path);
+				allFiles = Directory.GetFiles(path);
 			}
 			catch ( Exception exception )
 			{
-				if ( exception is not (UnauthorizedAccessException
-				    or DirectoryNotFoundException) ) throw;
-				
+				if ( exception is not ( UnauthorizedAccessException
+					or DirectoryNotFoundException ) ) throw;
+
 				_logger?.LogError(exception, "[GetAllFilesInDirectory] " +
-				                             "catch-ed UnauthorizedAccessException/DirectoryNotFoundException");
+											 "catch-ed UnauthorizedAccessException/DirectoryNotFoundException");
 				return Array.Empty();
 			}
 
 			var imageFilesList = new List();
-			foreach (var file in allFiles)
+			foreach ( var file in allFiles )
 			{
 				// Path.GetExtension uses (.ext)
 				// the same check in SingleFile
 				// Recruisive >= same check
 				// ignore Files with ._ names, this is Mac OS specific
 				var isAppleDouble = Path.GetFileName(file).StartsWith("._");
-				if (!isAppleDouble)
+				if ( !isAppleDouble )
 				{
 					imageFilesList.Add(file);
 				}
@@ -171,14 +171,15 @@ public IEnumerable GetDirectories(string path)
 		/// 
 		/// path
 		/// list of paths and last edited times - default ordered by last edited times
-		public IEnumerable> GetDirectoryRecursive(string path)
+		public IEnumerable> GetDirectoryRecursive(string path)
 		{
 			// Tuple > FilePath,Directory.GetLastWriteTime
-			var folders = new Queue>();
+			var folders = new Queue>();
 			folders.Enqueue(new KeyValuePair(path, Directory.GetLastWriteTime(path)));
-			var folderList = new List>();
-			while (folders.Count != 0) {
-				var (currentFolder,_) = folders.Dequeue();
+			var folderList = new List>();
+			while ( folders.Count != 0 )
+			{
+				var (currentFolder, _) = folders.Dequeue();
 				try
 				{
 					var foldersInCurrent = Directory.GetDirectories(currentFolder,
@@ -193,12 +194,12 @@ public IEnumerable> GetDirectoryRecursive(string p
 						}
 					}
 				}
-				catch(Exception exception) 
+				catch ( Exception exception )
 				{
-					if ( exception is not (UnauthorizedAccessException
-					    or DirectoryNotFoundException) ) throw;
+					if ( exception is not ( UnauthorizedAccessException
+						or DirectoryNotFoundException ) ) throw;
 					_logger?.LogError("[StorageHostFullPathFilesystem] Catch-ed " +
-					                  "DirectoryNotFoundException/UnauthorizedAccessException => " + exception.Message);
+									  "DirectoryNotFoundException/UnauthorizedAccessException => " + exception.Message);
 				}
 			}
 
@@ -213,18 +214,18 @@ public IEnumerable> GetDirectoryRecursive(string p
 		/// Stream with data (non-disposed)
 		public Stream ReadStream(string path, int maxRead = -1)
 		{
-			if ( ! ExistFile(path) ) throw new FileNotFoundException(path);
+			if ( !ExistFile(path) ) throw new FileNotFoundException(path);
 
 			try
 			{
-				var fileStream = new FileStream(path, FileMode.Open, 
-					FileAccess.Read, FileShare.Read, 4096,true );
+				var fileStream = new FileStream(path, FileMode.Open,
+					FileAccess.Read, FileShare.Read, 4096, true);
 
 				if ( maxRead < 1 )
 				{
 					return fileStream;
 				}
-				
+
 				// Only for when selecting the first part of the file
 				var buffer = new byte[maxRead];
 				// ReSharper disable once MustUseReturnValue
@@ -232,12 +233,12 @@ public Stream ReadStream(string path, int maxRead = -1)
 				fileStream.Close(); // see before max read for default setting
 				return new MemoryStream(buffer);
 			}
-			catch ( FileNotFoundException e)
+			catch ( FileNotFoundException e )
 			{
 				_logger?.LogError(e, "[ReadStream] catch-ed FileNotFoundException");
 				return Stream.Null;
 			}
-			
+
 		}
 
 		/// 
@@ -264,13 +265,13 @@ public bool ExistFolder(string path)
 		/// is file, folder or deleted
 		public FolderOrFileModel.FolderOrFileTypeList IsFolderOrFile(string path)
 		{
-			if (!Directory.Exists(path) && File.Exists(path))
+			if ( !Directory.Exists(path) && File.Exists(path) )
 			{
 				// file
 				return FolderOrFileModel.FolderOrFileTypeList.File;
 			}
 
-			if (!File.Exists(path) && Directory.Exists(path))
+			if ( !File.Exists(path) && Directory.Exists(path) )
 			{
 				// Directory
 				return FolderOrFileModel.FolderOrFileTypeList.Folder;
@@ -278,7 +279,7 @@ public FolderOrFileModel.FolderOrFileTypeList IsFolderOrFile(string path)
 
 			return FolderOrFileModel.FolderOrFileTypeList.Deleted;
 		}
-		
+
 		/// 
 		/// Move folder on disk
 		/// 
@@ -286,9 +287,9 @@ public FolderOrFileModel.FolderOrFileTypeList IsFolderOrFile(string path)
 		/// toFileFullPath
 		public void FolderMove(string fromPath, string toPath)
 		{
-			Directory.Move(fromPath,toPath);
+			Directory.Move(fromPath, toPath);
 		}
-	
+
 		/// 
 		/// Move file on real filesystem
 		/// 
@@ -296,10 +297,10 @@ public void FolderMove(string fromPath, string toPath)
 		/// toFileFullPath
 		public void FileMove(string fromPath, string toPath)
 		{
-			if ( fromPath == toPath) return;
-			File.Move(fromPath,toPath);
+			if ( fromPath == toPath ) return;
+			File.Move(fromPath, toPath);
 		}
-		
+
 		/// 
 		/// Copy file on real filesystem
 		/// 
@@ -307,7 +308,7 @@ public void FileMove(string fromPath, string toPath)
 		/// toFileFullPath
 		public void FileCopy(string fromPath, string toPath)
 		{
-			File.Copy(fromPath,toPath);
+			File.Copy(fromPath, toPath);
 		}
 
 		public bool FileDelete(string path)
@@ -316,15 +317,15 @@ public bool FileDelete(string path)
 			{
 				return false;
 			}
-			
+
 			bool LocalRun()
 			{
 				File.Delete(path);
 				return true;
 			}
-			return RetryHelper.Do(LocalRun, TimeSpan.FromSeconds(2),5);
+			return RetryHelper.Do(LocalRun, TimeSpan.FromSeconds(2), 5);
 		}
-		
+
 		public bool WriteStream(Stream stream, string path)
 		{
 			if ( !stream.CanRead ) return false;
@@ -332,12 +333,12 @@ public bool WriteStream(Stream stream, string path)
 			bool LocalRun()
 			{
 				stream.Seek(0, SeekOrigin.Begin);
-			
-				using (var fileStream = new FileStream(path, 
-					FileMode.Create, 
-					FileAccess.Write,FileShare.ReadWrite,
-					4096, 
-					FileOptions.Asynchronous))
+
+				using ( var fileStream = new FileStream(path,
+					FileMode.Create,
+					FileAccess.Write, FileShare.ReadWrite,
+					4096,
+					FileOptions.Asynchronous) )
 				{
 					stream.CopyTo(fileStream);
 					// fileStream is disposed due using
@@ -346,7 +347,7 @@ bool LocalRun()
 				stream.Dispose();
 				return true;
 			}
-						
+
 			return RetryHelper.Do(LocalRun, TimeSpan.FromSeconds(1));
 		}
 
@@ -355,12 +356,12 @@ public bool WriteStreamOpenOrCreate(Stream stream, string path)
 			if ( !stream.CanRead ) return false;
 
 			stream.Seek(0, SeekOrigin.Begin);
-			
-			using (var fileStream = new FileStream(path, 
+
+			using ( var fileStream = new FileStream(path,
 				FileMode.OpenOrCreate, // <= that's the difference
-				FileAccess.Write,FileShare.ReadWrite,
-				4096, 
-				FileOptions.Asynchronous))
+				FileAccess.Write, FileShare.ReadWrite,
+				4096,
+				FileOptions.Asynchronous) )
 			{
 				stream.CopyTo(fileStream);
 			}
@@ -383,20 +384,20 @@ async Task LocalRun()
 				{
 					stream.Seek(0, SeekOrigin.Begin);
 				}
-				catch (NotSupportedException)
+				catch ( NotSupportedException )
 				{
 					// HttpConnection.ContentLengthReadStream does not support this
 				}
-				
-				using (var fileStream = new FileStream(path, FileMode.Create, 
-					FileAccess.Write, FileShare.Read, 4096, 
-					FileOptions.Asynchronous | FileOptions.SequentialScan))
+
+				using ( var fileStream = new FileStream(path, FileMode.Create,
+					FileAccess.Write, FileShare.Read, 4096,
+					FileOptions.Asynchronous | FileOptions.SequentialScan) )
 				{
 					await stream.CopyToAsync(fileStream);
 				}
-				
+
 				await stream.DisposeAsync();
-				
+
 				return true;
 			}
 
@@ -409,17 +410,17 @@ async Task LocalRun()
 		/// The full file path.
 		/// 
 		public IEnumerable GetAllFilesInDirectoryRecursive(string path)
-        {
-            var findList = new List();
+		{
+			var findList = new List();
 
-            /* I begin a recursion, following the order:
+			/* I begin a recursion, following the order:
              * - Insert all the files in the current directory with the recursion
              * - Insert all subdirectories in the list and re-begin the recursion from there until the end
              */
-            RecurseFind( path, findList );
-            
-            return findList.OrderBy(x => x).ToList();
-        }
+			RecurseFind(path, findList);
+
+			return findList.OrderBy(x => x).ToList();
+		}
 
 		internal Tuple GetFilesAndDirectories(string path)
 		{
@@ -430,7 +431,7 @@ internal Tuple GetFilesAndDirectories(string path)
 				return new Tuple(filesArray,
 					directoriesArray);
 			}
-			catch ( Exception exception)
+			catch ( Exception exception )
 			{
 				_logger?.LogInformation($"[StorageHostFullPathFilesystem] catch-ed ex: {exception.Message} -  {path}");
 				return new Tuple(
@@ -445,29 +446,29 @@ internal Tuple GetFilesAndDirectories(string path)
 		/// 
 		/// The path.
 		/// The list of strings.
-		private void RecurseFind( string path, List list )
+		private void RecurseFind(string path, List list)
 		{
 			var (filesArray, directoriesArray) = GetFilesAndDirectories(path);
 			if ( filesArray.Length <= 0 && directoriesArray.Length <= 0 )
 			{
 				return;
 			}
-            //I begin with the files, and store all of them in the list
-            list.AddRange(filesArray);
-            // I then add the directory and recurse that directory,
-            // the process will repeat until there are no more files and directories to recurse
-            foreach(var s in directoriesArray)
-            {
-	            list.Add(s);
-	            RecurseFind(s, list);
-            }
-        }
+			//I begin with the files, and store all of them in the list
+			list.AddRange(filesArray);
+			// I then add the directory and recurse that directory,
+			// the process will repeat until there are no more files and directories to recurse
+			foreach ( var s in directoriesArray )
+			{
+				list.Add(s);
+				RecurseFind(s, list);
+			}
+		}
 
 		public DateTime SetLastWriteTime(string path, DateTime? dateTime = null)
 		{
 			if ( dateTime?.Year == null || dateTime.Value.Year <= 2000 )
 			{
-				dateTime =  DateTime.Now;
+				dateTime = DateTime.Now;
 			}
 			var type = IsFolderOrFile(path);
 
diff --git a/starsky/starsky.foundation.storage/Storage/StorageSubPathFilesystem.cs b/starsky/starsky.foundation.storage/Storage/StorageSubPathFilesystem.cs
index 7f15db11ba..fac622aebd 100644
--- a/starsky/starsky.foundation.storage/Storage/StorageSubPathFilesystem.cs
+++ b/starsky/starsky.foundation.storage/Storage/StorageSubPathFilesystem.cs
@@ -31,14 +31,14 @@ public StorageSubPathFilesystem(AppSettings appSettings, IWebLogger logger)
 		public StorageInfo Info(string path)
 		{
 			var subPath = _appSettings.DatabasePathToFilePath(path);
-			
+
 			return new StorageHostFullPathFilesystem(_logger).Info(subPath);
 		}
 
 		public DateTime SetLastWriteTime(string path, DateTime? dateTime = null)
 		{
 			var subPath = _appSettings.DatabasePathToFilePath(path);
-			
+
 			return new StorageHostFullPathFilesystem(_logger).SetLastWriteTime(subPath, dateTime);
 		}
 
@@ -85,7 +85,8 @@ public void FolderMove(string fromPath, string toPath)
 		{
 			var inputFileFullPath = _appSettings.DatabasePathToFilePath(fromPath);
 			var toFileFullPath = _appSettings.DatabasePathToFilePath(toPath);
-			new StorageHostFullPathFilesystem(_logger).FolderMove(inputFileFullPath,toFileFullPath);
+			new StorageHostFullPathFilesystem(_logger).FolderMove(inputFileFullPath,
+				toFileFullPath);
 		}
 
 		/// 
@@ -97,9 +98,9 @@ public void FileMove(string fromPath, string toPath)
 		{
 			var inputFileFullPath = _appSettings.DatabasePathToFilePath(fromPath);
 			var toFileFullPath = _appSettings.DatabasePathToFilePath(toPath);
-			new StorageHostFullPathFilesystem(_logger).FileMove(inputFileFullPath,toFileFullPath);
+			new StorageHostFullPathFilesystem(_logger).FileMove(inputFileFullPath, toFileFullPath);
 		}
-		
+
 		/// 
 		/// Copy a single file
 		/// 
@@ -109,9 +110,9 @@ public void FileCopy(string fromPath, string toPath)
 		{
 			var inputFileFullPath = _appSettings.DatabasePathToFilePath(fromPath);
 			var toFileFullPath = _appSettings.DatabasePathToFilePath(toPath);
-			new StorageHostFullPathFilesystem(_logger).FileCopy(inputFileFullPath,toFileFullPath);
+			new StorageHostFullPathFilesystem(_logger).FileCopy(inputFileFullPath, toFileFullPath);
 		}
-		
+
 		/// 
 		/// Delete a file
 		/// 
@@ -122,7 +123,7 @@ public bool FileDelete(string path)
 			var inputFileFullPath = _appSettings.DatabasePathToFilePath(path);
 			return new StorageHostFullPathFilesystem(_logger).FileDelete(inputFileFullPath);
 		}
-		
+
 		/// 
 		/// Create an Directory 
 		/// 
@@ -132,7 +133,7 @@ public void CreateDirectory(string path)
 			var inputFileFullPath = _appSettings.DatabasePathToFilePath(path);
 			Directory.CreateDirectory(inputFileFullPath);
 		}
-		
+
 		/// 
 		/// Delete folder and child items of that folder
 		/// 
@@ -143,7 +144,7 @@ public bool FolderDelete(string path)
 			var inputFileFullPath = _appSettings.DatabasePathToFilePath(path);
 			return new StorageHostFullPathFilesystem(_logger).FolderDelete(inputFileFullPath);
 		}
-		
+
 		/// 
 		/// Returns a list of Files in a directory (non-Recursive)
 		/// to filter use:
@@ -167,12 +168,12 @@ public IEnumerable GetAllFilesInDirectory(string path)
 			// to filter use:
 			// ..etAllFilesInDirectory(subPath)
 			//	.Where(ExtensionRolesHelper.IsExtensionExifToolSupported)
-			
+
 			// convert back to subPath style
 			return _appSettings.RenameListItemsToDbStyle(imageFilesList.ToList());
 		}
-		
-		
+
+
 		/// 
 		/// Returns a list of Files in a directory (Recursive)
 		/// to filter use:
@@ -185,7 +186,7 @@ public IEnumerable GetAllFilesInDirectoryRecursive(string path)
 		{
 			var fullFilePath = _appSettings.DatabasePathToFilePath(path);
 			var storage = new StorageHostFullPathFilesystem(_logger);
-			
+
 			if ( !storage.ExistFolder(fullFilePath) )
 			{
 				return Enumerable.Empty();
@@ -217,9 +218,9 @@ public IEnumerable GetDirectories(string path)
 			{
 				return Enumerable.Empty();
 			}
-			
+
 			var folders = storage.GetDirectories(fullFilePath);
-			
+
 			// Used For subfolders
 			// convert back to subPath style
 			return _appSettings.RenameListItemsToDbStyle(folders.ToList());
@@ -229,18 +230,17 @@ public IEnumerable GetDirectories(string path)
 		/// Returns a list of directories // Get list of child folders
 		/// 
 		/// subPath in dir
-		/// order by alphabet or last edited
 		/// list of paths
-		public IEnumerable> GetDirectoryRecursive(string path)
+		public IEnumerable> GetDirectoryRecursive(string path)
 		{
 			var storage = new StorageHostFullPathFilesystem(_logger);
 
 			var fullFilePath = _appSettings.DatabasePathToFilePath(path);
 			if ( !storage.ExistFolder(fullFilePath) )
 			{
-				return Enumerable.Empty>();
+				return Enumerable.Empty>();
 			}
-			
+
 			var folders = storage.GetDirectoryRecursive(fullFilePath);
 
 			// Used For subfolders
@@ -256,8 +256,11 @@ public IEnumerable> GetDirectoryRecursive(string p
 		/// FileStream or Stream.Null when file dont exist
 		public Stream ReadStream(string path, int maxRead = -1)
 		{
-			if ( ! ExistFile(path) ) return Stream.Null;
-			
+			if ( !ExistFile(path) )
+			{
+				return Stream.Null;
+			}
+
 			if ( _appSettings.IsVerbose() ) Console.WriteLine(path);
 
 			Stream LocalGet()
@@ -269,7 +272,7 @@ Stream LocalGet()
 					// read all
 					return fileStream;
 				}
-				
+
 				// read only the first number of bytes
 				byte[] buffer = new byte[maxRead];
 				fileStream.Read(buffer, 0, maxRead);
@@ -282,8 +285,6 @@ Stream LocalGet()
 		}
 
 
-		
-		
 		/// 
 		/// Write fileStream to disk
 		/// 
@@ -307,7 +308,8 @@ public bool WriteStreamOpenOrCreate(Stream stream, string path)
 		public Task WriteStreamAsync(Stream stream, string path)
 		{
 			var fullFilePath = _appSettings.DatabasePathToFilePath(path);
-			return new StorageHostFullPathFilesystem(_logger).WriteStreamAsync(stream, fullFilePath);
+			return new StorageHostFullPathFilesystem(_logger)
+				.WriteStreamAsync(stream, fullFilePath);
 		}
 	}
 }
diff --git a/starsky/starsky.foundation.storage/Storage/StorageThumbnailFilesystem.cs b/starsky/starsky.foundation.storage/Storage/StorageThumbnailFilesystem.cs
index 949aad2d37..2174c2d2d6 100644
--- a/starsky/starsky.foundation.storage/Storage/StorageThumbnailFilesystem.cs
+++ b/starsky/starsky.foundation.storage/Storage/StorageThumbnailFilesystem.cs
@@ -34,7 +34,7 @@ internal string CombinePath(string fileHash)
 			}
 			return Path.Combine(_appSettings.ThumbnailTempFolder, fileHash + ".jpg");
 		}
-		
+
 		/// 
 		/// 
 		/// 
@@ -71,11 +71,11 @@ public void FileMove(string fromPath, string toPath)
 			var existOldFile = hostFilesystem.ExistFile(oldThumbPath);
 			var existNewFile = hostFilesystem.ExistFile(newThumbPath);
 
-			if (!existOldFile || existNewFile)
+			if ( !existOldFile || existNewFile )
 			{
 				return;
 			}
-			hostFilesystem.FileMove(oldThumbPath,newThumbPath);
+			hostFilesystem.FileMove(oldThumbPath, newThumbPath);
 		}
 
 		public void FileCopy(string fromPath, string toPath)
@@ -88,11 +88,11 @@ public void FileCopy(string fromPath, string toPath)
 			var existOldFile = hostFilesystem.ExistFile(oldThumbPath);
 			var existNewFile = hostFilesystem.ExistFile(newThumbPath);
 
-			if (!existOldFile || existNewFile)
+			if ( !existOldFile || existNewFile )
 			{
 				return;
 			}
-			hostFilesystem.FileCopy(oldThumbPath,newThumbPath);
+			hostFilesystem.FileCopy(oldThumbPath, newThumbPath);
 		}
 
 		/// 
@@ -102,7 +102,7 @@ public void FileCopy(string fromPath, string toPath)
 		/// true when success
 		public bool FileDelete(string path)
 		{
-			if (string.IsNullOrEmpty(path) || !ExistFile(path) ) return false;
+			if ( string.IsNullOrEmpty(path) || !ExistFile(path) ) return false;
 
 			var thumbPath = CombinePath(path);
 			var hostFilesystem = new StorageHostFullPathFilesystem(_logger);
@@ -113,7 +113,7 @@ public void CreateDirectory(string path)
 		{
 			throw new System.NotImplementedException();
 		}
-		
+
 		public bool FolderDelete(string path)
 		{
 			throw new System.NotImplementedException();
@@ -136,7 +136,7 @@ public IEnumerable GetDirectories(string path)
 			throw new System.NotImplementedException();
 		}
 
-		public IEnumerable> GetDirectoryRecursive(string path)
+		public IEnumerable> GetDirectoryRecursive(string path)
 		{
 			throw new NotImplementedException();
 		}
@@ -149,7 +149,7 @@ public IEnumerable> GetDirectoryRecursive(string p
 		/// Stream with data (non-disposed)
 		public Stream ReadStream(string path, int maxRead = -1)
 		{
-			if ( !ExistFile(path) ) throw new FileNotFoundException(path); 
+			if ( !ExistFile(path) ) throw new FileNotFoundException(path);
 			return new StorageHostFullPathFilesystem(_logger).ReadStream(CombinePath(path), maxRead);
 		}
 
diff --git a/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs b/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs
index ddc476b56b..01db129444 100644
--- a/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs
+++ b/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs
@@ -15,16 +15,16 @@ public ThumbnailFileMoveAllSizes(IStorage thumbnailStorage)
 		public void FileMove(string oldFileHash, string newHashCode)
 		{
 			_thumbnailStorage.FileMove(
-				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Large), 
+				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Large),
 				ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.Large));
 			_thumbnailStorage.FileMove(
-				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Small), 
+				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Small),
 				ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.Small));
 			_thumbnailStorage.FileMove(
-				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.ExtraLarge), 
+				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.ExtraLarge),
 				ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.ExtraLarge));
 			_thumbnailStorage.FileMove(
-				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.TinyMeta), 
+				ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.TinyMeta),
 				ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.TinyMeta));
 		}
 	}
diff --git a/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs b/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs
index f9a80a355a..a9aecb6b41 100644
--- a/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs
+++ b/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs
@@ -20,13 +20,13 @@ public static class ThumbnailNameHelper
 			ThumbnailSize.Small,
 			ThumbnailSize.Large
 		};
-		
+
 		public static readonly ThumbnailSize[] SecondGeneratedThumbnailSizes = new ThumbnailSize[]
 		{
 			ThumbnailSize.Small,
 			ThumbnailSize.Large //  <- will be false when skipExtraLarge = true, its already created 
 		};
-		
+
 		public static readonly ThumbnailSize[] AllThumbnailSizes = new ThumbnailSize[]
 		{
 			ThumbnailSize.TinyMeta,
@@ -34,10 +34,10 @@ public static class ThumbnailNameHelper
 			ThumbnailSize.Small,
 			ThumbnailSize.Large
 		};
-		
+
 		public static int GetSize(ThumbnailSize size)
 		{
-			switch (size)
+			switch ( size )
 			{
 				case ThumbnailSize.TinyMeta:
 					return 150;
@@ -51,42 +51,42 @@ public static int GetSize(ThumbnailSize size)
 					throw new ArgumentOutOfRangeException(nameof(size), size, null);
 			}
 		}
-		
+
 		public static ThumbnailSize GetSize(int size)
 		{
-			switch (size)
+			switch ( size )
 			{
 				case 150:
 					return ThumbnailSize.TinyMeta;
 				case 300:
 					return ThumbnailSize.Small;
 				case 1000:
-					return ThumbnailSize.Large ;
+					return ThumbnailSize.Large;
 				case 2000:
 					return ThumbnailSize.ExtraLarge;
 				default:
 					return ThumbnailSize.Unknown;
 			}
 		}
-		
+
 		public static ThumbnailSize GetSize(string fileName)
 		{
 			var fileNameWithoutExtension =
 				fileName.Replace(".jpg", string.Empty);
 
-			var afterAtString = Regex.Match(fileNameWithoutExtension, "@\\d+", 
+			var afterAtString = Regex.Match(fileNameWithoutExtension, "@\\d+",
 					RegexOptions.None, TimeSpan.FromMilliseconds(100))
 				.Value.Replace("@", string.Empty);
-			
+
 			if ( fileNameWithoutExtension.Replace($"@{afterAtString}", string.Empty).Length != 26 )
 			{
 				return ThumbnailSize.Unknown;
 			}
-			
-			if ( string.IsNullOrEmpty(afterAtString))
+
+			if ( string.IsNullOrEmpty(afterAtString) )
 				return ThumbnailSize.Large;
-			
-			int.TryParse(afterAtString, NumberStyles.Number, 
+
+			int.TryParse(afterAtString, NumberStyles.Number,
 				CultureInfo.InvariantCulture, out var afterAt);
 			return GetSize(afterAt);
 		}
@@ -104,7 +104,7 @@ public static string Combine(string fileHash, ThumbnailSize size, bool appendExt
 
 		private static string GetAppend(ThumbnailSize size)
 		{
-			switch (size)
+			switch ( size )
 			{
 				case ThumbnailSize.TinyMeta:
 					return "@meta";
@@ -121,8 +121,8 @@ private static string GetAppend(ThumbnailSize size)
 
 		public static string RemoveSuffix(string? thumbnailOutputHash)
 		{
-			return thumbnailOutputHash == null ? string.Empty : 
-				Regex.Replace(thumbnailOutputHash, "@\\d+", 
+			return thumbnailOutputHash == null ? string.Empty :
+				Regex.Replace(thumbnailOutputHash, "@\\d+",
 					string.Empty, RegexOptions.None, TimeSpan.FromMilliseconds(100));
 		}
 	}
diff --git a/starsky/starsky.foundation.sync/Helpers/AddParentList.cs b/starsky/starsky.foundation.sync/Helpers/AddParentList.cs
index 56d3c3144a..2cbe7fa4fd 100644
--- a/starsky/starsky.foundation.sync/Helpers/AddParentList.cs
+++ b/starsky/starsky.foundation.sync/Helpers/AddParentList.cs
@@ -18,7 +18,7 @@ public AddParentList(IStorage subPathStorage, IQuery query)
 		_subPathStorage = subPathStorage;
 		_query = query;
 	}
-	
+
 	public async Task> AddParentItems(List updatedDbItems)
 	{
 		// give parent folders back
@@ -28,9 +28,9 @@ p.Status is FileIndexItem.ExifStatus.Ok
 				or FileIndexItem.ExifStatus.OkAndSame
 				or FileIndexItem.ExifStatus.Default).Select(p => p.ParentDirectory)
 			.Distinct().Where(p => p != null).Cast();
-		
+
 		foreach ( var subPath in okUpdatedDbItems
-			         .Where(p => _subPathStorage.ExistFolder(p)))
+					 .Where(p => _subPathStorage.ExistFolder(p)) )
 		{
 			var path = PathHelper.RemoveLatestSlash(subPath) + "/test.jpg";
 			if ( subPath == "/" ) path = "/";
diff --git a/starsky/starsky.foundation.sync/Helpers/CheckForStatusNotOkHelper.cs b/starsky/starsky.foundation.sync/Helpers/CheckForStatusNotOkHelper.cs
index b99424735e..2310e46c1b 100644
--- a/starsky/starsky.foundation.sync/Helpers/CheckForStatusNotOkHelper.cs
+++ b/starsky/starsky.foundation.sync/Helpers/CheckForStatusNotOkHelper.cs
@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using System.Linq;
 using starsky.foundation.database.Models;
 using starsky.foundation.platform.Helpers;
 using starsky.foundation.storage.Interfaces;
@@ -14,7 +13,7 @@ public CheckForStatusNotOkHelper(IStorage subPathStorage)
 	{
 		_subPathStorage = subPathStorage;
 	}
-	
+
 	internal IEnumerable CheckForStatusNotOk(IEnumerable subPaths)
 	{
 		var result = new List();
@@ -22,6 +21,7 @@ internal IEnumerable CheckForStatusNotOk(IEnumerable subP
 		{
 			result.AddRange(CheckForStatusNotOk(subPath));
 		}
+
 		return result;
 	}
 
@@ -32,40 +32,41 @@ internal IEnumerable CheckForStatusNotOk(IEnumerable subP
 	/// item with status
 	internal List CheckForStatusNotOk(string subPath)
 	{
-		var statusItem = new FileIndexItem(subPath){Status = FileIndexItem.ExifStatus.Ok};
+		var statusItem = new FileIndexItem(subPath) { Status = FileIndexItem.ExifStatus.Ok };
 
 		// File extension is not supported
 		if ( !ExtensionRolesHelper.IsExtensionSyncSupported(subPath) )
 		{
 			statusItem.Status = FileIndexItem.ExifStatus.OperationNotSupported;
-			return new List{statusItem};
+			return new List { statusItem };
 		}
 
 		if ( !_subPathStorage.ExistFile(subPath) )
 		{
 			statusItem.Status = FileIndexItem.ExifStatus.NotFoundSourceMissing;
-			return new List{statusItem};
+			return new List { statusItem };
 		}
-			
+
 		// File check if jpg #not corrupt
-		var imageFormat = ExtensionRolesHelper.GetImageFormat(_subPathStorage.ReadStream(subPath,160));
-			
+		var imageFormat =
+			ExtensionRolesHelper.GetImageFormat(_subPathStorage.ReadStream(subPath, 160));
+
 		// ReSharper disable once InvertIf
 		if ( !ExtensionRolesHelper.ExtensionSyncSupportedList.Contains(imageFormat.ToString()) )
 		{
 			statusItem.Status = FileIndexItem.ExifStatus.OperationNotSupported;
-			return new List{statusItem};
+			return new List { statusItem };
 		}
 
 		var xmpFilePath = ExtensionRolesHelper.ReplaceExtensionWithXmp(subPath);
 		if ( string.IsNullOrEmpty(xmpFilePath) ||
-		     !_subPathStorage.ExistFile(xmpFilePath) || 
-		     statusItem.FilePath == xmpFilePath )
+			 !_subPathStorage.ExistFile(xmpFilePath) ||
+			 statusItem.FilePath == xmpFilePath )
 		{
 			return new List { statusItem };
 		}
-		
-		var xmpStatusItem = new FileIndexItem(xmpFilePath){Status = FileIndexItem.ExifStatus.Ok};
-		return new List{statusItem,xmpStatusItem};
+
+		var xmpStatusItem = new FileIndexItem(xmpFilePath) { Status = FileIndexItem.ExifStatus.Ok };
+		return new List { statusItem, xmpStatusItem };
 	}
 }
diff --git a/starsky/starsky.foundation.sync/Helpers/DeleteStatusHelper.cs b/starsky/starsky.foundation.sync/Helpers/DeleteStatusHelper.cs
index 414ab7c5e7..06ca71f2bb 100644
--- a/starsky/starsky.foundation.sync/Helpers/DeleteStatusHelper.cs
+++ b/starsky/starsky.foundation.sync/Helpers/DeleteStatusHelper.cs
@@ -6,21 +6,21 @@ namespace starsky.foundation.sync.Helpers;
 
 public static class DeleteStatusHelper
 {
-	internal static FileIndexItem? AddDeleteStatus(FileIndexItem dbItem, 
+	internal static FileIndexItem? AddDeleteStatus(FileIndexItem dbItem,
 		FileIndexItem.ExifStatus exifStatus = FileIndexItem.ExifStatus.Deleted)
 	{
 		if ( dbItem?.Tags == null )
 		{
 			return null;
 		}
-		
+
 		if ( dbItem.Tags.Contains(TrashKeyword.TrashKeywordString) )
 		{
 			dbItem.Status = exifStatus;
 		}
 		return dbItem;
 	}
-	
+
 	internal static List AddDeleteStatus(IEnumerable dbItems,
 		FileIndexItem.ExifStatus exifStatus =
 			FileIndexItem.ExifStatus.Deleted)
diff --git a/starsky/starsky.foundation.sync/Helpers/FilterCommonTempFiles.cs b/starsky/starsky.foundation.sync/Helpers/FilterCommonTempFiles.cs
index 1cd4cfe874..acf2ae3003 100644
--- a/starsky/starsky.foundation.sync/Helpers/FilterCommonTempFiles.cs
+++ b/starsky/starsky.foundation.sync/Helpers/FilterCommonTempFiles.cs
@@ -5,11 +5,11 @@ namespace starsky.foundation.sync.Helpers
 {
 	public static class FilterCommonTempFiles
 	{
-		
+
 		public static bool Filter(string subPath)
 		{
 			return subPath.ToLowerInvariant().EndsWith(".ds_store") || subPath.ToLowerInvariant().EndsWith(".tmp") ||
-			       subPath.ToLowerInvariant().EndsWith("desktop.ini");
+				   subPath.ToLowerInvariant().EndsWith("desktop.ini");
 		}
 
 		public static List DefaultOperationNotSupported(string subPath)
diff --git a/starsky/starsky.foundation.sync/Helpers/NewItem.cs b/starsky/starsky.foundation.sync/Helpers/NewItem.cs
index a2e915db5d..ebde478362 100644
--- a/starsky/starsky.foundation.sync/Helpers/NewItem.cs
+++ b/starsky/starsky.foundation.sync/Helpers/NewItem.cs
@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using System.Linq;
 using System.Threading.Tasks;
 using starsky.foundation.database.Helpers;
 using starsky.foundation.database.Models;
@@ -23,7 +22,7 @@ public NewItem(IStorage subPathStorage, IReadMeta readMeta)
 			_subPathStorage = subPathStorage;
 			_readMeta = readMeta;
 		}
-		
+
 		public async Task> NewFileItemAsync(List inputItems)
 		{
 			var result = new List();
@@ -31,9 +30,10 @@ public async Task> NewFileItemAsync(List inpu
 			{
 				result.Add(await NewFileItemAsync(inputItem));
 			}
+
 			return result;
 		}
-		
+
 		/// 
 		/// Returns only an object (no db update)
 		/// 
@@ -53,23 +53,24 @@ public async Task NewFileItemAsync(FileIndexItem inputItem)
 		/// parent directory name
 		/// name without path
 		/// 
-		private async Task NewFileItemAsync(string filePath, string fileHash, string parentDirectory, string fileName)
+		private async Task NewFileItemAsync(string filePath, string fileHash,
+			string parentDirectory, string fileName)
 		{
-			var updatedDatabaseItem =  await _readMeta.ReadExifAndXmpFromFileAsync(filePath);
+			var updatedDatabaseItem = await _readMeta.ReadExifAndXmpFromFileAsync(filePath);
 			updatedDatabaseItem!.ImageFormat = ExtensionRolesHelper
-				.GetImageFormat(_subPathStorage.ReadStream(filePath,50));
+				.GetImageFormat(_subPathStorage.ReadStream(filePath, 50));
 
 			// future: read json sidecar
 			await SetFileHashStatus(filePath, fileHash, updatedDatabaseItem);
 			updatedDatabaseItem.SetAddToDatabase();
 			var info = _subPathStorage.Info(filePath);
-			
+
 			updatedDatabaseItem.LastEdited = info.LastWriteTime;
 			updatedDatabaseItem.IsDirectory = false;
 			updatedDatabaseItem.Size = info.Size;
 			updatedDatabaseItem.ParentDirectory = parentDirectory;
 			updatedDatabaseItem.FileName = fileName;
-			
+
 			return updatedDatabaseItem;
 		}
 
@@ -90,7 +91,7 @@ public async Task PrepareUpdateFileItemAsync(FileIndexItem dbItem
 			{
 				dbItem.Status = FileIndexItem.ExifStatus.OkAndSame;
 			}
-		
+
 			return dbItem;
 		}
 
@@ -101,12 +102,14 @@ public async Task PrepareUpdateFileItemAsync(FileIndexItem dbItem
 		/// 
 		/// new created object
 		/// 
-		private async Task SetFileHashStatus(string filePath, string fileHash,  FileIndexItem updatedDatabaseItem)
+		private async Task SetFileHashStatus(string filePath, string fileHash,
+			FileIndexItem updatedDatabaseItem)
 		{
 			updatedDatabaseItem.Status = FileIndexItem.ExifStatus.Ok;
 			if ( string.IsNullOrEmpty(fileHash) )
 			{
-				var (localHash, success) = await new FileHash(_subPathStorage).GetHashCodeAsync(filePath);
+				var (localHash, success) =
+					await new FileHash(_subPathStorage).GetHashCodeAsync(filePath);
 				updatedDatabaseItem.FileHash = localHash;
 				updatedDatabaseItem.Status = success
 					? FileIndexItem.ExifStatus.Ok
diff --git a/starsky/starsky.foundation.sync/Helpers/NewUpdateItemWrapper.cs b/starsky/starsky.foundation.sync/Helpers/NewUpdateItemWrapper.cs
index aa1a202087..f9c9632f3f 100644
--- a/starsky/starsky.foundation.sync/Helpers/NewUpdateItemWrapper.cs
+++ b/starsky/starsky.foundation.sync/Helpers/NewUpdateItemWrapper.cs
@@ -27,7 +27,7 @@ public NewUpdateItemWrapper(IQuery query, IStorage subPathStorage, AppSettings a
 		_logger = logger;
 		_subPathStorage = subPathStorage;
 	}
-	
+
 	/// 
 	/// Create an new item in the database
 	/// 
@@ -41,14 +41,14 @@ internal async Task NewItem(FileIndexItem statusItem, string subP
 
 		// When not OK do not Add (fileHash issues)
 		if ( dbItem.Status != FileIndexItem.ExifStatus.Ok ) return dbItem;
-				
+
 		await _query.AddItemAsync(dbItem);
 		await _query.AddParentItemsAsync(subPath);
 		DeleteStatusHelper.AddDeleteStatus(dbItem);
 		return dbItem;
 	}
-	
-	
+
+
 	/// 
 	/// Create an new item in the database
 	/// 
@@ -76,7 +76,7 @@ internal async Task> NewItem(List statusItems
 	internal async Task HandleLastEditedIsSame(FileIndexItem updatedDbItem, bool? fileHashSame)
 	{
 		// ReSharper disable once InvertIf
-		if ( _appSettings.SyncAlwaysUpdateLastEditedTime != true && fileHashSame == true)
+		if ( _appSettings.SyncAlwaysUpdateLastEditedTime != true && fileHashSame == true )
 		{
 			updatedDbItem.Status = FileIndexItem.ExifStatus.Ok;
 			return updatedDbItem;
@@ -84,7 +84,7 @@ internal async Task HandleLastEditedIsSame(FileIndexItem updatedD
 
 		return await UpdateItemLastEdited(updatedDbItem);
 	}
-	
+
 	/// 
 	/// Only update the last edited time
 	/// 
@@ -96,10 +96,10 @@ internal async Task UpdateItemLastEdited(FileIndexItem updatedDbI
 		updatedDbItem.Status = FileIndexItem.ExifStatus.Ok;
 		DeleteStatusHelper.AddDeleteStatus(updatedDbItem);
 		updatedDbItem.LastChanged =
-			new List {nameof(FileIndexItem.LastEdited)};
+			new List { nameof(FileIndexItem.LastEdited) };
 		return updatedDbItem;
 	}
-	
+
 	/// 
 	/// Update item to database
 	/// 
@@ -114,13 +114,13 @@ internal async Task UpdateItem(FileIndexItem dbItem, long size, s
 		{
 			_logger.LogDebug($"[SyncSingleFile] Trigger Update Item {subPath}");
 		}
-			
+
 		var updateItem = await _newItem.PrepareUpdateFileItemAsync(dbItem, size);
-		if ( updateItem.Status == FileIndexItem.ExifStatus.OkAndSame)
+		if ( updateItem.Status == FileIndexItem.ExifStatus.OkAndSame )
 		{
 			return updateItem;
 		}
-		
+
 		await _query.UpdateItemAsync(updateItem);
 		if ( addParentItems )
 		{
diff --git a/starsky/starsky.foundation.sync/Helpers/SizeFileHashIsTheSame.cs b/starsky/starsky.foundation.sync/Helpers/SizeFileHashIsTheSame.cs
index a4b46f651e..2951e1fcb9 100644
--- a/starsky/starsky.foundation.sync/Helpers/SizeFileHashIsTheSame.cs
+++ b/starsky/starsky.foundation.sync/Helpers/SizeFileHashIsTheSame.cs
@@ -24,73 +24,73 @@ public SizeFileHashIsTheSameHelper(IStorage subPathStorage)
 	/// item that contain size and fileHash
 	/// which item
 	/// Last Edited is the bool (null is should check further in process), FileHash Same bool (null is not checked) , database item
-	internal async Task> SizeFileHashIsTheSame(List dbItems, string subPath)
+	internal async Task> SizeFileHashIsTheSame(List dbItems, string subPath)
 	{
 		var dbItem = dbItems.Find(p => p.FilePath == subPath);
 		if ( dbItem == null )
 		{
 			return new Tuple(false, false, null!);
 		}
-		
+
 		// when last edited is the same
-		var (isRequestFileLastEditTheSame, lastEdit,_) = CompareLastEditIsTheSame(dbItem);
+		var (isRequestFileLastEditTheSame, lastEdit, _) = CompareLastEditIsTheSame(dbItem);
 		dbItem.LastEdited = lastEdit;
 		dbItem.Size = _subPathStorage.Info(dbItem.FilePath!).Size;
-		
+
 		// compare raw files
 		var otherRawItems = dbItems.Where(p =>
 			ExtensionRolesHelper.IsExtensionForceXmp(p.FilePath) && !ExtensionRolesHelper.IsExtensionSidecar(p.FilePath))
 			.Where(p => p.FilePath == subPath)
 			.Select(CompareLastEditIsTheSame)
 			.Where(p => p.Item1).ToList();
-	
+
 		if ( isRequestFileLastEditTheSame && otherRawItems.Count == 0 )
 		{
 			return new Tuple(true, null, dbItem);
 		}
-		
+
 		// when byte hash is different update
-		var (requestFileHashTheSame,_ ) = await CompareFileHashIsTheSame(dbItem);
-		
+		var (requestFileHashTheSame, _) = await CompareFileHashIsTheSame(dbItem);
+
 		// when there are xmp files in the list and the fileHash of the current raw is the same
 		if ( isRequestFileLastEditTheSame && requestFileHashTheSame )
 		{
 			return new Tuple(null, null, dbItem);
 		}
-		
+
 		return new Tuple(false, requestFileHashTheSame, dbItem);
 	}
-	
+
 	/// 
 	/// Compare the file hash en return 
 	/// 
 	/// database item
 	/// tuple that has value: is the same; and the fileHash
-	private async Task> CompareFileHashIsTheSame(FileIndexItem dbItem)
+	private async Task> CompareFileHashIsTheSame(FileIndexItem dbItem)
 	{
-		var (localHash,_) = await new 
+		var (localHash, _) = await new
 			FileHash(_subPathStorage).GetHashCodeAsync(dbItem.FilePath!);
 		var isTheSame = dbItem.FileHash == localHash;
 		dbItem.FileHash = localHash;
 		return new Tuple(isTheSame, localHash);
 	}
-	
+
 	/// 
 	/// True when result is the same
 	/// 
 	/// 
 	/// lastWriteTime is the same, lastWriteTime, filePath
-	private Tuple CompareLastEditIsTheSame(FileIndexItem dbItem)
+	private Tuple CompareLastEditIsTheSame(FileIndexItem dbItem)
 	{
 		var lastWriteTime = _subPathStorage.Info(dbItem.FilePath!).LastWriteTime;
-		if (lastWriteTime.Year == 1 )
+		if ( lastWriteTime.Year == 1 )
 		{
-			return new Tuple(false, lastWriteTime, dbItem.FilePath!);
+			return new Tuple(false, lastWriteTime, dbItem.FilePath!);
 		}
 
 		var isTheSame = DateTime.Compare(dbItem.LastEdited, lastWriteTime) == 0;
 
 		dbItem.LastEdited = lastWriteTime;
-		return new Tuple(isTheSame, lastWriteTime, dbItem.FilePath!);
+		return new Tuple(isTheSame, lastWriteTime, dbItem.FilePath!);
 	}
 }
diff --git a/starsky/starsky.foundation.sync/Helpers/SyncCli.cs b/starsky/starsky.foundation.sync/Helpers/SyncCli.cs
index 428b4cdde7..e0f1548958 100644
--- a/starsky/starsky.foundation.sync/Helpers/SyncCli.cs
+++ b/starsky/starsky.foundation.sync/Helpers/SyncCli.cs
@@ -26,35 +26,35 @@ public SyncCli(ISynchronize synchronize, AppSettings appSettings, IConsole conso
 			_synchronize = synchronize;
 			_selectorStorage = selectorStorage;
 		}
-		
+
 		public async Task Sync(string[] args)
 		{
 			_appSettings.Verbose = ArgsHelper.NeedVerbose(args);
 			_appSettings.ApplicationType = AppSettings.StarskyAppType.Sync;
 
-			if (ArgsHelper.NeedHelp(args))
+			if ( ArgsHelper.NeedHelp(args) )
 			{
 				new ArgsHelper(_appSettings, _console).NeedHelpShowDialog();
 				return;
 			}
-			
+
 			new ArgsHelper().SetEnvironmentByArgs(args);
 
 			var subPath = new ArgsHelper(_appSettings).SubPathOrPathValue(args);
 			var getSubPathRelative = new ArgsHelper(_appSettings).GetRelativeValue(args);
-			if (getSubPathRelative != null)
+			if ( getSubPathRelative != null )
 			{
 				var parseSubPath = new StructureService(
 						_selectorStorage.Get(SelectorStorage.StorageServices
 							.SubPath), _appSettings.Structure)
 					.ParseSubfolders(getSubPathRelative);
-				if ( !string.IsNullOrEmpty(parseSubPath)  )
+				if ( !string.IsNullOrEmpty(parseSubPath) )
 				{
 					subPath = parseSubPath;
 				}
 			}
 
-			if (ArgsHelper.GetIndexMode(args))
+			if ( ArgsHelper.GetIndexMode(args) )
 			{
 				var stopWatch = Stopwatch.StartNew();
 				_console.WriteLine($"Start indexing {subPath}");
@@ -63,7 +63,7 @@ public async Task Sync(string[] args)
 				{
 					_console.WriteLine($"Not Found: {subPath}");
 				}
-				
+
 				stopWatch.Stop();
 				_console.WriteLine($"\nDone SyncFiles! {GetStopWatchText(stopWatch)}");
 			}
diff --git a/starsky/starsky.foundation.sync/Helpers/SyncIgnoreCheck.cs b/starsky/starsky.foundation.sync/Helpers/SyncIgnoreCheck.cs
index 74712a3278..76018a907f 100644
--- a/starsky/starsky.foundation.sync/Helpers/SyncIgnoreCheck.cs
+++ b/starsky/starsky.foundation.sync/Helpers/SyncIgnoreCheck.cs
@@ -1,4 +1,3 @@
-using System.Linq;
 using starsky.foundation.platform.Interfaces;
 using starsky.foundation.platform.Models;
 
@@ -14,11 +13,12 @@ public SyncIgnoreCheck(AppSettings appSettings, IConsole console)
 			_appSettings = appSettings;
 			_console = console;
 		}
-		
+
 		public bool Filter(string subPath)
 		{
 			var isSynced = _appSettings.SyncIgnore.Exists(subPath.StartsWith);
-			if ( isSynced && _appSettings.IsVerbose()) _console.WriteLine($"sync ignored for: {subPath}");
+			if ( isSynced && _appSettings.IsVerbose() )
+				_console.WriteLine($"sync ignored for: {subPath}");
 			return isSynced;
 		}
 	}
diff --git a/starsky/starsky.foundation.sync/Metrics/DiskWatcherBackgroundTaskQueueMetrics.cs b/starsky/starsky.foundation.sync/Metrics/DiskWatcherBackgroundTaskQueueMetrics.cs
new file mode 100644
index 0000000000..2cd7857269
--- /dev/null
+++ b/starsky/starsky.foundation.sync/Metrics/DiskWatcherBackgroundTaskQueueMetrics.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.Metrics;
+using starsky.foundation.injection;
+using starsky.foundation.platform.MetricsNamespaces;
+
+namespace starsky.foundation.sync.Metrics;
+
+[Service(typeof(DiskWatcherBackgroundTaskQueueMetrics),
+	InjectionLifetime = InjectionLifetime.Singleton)]
+public class DiskWatcherBackgroundTaskQueueMetrics
+{
+	public int Value { get; set; }
+
+	public DiskWatcherBackgroundTaskQueueMetrics(IMeterFactory meterFactory)
+	{
+		var meter = meterFactory.Create(ActivitySourceMeter.SyncNameSpace);
+		const string name = "_s." + nameof(DiskWatcherBackgroundTaskQueueMetrics);
+		meter.CreateObservableGauge(name, ObserveValue);
+	}
+
+	private int ObserveValue()
+	{
+		return Value;
+	}
+}
diff --git a/starsky/starsky.foundation.sync/SyncInterfaces/IManualBackgroundSyncService.cs b/starsky/starsky.foundation.sync/SyncInterfaces/IManualBackgroundSyncService.cs
index 845739acd0..af55ed20ec 100644
--- a/starsky/starsky.foundation.sync/SyncInterfaces/IManualBackgroundSyncService.cs
+++ b/starsky/starsky.foundation.sync/SyncInterfaces/IManualBackgroundSyncService.cs
@@ -5,7 +5,6 @@ namespace starsky.foundation.sync.SyncInterfaces
 {
 	public interface IManualBackgroundSyncService
 	{
-		Task ManualSync(string subPath,
-			string? operationId = null);
+		Task ManualSync(string subPath);
 	}
 }
diff --git a/starsky/starsky.foundation.sync/SyncInterfaces/ISync.cs b/starsky/starsky.foundation.sync/SyncInterfaces/ISync.cs
index 280b443032..a826aabc94 100644
--- a/starsky/starsky.foundation.sync/SyncInterfaces/ISync.cs
+++ b/starsky/starsky.foundation.sync/SyncInterfaces/ISync.cs
@@ -15,7 +15,7 @@ public interface ISynchronize
 		Task> Sync(string subPath,
 			SocketUpdateDelegate? updateDelegate = null,
 			DateTime? childDirectoriesAfter = null);
-		Task> Sync(List subPaths, 
+		Task> Sync(List subPaths,
 			SocketUpdateDelegate? updateDelegate = null);
 	}
 }
diff --git a/starsky/starsky.foundation.sync/SyncServices/ManualBackgroundSyncService.cs b/starsky/starsky.foundation.sync/SyncServices/ManualBackgroundSyncService.cs
index 845fb45339..5f3cf76ca7 100644
--- a/starsky/starsky.foundation.sync/SyncServices/ManualBackgroundSyncService.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/ManualBackgroundSyncService.cs
@@ -1,21 +1,12 @@
 using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using System.Net.WebSockets;
-using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.DependencyInjection;
 using starsky.foundation.database.Interfaces;
 using starsky.foundation.database.Models;
 using starsky.foundation.injection;
-using starsky.foundation.platform.Enums;
 using starsky.foundation.platform.Interfaces;
-using starsky.foundation.platform.Models;
-using starsky.foundation.realtime.Interfaces;
 using starsky.foundation.sync.SyncInterfaces;
-using starsky.foundation.webtelemetry.Helpers;
 using starsky.foundation.worker.Interfaces;
 
 namespace starsky.foundation.sync.SyncServices
@@ -29,12 +20,10 @@ public sealed class ManualBackgroundSyncService : IManualBackgroundSyncService
 		private readonly IMemoryCache _cache;
 		private readonly IWebLogger _logger;
 		private readonly IUpdateBackgroundTaskQueue _bgTaskQueue;
-		private readonly IServiceScopeFactory _scopeFactory;
 
 		public ManualBackgroundSyncService(ISynchronize synchronize, IQuery query,
 			ISocketSyncUpdateService socketUpdateService,
-			IMemoryCache cache , IWebLogger logger, IUpdateBackgroundTaskQueue bgTaskQueue, 
-			IServiceScopeFactory scopeFactory)
+			IMemoryCache cache, IWebLogger logger, IUpdateBackgroundTaskQueue bgTaskQueue)
 		{
 			_synchronize = synchronize;
 			_socketUpdateService = socketUpdateService;
@@ -42,17 +31,15 @@ public ManualBackgroundSyncService(ISynchronize synchronize, IQuery query,
 			_cache = cache;
 			_logger = logger;
 			_bgTaskQueue = bgTaskQueue;
-			_scopeFactory = scopeFactory;
 		}
 
 		internal const string ManualSyncCacheName = "ManualSync_";
-		
-		public async Task ManualSync(string subPath,
-			string? operationId = null)
+
+		public async Task ManualSync(string subPath)
 		{
 			var fileIndexItem = await _query.GetObjectByFilePathAsync(subPath);
 			// on a new database ->
-			if ( subPath == "/" && fileIndexItem == null) fileIndexItem = new FileIndexItem();
+			if ( subPath == "/" && fileIndexItem == null ) fileIndexItem = new FileIndexItem();
 			if ( fileIndexItem == null )
 			{
 				_logger.LogInformation($"[ManualSync] NotFoundNotInIndex skip for: {subPath}");
@@ -72,8 +59,7 @@ public ManualBackgroundSyncService(ISynchronize synchronize, IQuery query,
 			// Runs within IUpdateBackgroundTaskQueue
 			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 			{
-				await BackgroundTaskExceptionWrapper(fileIndexItem.FilePath!,
-					operationId);
+				await BackgroundTaskExceptionWrapper(fileIndexItem.FilePath!);
 			}, fileIndexItem.FilePath!);
 
 			return FileIndexItem.ExifStatus.Ok;
@@ -81,8 +67,8 @@ await BackgroundTaskExceptionWrapper(fileIndexItem.FilePath!,
 
 		internal void CreateSyncLock(string subPath)
 		{
-			_cache.Set(ManualSyncCacheName + subPath, true, 
-				new TimeSpan(0,2,0));
+			_cache.Set(ManualSyncCacheName + subPath, true,
+				new TimeSpan(0, 2, 0));
 		}
 
 		private void RemoveSyncLock(string? subPath)
@@ -91,42 +77,39 @@ private void RemoveSyncLock(string? subPath)
 			_cache.Remove(ManualSyncCacheName + subPath);
 		}
 
-		internal async Task BackgroundTaskExceptionWrapper(string? subPath, string? operationId)
+		internal async Task BackgroundTaskExceptionWrapper(string? subPath)
 		{
 			try
 			{
-				await BackgroundTask(subPath, operationId);
+				await BackgroundTask(subPath);
 			}
-			catch ( Exception exception)
+			catch ( Exception exception )
 			{
-				_logger.LogError(exception,"ManualBackgroundSyncService [ManualSync] catch-ed exception");
+				_logger.LogError(exception,
+					"ManualBackgroundSyncService [ManualSync] catch-ed exception");
 				RemoveSyncLock(subPath);
 				throw;
 			}
 		}
 
-		internal async Task BackgroundTask(string? subPath, string? operationId)
+		internal async Task BackgroundTask(string? subPath)
 		{
 			subPath ??= string.Empty;
-			
-			var operationHolder = RequestTelemetryHelper.GetOperationHolder(_scopeFactory,
-				nameof(ManualSync), operationId);
-			
+
 			_logger.LogInformation($"[ManualBackgroundSyncService] start {subPath} " +
-			                       $"{DateTime.Now.ToShortTimeString()}");
-			
+								   $"{DateTime.Now.ToShortTimeString()}");
+
 			var updatedList = await _synchronize.Sync(subPath, _socketUpdateService.PushToSockets);
-			
+
 			_query.CacheUpdateItem(updatedList.Where(p => p.ParentDirectory == subPath).ToList());
-			
+
 			// so you can click on the button again
 			RemoveSyncLock(subPath);
 			_logger.LogInformation($"[ManualBackgroundSyncService] done {subPath} " +
-			                       $"{DateTime.Now.ToShortTimeString()}");
-			_logger.LogInformation($"[ManualBackgroundSyncService] Ok: {updatedList.Count(p => p.Status == FileIndexItem.ExifStatus.Ok)}" +
-			                       $" ~ OkAndSame: {updatedList.Count(p => p.Status == FileIndexItem.ExifStatus.OkAndSame)}");
-			operationHolder.SetData(_scopeFactory, updatedList);
+								   $"{DateTime.Now.ToShortTimeString()}");
+			_logger.LogInformation(
+				$"[ManualBackgroundSyncService] Ok: {updatedList.Count(p => p.Status == FileIndexItem.ExifStatus.Ok)}" +
+				$" ~ OkAndSame: {updatedList.Count(p => p.Status == FileIndexItem.ExifStatus.OkAndSame)}");
 		}
-
 	}
 }
diff --git a/starsky/starsky.foundation.sync/SyncServices/SocketSyncUpdateService.cs b/starsky/starsky.foundation.sync/SyncServices/SocketSyncUpdateService.cs
index 81874ec711..00b2cea4e2 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SocketSyncUpdateService.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SocketSyncUpdateService.cs
@@ -27,7 +27,7 @@ public SocketSyncUpdateService(IWebSocketConnectionsService connectionsService,
 		_notificationQuery = notificationQuery;
 		_logger = logger;
 	}
-	
+
 	/// 
 	/// Used by manual sync
 	/// 
@@ -37,7 +37,7 @@ public async Task PushToSockets(List changedFiles)
 		var webSocketResponse =
 			new ApiNotificationResponseModel>(FilterBefore(changedFiles), ApiNotificationType.ManualBackgroundSync);
 		await _notificationQuery.AddNotification(webSocketResponse);
-			
+
 		try
 		{
 			await _connectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None);
@@ -48,9 +48,9 @@ public async Task PushToSockets(List changedFiles)
 			_logger.LogError("[ManualBackgroundSyncService] catch-ed WebSocketException: " + exception.Message, exception);
 		}
 	}
-	
+
 	internal static List FilterBefore(IEnumerable syncData)
 	{
-		return syncData.Where(p => p.FilePath != "/" ).ToList();
+		return syncData.Where(p => p.FilePath != "/").ToList();
 	}
 }
diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncAddAddThumbnailTable.cs b/starsky/starsky.foundation.sync/SyncServices/SyncAddAddThumbnailTable.cs
index 4ef4a59de0..f361ecd598 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SyncAddAddThumbnailTable.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SyncAddAddThumbnailTable.cs
@@ -18,20 +18,20 @@ public SyncAddAddThumbnailTable(IThumbnailQuery thumbnailQuery)
 	{
 		_thumbnailQuery = thumbnailQuery;
 	}
-	
+
 	public async Task> SyncThumbnailTableAsync(List fileIndexItems)
 	{
 		var addObjects = fileIndexItems
-			.Where(p => p.Status == FileIndexItem.ExifStatus.Ok && 
-			            p.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp &&
-			            p.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json &&
-			            !string.IsNullOrEmpty(p.FileHash) && p.IsDirectory == false)
+			.Where(p => p.Status == FileIndexItem.ExifStatus.Ok &&
+						p.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp &&
+						p.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json &&
+						!string.IsNullOrEmpty(p.FileHash) && p.IsDirectory == false)
 			.DistinctBy(p => p.FileHash)
 			.Select(p => p.FileHash)
 			.Select(fileHash => new ThumbnailResultDataTransferModel(fileHash!)).ToList();
-		
+
 		await _thumbnailQuery.AddThumbnailRangeAsync(addObjects);
-		
+
 		return fileIndexItems;
 	}
 }
diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs b/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs
index 10a4b5c8b2..98339af010 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs
@@ -34,8 +34,8 @@ public sealed class SyncFolder
 		private readonly IServiceScopeFactory? _serviceScopeFactory;
 		private readonly SyncIgnoreCheck _syncIgnoreCheck;
 
-		public SyncFolder(AppSettings appSettings, IQuery query, 
-			ISelectorStorage selectorStorage, IConsole console, 
+		public SyncFolder(AppSettings appSettings, IQuery query,
+			ISelectorStorage selectorStorage, IConsole console,
 			IWebLogger logger, IMemoryCache? memoryCache, IServiceScopeFactory? serviceScopeFactory)
 		{
 			_subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
@@ -53,8 +53,8 @@ public SyncFolder(AppSettings appSettings, IQuery query,
 		public async Task> Folder(string inputSubPath,
 			ISynchronize.SocketUpdateDelegate? updateDelegate = null, DateTime? childDirectoriesAfter = null)
 		{
-			var subPaths = new List {inputSubPath};	
-			
+			var subPaths = new List { inputSubPath };
+
 			subPaths.AddRange(_subPathStorage.GetDirectoryRecursive(inputSubPath)
 				.Where(p =>
 				{
@@ -62,7 +62,7 @@ public async Task> Folder(string inputSubPath,
 					return p.Value >= childDirectoriesAfter;
 				})
 				.Select(p => p.Key));
-			
+
 			// Loop trough all folders recursive
 			var resultChunkList = await subPaths.ForEachAsync(
 				async subPath =>
@@ -71,11 +71,11 @@ public async Task> Folder(string inputSubPath,
 					var queryFactory = new QueryFactory(_setupDatabaseTypes,
 						_query, _memoryCache, _appSettings, _serviceScopeFactory, _logger);
 					var query = queryFactory.Query();
-					
+
 					// get only direct child files and folders and NOT recursive
 					var fileIndexItems = await query!.GetAllObjectsAsync(subPath);
 					fileIndexItems = await new Duplicate(query).RemoveDuplicateAsync(fileIndexItems);
-				
+
 					// And check files within this folder
 					var pathsOnDisk = _subPathStorage.GetAllFilesInDirectory(subPath)
 						.Where(ExtensionRolesHelper.IsExtensionSyncSupported).ToList();
@@ -84,11 +84,11 @@ public async Task> Folder(string inputSubPath,
 					{
 						_console.Write("⁘");
 					}
-				
+
 					var indexItems = await LoopOverFolder(fileIndexItems, pathsOnDisk, updateDelegate, false);
 					allResults.AddRange(indexItems);
 
-					var dirItems = (await CheckIfFolderExistOnDisk(fileIndexItems)).Where(p => p != null).ToList();
+					var dirItems = ( await CheckIfFolderExistOnDisk(fileIndexItems) ).Where(p => p != null).ToList();
 					if ( dirItems.Count != 0 )
 					{
 						allResults.AddRange(dirItems!);
@@ -97,27 +97,27 @@ public async Task> Folder(string inputSubPath,
 					await query.DisposeAsync();
 					return allResults;
 				}, _appSettings.MaxDegreesOfParallelism);
-			
+
 			// Convert chunks into one list
 			var allResults = new List();
 			foreach ( var resultChunk in resultChunkList! )
 			{
 				allResults.AddRange(resultChunk);
 			}
-			
+
 			// query.DisposeAsync is called to avoid memory usage
 			var queryFactory = new QueryFactory(_setupDatabaseTypes,
 				_query, _memoryCache, _appSettings, _serviceScopeFactory, _logger);
 			_query = queryFactory.Query()!;
-			
+
 			// // remove the duplicates from a large list of folders
 			var folderList = await _query.GetObjectsByFilePathQueryAsync(subPaths);
 			folderList = await _duplicate.RemoveDuplicateAsync(folderList);
 
 			await CompareFolderListAndFixMissingFolders(subPaths, folderList);
 
-			var parentItems = (await AddParentFolder(inputSubPath, allResults))
-				.Where( p => p.Status != FileIndexItem.ExifStatus.OkAndSame).ToList();
+			var parentItems = ( await AddParentFolder(inputSubPath, allResults) )
+				.Where(p => p.Status != FileIndexItem.ExifStatus.OkAndSame).ToList();
 			if ( parentItems.Count != 0 )
 			{
 				allResults.AddRange(parentItems);
@@ -126,21 +126,21 @@ public async Task> Folder(string inputSubPath,
 			var socketUpdates = allResults.Where(p =>
 				p.Status is FileIndexItem.ExifStatus.Ok
 					or FileIndexItem.ExifStatus.Deleted).ToList();
-			
+
 			if ( updateDelegate != null && socketUpdates.Count != 0 )
 			{
 				await updateDelegate(socketUpdates);
 			}
-			
+
 			return allResults;
 		}
-	
+
 		internal async Task CompareFolderListAndFixMissingFolders(List subPaths, List folderList)
 		{
 			if ( subPaths.Count == folderList.Count ) return;
-			
+
 			foreach ( var path in subPaths.Where(path => folderList.TrueForAll(p => p.FilePath != path) &&
-				_subPathStorage.ExistFolder(path) && !_syncIgnoreCheck.Filter(path) ) )
+				_subPathStorage.ExistFolder(path) && !_syncIgnoreCheck.Filter(path)) )
 			{
 				await _query.AddItemAsync(new FileIndexItem(path)
 				{
@@ -150,14 +150,14 @@ await _query.AddItemAsync(new FileIndexItem(path)
 				});
 			}
 		}
-	
+
 		internal async Task> AddParentFolder(string subPath, List? allResults)
 		{
 			allResults ??= new List();
 			// used to check later if the item already exists, to avoid duplicates
 			var filePathsAllResults = allResults.Select(p => p.FilePath).ToList();
 
-			if ( allResults.TrueForAll(p => p.FilePath != subPath))
+			if ( allResults.TrueForAll(p => p.FilePath != subPath) )
 			{
 				var subPathStatus = _subPathStorage.IsFolderOrFile(subPath);
 				var exifStatus = subPathStatus ==
@@ -166,10 +166,12 @@ internal async Task> AddParentFolder(string subPath, List Merge(IReadOnlyCollection allResults1)
@@ -180,7 +182,7 @@ List Merge(IReadOnlyCollection allResults1)
 				parentDirectoriesStart.AddRange(allResults1
 					.Where(p => p.IsDirectory == true)
 					.Select(p => p.FilePath));
-				return  parentDirectoriesStart.Where(p => !string.IsNullOrEmpty(p)).Distinct().ToList()!;
+				return parentDirectoriesStart.Where(p => !string.IsNullOrEmpty(p)).Distinct().ToList()!;
 			}
 
 			var parentDirectories = Merge(allResults);
@@ -211,10 +213,10 @@ List Merge(IReadOnlyCollection allResults1)
 				{
 					item.Status = FileIndexItem.ExifStatus.OkAndSame;
 				}
-				
+
 				newItems.Add(item);
 			}
-			
+
 			await _query.AddRangeAsync(newItems.Where(p => p.Status == FileIndexItem.ExifStatus.Ok).ToList());
 
 			return newItems;
@@ -227,7 +229,7 @@ private async Task> LoopOverFolder(
 		{
 			var fileIndexItemsOnlyFiles = fileIndexItems
 				.Where(p => p.IsDirectory == false).ToList();
-			
+
 			var pathsToUpdateInDatabase = PathsToUpdateInDatabase(fileIndexItemsOnlyFiles, pathsOnDisk);
 			if ( pathsToUpdateInDatabase.Count == 0 )
 			{
@@ -243,20 +245,20 @@ private async Task> LoopOverFolder(
 						_query, _memoryCache, _appSettings,
 						_serviceScopeFactory, _logger);
 					var query = queryFactory.Query()!;
-					
+
 					var syncMultiFile = new SyncMultiFile(_appSettings, query,
 						_subPathStorage,
 						null!,
 						_logger);
 					var databaseItems = await syncMultiFile.MultiFile(
 						subPathInFiles, updateDelegate, addParentFolder);
-				
+
 					await new SyncRemove(_appSettings, _setupDatabaseTypes,
 							query, _memoryCache, _logger)
 						.RemoveAsync(databaseItems, updateDelegate);
 
 					DisplayInlineConsole(_console, databaseItems);
-				
+
 					return databaseItems;
 				}, _appSettings.MaxDegreesOfParallelism);
 
@@ -266,7 +268,7 @@ private async Task> LoopOverFolder(
 			{
 				results.AddRange(resultChunk);
 			}
-		
+
 			return results;
 		}
 
@@ -277,18 +279,18 @@ internal static void DisplayInlineConsole(IConsole console, List
 				switch ( item.Status )
 				{
 					case FileIndexItem.ExifStatus
-						.NotFoundSourceMissing:	
-						console.Write("≠"); 
+						.NotFoundSourceMissing:
+						console.Write("≠");
 						break;
 
 					case FileIndexItem.ExifStatus.Ok:
-						console.Write("•"); 
+						console.Write("•");
 						break;
 					case FileIndexItem.ExifStatus.OkAndSame:
-						console.Write("⩮"); 
+						console.Write("⩮");
 						break;
 					case FileIndexItem.ExifStatus.DeletedAndSame:
-						console.Write("✘"); 
+						console.Write("✘");
 						break;
 					case FileIndexItem.ExifStatus.Deleted:
 						console.Write("\u058d"); // ֍ 
@@ -301,24 +303,24 @@ internal static void DisplayInlineConsole(IConsole console, List
 		}
 
 		internal static List PathsToUpdateInDatabase(
-			List databaseItems, 
+			List databaseItems,
 			IReadOnlyCollection pathsOnDisk)
 		{
 			var resultDatabaseItems = new List(databaseItems);
 			foreach ( var path in pathsOnDisk )
 			{
 				var item = databaseItems.Find(p => string.Equals(p.FilePath, path, StringComparison.InvariantCultureIgnoreCase));
-				if (item == null ) // when the file should be added to the index
+				if ( item == null ) // when the file should be added to the index
 				{
 					// Status is used by MultiFile
-					resultDatabaseItems.Add(new FileIndexItem(path){Status = FileIndexItem.ExifStatus.NotFoundNotInIndex});
+					resultDatabaseItems.Add(new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.NotFoundNotInIndex });
 					continue;
 				}
 				resultDatabaseItems.Add(item);
 			}
 			return resultDatabaseItems.DistinctBy(p => p.FilePath).ToList();
 		}
-	
+
 		private async Task> CheckIfFolderExistOnDisk(List fileIndexItems)
 		{
 			var fileIndexItemsOnlyFolders = fileIndexItems
@@ -328,9 +330,9 @@ internal static List PathsToUpdateInDatabase(
 			{
 				return new List();
 			}
-		
+
 			// can this be done in a batch?
-			return (await fileIndexItemsOnlyFolders
+			return ( await fileIndexItemsOnlyFolders
 				.ForEachAsync(async item =>
 				{
 					// assume only the input of directories
@@ -338,16 +340,16 @@ internal static List PathsToUpdateInDatabase(
 					{
 						return null;
 					}
-					
+
 					var queryFactory = new QueryFactory(_setupDatabaseTypes,
 						_query, _memoryCache, _appSettings,
 						_serviceScopeFactory, _logger);
 					var query = queryFactory.Query();
-					
+
 					return await RemoveChildItems(query!, item);
-				}, _appSettings.MaxDegreesOfParallelism))!.ToList();
+				}, _appSettings.MaxDegreesOfParallelism) )!.ToList();
 		}
-	
+
 		/// 
 		/// Remove all items that are included
 		/// 
@@ -363,7 +365,7 @@ internal async Task RemoveChildItems(IQuery query, FileIndexItem
 				_console.Write("✕");
 				await query.RemoveItemAsync(remove);
 			}
-			
+
 			// Item it self
 			await query.RemoveItemAsync(item);
 			_console.Write("✕");
@@ -375,5 +377,5 @@ internal async Task RemoveChildItems(IQuery query, FileIndexItem
 			return item;
 		}
 	}
-	
+
 }
diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncMultiFile.cs b/starsky/starsky.foundation.sync/SyncServices/SyncMultiFile.cs
index 42a8268763..cfa3f8ce22 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SyncMultiFile.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SyncMultiFile.cs
@@ -24,7 +24,7 @@ public sealed class SyncMultiFile
 		private readonly NewUpdateItemWrapper _newUpdateItemWrapper;
 		private readonly CheckForStatusNotOkHelper _checkForStatusNotOkHelper;
 
-		public SyncMultiFile(AppSettings appSettings, IQuery query, 
+		public SyncMultiFile(AppSettings appSettings, IQuery query,
 			IStorage subPathStorage, IMemoryCache? cache, IWebLogger logger)
 		{
 			_query = query;
@@ -50,12 +50,12 @@ internal async Task> MultiFile(List subPathInFiles,
 			var resultDatabaseItems = new List();
 			foreach ( var path in subPathInFiles )
 			{
-				var item = databaseItems.Find(p => 
+				var item = databaseItems.Find(p =>
 					string.Equals(p.FilePath, path, StringComparison.InvariantCultureIgnoreCase));
-				if (item == null ) // when the file should be added to the index
+				if ( item == null ) // when the file should be added to the index
 				{
 					// Status is used by MultiFile
-					resultDatabaseItems.Add(new FileIndexItem(path){Status = FileIndexItem.ExifStatus.NotFoundNotInIndex});
+					resultDatabaseItems.Add(new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.NotFoundNotInIndex });
 					continue;
 				}
 				resultDatabaseItems.Add(item);
@@ -77,10 +77,10 @@ internal async Task> MultiFile(
 			bool addParentFolder = true)
 		{
 			if ( dbItems == null ) return new List();
-						
+
 			dbItems = DeleteStatusHelper.AddDeleteStatus(dbItems, FileIndexItem.ExifStatus.DeletedAndSame);
 
-			var statusItems =  _checkForStatusNotOkHelper
+			var statusItems = _checkForStatusNotOkHelper
 				.CheckForStatusNotOk(dbItems.Select(p => p.FilePath).Cast()).ToList();
 			UpdateCheckStatus(dbItems, statusItems);
 
@@ -116,12 +116,12 @@ internal async Task> MultiFile(
 
 			if ( addParentFolder )
 			{
-				_logger.LogInformation("Add Parent Folder For: " + 
-				                       string.Join(",", dbItems.Select(p => p.FilePath)));
-				
+				_logger.LogInformation("Add Parent Folder For: " +
+									   string.Join(",", dbItems.Select(p => p.FilePath)));
+
 				dbItems = await new AddParentList(_subPathStorage, _query).AddParentItems(dbItems);
 			}
-		
+
 			if ( updateDelegate == null ) return dbItems;
 			return await PushToSocket(dbItems, updateDelegate);
 		}
@@ -137,17 +137,17 @@ private static void UpdateCheckStatus(List dbItems, List> IsSameUpdatedItemList(IEnumerable p.Item1 != true) )
+
+			foreach ( var (isLastEditedSame, isFileHashSame, isSameUpdatedItem) in
+					 isSameUpdatedItemList.Where(p => p.Item1 != true) )
 			{
 				var updateItemIndex = dbItems.FindIndex(
 					p => p.FilePath == isSameUpdatedItem.FilePath);
-					
+
 				if ( isLastEditedSame == false && isFileHashSame == true )
 				{
 					dbItems[updateItemIndex] = await _newUpdateItemWrapper.HandleLastEditedIsSame(isSameUpdatedItem, true);
 					continue;
 				}
-					
+
 				dbItems[updateItemIndex] = await _newUpdateItemWrapper.UpdateItem(isSameUpdatedItem,
 					isSameUpdatedItem.Size,
 					isSameUpdatedItem.FilePath!, false);
@@ -192,21 +192,21 @@ private static void AddSidecarExtensionData(List dbItems, List
-					         p.FileCollectionName == statusItem.FileCollectionName
-					         && p.ParentDirectory == statusItem.ParentDirectory
-					         && ExtensionRolesHelper.IsExtensionSidecar(p.FileName) && 
-					         p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.OkAndSame 
-						         or FileIndexItem.ExifStatus.NotFoundNotInIndex))
+							 p.FileCollectionName == statusItem.FileCollectionName
+							 && p.ParentDirectory == statusItem.ParentDirectory
+							 && ExtensionRolesHelper.IsExtensionSidecar(p.FileName) &&
+							 p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.OkAndSame
+								 or FileIndexItem.ExifStatus.NotFoundNotInIndex) )
 				{
 					var dbMatchItemSearchedIndex = dbItems.FindIndex(p =>
 						p.ParentDirectory == item.ParentDirectory && p.FileCollectionName == item.FileCollectionName);
-						
+
 					dbItems[dbMatchItemSearchedIndex].AddSidecarExtension("xmp");
 					dbItems[dbMatchItemSearchedIndex].LastChanged.Add(nameof(FileIndexItem.SidecarExtensions));
 				}
 			}
 		}
-	
+
 		private static async Task> PushToSocket(List updatedDbItems,
 			ISynchronize.SocketUpdateDelegate updateDelegate)
 		{
diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncRemove.cs b/starsky/starsky.foundation.sync/SyncServices/SyncRemove.cs
index 8b0a30e439..374b8358f7 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SyncRemove.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SyncRemove.cs
@@ -24,7 +24,7 @@ public sealed class SyncRemove
 		private readonly IWebLogger _logger;
 		private readonly IServiceScopeFactory? _serviceScopeFactory;
 
-		public SyncRemove(AppSettings appSettings, IQuery query, 
+		public SyncRemove(AppSettings appSettings, IQuery query,
 			IMemoryCache? memoryCache, IWebLogger logger, IServiceScopeFactory? serviceScopeFactory)
 		{
 			_appSettings = appSettings;
@@ -54,7 +54,7 @@ public SyncRemove(AppSettings appSettings, SetupDatabaseTypes setupDatabaseTypes
 		public async Task> RemoveAsync(string subPath,
 			ISynchronize.SocketUpdateDelegate? updateDelegate = null)
 		{
-			return await RemoveAsync(new List {subPath}, updateDelegate);
+			return await RemoveAsync(new List { subPath }, updateDelegate);
 		}
 
 		/// 
@@ -70,7 +70,7 @@ public async Task> RemoveAsync(List subPaths,
 			var toDeleteList = await _query.GetAllRecursiveAsync(subPaths);
 			// and single objects 
 			toDeleteList.AddRange(await _query.GetObjectsByFilePathQueryAsync(subPaths));
-			
+
 			await toDeleteList
 				.ForEachAsync(async item =>
 				{
@@ -88,7 +88,7 @@ await toDeleteList
 			await LoopOverSidecarFiles(subPaths);
 
 			// Add items that are not in the database
-			foreach ( var subPath in subPaths.Where(subPath => 
+			foreach ( var subPath in subPaths.Where(subPath =>
 				!toDeleteList.Exists(p => p.FilePath == subPath)) )
 			{
 				toDeleteList.Add(new FileIndexItem(subPath)
@@ -101,7 +101,7 @@ await toDeleteList
 			{
 				await updateDelegate(toDeleteList);
 			}
-			
+
 			return toDeleteList.OrderBy(p => p.FilePath).ToList();
 		}
 
@@ -120,10 +120,10 @@ public async Task> RemoveAsync(
 					p.Status is FileIndexItem.ExifStatus
 						.NotFoundSourceMissing).Select(p => p.FilePath)
 				.Cast().ToList();
-			
+
 			return await RemoveAsync(deleted, updateDelegate);
 		}
-		
+
 		private async Task LoopOverSidecarFiles(List subPaths)
 		{
 			var parentDirectories = new HashSet();
@@ -146,8 +146,8 @@ private async Task LoopOverSidecarFiles(List subPaths)
 			{
 				foreach ( var singleCollectionPath in collectionPath )
 				{
-					if ( item.FilePath!.StartsWith(singleCollectionPath) 
-					     && !ExtensionRolesHelper.IsExtensionSidecar(item.FilePath) )
+					if ( item.FilePath!.StartsWith(singleCollectionPath)
+						 && !ExtensionRolesHelper.IsExtensionSidecar(item.FilePath) )
 					{
 						item.RemoveSidecarExtension("xmp");
 						await _query.UpdateItemAsync(item);
diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncSingleFile.cs b/starsky/starsky.foundation.sync/SyncServices/SyncSingleFile.cs
index d4acaff790..fb87f2211f 100644
--- a/starsky/starsky.foundation.sync/SyncServices/SyncSingleFile.cs
+++ b/starsky/starsky.foundation.sync/SyncServices/SyncSingleFile.cs
@@ -1,4 +1,3 @@
-#nullable enable
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
@@ -22,7 +21,7 @@ public sealed class SyncSingleFile
 		private readonly SyncMultiFile _syncMultiFile;
 		private readonly CheckForStatusNotOkHelper _checkForStatusNotOkHelper;
 
-		public SyncSingleFile(AppSettings appSettings, IQuery query, IStorage subPathStorage, 
+		public SyncSingleFile(AppSettings appSettings, IQuery query, IStorage subPathStorage,
 			IMemoryCache? memoryCache, IWebLogger logger)
 		{
 			_appSettings = appSettings;
@@ -66,23 +65,23 @@ internal async Task> SingleFile(string subPath,
 			{
 				_logger.LogInformation($"[SingleFile/db] info {subPath} " + Synchronize.DateTimeDebug());
 			}
-			
+
 			// ignore all the 'wrong' files
 			var statusItems = _checkForStatusNotOkHelper.CheckForStatusNotOk(subPath);
 
 			if ( statusItems.FirstOrDefault()!.Status != FileIndexItem.ExifStatus.Ok )
 			{
 				_logger.LogDebug($"[SingleFile/db] status " +
-				                 $"{statusItems.FirstOrDefault()!.Status} for {subPath} {Synchronize.DateTimeDebug()}");
+								 $"{statusItems.FirstOrDefault()!.Status} for {subPath} {Synchronize.DateTimeDebug()}");
 				return statusItems;
 			}
 
 			var scanItems = new List();
-			var dbItems =  await _query.GetObjectsByFilePathAsync(subPath,true);
+			var dbItems = await _query.GetObjectsByFilePathAsync(subPath, true);
 			foreach ( var item in statusItems )
 			{
 				var dbItem = dbItems.Find(p => item.FilePath == p.FilePath);
-				if ( dbItem != null  )
+				if ( dbItem != null )
 				{
 					scanItems.Add(dbItem);
 					continue;
@@ -115,10 +114,10 @@ public async Task UpdateSidecarFile(string xmpSubPath)
 			var parentPath = FilenamesHelper.GetParentPath(xmpSubPath);
 			var fileNameWithoutExtension = FilenamesHelper.GetFileNameWithoutExtension(xmpSubPath);
 
-			var directoryWithFileIndexItems = (await 
-				_query.GetAllFilesAsync(parentPath)).Where(
+			var directoryWithFileIndexItems = ( await
+				_query.GetAllFilesAsync(parentPath) ).Where(
 				p => p.ParentDirectory == parentPath &&
-				     p.FileCollectionName == fileNameWithoutExtension).ToList();
+					 p.FileCollectionName == fileNameWithoutExtension).ToList();
 
 			await UpdateSidecarFile(xmpSubPath, directoryWithFileIndexItems);
 		}
@@ -137,8 +136,8 @@ internal async Task UpdateSidecarFile(string xmpSubPath, List !item.SidecarExtensionsList.Contains(sidecarExt)) )
 			{
 				item.AddSidecarExtension(sidecarExt);
@@ -146,6 +145,6 @@ internal async Task UpdateSidecarFile(string xmpSubPath, List
 		/// 
 		/// 
-		public async Task> Sync(List subPaths, 
+		public async Task> Sync(List subPaths,
 			ISynchronize.SocketUpdateDelegate? updateDelegate = null)
 		{
-			var results = await _syncMultiFile.MultiFile(subPaths,updateDelegate);
+			var results = await _syncMultiFile.MultiFile(subPaths, updateDelegate);
 			return await _syncAddThumbnail.SyncThumbnailTableAsync(results);
 		}
 
-		private async Task> SyncWithoutThumbnail(string subPath, 
+		private async Task> SyncWithoutThumbnail(string subPath,
 			ISynchronize.SocketUpdateDelegate? updateDelegate = null,
 			DateTime? childDirectoriesAfter = null)
 		{
 			// Prefix / for database
 			subPath = PathHelper.PrefixDbSlash(subPath);
 			if ( subPath != "/" ) subPath = PathHelper.RemoveLatestSlash(subPath);
-			
-			if ( FilterCommonTempFiles.Filter(subPath)  || _syncIgnoreCheck.Filter(subPath)  ) 
+
+			if ( FilterCommonTempFiles.Filter(subPath) || _syncIgnoreCheck.Filter(subPath) )
 				return FilterCommonTempFiles.DefaultOperationNotSupported(subPath);
 
 			_console.WriteLine($"[Synchronize] Sync {subPath} {DateTimeDebug()}");
-			
+
 			// ReSharper disable once ConvertSwitchStatementToSwitchExpression
 			switch ( _subPathStorage.IsFolderOrFile(subPath) )
 			{
@@ -98,7 +98,7 @@ private async Task> SyncWithoutThumbnail(string subPath,
 
 		internal static string DateTimeDebug()
 		{
-			return ": " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", 
+			return ": " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss",
 				CultureInfo.InvariantCulture);
 		}
 	}
diff --git a/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherBackgroundTaskQueue.cs b/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherBackgroundTaskQueue.cs
index c04dcf22c8..05dc1a7dc6 100644
--- a/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherBackgroundTaskQueue.cs
+++ b/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherBackgroundTaskQueue.cs
@@ -1,31 +1,33 @@
-#nullable enable
 using System;
 using System.Threading;
 using System.Threading.Channels;
 using System.Threading.Tasks;
-using Microsoft.ApplicationInsights;
 using Microsoft.Extensions.DependencyInjection;
 using starsky.foundation.injection;
-using starsky.foundation.webtelemetry.Helpers;
+using starsky.foundation.sync.Metrics;
 using starsky.foundation.worker.Helpers;
-using starsky.foundation.worker.Services;
 
 namespace starsky.foundation.sync.WatcherBackgroundService
 {
 	/// 
 	/// @see: https://learn.microsoft.com/en-us/dotnet/core/extensions/queue-service
 	/// 
-	[Service(typeof(IDiskWatcherBackgroundTaskQueue), InjectionLifetime = InjectionLifetime.Singleton)]
+	[Service(typeof(IDiskWatcherBackgroundTaskQueue),
+		InjectionLifetime = InjectionLifetime.Singleton)]
 	public sealed class DiskWatcherBackgroundTaskQueue : IDiskWatcherBackgroundTaskQueue
 	{
-		private readonly Channel, string>> _queue;
-		private readonly TelemetryClient? _telemetryClient;
+		private readonly Channel, string?, string?>>
+			_queue;
+
+		private readonly DiskWatcherBackgroundTaskQueueMetrics _metrics;
 
 		public DiskWatcherBackgroundTaskQueue(IServiceScopeFactory scopeFactory)
 		{
-			_telemetryClient = scopeFactory.CreateScope().ServiceProvider
-				.GetService();
-			_queue = Channel.CreateBounded, string>>(ProcessTaskQueue.DefaultBoundedChannelOptions);
+			_queue = Channel
+				.CreateBounded, string?, string?>>(
+					ProcessTaskQueue.DefaultBoundedChannelOptions);
+			_metrics = scopeFactory.CreateScope().ServiceProvider
+				.GetRequiredService();
 		}
 
 		public int Count()
@@ -34,17 +36,21 @@ public int Count()
 		}
 
 		public ValueTask QueueBackgroundWorkItemAsync(
-			Func workItem, string metaData)
+			Func workItem, string? metaData = null,
+			string? traceParentId = null)
 		{
+			_metrics.Value = Count();
 			return ProcessTaskQueue.QueueBackgroundWorkItemAsync(_queue, workItem, metaData);
 		}
 
-		public async ValueTask, string>> DequeueAsync(
-			CancellationToken cancellationToken)
+		public async ValueTask, string?, string?>>
+			DequeueAsync(
+				CancellationToken cancellationToken)
 		{
-			MetricsHelper.Add(_telemetryClient, nameof(DiskWatcherBackgroundTaskQueue), Count());
 			var workItem =
 				await _queue.Reader.ReadAsync(cancellationToken);
+
+			_metrics.Value = Count();
 			return workItem;
 		}
 	}
diff --git a/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherQueuedHostedService.cs b/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherQueuedHostedService.cs
index 5d2a11c004..291ed5b744 100644
--- a/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherQueuedHostedService.cs
+++ b/starsky/starsky.foundation.sync/WatcherBackgroundService/DiskWatcherQueuedHostedService.cs
@@ -17,7 +17,7 @@ public sealed class DiskWatcherQueuedHostedService : BackgroundService
 		private readonly IDiskWatcherBackgroundTaskQueue _taskQueue;
 		private readonly IWebLogger _logger;
 		private readonly AppSettings _appSettings;
-		
+
 		public DiskWatcherQueuedHostedService(
 			IDiskWatcherBackgroundTaskQueue taskQueue,
 			IWebLogger logger, AppSettings appSettings) =>
diff --git a/starsky/starsky.foundation.sync/WatcherHelpers/EventQueueOverflowException.cs b/starsky/starsky.foundation.sync/WatcherHelpers/EventQueueOverflowException.cs
index c37c51f992..c15b4a3c3d 100644
--- a/starsky/starsky.foundation.sync/WatcherHelpers/EventQueueOverflowException.cs
+++ b/starsky/starsky.foundation.sync/WatcherHelpers/EventQueueOverflowException.cs
@@ -1,29 +1,29 @@
-using System;
+using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.Serialization;
 
 namespace starsky.foundation.sync.WatcherHelpers
 {
 	[Serializable]
-    public class EventQueueOverflowException : Exception
-    {
-        [SuppressMessage("ReSharper", "RedundantBaseConstructorCall")]
-        public EventQueueOverflowException()
-            : base() { }
+	public class EventQueueOverflowException : Exception
+	{
+		[SuppressMessage("ReSharper", "RedundantBaseConstructorCall")]
+		public EventQueueOverflowException()
+			: base() { }
 
-        public EventQueueOverflowException(string message)
-            : base(message) { }
-        
-        /// 
-        /// Without this constructor, deserialization will fail
-        /// 
-        /// 
-        /// 
-        protected EventQueueOverflowException(SerializationInfo info, StreamingContext context) 
+		public EventQueueOverflowException(string message)
+			: base(message) { }
+
+		/// 
+		/// Without this constructor, deserialization will fail
+		/// 
+		/// 
+		/// 
+		protected EventQueueOverflowException(SerializationInfo info, StreamingContext context)
 #pragma warning disable SYSLIB0051
-	        : base(info, context)
+			: base(info, context)
 #pragma warning restore SYSLIB0051
-        {
-        }
-    }
+		{
+		}
+	}
 }
diff --git a/starsky/starsky.foundation.sync/WatcherHelpers/QueueProcessor.cs b/starsky/starsky.foundation.sync/WatcherHelpers/QueueProcessor.cs
index c6bb4db51b..74da22e868 100644
--- a/starsky/starsky.foundation.sync/WatcherHelpers/QueueProcessor.cs
+++ b/starsky/starsky.foundation.sync/WatcherHelpers/QueueProcessor.cs
@@ -38,8 +38,8 @@ public async Task QueueInput(string filepath, string? toPath,
 		{
 			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 			{
-				await _processFile.Invoke(new Tuple(filepath,toPath,changeTypes));
-			}, $"from:{filepath}" + (string.IsNullOrEmpty(toPath) ? "" : "_to:" + toPath));
+				await _processFile.Invoke(new Tuple(filepath, toPath, changeTypes));
+			}, $"from:{filepath}" + ( string.IsNullOrEmpty(toPath) ? "" : "_to:" + toPath ));
 		}
 	}
 
diff --git a/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs b/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs
index 3ce83fbeb8..39d619fcff 100644
--- a/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs
+++ b/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs
@@ -6,9 +6,6 @@
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.ApplicationInsights;
-using Microsoft.ApplicationInsights.DataContracts;
-using Microsoft.ApplicationInsights.Extensibility;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.DependencyInjection;
 using starsky.foundation.database.Helpers;
@@ -21,205 +18,173 @@
 using starsky.foundation.platform.Models;
 using starsky.foundation.realtime.Interfaces;
 using starsky.foundation.sync.SyncInterfaces;
-using starsky.foundation.webtelemetry.Initializers;
-using starsky.foundation.webtelemetry.Models;
 
 [assembly: InternalsVisibleTo("starskytest")]
-namespace starsky.foundation.sync.WatcherHelpers
+
+namespace starsky.foundation.sync.WatcherHelpers;
+
+public sealed class SyncWatcherConnector
 {
-	public sealed class SyncWatcherConnector
+	private ISynchronize? _synchronize;
+	private AppSettings? _appSettings;
+	private IWebSocketConnectionsService? _connectionsService;
+	private IQuery? _query;
+	private IWebLogger? _logger;
+	private readonly IServiceScope? _serviceScope;
+	private INotificationQuery? _notificationQuery;
+
+	internal SyncWatcherConnector(AppSettings appSettings, ISynchronize synchronize,
+		IWebSocketConnectionsService connectionsService, IQuery query, IWebLogger logger,
+		INotificationQuery notificationQuery)
 	{
-		private ISynchronize? _synchronize;
-		private AppSettings? _appSettings;
-		private IWebSocketConnectionsService? _connectionsService;
-		private IQuery? _query;
-		private IWebLogger? _logger;
-		private readonly IServiceScope? _serviceScope;
-		private TelemetryClient? _telemetryClient;
-		private INotificationQuery? _notificationQuery;
-
-		internal SyncWatcherConnector(AppSettings appSettings, ISynchronize synchronize, 
-			IWebSocketConnectionsService connectionsService, IQuery query, IWebLogger logger, 
-			INotificationQuery notificationQuery, TelemetryClient? telemetryClient)
-		{
-			_appSettings = appSettings;
-			_synchronize = synchronize;
-			_connectionsService = connectionsService;
-			_query = query;
-			_logger = logger;
-			_notificationQuery = notificationQuery;
-			_telemetryClient = telemetryClient;
-		}
+		_appSettings = appSettings;
+		_synchronize = synchronize;
+		_connectionsService = connectionsService;
+		_query = query;
+		_logger = logger;
+		_notificationQuery = notificationQuery;
+	}
 
-		public SyncWatcherConnector(IServiceScopeFactory scopeFactory)
-		{
-			_serviceScope = scopeFactory.CreateScope();
-		}
+	public SyncWatcherConnector(IServiceScopeFactory scopeFactory)
+	{
+		_serviceScope = scopeFactory.CreateScope();
+	}
+
+	internal bool InjectScopes()
+	{
+		if ( _serviceScope == null ) return false;
+		// ISynchronize is a scoped service
+		_synchronize = _serviceScope.ServiceProvider.GetRequiredService();
+		_appSettings = _serviceScope.ServiceProvider.GetRequiredService();
+		_connectionsService = _serviceScope.ServiceProvider
+			.GetRequiredService();
+		var query = _serviceScope.ServiceProvider.GetRequiredService();
+		_logger = _serviceScope.ServiceProvider.GetRequiredService();
+		var memoryCache = _serviceScope.ServiceProvider.GetService();
+		var serviceScopeFactory =
+			_serviceScope.ServiceProvider.GetService();
+		_query = new QueryFactory(new SetupDatabaseTypes(_appSettings), query,
+			memoryCache, _appSettings, serviceScopeFactory, _logger).Query();
+		_notificationQuery = _serviceScope.ServiceProvider
+			.GetService();
+		return true;
+	}
 
-		internal bool InjectScopes()
+	public Task> Sync(
+		Tuple watcherOutput)
+	{
+		// Avoid Disposed Query objects
+		if ( _serviceScope != null )
 		{
-			if ( _serviceScope == null ) return false;
-			// ISynchronize is a scoped service
-			_synchronize = _serviceScope.ServiceProvider.GetRequiredService();
-			_appSettings = _serviceScope.ServiceProvider.GetRequiredService();
-			_connectionsService = _serviceScope.ServiceProvider.GetRequiredService();
-			var query = _serviceScope.ServiceProvider.GetRequiredService();
-			_logger = _serviceScope.ServiceProvider.GetRequiredService();
-			var memoryCache = _serviceScope.ServiceProvider.GetService();
-			var serviceScopeFactory = _serviceScope.ServiceProvider.GetService();
-			_query = new QueryFactory(new SetupDatabaseTypes(_appSettings), query,
-				memoryCache, _appSettings, serviceScopeFactory, _logger).Query();
-			_notificationQuery = _serviceScope.ServiceProvider
-				.GetService();
-			_telemetryClient = _serviceScope.ServiceProvider
-				.GetService();
-			return true;
+			InjectScopes();
 		}
 
-		internal IOperationHolder CreateNewRequestTelemetry(string? fullFilePath = null)
+		if ( _synchronize == null || _logger == null || _appSettings == null ||
+			 _connectionsService == null || _query == null )
 		{
-			if (_telemetryClient == null || string.IsNullOrEmpty(_appSettings!
-				    .ApplicationInsightsConnectionString) )
-			{
-				return new EmptyOperationHolder();
-			}
-
-			var requestTelemetry = new RequestTelemetry {Name = "FSW " + nameof(SyncWatcherConnector) };
-			var operation = _telemetryClient.StartOperation(requestTelemetry);
-			operation.Telemetry.Timestamp = DateTimeOffset.UtcNow;
-			operation.Telemetry.Source = "FileSystem";
-			if ( !string.IsNullOrEmpty(fullFilePath) )
-			{
-				operation.Telemetry.Url = new Uri($"?f={fullFilePath}", UriKind.Relative);
-			}
-			new CloudRoleNameInitializer($"{_appSettings.ApplicationType}").Initialize(requestTelemetry);
-			return operation;
+			throw new ArgumentException(
+				"any of:  _synchronize, _logger, _appSettings, _connectionsService or" +
+				" _query should not be null");
 		}
 
-		internal bool EndRequestOperation(IOperationHolder operation, string statusCode)
+		return SyncTaskInternal(watcherOutput);
+	}
+
+	/// 
+	/// Internal sync connector task
+	/// 
+	/// data
+	/// Task with data
+	private async Task> SyncTaskInternal(
+		Tuple watcherOutput)
+	{
+		var (fullFilePath, toPath, type) = watcherOutput;
+
+		var syncData = new List();
+
+		_logger!.LogInformation(
+			$"[SyncWatcherConnector] [{fullFilePath}] - [{toPath}] - [{type}]");
+
+		if ( type == WatcherChangeTypes.Renamed && !string.IsNullOrEmpty(toPath) )
 		{
-			if ( _telemetryClient == null || string.IsNullOrEmpty(_appSettings!
-				    .ApplicationInsightsConnectionString) )
+			// from path sync
+			var path = _appSettings!.FullPathToDatabaseStyle(fullFilePath);
+			await _synchronize!.Sync(path);
+
+			syncData.Add(new FileIndexItem(_appSettings.FullPathToDatabaseStyle(fullFilePath))
 			{
-				return false;
-			}
-			
-			// end operation
-			operation.Telemetry.Success = true;
-			operation.Telemetry.Duration = DateTimeOffset.UtcNow - operation.Telemetry.Timestamp;
-			operation.Telemetry.ResponseCode = statusCode;
-			_telemetryClient.StopOperation(operation);
-			return true;
-		}
+				IsDirectory = true,
+				Status = FileIndexItem.ExifStatus.NotFoundSourceMissing
+			});
 
-		public Task> Sync(
-			Tuple watcherOutput)
+			// and now to-path sync
+			var pathToDatabaseStyle = _appSettings.FullPathToDatabaseStyle(toPath);
+			syncData.AddRange(await _synchronize.Sync(pathToDatabaseStyle));
+		}
+		else
 		{
-			// Avoid Disposed Query objects
-			if ( _serviceScope != null )
-			{
-				InjectScopes();
-			}
-			
-			if ( _synchronize == null || _logger == null || _appSettings == null || _connectionsService == null || _query == null)
-			{
-				throw new ArgumentException("any of:  _synchronize, _logger, _appSettings, _connectionsService or" +
-				                            " _query should not be null");
-			}
-			
-			return SyncTaskInternal(watcherOutput);
+			syncData =
+				await _synchronize!.Sync(_appSettings!.FullPathToDatabaseStyle(fullFilePath));
 		}
 
-		/// 
-		/// Internal sync connector task
-		/// 
-		/// data
-		/// Task with data
-		private async Task> SyncTaskInternal(Tuple watcherOutput)
+		var filtered = FilterBefore(syncData);
+		if ( filtered.Count == 0 )
 		{
-			var (fullFilePath, toPath, type ) = watcherOutput;
-			var operation = CreateNewRequestTelemetry(fullFilePath);
-
-			var syncData = new List();
-			
-			_logger!.LogInformation($"[SyncWatcherConnector] [{fullFilePath}] - [{toPath}] - [{type}]");
-			
-			if ( type == WatcherChangeTypes.Renamed && !string.IsNullOrEmpty(toPath))
-			{
-				// from path sync
-				var path = _appSettings!.FullPathToDatabaseStyle(fullFilePath);
-				await _synchronize!.Sync(path); 
-				
-				syncData.Add(new FileIndexItem(_appSettings.FullPathToDatabaseStyle(fullFilePath))
-				{
-					IsDirectory = true, 
-					Status = FileIndexItem.ExifStatus.NotFoundSourceMissing
-				});
-				
-				// and now to-path sync
-				var pathToDatabaseStyle = _appSettings.FullPathToDatabaseStyle(toPath);
-				syncData.AddRange(await _synchronize.Sync(pathToDatabaseStyle));
-			}
-			else
-			{
-				syncData = await _synchronize!.Sync(_appSettings!.FullPathToDatabaseStyle(fullFilePath));
-			}
-
-			var filtered = FilterBefore(syncData);
-			if ( filtered.Count == 0 )
-			{
-				_logger.LogInformation($"[SyncWatcherConnector/EndOperation] " +
-				                       $"f:{filtered.Count}/s:{syncData.Count} ~ skip: "+ 
-	                       string.Join(", ", syncData.Select(p => p.FileName).ToArray()) + " ~ " +
-	                                         string.Join(", ", syncData.Select(p => p.Status).ToArray()));
-				EndRequestOperation(operation, string.Join(", ", syncData.Select(p => p.Status).ToArray()));
-				return syncData;
-			}
-
-			await PushToSockets(filtered);
-			
-			// And update the query Cache
-			_query!.CacheUpdateItem(filtered.Where(p => p.Status == FileIndexItem.ExifStatus.Ok ||
-				p.Status == FileIndexItem.ExifStatus.Deleted).ToList());
-			
-			// remove files that are not in the index from cache
-			_query.RemoveCacheItem(filtered.
-				Where(p => p.Status is 
-					FileIndexItem.ExifStatus.NotFoundNotInIndex or FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList());
-
-			if ( _serviceScope != null ) await _query.DisposeAsync();
-			EndRequestOperation(operation, "OK");
-			
+			_logger.LogInformation($"[SyncWatcherConnector/EndOperation] " +
+								   $"f:{filtered.Count}/s:{syncData.Count} ~ skip: " +
+								   string.Join(", ",
+									   syncData.Select(p => p.FileName).ToArray()) + " ~ " +
+								   string.Join(", ", syncData.Select(p => p.Status).ToArray()));
 			return syncData;
 		}
 
-		/// 
-		/// Both websockets and NotificationAPI
-		/// update users who are active right now
-		/// 
-		/// list of messages to push
-		private async Task PushToSockets(List filtered)
+		await PushToSockets(filtered);
+
+		// And update the query Cache
+		_query!.CacheUpdateItem(filtered.Where(p => p.Status == FileIndexItem.ExifStatus.Ok ||
+													p.Status == FileIndexItem.ExifStatus
+														.Deleted).ToList());
+
+		// remove files that are not in the index from cache
+		_query.RemoveCacheItem(filtered.Where(p => p.Status is
+			FileIndexItem.ExifStatus.NotFoundNotInIndex
+			or FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList());
+
+		if ( _serviceScope != null )
 		{
-			_logger!.LogInformation("[SyncWatcherConnector/Socket] "+ 
-			                        string.Join(", ", filtered.Select(p => p.FilePath).ToArray()));
-			
-			var webSocketResponse =
-				new ApiNotificationResponseModel>(filtered, ApiNotificationType.SyncWatcherConnector);
-			await _connectionsService!.SendToAllAsync(JsonSerializer.Serialize(webSocketResponse,
-				DefaultJsonSerializer.CamelCaseNoEnters), CancellationToken.None);
-			await _notificationQuery!.AddNotification(webSocketResponse);
+			await _query.DisposeAsync();
 		}
 
-		internal static List FilterBefore(IReadOnlyCollection syncData)
-		{
-			// also remove duplicates from output list
-			return syncData.GroupBy(x => x.FilePath).
-				Select(x => x.First())
-				.Where(p =>
-				p.Status is FileIndexItem.ExifStatus.Ok or 
-					FileIndexItem.ExifStatus.Deleted or 
-					FileIndexItem.ExifStatus.NotFoundNotInIndex or 
+		return syncData;
+	}
+
+	/// 
+	/// Both websockets and NotificationAPI
+	/// update users who are active right now
+	/// 
+	/// list of messages to push
+	private async Task PushToSockets(List filtered)
+	{
+		_logger!.LogInformation("[SyncWatcherConnector/Socket] " +
+								string.Join(", ", filtered.Select(p => p.FilePath).ToArray()));
+
+		var webSocketResponse =
+			new ApiNotificationResponseModel>(filtered,
+				ApiNotificationType.SyncWatcherConnector);
+		await _connectionsService!.SendToAllAsync(JsonSerializer.Serialize(webSocketResponse,
+			DefaultJsonSerializer.CamelCaseNoEnters), CancellationToken.None);
+		await _notificationQuery!.AddNotification(webSocketResponse);
+	}
+
+	internal static List FilterBefore(
+		IReadOnlyCollection syncData)
+	{
+		// also remove duplicates from output list
+		return syncData.GroupBy(x => x.FilePath).Select(x => x.First())
+			.Where(p =>
+				p.Status is FileIndexItem.ExifStatus.Ok or
+					FileIndexItem.ExifStatus.Deleted or
+					FileIndexItem.ExifStatus.NotFoundNotInIndex or
 					FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList();
-		}
 	}
 }
diff --git a/starsky/starsky.foundation.sync/WatcherInterfaces/IFileSystemWatcherWrapper.cs b/starsky/starsky.foundation.sync/WatcherInterfaces/IFileSystemWatcherWrapper.cs
index 5fd5e10c4f..6dd909e362 100644
--- a/starsky/starsky.foundation.sync/WatcherInterfaces/IFileSystemWatcherWrapper.cs
+++ b/starsky/starsky.foundation.sync/WatcherInterfaces/IFileSystemWatcherWrapper.cs
@@ -8,7 +8,7 @@ namespace starsky.foundation.sync.WatcherInterfaces
 	/// @see: https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=netcore-3.1
 	/// @see: https://stackoverflow.com/a/50948255/8613589
 	/// 
-	public interface IFileSystemWatcherWrapper:  IDisposable
+	public interface IFileSystemWatcherWrapper : IDisposable
 	{
 		event FileSystemEventHandler Created;
 		event FileSystemEventHandler Deleted;
diff --git a/starsky/starsky.foundation.sync/WatcherServices/BufferingFileSystemWatcher.cs b/starsky/starsky.foundation.sync/WatcherServices/BufferingFileSystemWatcher.cs
index 6e5e113f21..dcf5e19930 100644
--- a/starsky/starsky.foundation.sync/WatcherServices/BufferingFileSystemWatcher.cs
+++ b/starsky/starsky.foundation.sync/WatcherServices/BufferingFileSystemWatcher.cs
@@ -20,438 +20,438 @@ namespace starsky.foundation.sync.WatcherServices
 	[Service(typeof(IFileSystemWatcherWrapper), InjectionLifetime = InjectionLifetime.Singleton)]
 	[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
 	public sealed class BufferingFileSystemWatcher : Component, IFileSystemWatcherWrapper
-    {
-        private readonly FileSystemWatcher _containedFsw;
-
-        private FileSystemEventHandler? _onExistedHandler = null;
-        private FileSystemEventHandler? _onAllChangesHandler = null;
-
-        private FileSystemEventHandler? _onCreatedHandler = null;
-        private FileSystemEventHandler? _onChangedHandler = null;
-        private FileSystemEventHandler? _onDeletedHandler = null;
-        private RenamedEventHandler? _onRenamedHandler = null;
-
-        private ErrorEventHandler? _onErrorHandler = null;
-
-        //We use a single buffer for all change types. Alternatively we could use one buffer per event type, costing additional enumerate tasks.
-        private BlockingCollection _fileSystemEventBuffer = new BlockingCollection();
-        
-        private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-
-        
-        /// 
-        /// Features:
-        /// - Buffers FileSystemWatcher events in a BlockinCollection to prevent InternalBufferOverflowExceptions.
-        /// - Does not break the original FileSystemWatcher API.
-        /// - Supports reporting existing files via a new Existed event.
-        /// - Supports sorting events by oldest (existing) file first.
-        /// - Supports an new event Any reporting any FSW change.
-        /// - Offers the Error event in Win Forms designer (via [Browsable[true)]
-        /// - Does not prevent duplicate files occuring.
-        /// Notes:
-        ///   We contain FilSystemWatcher to follow the prinicple composition over inheritance
-        ///   and because System.IO.FileSystemWatcher is not designed to be inherited from:
-        ///   Event handlers and Dispose(disposing) are not virtual.
-        /// 
-        public BufferingFileSystemWatcher()
-        {
-	        _containedFsw = new FileSystemWatcher();
-        }
-
-        public BufferingFileSystemWatcher(string path)
-        {
-	        _containedFsw = new FileSystemWatcher(path, "*.*");
-        }
-        
-        public BufferingFileSystemWatcher(string path, string filter)
-        {
-	        _containedFsw = new FileSystemWatcher(path, filter);
-        }
-        
-        public BufferingFileSystemWatcher(FileSystemWatcher fileSystemWatcher)
-        {
-	        _containedFsw = fileSystemWatcher;
-        }
-
-        public bool EnableRaisingEvents
-        {
-            get
-            {
-                return _containedFsw.EnableRaisingEvents;
-            }
-            set
-            {
-                if (_containedFsw.EnableRaisingEvents == value) return;
-
-                StopRaisingBufferedEvents();
-                _cancellationTokenSource = new CancellationTokenSource();
-
-                //We EnableRaisingEvents, before NotifyExistingFiles
-                //  to prevent missing any events
-                //  accepting more duplicates (which may occure anyway).
-                _containedFsw.EnableRaisingEvents = value;
-                if (value)
-                    RaiseBufferedEventsUntilCancelled();
-            }
-        }
-
-        public string Filter
-        {
-            get { return _containedFsw.Filter; }
-            set { _containedFsw.Filter = value; }
-        }
-
-        public bool IncludeSubdirectories
-        {
-            get { return _containedFsw.IncludeSubdirectories; }
-            set { _containedFsw.IncludeSubdirectories = value; }
-        }
-
-        public int InternalBufferSize
-        {
-            get { return _containedFsw.InternalBufferSize; }
-            set { _containedFsw.InternalBufferSize = value; }
-        }
-
-        public NotifyFilters NotifyFilter
-        {
-            get { return _containedFsw.NotifyFilter; }
-            set => _containedFsw.NotifyFilter = value;
-        }
-
-        public string Path
-        {
-            get { return _containedFsw.Path; }
-            set { _containedFsw.Path = value; }
-        }
-
-        public ISynchronizeInvoke? SynchronizingObject
-        {
-            get { return _containedFsw.SynchronizingObject; }
-            set { _containedFsw.SynchronizingObject = value; }
-        }
-
-        public override ISite? Site
-        {
-            get { return _containedFsw.Site; }
-            set { _containedFsw.Site = value; }
-        }
-
-        [DefaultValue(false)]
-        public bool OrderByOldestFirst { get; set; } = false;
-
-        public int EventQueueCapacity { get; set;  } = int.MaxValue;
-
-        // New BufferingFileSystemWatcher specific events
-        public event FileSystemEventHandler Existed
-        {
-            add
-            {
-                _onExistedHandler += value;
-            }
-            remove
-            {
-                _onExistedHandler -= value;
-            }
-        }
-
-        public event FileSystemEventHandler All
-        {
-            add
-            {
-                if (_onAllChangesHandler == null)
-                {
-                    _containedFsw.Created += BufferEvent;
-                    _containedFsw.Changed += BufferEvent;
-                    _containedFsw.Renamed += BufferEvent;
-                    _containedFsw.Deleted += BufferEvent;
-                }
-                _onAllChangesHandler += value;
-            }
-            remove
-            {
-                _containedFsw.Created -= BufferEvent;
-                _containedFsw.Changed -= BufferEvent;
-                _containedFsw.Renamed -= BufferEvent;
-                _containedFsw.Deleted -= BufferEvent;
-                _onAllChangesHandler -= value;
-            }
-        }
-
-
-        // region Standard FSW events
-        
-        //- The _fsw events add to the buffer.
-        //- The public events raise from the buffer to the consumer.
-        public event FileSystemEventHandler Created
-        {
-            add
-            {
-                if (_onCreatedHandler == null)
-                    _containedFsw.Created += BufferEvent;
-                _onCreatedHandler += value;
-            }
-            remove
-            {
-                _containedFsw.Created -= BufferEvent;
-                _onCreatedHandler -= value;
-            }
-        }
-
-        public event FileSystemEventHandler Changed
-        {
-            add
-            {
-                if (_onChangedHandler == null)
-                    _containedFsw.Changed += BufferEvent;
-                _onChangedHandler += value;
-            }
-            remove
-            {
-                _containedFsw.Changed -= BufferEvent;
-                _onChangedHandler -= value;
-            }
-        }
-
-        public event FileSystemEventHandler Deleted
-        {
-            add
-            {
-                if (_onDeletedHandler == null)
-                    _containedFsw.Deleted += BufferEvent;
-                _onDeletedHandler += value;
-            }
-            remove
-            {
-                _containedFsw.Deleted -= BufferEvent;
-                _onDeletedHandler -= value;
-            }
-        }
-
-        public event RenamedEventHandler Renamed
-        {
-            add
-            {
-                if (_onRenamedHandler == null)
-                    _containedFsw.Renamed += BufferEvent;
-                _onRenamedHandler += value;
-            }
-            remove
-            {
-                _containedFsw.Renamed -= BufferEvent;
-                _onRenamedHandler -= value;
-            }
-        }
-
-        internal void BufferEvent(object _, FileSystemEventArgs e)
-        {
-	        if ( _fileSystemEventBuffer.TryAdd(e) ) return;
-	        var ex = new EventQueueOverflowException($"Event queue size {_fileSystemEventBuffer.BoundedCapacity} events exceeded.");
-	        InvokeHandler(_onErrorHandler!, new ErrorEventArgs(ex));
-        }
-
-        internal void StopRaisingBufferedEvents(object? _ = null, EventArgs? __ = null)
-        {
-            _cancellationTokenSource.Cancel();
-            _cancellationTokenSource.Dispose();
-            _fileSystemEventBuffer = new BlockingCollection(EventQueueCapacity);
-        }
-
-        public event ErrorEventHandler Error
-        {
-            add
-            {
-                if (_onErrorHandler == null)
-                    _containedFsw.Error += BufferingFileSystemWatcher_Error;
-                _onErrorHandler += value;
-            }
-            remove
-            {
-                if (_onErrorHandler == null)
-                    _containedFsw.Error -= BufferingFileSystemWatcher_Error;
-                _onErrorHandler -= value;
-            }
-        }
-
-        internal void BufferingFileSystemWatcher_Error(object sender, ErrorEventArgs e)
-        {
-            InvokeHandler(_onErrorHandler!, e);
-        }
-        
-        // end standard events
-
-        internal WatcherChangeTypes? RaiseBufferedEventsUntilCancelledInLoop(
-	        FileSystemEventArgs fileSystemEventArgs)
-        {
-	        if (_onAllChangesHandler != null)
-		        InvokeHandler(_onAllChangesHandler, fileSystemEventArgs);
-	        else
-	        {
-		        switch (fileSystemEventArgs.ChangeType)
-		        {
-			        case WatcherChangeTypes.Created:
-				        InvokeHandler(_onCreatedHandler!, fileSystemEventArgs);
-				        break;
-			        case WatcherChangeTypes.Changed:
-				        InvokeHandler(_onChangedHandler!, fileSystemEventArgs);
-				        break;
-			        case WatcherChangeTypes.Deleted:
-				        InvokeHandler(_onDeletedHandler!, fileSystemEventArgs);
-				        break;
-			        case WatcherChangeTypes.Renamed:
-				        InvokeHandler(_onRenamedHandler!, fileSystemEventArgs as RenamedEventArgs);
-				        break;
-		        }
-		        return fileSystemEventArgs.ChangeType;
-	        }
-
-	        return null;
-        }
-
-        private void RaiseBufferedEventsUntilCancelled()
-        {
-            Task.Run(() =>
-            {
-	            try
-	            {
-		            if ( _onExistedHandler != null ||
-		                 _onAllChangesHandler != null )
-			            NotifyExistingFiles();
-
-		            foreach ( FileSystemEventArgs fileSystemEventArgs in
-		                     _fileSystemEventBuffer.GetConsumingEnumerable(
-			                     _cancellationTokenSource.Token) )
-		            {
-			            RaiseBufferedEventsUntilCancelledInLoop(
-				            fileSystemEventArgs);
-		            }
-	            }
-	            catch ( OperationCanceledException )
-	            {
-		            // ignore
-	            } 
-                catch (Exception ex)
-                {
-                    BufferingFileSystemWatcher_Error(this, new ErrorEventArgs(ex));
-                }
-            });
-        }
-
-        internal void NotifyExistingFiles()
-        {
-            var searchSubDirectoriesOption = (IncludeSubdirectories) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-            if (OrderByOldestFirst)
-            {
-                var sortedFileInfos = from fi in new DirectoryInfo(Path).GetFiles(Filter, searchSubDirectoriesOption)
-                                      orderby fi.LastWriteTime ascending
-                                      select fi;
-                foreach (var fi in sortedFileInfos)
-                {
-	                if ( fi.DirectoryName == null )
-	                {
-		                continue;
-	                }
-                    InvokeHandler(_onExistedHandler, new FileSystemEventArgs(WatcherChangeTypes.All, fi.DirectoryName, fi.Name));
-                    InvokeHandler(_onAllChangesHandler, new FileSystemEventArgs(WatcherChangeTypes.All,fi.DirectoryName, fi.Name));
-                }
-            }
-            else
-            {
-                foreach (var fsi in new DirectoryInfo(Path).EnumerateFileSystemInfos(Filter, searchSubDirectoriesOption))
-                {
-	                var directoryName =
-		                System.IO.Path.GetDirectoryName(fsi.FullName);
-	                if ( directoryName == null )
-	                {
-		                continue;
-	                }
-                    InvokeHandler(_onExistedHandler, new FileSystemEventArgs(WatcherChangeTypes.All, directoryName, fsi.Name ));
-                    InvokeHandler(_onAllChangesHandler, new FileSystemEventArgs(WatcherChangeTypes.All, directoryName, fsi.Name));
-                }
-            }
-        }
-
-        // InvokeHandlers
-        // Automatically raise event in calling thread when _fsw.SynchronizingObject is set. Ex: When used as a component in Win Forms.
-        //  remove redundancy. I don't understand how to cast the specific *EventHandler to a generic Delegate, EventHandler, Action or whatever.
-        internal bool? InvokeHandler(FileSystemEventHandler? eventHandler, FileSystemEventArgs e)
-        {
-	        if ( eventHandler == null )
-	        {
-		        return null;
-	        }
-	        
-	        if ( _containedFsw.SynchronizingObject != null && _containedFsw
-		            .SynchronizingObject.InvokeRequired )
-	        {
-		        _containedFsw.SynchronizingObject.BeginInvoke(eventHandler, new object[] { this, e });
-		        return true;
-	        }
-
-	        eventHandler(this, e);
-	        return false;
-        }
-        internal bool? InvokeHandler(RenamedEventHandler? eventHandler, RenamedEventArgs? e)
-        {
-	        if ( eventHandler == null)
-	        {
-		        return null;
-	        }
-	        
-	        ArgumentNullException.ThrowIfNull(e);
-
-	        if ( _containedFsw.SynchronizingObject is { InvokeRequired: true } )
-	        {
-		        _containedFsw.SynchronizingObject.BeginInvoke(eventHandler, new object[] { this, e });
-		        return true;
-	        }
-
-	        eventHandler(this, e);
-	        return false;
-        }
-        
-        internal bool? InvokeHandler(ErrorEventHandler? eventHandler, ErrorEventArgs e)
-        {
-	        if ( eventHandler == null )
-	        {
-		        return null;
-	        }
-	        
-	        if ( _containedFsw.SynchronizingObject != null && this._containedFsw
-		            .SynchronizingObject.InvokeRequired )
-	        {
-		        _containedFsw.SynchronizingObject.BeginInvoke(eventHandler, new object[] { this, e });
-		        return true;
-	        }
-
-            eventHandler(this, e);
-            return false;
-        }
-        // end InvokeHandlers
-
-        /// 
-        /// Status if is Disposed
-        /// 
-        internal bool IsDisposed { get; set; } = false;
-
-        /// 
-        /// Dispose and unsubscribe all events
-        /// 
-        /// is disposing
-        protected override void Dispose(bool disposing)
-        {
-            if (disposing)
-            {
-	            IsDisposed = true;
-	            if ( !_cancellationTokenSource.IsCancellationRequested )
-	            {
-		            _cancellationTokenSource.Cancel();
-	            }
-	            
-	            _cancellationTokenSource.Dispose();
-                _containedFsw.Dispose();
-            }
-            base.Dispose(disposing);
-        }
-    }
+	{
+		private readonly FileSystemWatcher _containedFsw;
+
+		private FileSystemEventHandler? _onExistedHandler = null;
+		private FileSystemEventHandler? _onAllChangesHandler = null;
+
+		private FileSystemEventHandler? _onCreatedHandler = null;
+		private FileSystemEventHandler? _onChangedHandler = null;
+		private FileSystemEventHandler? _onDeletedHandler = null;
+		private RenamedEventHandler? _onRenamedHandler = null;
+
+		private ErrorEventHandler? _onErrorHandler = null;
+
+		//We use a single buffer for all change types. Alternatively we could use one buffer per event type, costing additional enumerate tasks.
+		private BlockingCollection _fileSystemEventBuffer = new BlockingCollection();
+
+		private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+
+		/// 
+		/// Features:
+		/// - Buffers FileSystemWatcher events in a BlockinCollection to prevent InternalBufferOverflowExceptions.
+		/// - Does not break the original FileSystemWatcher API.
+		/// - Supports reporting existing files via a new Existed event.
+		/// - Supports sorting events by oldest (existing) file first.
+		/// - Supports an new event Any reporting any FSW change.
+		/// - Offers the Error event in Win Forms designer (via [Browsable[true)]
+		/// - Does not prevent duplicate files occuring.
+		/// Notes:
+		///   We contain FilSystemWatcher to follow the prinicple composition over inheritance
+		///   and because System.IO.FileSystemWatcher is not designed to be inherited from:
+		///   Event handlers and Dispose(disposing) are not virtual.
+		/// 
+		public BufferingFileSystemWatcher()
+		{
+			_containedFsw = new FileSystemWatcher();
+		}
+
+		public BufferingFileSystemWatcher(string path)
+		{
+			_containedFsw = new FileSystemWatcher(path, "*.*");
+		}
+
+		public BufferingFileSystemWatcher(string path, string filter)
+		{
+			_containedFsw = new FileSystemWatcher(path, filter);
+		}
+
+		public BufferingFileSystemWatcher(FileSystemWatcher fileSystemWatcher)
+		{
+			_containedFsw = fileSystemWatcher;
+		}
+
+		public bool EnableRaisingEvents
+		{
+			get
+			{
+				return _containedFsw.EnableRaisingEvents;
+			}
+			set
+			{
+				if ( _containedFsw.EnableRaisingEvents == value ) return;
+
+				StopRaisingBufferedEvents();
+				_cancellationTokenSource = new CancellationTokenSource();
+
+				//We EnableRaisingEvents, before NotifyExistingFiles
+				//  to prevent missing any events
+				//  accepting more duplicates (which may occure anyway).
+				_containedFsw.EnableRaisingEvents = value;
+				if ( value )
+					RaiseBufferedEventsUntilCancelled();
+			}
+		}
+
+		public string Filter
+		{
+			get { return _containedFsw.Filter; }
+			set { _containedFsw.Filter = value; }
+		}
+
+		public bool IncludeSubdirectories
+		{
+			get { return _containedFsw.IncludeSubdirectories; }
+			set { _containedFsw.IncludeSubdirectories = value; }
+		}
+
+		public int InternalBufferSize
+		{
+			get { return _containedFsw.InternalBufferSize; }
+			set { _containedFsw.InternalBufferSize = value; }
+		}
+
+		public NotifyFilters NotifyFilter
+		{
+			get { return _containedFsw.NotifyFilter; }
+			set => _containedFsw.NotifyFilter = value;
+		}
+
+		public string Path
+		{
+			get { return _containedFsw.Path; }
+			set { _containedFsw.Path = value; }
+		}
+
+		public ISynchronizeInvoke? SynchronizingObject
+		{
+			get { return _containedFsw.SynchronizingObject; }
+			set { _containedFsw.SynchronizingObject = value; }
+		}
+
+		public override ISite? Site
+		{
+			get { return _containedFsw.Site; }
+			set { _containedFsw.Site = value; }
+		}
+
+		[DefaultValue(false)]
+		public bool OrderByOldestFirst { get; set; } = false;
+
+		public int EventQueueCapacity { get; set; } = int.MaxValue;
+
+		// New BufferingFileSystemWatcher specific events
+		public event FileSystemEventHandler Existed
+		{
+			add
+			{
+				_onExistedHandler += value;
+			}
+			remove
+			{
+				_onExistedHandler -= value;
+			}
+		}
+
+		public event FileSystemEventHandler All
+		{
+			add
+			{
+				if ( _onAllChangesHandler == null )
+				{
+					_containedFsw.Created += BufferEvent;
+					_containedFsw.Changed += BufferEvent;
+					_containedFsw.Renamed += BufferEvent;
+					_containedFsw.Deleted += BufferEvent;
+				}
+				_onAllChangesHandler += value;
+			}
+			remove
+			{
+				_containedFsw.Created -= BufferEvent;
+				_containedFsw.Changed -= BufferEvent;
+				_containedFsw.Renamed -= BufferEvent;
+				_containedFsw.Deleted -= BufferEvent;
+				_onAllChangesHandler -= value;
+			}
+		}
+
+
+		// region Standard FSW events
+
+		//- The _fsw events add to the buffer.
+		//- The public events raise from the buffer to the consumer.
+		public event FileSystemEventHandler Created
+		{
+			add
+			{
+				if ( _onCreatedHandler == null )
+					_containedFsw.Created += BufferEvent;
+				_onCreatedHandler += value;
+			}
+			remove
+			{
+				_containedFsw.Created -= BufferEvent;
+				_onCreatedHandler -= value;
+			}
+		}
+
+		public event FileSystemEventHandler Changed
+		{
+			add
+			{
+				if ( _onChangedHandler == null )
+					_containedFsw.Changed += BufferEvent;
+				_onChangedHandler += value;
+			}
+			remove
+			{
+				_containedFsw.Changed -= BufferEvent;
+				_onChangedHandler -= value;
+			}
+		}
+
+		public event FileSystemEventHandler Deleted
+		{
+			add
+			{
+				if ( _onDeletedHandler == null )
+					_containedFsw.Deleted += BufferEvent;
+				_onDeletedHandler += value;
+			}
+			remove
+			{
+				_containedFsw.Deleted -= BufferEvent;
+				_onDeletedHandler -= value;
+			}
+		}
+
+		public event RenamedEventHandler Renamed
+		{
+			add
+			{
+				if ( _onRenamedHandler == null )
+					_containedFsw.Renamed += BufferEvent;
+				_onRenamedHandler += value;
+			}
+			remove
+			{
+				_containedFsw.Renamed -= BufferEvent;
+				_onRenamedHandler -= value;
+			}
+		}
+
+		internal void BufferEvent(object _, FileSystemEventArgs e)
+		{
+			if ( _fileSystemEventBuffer.TryAdd(e) ) return;
+			var ex = new EventQueueOverflowException($"Event queue size {_fileSystemEventBuffer.BoundedCapacity} events exceeded.");
+			InvokeHandler(_onErrorHandler!, new ErrorEventArgs(ex));
+		}
+
+		internal void StopRaisingBufferedEvents(object? _ = null, EventArgs? __ = null)
+		{
+			_cancellationTokenSource.Cancel();
+			_cancellationTokenSource.Dispose();
+			_fileSystemEventBuffer = new BlockingCollection(EventQueueCapacity);
+		}
+
+		public event ErrorEventHandler Error
+		{
+			add
+			{
+				if ( _onErrorHandler == null )
+					_containedFsw.Error += BufferingFileSystemWatcher_Error;
+				_onErrorHandler += value;
+			}
+			remove
+			{
+				if ( _onErrorHandler == null )
+					_containedFsw.Error -= BufferingFileSystemWatcher_Error;
+				_onErrorHandler -= value;
+			}
+		}
+
+		internal void BufferingFileSystemWatcher_Error(object sender, ErrorEventArgs e)
+		{
+			InvokeHandler(_onErrorHandler!, e);
+		}
+
+		// end standard events
+
+		internal WatcherChangeTypes? RaiseBufferedEventsUntilCancelledInLoop(
+			FileSystemEventArgs fileSystemEventArgs)
+		{
+			if ( _onAllChangesHandler != null )
+				InvokeHandler(_onAllChangesHandler, fileSystemEventArgs);
+			else
+			{
+				switch ( fileSystemEventArgs.ChangeType )
+				{
+					case WatcherChangeTypes.Created:
+						InvokeHandler(_onCreatedHandler!, fileSystemEventArgs);
+						break;
+					case WatcherChangeTypes.Changed:
+						InvokeHandler(_onChangedHandler!, fileSystemEventArgs);
+						break;
+					case WatcherChangeTypes.Deleted:
+						InvokeHandler(_onDeletedHandler!, fileSystemEventArgs);
+						break;
+					case WatcherChangeTypes.Renamed:
+						InvokeHandler(_onRenamedHandler!, fileSystemEventArgs as RenamedEventArgs);
+						break;
+				}
+				return fileSystemEventArgs.ChangeType;
+			}
+
+			return null;
+		}
+
+		private void RaiseBufferedEventsUntilCancelled()
+		{
+			Task.Run(() =>
+			{
+				try
+				{
+					if ( _onExistedHandler != null ||
+						 _onAllChangesHandler != null )
+						NotifyExistingFiles();
+
+					foreach ( FileSystemEventArgs fileSystemEventArgs in
+							 _fileSystemEventBuffer.GetConsumingEnumerable(
+								 _cancellationTokenSource.Token) )
+					{
+						RaiseBufferedEventsUntilCancelledInLoop(
+							fileSystemEventArgs);
+					}
+				}
+				catch ( OperationCanceledException )
+				{
+					// ignore
+				}
+				catch ( Exception ex )
+				{
+					BufferingFileSystemWatcher_Error(this, new ErrorEventArgs(ex));
+				}
+			});
+		}
+
+		internal void NotifyExistingFiles()
+		{
+			var searchSubDirectoriesOption = ( IncludeSubdirectories ) ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+			if ( OrderByOldestFirst )
+			{
+				var sortedFileInfos = from fi in new DirectoryInfo(Path).GetFiles(Filter, searchSubDirectoriesOption)
+									  orderby fi.LastWriteTime ascending
+									  select fi;
+				foreach ( var fi in sortedFileInfos )
+				{
+					if ( fi.DirectoryName == null )
+					{
+						continue;
+					}
+					InvokeHandler(_onExistedHandler, new FileSystemEventArgs(WatcherChangeTypes.All, fi.DirectoryName, fi.Name));
+					InvokeHandler(_onAllChangesHandler, new FileSystemEventArgs(WatcherChangeTypes.All, fi.DirectoryName, fi.Name));
+				}
+			}
+			else
+			{
+				foreach ( var fsi in new DirectoryInfo(Path).EnumerateFileSystemInfos(Filter, searchSubDirectoriesOption) )
+				{
+					var directoryName =
+						System.IO.Path.GetDirectoryName(fsi.FullName);
+					if ( directoryName == null )
+					{
+						continue;
+					}
+					InvokeHandler(_onExistedHandler, new FileSystemEventArgs(WatcherChangeTypes.All, directoryName, fsi.Name));
+					InvokeHandler(_onAllChangesHandler, new FileSystemEventArgs(WatcherChangeTypes.All, directoryName, fsi.Name));
+				}
+			}
+		}
+
+		// InvokeHandlers
+		// Automatically raise event in calling thread when _fsw.SynchronizingObject is set. Ex: When used as a component in Win Forms.
+		//  remove redundancy. I don't understand how to cast the specific *EventHandler to a generic Delegate, EventHandler, Action or whatever.
+		internal bool? InvokeHandler(FileSystemEventHandler? eventHandler, FileSystemEventArgs e)
+		{
+			if ( eventHandler == null )
+			{
+				return null;
+			}
+
+			if ( _containedFsw.SynchronizingObject != null && _containedFsw
+					.SynchronizingObject.InvokeRequired )
+			{
+				_containedFsw.SynchronizingObject.BeginInvoke(eventHandler, new object[] { this, e });
+				return true;
+			}
+
+			eventHandler(this, e);
+			return false;
+		}
+		internal bool? InvokeHandler(RenamedEventHandler? eventHandler, RenamedEventArgs? e)
+		{
+			if ( eventHandler == null )
+			{
+				return null;
+			}
+
+			ArgumentNullException.ThrowIfNull(e);
+
+			if ( _containedFsw.SynchronizingObject is { InvokeRequired: true } )
+			{
+				_containedFsw.SynchronizingObject.BeginInvoke(eventHandler, new object[] { this, e });
+				return true;
+			}
+
+			eventHandler(this, e);
+			return false;
+		}
+
+		internal bool? InvokeHandler(ErrorEventHandler? eventHandler, ErrorEventArgs e)
+		{
+			if ( eventHandler == null )
+			{
+				return null;
+			}
+
+			if ( _containedFsw.SynchronizingObject != null && this._containedFsw
+					.SynchronizingObject.InvokeRequired )
+			{
+				_containedFsw.SynchronizingObject.BeginInvoke(eventHandler, new object[] { this, e });
+				return true;
+			}
+
+			eventHandler(this, e);
+			return false;
+		}
+		// end InvokeHandlers
+
+		/// 
+		/// Status if is Disposed
+		/// 
+		internal bool IsDisposed { get; set; } = false;
+
+		/// 
+		/// Dispose and unsubscribe all events
+		/// 
+		/// is disposing
+		protected override void Dispose(bool disposing)
+		{
+			if ( disposing )
+			{
+				IsDisposed = true;
+				if ( !_cancellationTokenSource.IsCancellationRequested )
+				{
+					_cancellationTokenSource.Cancel();
+				}
+
+				_cancellationTokenSource.Dispose();
+				_containedFsw.Dispose();
+			}
+			base.Dispose(disposing);
+		}
+	}
 }
diff --git a/starsky/starsky.foundation.sync/WatcherServices/DiskWatcher.cs b/starsky/starsky.foundation.sync/WatcherServices/DiskWatcher.cs
index 8397532bb4..bccd0b85d8 100644
--- a/starsky/starsky.foundation.sync/WatcherServices/DiskWatcher.cs
+++ b/starsky/starsky.foundation.sync/WatcherServices/DiskWatcher.cs
@@ -2,7 +2,6 @@
 using System.Globalization;
 using System.IO;
 using System.Runtime.CompilerServices;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.DependencyInjection;
 using starsky.foundation.injection;
 using starsky.foundation.platform.Helpers;
@@ -11,6 +10,7 @@
 using starsky.foundation.sync.WatcherInterfaces;
 
 [assembly: InternalsVisibleTo("starskytest")]
+
 namespace starsky.foundation.sync.WatcherServices
 {
 	/// 
@@ -29,7 +29,8 @@ public DiskWatcher(IFileSystemWatcherWrapper fileSystemWatcherWrapper,
 			_fileSystemWatcherWrapper = fileSystemWatcherWrapper;
 			var serviceProvider = scopeFactory.CreateScope().ServiceProvider;
 			_webLogger = serviceProvider.GetRequiredService();
-			_queueProcessor = new QueueProcessor(scopeFactory, new SyncWatcherConnector(scopeFactory).Sync);
+			_queueProcessor =
+				new QueueProcessor(scopeFactory, new SyncWatcherConnector(scopeFactory).Sync);
 		}
 
 		internal DiskWatcher(
@@ -48,61 +49,64 @@ public void Watcher(string fullFilePath)
 		{
 			if ( !Directory.Exists(fullFilePath) )
 			{
-				_webLogger.LogError($"[DiskWatcher] FAIL can't find directory: {fullFilePath} so watcher is not started");
+				_webLogger.LogError(
+					$"[DiskWatcher] FAIL can't find directory: {fullFilePath} so watcher is not started");
 				return;
 			}
-			
+
 			_webLogger.LogInformation($"[DiskWatcher] started {fullFilePath}" +
-			        $"{DateTimeDebug()}");
-			
+									  $"{DateTimeDebug()}");
+
 			// Create a new FileSystemWatcher and set its properties.
 
 			_fileSystemWatcherWrapper.Path = fullFilePath;
 			_fileSystemWatcherWrapper.Filter = "*";
 			_fileSystemWatcherWrapper.IncludeSubdirectories = true;
 			_fileSystemWatcherWrapper.NotifyFilter = NotifyFilters.FileName
-			                                         | NotifyFilters.DirectoryName
-			                                         | NotifyFilters.Attributes
-			                                         | NotifyFilters.Size
-			                                         | NotifyFilters.LastWrite
-			                                         | NotifyFilters.CreationTime
-			                                         | NotifyFilters.Security;
+													 | NotifyFilters.DirectoryName
+													 | NotifyFilters.Attributes
+													 | NotifyFilters.Size
+													 | NotifyFilters.LastWrite
+													 | NotifyFilters.CreationTime
+													 | NotifyFilters.Security;
 
 			// Watch for changes in LastAccess and LastWrite times, and
 			// the renaming of files or directories.
 
 			// Add event handlers.
-			
+
 			_fileSystemWatcherWrapper.Created += OnChanged;
 			_fileSystemWatcherWrapper.Changed += OnChanged;
 			_fileSystemWatcherWrapper.Deleted += OnChanged;
 			_fileSystemWatcherWrapper.Renamed += OnRenamed;
 			_fileSystemWatcherWrapper.Error += OnError;
-				
+
 			// Begin watching.
 			_fileSystemWatcherWrapper.EnableRaisingEvents = true;
 		}
-		
+
 		private void OnError(object source, ErrorEventArgs e)
 		{
 			//  Show that an error has been detected.
-			_webLogger.LogError(e.GetException(),"[DiskWatcher] The FileSystemWatcher has an error (catch-ed) - next: retry " +
-			                     $"{DateTimeDebug()}");
+			_webLogger.LogError(e.GetException(),
+				"[DiskWatcher] The FileSystemWatcher has an error (catch-ed) - next: retry " +
+				$"{DateTimeDebug()}");
 			_webLogger.LogError("[DiskWatcher] (catch-ed) " + e.GetException().Message);
-			
+
 			//  Give more information if the error is due to an internal buffer overflow.
-			if (e.GetException().GetType() == typeof(InternalBufferOverflowException))
+			if ( e.GetException().GetType() == typeof(InternalBufferOverflowException) )
 			{
 				//  This can happen if Windows is reporting many file system events quickly 
 				//  and internal buffer of the  FileSystemWatcher is not large enough to handle this
 				//  rate of events. The InternalBufferOverflowException error informs the application
 				//  that some of the file system events are being lost.
-				_webLogger.LogError(e.GetException(),"[DiskWatcher] The file system watcher experienced an internal buffer overflow ");
+				_webLogger.LogError(e.GetException(),
+					"[DiskWatcher] The file system watcher experienced an internal buffer overflow ");
 			}
 
 			// when test dont retry
 			if ( e.GetException().Message == "test" ) return;
-			
+
 			// When fail it should try it again
 			Retry(new BufferingFileSystemWatcher(_fileSystemWatcherWrapper.Path));
 		}
@@ -110,17 +114,18 @@ private void OnError(object source, ErrorEventArgs e)
 		/// 
 		/// @see: https://www.codeguru.com/dotnet/filesystemwatcher%EF%BF%BDwhy-does-it-stop-working/
 		/// 
-		internal bool Retry(IFileSystemWatcherWrapper fileSystemWatcherWrapper, int numberOfTries = 20, int milliSecondsTimeout = 5000)
+		internal bool Retry(IFileSystemWatcherWrapper fileSystemWatcherWrapper,
+			int numberOfTries = 20, int milliSecondsTimeout = 5000)
 		{
 			_webLogger.LogInformation("[DiskWatcher] next retry " +
-			        $"{DateTimeDebug()}");
+									  $"{DateTimeDebug()}");
 			var path = _fileSystemWatcherWrapper.Path;
 
 			_fileSystemWatcherWrapper.Dispose();
 			_fileSystemWatcherWrapper = fileSystemWatcherWrapper;
 
 			var i = 0;
-			while (!_fileSystemWatcherWrapper.EnableRaisingEvents && i < numberOfTries)
+			while ( !_fileSystemWatcherWrapper.EnableRaisingEvents && i < numberOfTries )
 			{
 				try
 				{
@@ -131,29 +136,32 @@ internal bool Retry(IFileSystemWatcherWrapper fileSystemWatcherWrapper, int numb
 					{
 						_webLogger.LogInformation("[DiskWatcher] I'm Back!");
 					}
+
 					return true;
 				}
 				catch
 				{
-					_webLogger.LogInformation($"[DiskWatcher] next retry {i} - wait for {milliSecondsTimeout}ms");
+					_webLogger.LogInformation(
+						$"[DiskWatcher] next retry {i} - wait for {milliSecondsTimeout}ms");
 					// Sleep for a bit; otherwise, it takes a bit of
 					// processor time
 					System.Threading.Thread.Sleep(milliSecondsTimeout);
 					i++;
 				}
 			}
+
 			_webLogger.LogError($"[DiskWatcher] Failed after {i} times - so stop trying");
 			return false;
 		}
 
 		private static string DateTimeDebug()
 		{
-			return ": " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", 
+			return ": " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss",
 				CultureInfo.InvariantCulture);
 		}
-		
+
 		// Define the event handlers.
-		
+
 		/// 
 		/// Specify what is done when a file is changed. e.FullPath
 		/// 
@@ -161,14 +169,16 @@ private static string DateTimeDebug()
 		/// 
 		internal void OnChanged(object source, FileSystemEventArgs e)
 		{
-			if ( e.FullPath.EndsWith(".tmp") || !ExtensionRolesHelper.IsExtensionSyncSupported(e.FullPath) )
+			if ( e.FullPath.EndsWith(".tmp") ||
+				 !ExtensionRolesHelper.IsExtensionSyncSupported(e.FullPath) )
 			{
 				return;
 			}
-			
+
 			_webLogger.LogDebug($"[DiskWatcher] " +
-			                    $"{e.FullPath} OnChanged ChangeType is: {e.ChangeType} " + DateTimeDebug());
-			
+								$"{e.FullPath} OnChanged ChangeType is: {e.ChangeType} " +
+								DateTimeDebug());
+
 			_queueProcessor.QueueInput(e.FullPath, null, e.ChangeType).ConfigureAwait(false);
 			// Specify what is done when a file is changed, created, or deleted.
 		}
@@ -193,34 +203,36 @@ internal void OnChanged(object source, FileSystemEventArgs e)
 		internal void OnRenamed(object source, RenamedEventArgs e)
 		{
 			_webLogger.LogInformation($"[DiskWatcher] {e.OldFullPath} OnRenamed to: {e.FullPath}" +
-			                          DateTimeDebug());
+									  DateTimeDebug());
 
 			var fileAttributes = GetFileAttributes(e.FullPath);
 			var isDirectory = fileAttributes == FileAttributes.Directory;
 
-			var isOldFullPathTempFile = e.OldFullPath.Contains(Path.DirectorySeparatorChar + "tmp.{")
-			                || e.OldFullPath.EndsWith(".tmp");
+			var isOldFullPathTempFile =
+				e.OldFullPath.Contains(Path.DirectorySeparatorChar + "tmp.{")
+				|| e.OldFullPath.EndsWith(".tmp");
 			var isNewFullPathTempFile = e.FullPath.Contains(Path.DirectorySeparatorChar + "tmp.{")
-			                            || e.FullPath.EndsWith(".tmp");
+										|| e.FullPath.EndsWith(".tmp");
 			if ( !isDirectory && isOldFullPathTempFile )
 			{
-				_queueProcessor.QueueInput(e.FullPath, null, WatcherChangeTypes.Created).ConfigureAwait(false);
+				_queueProcessor.QueueInput(e.FullPath, null, WatcherChangeTypes.Created)
+					.ConfigureAwait(false);
 				return;
 			}
-			
+
 			if ( !isDirectory && isNewFullPathTempFile )
 			{
 				return;
 			}
 
-			_queueProcessor.QueueInput(e.OldFullPath, e.FullPath, WatcherChangeTypes.Renamed).ConfigureAwait(false);
+			_queueProcessor.QueueInput(e.OldFullPath, e.FullPath, WatcherChangeTypes.Renamed)
+				.ConfigureAwait(false);
 		}
-		
+
 		public void Dispose()
 		{
 			_fileSystemWatcherWrapper.EnableRaisingEvents = false;
 			_fileSystemWatcherWrapper.Dispose();
 		}
-
 	}
 }
diff --git a/starsky/starsky.foundation.sync/WatcherServices/DiskWatcherBackgroundService.cs b/starsky/starsky.foundation.sync/WatcherServices/DiskWatcherBackgroundService.cs
index b0cb89bdbc..c6ddf820b4 100644
--- a/starsky/starsky.foundation.sync/WatcherServices/DiskWatcherBackgroundService.cs
+++ b/starsky/starsky.foundation.sync/WatcherServices/DiskWatcherBackgroundService.cs
@@ -25,7 +25,7 @@ public DiskWatcherBackgroundService(IDiskWatcher diskWatcher, AppSettings appSet
 			_appSettings = appSettings;
 			_logger = logger;
 		}
-		
+
 		/// 
 		/// This method is triggered by BackgroundService
 		/// 
@@ -33,10 +33,10 @@ public DiskWatcherBackgroundService(IDiskWatcher diskWatcher, AppSettings appSet
 		/// Task/nothing
 		protected override Task ExecuteAsync(CancellationToken stoppingToken)
 		{
-			_logger.LogInformation((_appSettings.UseDiskWatcher != false ? $"UseDiskWatcher is enabled" 
+			_logger.LogInformation(( _appSettings.UseDiskWatcher != false ? $"UseDiskWatcher is enabled"
 				: "UseDiskWatcher is disabled" ) + $" on {Environment.MachineName}");
 			if ( _appSettings.UseDiskWatcher == false
-			     && _appSettings.ApplicationType == AppSettings.StarskyAppType.WebController )
+				 && _appSettings.ApplicationType == AppSettings.StarskyAppType.WebController )
 			{
 				return Task.CompletedTask;
 			}
diff --git a/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs b/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs
index ec368d37fa..9dc1e6da2d 100644
--- a/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs
+++ b/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs
@@ -57,24 +57,24 @@ internal async Task> CreateThumbnailAsync(string sub
 						}
 					};
 				case FolderOrFileModel.FolderOrFileTypeList.Folder:
-				{
-					var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath)
-						.Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList();
-					toAddFilePaths.AddRange(contentOfDir);
-					break;
-				}
+					{
+						var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath)
+							.Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList();
+						toAddFilePaths.AddRange(contentOfDir);
+						break;
+					}
 				case FolderOrFileModel.FolderOrFileTypeList.File:
 				default:
-				{
-					toAddFilePaths.Add(subPath);
-					break;
-				}
+					{
+						toAddFilePaths.Add(subPath);
+						break;
+					}
 			}
-			
+
 			var resultChunkList = await toAddFilePaths.ForEachAsync(
 				async singleSubPath =>
 				{
-					var hashResult =  await new FileHash(_iStorage).GetHashCodeAsync(singleSubPath);
+					var hashResult = await new FileHash(_iStorage).GetHashCodeAsync(singleSubPath);
 					if ( !hashResult.Value )
 					{
 						return null;
@@ -82,9 +82,9 @@ internal async Task> CreateThumbnailAsync(string sub
 
 					return await CreateThumbAsync(singleSubPath, hashResult.Key);
 				}, _appSettings.MaxDegreesOfParallelismThumbnail);
-			
+
 			var results = new List();
-			
+
 			foreach ( var resultChunk in resultChunkList! )
 			{
 				results.AddRange(resultChunk ?? new List());
@@ -92,7 +92,7 @@ internal async Task> CreateThumbnailAsync(string sub
 
 			return results;
 		}
-		
+
 
 		/// 
 		/// Create a Thumbnail file to load it faster in the UI. Use FileIndexItem or database style path, Feature used by the cli tool
@@ -161,9 +161,9 @@ private async Task> CreateThumbInternal(strin
 			{
 				return ThumbnailNameHelper.GeneratedThumbnailSizes.Select(size => new GenerationResultModel
 				{
-					Success = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash, size)), 
-					Size = size, 
-					FileHash = fileHash, 
+					Success = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash, size)),
+					Size = size,
+					FileHash = fileHash,
 					IsNotFound = false
 				}).ToList();
 			}
@@ -180,7 +180,7 @@ private async Task> CreateThumbInternal(strin
 			_logger.LogInformation(".");
 
 			// results return null if thumbnailFromThumbnailUpdateList has count 0
-			return results!.Select(p =>p.Item2).Append(largeImageResult);
+			return results!.Select(p => p.Item2).Append(largeImageResult);
 		}
 
 		private static ThumbnailSize ThumbnailToSourceSize(bool skipExtraLarge)
@@ -195,7 +195,7 @@ private static string LargeThumbnailHash(string fileHash, ThumbnailSize thumbnai
 			var largeThumbnailHash = ThumbnailNameHelper.Combine(fileHash, thumbnailToSourceSize);
 			return largeThumbnailHash;
 		}
-		
+
 		internal async Task CreateLargestImageFromSource(
 			string fileHash, string largeThumbnailHash, string subPath,
 			ThumbnailSize thumbnailToSourceSize)
@@ -211,7 +211,7 @@ internal async Task CreateLargestImageFromSource(
 			};
 
 			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(
-				    fileHash, thumbnailToSourceSize)) )
+					fileHash, thumbnailToSourceSize)) )
 			{
 				// file already exist so skip
 				resultModel.Success = true;
@@ -219,24 +219,24 @@ internal async Task CreateLargestImageFromSource(
 			}
 
 			// run resize sync
-			var (_, resizeSuccess, resizeMessage) = await ResizeThumbnailFromSourceImage(subPath, 
-				ThumbnailNameHelper.GetSize(thumbnailToSourceSize), 
-				largeThumbnailHash );
+			var (_, resizeSuccess, resizeMessage) = await ResizeThumbnailFromSourceImage(subPath,
+				ThumbnailNameHelper.GetSize(thumbnailToSourceSize),
+				largeThumbnailHash);
 
 			// check if output any good
 			RemoveCorruptImage(fileHash, thumbnailToSourceSize);
 
-			if ( !resizeSuccess || ! _thumbnailStorage.ExistFile(
-				    ThumbnailNameHelper.Combine(fileHash, thumbnailToSourceSize)) )
+			if ( !resizeSuccess || !_thumbnailStorage.ExistFile(
+					ThumbnailNameHelper.Combine(fileHash, thumbnailToSourceSize)) )
 			{
 				_logger.LogError($"[ResizeThumbnailFromSourceImage] " +
-				                 $"output is null or corrupt for subPath {subPath}");
+								 $"output is null or corrupt for subPath {subPath}");
 				await WriteErrorMessageToBlockLog(subPath, resizeMessage);
 
 				resultModel.ErrorMessage = resizeMessage;
 				return resultModel;
 			}
-			
+
 			resultModel.Success = true;
 			return resultModel;
 		}
@@ -249,8 +249,8 @@ private List ListThumbnailToBeCreated(string fileHash)
 			void AddFileNames(ThumbnailSize size)
 			{
 				if ( !_thumbnailStorage.ExistFile(
-					    ThumbnailNameHelper.Combine(
-						    fileHash, size))
+						ThumbnailNameHelper.Combine(
+							fileHash, size))
 				   )
 				{
 					thumbnailFromThumbnailUpdateList.Add(size);
@@ -258,7 +258,7 @@ void AddFileNames(ThumbnailSize size)
 			}
 			// Large <- will be false when skipExtraLarge = true, its already created 
 			ThumbnailNameHelper.SecondGeneratedThumbnailSizes.ToList().ForEach(AddFileNames);
-			
+
 			return thumbnailFromThumbnailUpdateList;
 		}
 
@@ -267,14 +267,14 @@ internal async Task WriteErrorMessageToBlockLog(string subPath, string resizeMes
 			var stream = StringToStreamHelper.StringToStream("Thumbnail error " + resizeMessage);
 			await _iStorage.WriteStreamAsync(stream, GetErrorLogItemFullPath(subPath));
 		}
-		
+
 		private static string GetErrorLogItemFullPath(string subPath)
 		{
 			return Breadcrumbs.BreadcrumbHelper(subPath).LastOrDefault()
-			       + "/"
-			       + "_"
-			       + Path.GetFileNameWithoutExtension(PathHelper.GetFileName(subPath)) 
-			       + ".log";
+				   + "/"
+				   + "_"
+				   + Path.GetFileNameWithoutExtension(PathHelper.GetFileName(subPath))
+				   + ".log";
 		}
 
 		/// 
@@ -285,11 +285,11 @@ private static string GetErrorLogItemFullPath(string subPath)
 		internal bool RemoveCorruptImage(string fileHash,
 			ThumbnailSize thumbnailToSourceSize)
 		{
-			if (!_thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash,thumbnailToSourceSize))) return false;
+			if ( !_thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash, thumbnailToSourceSize)) ) return false;
 			var imageFormat = ExtensionRolesHelper.GetImageFormat(_thumbnailStorage.ReadStream(
-				ThumbnailNameHelper.Combine(fileHash,thumbnailToSourceSize),160));
+				ThumbnailNameHelper.Combine(fileHash, thumbnailToSourceSize), 160));
 			if ( imageFormat != ExtensionRolesHelper.ImageFormat.unknown ) return false;
-			_thumbnailStorage.FileDelete(ThumbnailNameHelper.Combine(fileHash,thumbnailToSourceSize));
+			_thumbnailStorage.FileDelete(ThumbnailNameHelper.Combine(fileHash, thumbnailToSourceSize));
 			return true;
 		}
 
@@ -303,8 +303,8 @@ internal bool RemoveCorruptImage(string fileHash,
 		/// jpg, or png
 		/// for reference only
 		/// (stream, fileHash, and is ok)
-		public async Task<(MemoryStream?,GenerationResultModel)> ResizeThumbnailFromThumbnailImage(string fileHash, // source location
-			int width,  string? subPathReference = null, string? thumbnailOutputHash = null,
+		public async Task<(MemoryStream?, GenerationResultModel)> ResizeThumbnailFromThumbnailImage(string fileHash, // source location
+			int width, string? subPathReference = null, string? thumbnailOutputHash = null,
 			bool removeExif = false,
 			ExtensionRolesHelper.ImageFormat imageFormat = ExtensionRolesHelper.ImageFormat.jpg
 		)
@@ -318,12 +318,12 @@ internal bool RemoveCorruptImage(string fileHash,
 				Success = true,
 				SubPath = subPathReference!
 			};
-			
+
 			try
 			{
 				// resize the image and save it to the output stream
-				using (var inputStream = _thumbnailStorage.ReadStream(fileHash))
-				using (var image = await Image.LoadAsync(inputStream))
+				using ( var inputStream = _thumbnailStorage.ReadStream(fileHash) )
+				using ( var image = await Image.LoadAsync(inputStream) )
 				{
 
 					ImageSharpImageResize(image, width, removeExif);
@@ -331,28 +331,28 @@ internal bool RemoveCorruptImage(string fileHash,
 
 					// When thumbnailOutputHash is nothing return stream instead of writing down
 					if ( string.IsNullOrEmpty(thumbnailOutputHash) ) return (outputStream, result);
-					
+
 					// only when a hash exists
 					await _thumbnailStorage.WriteStreamAsync(outputStream, thumbnailOutputHash);
 					// Disposed in WriteStreamAsync
 				}
-	
+
 			}
-			catch (Exception ex)            
+			catch ( Exception ex )
 			{
 				var message = ex.Message;
 				if ( message.StartsWith("Image cannot be loaded") ) message = "Image cannot be loaded";
 				_logger.LogError($"[ResizeThumbnailFromThumbnailImage] Exception {fileHash} {message}", ex);
 				result.Success = false;
 				result.ErrorMessage = message;
-				return (null,result);
+				return (null, result);
 			}
-			
+
 			return (outputStream, result);
 		}
-		
-		
-		public async Task<(MemoryStream?, bool, string)> ResizeThumbnailFromSourceImage(string subPath, 
+
+
+		public async Task<(MemoryStream?, bool, string)> ResizeThumbnailFromSourceImage(string subPath,
 			int width, string? thumbnailOutputHash = null,
 			bool removeExif = false,
 			ExtensionRolesHelper.ImageFormat imageFormat = ExtensionRolesHelper.ImageFormat.jpg)
@@ -362,8 +362,8 @@ internal bool RemoveCorruptImage(string fileHash,
 			try
 			{
 				// resize the image and save it to the output stream
-				using (var inputStream = _iStorage.ReadStream(subPath))
-				using (var image = await Image.LoadAsync(inputStream))
+				using ( var inputStream = _iStorage.ReadStream(subPath) )
+				using ( var image = await Image.LoadAsync(inputStream) )
 				{
 					ImageSharpImageResize(image, width, removeExif);
 					await SaveThumbnailImageFormat(image, imageFormat, outputStream);
@@ -373,33 +373,33 @@ internal bool RemoveCorruptImage(string fileHash,
 					{
 						return (outputStream, true, "Ok give stream back instead of disk write");
 					}
-					
+
 					// only when a hash exists
 					await _thumbnailStorage.WriteStreamAsync(outputStream, thumbnailOutputHash);
 					// Disposed in WriteStreamAsync
 				}
-	
+
 			}
-			catch (Exception ex)
+			catch ( Exception ex )
 			{
 				var message = ex.Message;
 				if ( message.StartsWith("Image cannot be loaded") ) message = "Image cannot be loaded";
 				_logger.LogError($"[ResizeThumbnailFromSourceImage] Exception {subPath} {message}", ex);
 				return (null, false, message);
 			}
-			
+
 			return (null, true, "Ok but written to disk");
 		}
 
 		private static void ImageSharpImageResize(Image image, int width, bool removeExif)
 		{
 			// Add original rotation to the image as json
-			if (image.Metadata.ExifProfile != null && !removeExif)
+			if ( image.Metadata.ExifProfile != null && !removeExif )
 			{
 				image.Metadata.ExifProfile.SetValue(ExifTag.Software, "Starsky");
 			}
-					
-			if (image.Metadata.ExifProfile != null && removeExif)
+
+			if ( image.Metadata.ExifProfile != null && removeExif )
 			{
 				image.Metadata.ExifProfile = null;
 				image.Metadata.IccProfile = null;
@@ -440,14 +440,15 @@ internal static Task SaveThumbnailImageFormat(Image image,
 		/// Rgba32 image
 		/// Files ImageFormat
 		/// input stream to save
-		private static async Task SaveThumbnailImageFormatInternal(Image image, ExtensionRolesHelper.ImageFormat imageFormat, 
+		private static async Task SaveThumbnailImageFormatInternal(Image image, ExtensionRolesHelper.ImageFormat imageFormat,
 			MemoryStream outputStream)
 		{
-			if (imageFormat == ExtensionRolesHelper.ImageFormat.png)
+			if ( imageFormat == ExtensionRolesHelper.ImageFormat.png )
 			{
-				await image.SaveAsync(outputStream, new PngEncoder{
-					ColorType = PngColorType.Rgb, 
-					CompressionLevel = PngCompressionLevel.BestSpeed, 
+				await image.SaveAsync(outputStream, new PngEncoder
+				{
+					ColorType = PngColorType.Rgb,
+					CompressionLevel = PngCompressionLevel.BestSpeed,
 					// IgnoreMetadata in older versions
 					SkipMetadata = true,
 					TransparentColorMode = PngTransparentColorMode.Clear,
@@ -455,11 +456,12 @@ private static async Task SaveThumbnailImageFormatInternal(Image image, Extensio
 				return;
 			}
 
-			await image.SaveAsync(outputStream, new JpegEncoder{
+			await image.SaveAsync(outputStream, new JpegEncoder
+			{
 				Quality = 90,
 			});
 		}
-		
+
 		/// 
 		/// Rotate an image, by rotating the pixels and resize the thumbnail.Please do not apply any orientation exif-tag on this file
 		/// 
@@ -468,18 +470,18 @@ private static async Task SaveThumbnailImageFormatInternal(Image image, Extensio
 		/// to resize, default 1000
 		/// to resize, default keep ratio (0)
 		/// Is successful? // private feature
-		internal async Task RotateThumbnail(string fileHash, int orientation, int width = 1000, int height = 0 )
+		internal async Task RotateThumbnail(string fileHash, int orientation, int width = 1000, int height = 0)
 		{
-			if (!_thumbnailStorage.ExistFile(fileHash)) return false;
+			if ( !_thumbnailStorage.ExistFile(fileHash) ) return false;
 
 			// the orientation is -1 or 1
 			var rotateMode = RotateMode.Rotate90;
-			if (orientation == -1) rotateMode = RotateMode.Rotate270; 
+			if ( orientation == -1 ) rotateMode = RotateMode.Rotate270;
 
 			try
 			{
-				using (var inputStream = _thumbnailStorage.ReadStream(fileHash))
-				using (var image = await Image.LoadAsync(inputStream))
+				using ( var inputStream = _thumbnailStorage.ReadStream(fileHash) )
+				using ( var image = await Image.LoadAsync(inputStream) )
 				using ( var stream = new MemoryStream() )
 				{
 					image.Mutate(x => x
@@ -487,18 +489,18 @@ internal async Task RotateThumbnail(string fileHash, int orientation, int
 					);
 					image.Mutate(x => x
 						.Rotate(rotateMode));
-					
+
 					// Image image, ExtensionRolesHelper.ImageFormat imageFormat, MemoryStream outputStream
 					await SaveThumbnailImageFormat(image, ExtensionRolesHelper.ImageFormat.jpg, stream);
 					await _thumbnailStorage.WriteStreamAsync(stream, fileHash);
 				}
 			}
-			catch (Exception ex)            
+			catch ( Exception ex )
 			{
 				Console.WriteLine(ex);
 				return false;
 			}
-            
+
 			return true;
 		}
 	}
diff --git a/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs b/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs
index e037cab253..bb908862e8 100644
--- a/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs
+++ b/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs
@@ -18,9 +18,9 @@ public sealed class ThumbnailCli
 		private readonly ISelectorStorage _selectorStorage;
 		private readonly IThumbnailService _thumbnailService;
 
-		public ThumbnailCli(AppSettings appSettings, 
-			IConsole console, IThumbnailService thumbnailService, 
-			IThumbnailCleaner thumbnailCleaner, 
+		public ThumbnailCli(AppSettings appSettings,
+			IConsole console, IThumbnailService thumbnailService,
+			IThumbnailCleaner thumbnailCleaner,
 			ISelectorStorage selectorStorage)
 		{
 			_appSettings = appSettings;
@@ -29,44 +29,44 @@ public ThumbnailCli(AppSettings appSettings,
 			_thumbnailCleaner = thumbnailCleaner;
 			_selectorStorage = selectorStorage;
 		}
-		
+
 		public async Task Thumbnail(string[] args)
 		{
 			_appSettings.Verbose = ArgsHelper.NeedVerbose(args);
 			_appSettings.ApplicationType = AppSettings.StarskyAppType.Thumbnail;
 
-			if (ArgsHelper.NeedHelp(args))
+			if ( ArgsHelper.NeedHelp(args) )
 			{
 				new ArgsHelper(_appSettings, _console).NeedHelpShowDialog();
 				return;
 			}
-			
+
 			new ArgsHelper().SetEnvironmentByArgs(args);
 
 			var subPath = new ArgsHelper(_appSettings).SubPathOrPathValue(args);
 			var getSubPathRelative = new ArgsHelper(_appSettings).GetRelativeValue(args);
-			if (getSubPathRelative != null)
+			if ( getSubPathRelative != null )
 			{
 				subPath = new StructureService(_selectorStorage.Get(
 						SelectorStorage.StorageServices.SubPath), _appSettings.Structure)
 					.ParseSubfolders(getSubPathRelative)!;
 			}
 
-			if (ArgsHelper.GetThumbnail(args))
+			if ( ArgsHelper.GetThumbnail(args) )
 			{
-				if (_appSettings.IsVerbose()) _console.WriteLine($">> GetThumbnail True ({DateTime.UtcNow:HH:mm:ss})");
-				
+				if ( _appSettings.IsVerbose() ) _console.WriteLine($">> GetThumbnail True ({DateTime.UtcNow:HH:mm:ss})");
+
 				var storage = _selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
 
 				var isFolderOrFile = storage.IsFolderOrFile(subPath);
 
-				if (_appSettings.IsVerbose()) _console.WriteLine(isFolderOrFile.ToString());
+				if ( _appSettings.IsVerbose() ) _console.WriteLine(isFolderOrFile.ToString());
 
 				await _thumbnailService.CreateThumbnailAsync(subPath);
-				
+
 				_console.WriteLine($"Thumbnail Done! ({DateTime.UtcNow:HH:mm:ss})");
 			}
-            
+
 			if ( ArgsHelper.NeedCleanup(args) )
 			{
 				_console.WriteLine($"Next: Start Thumbnail Cache cleanup (-x true) ({DateTime.UtcNow:HH:mm:ss})");
diff --git a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs
index bef346572e..6245433bb1 100644
--- a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs
+++ b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs
@@ -42,7 +42,7 @@ public async Task> CleanAllUnusedFilesAsync(int chunkSize = 50)
 
 			var allThumbnailFiles = _thumbnailStorage
 				.GetAllFilesInDirectory(null!).ToList();
-			
+
 			_logger.LogDebug($"Total files in thumb dir: {allThumbnailFiles.Count}");
 
 			var deletedFileHashes = new List();
@@ -53,10 +53,10 @@ public async Task> CleanAllUnusedFilesAsync(int chunkSize = 50)
 				{
 					await LoopThoughChunk(itemsInChunk, deletedFileHashes);
 				}
-				catch (Microsoft.EntityFrameworkCore.Storage.RetryLimitExceededException exception)
+				catch ( Microsoft.EntityFrameworkCore.Storage.RetryLimitExceededException exception )
 				{
 					_logger.LogInformation($"[CleanAllUnusedFiles] catch-ed and " +
-					                       $"skip {string.Join(",", itemsInChunk.ToList())} ~ {exception.Message}", exception);
+										   $"skip {string.Join(",", itemsInChunk.ToList())} ~ {exception.Message}", exception);
 				}
 			}
 
@@ -64,12 +64,12 @@ public async Task> CleanAllUnusedFilesAsync(int chunkSize = 50)
 			return deletedFileHashes;
 		}
 
-		private async Task LoopThoughChunk(IEnumerable itemsInChunk , List deletedFileHashes)
+		private async Task LoopThoughChunk(IEnumerable itemsInChunk, List deletedFileHashes)
 		{
 			var fileIndexItems = await _query.GetObjectsByFileHashAsync(itemsInChunk.ToList());
-			foreach ( var resultFileHash in fileIndexItems.Where(result => 
+			foreach ( var resultFileHash in fileIndexItems.Where(result =>
 				result is { Status: FileIndexItem.ExifStatus.NotFoundNotInIndex, FileHash: { } }
-				).Select(p => p.FileHash).Cast())
+				).Select(p => p.FileHash).Cast() )
 			{
 				var fileHashesToDelete = new List
 				{
@@ -82,12 +82,12 @@ private async Task LoopThoughChunk(IEnumerable itemsInChunk , List GetFileNamesWithExtension(List allThumbnailFiles
 			foreach ( var thumbnailFile in allThumbnailFiles )
 			{
 				var fileHash = Path.GetFileNameWithoutExtension(thumbnailFile);
-				var fileHashWithoutSize = Regex.Match(fileHash, "^.*(?=(@))", 
+				var fileHashWithoutSize = Regex.Match(fileHash, "^.*(?=(@))",
 					RegexOptions.None, TimeSpan.FromMilliseconds(100)).Value;
-				if ( string.IsNullOrEmpty(fileHashWithoutSize)  )
+				if ( string.IsNullOrEmpty(fileHashWithoutSize) )
 				{
 					fileHashWithoutSize = fileHash;
 				}
diff --git a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.cs
index 5f7bedcfcf..67c3460967 100644
--- a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.cs
@@ -18,13 +18,13 @@ public sealed class ThumbnailService : IThumbnailService
 
 		private readonly Thumbnail _thumbnail;
 
-		public ThumbnailService(ISelectorStorage selectorStorage, IWebLogger logger, AppSettings appSettings, 
+		public ThumbnailService(ISelectorStorage selectorStorage, IWebLogger logger, AppSettings appSettings,
 			IUpdateStatusGeneratedThumbnailService updateStatusGeneratedThumbnailService)
 		{
 			_updateStatusGeneratedThumbnailService = updateStatusGeneratedThumbnailService;
 			var iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
 			var thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail);
-			_thumbnail = new Thumbnail(iStorage, thumbnailStorage,logger,appSettings);
+			_thumbnail = new Thumbnail(iStorage, thumbnailStorage, logger, appSettings);
 		}
 
 		/// 
diff --git a/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs
index 417b867fde..ec580115ff 100644
--- a/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs
@@ -66,7 +66,7 @@ public async Task> RemoveNotfoundStatusAsync(List p.FileHash)
 			.Select(p => p.FileHash)
 			.ToList();
-		
+
 		await _thumbnailQuery.RemoveThumbnailsAsync(dtoObjects);
 		return dtoObjects;
 	}
diff --git a/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj b/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj
index 333bfdb39b..26ee50585c 100644
--- a/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj
+++ b/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj
@@ -17,7 +17,7 @@
 
     
         
-        
+        
     
 
     
diff --git a/starsky/starsky.foundation.thumbnailmeta/Helpers/MetaThumbnailCommandLineHelper.cs b/starsky/starsky.foundation.thumbnailmeta/Helpers/MetaThumbnailCommandLineHelper.cs
index 0b12bca810..cfbe6c514b 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Helpers/MetaThumbnailCommandLineHelper.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Helpers/MetaThumbnailCommandLineHelper.cs
@@ -1,5 +1,4 @@
 using System;
-using System.Linq;
 using System.Threading.Tasks;
 using starsky.foundation.thumbnailmeta.Interfaces;
 using starsky.foundation.platform.Helpers;
@@ -19,8 +18,10 @@ public class MetaThumbnailCommandLineHelper
 		private readonly ISelectorStorage _selectorStorage;
 		private readonly IMetaUpdateStatusThumbnailService _statusThumbnailService;
 
-		public MetaThumbnailCommandLineHelper(ISelectorStorage selectorStorage, AppSettings appSettings, 
-			IConsole console, IMetaExifThumbnailService metaExifThumbnailService, IMetaUpdateStatusThumbnailService statusThumbnailService)
+		public MetaThumbnailCommandLineHelper(ISelectorStorage selectorStorage,
+			AppSettings appSettings,
+			IConsole console, IMetaExifThumbnailService metaExifThumbnailService,
+			IMetaUpdateStatusThumbnailService statusThumbnailService)
 		{
 			_selectorStorage = selectorStorage;
 			_appSettings = appSettings;
@@ -28,33 +29,35 @@ public MetaThumbnailCommandLineHelper(ISelectorStorage selectorStorage, AppSetti
 			_console = console;
 			_statusThumbnailService = statusThumbnailService;
 		}
-		
+
 		public async Task CommandLineAsync(string[] args)
 		{
 			_appSettings.Verbose = ArgsHelper.NeedVerbose(args);
 			_appSettings.ApplicationType = AppSettings.StarskyAppType.MetaThumbnail;
 
-			if (ArgsHelper.NeedHelp(args))
+			if ( ArgsHelper.NeedHelp(args) )
 			{
 				new ArgsHelper(_appSettings, _console).NeedHelpShowDialog();
 				return;
 			}
-			
+
 			new ArgsHelper().SetEnvironmentByArgs(args);
 
 			var subPath = new ArgsHelper(_appSettings).SubPathOrPathValue(args);
 			var getSubPathRelative = new ArgsHelper(_appSettings).GetRelativeValue(args);
-			if (getSubPathRelative != null)
+			if ( getSubPathRelative != null )
 			{
-				subPath = new StructureService(_selectorStorage.Get(SelectorStorage.StorageServices.SubPath), _appSettings.Structure)
+				subPath = new StructureService(
+						_selectorStorage.Get(SelectorStorage.StorageServices.SubPath),
+						_appSettings.Structure)
 					.ParseSubfolders(getSubPathRelative);
 			}
 
-			var statusResultsWithSubPaths = await _metaExifThumbnailService.AddMetaThumbnail(subPath!);
+			var statusResultsWithSubPaths =
+				await _metaExifThumbnailService.AddMetaThumbnail(subPath!);
 			_console.WriteLine($"next: run update status ({DateTime.UtcNow:HH:mm:ss})");
 			await _statusThumbnailService.UpdateStatusThumbnail(statusResultsWithSubPaths);
 			_console.WriteLine($"Done! ({DateTime.UtcNow:HH:mm:ss})");
 		}
 	}
 }
-
diff --git a/starsky/starsky.foundation.thumbnailmeta/Helpers/NewImageSize.cs b/starsky/starsky.foundation.thumbnailmeta/Helpers/NewImageSize.cs
index b039b12662..fada592526 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Helpers/NewImageSize.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Helpers/NewImageSize.cs
@@ -2,24 +2,24 @@ namespace starsky.foundation.thumbnailmeta.Helpers
 {
 	public static class NewImageSize
 	{
-		
+
 		public class ImageSizeModel
 		{
-			public ImageSizeModel(int destWidth, int destHeight, 
-				int destX,  int destY)
+			public ImageSizeModel(int destWidth, int destHeight,
+				int destX, int destY)
 			{
 				DestWidth = destWidth;
 				DestHeight = destHeight;
 				DestX = destX;
 				DestY = destY;
 			}
-			
+
 			public int DestWidth { get; set; }
 			public int DestHeight { get; set; }
 			public int DestX { get; set; }
 			public int DestY { get; set; }
 		}
-		
+
 		/// 
 		/// @see: https://stackoverflow.com/a/2001462
 		/// 
@@ -35,26 +35,26 @@ public static ImageSizeModel NewImageSizeCalc(int smallWidth, int smallHeight, i
 			float nPercentH = 0;
 			int destX = 0;
 			int destY = 0;
-			
-			nPercentW = ((float)smallWidth / (float)sourceWidth);
-			nPercentH = ((float)smallHeight / (float)sourceHeight);
-			if (nPercentH < nPercentW)
+
+			nPercentW = ( ( float )smallWidth / ( float )sourceWidth );
+			nPercentH = ( ( float )smallHeight / ( float )sourceHeight );
+			if ( nPercentH < nPercentW )
 			{
 				nPercent = nPercentH;
-				destX = System.Convert.ToInt16((smallWidth -
-				                                (sourceWidth * nPercent)) / 2);
+				destX = System.Convert.ToInt16(( smallWidth -
+												( sourceWidth * nPercent ) ) / 2);
 			}
 			else
 			{
 				nPercent = nPercentW;
-				destY = System.Convert.ToInt16((smallHeight -
-				                                (sourceHeight * nPercent)) / 2);
+				destY = System.Convert.ToInt16(( smallHeight -
+												( sourceHeight * nPercent ) ) / 2);
 			}
 
-			int destWidth = (int)(sourceWidth * nPercent);
-			int destHeight = (int)(sourceHeight * nPercent);
+			int destWidth = ( int )( sourceWidth * nPercent );
+			int destHeight = ( int )( sourceHeight * nPercent );
 
-			return new ImageSizeModel( destWidth, destHeight, destX,  destY);
+			return new ImageSizeModel(destWidth, destHeight, destX, destY);
 		}
 	}
 }
diff --git a/starsky/starsky.foundation.thumbnailmeta/Interfaces/IMetaExifThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Interfaces/IMetaExifThumbnailService.cs
index 14cb2fb060..3560c41850 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Interfaces/IMetaExifThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Interfaces/IMetaExifThumbnailService.cs
@@ -11,20 +11,20 @@ public interface IMetaExifThumbnailService
 		/// (FilePath,FileHash)
 		/// fail/pass, right type, string=subPath, string?2= error reason
 		Task> AddMetaThumbnail(IEnumerable<(string, string)> subPathsAndHash);
-		
+
 		/// 
 		/// add meta thumbnail to a single file
 		/// 
 		/// subPath
 		/// fail/pass, right type, string=subPath, string?2= error reason
 		Task> AddMetaThumbnail(string subPath);
-		
+
 		/// 
 		/// Add Meta Thumbnail to a single file by fileHash
 		/// 
 		/// subPath
 		/// hash
 		/// fail/pass, right type, string=subPath, string?2= error reason
-		Task<(bool,bool, string, string?)> AddMetaThumbnail(string subPath, string fileHash);
+		Task<(bool, bool, string, string?)> AddMetaThumbnail(string subPath, string fileHash);
 	}
 }
diff --git a/starsky/starsky.foundation.thumbnailmeta/Interfaces/IOffsetDataMetaExifThumbnail.cs b/starsky/starsky.foundation.thumbnailmeta/Interfaces/IOffsetDataMetaExifThumbnail.cs
index 0fd1eeb4e4..211182584e 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Interfaces/IOffsetDataMetaExifThumbnail.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Interfaces/IOffsetDataMetaExifThumbnail.cs
@@ -1,7 +1,6 @@
 using MetadataExtractor.Formats.Exif;
 using starsky.foundation.database.Models;
 using starsky.foundation.thumbnailmeta.Models;
-using starsky.foundation.readmeta.Models;
 
 namespace starsky.foundation.thumbnailmeta.Interfaces
 {
@@ -10,6 +9,7 @@ public interface IOffsetDataMetaExifThumbnail
 		OffsetModel ParseOffsetData(ExifThumbnailDirectory? exifThumbnailDir,
 			string subPath);
 
-		(ExifThumbnailDirectory?, int, int, FileIndexItem.Rotation) GetExifMetaDirectories(string subPath);
+		(ExifThumbnailDirectory?, int, int, FileIndexItem.Rotation) GetExifMetaDirectories(
+			string subPath);
 	}
 }
diff --git a/starsky/starsky.foundation.thumbnailmeta/Interfaces/IWriteMetaThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Interfaces/IWriteMetaThumbnailService.cs
index 105ea8ded9..761b08bd9c 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Interfaces/IWriteMetaThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Interfaces/IWriteMetaThumbnailService.cs
@@ -8,7 +8,7 @@ public interface IWriteMetaThumbnailService
 	{
 		Task WriteAndCropFile(string fileHash,
 			OffsetModel offsetData, int sourceWidth,
-			int sourceHeight, FileIndexItem.Rotation rotation, 
+			int sourceHeight, FileIndexItem.Rotation rotation,
 			string? reference = null);
 	}
 }
diff --git a/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs
index 58aa29dfb4..d3594ee163 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs
@@ -26,8 +26,8 @@ public sealed class MetaExifThumbnailService : IMetaExifThumbnailService
 		private readonly IWriteMetaThumbnailService _writeMetaThumbnailService;
 		private readonly AppSettings _appSettings;
 
-		public MetaExifThumbnailService(AppSettings appSettings, ISelectorStorage selectorStorage, 
-			IOffsetDataMetaExifThumbnail offsetDataMetaExifThumbnail, 
+		public MetaExifThumbnailService(AppSettings appSettings, ISelectorStorage selectorStorage,
+			IOffsetDataMetaExifThumbnail offsetDataMetaExifThumbnail,
 			IWriteMetaThumbnailService writeMetaThumbnailService, IWebLogger logger)
 		{
 			_appSettings = appSettings;
@@ -45,10 +45,10 @@ public MetaExifThumbnailService(AppSettings appSettings, ISelectorStorage select
 		/// fail/pass, string=subPath, string?2= error reason
 		public async Task> AddMetaThumbnail(IEnumerable<(string, string)> subPathsAndHash)
 		{
-			return (await subPathsAndHash
-				.ForEachAsync(async item => 
+			return ( await subPathsAndHash
+				.ForEachAsync(async item =>
 						await AddMetaThumbnail(item.Item1, item.Item2),
-					_appSettings.MaxDegreesOfParallelism))!;
+					_appSettings.MaxDegreesOfParallelism) )!;
 		}
 
 		/// 
@@ -71,23 +71,23 @@ await AddMetaThumbnail(item.Item1, item.Item2),
 						(false, false, subPath, "folder or file not found")
 					};
 				case FolderOrFileModel.FolderOrFileTypeList.Folder:
-				{
-					var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath)
-						.Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList();
-					
-					var results = await contentOfDir
-						.ForEachAsync(async singleSubPath => 
-							await AddMetaThumbnail(singleSubPath, null!),
-							_appSettings.MaxDegreesOfParallelism);
+					{
+						var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath)
+							.Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList();
 
-					return results!.ToList();
-				}
+						var results = await contentOfDir
+							.ForEachAsync(async singleSubPath =>
+								await AddMetaThumbnail(singleSubPath, null!),
+								_appSettings.MaxDegreesOfParallelism);
+
+						return results!.ToList();
+					}
 				default:
-				{
-					var result = (await  new FileHash(_iStorage).GetHashCodeAsync(subPath));
-					return !result.Value ? new List<(bool, bool, string, string?)>{(false,false, subPath,"hash not found")} : 
-						new List<(bool, bool, string, string?)>{await AddMetaThumbnail(subPath, result.Key)};
-				}
+					{
+						var result = ( await new FileHash(_iStorage).GetHashCodeAsync(subPath) );
+						return !result.Value ? new List<(bool, bool, string, string?)> { (false, false, subPath, "hash not found") } :
+							new List<(bool, bool, string, string?)> { await AddMetaThumbnail(subPath, result.Key) };
+					}
 			}
 		}
 
@@ -99,15 +99,15 @@ await AddMetaThumbnail(singleSubPath, null!),
 		/// fail/pass, right type, subPath
 		public async Task<(bool, bool, string, string?)> AddMetaThumbnail(string subPath, string fileHash)
 		{
-			if ( !_iStorage.ExistFile(subPath))
+			if ( !_iStorage.ExistFile(subPath) )
 			{
-				return (false,false, subPath, "not found");
+				return (false, false, subPath, "not found");
 			}
-			
-			var first50BytesStream = _iStorage.ReadStream(subPath,50);
+
+			var first50BytesStream = _iStorage.ReadStream(subPath, 50);
 			var imageFormat = ExtensionRolesHelper.GetImageFormat(first50BytesStream);
-			
-			if ( imageFormat != ExtensionRolesHelper.ImageFormat.jpg && imageFormat != ExtensionRolesHelper.ImageFormat.tiff)
+
+			if ( imageFormat != ExtensionRolesHelper.ImageFormat.jpg && imageFormat != ExtensionRolesHelper.ImageFormat.tiff )
 			{
 				_logger.LogDebug($"[AddMetaThumbnail] {subPath} is not a jpg or tiff file");
 				return (false, false, subPath, $"{subPath} is not a jpg or tiff file");
@@ -115,24 +115,24 @@ await AddMetaThumbnail(singleSubPath, null!),
 
 			if ( string.IsNullOrEmpty(fileHash) )
 			{
-				var result = (await  new FileHash(_iStorage).GetHashCodeAsync(subPath));
+				var result = ( await new FileHash(_iStorage).GetHashCodeAsync(subPath) );
 				if ( !result.Value )
 				{
 					_logger.LogError("[MetaExifThumbnail] hash failed");
-					return (false, true, subPath,"hash failed");
+					return (false, true, subPath, "hash failed");
 				}
 				fileHash = result.Key;
 			}
 
-			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash,ThumbnailSize.TinyMeta)) )
+			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.TinyMeta)) )
 			{
 				return (true, true, subPath, "already exist");
 			}
-				
-			var (exifThumbnailDir, sourceWidth, sourceHeight, rotation) = 
+
+			var (exifThumbnailDir, sourceWidth, sourceHeight, rotation) =
 				_offsetDataMetaExifThumbnail.GetExifMetaDirectories(subPath);
 			var offsetData = _offsetDataMetaExifThumbnail.
-				ParseOffsetData(exifThumbnailDir,subPath);
+				ParseOffsetData(exifThumbnailDir, subPath);
 			if ( !offsetData.Success ) return (false, true, subPath, offsetData.Reason);
 
 			return (await _writeMetaThumbnailService.WriteAndCropFile(fileHash, offsetData, sourceWidth,
diff --git a/starsky/starsky.foundation.thumbnailmeta/Services/MetaUpdateStatusThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Services/MetaUpdateStatusThumbnailService.cs
index 1643a88d81..96d6b90e5c 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Services/MetaUpdateStatusThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Services/MetaUpdateStatusThumbnailService.cs
@@ -22,7 +22,7 @@ public MetaUpdateStatusThumbnailService(IThumbnailQuery thumbnailQuery, ISelecto
 		var storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
 		_fileHashStorage = new FileHash(storage);
 	}
-	
+
 	/// 
 	/// 
 	/// 
@@ -33,13 +33,13 @@ public async Task UpdateStatusThumbnail(List<(bool, bool, string, string?)> stat
 		foreach ( var (status, rightType, subPath, reason) in statusResultsWithSubPaths )
 		{
 			if ( !rightType ) continue;
-			var fileHash = ( await _fileHashStorage.GetHashCodeAsync(subPath)).Key;
+			var fileHash = ( await _fileHashStorage.GetHashCodeAsync(subPath) ).Key;
 			statusResultsWithFileHashes.Add(new ThumbnailResultDataTransferModel(fileHash, status)
 			{
 				Reasons = reason
 			});
 		}
-		
+
 		await _thumbnailQuery.AddThumbnailRangeAsync(statusResultsWithFileHashes);
 	}
 }
diff --git a/starsky/starsky.foundation.thumbnailmeta/Services/OffsetDataMetaExifThumbnail.cs b/starsky/starsky.foundation.thumbnailmeta/Services/OffsetDataMetaExifThumbnail.cs
index 6f3fc22b76..f0f114b00c 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Services/OffsetDataMetaExifThumbnail.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Services/OffsetDataMetaExifThumbnail.cs
@@ -11,13 +11,13 @@
 using starsky.foundation.thumbnailmeta.Interfaces;
 using starsky.foundation.thumbnailmeta.Models;
 using starsky.foundation.platform.Interfaces;
-using starsky.foundation.readmeta.Models;
 using starsky.foundation.readmeta.ReadMetaHelpers;
 using starsky.foundation.storage.Interfaces;
 using starsky.foundation.storage.Storage;
 using Directory = MetadataExtractor.Directory;
 
 [assembly: InternalsVisibleTo("starskytest")]
+
 namespace starsky.foundation.thumbnailmeta.Services
 {
 	[Service(typeof(IOffsetDataMetaExifThumbnail), InjectionLifetime = InjectionLifetime.Scoped)]
@@ -35,10 +35,10 @@ public OffsetDataMetaExifThumbnail(ISelectorStorage selectorStorage, IWebLogger
 		public (ExifThumbnailDirectory?, int, int, FileIndexItem.Rotation)
 			GetExifMetaDirectories(string subPath)
 		{
-			var (allExifItems,exifThumbnailDir) = ReadExifMetaDirectories(subPath);
+			var (allExifItems, exifThumbnailDir) = ReadExifMetaDirectories(subPath);
 			return ParseMetaThumbnail(allExifItems, exifThumbnailDir, subPath);
 		}
-		
+
 		internal (List?, ExifThumbnailDirectory?) ReadExifMetaDirectories(string subPath)
 		{
 			using ( var stream = _iStorage.ReadStream(subPath) )
@@ -49,31 +49,37 @@ public OffsetDataMetaExifThumbnail(ISelectorStorage selectorStorage, IWebLogger
 					allExifItems.Find(p =>
 						p.Name == "Exif Thumbnail") as ExifThumbnailDirectory;
 
-				return ( allExifItems,  exifThumbnailDir);
+				return (allExifItems, exifThumbnailDir);
 			}
 		}
-		
-		internal (ExifThumbnailDirectory?, int, int, FileIndexItem.Rotation) ParseMetaThumbnail(List? allExifItems, 
+
+		internal (ExifThumbnailDirectory?, int, int, FileIndexItem.Rotation) ParseMetaThumbnail(
+			List? allExifItems,
 			ExifThumbnailDirectory? exifThumbnailDir, string? reference = null)
 		{
-
 			if ( exifThumbnailDir == null || allExifItems == null )
 			{
-				return ( null, 0, 0, FileIndexItem.Rotation.DoNotChange );
+				return (null, 0, 0, FileIndexItem.Rotation.DoNotChange);
 			}
-				
+
 			var jpegTags = allExifItems.OfType().FirstOrDefault()?.Tags;
 
-			var heightPixels = jpegTags?.FirstOrDefault(p => p.Type == JpegDirectory.TagImageHeight)?.Description;
-			var widthPixels = jpegTags?.FirstOrDefault(p => p.Type == JpegDirectory.TagImageWidth)?.Description;
+			var heightPixels = jpegTags?.FirstOrDefault(p => p.Type == JpegDirectory.TagImageHeight)
+				?.Description;
+			var widthPixels = jpegTags?.FirstOrDefault(p => p.Type == JpegDirectory.TagImageWidth)
+				?.Description;
 
 			if ( string.IsNullOrEmpty(heightPixels) && string.IsNullOrEmpty(widthPixels) )
 			{
 				var exifSubIfdDirectories = allExifItems.OfType().ToList();
 				foreach ( var exifSubIfdDirectoryTags in exifSubIfdDirectories.Select(p => p.Tags) )
 				{
-					var heightValue =  exifSubIfdDirectoryTags.FirstOrDefault(p => p.Type == ExifDirectoryBase.TagImageHeight)?.Description;
-					var widthValue =  exifSubIfdDirectoryTags.FirstOrDefault(p => p.Type == ExifDirectoryBase.TagImageWidth)?.Description;
+					var heightValue = exifSubIfdDirectoryTags
+						.FirstOrDefault(p => p.Type == ExifDirectoryBase.TagImageHeight)
+						?.Description;
+					var widthValue = exifSubIfdDirectoryTags
+						.FirstOrDefault(p => p.Type == ExifDirectoryBase.TagImageWidth)
+						?.Description;
 					if ( heightValue == null || widthValue == null ) continue;
 					heightPixels = heightValue;
 					widthPixels = widthValue;
@@ -81,26 +87,31 @@ public OffsetDataMetaExifThumbnail(ISelectorStorage selectorStorage, IWebLogger
 			}
 
 			var rotation = ReadMetaExif.GetOrientationFromExifItem(allExifItems);
-					
+
 			var heightParseResult = int.TryParse(heightPixels?.Replace(
-				" pixels",string.Empty), out var height);
-				
-			var widthParseResult = int.TryParse(widthPixels?.Replace(" pixels",string.Empty), out var width);
-			
-			if ( !heightParseResult || !widthParseResult || height == 0||  width == 0)
+				" pixels", string.Empty), out var height);
+
+			var widthParseResult =
+				int.TryParse(widthPixels?.Replace(" pixels", string.Empty), out var width);
+
+			if ( !heightParseResult || !widthParseResult || height == 0 || width == 0 )
 			{
-				_logger.LogInformation($"[ParseMetaThumbnail] ${reference} has no height or width {width}x{height} ");
+				_logger.LogInformation(
+					$"[ParseMetaThumbnail] ${reference} has no height or width {width}x{height} ");
 			}
+
 			return (exifThumbnailDir, width, height, rotation);
 		}
 
 		public OffsetModel ParseOffsetData(ExifThumbnailDirectory? exifThumbnailDir, string subPath)
 		{
-			if ( exifThumbnailDir == null )  return new OffsetModel
-			{
-				Success = false, 
-				Reason = $"{FilenamesHelper.GetFileName(subPath)} ExifThumbnailDirectory null"
-			};
+			if ( exifThumbnailDir == null )
+				return new OffsetModel
+				{
+					Success = false,
+					Reason =
+						$"{FilenamesHelper.GetFileName(subPath)} ExifThumbnailDirectory null"
+				};
 
 			long thumbnailOffset = long.Parse(exifThumbnailDir!.GetDescription(
 				ExifThumbnailDirectory.TagThumbnailOffset)!.Split(' ')[0]);
@@ -108,30 +119,36 @@ public OffsetModel ParseOffsetData(ExifThumbnailDirectory? exifThumbnailDir, str
 			int thumbnailLength = int.Parse(exifThumbnailDir.GetDescription(
 				ExifThumbnailDirectory.TagThumbnailLength)!.Split(' ')[0]) + maxIssue35Offset;
 			byte[] thumbnail = new byte[thumbnailLength];
-			
-			using (var imageStream = _iStorage.ReadStream(subPath))
+
+			using ( var imageStream = _iStorage.ReadStream(subPath) )
 			{
 				imageStream.Seek(thumbnailOffset, SeekOrigin.Begin);
 				imageStream.Read(thumbnail, 0, thumbnailLength);
 			}
-			
+
 			// work around Metadata Extractor issue #35
 			if ( thumbnailLength <= maxIssue35Offset + 1 )
 			{
-				_logger.LogInformation($"[ParseOffsetData] thumbnailLength : {thumbnailLength} {maxIssue35Offset + 1}");
-				return new OffsetModel {Success = false, Reason =  $"{FilenamesHelper.GetFileName(subPath)} offsetLength"};
+				_logger.LogInformation(
+					$"[ParseOffsetData] thumbnailLength : {thumbnailLength} {maxIssue35Offset + 1}");
+				return new OffsetModel
+				{
+					Success = false,
+					Reason = $"{FilenamesHelper.GetFileName(subPath)} offsetLength"
+				};
 			}
-			
+
 			int issue35Offset = 0;
-			for (int offset = 0; offset <= maxIssue35Offset; ++offset)
+			for ( int offset = 0; offset <= maxIssue35Offset; ++offset )
 			{
 				// 0xffd8 is the JFIF start of image segment indicator
-				if ((thumbnail[offset] == 0xff) && (thumbnail[offset + 1] == 0xd8))
+				if ( ( thumbnail[offset] == 0xff ) && ( thumbnail[offset + 1] == 0xd8 ) )
 				{
 					issue35Offset = offset;
 					break;
 				}
 			}
+
 			return new OffsetModel
 			{
 				Success = true,
@@ -139,8 +156,6 @@ public OffsetModel ParseOffsetData(ExifThumbnailDirectory? exifThumbnailDir, str
 				Count = thumbnailLength - issue35Offset,
 				Data = thumbnail, // byte array
 			};
-
 		}
 	}
-
 }
diff --git a/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs
index c337f621c2..bebf4dd5b6 100644
--- a/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs
+++ b/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs
@@ -40,7 +40,7 @@ public async Task WriteAndCropFile(string fileHash,
 		try
 		{
 			using ( var thumbnailStream =
-			       new MemoryStream(offsetData.Data, offsetData.Index, offsetData.Count) )
+				   new MemoryStream(offsetData.Data, offsetData.Index, offsetData.Count) )
 			using ( var smallImage = await Image.LoadAsync(thumbnailStream) )
 			using ( var outputStream = new MemoryStream() )
 			{
diff --git a/starsky/starsky.foundation.webtelemetry/Extensions/ApplicationInsightsExtension.cs b/starsky/starsky.foundation.webtelemetry/Extensions/ApplicationInsightsExtension.cs
deleted file mode 100644
index 5a1e2cd2ac..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Extensions/ApplicationInsightsExtension.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.Runtime.CompilerServices;
-using Microsoft.ApplicationInsights.AspNetCore.Extensions;
-using Microsoft.ApplicationInsights.Channel;
-using Microsoft.ApplicationInsights.Extensibility;
-using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;
-using Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel;
-using Microsoft.Extensions.DependencyInjection;
-using starsky.foundation.platform.Models;
-using starsky.foundation.webtelemetry.Initializers;
-
-[assembly: InternalsVisibleTo("starskytest")]
-namespace starsky.foundation.webtelemetry.Extensions
-{
-	public static class ApplicationInsightsExtension
-	{
-		/// 
-		/// Add Metrics & Monitoring for Application Insights (AddApplicationInsights)
-		/// 
-		/// collection service
-		/// to use for ApplicationInsights InstrumentationKey
-		public static void AddMonitoring(this IServiceCollection services, AppSettings appSettings)
-		{
-			if ( string.IsNullOrWhiteSpace(appSettings.ApplicationInsightsConnectionString) )
-			{
-				return;
-			}
-
-			// https://docs.microsoft.com/en-us/azure/azure-monitor/app/telemetry-channels
-			services.AddSingleton(typeof(ITelemetryChannel),
-				new ServerTelemetryChannel()
-				{
-					StorageFolder = appSettings.TempFolder,
-				});
-
-			services.AddApplicationInsightsTelemetry(
-				new ApplicationInsightsServiceOptions
-				{
-					ApplicationVersion = appSettings.AppVersion,
-					EnableDependencyTrackingTelemetryModule = true,
-					EnableHeartbeat = true,
-					EnableAuthenticationTrackingJavaScript = true,
-					EnableEventCounterCollectionModule = true,
-					ConnectionString = appSettings.ApplicationInsightsConnectionString
-				});
-			
-			services.AddSingleton(new CloudRoleNameInitializer($"{AppSettings.StarskyAppType.WebController}"));
-
-			services.ConfigureTelemetryModule(
-				(module, _) => SetEventCounterCollectionModule(module));
-			
-		}
-
-		/// 
-		/// Add Event Counters to Application Insights
-		/// @see: https://docs.microsoft.com/en-us/azure/azure-monitor/app/eventcounters
-		/// 
-		/// Modules
-		internal static void SetEventCounterCollectionModule(
-			EventCounterCollectionModule module)
-		{
-			// in .NET Core 3 there are no default Counters (Event counters)
-			module.Counters.Clear();
-			// https://docs.microsoft.com/en-us/dotnet/core/diagnostics/available-counters
-			module.Counters.Add(
-				new EventCounterCollectionRequest("System.Runtime",
-					"gen-0-size"));
-			module.Counters.Add(
-				new EventCounterCollectionRequest("System.Runtime",
-					"time-in-gc"));
-			module.Counters.Add(
-				new EventCounterCollectionRequest("System.Runtime",
-					"cpu-usage"));
-			// memory usage
-			module.Counters.Add(
-				new EventCounterCollectionRequest("System.Runtime",
-					"working-set"));	
-			// Retrieves the number of bytes currently thought to be allocated
-			module.Counters.Add(
-				new EventCounterCollectionRequest("System.Runtime",
-					"gc-heap-size"));	
-			module.Counters.Add(
-				new EventCounterCollectionRequest("Microsoft.AspNetCore.Hosting",
-					"current-request"));
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Extensions/OpenTelemetryExtension.cs b/starsky/starsky.foundation.webtelemetry/Extensions/OpenTelemetryExtension.cs
index d231be561b..3ba0f64526 100644
--- a/starsky/starsky.foundation.webtelemetry/Extensions/OpenTelemetryExtension.cs
+++ b/starsky/starsky.foundation.webtelemetry/Extensions/OpenTelemetryExtension.cs
@@ -4,19 +4,22 @@
 using System.Runtime.CompilerServices;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
+using OpenTelemetry;
 using OpenTelemetry.Exporter;
 using OpenTelemetry.Metrics;
 using OpenTelemetry.Resources;
 using OpenTelemetry.Trace;
+using starsky.foundation.platform.MetricsNamespaces;
 using starsky.foundation.platform.Models;
 
 [assembly: InternalsVisibleTo("starskytest")]
+
 namespace starsky.foundation.webtelemetry.Extensions;
 
 public static class OpenTelemetryExtension
 {
 	/// 
-	/// Add Metrics & Monitoring for OpenTelemetry
+	/// Add Metrics and Monitoring for OpenTelemetry
 	/// 
 	/// collection service
 	/// to use for OpenTelemetry keys and info
@@ -27,7 +30,7 @@ public static void AddOpenTelemetryMonitoring(
 		{
 			return;
 		}
-		
+
 		var telemetryBuilder = services.AddOpenTelemetry()
 			.ConfigureResource(resource => resource.AddService(
 				serviceNamespace: appSettings.OpenTelemetry.GetServiceName(),
@@ -39,14 +42,27 @@ public static void AddOpenTelemetryMonitoring(
 			{
 				{
 					"deployment.environment",
-						Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty
-				}
+					Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? string.Empty
+				},
+				{ "service.name", appSettings.OpenTelemetry.GetServiceName() },
+				{ "service.namespace", appSettings.OpenTelemetry.GetServiceName() },
+				{ "service.instance.id", appSettings.OpenTelemetry.GetServiceName() }
 			}));
-		
+
 		if ( !string.IsNullOrWhiteSpace(appSettings.OpenTelemetry.TracesEndpoint) )
 		{
+			// AddEntityFrameworkCoreInstrumentation from OpenTelemetry.Instrumentation.EntityFrameworkCore
 			telemetryBuilder.WithTracing(tracing => tracing
 				.AddAspNetCoreInstrumentation(o => o.Filter = FilterPath)
+				.AddEntityFrameworkCoreInstrumentation(p =>
+				{
+					// DbStatementForText can contain sensitive data
+#if DEBUG
+					p.SetDbStatementForText = true;
+#else
+					p.SetDbStatementForText = false;
+#endif
+				})
 				.AddOtlpExporter(
 					o =>
 					{
@@ -70,39 +86,43 @@ public static void AddOpenTelemetryMonitoring(
 
 		// AddHttpClientInstrumentation from OpenTelemetry.Instrumentation.Http
 		telemetryBuilder.WithMetrics(metrics =>
-			metrics.AddAspNetCoreInstrumentation()
+			metrics
+				.AddAspNetCoreInstrumentation()
 				.AddRuntimeInstrumentation()
+				.AddMeter(ActivitySourceMeter.SyncNameSpace)
+				.AddMeter(ActivitySourceMeter.WorkerNameSpace)
 				.AddOtlpExporter(
 					o =>
 					{
+						o.ExportProcessorType = ExportProcessorType.Batch;
 						o.Endpoint = new Uri(appSettings.OpenTelemetry.MetricsEndpoint);
 						o.Protocol = OtlpExportProtocol.HttpProtobuf;
 						o.Headers = appSettings.OpenTelemetry.GetMetricsHeader();
 					})
 				.SetResourceBuilder(
 					ResourceBuilder.CreateDefault()
-						.AddService(appSettings.OpenTelemetry.GetServiceName())	
+						.AddService(appSettings.OpenTelemetry.GetServiceName())
 				)
 		);
 	}
-	
+
 	internal static bool FilterPath(HttpContext context)
 	{
-		if ( (context.Request.Path.Value?.EndsWith("/realtime") == true || 
-		     context.Request.Path.Value?.EndsWith("/api/health") == true || 
-		     context.Request.Path.Value?.EndsWith("/api/health/details") == true || 
-		     context.Request.Path.Value?.EndsWith("/api/open-telemetry/trace") == true) 
-		     && context.Response.StatusCode == 200)
+		if ( ( context.Request.Path.Value?.EndsWith("/realtime") == true ||
+		       context.Request.Path.Value?.EndsWith("/api/health") == true ||
+		       context.Request.Path.Value?.EndsWith("/api/health/details") == true ||
+		       context.Request.Path.Value?.EndsWith("/api/open-telemetry/trace") == true )
+		     && context.Response.StatusCode == 200 )
 		{
 			return false;
 		}
 
 		if ( context.Request.Path.Value?.EndsWith("/api/index") == true
-		     && context.Response.StatusCode == 401)
+		     && context.Response.StatusCode == 401 )
 		{
 			return false;
 		}
-		
+
 		return true;
 	}
 }
diff --git a/starsky/starsky.foundation.webtelemetry/Helpers/FlushApplicationInsights.cs b/starsky/starsky.foundation.webtelemetry/Helpers/FlushApplicationInsights.cs
deleted file mode 100644
index 532961658b..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Helpers/FlushApplicationInsights.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.ApplicationInsights;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.DependencyInjection;
-using starsky.foundation.platform.Interfaces;
-using starsky.foundation.platform.Models;
-
-namespace starsky.foundation.webtelemetry.Helpers
-{
-	public sealed class FlushApplicationInsights
-	{
-		private readonly ServiceProvider? _serviceProvider;
-		private readonly IApplicationBuilder? _app;
-		private readonly IWebLogger? _logger;
-		private readonly AppSettings? _appSettings;
-
-		public FlushApplicationInsights(ServiceProvider serviceProvider, AppSettings? appSettings = null, IWebLogger? logger = null)
-		{
-			_serviceProvider = serviceProvider;
-			_logger = logger;
-			_appSettings = appSettings;
-		}
-
-		public FlushApplicationInsights(IApplicationBuilder app)
-		{
-			_app = app;
-		}
-
-		internal TelemetryClient? GetTelemetryClient()
-		{
-			var client = _serviceProvider != null ? _serviceProvider.GetService() : _app?.ApplicationServices.GetService();
-			if ( client == null && _appSettings != null && !string.IsNullOrEmpty(_appSettings.ApplicationInsightsConnectionString) )
-			{
-				_logger?.LogInformation("TelemetryClient is null on exit");
-			}
-			return client;
-		}
-
-		public async Task FlushAsync()
-		{
-			var client = GetTelemetryClient();
-			if ( client == null ) return;
-			await client.FlushAsync(CancellationToken.None);
-			await Task.Delay(10);
-		}
-		
-		public void Flush()
-		{
-			var client = GetTelemetryClient();
-			if ( client == null ) return;
-			client.FlushAsync(CancellationToken.None).ConfigureAwait(false);
-			Thread.Sleep(10);
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Helpers/MetricsHelper.cs b/starsky/starsky.foundation.webtelemetry/Helpers/MetricsHelper.cs
deleted file mode 100644
index de7886ee1d..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Helpers/MetricsHelper.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Microsoft.ApplicationInsights;
-using Microsoft.ApplicationInsights.DataContracts;
-
-namespace starsky.foundation.webtelemetry.Helpers;
-
-public static class MetricsHelper
-{
-	public static bool Add(TelemetryClient? telemetryClient, string name, int value)
-	{
-		if ( telemetryClient == null )
-		{
-			return false;
-		}
-		
-		var sample = new MetricTelemetry
-		{
-			Name = name, 
-			Sum = value
-		};
-		
-		telemetryClient.TrackMetric(sample);
-		return true;
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Helpers/RequestTelemetryHelper.cs b/starsky/starsky.foundation.webtelemetry/Helpers/RequestTelemetryHelper.cs
deleted file mode 100644
index 1a59294320..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Helpers/RequestTelemetryHelper.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-#nullable enable
-using System;
-using System.Text.Json;
-using Microsoft.ApplicationInsights;
-using Microsoft.ApplicationInsights.DataContracts;
-using Microsoft.ApplicationInsights.Extensibility;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using starsky.foundation.webtelemetry.Models;
-
-namespace starsky.foundation.webtelemetry.Helpers
-{
-	public static class RequestTelemetryHelper
-	{
-		public static string GetOperationId(this HttpContext? httpContext)
-		{
-			if ( httpContext == null ) return string.Empty;
-			var requestTelemetry = httpContext.Features.Get();
-			return requestTelemetry == null ? string.Empty : requestTelemetry.Context.Operation.Id;
-		}
-
-		private static TelemetryClient? GetTelemetryClient(IServiceScopeFactory scopeFactory)
-		{
-			return scopeFactory.CreateScope()
-				.ServiceProvider.GetService();
-		}
-
-		public static IOperationHolder GetOperationHolder(IServiceScopeFactory scopeFactory, string jobName, string? operationId)
-		{
-			if ( string.IsNullOrEmpty(operationId) )
-			{
-				return new EmptyOperationHolder();
-			}
-			var telemetryClient = GetTelemetryClient(scopeFactory);
-
-			if ( telemetryClient == null ) return new EmptyOperationHolder();
-			
-			var operationHolder = telemetryClient.StartOperation(
-					jobName, operationId);
-			operationHolder.Telemetry.Timestamp = DateTimeOffset.UtcNow;
-			return operationHolder;
-		}
-
-		public static void SetData(this IOperationHolder? operationHolder, IServiceScopeFactory scopeFactory, object? data)
-		{
-
-			if ( data == null || operationHolder == null ) return;
-			operationHolder.Telemetry.Data = JsonSerializer.Serialize(data);
-			operationHolder.Telemetry.Target = "Task";
-			operationHolder.Telemetry.Type = "BackgroundTask";
-			operationHolder.Telemetry.ResultCode = "OK";
-			operationHolder.Telemetry.Duration = DateTimeOffset.UtcNow - operationHolder.Telemetry.Timestamp;
-			var telemetryClient = GetTelemetryClient(scopeFactory);
-			telemetryClient?.StopOperation(operationHolder);
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Helpers/SetupLogging.cs b/starsky/starsky.foundation.webtelemetry/Helpers/SetupLogging.cs
index e222ab25f6..bd2b67f39d 100644
--- a/starsky/starsky.foundation.webtelemetry/Helpers/SetupLogging.cs
+++ b/starsky/starsky.foundation.webtelemetry/Helpers/SetupLogging.cs
@@ -14,7 +14,8 @@ namespace starsky.foundation.webtelemetry.Helpers
 	public static class SetupLogging
 	{
 		[SuppressMessage("Usage", "S4792:Make sure that this logger's configuration is safe.")]
-		public static void AddTelemetryLogging(this IServiceCollection services, AppSettings appSettings)
+		public static void AddTelemetryLogging(this IServiceCollection services,
+			AppSettings appSettings)
 		{
 			services.AddLogging(logging =>
 			{
@@ -23,33 +24,21 @@ public static void AddTelemetryLogging(this IServiceCollection services, AppSett
 
 				if ( !string.IsNullOrEmpty(appSettings.OpenTelemetry?.LogsEndpoint) )
 				{
-					logging.AddOpenTelemetry(builder => 
+					logging.AddOpenTelemetry(builder =>
 						builder.AddOtlpExporter(
-						options =>
-						{
-							options.Protocol = OtlpExportProtocol.HttpProtobuf;
-							options.Headers = appSettings.OpenTelemetry.GetLogsHeader();
-							options.Endpoint = new Uri(appSettings.OpenTelemetry.LogsEndpoint);
-						})
-						.SetResourceBuilder(
-							ResourceBuilder.CreateDefault()
-								.AddService(appSettings.OpenTelemetry.GetServiceName())
-						)
+								options =>
+								{
+									options.Protocol = OtlpExportProtocol.HttpProtobuf;
+									options.Headers = appSettings.OpenTelemetry.GetLogsHeader();
+									options.Endpoint =
+										new Uri(appSettings.OpenTelemetry.LogsEndpoint);
+								})
+							.SetResourceBuilder(
+								ResourceBuilder.CreateDefault()
+									.AddService(appSettings.OpenTelemetry.GetServiceName())
+							)
 					);
 				}
-				
-				// Remove when ApplicationInsights is phased out
-				if (appSettings.ApplicationInsightsLog != true || 
-				    string.IsNullOrWhiteSpace(appSettings.ApplicationInsightsConnectionString)) return;
-	            
-				logging.AddApplicationInsights(
-					telemetryConfiguration =>
-					{
-						telemetryConfiguration.ConnectionString = appSettings.ApplicationInsightsConnectionString;
-					},
-					_ => { });
-				// End Remove when ApplicationInsights is phased out
-
 			});
 
 			services.AddScoped();
diff --git a/starsky/starsky.foundation.webtelemetry/Initializers/CloudRoleNameInitializer.cs b/starsky/starsky.foundation.webtelemetry/Initializers/CloudRoleNameInitializer.cs
deleted file mode 100644
index deb1a6cd47..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Initializers/CloudRoleNameInitializer.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using Microsoft.ApplicationInsights.Channel;
-using Microsoft.ApplicationInsights.Extensibility;
-
-namespace starsky.foundation.webtelemetry.Initializers
-{
-	public sealed class CloudRoleNameInitializer : ITelemetryInitializer
-	{
-		private readonly string _roleName;
-
-		public CloudRoleNameInitializer(string roleName)
-		{
-			_roleName = roleName ?? throw new ArgumentNullException(nameof(roleName));
-		}
-
-		public void Initialize(ITelemetry telemetry)
-		{
-			telemetry.Context.Cloud.RoleName = _roleName;
-			telemetry.Context.Cloud.RoleInstance = Environment.MachineName;
-			// @see: TelemetryConfigurationHelper
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Interfaces/ITelemetryService.cs b/starsky/starsky.foundation.webtelemetry/Interfaces/ITelemetryService.cs
deleted file mode 100644
index 24c09492e4..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Interfaces/ITelemetryService.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace starsky.foundation.webtelemetry.Interfaces
-{
-	public interface ITelemetryService
-	{
-		bool TrackException(Exception exception);
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Models/EmthyOperationHolder.cs b/starsky/starsky.foundation.webtelemetry/Models/EmthyOperationHolder.cs
deleted file mode 100644
index cae32c8e1c..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Models/EmthyOperationHolder.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using Microsoft.ApplicationInsights.Extensibility;
-
-namespace starsky.foundation.webtelemetry.Models
-{
-	public sealed class EmptyOperationHolder : IOperationHolder where T : new()
-	{
-		public EmptyOperationHolder()
-		{
-			Telemetry = new T();
-		}
-
-		// ReSharper disable once ConvertToConstant.Global
-		public readonly bool Empty = true;
-
-		public void Dispose()
-		{
-			Dispose(true);
-			GC.SuppressFinalize(this);
-		}
-
-		private bool IsDisposed { get; set; }
-
-		private void Dispose(bool disposing)
-		{
-			if ( disposing && !IsDisposed )
-			{
-				IsDisposed = true;
-			}
-		}
-
-		public T Telemetry { get; }
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Processor/FilterWebsocketsTelemetryProcessor.cs b/starsky/starsky.foundation.webtelemetry/Processor/FilterWebsocketsTelemetryProcessor.cs
deleted file mode 100644
index 301e6affeb..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Processor/FilterWebsocketsTelemetryProcessor.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Microsoft.ApplicationInsights.Channel;
-using Microsoft.ApplicationInsights.DataContracts;
-using Microsoft.ApplicationInsights.Extensibility;
-
-namespace starsky.foundation.webtelemetry.Processor
-{
-	public sealed class FilterWebsocketsTelemetryProcessor : ITelemetryProcessor
-	{
-		private readonly ITelemetryProcessor _next;
-
-		public FilterWebsocketsTelemetryProcessor(ITelemetryProcessor next)
-		{
-			// Next TelemetryProcessor in the chain
-			_next = next;
-		}
-
-		public void Process(ITelemetry item)
-		{
-			if (item is RequestTelemetry request && request.ResponseCode == "101")
-			{
-				return;
-			}
-
-			// Send the item to the next TelemetryProcessor
-			_next.Process(item);
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs b/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs
deleted file mode 100644
index 0f953370ab..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Services/ApplicationInsightsJsHelper.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-#nullable enable
-using System;
-using System.Linq;
-using System.Security.Claims;
-using Microsoft.ApplicationInsights.AspNetCore;
-using Microsoft.AspNetCore.Http;
-using starsky.foundation.injection;
-
-namespace starsky.foundation.webtelemetry.Services
-{
-	/// 
-	/// Remove when App insights is phased out
-	/// 
-	[Service(InjectionLifetime = InjectionLifetime.Scoped)]
-	public sealed class ApplicationInsightsJsHelper
-	{
-		private readonly IHttpContextAccessor? _httpContext;
-		private readonly JavaScriptSnippet? _aiJavaScriptSnippet;
-        
-		/// 
-		/// Init script helper - need a csp-nonce in the context
-		/// 
-		/// Your IHttpContext
-		/// the snip-it from app insights
-		public ApplicationInsightsJsHelper(IHttpContextAccessor? httpContext, JavaScriptSnippet? aiJavaScriptSnippet = null)
-		{
-			_httpContext = httpContext;
-			if(aiJavaScriptSnippet != null && !string.IsNullOrEmpty(aiJavaScriptSnippet.FullScript)) _aiJavaScriptSnippet = aiJavaScriptSnippet;
-		}
-		
-		/// 
-		/// Get the App Insights front-end script with your app insights token visible
-		/// Need a csp-nonce in the context
-		/// 
-		public string ScriptTag
-		{
-			get
-			{
-				if ( _aiJavaScriptSnippet == null ) return "";
-				var js = _aiJavaScriptSnippet.FullScript;
-				
-				// Replace the default script with a nonce version, to avoid XSS attacks
-				const string scriptTagStart = @"";
-				
-				var script = js.Replace(scriptTagStart, string.Empty);
-				script = script.Replace(scriptEndStart, string.Empty);
-				
-				const string setAuthenticatedUserContextItemStart =
-					"appInsights.setAuthenticatedUserContext(\"";
-				const string setAuthenticatedUserContextItemEnd = "\")";
-				
-				script = script.Replace(
-					setAuthenticatedUserContextItemStart + 
-					setAuthenticatedUserContextItemEnd, 
-					$"\n {setAuthenticatedUserContextItemStart}" + 
-					GetCurrentUserId() + 
-					setAuthenticatedUserContextItemEnd);
-
-				// when a react plugin is enabled disable: enableAutoRouteTracking
-				script += "\n appInsights.enableAutoRouteTracking = true;";
-				script += "\n appInsights.disableFetchTracking = false;";
-				script += "\n appInsights.enableAjaxPerfTracking = true;";
-				script += "\n appInsights.enableRequestHeaderTracking = true;";
-				script += "\n appInsights.enableResponseHeaderTracking = true;";
-
-				// set role and roleInstance WebController
-				script += "\n const telemetryInitializer = (envelope) => {";
-				script +=  "\n\tenvelope.tags[\"ai.cloud.role\"] = \"WebController\";";
-				script +=  $"\n\tenvelope.tags[\"ai.cloud.roleInstance\"] = \"{Environment.MachineName}\";";
-				script += "\n } ";
-				script += "\n appInsights.addTelemetryInitializer(telemetryInitializer);";
-
-				return script;
-			}
-		}
-
-		internal string? GetCurrentUserId()
-		{
-			if (_httpContext?.HttpContext?.User.Identity?.IsAuthenticated == false)
-			{
-				return string.Empty;
-			}
-
-			return _httpContext!.HttpContext!.User.Claims
-				.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)
-				?.Value;
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Services/FilterStatusCodesInitializer.cs b/starsky/starsky.foundation.webtelemetry/Services/FilterStatusCodesInitializer.cs
deleted file mode 100644
index 3bfedecbf8..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Services/FilterStatusCodesInitializer.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Microsoft.ApplicationInsights.Channel;
-using Microsoft.ApplicationInsights.DataContracts;
-using Microsoft.ApplicationInsights.Extensibility;
-using starsky.foundation.injection;
-
-namespace starsky.foundation.webtelemetry.Services
-{
-	/// 
-	/// App Insights Filter Status Codes Initializer
-	/// Remove when App insights is phased out
-	/// 
-	[Service(typeof(ITelemetryInitializer), InjectionLifetime = InjectionLifetime.Singleton)]
-	public sealed class FilterStatusCodesInitializer : ITelemetryInitializer
-	{
-		public void Initialize(ITelemetry telemetry)
-		{
-			var request = telemetry as RequestTelemetry;
-			if ( request == null ) return;
-			
-			switch (request.ResponseCode)
-			{
-				case "401":
-				case "404":
-					request.Success = true;
-					break;
-
-			}
-		}
-	}
-}
diff --git a/starsky/starsky.foundation.webtelemetry/Services/TelemetryService.cs b/starsky/starsky.foundation.webtelemetry/Services/TelemetryService.cs
deleted file mode 100644
index 2531eb2d4d..0000000000
--- a/starsky/starsky.foundation.webtelemetry/Services/TelemetryService.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using Microsoft.ApplicationInsights;
-using Microsoft.ApplicationInsights.Extensibility;
-using starsky.foundation.injection;
-using starsky.foundation.platform.Models;
-using starsky.foundation.webtelemetry.Interfaces;
-
-namespace starsky.foundation.webtelemetry.Services
-{
-	/// 
-	/// Remove when App insights is phased out
-	/// 
-	[Service(typeof(ITelemetryService), InjectionLifetime = InjectionLifetime.Singleton)]
-	public sealed class TelemetryService : ITelemetryService
-	{
-		private readonly TelemetryClient? _telemetry;
-
-		public TelemetryService(AppSettings? appSettings)
-		{
-			if (appSettings == null ||  string.IsNullOrEmpty(appSettings.ApplicationInsightsConnectionString) ) return;
-			_telemetry = new TelemetryClient(TelemetryConfiguration.CreateDefault())
-			{
-				TelemetryConfiguration =
-				{
-					ConnectionString = appSettings.ApplicationInsightsConnectionString
-				}
-			};
-		}
-		public bool TrackException(Exception exception)
-		{
-			if ( _telemetry == null ) return false;
-			_telemetry.TrackException(exception);
-			return true;
-		}
-	}
-
-}
diff --git a/starsky/starsky.foundation.webtelemetry/starsky.foundation.webtelemetry.csproj b/starsky/starsky.foundation.webtelemetry/starsky.foundation.webtelemetry.csproj
index 5878e961b7..9ddec18c4e 100644
--- a/starsky/starsky.foundation.webtelemetry/starsky.foundation.webtelemetry.csproj
+++ b/starsky/starsky.foundation.webtelemetry/starsky.foundation.webtelemetry.csproj
@@ -1,5 +1,5 @@
 
-    
+
     
         net8.0
         
@@ -10,27 +10,28 @@
         latest
         enable
     
-    
+
     
-      
-      
-      
+        
+        
+        
     
 
     
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
-      
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
     
 
     
diff --git a/starsky/starsky.foundation.worker/CpuEventListener/CpuUsageListener.cs b/starsky/starsky.foundation.worker/CpuEventListener/CpuUsageListener.cs
index 3428260695..a4d9cb566f 100644
--- a/starsky/starsky.foundation.worker/CpuEventListener/CpuUsageListener.cs
+++ b/starsky/starsky.foundation.worker/CpuEventListener/CpuUsageListener.cs
@@ -2,7 +2,6 @@
 using System.Collections.ObjectModel;
 using System.Diagnostics.Tracing;
 using starsky.foundation.injection;
-using starsky.foundation.platform.Interfaces;
 using starsky.foundation.worker.CpuEventListener.Interfaces;
 
 namespace starsky.foundation.worker.CpuEventListener;
@@ -15,9 +14,9 @@ public sealed class CpuUsageListener : EventListener, ICpuUsageListener
 
 	protected override void OnEventSourceCreated(EventSource eventSource)
 	{
-		if (eventSource.Name.Equals("System.Runtime"))
-			EnableEvents(eventSource, EventLevel.LogAlways, 
-				EventKeywords.All, 
+		if ( eventSource.Name.Equals("System.Runtime") )
+			EnableEvents(eventSource, EventLevel.LogAlways,
+				EventKeywords.All,
 				new Dictionary { { "EventCounterIntervalSec", "15" } }!);
 	}
 
@@ -27,23 +26,22 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData)
 		base.OnEventWritten(eventData);
 	}
 
-	internal void UpdateEventData(string? eventDataEventName, ReadOnlyCollection? eventDataPayload)
+	internal void UpdateEventData(string? eventDataEventName,
+		ReadOnlyCollection? eventDataPayload)
 	{
-		if (eventDataEventName != "EventCounters" || eventDataPayload == null || eventDataPayload.Count == 0)
+		if ( eventDataEventName != "EventCounters" || eventDataPayload == null ||
+			 eventDataPayload.Count == 0 )
 			return;
 
 		if ( eventDataPayload[0] is not IDictionary
-			     eventPayload ||
-		     !eventPayload.TryGetValue("Name", out var nameData) ||
-		     nameData is not ("cpu-usage") ) return;
-		
+				 eventPayload ||
+			 !eventPayload.TryGetValue("Name", out var nameData) ||
+			 nameData is not ( "cpu-usage" ) ) return;
+
 		if ( !eventPayload.TryGetValue("Mean", out var value) ) return;
-		
+
 		if ( value is not double dValue ) return;
 		CpuUsageMean = dValue;
 		IsReady = true;
 	}
 }
-
-
-
diff --git a/starsky/starsky.foundation.worker/Helpers/ProcessTaskQueue.cs b/starsky/starsky.foundation.worker/Helpers/ProcessTaskQueue.cs
index fa04cce162..e6adb265a8 100644
--- a/starsky/starsky.foundation.worker/Helpers/ProcessTaskQueue.cs
+++ b/starsky/starsky.foundation.worker/Helpers/ProcessTaskQueue.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Threading;
 using System.Threading.Channels;
@@ -12,26 +13,26 @@ namespace starsky.foundation.worker.Helpers
 {
 	public static class ProcessTaskQueue
 	{
-		public static Tuple RoundUp(AppSettings appSettings)
+		public static Tuple RoundUp(AppSettings appSettings)
 		{
 			if ( appSettings.UseDiskWatcherIntervalInMilliseconds <= 0 )
 			{
 				return new Tuple(TimeSpan.Zero, 0);
 			}
-			
+
 			var current = DateTime.UtcNow.TimeOfDay.TotalMilliseconds;
 			var atMinuteInBlock = current % appSettings.UseDiskWatcherIntervalInMilliseconds;
 			var msToAdd = appSettings.UseDiskWatcherIntervalInMilliseconds - atMinuteInBlock;
 			return new Tuple(TimeSpan.FromMilliseconds(msToAdd), current);
 		}
-		
+
 		public static async Task ProcessBatchedLoopAsync(
 			IBaseBackgroundTaskQueue taskQueue, IWebLogger logger, AppSettings appSettings,
 			CancellationToken cancellationToken)
 		{
 			await Task.Yield();
-			
-			while (!cancellationToken.IsCancellationRequested)
+
+			while ( !cancellationToken.IsCancellationRequested )
 			{
 				try
 				{
@@ -46,26 +47,32 @@ public static async Task ProcessBatchedLoopAsync(
 					{
 						continue;
 					}
-				
+
 					var toDoItems = new List,
-						string>>();
+						string?, string?>>();
 
 					for ( var i = 0; i < taskQueueCount; i++ )
 					{
-						var (workItem,metaData) = await taskQueue.DequeueAsync(cancellationToken);
-						toDoItems.Add(new Tuple, string>(workItem,metaData));
+						var (workItem, metaData, parentTraceId) = await taskQueue.DequeueAsync(cancellationToken);
+						toDoItems.Add(
+							new Tuple, string?, string?>(workItem,
+								metaData, parentTraceId));
 					}
 
 					var afterDistinct = toDoItems.DistinctBy(p => p.Item2).ToList();
 
-					foreach ( var (task, meta) in afterDistinct )
+					foreach ( var (task, meta, _) in afterDistinct )
 					{
-						logger.LogInformation($"[{taskQueue.GetType().ToString().Split(".").LastOrDefault()}] next task: " + meta);
+						logger.LogInformation(
+							$"[{taskQueue.GetType().ToString().Split(".").LastOrDefault()}] next task: " +
+							meta);
 						await ExecuteTask(task, logger, null, cancellationToken);
 					}
-					logger.LogInformation($"[{taskQueue.GetType().ToString().Split(".").LastOrDefault()}] next done & wait ");
+
+					logger.LogInformation(
+						$"[{taskQueue.GetType().ToString().Split(".").LastOrDefault()}] next done & wait ");
 				}
-				catch ( TaskCanceledException)
+				catch ( TaskCanceledException )
 				{
 					// do nothing
 				}
@@ -77,32 +84,67 @@ private static async Task ExecuteTask(
 			IWebLogger logger,
 			IBaseBackgroundTaskQueue? taskQueue, CancellationToken cancellationToken)
 		{
+			string? metaData = null;
+			var activity = CreateActivity(null, metaData);
+
 			try
 			{
 				if ( taskQueue != null )
 				{
-					(workItem, _ ) = await taskQueue.DequeueAsync(cancellationToken);
-					// _ is metaData from the queue
+					string? parentTraceId;
+					// Dequeue here
+					( workItem, metaData, parentTraceId ) =
+						await taskQueue.DequeueAsync(cancellationToken);
+					
+					// set as parent for activity
+					activity = CreateActivity(parentTraceId, metaData);
 				}
+
+				activity.Start();
+
 				await workItem(cancellationToken);
+
+				StopActivity(activity);
 			}
-			catch (OperationCanceledException)
+			catch ( OperationCanceledException )
 			{
 				// do nothing! Prevent throwing if stoppingToken was signaled
 			}
-			catch (Exception ex)
+			catch ( Exception ex )
 			{
 				logger.LogError(ex, "Error occurred executing task work item.");
 			}
 		}
 
-		public static async Task ProcessTaskQueueAsync(IBaseBackgroundTaskQueue taskQueue, 
+		private static Activity CreateActivity(string? parentTraceId, string? metaData)
+		{
+			metaData ??= nameof(ProcessTaskQueue);
+			var activity = new Activity(metaData);
+			if ( parentTraceId == null )
+			{
+				return activity;
+			}
+			activity.SetParentId(parentTraceId);
+			return activity;
+		}
+
+		private static void StopActivity(Activity activity)
+		{
+			if ( activity.Duration == TimeSpan.Zero )
+			{
+				activity.SetEndTime(DateTime.UtcNow);
+			}
+
+			activity.Stop();
+		}
+
+		public static async Task ProcessTaskQueueAsync(IBaseBackgroundTaskQueue taskQueue,
 			IWebLogger logger, CancellationToken cancellationToken)
 		{
 			logger.LogInformation($"Queued Hosted Service {taskQueue.GetType().Name} is " +
 			                      $"starting on {Environment.MachineName}");
-		
-			while (!cancellationToken.IsCancellationRequested)
+
+			while ( !cancellationToken.IsCancellationRequested )
 			{
 				await ExecuteTask(null!, logger, taskQueue, cancellationToken);
 			}
@@ -111,19 +153,23 @@ public static async Task ProcessTaskQueueAsync(IBaseBackgroundTaskQueue taskQueu
 		public static readonly BoundedChannelOptions DefaultBoundedChannelOptions =
 			new(int.MaxValue) { FullMode = BoundedChannelFullMode.Wait };
 
-		public static ValueTask QueueBackgroundWorkItemAsync(Channel, string>> channel,
-			Func workItem, string metaData)
+		public static ValueTask QueueBackgroundWorkItemAsync(
+			Channel, string?, string?>> channel,
+			Func workItem,
+			string? metaData = null, string? traceParentId = null)
 		{
 			ArgumentNullException.ThrowIfNull(workItem);
-			return QueueBackgroundWorkItemInternalAsync(channel, workItem, metaData);
+			return QueueBackgroundWorkItemInternalAsync(channel, workItem, metaData, traceParentId);
 		}
-		
+
 		private static async ValueTask QueueBackgroundWorkItemInternalAsync(
-			Channel, string>> channel,
-			Func workItem, string metaData)
+			Channel, string?, string?>> channel,
+			Func workItem,
+			string? metaData = null, string? traceParentId = null)
 		{
-			await channel.Writer.WriteAsync(new Tuple, string>(workItem,metaData));
+			await channel.Writer.WriteAsync(
+				new Tuple, string?, string?>(workItem, metaData,
+					traceParentId));
 		}
-		
 	}
 }
diff --git a/starsky/starsky.foundation.worker/Interfaces/IBaseBackgroundTaskQueue.cs b/starsky/starsky.foundation.worker/Interfaces/IBaseBackgroundTaskQueue.cs
index 034f3f5bec..29ab1ace88 100644
--- a/starsky/starsky.foundation.worker/Interfaces/IBaseBackgroundTaskQueue.cs
+++ b/starsky/starsky.foundation.worker/Interfaces/IBaseBackgroundTaskQueue.cs
@@ -7,11 +7,12 @@ namespace starsky.foundation.worker.Interfaces
 	public interface IBaseBackgroundTaskQueue
 	{
 		public int Count();
+
 		ValueTask QueueBackgroundWorkItemAsync(
-			Func workItem, 
-			string metaData);
-		
-		ValueTask, string>> DequeueAsync(
+			Func workItem,
+			string? metaData = null, string? traceParentId = null);
+
+		ValueTask, string?, string?>> DequeueAsync(
 			CancellationToken cancellationToken);
 	}
 }
diff --git a/starsky/starsky.foundation.worker/Metrics/ThumbnailBackgroundQueuedMetrics.cs b/starsky/starsky.foundation.worker/Metrics/ThumbnailBackgroundQueuedMetrics.cs
new file mode 100644
index 0000000000..b288b381a3
--- /dev/null
+++ b/starsky/starsky.foundation.worker/Metrics/ThumbnailBackgroundQueuedMetrics.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.Metrics;
+using starsky.foundation.injection;
+using starsky.foundation.platform.MetricsNamespaces;
+
+namespace starsky.foundation.worker.Metrics;
+
+[Service(typeof(ThumbnailBackgroundQueuedMetrics),
+	InjectionLifetime = InjectionLifetime.Singleton)]
+public class ThumbnailBackgroundQueuedMetrics
+{
+	public int Value { get; set; }
+
+	public ThumbnailBackgroundQueuedMetrics(IMeterFactory meterFactory)
+	{
+		var meter = meterFactory.Create(ActivitySourceMeter.WorkerNameSpace);
+		const string name = "_s." + nameof(ThumbnailBackgroundQueuedMetrics);
+		meter.CreateObservableGauge(name, ObserveValue);
+	}
+
+	private int ObserveValue()
+	{
+		return Value;
+	}
+}
diff --git a/starsky/starsky.foundation.worker/Metrics/UpdateBackgroundQueuedMetrics.cs b/starsky/starsky.foundation.worker/Metrics/UpdateBackgroundQueuedMetrics.cs
new file mode 100644
index 0000000000..25df2b95a0
--- /dev/null
+++ b/starsky/starsky.foundation.worker/Metrics/UpdateBackgroundQueuedMetrics.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.Metrics;
+using starsky.foundation.injection;
+using starsky.foundation.platform.MetricsNamespaces;
+
+namespace starsky.foundation.worker.Metrics;
+
+[Service(typeof(UpdateBackgroundQueuedMetrics),
+	InjectionLifetime = InjectionLifetime.Singleton)]
+public class UpdateBackgroundQueuedMetrics
+{
+	public int Value { get; set; }
+
+	public UpdateBackgroundQueuedMetrics(IMeterFactory meterFactory)
+	{
+		var meter = meterFactory.Create(ActivitySourceMeter.WorkerNameSpace);
+		const string name = "_s." + nameof(UpdateBackgroundQueuedMetrics);
+		meter.CreateObservableGauge(name, ObserveValue);
+	}
+
+	private int ObserveValue()
+	{
+		return Value;
+	}
+}
diff --git a/starsky/starsky.foundation.worker/Services/UpdateBackgroundQueuedHostedService.cs b/starsky/starsky.foundation.worker/Services/UpdateBackgroundQueuedHostedService.cs
index 2f4d9d9667..fdff0be9f9 100755
--- a/starsky/starsky.foundation.worker/Services/UpdateBackgroundQueuedHostedService.cs
+++ b/starsky/starsky.foundation.worker/Services/UpdateBackgroundQueuedHostedService.cs
@@ -1,4 +1,3 @@
-#nullable enable
 using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading.Tasks;
diff --git a/starsky/starsky.foundation.worker/Services/UpdateBackgroundTaskQueue.cs b/starsky/starsky.foundation.worker/Services/UpdateBackgroundTaskQueue.cs
index ed2291efbb..acb3ae9293 100644
--- a/starsky/starsky.foundation.worker/Services/UpdateBackgroundTaskQueue.cs
+++ b/starsky/starsky.foundation.worker/Services/UpdateBackgroundTaskQueue.cs
@@ -1,49 +1,51 @@
-using System;
+using System;
 using System.Threading;
 using System.Threading.Channels;
 using System.Threading.Tasks;
-using Microsoft.ApplicationInsights;
 using Microsoft.Extensions.DependencyInjection;
 using starsky.foundation.injection;
-using starsky.foundation.webtelemetry.Helpers;
 using starsky.foundation.worker.Helpers;
 using starsky.foundation.worker.Interfaces;
+using starsky.foundation.worker.Metrics;
 
-namespace starsky.foundation.worker.Services
+namespace starsky.foundation.worker.Services;
+
+/// 
+/// @see: https://learn.microsoft.com/en-us/dotnet/core/extensions/queue-service
+/// 
+[Service(typeof(IUpdateBackgroundTaskQueue), InjectionLifetime = InjectionLifetime.Singleton)]
+public sealed class UpdateBackgroundTaskQueue : IUpdateBackgroundTaskQueue
 {
-    /// 
-    /// @see: https://learn.microsoft.com/en-us/dotnet/core/extensions/queue-service
-    /// 
-    [Service(typeof(IUpdateBackgroundTaskQueue), InjectionLifetime = InjectionLifetime.Singleton)]
-    public sealed class UpdateBackgroundTaskQueue : IUpdateBackgroundTaskQueue
-    {
-	    private readonly TelemetryClient? _telemetryClient;
-	    private readonly Channel, string>> _queue;
+	private readonly Channel, string?, string?>> _queue;
+	private readonly UpdateBackgroundQueuedMetrics _metrics;
+
+	public UpdateBackgroundTaskQueue(IServiceScopeFactory scopeFactory)
+	{
+		_queue = Channel.CreateBounded,
+			string?, string?>>(ProcessTaskQueue.DefaultBoundedChannelOptions);
+		
+		_metrics = scopeFactory.CreateScope().ServiceProvider
+			.GetRequiredService();
+	}
+
+	public int Count()
+	{
+		return _queue.Reader.Count;
+	}
 
-	    public UpdateBackgroundTaskQueue(IServiceScopeFactory scopeFactory)
-	    {
-		    _telemetryClient = scopeFactory.CreateScope().ServiceProvider
-			    .GetService();
-		    _queue = Channel.CreateBounded, 
-			    string>>(ProcessTaskQueue.DefaultBoundedChannelOptions);
-	    }
-	    
-	    public int Count()
-	    {
-		    return _queue.Reader.Count;
-	    }
-	    
-	    public ValueTask QueueBackgroundWorkItemAsync(
-		    Func workItem, string metaData)
-	    {
-		    return ProcessTaskQueue.QueueBackgroundWorkItemAsync(_queue, workItem, metaData);
-	    }
+	public ValueTask QueueBackgroundWorkItemAsync(
+		Func workItem,
+		string? metaData = null, string? traceParentId = null)
+	{
+		return ProcessTaskQueue.QueueBackgroundWorkItemAsync(_queue, workItem, metaData,
+			traceParentId);
+	}
 
-	    public async ValueTask, string>> DequeueAsync(
-		    CancellationToken cancellationToken)
-	    {
-		    MetricsHelper.Add(_telemetryClient, nameof(UpdateBackgroundTaskQueue), Count());
-		    return await _queue.Reader.ReadAsync(cancellationToken);
-	    }
-    }
+	public async ValueTask, string?, string?>> DequeueAsync(
+		CancellationToken cancellationToken)
+	{
+		var queueItem = await _queue.Reader.ReadAsync(cancellationToken);
+		_metrics.Value = Count();
+		return queueItem;
+	}
 }
diff --git a/starsky/starsky.foundation.worker/ThumbnailServices/Exceptions/ToManyUsageException.cs b/starsky/starsky.foundation.worker/ThumbnailServices/Exceptions/ToManyUsageException.cs
index dbc79693ef..77f1cf286d 100644
--- a/starsky/starsky.foundation.worker/ThumbnailServices/Exceptions/ToManyUsageException.cs
+++ b/starsky/starsky.foundation.worker/ThumbnailServices/Exceptions/ToManyUsageException.cs
@@ -16,14 +16,14 @@ public ToManyUsageException()
 
 		public ToManyUsageException(string message)
 			: base(message) { }
-        
+
 		/// 
 		/// Without this constructor, deserialization will fail
 		/// 
 		/// 
 		/// 
 #pragma warning disable SYSLIB0051
-		protected ToManyUsageException(SerializationInfo info, StreamingContext context) 
+		protected ToManyUsageException(SerializationInfo info, StreamingContext context)
 			: base(info, context)
 		{
 		}
diff --git a/starsky/starsky.foundation.worker/ThumbnailServices/Interfaces/IThumbnailQueuedHostedService.cs b/starsky/starsky.foundation.worker/ThumbnailServices/Interfaces/IThumbnailQueuedHostedService.cs
index 244f8fa573..1ea6f0b326 100644
--- a/starsky/starsky.foundation.worker/ThumbnailServices/Interfaces/IThumbnailQueuedHostedService.cs
+++ b/starsky/starsky.foundation.worker/ThumbnailServices/Interfaces/IThumbnailQueuedHostedService.cs
@@ -2,7 +2,7 @@
 
 namespace starsky.foundation.worker.ThumbnailServices.Interfaces;
 
-public interface IThumbnailQueuedHostedService: IBaseBackgroundTaskQueue
+public interface IThumbnailQueuedHostedService : IBaseBackgroundTaskQueue
 {
 	// nothing here
 }
diff --git a/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailBackgroundTaskQueue.cs b/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailBackgroundTaskQueue.cs
index dc39e93915..23512d8435 100644
--- a/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailBackgroundTaskQueue.cs
+++ b/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailBackgroundTaskQueue.cs
@@ -3,14 +3,13 @@
 using System.Threading;
 using System.Threading.Channels;
 using System.Threading.Tasks;
-using Microsoft.ApplicationInsights;
 using Microsoft.Extensions.DependencyInjection;
 using starsky.foundation.injection;
 using starsky.foundation.platform.Interfaces;
 using starsky.foundation.platform.Models;
-using starsky.foundation.webtelemetry.Helpers;
 using starsky.foundation.worker.CpuEventListener.Interfaces;
 using starsky.foundation.worker.Helpers;
+using starsky.foundation.worker.Metrics;
 using starsky.foundation.worker.ThumbnailServices.Exceptions;
 using starsky.foundation.worker.ThumbnailServices.Interfaces;
 
@@ -19,25 +18,30 @@ namespace starsky.foundation.worker.ThumbnailServices
 	/// 
 	/// @see: https://learn.microsoft.com/en-us/dotnet/core/extensions/queue-service
 	/// 
-	[Service(typeof(IThumbnailQueuedHostedService), InjectionLifetime = InjectionLifetime.Singleton)]
+	[Service(typeof(IThumbnailQueuedHostedService),
+		InjectionLifetime = InjectionLifetime.Singleton)]
 	public sealed class ThumbnailBackgroundTaskQueue : IThumbnailQueuedHostedService
 	{
 		private readonly ICpuUsageListener _cpuUsageListenerService;
 		private readonly IWebLogger _logger;
 		private readonly AppSettings _appSettings;
-		private readonly Channel, string>> _queue;
-		private readonly TelemetryClient? _telemetryClient;
 
-		public ThumbnailBackgroundTaskQueue(ICpuUsageListener cpuUsageListenerService, 
+		private readonly Channel, string?, string?>>
+			_queue;
+
+		private readonly ThumbnailBackgroundQueuedMetrics _metrics;
+
+		public ThumbnailBackgroundTaskQueue(ICpuUsageListener cpuUsageListenerService,
 			IWebLogger logger, AppSettings appSettings, IServiceScopeFactory scopeFactory)
 		{
 			_cpuUsageListenerService = cpuUsageListenerService;
 			_logger = logger;
 			_appSettings = appSettings;
-			_queue = Channel.CreateBounded, string>>(
-				ProcessTaskQueue.DefaultBoundedChannelOptions);
-			_telemetryClient = scopeFactory.CreateScope().ServiceProvider
-				.GetService();
+			_queue = Channel
+				.CreateBounded, string?, string?>>(
+					ProcessTaskQueue.DefaultBoundedChannelOptions);
+			_metrics = scopeFactory.CreateScope().ServiceProvider
+				.GetRequiredService();
 		}
 
 		public int Count()
@@ -47,7 +51,8 @@ public int Count()
 
 		[SuppressMessage("ReSharper", "InvertIf")]
 		public ValueTask QueueBackgroundWorkItemAsync(
-			Func workItem, string metaData)
+			Func workItem, string? metaData = null,
+			string? traceParentId = null)
 		{
 			if ( _cpuUsageListenerService.CpuUsageMean > _appSettings.CpuUsageMaxPercentage )
 			{
@@ -55,17 +60,18 @@ public ValueTask QueueBackgroundWorkItemAsync(
 				throw new ToManyUsageException($"QueueBackgroundWorkItemAsync: " +
 				                               $"Skip {metaData} because of high CPU usage");
 			}
-			
+
 			return ProcessTaskQueue.QueueBackgroundWorkItemAsync(_queue,
-				workItem, metaData);
+				workItem, metaData, traceParentId);
 		}
 
-		public async ValueTask, string>> DequeueAsync(
-			CancellationToken cancellationToken)
+		public async ValueTask, string?, string?>>
+			DequeueAsync(
+				CancellationToken cancellationToken)
 		{
-			MetricsHelper.Add(_telemetryClient, nameof(ThumbnailBackgroundTaskQueue), Count());
 			var workItem =
 				await _queue.Reader.ReadAsync(cancellationToken);
+			_metrics.Value = Count();
 			return workItem;
 		}
 	}
diff --git a/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailQueuedHostedService.cs b/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailQueuedHostedService.cs
index 395595cfe0..2b58d94b46 100644
--- a/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailQueuedHostedService.cs
+++ b/starsky/starsky.foundation.worker/ThumbnailServices/ThumbnailQueuedHostedService.cs
@@ -26,7 +26,7 @@ public ThumbnailQueuedHostedService(
 		protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 		{
 			_logger.LogInformation("Queued Hosted Service for Thumbnails");
-			await ProcessTaskQueue.ProcessTaskQueueAsync(_taskQueue, _logger, 
+			await ProcessTaskQueue.ProcessTaskQueueAsync(_taskQueue, _logger,
 				stoppingToken);
 		}
 
diff --git a/starsky/starsky.foundation.writemeta/Helpers/ExifTool.cs b/starsky/starsky.foundation.writemeta/Helpers/ExifTool.cs
index d60152bcbe..617f4a878f 100644
--- a/starsky/starsky.foundation.writemeta/Helpers/ExifTool.cs
+++ b/starsky/starsky.foundation.writemeta/Helpers/ExifTool.cs
@@ -74,7 +74,7 @@ private async Task>
 		inputStream.Close();
 
 		if ( stream.Length <= 15 && ( await StreamToStringHelper.StreamToStringAsync(stream, true) )
-		    .Contains("Fake ExifTool", StringComparison.InvariantCultureIgnoreCase) )
+			.Contains("Fake ExifTool", StringComparison.InvariantCultureIgnoreCase) )
 		{
 			_logger.LogError(
 				$"[WriteTagsAndRenameThumbnailAsync] Fake Exiftool detected {subPath}");
@@ -202,7 +202,7 @@ public async Task RunProcessAsync(string exifToolInputArguments)
 			if ( _appSettings.IsVerbose() )
 			{
 				_logger.LogInformation($"[RunProcessAsync] ~ exifTool {exifToolInputArguments} " +
-				                       $"run with result: {result.Success} ~ ");
+									   $"run with result: {result.Success} ~ ");
 			}
 
 			memoryStream.Seek(0, SeekOrigin.Begin);
@@ -212,8 +212,8 @@ public async Task RunProcessAsync(string exifToolInputArguments)
 		catch ( Win32Exception exception )
 		{
 			throw new ArgumentException("Error when trying to start the exifTool process.  " +
-			                            "Please make sure exifTool is installed, and its path is properly " +
-			                            "specified in the options.", exception);
+										"Please make sure exifTool is installed, and its path is properly " +
+										"specified in the options.", exception);
 		}
 	}
 }
diff --git a/starsky/starsky.foundation.writemeta/Helpers/ExifToolCmdHelper.cs b/starsky/starsky.foundation.writemeta/Helpers/ExifToolCmdHelper.cs
index 51969b2f40..6dad257407 100644
--- a/starsky/starsky.foundation.writemeta/Helpers/ExifToolCmdHelper.cs
+++ b/starsky/starsky.foundation.writemeta/Helpers/ExifToolCmdHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
@@ -34,7 +34,7 @@ public sealed class ExifToolCmdHelper
 		/// Thumbnail Storage Abstraction provider
 		/// ReadMeta abstraction
 		/// thumbnailQuery
-		public ExifToolCmdHelper(IExifTool exifTool, IStorage iStorage, 
+		public ExifToolCmdHelper(IExifTool exifTool, IStorage iStorage,
 			IStorage thumbnailStorage, IReadMeta readMeta, IThumbnailQuery thumbnailQuery)
 		{
 			_exifTool = exifTool;
@@ -43,7 +43,7 @@ public ExifToolCmdHelper(IExifTool exifTool, IStorage iStorage,
 			_thumbnailQuery = thumbnailQuery;
 			_thumbnailStorage = thumbnailStorage;
 		}
-		
+
 
 		/// 
 		/// To update ExifTool (both Thumbnail as Storage item)
@@ -68,9 +68,9 @@ public string Update(FileIndexItem updateModel, List inputSubPaths,
 		private static IEnumerable PathsListTagsFromFile(List inputSubPaths)
 		{
 			var pathsList = new List();
-			foreach (var subPath in inputSubPaths)
+			foreach ( var subPath in inputSubPaths )
 			{
-				if(ExtensionRolesHelper.IsExtensionForceXmp(subPath))
+				if ( ExtensionRolesHelper.IsExtensionForceXmp(subPath) )
 				{
 					var xmpPath = ExtensionRolesHelper.ReplaceExtensionWithXmp(subPath);
 					pathsList.Add(xmpPath);
@@ -92,11 +92,11 @@ private static IEnumerable PathsListTagsFromFile(List inputSubPa
 		private Task UpdateAsyncWrapperBoth(FileIndexItem updateModel, List inputSubPaths,
 			List comparedNames, bool includeSoftware = true)
 		{
-			var task = Task.Run(() => UpdateAsync(updateModel,inputSubPaths,
-				comparedNames,includeSoftware, true));
+			var task = Task.Run(() => UpdateAsync(updateModel, inputSubPaths,
+				comparedNames, includeSoftware, true));
 			return Task.FromResult(task.Wait(TimeSpan.FromSeconds(20)) ? task.Result.Item1 : string.Empty);
 		}
-	    
+
 		/// 
 		/// Get command line args for exifTool by updateModel as data, comparedNames
 		/// 
@@ -104,8 +104,8 @@ private Task UpdateAsyncWrapperBoth(FileIndexItem updateModel, Listlist of fields that are changed, other fields are ignored
 		/// to include the original software name
 		/// command line args
-		internal static string ExifToolCommandLineArgs( FileIndexItem updateModel, 
-			List comparedNames, bool includeSoftware )
+		internal static string ExifToolCommandLineArgs(FileIndexItem updateModel,
+			List comparedNames, bool includeSoftware)
 		{
 			var command = "-json -overwrite_original";
 			var initCommand = command; // to check if nothing
@@ -114,7 +114,7 @@ internal static string ExifToolCommandLineArgs( FileIndexItem updateModel,
 			// Check first if it is needed
 
 			// When change update metadata.md
-			
+
 			command = UpdateKeywordsCommand(command, comparedNames, updateModel);
 			command = UpdateDescriptionCommand(command, comparedNames, updateModel);
 			command = UpdateTitleCommand(command, comparedNames, updateModel);
@@ -128,12 +128,12 @@ internal static string ExifToolCommandLineArgs( FileIndexItem updateModel,
 			command = UpdateLocationCountryCodeCommand(command, comparedNames, updateModel);
 			command = UpdateLocationStateCommand(command, comparedNames, updateModel);
 			command = UpdateLocationCityCommand(command, comparedNames, updateModel);
-		    
+
 			command = UpdateSoftwareCommand(command, comparedNames, updateModel, includeSoftware);
 
 			command = UpdateImageHeightCommand(command, comparedNames, updateModel);
 			command = UpdateImageWidthCommand(command, comparedNames, updateModel);
-		        
+
 			command = UpdateOrientationCommand(command, comparedNames, updateModel);
 			command = UpdateDateTimeCommand(command, comparedNames, updateModel);
 
@@ -145,9 +145,9 @@ internal static string ExifToolCommandLineArgs( FileIndexItem updateModel,
 
 			command = UpdateMakeModelCommand(command, comparedNames, updateModel);
 			command = UpdateImageStabilization(command, comparedNames, updateModel);
-				
+
 			if ( command == initCommand ) return string.Empty;
-		    
+
 			return command;
 		}
 
@@ -167,25 +167,25 @@ internal async Task CreateXmpFileIsNotExist(FileIndexItem updateModel, List>> UpdateAsync(FileIndexItem updateModel,
+		public Task>> UpdateAsync(FileIndexItem updateModel,
 			List comparedNames, bool includeSoftware = true, bool renameThumbnail = true)
 		{
 			var exifUpdateFilePaths = new List
 			{
-				updateModel.FilePath!           
+				updateModel.FilePath!
 			};
 			return UpdateAsync(updateModel, exifUpdateFilePaths, comparedNames, includeSoftware, renameThumbnail);
 		}
@@ -200,8 +200,8 @@ public Task>> UpdateAsync(FileIndexItem updateMod
 		/// update name
 		/// to cancel
 		/// Tuple (command, hash)
-		public async Task>> UpdateAsync(FileIndexItem updateModel, 
-			List inputSubPaths, List comparedNames, bool includeSoftware, 
+		public async Task>> UpdateAsync(FileIndexItem updateModel,
+			List inputSubPaths, List comparedNames, bool includeSoftware,
 			bool renameThumbnail, CancellationToken cancellationToken = default)
 		{
 			// Creation and update .xmp file with all available content
@@ -209,14 +209,14 @@ public async Task>> UpdateAsync(FileIndexItem upd
 
 			// Rename .dng files .xmp to update in exifTool
 			var subPathsList = PathsListTagsFromFile(inputSubPaths);
- 
+
 			var command = ExifToolCommandLineArgs(updateModel, comparedNames, includeSoftware);
 
 			var fileHashes = new List();
 			foreach ( var path in subPathsList.Where(path => _iStorage.ExistFile(path)) )
 			{
 				// to rename to filename of the thumbnail to the new hash
-				if (!renameThumbnail )
+				if ( !renameThumbnail )
 				{
 					await _exifTool.WriteTagsAsync(path, command);
 					continue;
@@ -235,7 +235,7 @@ public async Task>> UpdateAsync(FileIndexItem upd
 				await _exifTool.WriteTagsThumbnailAsync(updateModel.FileHash, command);
 			}
 
-			return new ValueTuple>(command, fileHashes);
+			return new ValueTuple>(command, fileHashes);
 		}
 
 		private async Task BeforeFileHash(FileIndexItem updateModel, string path)
@@ -259,19 +259,19 @@ private static string UpdateLocationAltitudeCommand(
 			string command, List comparedNames, FileIndexItem updateModel)
 		{
 			// -GPSAltitude="+160" -GPSAltitudeRef=above
-			if (comparedNames.Contains(nameof(FileIndexItem.LocationAltitude).ToLowerInvariant() ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.LocationAltitude).ToLowerInvariant()) )
 			{
 				// 0 = "Above Sea Level"
 				// 1 = Below Sea Level
 				var gpsAltitudeRef = "0";
 				var gpsAltitude = "+" + updateModel.LocationAltitude.ToString(CultureInfo.InvariantCulture);
-				if (updateModel.LocationAltitude < 0)
+				if ( updateModel.LocationAltitude < 0 )
 				{
 					gpsAltitudeRef = "1";
-					gpsAltitude = "-" + (updateModel.LocationAltitude * -1).ToString(CultureInfo.InvariantCulture);
-				} 
+					gpsAltitude = "-" + ( updateModel.LocationAltitude * -1 ).ToString(CultureInfo.InvariantCulture);
+				}
 				command += $" -GPSAltitude=\"{gpsAltitude}\" -gpsaltituderef#=\"{gpsAltitudeRef}\" " +
-				           $"-xmp-exif:GPSAltitude=\"{gpsAltitude}\" -xmp-exif:gpsaltituderef#=\"{gpsAltitudeRef}\" ";
+						   $"-xmp-exif:GPSAltitude=\"{gpsAltitude}\" -xmp-exif:gpsaltituderef#=\"{gpsAltitudeRef}\" ";
 			}
 			return command;
 		}
@@ -283,7 +283,7 @@ private static string UpdateGpsLatitudeCommand(
 			// exiftool reset.jpg -gps:all= -xmp:geotag= -City= -xmp:City= -State= -xmp:State= -overwrite_original
 
 			// CultureInfo.InvariantCulture is used for systems where comma is the default seperator
-			if (comparedNames.Contains( nameof(FileIndexItem.Latitude).ToLowerInvariant() ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.Latitude).ToLowerInvariant()) )
 			{
 				var latitudeString = updateModel.Latitude.ToString(CultureInfo.InvariantCulture);
 				command +=
@@ -293,10 +293,10 @@ private static string UpdateGpsLatitudeCommand(
 			}
 			return command;
 		}
-        
+
 		private static string UpdateGpsLongitudeCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.Longitude).ToLowerInvariant()))
+			if ( comparedNames.Contains(nameof(FileIndexItem.Longitude).ToLowerInvariant()) )
 			{
 				var longitudeString = updateModel.Longitude.ToString(CultureInfo.InvariantCulture);
 				command +=
@@ -309,24 +309,24 @@ private static string UpdateGpsLongitudeCommand(string command, List com
 
 		private static string UpdateKeywordsCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.Tags).ToLowerInvariant() ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.Tags).ToLowerInvariant()) )
 			{
 				command += " -sep \", \" \"-xmp:subject\"=\"" + updateModel.Tags
 					+ $" \" -Keywords=\"{updateModel.Tags}\""; // space before
 			}
 			return command;
 		}
-        
+
 		private static string UpdateLocationCityCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.LocationCity).ToLowerInvariant() ) )
+			if ( comparedNames.Contains(nameof(FileIndexItem.LocationCity).ToLowerInvariant()) )
 			{
-				command += " -City=\"" + updateModel.LocationCity 
-				                       + "\" -xmp:City=\"" + updateModel.LocationCity + "\"";
+				command += " -City=\"" + updateModel.LocationCity
+									   + "\" -xmp:City=\"" + updateModel.LocationCity + "\"";
 			}
 			return command;
 		}
-        
+
 		/// 
 		/// Add state to ExifTool command
 		/// to remove:
@@ -339,47 +339,47 @@ private static string UpdateLocationCityCommand(string command, List com
 		/// 
 		private static string UpdateLocationStateCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.LocationState).ToLowerInvariant() ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.LocationState).ToLowerInvariant()) )
 			{
-				command += " -State=\"" + updateModel.LocationState 
-				                        + "\" -Province-State=\"" + updateModel.LocationState + "\"";
+				command += " -State=\"" + updateModel.LocationState
+										+ "\" -Province-State=\"" + updateModel.LocationState + "\"";
 			}
 			return command;
 		}
-        
+
 		private static string UpdateLocationCountryCommand(
 			string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.LocationCountry).ToLowerInvariant() ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.LocationCountry).ToLowerInvariant()) )
 			{
-				command += " -Country=\"" + updateModel.LocationCountry 
-				                          + "\" -Country-PrimaryLocationName=\"" + updateModel.LocationCountry + "\"";
+				command += " -Country=\"" + updateModel.LocationCountry
+										  + "\" -Country-PrimaryLocationName=\"" + updateModel.LocationCountry + "\"";
 			}
 			return command;
 		}
-		
+
 		private static string UpdateLocationCountryCodeCommand(
 			string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.LocationCountryCode).ToLowerInvariant() ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.LocationCountryCode).ToLowerInvariant()) )
 			{
-				command += " -Country-PrimaryLocationCode=\"" + updateModel.LocationCountryCode 
-				                          + "\" -XMP:CountryCode=\"" + updateModel.LocationCountryCode + "\"";
+				command += " -Country-PrimaryLocationCode=\"" + updateModel.LocationCountryCode
+										  + "\" -XMP:CountryCode=\"" + updateModel.LocationCountryCode + "\"";
 			}
 			return command;
 		}
-        
+
 		private static string UpdateDescriptionCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains( nameof(FileIndexItem.Description).ToLowerInvariant()    ))
+			if ( comparedNames.Contains(nameof(FileIndexItem.Description).ToLowerInvariant()) )
 			{
-				command += " -Caption-Abstract=\"" + updateModel.Description 
-				                                   + "\" -Description=\"" + updateModel.Description + "\""
-				                                   + $" \"-xmp-dc:description={updateModel.Description}\"";
+				command += " -Caption-Abstract=\"" + updateModel.Description
+												   + "\" -Description=\"" + updateModel.Description + "\""
+												   + $" \"-xmp-dc:description={updateModel.Description}\"";
 			}
 			return command;
 		}
-        
+
 		/// 
 		/// Update Software field
 		/// 
@@ -388,7 +388,7 @@ private static string UpdateDescriptionCommand(string command, List comp
 		/// the model that has the data
 		/// to include the original software name
 		/// 
-		internal static string UpdateSoftwareCommand(string command, List comparedNames, 
+		internal static string UpdateSoftwareCommand(string command, List comparedNames,
 			FileIndexItem updateModel, bool includeSoftware)
 		{
 			if ( !comparedNames.Contains(nameof(FileIndexItem.Software).ToLowerInvariant()) )
@@ -408,10 +408,10 @@ internal static string UpdateSoftwareCommand(string command, List compar
 					" -Software=\"Starsky\" -CreatorTool=\"Starsky\" -HistorySoftwareAgent=\"Starsky\" " +
 					"-HistoryParameters=\"\" -PMVersion=\"\" ";
 			}
-		    
+
 			return command;
 		}
-	    
+
 		/// 
 		/// Update Meta Field that contains Image Height (DOES NOT change the actual size)
 		/// 
@@ -427,10 +427,10 @@ private static string UpdateImageHeightCommand(string command, List comp
 			// add space before
 			command +=
 				$" -exifimageheight={updateModel.ImageHeight} ";
-		    
+
 			return command;
 		}
-	    
+
 		/// 
 		/// Update Meta Field that contains Image Width (DOES NOT change the actual size)
 		/// 
@@ -447,17 +447,17 @@ private static string UpdateImageWidthCommand(string command, List compa
 			// add space before
 			command +=
 				$" -exifimagewidth={updateModel.ImageWidth} ";
-		    
+
 			return command;
 		}
-	    
+
 		private static string UpdateTitleCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains(nameof(FileIndexItem.Title).ToLowerInvariant()))
+			if ( comparedNames.Contains(nameof(FileIndexItem.Title).ToLowerInvariant()) )
 			{
-				command += " -ObjectName=\"" + updateModel.Title + "\"" 
-				           + " \"-title\"=" + "\"" + updateModel.Title  + "\""
-				           + $" \"-xmp-dc:title={updateModel.Title}\"";
+				command += " -ObjectName=\"" + updateModel.Title + "\""
+						   + " \"-title\"=" + "\"" + updateModel.Title + "\""
+						   + $" \"-xmp-dc:title={updateModel.Title}\"";
 
 			}
 			return command;
@@ -465,14 +465,14 @@ private static string UpdateTitleCommand(string command, List comparedNa
 
 		private static string UpdateColorClassCommand(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains(nameof(FileIndexItem.ColorClass).ToLowerInvariant()) && 
-			    updateModel.ColorClass != ColorClassParser.Color.DoNotChange)
+			if ( comparedNames.Contains(nameof(FileIndexItem.ColorClass).ToLowerInvariant()) &&
+				updateModel.ColorClass != ColorClassParser.Color.DoNotChange )
 			{
-				var intColorClass = (int) updateModel.ColorClass;
-	
+				var intColorClass = ( int )updateModel.ColorClass;
+
 				var colorDisplayName = EnumHelper.GetDisplayName(updateModel.ColorClass);
-				command += " \"-xmp:Label\"=" + "\"" + colorDisplayName + "\"" + " -ColorClass=\""+ intColorClass + 
-				           "\" -Prefs=\"Tagged:0 ColorClass:" + intColorClass + " Rating:0 FrameNum:0\" ";
+				command += " \"-xmp:Label\"=" + "\"" + colorDisplayName + "\"" + " -ColorClass=\"" + intColorClass +
+						   "\" -Prefs=\"Tagged:0 ColorClass:" + intColorClass + " Rating:0 FrameNum:0\" ";
 			}
 			return command;
 		}
@@ -481,11 +481,11 @@ private static string UpdateOrientationCommand(string command, List comp
 			FileIndexItem updateModel)
 		{
 			// // exiftool -Orientation#=5
-			if (comparedNames.Contains( nameof(FileIndexItem.Orientation).ToLowerInvariant() ) && 
-			    updateModel.Orientation != FileIndexItem.Rotation.DoNotChange)
+			if ( comparedNames.Contains(nameof(FileIndexItem.Orientation).ToLowerInvariant()) &&
+				updateModel.Orientation != FileIndexItem.Rotation.DoNotChange )
 			{
-				var intOrientation = (int) updateModel.Orientation;
-				command += " \"-Orientation#="+ intOrientation +"\" ";
+				var intOrientation = ( int )updateModel.Orientation;
+				command += " \"-Orientation#=" + intOrientation + "\" ";
 			}
 			return command;
 		}
@@ -495,14 +495,14 @@ private static string UpdateDateTimeCommand(string command, List compare
 		{
 
 			if ( comparedNames.Contains(nameof(FileIndexItem.DateTime).ToLowerInvariant()) &&
-			     updateModel.DateTime.Year > 2 )
+				 updateModel.DateTime.Year > 2 )
 			{
 				var exifToolDatetimeString = updateModel.DateTime.ToString(
 					"yyyy:MM:dd HH:mm:ss",
 					CultureInfo.InvariantCulture);
 				command += $" -AllDates=\"{exifToolDatetimeString}\" \"-xmp:datecreated={exifToolDatetimeString}\"";
 			}
-		    
+
 			return command;
 		}
 
@@ -523,19 +523,19 @@ private static string UpdateApertureCommand(string command, List compare
 			// Warning: Sorry, Aperture is not writable => FNumber is writable
 			// XMP,http://ns.adobe.com/exif/1.0/,exif:FNumber,9/1
 			if ( !comparedNames.Contains(nameof(FileIndexItem.Aperture).ToLowerInvariant()) ) return command;
-		    
+
 			var aperture = updateModel.Aperture.ToString(CultureInfo.InvariantCulture);
 			command += $" -FNumber=\"{aperture}\" \"-xmp:FNumber={aperture}\" ";
 			return command;
 		}
-	    
+
 		private static string UpdateShutterSpeedCommand(string command, List comparedNames,
 			FileIndexItem updateModel)
 		{
 			// // -ExposureTime=1/31
 			// Warning: Sorry, ShutterSpeed is not writable => ExposureTime is writable
 			if ( !comparedNames.Contains(nameof(FileIndexItem.ShutterSpeed).ToLowerInvariant()) ) return command;
-		    
+
 			command += $" -ExposureTime=\"{updateModel.ShutterSpeed}\" \"-xmp:ExposureTime={updateModel.ShutterSpeed}\" ";
 
 			return command;
@@ -546,17 +546,17 @@ private static string UpdateMakeModelCommand(string command, List compar
 		{
 			// Make and Model are not writable so those never exist in this list
 			if ( !comparedNames.Contains(nameof(FileIndexItem.MakeModel).ToLowerInvariant()) ) return command;
-		    
+
 			var make = updateModel.Make;
 			var model = updateModel.Model;
 			command += " -make=\"" + make + "\"" + " -model=\"" + model + "\"";
-		    
+
 			if ( !string.IsNullOrWhiteSpace(updateModel.LensModel) )
 			{
 				// add space before
 				command += $" -lensmodel=\"{updateModel.LensModel}\" -xmp:lensmodel=\"{updateModel.LensModel}\"";
 			}
-		    
+
 			return command;
 		}
 
@@ -572,8 +572,8 @@ private static string UpdateFocalLengthCommand(string command, List comp
 
 		private static string UpdateImageStabilization(string command, List comparedNames, FileIndexItem updateModel)
 		{
-			if (comparedNames.Contains(nameof(FileIndexItem.ImageStabilisation).ToLowerInvariant()) && 
-			    updateModel.ImageStabilisation != ImageStabilisationType.Unknown)
+			if ( comparedNames.Contains(nameof(FileIndexItem.ImageStabilisation).ToLowerInvariant()) &&
+				updateModel.ImageStabilisation != ImageStabilisationType.Unknown )
 			{
 				// there is no XMP version of the name
 				command += " -ImageStabilization=\"" + updateModel.ImageStabilisation + "\"";
diff --git a/starsky/starsky.foundation.writemeta/Interfaces/IExifTool.cs b/starsky/starsky.foundation.writemeta/Interfaces/IExifTool.cs
index 041ecdaf0a..d095c4152c 100644
--- a/starsky/starsky.foundation.writemeta/Interfaces/IExifTool.cs
+++ b/starsky/starsky.foundation.writemeta/Interfaces/IExifTool.cs
@@ -1,19 +1,18 @@
-#nullable enable
 using System.Collections.Generic;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace starsky.foundation.writemeta.Interfaces
 {
-    public interface IExifTool
-    {
-	    Task WriteTagsAsync(string subPath, string command);
+	public interface IExifTool
+	{
+		Task WriteTagsAsync(string subPath, string command);
 
-	    Task> WriteTagsAndRenameThumbnailAsync(
-		    string subPath,
-		    string? beforeFileHash, string command,
-		    CancellationToken cancellationToken = default);
+		Task> WriteTagsAndRenameThumbnailAsync(
+			string subPath,
+			string? beforeFileHash, string command,
+			CancellationToken cancellationToken = default);
 
-	    Task WriteTagsThumbnailAsync(string fileHash, string command);
-    }
+		Task WriteTagsThumbnailAsync(string fileHash, string command);
+	}
 }
diff --git a/starsky/starsky.foundation.writemeta/Interfaces/IExifToolHostStorage.cs b/starsky/starsky.foundation.writemeta/Interfaces/IExifToolHostStorage.cs
index f6a9dba818..7252117e31 100644
--- a/starsky/starsky.foundation.writemeta/Interfaces/IExifToolHostStorage.cs
+++ b/starsky/starsky.foundation.writemeta/Interfaces/IExifToolHostStorage.cs
@@ -1,6 +1,6 @@
-namespace starsky.foundation.writemeta.Interfaces
+namespace starsky.foundation.writemeta.Interfaces
 {
-    public interface IExifToolHostStorage : IExifTool
-    {
-    }
+	public interface IExifToolHostStorage : IExifTool
+	{
+	}
 }
diff --git a/starsky/starsky.foundation.writemeta/JsonService/FileIndexItemJsonParser.cs b/starsky/starsky.foundation.writemeta/JsonService/FileIndexItemJsonParser.cs
index 13913f7b96..5282c38a85 100644
--- a/starsky/starsky.foundation.writemeta/JsonService/FileIndexItemJsonParser.cs
+++ b/starsky/starsky.foundation.writemeta/JsonService/FileIndexItemJsonParser.cs
@@ -3,7 +3,6 @@
 using starsky.foundation.database.Models;
 using starsky.foundation.platform.Helpers;
 using starsky.foundation.platform.JsonConverter;
-using starsky.foundation.platform.Models;
 using starsky.foundation.storage.Helpers;
 using starsky.foundation.storage.Interfaces;
 
@@ -17,7 +16,7 @@ public FileIndexItemJsonParser(IStorage storage)
 		{
 			_iStorage = storage;
 		}
-		
+
 		/// 
 		/// Write FileIndexItem to IStorage .meta.json file
 		/// 
@@ -25,15 +24,13 @@ public FileIndexItemJsonParser(IStorage storage)
 		/// Completed Task
 		public async Task WriteAsync(FileIndexItem fileIndexItem)
 		{
-			var jsonOutput = JsonSerializer.Serialize(new MetadataContainer
-			{
-				Item = fileIndexItem
-			}, DefaultJsonSerializer.CamelCase);
-			
+			var jsonOutput = JsonSerializer.Serialize(
+				new MetadataContainer { Item = fileIndexItem }, DefaultJsonSerializer.CamelCase);
+
 			var jsonSubPath = JsonSidecarLocation.JsonLocation(
-				fileIndexItem.ParentDirectory!, 
+				fileIndexItem.ParentDirectory!,
 				fileIndexItem.FileName!);
-			
+
 			await _iStorage.WriteStreamAsync(
 				StringToStreamHelper.StringToStream(jsonOutput), jsonSubPath);
 		}
@@ -45,18 +42,19 @@ await _iStorage.WriteStreamAsync(
 		/// data
 		public async Task ReadAsync(FileIndexItem fileIndexItem)
 		{
-			var jsonSubPath = JsonSidecarLocation.JsonLocation(fileIndexItem.ParentDirectory!, fileIndexItem.FileName!);
+			var jsonSubPath = JsonSidecarLocation.JsonLocation(fileIndexItem.ParentDirectory!,
+				fileIndexItem.FileName!);
 			// when sidecar file does not exist
 			if ( !_iStorage.ExistFile(jsonSubPath) ) return fileIndexItem;
-			
-			var returnContainer = await new DeserializeJson(_iStorage).ReadAsync(jsonSubPath);
-			
+
+			var returnContainer =
+				await new DeserializeJson(_iStorage).ReadAsync(jsonSubPath);
+
 			// in case of invalid json
 			returnContainer!.Item ??= fileIndexItem;
-			
+
 			returnContainer.Item.Status = FileIndexItem.ExifStatus.ExifWriteNotSupported;
 			return returnContainer.Item;
 		}
 	}
-
 }
diff --git a/starsky/starsky.foundation.writemeta/Services/ExifCopy.cs b/starsky/starsky.foundation.writemeta/Services/ExifCopy.cs
index 1636f01dc1..7fabd46a55 100644
--- a/starsky/starsky.foundation.writemeta/Services/ExifCopy.cs
+++ b/starsky/starsky.foundation.writemeta/Services/ExifCopy.cs
@@ -18,7 +18,7 @@ public sealed class ExifCopy
 		private readonly IReadMeta _readMeta;
 		private readonly ExifToolCmdHelper _exifToolCmdHelper;
 
-		public ExifCopy(IStorage iStorage, IStorage thumbnailStorage,  IExifTool exifTool, 
+		public ExifCopy(IStorage iStorage, IStorage thumbnailStorage, IExifTool exifTool,
 			IReadMeta readMeta, IThumbnailQuery thumbnailQuery)
 		{
 			_iStorage = iStorage;
@@ -39,11 +39,11 @@ public ExifCopy(IStorage iStorage, IStorage thumbnailStorage,  IExifTool exifToo
 		public void XmpCreate(string xmpPath)
 		{
 			if ( _iStorage.ExistFile(xmpPath) ) return;
-			
+
 			var plainTextStream = StringToStreamHelper.StringToStream(XmpStartContent);
 			_iStorage.WriteStream(plainTextStream, xmpPath);
 		}
-		
+
 		/// 
 		/// Add a .xmp sidecar file
 		/// 
@@ -57,17 +57,17 @@ public async Task XmpSync(string subPath)
 			var withXmp = ExtensionRolesHelper.ReplaceExtensionWithXmp(subPath);
 
 			// only for files that not exist yet
-			if ( _iStorage.IsFolderOrFile(withXmp) != 
-			     FolderOrFileModel.FolderOrFileTypeList.Deleted) return withXmp;
-			
+			if ( _iStorage.IsFolderOrFile(withXmp) !=
+				 FolderOrFileModel.FolderOrFileTypeList.Deleted ) return withXmp;
+
 			XmpCreate(withXmp);
-				
+
 			// Now copy content using exifTool
 			await CopyExifPublish(subPath, withXmp);
 
 			return withXmp;
 		}
-	    
+
 		/// 
 		/// Keep within the same storage provider
 		/// Used for example by Import
@@ -81,8 +81,8 @@ internal async Task CopyExifPublish(string fromSubPath, string toSubPath
 			var comparedNames = FileIndexCompareHelper.Compare(new FileIndexItem(), updateModel);
 			comparedNames.Add(nameof(FileIndexItem.Software));
 			updateModel!.SetFilePath(toSubPath);
-			return (await _exifToolCmdHelper.UpdateAsync(updateModel, 
-				comparedNames, true, false)).Item1;
+			return ( await _exifToolCmdHelper.UpdateAsync(updateModel,
+				comparedNames, true, false) ).Item1;
 		}
 
 
diff --git a/starsky/starsky.foundation.writemeta/Services/ExifToolDownload.cs b/starsky/starsky.foundation.writemeta/Services/ExifToolDownload.cs
index 4fa686564f..dc268ebbb3 100644
--- a/starsky/starsky.foundation.writemeta/Services/ExifToolDownload.cs
+++ b/starsky/starsky.foundation.writemeta/Services/ExifToolDownload.cs
@@ -44,7 +44,7 @@ public ExifToolDownload(IHttpClientHelper httpClientHelper, AppSettings appSetti
 			_hostFileSystemStorage = new StorageHostFullPathFilesystem(logger);
 			_logger = logger;
 		}
-		
+
 		internal ExifToolDownload(IHttpClientHelper httpClientHelper, AppSettings appSettings, IWebLogger logger, IStorage storage)
 		{
 			_httpClientHelper = httpClientHelper;
@@ -61,8 +61,8 @@ internal ExifToolDownload(IHttpClientHelper httpClientHelper, AppSettings appSet
 		/// 
 		public async Task DownloadExifTool(bool isWindows, int minimumSize = 30)
 		{
-			if (  _appSettings.ExiftoolSkipDownloadOnStartup == true || (
-				    _appSettings.AddSwaggerExport == true && _appSettings.AddSwaggerExportExitAfter == true) )
+			if ( _appSettings.ExiftoolSkipDownloadOnStartup == true || (
+					_appSettings.AddSwaggerExport == true && _appSettings.AddSwaggerExportExitAfter == true ) )
 			{
 				var name = _appSettings.ExiftoolSkipDownloadOnStartup == true
 					? "ExiftoolSkipDownloadOnStartup"
@@ -72,17 +72,17 @@ public async Task DownloadExifTool(bool isWindows, int minimumSize = 30)
 			}
 
 			CreateDirectoryDependenciesFolderIfNotExists();
-			
+
 			if ( isWindows &&
-			     (!_hostFileSystemStorage.ExistFile(ExeExifToolWindowsFullFilePath ()) ||
-			     _hostFileSystemStorage.Info(ExeExifToolWindowsFullFilePath()).Size <= minimumSize))
+				 ( !_hostFileSystemStorage.ExistFile(ExeExifToolWindowsFullFilePath()) ||
+				 _hostFileSystemStorage.Info(ExeExifToolWindowsFullFilePath()).Size <= minimumSize ) )
 			{
 				return await StartDownloadForWindows();
 			}
 
 			if ( !isWindows &&
-			     (!_hostFileSystemStorage.ExistFile(ExeExifToolUnixFullFilePath() ) ||
-			      _hostFileSystemStorage.Info(ExeExifToolUnixFullFilePath()).Size <= minimumSize))
+				 ( !_hostFileSystemStorage.ExistFile(ExeExifToolUnixFullFilePath()) ||
+				  _hostFileSystemStorage.Info(ExeExifToolUnixFullFilePath()).Size <= minimumSize ) )
 			{
 				return await StartDownloadForUnix();
 			}
@@ -93,9 +93,9 @@ public async Task DownloadExifTool(bool isWindows, int minimumSize = 30)
 					: ExeExifToolUnixFullFilePath();
 				_logger.LogInformation($"[DownloadExifTool] {debugPath}");
 			}
-			
+
 			// When running deploy scripts rights might reset (only for unix)
-			if ( isWindows) return true;
+			if ( isWindows ) return true;
 
 			return await RunChmodOnExifToolUnixExe();
 		}
@@ -103,7 +103,7 @@ public async Task DownloadExifTool(bool isWindows, int minimumSize = 30)
 		private void CreateDirectoryDependenciesFolderIfNotExists()
 		{
 			if ( _hostFileSystemStorage.ExistFolder(_appSettings
-				    .DependenciesFolder) ) return;
+					.DependenciesFolder) ) return;
 			_logger.LogInformation("[DownloadExifTool] Create Directory: " + _appSettings.DependenciesFolder);
 			_hostFileSystemStorage.CreateDirectory(_appSettings.DependenciesFolder);
 		}
@@ -116,12 +116,12 @@ private void CreateDirectoryDependenciesFolderIfNotExists()
 				return checksums;
 			}
 			_logger.LogError($"Checksum loading failed {CheckSumLocation}, next retry from mirror ~ error > " + checksums.Value);
-			
+
 			checksums = await _httpClientHelper.ReadString(CheckSumLocationMirror);
 			if ( checksums.Key ) return new KeyValuePair(false, checksums.Value);
-			
+
 			_logger.LogError($"Checksum loading failed {CheckSumLocationMirror}" +
-			                 $", next stop; please connect to internet and restart app ~ error > " + checksums.Value);
+							 $", next stop; please connect to internet and restart app ~ error > " + checksums.Value);
 			return null;
 		}
 
@@ -130,51 +130,51 @@ internal async Task StartDownloadForUnix()
 			var checksums = await DownloadCheckSums();
 			if ( checksums == null ) return false;
 			var matchExifToolForUnixName = GetUnixTarGzFromChecksum(checksums.Value.Value);
-			return await DownloadForUnix(matchExifToolForUnixName, 
+			return await DownloadForUnix(matchExifToolForUnixName,
 				GetChecksumsFromTextFile(checksums.Value.Value), !checksums.Value.Key);
 		}
-		
+
 		internal static string GetUnixTarGzFromChecksum(string checksumsValue)
 		{
 			// (?<=SHA1\()Image-ExifTool-[\d\.]+\.zip
-			var regexExifToolForWindowsName = new Regex(@"(?<=SHA1\()Image-ExifTool-[0-9\.]+\.tar.gz", 
+			var regexExifToolForWindowsName = new Regex(@"(?<=SHA1\()Image-ExifTool-[0-9\.]+\.tar.gz",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100));
 			return regexExifToolForWindowsName.Match(checksumsValue).Value;
 		}
 
 		private string ExeExifToolUnixFullFilePath()
 		{
-			var path = Path.Combine(_appSettings.DependenciesFolder, 
+			var path = Path.Combine(_appSettings.DependenciesFolder,
 					"exiftool-unix",
 					"exiftool");
 			return path;
 		}
-		
+
 		internal async Task DownloadForUnix(string matchExifToolForUnixName,
 			string[] getChecksumsFromTextFile, bool downloadFromMirror = false)
 		{
 
 			if ( _hostFileSystemStorage.ExistFile(
 				ExeExifToolUnixFullFilePath()) ) return true;
-			
+
 			var tarGzArchiveFullFilePath = Path.Combine(_appSettings.DependenciesFolder, "exiftool.tar.gz");
 
 			var url = $"{ExiftoolDownloadBasePath}{matchExifToolForUnixName}";
 			if ( downloadFromMirror ) url = $"{ExiftoolDownloadBasePathMirror}{matchExifToolForUnixName}";
-			
+
 			var unixDownloaded = await _httpClientHelper.Download(url, tarGzArchiveFullFilePath);
 			if ( !unixDownloaded )
 			{
 				throw new HttpRequestException($"file is not downloaded {matchExifToolForUnixName}");
 			}
-			if ( !CheckSha1(tarGzArchiveFullFilePath, getChecksumsFromTextFile) ) 
+			if ( !CheckSha1(tarGzArchiveFullFilePath, getChecksumsFromTextFile) )
 			{
 				throw new HttpRequestException($"checksum for {tarGzArchiveFullFilePath} is not valid");
 			}
-			
+
 			await new TarBal(_hostFileSystemStorage).ExtractTarGz(
 				_hostFileSystemStorage.ReadStream(tarGzArchiveFullFilePath), _appSettings.DependenciesFolder, CancellationToken.None);
-			
+
 			var imageExifToolVersionFolder = _hostFileSystemStorage.GetDirectories(_appSettings.DependenciesFolder)
 				.FirstOrDefault(p => p.StartsWith(Path.Combine(_appSettings.DependenciesFolder, "Image-ExifTool-")));
 			if ( imageExifToolVersionFolder != null )
@@ -185,7 +185,7 @@ internal async Task DownloadForUnix(string matchExifToolForUnixName,
 					_hostFileSystemStorage.FolderDelete(
 						exifToolUnixFolderFullFilePath);
 				}
-				_hostFileSystemStorage.FolderMove(imageExifToolVersionFolder,exifToolUnixFolderFullFilePath);
+				_hostFileSystemStorage.FolderMove(imageExifToolVersionFolder, exifToolUnixFolderFullFilePath);
 			}
 			else
 			{
@@ -195,8 +195,8 @@ internal async Task DownloadForUnix(string matchExifToolForUnixName,
 
 			// remove tar.gz file afterwards
 			_hostFileSystemStorage.FileDelete(tarGzArchiveFullFilePath);
-			
-			var exifToolExePath = Path.Combine(_appSettings.DependenciesFolder, "exiftool-unix","exiftool");
+
+			var exifToolExePath = Path.Combine(_appSettings.DependenciesFolder, "exiftool-unix", "exiftool");
 			_logger.LogInformation($"[DownloadForUnix] ExifTool is just downloaded: {exifToolExePath} for {_appSettings.ApplicationType}");
 			return await RunChmodOnExifToolUnixExe();
 		}
@@ -207,17 +207,17 @@ internal async Task RunChmodOnExifToolUnixExe()
 			// when not exist
 			if ( !_hostFileSystemStorage.ExistFile(ExeExifToolUnixFullFilePath()) ) return false;
 			if ( _appSettings.IsWindows ) return true;
-			
-			if (! _hostFileSystemStorage.ExistFile("/bin/chmod") )
+
+			if ( !_hostFileSystemStorage.ExistFile("/bin/chmod") )
 			{
 				_logger.LogError("[RunChmodOnExifToolUnixExe] WARNING: /bin/chmod does not exist");
 				return true;
 			}
-			
+
 			// command.run does not care about the $PATH
-			var result = await Command.Run("/bin/chmod","0755", ExeExifToolUnixFullFilePath()).Task; 
+			var result = await Command.Run("/bin/chmod", "0755", ExeExifToolUnixFullFilePath()).Task;
 			if ( result.Success ) return true;
-			
+
 			_logger.LogError($"command failed with exit code {result.ExitCode}: {result.StandardError}");
 			return false;
 		}
@@ -226,7 +226,7 @@ internal async Task StartDownloadForWindows()
 		{
 			var checksums = await DownloadCheckSums();
 			if ( checksums == null ) return false;
-			
+
 			var matchExifToolForWindowsName = GetWindowsZipFromChecksum(checksums.Value.Value);
 			return await DownloadForWindows(matchExifToolForWindowsName,
 				GetChecksumsFromTextFile(checksums.Value.Value), !checksums.Value.Key);
@@ -235,11 +235,11 @@ internal async Task StartDownloadForWindows()
 		internal static string GetWindowsZipFromChecksum(string checksumsValue)
 		{
 			// (?<=SHA1\()exiftool-[\d\.]+\.zip
-			var regexExifToolForWindowsName = new Regex(@"(?<=SHA1\()exiftool-[0-9\.]+\.zip", 
+			var regexExifToolForWindowsName = new Regex(@"(?<=SHA1\()exiftool-[0-9\.]+\.zip",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100));
 			return regexExifToolForWindowsName.Match(checksumsValue).Value;
 		}
-		
+
 		/// 
 		/// Parse the content of checksum file
 		/// 
@@ -248,13 +248,13 @@ internal static string GetWindowsZipFromChecksum(string checksumsValue)
 		/// 
 		internal string[] GetChecksumsFromTextFile(string checksumsValue, int max = 8)
 		{
-			var regexExifToolForWindowsName = new Regex("[a-z0-9]{40}", 
+			var regexExifToolForWindowsName = new Regex("[a-z0-9]{40}",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100));
 			var results = regexExifToolForWindowsName.Matches(checksumsValue).
 				Select(m => m.Value).
 				ToArray();
 			if ( results.Length < max ) return results;
-			
+
 			_logger.LogError($"More than {max} checksums found, this is not expected, code stops now");
 			return Array.Empty();
 		}
@@ -270,15 +270,15 @@ internal bool CheckSha1(string fullFilePath, IEnumerable checkSumOptions
 		{
 			using var buffer = _hostFileSystemStorage.ReadStream(fullFilePath);
 			using var hashAlgorithm = SHA1.Create();
-			
+
 			var byteHash = hashAlgorithm.ComputeHash(buffer);
-			var hash = BitConverter.ToString(byteHash).Replace("-",string.Empty).ToLowerInvariant();
+			var hash = BitConverter.ToString(byteHash).Replace("-", string.Empty).ToLowerInvariant();
 			return checkSumOptions.AsEnumerable().Any(p => p.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
 		}
 
 		private string ExeExifToolWindowsFullFilePath()
 		{
-			return Path.Combine(Path.Combine(_appSettings.DependenciesFolder,"exiftool-windows"), "exiftool.exe");
+			return Path.Combine(Path.Combine(_appSettings.DependenciesFolder, "exiftool-windows"), "exiftool.exe");
 		}
 
 		internal async Task DownloadForWindows(string matchExifToolForWindowsName,
@@ -289,21 +289,21 @@ internal async Task DownloadForWindows(string matchExifToolForWindowsName,
 
 			var zipArchiveFullFilePath = Path.Combine(_appSettings.DependenciesFolder, "exiftool.zip");
 			var windowsExifToolFolder = Path.Combine(_appSettings.DependenciesFolder, "exiftool-windows");
-			
+
 			var url = $"{ExiftoolDownloadBasePath}{matchExifToolForWindowsName}";
 			if ( downloadFromMirror ) url = $"{ExiftoolDownloadBasePathMirror}{matchExifToolForWindowsName}";
-			
+
 			var windowsDownloaded = await _httpClientHelper.Download(url, zipArchiveFullFilePath);
 			if ( !windowsDownloaded )
 			{
 				throw new HttpRequestException($"file is not downloaded {matchExifToolForWindowsName}");
 			}
-			
-			if ( !CheckSha1(zipArchiveFullFilePath, getChecksumsFromTextFile) ) 
+
+			if ( !CheckSha1(zipArchiveFullFilePath, getChecksumsFromTextFile) )
 			{
 				throw new HttpRequestException($"checksum for {zipArchiveFullFilePath} is not valid");
 			}
-			
+
 			_hostFileSystemStorage.CreateDirectory(windowsExifToolFolder);
 
 			new Zipper().ExtractZip(zipArchiveFullFilePath, windowsExifToolFolder);
diff --git a/starsky/starsky.foundation.writemeta/Services/ExifToolDownloadBackgroundService.cs b/starsky/starsky.foundation.writemeta/Services/ExifToolDownloadBackgroundService.cs
index 859bd59eca..a50862ed73 100644
--- a/starsky/starsky.foundation.writemeta/Services/ExifToolDownloadBackgroundService.cs
+++ b/starsky/starsky.foundation.writemeta/Services/ExifToolDownloadBackgroundService.cs
@@ -6,11 +6,9 @@
 using starsky.foundation.injection;
 using starsky.foundation.platform.Interfaces;
 using starsky.foundation.platform.Models;
-using starsky.foundation.writemeta.Helpers;
 
 namespace starsky.foundation.writemeta.Services
 {
-
 	[Service(typeof(IHostedService), InjectionLifetime = InjectionLifetime.Singleton)]
 	public sealed class ExifToolDownloadBackgroundService : BackgroundService
 	{
@@ -29,12 +27,14 @@ public ExifToolDownloadBackgroundService(IServiceScopeFactory serviceScopeFactor
 		/// CompletedTask
 		protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 		{
-			using (var scope = _serviceScopeFactory.CreateScope())
+			using ( var scope = _serviceScopeFactory.CreateScope() )
 			{
 				var appSettings = scope.ServiceProvider.GetRequiredService();
-				var httpClientHelper = scope.ServiceProvider.GetRequiredService();
+				var httpClientHelper =
+					scope.ServiceProvider.GetRequiredService();
 				var logger = scope.ServiceProvider.GetRequiredService();
-				await new ExifToolDownload(httpClientHelper, appSettings, logger).DownloadExifTool(appSettings.IsWindows);
+				await new ExifToolDownload(httpClientHelper, appSettings, logger).DownloadExifTool(
+					appSettings.IsWindows);
 			}
 		}
 	}
diff --git a/starsky/starsky.foundation.writemeta/Services/GeoLocationWrite.cs b/starsky/starsky.foundation.writemeta/Services/GeoLocationWrite.cs
index 77cd4b691e..ae86fa0643 100644
--- a/starsky/starsky.foundation.writemeta/Services/GeoLocationWrite.cs
+++ b/starsky/starsky.foundation.writemeta/Services/GeoLocationWrite.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 using starsky.foundation.database.Interfaces;
@@ -22,17 +22,17 @@ public sealed class GeoLocationWrite : IGeoLocationWrite
 		private readonly IConsole _console;
 		private readonly ExifToolCmdHelper _exifToolCmdHelper;
 
-		public GeoLocationWrite(AppSettings appSettings, IExifTool exifTool, 
+		public GeoLocationWrite(AppSettings appSettings, IExifTool exifTool,
 			ISelectorStorage selectorStorage, IConsole console, IWebLogger logger, IThumbnailQuery thumbnailQuery)
 		{
 			_appSettings = appSettings;
 			var thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail);
 			var iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
 			_console = console;
-			_exifToolCmdHelper = new ExifToolCmdHelper(exifTool, 
-				iStorage, 
-				thumbnailStorage, 
-				new ReadMeta(iStorage, _appSettings, null, logger),thumbnailQuery);
+			_exifToolCmdHelper = new ExifToolCmdHelper(exifTool,
+				iStorage,
+				thumbnailStorage,
+				new ReadMeta(iStorage, _appSettings, null, logger), thumbnailQuery);
 		}
 
 		/// 
@@ -43,8 +43,8 @@ public GeoLocationWrite(AppSettings appSettings, IExifTool exifTool,
 		public async Task LoopFolderAsync(List metaFilesInDirectory,
 			bool syncLocationNames)
 		{
-			foreach ( var metaFileItem in metaFilesInDirectory.Where(metaFileItem => 
-				         ExtensionRolesHelper.IsExtensionExifToolSupported(metaFileItem.FileName)) )
+			foreach ( var metaFileItem in metaFilesInDirectory.Where(metaFileItem =>
+						 ExtensionRolesHelper.IsExtensionExifToolSupported(metaFileItem.FileName)) )
 			{
 				if ( _appSettings.IsVerbose() ) _console.Write(" 👟 ");
 
@@ -54,15 +54,15 @@ public async Task LoopFolderAsync(List metaFilesInDirectory,
 					nameof(FileIndexItem.Longitude).ToLowerInvariant(),
 					nameof(FileIndexItem.LocationAltitude).ToLowerInvariant()
 				};
-                
-				if(syncLocationNames) comparedNamesList.AddRange( new List
+
+				if ( syncLocationNames ) comparedNamesList.AddRange(new List
 				{
 					nameof(FileIndexItem.LocationCity).ToLowerInvariant(),
 					nameof(FileIndexItem.LocationState).ToLowerInvariant(),
 					nameof(FileIndexItem.LocationCountry).ToLowerInvariant(),
 					nameof(FileIndexItem.LocationCountryCode).ToLowerInvariant()
 				});
-				
+
 				await _exifToolCmdHelper.UpdateAsync(metaFileItem, comparedNamesList);
 
 				// Rocket man!
diff --git a/starsky/starsky.sln b/starsky/starsky.sln
index 5fd79f26ca..5623f89bd0 100644
--- a/starsky/starsky.sln
+++ b/starsky/starsky.sln
@@ -111,10 +111,6 @@ EndProjectSection
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docker-compose", "docker-compose.dcproj", "{2F38C5AC-1FFA-4FC5-9BE3-B960124DCE5E}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.foundation.databasetelemetry", "starsky.foundation.databasetelemetry\starsky.foundation.databasetelemetry.csproj", "{6A237457-45EF-47F2-A225-6340AFF19D4F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.foundation.consoletelemetry", "starsky.foundation.consoletelemetry\starsky.foundation.consoletelemetry.csproj", "{48D826B3-2262-422A-941A-AF90E458F647}"
-EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{A3C71266-A3AD-4611-8990-846EC0DDA16E}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build-tools", "Build-tools", "{52237C88-FC6B-4DE3-BB27-8D2F9FF45F25}"
@@ -314,14 +310,6 @@ Global
 		{2F38C5AC-1FFA-4FC5-9BE3-B960124DCE5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2F38C5AC-1FFA-4FC5-9BE3-B960124DCE5E}.Release|Any CPU.ActiveCfg = Debug|Any CPU
 		{2F38C5AC-1FFA-4FC5-9BE3-B960124DCE5E}.Release|Any CPU.Build.0 = Debug|Any CPU
-		{6A237457-45EF-47F2-A225-6340AFF19D4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{6A237457-45EF-47F2-A225-6340AFF19D4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{6A237457-45EF-47F2-A225-6340AFF19D4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{6A237457-45EF-47F2-A225-6340AFF19D4F}.Release|Any CPU.Build.0 = Release|Any CPU
-		{48D826B3-2262-422A-941A-AF90E458F647}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{48D826B3-2262-422A-941A-AF90E458F647}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{48D826B3-2262-422A-941A-AF90E458F647}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{48D826B3-2262-422A-941A-AF90E458F647}.Release|Any CPU.Build.0 = Release|Any CPU
 		{7AB42607-88DF-427C-823B-033A224A73A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{7AB42607-88DF-427C-823B-033A224A73A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{7AB42607-88DF-427C-823B-033A224A73A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -416,8 +404,6 @@ Global
 		{EE4E6FCA-9E87-4E31-A137-8D1809FFED54} = {1C1EB4A5-08D0-4014-AE1F-962642A4E5D3}
 		{1FD59380-AC19-4AF1-A5A5-F295A2844B11} = {1C1EB4A5-08D0-4014-AE1F-962642A4E5D3}
 		{E0559015-77F2-4135-9AF5-41EBEA6C2C9C} = {B974FC20-C3EE-4EB0-AF25-F0D8DA2C28D7}
-		{6A237457-45EF-47F2-A225-6340AFF19D4F} = {1C1EB4A5-08D0-4014-AE1F-962642A4E5D3}
-		{48D826B3-2262-422A-941A-AF90E458F647} = {1C1EB4A5-08D0-4014-AE1F-962642A4E5D3}
 		{52237C88-FC6B-4DE3-BB27-8D2F9FF45F25} = {A3C71266-A3AD-4611-8990-846EC0DDA16E}
 		{7AB42607-88DF-427C-823B-033A224A73A6} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A}
 		{6630A2A7-FB58-40FA-B27B-013FDA1F74AA} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A}
diff --git a/starsky/starsky.sln.DotSettings b/starsky/starsky.sln.DotSettings
index 82d6a16c73..08448154ef 100644
--- a/starsky/starsky.sln.DotSettings
+++ b/starsky/starsky.sln.DotSettings
@@ -25,5 +25,7 @@
 	True
 	True
 	True
+	True
+	True
 	True
 	True
\ No newline at end of file
diff --git a/starsky/starsky/Attributes/DisableFormValueModelBindingAttribute.cs b/starsky/starsky/Attributes/DisableFormValueModelBindingAttribute.cs
index 0b568e6b1d..4aab36721c 100644
--- a/starsky/starsky/Attributes/DisableFormValueModelBindingAttribute.cs
+++ b/starsky/starsky/Attributes/DisableFormValueModelBindingAttribute.cs
@@ -1,22 +1,22 @@
-using System;
+using System;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.AspNetCore.Mvc.ModelBinding;
 
 namespace starsky.Attributes
 {
-    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
-    public sealed class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
-    {
-        public void OnResourceExecuting(ResourceExecutingContext context)
-        {
-            var factories = context.ValueProviderFactories;
-            factories.RemoveType();
-            factories.RemoveType();
-        }
+	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+	public sealed class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
+	{
+		public void OnResourceExecuting(ResourceExecutingContext context)
+		{
+			var factories = context.ValueProviderFactories;
+			factories.RemoveType();
+			factories.RemoveType();
+		}
 
-        public void OnResourceExecuted(ResourceExecutedContext context)
-        {
-            // Do nothing because this is used by .NET
-        }
-    }
+		public void OnResourceExecuted(ResourceExecutedContext context)
+		{
+			// Do nothing because this is used by .NET
+		}
+	}
 }
diff --git a/starsky/starsky/Attributes/PermissionAttribute.cs b/starsky/starsky/Attributes/PermissionAttribute.cs
index 2be7bd6d9a..476776e1e1 100644
--- a/starsky/starsky/Attributes/PermissionAttribute.cs
+++ b/starsky/starsky/Attributes/PermissionAttribute.cs
@@ -21,7 +21,7 @@ public void OnAuthorization(AuthorizationFilterContext context)
 		{
 			var user = context.HttpContext.User;
 
-			if (user.Identity?.IsAuthenticated == false)
+			if ( user.Identity?.IsAuthenticated == false )
 			{
 				context.Result = new UnauthorizedResult();
 			}
@@ -41,10 +41,10 @@ public void OnAuthorization(AuthorizationFilterContext context)
 
 			if ( collectedPermissions.Count == 0 )
 			{
-				context.Result =  new UnauthorizedResult();
+				context.Result = new UnauthorizedResult();
 				return;
 			}
-			
+
 			// add header for testing
 			context.HttpContext.Response.Headers.TryAdd("x-permission", "true");
 		}
diff --git a/starsky/starsky/Controllers/AccountController.cs b/starsky/starsky/Controllers/AccountController.cs
index 87f894add9..c5e26c60c2 100644
--- a/starsky/starsky/Controllers/AccountController.cs
+++ b/starsky/starsky/Controllers/AccountController.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
@@ -16,21 +16,21 @@
 
 namespace starsky.Controllers
 {
-    public sealed class AccountController : Controller
-    {
-        private readonly IUserManager _userManager;
-        private readonly AppSettings _appSettings;
-        private readonly IAntiforgery _antiForgery;
-        private readonly IStorage _storageHostFullPathFilesystem;
-
-        public AccountController(IUserManager userManager, AppSettings appSettings, IAntiforgery antiForgery, ISelectorStorage selectorStorage)
-        {
-            _userManager = userManager;
-            _appSettings = appSettings;
-            _antiForgery = antiForgery;
-            _storageHostFullPathFilesystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
-        }
-        
+	public sealed class AccountController : Controller
+	{
+		private readonly IUserManager _userManager;
+		private readonly AppSettings _appSettings;
+		private readonly IAntiforgery _antiForgery;
+		private readonly IStorage _storageHostFullPathFilesystem;
+
+		public AccountController(IUserManager userManager, AppSettings appSettings, IAntiforgery antiForgery, ISelectorStorage selectorStorage)
+		{
+			_userManager = userManager;
+			_appSettings = appSettings;
+			_antiForgery = antiForgery;
+			_storageHostFullPathFilesystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
+		}
+
 		/// 
 		/// Check the account status of the current logged in user
 		/// 
@@ -50,12 +50,12 @@ public AccountController(IUserManager userManager, AppSettings appSettings, IAnt
 		public async Task Status()
 		{
 			var userOverview = await _userManager.AllUsersAsync();
-			if ( !userOverview.IsSuccess)
+			if ( !userOverview.IsSuccess )
 			{
 				Response.StatusCode = 503;
 				return Json("Database error");
 			}
-			
+
 			if ( userOverview.Users.Count == 0 && _appSettings.NoAccountLocalhost != true )
 			{
 				Response.StatusCode = 406;
@@ -73,14 +73,14 @@ public async Task Status()
 			{
 				return Conflict("Current User does not exist in database");
 			}
-			
+
 			var model = new UserIdentifierStatusModel
 			{
 				Name = currentUser.Name,
 				Id = currentUser.Id,
 				Created = currentUser.Created,
 			};
-			
+
 			var credentials = _userManager.GetCredentialsByUserId(currentUser.Id);
 			if ( credentials == null )
 			{
@@ -88,13 +88,13 @@ public async Task Status()
 				model.CredentialTypeIds = null;
 				return Json(model);
 			}
-			
+
 			model.CredentialsIdentifiers?.Add(credentials.Identifier!);
 			model.CredentialTypeIds?.Add(credentials.CredentialTypeId);
 			return Json(model);
 		}
 
-		
+
 		/// 
 		/// Login form page (HTML)
 		/// 
@@ -115,220 +115,220 @@ public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null)
 			if ( !_storageHostFullPathFilesystem.ExistFile(clientApp) ) return Content("Please check if the client code exist");
 			return PhysicalFile(clientApp, "text/html");
 		}
-		
-        /// 
-        /// Login the current HttpContext in
-        /// 
-        /// Email, password and remember me bool
-        /// Login status
-        /// Successful login
-        /// Login failed
-        /// ValidateAntiForgeryToken error
-        /// Login failed due lock
-        /// Login failed due signIn errors
-        [HttpPost("/api/account/login")]
-        [ProducesResponseType(typeof(string),200)]
-        [ProducesResponseType(typeof(string),401)]
-        [ProducesResponseType(typeof(string),405)]
-#if ! DEBUG
+
+		/// 
+		/// Login the current HttpContext in
+		/// 
+		/// Email, password and remember me bool
+		/// Login status
+		/// Successful login
+		/// Login failed
+		/// ValidateAntiForgeryToken error
+		/// Login failed due lock
+		/// Login failed due signIn errors
+		[HttpPost("/api/account/login")]
+		[ProducesResponseType(typeof(string), 200)]
+		[ProducesResponseType(typeof(string), 401)]
+		[ProducesResponseType(typeof(string), 405)]
+#if !DEBUG
         [ValidateAntiForgeryToken]
 #endif
-        [Produces("application/json")]
-        [AllowAnonymous]
-        public async Task LoginPost(LoginViewModel model)
-        {
-            ValidateResult validateResult = await _userManager.ValidateAsync("Email", model.Email, model.Password);
-
-            if (!validateResult.Success)
-            {
-	            Response.StatusCode = 401;
-	            if ( validateResult.Error == ValidateResultError.Lockout )
-	            {
-		            Response.StatusCode = 423;
-	            }
-                return Json("Login failed");
-            } 
-            
-            await _userManager.SignIn(HttpContext, validateResult.User, model.RememberMe);
-            if ( User.Identity?.IsAuthenticated == true)
-            {
-	            return Json("Login Success");
-            }
-
-            Response.StatusCode = 500;
-            return Json("Login failed");
-        }
-
-        /// 
-        /// Logout the current HttpContext out
-        /// 
-        /// Login Status
-        /// Successful logout
-        [HttpPost("/api/account/logout")]
-        [ProducesResponseType(200)]
-        [AllowAnonymous]
-        public IActionResult LogoutJson()
-        {
-	        _userManager.SignOut(HttpContext);
-	        return Json("your logged out");
-        }
-        
-        /// 
-        /// Logout the current HttpContext and redirect to login 
-        /// 
-        /// insert url to redirect
-        /// redirect to return url
-        /// Redirect to login page
-        [HttpGet("/account/logout")]
-        [ProducesResponseType(200)]
-        [AllowAnonymous]
-        public IActionResult Logout(string? returnUrl = null)
-        {
-            _userManager.SignOut(HttpContext);
-            // fromLogout is used in middleware
-            return RedirectToAction(nameof(LoginGet), new {ReturnUrl = returnUrl, fromLogout = true});
-        }
-        
-        /// 
-        /// Update password for current user
-        /// 
-        /// Password, ChangedPassword and ChangedConfirmPassword
-        /// Change secret status
-        /// successful login
-        /// Model is not correct
-        ///  please login first or your current password is not correct
-        [HttpPost("/api/account/change-secret")]
-        [ProducesResponseType(typeof(string),200)]
-        [ProducesResponseType(typeof(string),400)]
-        [ProducesResponseType(typeof(string),401)]
-        [Produces("application/json")]
-        [Authorize]
-        public async Task ChangeSecret(ChangePasswordViewModel model)
-        {
-	        if ( User.Identity?.IsAuthenticated != true )
-	        {
-		        return Unauthorized("please login first");
-	        }
-
-	        if ( !ModelState.IsValid ||
-	             model.ChangedPassword != model.ChangedConfirmPassword )
-	        {
-		        return BadRequest("Model is not correct");
-	        }
-
-	        var currentUserId = _userManager.GetCurrentUser(HttpContext)?.Id;
-	        currentUserId ??= -1;
-	        var credential = _userManager.GetCredentialsByUserId((int) currentUserId);
-
-	        // Re-check password
-	        var validateResult = await _userManager.ValidateAsync(
-		        "Email", 
-		        credential?.Identifier, 
-		        model.Password);
-	        
-	        if ( !validateResult.Success )
-	        {
-		        return Unauthorized("Password is not correct");
-	        }
-
-	        var changeSecretResult =
-		        _userManager.ChangeSecret("Email", credential?.Identifier,
-			        model.ChangedPassword);
-
-	        return Json(changeSecretResult);
-        }
-
-        /// 
-        /// Create a new user (you need a AF-token first)
-        /// 
-        /// with the userdata
-        /// redirect or json
-        /// successful register
-        /// Wrong model or Wrong AntiForgeryToken
-        /// Account Register page is closed
-        /// AF token is missing
-        [HttpPost("/api/account/register")]
-        [ProducesResponseType(typeof(string),200)]
-        [ProducesResponseType(typeof(string),400)]
-        [ProducesResponseType(typeof(string),403)]
-        [ProducesResponseType(typeof(string),405)]
-        [Produces("application/json")]
-        [AllowAnonymous]
-        [ValidateAntiForgeryToken]
-        public async Task Register(RegisterViewModel model)
-        {
-	        if ( await IsAccountRegisterClosed(User.Identity?.IsAuthenticated == true) )
-	        {
-		        Response.StatusCode = 403;
-		        return Json("Account Register page is closed");
-	        }
-	        
-            if (ModelState.IsValid && model.ConfirmPassword == model.Password)
-            {
-                await _userManager.SignUpAsync(model.Name, "email", model.Email, model.Password );
-                return Json("Account Created");
-            }
-
-            // If we got this far, something failed, redisplay form
-            Response.StatusCode = 400;
-            return Json("Model is not correct");
-        }
-
-        /// 
-        /// True == not allowed
-        /// 
-        /// 
-        /// 
-        private async Task IsAccountRegisterClosed(bool userIdentityIsAuthenticated)
-        {
-	        if ( userIdentityIsAuthenticated ) return false;
-	        return _appSettings.IsAccountRegisterOpen != true && (await _userManager.AllUsersAsync()).Users.Count != 0;
-        }
-        
-        /// 
-        /// Is the register form open
-        /// 
-        /// 
-        /// Account Register page is open
-        /// open, but you are the first user
-        /// Account Register page is closed
-        [HttpGet("/api/account/register/status")]
-        [ProducesResponseType(typeof(string),200)]
-        [ProducesResponseType(typeof(string),403)]
-        [Produces("application/json")]
-        [AllowAnonymous]
-        public async Task RegisterStatus()
-        {
-	        if ( ( await _userManager.AllUsersAsync() ).Users.Count == 0 )
-	        {
-		        Response.StatusCode = 202;
-	        }
-
-	        if ( !await IsAccountRegisterClosed(
-		            User.Identity?.IsAuthenticated == true) )
-	        {
-		        return Json("RegisterStatus open");
-	        }
-	        
-	        Response.StatusCode = 403;
-	        return Json("Account Register page is closed");
-        }
-        
-        /// 
-        /// List of current permissions
-        /// 
-        /// list of current permissions
-        /// list of permissions
-        ///  please login first
-        [HttpGet("/api/account/permissions")]
-        [Authorize]
-        [ProducesResponseType(typeof(List),200)]
-        [ProducesResponseType(401)]
-        public IActionResult Permissions()
-        {
-	        var claims = User.Claims.Where(p=> p.Type == "Permission").Select( p=>  p.Value);
-	        return Json(claims);
-        }
-
-    }
+		[Produces("application/json")]
+		[AllowAnonymous]
+		public async Task LoginPost(LoginViewModel model)
+		{
+			ValidateResult validateResult = await _userManager.ValidateAsync("Email", model.Email, model.Password);
+
+			if ( !validateResult.Success )
+			{
+				Response.StatusCode = 401;
+				if ( validateResult.Error == ValidateResultError.Lockout )
+				{
+					Response.StatusCode = 423;
+				}
+				return Json("Login failed");
+			}
+
+			await _userManager.SignIn(HttpContext, validateResult.User, model.RememberMe);
+			if ( User.Identity?.IsAuthenticated == true )
+			{
+				return Json("Login Success");
+			}
+
+			Response.StatusCode = 500;
+			return Json("Login failed");
+		}
+
+		/// 
+		/// Logout the current HttpContext out
+		/// 
+		/// Login Status
+		/// Successful logout
+		[HttpPost("/api/account/logout")]
+		[ProducesResponseType(200)]
+		[AllowAnonymous]
+		public IActionResult LogoutJson()
+		{
+			_userManager.SignOut(HttpContext);
+			return Json("your logged out");
+		}
+
+		/// 
+		/// Logout the current HttpContext and redirect to login 
+		/// 
+		/// insert url to redirect
+		/// redirect to return url
+		/// Redirect to login page
+		[HttpGet("/account/logout")]
+		[ProducesResponseType(200)]
+		[AllowAnonymous]
+		public IActionResult Logout(string? returnUrl = null)
+		{
+			_userManager.SignOut(HttpContext);
+			// fromLogout is used in middleware
+			return RedirectToAction(nameof(LoginGet), new { ReturnUrl = returnUrl, fromLogout = true });
+		}
+
+		/// 
+		/// Update password for current user
+		/// 
+		/// Password, ChangedPassword and ChangedConfirmPassword
+		/// Change secret status
+		/// successful login
+		/// Model is not correct
+		///  please login first or your current password is not correct
+		[HttpPost("/api/account/change-secret")]
+		[ProducesResponseType(typeof(string), 200)]
+		[ProducesResponseType(typeof(string), 400)]
+		[ProducesResponseType(typeof(string), 401)]
+		[Produces("application/json")]
+		[Authorize]
+		public async Task ChangeSecret(ChangePasswordViewModel model)
+		{
+			if ( User.Identity?.IsAuthenticated != true )
+			{
+				return Unauthorized("please login first");
+			}
+
+			if ( !ModelState.IsValid ||
+				 model.ChangedPassword != model.ChangedConfirmPassword )
+			{
+				return BadRequest("Model is not correct");
+			}
+
+			var currentUserId = _userManager.GetCurrentUser(HttpContext)?.Id;
+			currentUserId ??= -1;
+			var credential = _userManager.GetCredentialsByUserId(( int )currentUserId);
+
+			// Re-check password
+			var validateResult = await _userManager.ValidateAsync(
+				"Email",
+				credential?.Identifier,
+				model.Password);
+
+			if ( !validateResult.Success )
+			{
+				return Unauthorized("Password is not correct");
+			}
+
+			var changeSecretResult =
+				_userManager.ChangeSecret("Email", credential?.Identifier,
+					model.ChangedPassword);
+
+			return Json(changeSecretResult);
+		}
+
+		/// 
+		/// Create a new user (you need a AF-token first)
+		/// 
+		/// with the userdata
+		/// redirect or json
+		/// successful register
+		/// Wrong model or Wrong AntiForgeryToken
+		/// Account Register page is closed
+		/// AF token is missing
+		[HttpPost("/api/account/register")]
+		[ProducesResponseType(typeof(string), 200)]
+		[ProducesResponseType(typeof(string), 400)]
+		[ProducesResponseType(typeof(string), 403)]
+		[ProducesResponseType(typeof(string), 405)]
+		[Produces("application/json")]
+		[AllowAnonymous]
+		[ValidateAntiForgeryToken]
+		public async Task Register(RegisterViewModel model)
+		{
+			if ( await IsAccountRegisterClosed(User.Identity?.IsAuthenticated == true) )
+			{
+				Response.StatusCode = 403;
+				return Json("Account Register page is closed");
+			}
+
+			if ( ModelState.IsValid && model.ConfirmPassword == model.Password )
+			{
+				await _userManager.SignUpAsync(model.Name, "email", model.Email, model.Password);
+				return Json("Account Created");
+			}
+
+			// If we got this far, something failed, redisplay form
+			Response.StatusCode = 400;
+			return Json("Model is not correct");
+		}
+
+		/// 
+		/// True == not allowed
+		/// 
+		/// 
+		/// 
+		private async Task IsAccountRegisterClosed(bool userIdentityIsAuthenticated)
+		{
+			if ( userIdentityIsAuthenticated ) return false;
+			return _appSettings.IsAccountRegisterOpen != true && ( await _userManager.AllUsersAsync() ).Users.Count != 0;
+		}
+
+		/// 
+		/// Is the register form open
+		/// 
+		/// 
+		/// Account Register page is open
+		/// open, but you are the first user
+		/// Account Register page is closed
+		[HttpGet("/api/account/register/status")]
+		[ProducesResponseType(typeof(string), 200)]
+		[ProducesResponseType(typeof(string), 403)]
+		[Produces("application/json")]
+		[AllowAnonymous]
+		public async Task RegisterStatus()
+		{
+			if ( ( await _userManager.AllUsersAsync() ).Users.Count == 0 )
+			{
+				Response.StatusCode = 202;
+			}
+
+			if ( !await IsAccountRegisterClosed(
+					User.Identity?.IsAuthenticated == true) )
+			{
+				return Json("RegisterStatus open");
+			}
+
+			Response.StatusCode = 403;
+			return Json("Account Register page is closed");
+		}
+
+		/// 
+		/// List of current permissions
+		/// 
+		/// list of current permissions
+		/// list of permissions
+		///  please login first
+		[HttpGet("/api/account/permissions")]
+		[Authorize]
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(401)]
+		public IActionResult Permissions()
+		{
+			var claims = User.Claims.Where(p => p.Type == "Permission").Select(p => p.Value);
+			return Json(claims);
+		}
+
+	}
 }
diff --git a/starsky/starsky/Controllers/AllowedTypesController.cs b/starsky/starsky/Controllers/AllowedTypesController.cs
index c8dcf1eb62..a695ef484a 100644
--- a/starsky/starsky/Controllers/AllowedTypesController.cs
+++ b/starsky/starsky/Controllers/AllowedTypesController.cs
@@ -10,7 +10,7 @@ namespace starsky.Controllers
 	[Authorize]
 	public sealed class AllowedTypesController : Controller
 	{
-		
+
 		/// 
 		/// A (string) list of allowed MIME-types ExtensionSyncSupportedList
 		/// 
@@ -18,15 +18,15 @@ public sealed class AllowedTypesController : Controller
 		/// list
 		/// please login first
 		[HttpGet("/api/allowed-types/mimetype/sync")]
-		[ProducesResponseType(typeof(HashSet),200)]
+		[ProducesResponseType(typeof(HashSet), 200)]
 		[Produces("application/json")]
 		public IActionResult AllowedTypesMimetypeSync()
 		{
 			var mimeTypes = ExtensionRolesHelper.ExtensionSyncSupportedList.Select(MimeHelper.GetMimeType).ToHashSet();
 			return Json(mimeTypes);
-		} 
-		
-		
+		}
+
+
 		/// 
 		/// A (string) list of allowed ExtensionThumbSupportedList MimeTypes
 		/// 
@@ -34,14 +34,14 @@ public IActionResult AllowedTypesMimetypeSync()
 		/// list
 		/// please login first
 		[HttpGet("/api/allowed-types/mimetype/thumb")]
-		[ProducesResponseType(typeof(HashSet),200)]
+		[ProducesResponseType(typeof(HashSet), 200)]
 		[Produces("application/json")]
 		public IActionResult AllowedTypesMimetypeSyncThumb()
 		{
 			var mimeTypes = ExtensionRolesHelper.ExtensionThumbSupportedList.Select(MimeHelper.GetMimeType).ToHashSet();
 			return Json(mimeTypes);
-		} 
-		
+		}
+
 		/// 
 		/// Check if IsExtensionThumbnailSupported
 		/// 
@@ -51,14 +51,14 @@ public IActionResult AllowedTypesMimetypeSyncThumb()
 		/// the extenstion from the filename is not supported to generate thumbnails
 		/// please login first
 		[HttpGet("/api/allowed-types/thumb")]
-		[ProducesResponseType(typeof(bool),200)]
-		[ProducesResponseType(typeof(bool),415)]
+		[ProducesResponseType(typeof(bool), 200)]
+		[ProducesResponseType(typeof(bool), 415)]
 		[Produces("application/json")]
 		public IActionResult AllowedTypesThumb(string f)
 		{
 			var result = ExtensionRolesHelper.IsExtensionThumbnailSupported(f);
 			if ( !result ) Response.StatusCode = 415;
 			return Json(result);
-		} 
+		}
 	}
 }
diff --git a/starsky/starsky/Controllers/AppSettingsController.cs b/starsky/starsky/Controllers/AppSettingsController.cs
index 1e7ada77c2..783f8c4b03 100644
--- a/starsky/starsky/Controllers/AppSettingsController.cs
+++ b/starsky/starsky/Controllers/AppSettingsController.cs
@@ -1,19 +1,12 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using starsky.Attributes;
 using starsky.feature.settings.Interfaces;
 using starsky.foundation.accountmanagement.Services;
-using starsky.foundation.platform.Helpers;
-using starsky.foundation.platform.JsonConverter;
 using starsky.foundation.platform.Models;
-using starsky.foundation.storage.Helpers;
-using starsky.foundation.storage.Interfaces;
-using starsky.foundation.storage.Storage;
 
 namespace starsky.Controllers
 {
@@ -23,12 +16,13 @@ public sealed class AppSettingsController : Controller
 		private readonly AppSettings _appSettings;
 		private readonly IUpdateAppSettingsByPath _updateAppSettingsByPath;
 
-		public AppSettingsController(AppSettings appSettings, IUpdateAppSettingsByPath updateAppSettingsByPath)
+		public AppSettingsController(AppSettings appSettings,
+			IUpdateAppSettingsByPath updateAppSettingsByPath)
 		{
 			_appSettings = appSettings;
 			_updateAppSettingsByPath = updateAppSettingsByPath;
 		}
-		
+
 		/// 
 		/// Show the runtime settings (dont allow AllowAnonymous)
 		/// 
@@ -37,19 +31,21 @@ public AppSettingsController(AppSettings appSettings, IUpdateAppSettingsByPath u
 		[HttpHead("/api/env")]
 		[HttpGet("/api/env")]
 		[Produces("application/json")]
-		[ProducesResponseType(typeof(AppSettings),200)]
-		[ProducesResponseType(typeof(AppSettings),401)]
-		[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse",Justification = "Request in tests")]
+		[ProducesResponseType(typeof(AppSettings), 200)]
+		[ProducesResponseType(typeof(AppSettings), 401)]
+		[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse",
+			Justification = "Request in tests")]
 		public IActionResult Env()
 		{
 			var appSettings = _appSettings.CloneToDisplay();
-			if ( Request != null && Request.Headers.Any(p => p.Key == "x-force-html") )
+			if ( Request != null! && Request.Headers.Any(p => p.Key == "x-force-html") )
 			{
 				Response.Headers.ContentType = "text/html; charset=utf-8";
 			}
+
 			return Json(appSettings);
 		}
-		
+
 		/// 
 		/// Show the runtime settings (dont allow AllowAnonymous)
 		/// 
@@ -57,19 +53,20 @@ public IActionResult Env()
 		/// returns the runtime settings of Starsky
 		[HttpPost("/api/env")]
 		[Produces("application/json")]
-		[ProducesResponseType(typeof(AppSettings),200)]
-		[ProducesResponseType(typeof(AppSettings),401)]
+		[ProducesResponseType(typeof(AppSettings), 200)]
+		[ProducesResponseType(typeof(AppSettings), 401)]
 		[Permission(UserManager.AppPermissions.AppSettingsWrite)]
-		public async Task UpdateAppSettings(AppSettingsTransferObject appSettingTransferObject  )
+		public async Task UpdateAppSettings(
+			AppSettingsTransferObject appSettingTransferObject)
 		{
 			var result = await _updateAppSettingsByPath.UpdateAppSettingsAsync(
 				appSettingTransferObject);
-			
+
 			if ( !result.IsError )
 			{
 				return Env();
 			}
-			
+
 			Response.StatusCode = result.StatusCode;
 			return Content(result.Message);
 		}
diff --git a/starsky/starsky/Controllers/AppSettingsFeaturesController.cs b/starsky/starsky/Controllers/AppSettingsFeaturesController.cs
index 5c9a76bd63..fd7d9dfb06 100644
--- a/starsky/starsky/Controllers/AppSettingsFeaturesController.cs
+++ b/starsky/starsky/Controllers/AppSettingsFeaturesController.cs
@@ -16,7 +16,7 @@ public AppSettingsFeaturesController(IMoveToTrashService moveToTrashService, App
 		_moveToTrashService = moveToTrashService;
 		_appSettings = appSettings;
 	}
-	
+
 	/// 
 	/// Show features that used in the frontend app / menu
 	/// 
@@ -38,7 +38,7 @@ public IActionResult FeaturesView()
 			SystemTrashEnabled = _moveToTrashService.IsEnabled(),
 			UseLocalDesktopUi = _appSettings.UseLocalDesktopUi == true
 		};
-		
+
 		return Json(shortAppSettings);
 	}
 }
diff --git a/starsky/starsky/Controllers/CacheIndexController.cs b/starsky/starsky/Controllers/CacheIndexController.cs
index 6f9b8ce760..65c0d1d79a 100644
--- a/starsky/starsky/Controllers/CacheIndexController.cs
+++ b/starsky/starsky/Controllers/CacheIndexController.cs
@@ -6,78 +6,78 @@
 
 namespace starsky.Controllers
 {
-    [Authorize]
-    public sealed class CacheIndexController : Controller
-    {
-        private readonly IQuery _query;
-        private readonly AppSettings _appSettings;
+	[Authorize]
+	public sealed class CacheIndexController : Controller
+	{
+		private readonly IQuery _query;
+		private readonly AppSettings _appSettings;
 
-	    public CacheIndexController(
-            IQuery query, AppSettings appSettings)
-        {
-            _appSettings = appSettings;
-            _query = query;
-        }
-	    
-	    /// 
-	    /// Get Database Cache (only the cache)
-	    /// 
-	    /// subPath (only direct so no dot;comma list)
-	    /// redirect or if json enabled a status
-	    /// when json"
-	    /// "cache disabled in config"
-	    /// ignored, please check if the 'f' path exist or use a folder string to clear the cache
-	    /// User unauthorized
-	    [HttpGet("/api/cache/list")]
-	    public IActionResult ListCache(string f = "/")
-	    {
-		    //For folder paths only
-		    if (_appSettings.AddMemoryCache == false)
-		    {
-			    Response.StatusCode = 412;
-			    return Json("cache disabled in config");
-		    }
+		public CacheIndexController(
+			IQuery query, AppSettings appSettings)
+		{
+			_appSettings = appSettings;
+			_query = query;
+		}
 
-		    var (success, singleItem) = _query.CacheGetParentFolder(f);
-		    if ( !success || singleItem == null )
-			    return BadRequest(
-				    "ignored, please check if the 'f' path exist or use a folder string to get the cache");
-            
-		    return Json(singleItem);
-	    }
+		/// 
+		/// Get Database Cache (only the cache)
+		/// 
+		/// subPath (only direct so no dot;comma list)
+		/// redirect or if json enabled a status
+		/// when json"
+		/// "cache disabled in config"
+		/// ignored, please check if the 'f' path exist or use a folder string to clear the cache
+		/// User unauthorized
+		[HttpGet("/api/cache/list")]
+		public IActionResult ListCache(string f = "/")
+		{
+			//For folder paths only
+			if ( _appSettings.AddMemoryCache == false )
+			{
+				Response.StatusCode = 412;
+				return Json("cache disabled in config");
+			}
+
+			var (success, singleItem) = _query.CacheGetParentFolder(f);
+			if ( !success || singleItem == null )
+				return BadRequest(
+					"ignored, please check if the 'f' path exist or use a folder string to get the cache");
 
-	    /// 
-        /// Delete Database Cache (only the cache)
-        /// 
-        /// subPath (only direct so no dot;comma list)
-        /// redirect or if json enabled a status
-        /// when json is true, "cache successful cleared"
-        /// "cache disabled in config"
-        /// ignored, please check if the 'f' path exist or use a folder string to clear the cache
-        /// redirect back to the url
-        /// User unauthorized
-        [HttpGet("/api/remove-cache")]
-        [HttpPost("/api/remove-cache")]
-        [ProducesResponseType(200)] // "cache successful cleared"
-        [ProducesResponseType(412)] // "cache disabled in config"
-        [ProducesResponseType(400)] // "ignored, please check if the 'f' path exist or use a folder string to clear the cache"
-        [ProducesResponseType(302)] // redirect back to the url
-        public IActionResult RemoveCache(string f = "/")
-        {
-            //For folder paths only
-            if (_appSettings.AddMemoryCache == false)
-            {
+			return Json(singleItem);
+		}
+
+		/// 
+		/// Delete Database Cache (only the cache)
+		/// 
+		/// subPath (only direct so no dot;comma list)
+		/// redirect or if json enabled a status
+		/// when json is true, "cache successful cleared"
+		/// "cache disabled in config"
+		/// ignored, please check if the 'f' path exist or use a folder string to clear the cache
+		/// redirect back to the url
+		/// User unauthorized
+		[HttpGet("/api/remove-cache")]
+		[HttpPost("/api/remove-cache")]
+		[ProducesResponseType(200)] // "cache successful cleared"
+		[ProducesResponseType(412)] // "cache disabled in config"
+		[ProducesResponseType(400)] // "ignored, please check if the 'f' path exist or use a folder string to clear the cache"
+		[ProducesResponseType(302)] // redirect back to the url
+		public IActionResult RemoveCache(string f = "/")
+		{
+			//For folder paths only
+			if ( _appSettings.AddMemoryCache == false )
+			{
 				Response.StatusCode = 412;
 				return Json("cache disabled in config");
-            }
+			}
+
+			var singleItem = _query.SingleItem(f);
+			if ( singleItem == null || !singleItem.IsDirectory )
+				return BadRequest(
+					"ignored, please check if the 'f' path exist or use a folder string to clear the cache");
 
-            var singleItem = _query.SingleItem(f);
-            if ( singleItem == null || !singleItem.IsDirectory )
-	            return BadRequest(
-		            "ignored, please check if the 'f' path exist or use a folder string to clear the cache");
-            
-            return Json(_query.RemoveCacheParentItem(f) ? "cache successful cleared" : "cache did not exist");
-        }
+			return Json(_query.RemoveCacheParentItem(f) ? "cache successful cleared" : "cache did not exist");
+		}
 
-    }
+	}
 }
diff --git a/starsky/starsky/Controllers/DeleteController.cs b/starsky/starsky/Controllers/DeleteController.cs
index 91a1df3aa2..9257b03242 100644
--- a/starsky/starsky/Controllers/DeleteController.cs
+++ b/starsky/starsky/Controllers/DeleteController.cs
@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using System.Linq;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
@@ -17,33 +16,33 @@ public DeleteController(IDeleteItem deleteItem)
 		{
 			_deleteItem = deleteItem;
 		}
-		
+
 		/// 
-        /// Remove files from the disk, but the file must contain the !delete! (TrashKeyword.TrashKeywordString) tag
-        /// 
-        /// subPaths, separated by dot comma
-        /// true is to update files with the same name before the extenstion
-        /// list of deleted files
-        /// file is gone
-        /// item not found on disk or !delete! (TrashKeyword.TrashKeywordString) tag is missing
-        /// User unauthorized
-        [HttpDelete("/api/delete")]
-        [ProducesResponseType(typeof(List),200)]
-        [ProducesResponseType(typeof(List),404)]
-        [Produces("application/json")]
-        public async Task Delete(string f, bool collections = false)
+		/// Remove files from the disk, but the file must contain the !delete! (TrashKeyword.TrashKeywordString) tag
+		/// 
+		/// subPaths, separated by dot comma
+		/// true is to update files with the same name before the extenstion
+		/// list of deleted files
+		/// file is gone
+		/// item not found on disk or !delete! (TrashKeyword.TrashKeywordString) tag is missing
+		/// User unauthorized
+		[HttpDelete("/api/delete")]
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(typeof(List), 404)]
+		[Produces("application/json")]
+		public async Task Delete(string f, bool collections = false)
 		{
 			var fileIndexResultsList = await _deleteItem.DeleteAsync(f, collections);
-            // When all items are not found
-	        // ok = file is deleted
-	        if ( fileIndexResultsList.TrueForAll(p =>
-		            p.Status != FileIndexItem.ExifStatus.Ok) )
-	        {
-		        return NotFound(fileIndexResultsList);
-	        }
-                
-     
-            return Json(fileIndexResultsList);
-        }
+			// When all items are not found
+			// ok = file is deleted
+			if ( fileIndexResultsList.TrueForAll(p =>
+				    p.Status != FileIndexItem.ExifStatus.Ok) )
+			{
+				return NotFound(fileIndexResultsList);
+			}
+
+
+			return Json(fileIndexResultsList);
+		}
 	}
 }
diff --git a/starsky/starsky/Controllers/DiskController.cs b/starsky/starsky/Controllers/DiskController.cs
index 12c918faef..855b1b3fa0 100644
--- a/starsky/starsky/Controllers/DiskController.cs
+++ b/starsky/starsky/Controllers/DiskController.cs
@@ -25,7 +25,7 @@ public sealed class DiskController : Controller
 		private readonly IWebSocketConnectionsService _connectionsService;
 		private readonly INotificationQuery _notificationQuery;
 
-		public DiskController(IQuery query, ISelectorStorage selectorStorage, 
+		public DiskController(IQuery query, ISelectorStorage selectorStorage,
 			IWebSocketConnectionsService connectionsService, INotificationQuery notificationQuery)
 		{
 			_query = query;
@@ -44,11 +44,11 @@ public DiskController(IQuery query, ISelectorStorage selectorStorage,
 		/// missing path
 		/// User unauthorized
 		[HttpPost("/api/disk/mkdir")]
-		[ProducesResponseType(typeof(List),200)]
-		[ProducesResponseType(typeof(List),409)]
-		[ProducesResponseType(typeof(List),400)]
-		[ProducesResponseType(typeof(string),401)]
-		[Produces("application/json")]	    
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(typeof(List), 409)]
+		[ProducesResponseType(typeof(List), 400)]
+		[ProducesResponseType(typeof(string), 401)]
+		[Produces("application/json")]
 		public async Task Mkdir(string f)
 		{
 			var inputFilePaths = PathHelper.SplitInputFilePaths(f).ToList();
@@ -65,10 +65,10 @@ public async Task Mkdir(string f)
 
 				var toAddStatus = new SyncViewModel
 				{
-					FilePath = subPath, 
+					FilePath = subPath,
 					Status = FileIndexItem.ExifStatus.Ok
 				};
-			        
+
 				if ( _iStorage.ExistFolder(subPath) )
 				{
 					toAddStatus.Status = FileIndexItem.ExifStatus.OperationNotSupported;
@@ -80,17 +80,17 @@ await _query.AddItemAsync(new FileIndexItem(subPath)
 				{
 					IsDirectory = true
 				});
-			        
+
 				// add to fs
 				_iStorage.CreateDirectory(subPath);
-		        
+
 				syncResultsList.Add(toAddStatus);
 			}
-	        
+
 			// When all items are not found
-			if (syncResultsList.TrueForAll(p => p.Status != FileIndexItem.ExifStatus.Ok))
+			if ( syncResultsList.TrueForAll(p => p.Status != FileIndexItem.ExifStatus.Ok) )
 				Response.StatusCode = 409; // A conflict, Directory already exist
-	        
+
 			await SyncMessageToSocket(syncResultsList, ApiNotificationType.Mkdir);
 
 			return Json(syncResultsList);
@@ -106,7 +106,8 @@ private async Task SyncMessageToSocket(IEnumerable syncResultsLis
 		{
 			var list = syncResultsList.Select(t => new FileIndexItem(t.FilePath)
 			{
-				Status = t.Status, IsDirectory = true
+				Status = t.Status,
+				IsDirectory = true
 			}).ToList();
 
 			var webSocketResponse = new ApiNotificationResponseModel<
@@ -127,30 +128,30 @@ private async Task SyncMessageToSocket(IEnumerable syncResultsLis
 		/// the item including the updated content
 		/// item not found in the database or on disk
 		/// User unauthorized
-		[ProducesResponseType(typeof(List),200)]
-		[ProducesResponseType(typeof(List),404)]
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(typeof(List), 404)]
 		[HttpPost("/api/disk/rename")]
-		[Produces("application/json")]	    
+		[Produces("application/json")]
 		public async Task Rename(string f, string to, bool collections = true, bool currentStatus = true)
 		{
 			if ( string.IsNullOrEmpty(f) )
 			{
 				return BadRequest("No input files");
 			}
-			
+
 			var rename = await new RenameService(_query, _iStorage).Rename(f, to, collections);
-		    
+
 			// When all items are not found
-			if (rename.TrueForAll(p => p.Status != FileIndexItem.ExifStatus.Ok))
+			if ( rename.TrueForAll(p => p.Status != FileIndexItem.ExifStatus.Ok) )
 				return NotFound(rename);
-		    
+
 			var webSocketResponse =
-				new ApiNotificationResponseModel>(rename,ApiNotificationType.Rename);
+				new ApiNotificationResponseModel>(rename, ApiNotificationType.Rename);
 
 			await _notificationQuery.AddNotification(webSocketResponse);
 			await _connectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None);
 
-			return Json(currentStatus ? rename.Where(p => p.Status 
+			return Json(currentStatus ? rename.Where(p => p.Status
 				!= FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList() : rename);
 		}
 
diff --git a/starsky/starsky/Controllers/DownloadPhotoController.cs b/starsky/starsky/Controllers/DownloadPhotoController.cs
index 427449b2c9..ee409f1f02 100644
--- a/starsky/starsky/Controllers/DownloadPhotoController.cs
+++ b/starsky/starsky/Controllers/DownloadPhotoController.cs
@@ -23,7 +23,8 @@ public sealed class DownloadPhotoController : Controller
 		private readonly IWebLogger _logger;
 		private readonly IThumbnailService _thumbnailService;
 
-		public DownloadPhotoController(IQuery query, ISelectorStorage selectorStorage, IWebLogger logger, IThumbnailService thumbnailService)
+		public DownloadPhotoController(IQuery query, ISelectorStorage selectorStorage,
+			IWebLogger logger, IThumbnailService thumbnailService)
 		{
 			_query = query;
 			_iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
@@ -45,90 +46,94 @@ public DownloadPhotoController(IQuery query, ISelectorStorage selectorStorage, I
 		[ProducesResponseType(404)] // not found
 		public IActionResult DownloadSidecar(string f)
 		{
-			if ( !ExtensionRolesHelper.IsExtensionSidecar(f))
+			if ( !ExtensionRolesHelper.IsExtensionSidecar(f) )
 			{
 				return NotFound("FileName is not a sidecar");
 			}
-			
-			if (!_iStorage.ExistFile(f))
-				return NotFound($"source image missing {f}" );
+
+			if ( !_iStorage.ExistFile(f) )
+				return NotFound($"source image missing {f}");
 
 			var fs = _iStorage.ReadStream(f);
 			return File(fs, MimeHelper.GetMimeTypeByFileName(f));
 		}
 
 		/// 
-        /// Select manually the original or thumbnail
-        /// 
-        /// string, 'sub path' to find the file
-        /// true = 1000px thumb (if supported)
-        /// true = send client headers to cache
-        /// FileStream with image
-        /// returns content of the file
-        /// source image missing
-        /// "Thumbnail generation failed"
-        /// User unauthorized
-        [HttpGet("/api/download-photo")]
-        [ProducesResponseType(200)] // file
-        [ProducesResponseType(404)] // not found
-        [ProducesResponseType(500)] // "Thumbnail generation failed"
-        public async Task DownloadPhoto(string f, bool isThumbnail = true, bool cache = true)
-        {
-            // f = subpath/filepath
-            if (f.Contains("?isthumbnail")) {
-	            return NotFound("please use &isthumbnail = instead of ?isthumbnail= ");
-            }
+		/// Select manually the original or thumbnail
+		/// 
+		/// string, 'sub path' to find the file
+		/// true = 1000px thumb (if supported)
+		/// true = send client headers to cache
+		/// FileStream with image
+		/// returns content of the file
+		/// source image missing
+		/// "Thumbnail generation failed"
+		/// User unauthorized
+		[HttpGet("/api/download-photo")]
+		[ProducesResponseType(200)] // file
+		[ProducesResponseType(404)] // not found
+		[ProducesResponseType(500)] // "Thumbnail generation failed"
+		public async Task DownloadPhoto(string f, bool isThumbnail = true,
+			bool cache = true)
+		{
+			// f = subpath/filepath
+			if ( f.Contains("?isthumbnail") )
+			{
+				return NotFound("please use &isthumbnail = instead of ?isthumbnail= ");
+			}
 
-            var fileIndexItem = await _query.GetObjectByFilePathAsync(f);
-            if ( fileIndexItem == null)
-            {
-	            return NotFound("not in index " + f);
-            }
+			var fileIndexItem = await _query.GetObjectByFilePathAsync(f);
+			if ( fileIndexItem == null )
+			{
+				return NotFound("not in index " + f);
+			}
 
-            if (!_iStorage.ExistFile(fileIndexItem.FilePath!))
-                return NotFound($"source image missing {fileIndexItem.FilePath}" );
+			if ( !_iStorage.ExistFile(fileIndexItem.FilePath!) )
+				return NotFound($"source image missing {fileIndexItem.FilePath}");
 
-            // Return full image
-            if (!isThumbnail)
-            {
-	            if ( cache ) CacheControlOverwrite.SetExpiresResponseHeaders(Request);
-	            var fileStream = _iStorage.ReadStream(fileIndexItem.FilePath!);
-	            // Return the right mime type (enableRangeProcessing = needed for safari and mp4)
-	            return File(fileStream, MimeHelper.GetMimeTypeByFileName(fileIndexItem.FilePath),true);
-            }
+			// Return full image
+			if ( !isThumbnail )
+			{
+				if ( cache ) CacheControlOverwrite.SetExpiresResponseHeaders(Request);
+				var fileStream = _iStorage.ReadStream(fileIndexItem.FilePath!);
+				// Return the right mime type (enableRangeProcessing = needed for safari and mp4)
+				return File(fileStream, MimeHelper.GetMimeTypeByFileName(fileIndexItem.FilePath!),
+					true);
+			}
 
-            if (!_thumbnailStorage.ExistFolder("/"))
-            {
-	            return NotFound("ThumbnailTempFolder not found");
-            }
-            
-            var data = new ThumbnailSizesExistStatusModel{ 
-	            Small = _thumbnailStorage.ExistFile(
-		            ThumbnailNameHelper.Combine(fileIndexItem.FileHash!,ThumbnailSize.Small)),
-	            Large = _thumbnailStorage.ExistFile(
-		            ThumbnailNameHelper.Combine(fileIndexItem.FileHash!,ThumbnailSize.Large)),
-	            ExtraLarge = _thumbnailStorage.ExistFile(
-		            ThumbnailNameHelper.Combine(fileIndexItem.FileHash!,ThumbnailSize.ExtraLarge))
-            };
+			if ( !_thumbnailStorage.ExistFolder("/") )
+			{
+				return NotFound("ThumbnailTempFolder not found");
+			}
 
-            if (!data.Small || !data.Large || !data.ExtraLarge)
-            {
-	            _logger.LogDebug("Thumbnail generation started");
-                await _thumbnailService.CreateThumbAsync(fileIndexItem.FilePath!, 
-	                fileIndexItem.FileHash!);
-                
-                if ( !_thumbnailStorage.ExistFile(
-	                ThumbnailNameHelper.Combine(fileIndexItem.FileHash!,
-		                ThumbnailSize.Large)) )
-                {
-	                Response.StatusCode = 500;
-	                return Json("Thumbnail generation failed");
-                }
-            }
+			var data = new ThumbnailSizesExistStatusModel
+			{
+				Small = _thumbnailStorage.ExistFile(
+					ThumbnailNameHelper.Combine(fileIndexItem.FileHash!, ThumbnailSize.Small)),
+				Large = _thumbnailStorage.ExistFile(
+					ThumbnailNameHelper.Combine(fileIndexItem.FileHash!, ThumbnailSize.Large)),
+				ExtraLarge = _thumbnailStorage.ExistFile(
+					ThumbnailNameHelper.Combine(fileIndexItem.FileHash!, ThumbnailSize.ExtraLarge))
+			};
 
-            var thumbnailFs = _thumbnailStorage.ReadStream(
-	            ThumbnailNameHelper.Combine(fileIndexItem.FileHash!,ThumbnailSize.Large));
-            return File(thumbnailFs, "image/jpeg");
-        }
+			if ( !data.Small || !data.Large || !data.ExtraLarge )
+			{
+				_logger.LogDebug("Thumbnail generation started");
+				await _thumbnailService.CreateThumbAsync(fileIndexItem.FilePath!,
+					fileIndexItem.FileHash!);
+
+				if ( !_thumbnailStorage.ExistFile(
+					    ThumbnailNameHelper.Combine(fileIndexItem.FileHash!,
+						    ThumbnailSize.Large)) )
+				{
+					Response.StatusCode = 500;
+					return Json("Thumbnail generation failed");
+				}
+			}
+
+			var thumbnailFs = _thumbnailStorage.ReadStream(
+				ThumbnailNameHelper.Combine(fileIndexItem.FileHash!, ThumbnailSize.Large));
+			return File(thumbnailFs, "image/jpeg");
+		}
 	}
 }
diff --git a/starsky/starsky/Controllers/ErrorController.cs b/starsky/starsky/Controllers/ErrorController.cs
index 0560d0dd8d..5ad82af93d 100644
--- a/starsky/starsky/Controllers/ErrorController.cs
+++ b/starsky/starsky/Controllers/ErrorController.cs
@@ -6,16 +6,16 @@
 namespace starsky.Controllers
 {
 	[AllowAnonymous]
-	public sealed class ErrorController : Controller  
+	public sealed class ErrorController : Controller
 	{
-		private readonly string  _clientApp;
+		private readonly string _clientApp;
 
 		public ErrorController(AppSettings appSettings)
 		{
 			_clientApp = Path.Combine(appSettings.BaseDirectoryProject,
 				"clientapp", "build", "index.html");
 		}
-		
+
 		/// 
 		/// Return Error page (HTML)
 		/// 
@@ -25,7 +25,7 @@ public ErrorController(AppSettings appSettings)
 		[Produces("text/html")]
 		public IActionResult Error(int? statusCode = null)
 		{
-			if (statusCode.HasValue)
+			if ( statusCode.HasValue )
 			{
 				// here is the trick
 				HttpContext.Response.StatusCode = statusCode.Value;
diff --git a/starsky/starsky/Controllers/ExportController.cs b/starsky/starsky/Controllers/ExportController.cs
index 1a147e8bc4..0dd68b2d14 100644
--- a/starsky/starsky/Controllers/ExportController.cs
+++ b/starsky/starsky/Controllers/ExportController.cs
@@ -1,6 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
@@ -21,11 +21,12 @@ public sealed class ExportController : Controller
 		private readonly IStorage _hostFileSystemStorage;
 		private readonly IExport _export;
 
-		public ExportController( IUpdateBackgroundTaskQueue queue,
+		public ExportController(IUpdateBackgroundTaskQueue queue,
 			ISelectorStorage selectorStorage, IExport export)
 		{
 			_bgTaskQueue = queue;
-			_hostFileSystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
+			_hostFileSystemStorage =
+				selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
 			_export = export;
 		}
 
@@ -39,27 +40,30 @@ public ExportController( IUpdateBackgroundTaskQueue queue,
 		/// the name of the to generated zip file
 		/// files not found
 		[HttpPost("/api/export/create-zip")]
-		[ProducesResponseType(typeof(string),200)] // "zipHash"
-		[ProducesResponseType(typeof(List),404)] // "Not found"
+		[ProducesResponseType(typeof(string), 200)] // "zipHash"
+		[ProducesResponseType(typeof(List), 404)] // "Not found"
 		[Produces("application/json")]
-		public async Task CreateZip(string f, bool collections = true, bool thumbnail = false)
+		public async Task CreateZip(string f, bool collections = true,
+			bool thumbnail = false)
 		{
 			var inputFilePaths = PathHelper.SplitInputFilePaths(f);
-			var (zipOutputName, fileIndexResultsList) = await _export.PreflightAsync(inputFilePaths, collections, thumbnail);
-			
+			var (zipOutputName, fileIndexResultsList) =
+				await _export.PreflightAsync(inputFilePaths, collections, thumbnail);
+
 			// When all items are not found
 			// allow read only
-			if (fileIndexResultsList.TrueForAll(p => p.Status != FileIndexItem.ExifStatus.Ok) )
+			if ( fileIndexResultsList.TrueForAll(p => p.Status != FileIndexItem.ExifStatus.Ok) )
 				return NotFound(fileIndexResultsList);
-			
+
 			// NOT covered: when try to export for example image thumbnails of xml file
-				
+
 			// Creating a zip is a background task
-			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
-			{
-				await _export.CreateZip(fileIndexResultsList, thumbnail, zipOutputName);
-			}, zipOutputName);
-			
+			await _bgTaskQueue.QueueBackgroundWorkItemAsync(
+				async _ =>
+				{
+					await _export.CreateZip(fileIndexResultsList, thumbnail, zipOutputName);
+				}, zipOutputName, Activity.Current?.Id);
+
 			// for the rest api
 			return Json(zipOutputName);
 		}
@@ -90,8 +94,8 @@ public ActionResult Status(string f, bool json = false)
 			}
 
 			if ( json ) return Json("OK");
-			
-			var fs = _hostFileSystemStorage.ReadStream(sourceFullPath);
+
+			var fs = _hostFileSystemStorage.ReadStream(sourceFullPath!);
 			// Return the right mime type
 			return File(fs, MimeHelper.GetMimeTypeByFileName(sourceFullPath));
 		}
diff --git a/starsky/starsky/Controllers/GeoController.cs b/starsky/starsky/Controllers/GeoController.cs
index 1dadb11adf..b9a7f494aa 100644
--- a/starsky/starsky/Controllers/GeoController.cs
+++ b/starsky/starsky/Controllers/GeoController.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
@@ -11,7 +12,6 @@
 using starsky.foundation.storage.Interfaces;
 using starsky.foundation.storage.Models;
 using starsky.foundation.storage.Storage;
-using starsky.foundation.webtelemetry.Helpers;
 using starsky.foundation.worker.Interfaces;
 
 namespace starsky.Controllers
@@ -26,7 +26,7 @@ public sealed class GeoController : Controller
 		private readonly IWebLogger _logger;
 
 		public GeoController(IUpdateBackgroundTaskQueue queue,
-			ISelectorStorage selectorStorage, 
+			ISelectorStorage selectorStorage,
 			IMemoryCache? memoryCache, IWebLogger logger, IServiceScopeFactory serviceScopeFactory)
 		{
 			_bgTaskQueue = queue;
@@ -44,8 +44,8 @@ public GeoController(IUpdateBackgroundTaskQueue queue,
 		/// the current status
 		/// cache service is missing
 		[HttpGet("/api/geo/status")]
-		[ProducesResponseType(typeof(GeoCacheStatus),200)] // "cache service is missing"
-		[ProducesResponseType(typeof(string),404)] // "Not found"
+		[ProducesResponseType(typeof(GeoCacheStatus), 200)] // "cache service is missing"
+		[ProducesResponseType(typeof(string), 404)] // "Not found"
 		[Produces("application/json")]
 		public IActionResult Status(
 			string f = "/")
@@ -53,8 +53,8 @@ public IActionResult Status(
 			if ( _cache == null ) return NotFound("cache service is missing");
 			return Json(new GeoCacheStatusService(_cache).Status(f));
 		}
-		
-		
+
+
 		/// 
 		/// Reverse lookup for Geo Information and/or add Geo location based on a GPX file within the same directory
 		/// 
@@ -67,8 +67,8 @@ public IActionResult Status(
 		/// User unauthorized
 		[HttpPost("/api/geo/sync")]
 		[Produces("application/json")]
-		[ProducesResponseType(typeof(string),404)] // event is fired
-		[ProducesResponseType(typeof(string),200)] // "Not found"
+		[ProducesResponseType(typeof(string), 404)] // event is fired
+		[ProducesResponseType(typeof(string), 200)] // "Not found"
 		public async Task GeoSyncFolder(
 			string f = "/",
 			bool index = true,
@@ -79,28 +79,22 @@ public async Task GeoSyncFolder(
 			{
 				return NotFound("Folder location is not found");
 			}
-			
-			var operationId = HttpContext.GetOperationId();
+
 
 			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 			{
-				_logger.LogInformation($"{nameof(GeoSyncFolder)} started {f} {DateTime.UtcNow.ToShortTimeString()}");
-				var operationHolder = RequestTelemetryHelper.GetOperationHolder(_serviceScopeFactory,
-					nameof(GeoSyncFolder), operationId);
-				
+				_logger.LogInformation(
+					$"{nameof(GeoSyncFolder)} started {f} {DateTime.UtcNow.ToShortTimeString()}");
+
 				var geoBackgroundTask = _serviceScopeFactory.CreateScope().ServiceProvider
 					.GetRequiredService();
 				var result = await geoBackgroundTask.GeoBackgroundTaskAsync(f, index,
 					overwriteLocationNames);
-				
-				operationHolder.SetData(_serviceScopeFactory, result);
-				
-				_logger.LogInformation($"{nameof(GeoSyncFolder)} end {f} {operationHolder.Telemetry?.Duration}");
-			}, f);
-			
+
+				_logger.LogInformation($"{nameof(GeoSyncFolder)} end {f} {result.Count}");
+			}, f, Activity.Current?.Id);
+
 			return Json("job started");
 		}
-
-		
 	}
 }
diff --git a/starsky/starsky/Controllers/GeoReverseLookupController.cs b/starsky/starsky/Controllers/GeoReverseLookupController.cs
index 0adfa036b5..60f1e765a0 100644
--- a/starsky/starsky/Controllers/GeoReverseLookupController.cs
+++ b/starsky/starsky/Controllers/GeoReverseLookupController.cs
@@ -14,7 +14,7 @@ public GeoReverseLookupController(IGeoReverseLookup geoReverseLookup)
 	{
 		_geoReverseLookup = geoReverseLookup;
 	}
-	
+
 	/// 
 	/// Reverse geo lookup
 	/// 
diff --git a/starsky/starsky/Controllers/HealthController.cs b/starsky/starsky/Controllers/HealthController.cs
index 01d7e3bb8f..ceb03fb6e6 100644
--- a/starsky/starsky/Controllers/HealthController.cs
+++ b/starsky/starsky/Controllers/HealthController.cs
@@ -2,36 +2,29 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Runtime.CompilerServices;
-using System.Text.Json;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Diagnostics.HealthChecks;
-using starsky.foundation.platform.Exceptions;
 using starsky.foundation.platform.Extensions;
 using starsky.foundation.platform.VersionHelpers;
-using starsky.foundation.webtelemetry.Interfaces;
-using starsky.foundation.webtelemetry.Services;
 using starskycore.ViewModels;
 
 [assembly: InternalsVisibleTo("starskytest")]
+
 namespace starsky.Controllers
 {
-	public sealed class HealthController: Controller
+	public sealed class HealthController : Controller
 	{
 		private readonly HealthCheckService _service;
-		private readonly ApplicationInsightsJsHelper? _applicationInsightsJsHelper;
-		private readonly ITelemetryService _telemetryService;
 		private readonly IMemoryCache? _cache;
 
-		public HealthController(HealthCheckService service, ITelemetryService telemetryService, 
-			ApplicationInsightsJsHelper? applicationInsightsJsHelper = null, IMemoryCache? memoryCache = null)
+		public HealthController(HealthCheckService service,
+			IMemoryCache? memoryCache = null)
 		{
 			_service = service;
-			_applicationInsightsJsHelper = applicationInsightsJsHelper;
-			_telemetryService = telemetryService;
 			_cache = memoryCache;
 		}
 
@@ -53,7 +46,6 @@ public async Task Index()
 			var result = await CheckHealthAsyncWithTimeout(10000);
 			if ( result.Status == HealthStatus.Healthy ) return Content(result.Status.ToString());
 			Response.StatusCode = 503;
-			PushNonHealthResultsToTelemetry(result);
 			return Content(result.Status.ToString());
 		}
 
@@ -67,18 +59,20 @@ internal async Task CheckHealthAsyncWithTimeout(int timeoutTime =
 			const string healthControllerCacheKey = "health";
 			try
 			{
-				if ( _cache != null && _cache.TryGetValue(healthControllerCacheKey, out var objectHealthStatus) && 
-				     objectHealthStatus is HealthReport healthStatus && 
-				     healthStatus.Status == HealthStatus.Healthy )
+				if ( _cache != null &&
+					 _cache.TryGetValue(healthControllerCacheKey, out var objectHealthStatus) &&
+					 objectHealthStatus is HealthReport healthStatus &&
+					 healthStatus.Status == HealthStatus.Healthy )
 				{
 					return healthStatus;
 				}
-				
+
 				var result = await _service.CheckHealthAsync().TimeoutAfter(timeoutTime);
-				if (_cache != null && result.Status == HealthStatus.Healthy )
+				if ( _cache != null && result.Status == HealthStatus.Healthy )
 				{
-					_cache.Set(healthControllerCacheKey, result, new TimeSpan(0,1,30));
+					_cache.Set(healthControllerCacheKey, result, new TimeSpan(0, 1, 30));
 				}
+
 				return result;
 			}
 			catch ( TimeoutException exception )
@@ -89,25 +83,13 @@ objectHealthStatus is HealthReport healthStatus &&
 					TimeSpan.FromMilliseconds(timeoutTime),
 					exception,
 					null);
-				
+
 				return new HealthReport(
-					new Dictionary{{"timeout",entry}}, 
+					new Dictionary { { "timeout", entry } },
 					TimeSpan.FromMilliseconds(timeoutTime));
 			}
 		}
 
-		/// 
-		/// Push Non Healthy results to Telemetry Service
-		/// 
-		/// report
-		private void PushNonHealthResultsToTelemetry(HealthReport result)
-		{
-			if ( result.Status == HealthStatus.Healthy ) return;
-			var message = JsonSerializer.Serialize(CreateHealthEntryLog(result).Entries.Where(p => !p.IsHealthy));
-			_telemetryService.TrackException(
-				new TelemetryServiceException(message)
-			);
-		}
 
 		/// 
 		/// Check if the service has any known errors
@@ -120,17 +102,16 @@ private void PushNonHealthResultsToTelemetry(HealthReport result)
 		[HttpGet("/api/health/details")]
 		[Authorize] // <--------------
 		[Produces("application/json")]
-		[ProducesResponseType(typeof(HealthView),200)]
-		[ProducesResponseType(typeof(HealthView),503)]
+		[ProducesResponseType(typeof(HealthView), 200)]
+		[ProducesResponseType(typeof(HealthView), 503)]
 		[ProducesResponseType(401)]
 		public async Task Details()
 		{
 			var result = await CheckHealthAsyncWithTimeout();
-			PushNonHealthResultsToTelemetry(result);
 
 			var health = CreateHealthEntryLog(result);
 			if ( !health.IsHealthy ) Response.StatusCode = 503;
-			
+
 			return Json(health);
 		}
 
@@ -141,42 +122,30 @@ private static HealthView CreateHealthEntryLog(HealthReport result)
 				IsHealthy = result.Status == HealthStatus.Healthy,
 				TotalDuration = result.TotalDuration
 			};
-			
+
 			foreach ( var (key, value) in result.Entries )
 			{
 				health.Entries.Add(
-					new HealthEntry{
-							Duration = value.Duration, 
-							Name = key, 
-							IsHealthy = value.Status == HealthStatus.Healthy,
-							Description = value.Description + value.Exception?.Message + value.Exception?.StackTrace
-						}
-					);
+					new HealthEntry
+					{
+						Duration = value.Duration,
+						Name = key,
+						IsHealthy = value.Status == HealthStatus.Healthy,
+						Description = value.Description + value.Exception?.Message +
+									  value.Exception?.StackTrace
+					}
+				);
 			}
+
 			return health;
 		}
-				
-		/// 
-		/// Add Application Insights script to user context
-		/// 
-		/// AI script
-		/// Ok
-		[HttpGet("/api/health/application-insights")]
-		[ResponseCache(Duration = 29030400)] // 4 weeks
-		[Produces("application/javascript")]
-		[AllowAnonymous]
-		public IActionResult ApplicationInsights()
-		{
-			// Remove when ApplicationInsights is phased out
-			return Content(_applicationInsightsJsHelper?.ScriptPlain!, "application/javascript");
-		}
 
 		/// 
 		/// Check if min version is matching
 		/// keywords: MinVersion or Version( or SemVersion(
 		/// 
 		internal const string MinimumVersion = "0.5"; // only insert 0.5 or 0.6
-		
+
 		/// 
 		/// Name of the header for api version
 		/// 
@@ -199,15 +168,16 @@ public IActionResult Version(string? version = null)
 				var headerVersion =
 					Request.Headers.FirstOrDefault(p =>
 						p.Key == ApiVersionHeaderName).Value;
-				if (!string.IsNullOrEmpty(headerVersion))
+				if ( !string.IsNullOrEmpty(headerVersion) )
 				{
 					version = headerVersion;
 				}
 			}
 
-			if ( string.IsNullOrEmpty(version))
+			if ( string.IsNullOrEmpty(version) )
 			{
-				return BadRequest($"Missing version data, Add {ApiVersionHeaderName} header with value");
+				return BadRequest(
+					$"Missing version data, Add {ApiVersionHeaderName} header with value");
 			}
 
 			try
@@ -216,6 +186,7 @@ public IActionResult Version(string? version = null)
 				{
 					return Ok(version);
 				}
+
 				return StatusCode(StatusCodes.Status202Accepted,
 					$"Please Upgrade ClientApp to {MinimumVersion} or newer");
 			}
diff --git a/starsky/starsky/Controllers/HomeController.cs b/starsky/starsky/Controllers/HomeController.cs
index 1c9d12d348..9d77106361 100644
--- a/starsky/starsky/Controllers/HomeController.cs
+++ b/starsky/starsky/Controllers/HomeController.cs
@@ -15,7 +15,7 @@ namespace starsky.Controllers
 	[Authorize]
 	public sealed class HomeController : Controller
 	{
-		private readonly string  _clientApp;
+		private readonly string _clientApp;
 		private readonly IAntiforgery _antiForgery;
 
 		public HomeController(AppSettings appSettings, IAntiforgery antiForgery)
@@ -55,19 +55,19 @@ public IActionResult Index(string f = "")
 		[HttpPost("/search")]
 		public IActionResult SearchPost(string t = "", int p = 0)
 		{
-			if (string.IsNullOrEmpty(t))
+			if ( string.IsNullOrEmpty(t) )
 			{
 				return Redirect($"/search");
 			}
-			
+
 			// Added filter to prevent redirects based on tainted, user-controlled data
 			// unescaped: ^[a-zA-Z0-9_\-+"'/=:,\.>< ]+$
-			if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:,\\.>< ]+$", 
+			if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:,\\.>< ]+$",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
 			{
 				return BadRequest("`t` is not allowed");
 			}
-			return Redirect(AppendPathBasePrefix(Request.PathBase.Value,$"/search?t={t}&p={p}"));
+			return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search?t={t}&p={p}"));
 		}
 
 		/// 
@@ -86,29 +86,29 @@ public IActionResult SearchPost(string t = "", int p = 0)
 		[ProducesResponseType(400)]
 		[ProducesResponseType(401)]
 		[HttpGet("/search")]
-		public IActionResult Search(string t= "", int p = 0)
+		public IActionResult Search(string t = "", int p = 0)
 		{
 			new AntiForgeryCookie(_antiForgery).SetAntiForgeryCookie(HttpContext);
 
 			if ( !IsCaseSensitiveRedirect("/search", Request.Path.Value) )
 				return PhysicalFile(_clientApp, "text/html");
-			
+
 			// if not case sensitive is already served
-			if (string.IsNullOrEmpty(t))
+			if ( string.IsNullOrEmpty(t) )
 			{
-				return Redirect(AppendPathBasePrefix(Request.PathBase.Value,$"/search"));
+				return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search"));
 			}
 
 			// Added filter to prevent redirects based on tainted, user-controlled data
 			// unescaped: ^[a-zA-Z0-9_\-+"'/=:>< ]+$
-			if (!Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:>< ]+$", 
+			if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:>< ]+$",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
 			{
 				return BadRequest("`t` is not allowed");
 			}
-			return Redirect(AppendPathBasePrefix(Request.PathBase.Value,$"/search?t={t}&p={p}"));
+			return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search?t={t}&p={p}"));
 		}
-		
+
 		/// 
 		/// Trash page (HTML)
 		/// 
@@ -122,11 +122,11 @@ public IActionResult Search(string t= "", int p = 0)
 		[ProducesResponseType(301)]
 		[ProducesResponseType(401)]
 		[HttpGet("/trash")]
-		public IActionResult Trash( int p = 0)
+		public IActionResult Trash(int p = 0)
 		{
 			if ( IsCaseSensitiveRedirect("/trash", Request.Path.Value) )
 			{
-				return Redirect(AppendPathBasePrefix(Request.PathBase.Value,$"/trash?p={p}"));
+				return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/trash?p={p}"));
 			}
 			return PhysicalFile(_clientApp, "text/html");
 		}
@@ -145,11 +145,11 @@ public IActionResult Import()
 		{
 			if ( IsCaseSensitiveRedirect("/import", Request.Path.Value) )
 			{
-				return Redirect(AppendPathBasePrefix(Request.PathBase.Value,$"/import"));
+				return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/import"));
 			}
 			return PhysicalFile(_clientApp, "text/html");
 		}
-		
+
 		/// 
 		/// Preferences page (HTML)
 		/// 
@@ -164,11 +164,11 @@ public IActionResult Preferences()
 		{
 			if ( IsCaseSensitiveRedirect("/preferences", Request.Path.Value) )
 			{
-				return Redirect(AppendPathBasePrefix(Request.PathBase.Value,$"/preferences"));
+				return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/preferences"));
 			}
 			return PhysicalFile(_clientApp, "text/html");
 		}
-		
+
 		/// 
 		/// View the Register form (HTML)
 		/// 
@@ -189,7 +189,7 @@ public IActionResult Register(string? returnUrl = null)
 
 		internal static string AppendPathBasePrefix(string? requestPathBase, string url)
 		{
-			return requestPathBase?.Equals("/starsky", 
+			return requestPathBase?.Equals("/starsky",
 				StringComparison.InvariantCultureIgnoreCase) == true ? $"/starsky{url}" : url;
 		}
 
diff --git a/starsky/starsky/Controllers/ImportController.cs b/starsky/starsky/Controllers/ImportController.cs
index d71515f6be..517a62afd8 100644
--- a/starsky/starsky/Controllers/ImportController.cs
+++ b/starsky/starsky/Controllers/ImportController.cs
@@ -1,7 +1,6 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Authorization;
@@ -31,7 +30,8 @@ namespace starsky.Controllers
 {
 	[Authorize] // <- should be logged in!
 	[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "S5693:Make sure the content " +
-		"length limit is safe here", Justification = "Is checked")]
+	                                                          "length limit is safe here",
+		Justification = "Is checked")]
 	public sealed class ImportController : Controller
 	{
 		private readonly IImport _import;
@@ -44,20 +44,21 @@ public sealed class ImportController : Controller
 		private readonly IWebLogger _logger;
 
 		public ImportController(IImport import, AppSettings appSettings,
-			IUpdateBackgroundTaskQueue queue, 
-			IHttpClientHelper httpClientHelper, ISelectorStorage selectorStorage, 
+			IUpdateBackgroundTaskQueue queue,
+			IHttpClientHelper httpClientHelper, ISelectorStorage selectorStorage,
 			IServiceScopeFactory scopeFactory, IWebLogger logger)
 		{
 			_appSettings = appSettings;
 			_import = import;
 			_bgTaskQueue = queue;
 			_httpClientHelper = httpClientHelper;
-			_selectorStorage = selectorStorage; 
-			_hostFileSystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
+			_selectorStorage = selectorStorage;
+			_hostFileSystemStorage =
+				selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
 			_scopeFactory = scopeFactory;
 			_logger = logger;
 		}
-        
+
 		/// 
 		/// Import a file using the structure format
 		/// 
@@ -70,29 +71,33 @@ public ImportController(IImport import, AppSettings appSettings,
 		[Produces("application/json")]
 		[RequestFormLimits(MultipartBodyLengthLimit = 320_000_000)]
 		[RequestSizeLimit(320_000_000)] // in bytes, 305MB
-		[ProducesResponseType(typeof(List),200)] // yes
-		[ProducesResponseType(typeof(List),206)]  // When all items are already imported
-		[ProducesResponseType(typeof(List),415)]  // Wrong input (e.g. wrong extenstion type)
+		[ProducesResponseType(typeof(List), 200)] // yes
+		[ProducesResponseType(typeof(List),
+			206)] // When all items are already imported
+		[ProducesResponseType(typeof(List),
+			415)] // Wrong input (e.g. wrong extenstion type)
 		public async Task IndexPost() // aka ActionResult Import
 		{
-			var tempImportPaths = await Request.StreamFile(_appSettings,_selectorStorage);
+			var tempImportPaths = await Request.StreamFile(_appSettings, _selectorStorage);
 			var importSettings = new ImportSettingsModel(Request);
 
 			var fileIndexResultsList = await _import.Preflight(tempImportPaths, importSettings);
 
 			// Import files >
-			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
-			{
-				await ImportPostBackgroundTask(tempImportPaths, importSettings, _appSettings.IsVerbose());
-			}, string.Join(",", tempImportPaths));
-            
+			await _bgTaskQueue.QueueBackgroundWorkItemAsync(
+				async _ =>
+				{
+					await ImportPostBackgroundTask(tempImportPaths, importSettings,
+						_appSettings.IsVerbose());
+				}, string.Join(",", tempImportPaths));
+
 			// When all items are already imported
 			if ( importSettings.IndexMode &&
 			     fileIndexResultsList.TrueForAll(p => p.Status != ImportStatus.Ok) )
 			{
 				Response.StatusCode = 206;
 			}
-            
+
 			// Wrong input (extension is not allowed)
 			if ( fileIndexResultsList.TrueForAll(p => p.Status == ImportStatus.FileError) )
 			{
@@ -102,11 +107,12 @@ await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 			return Json(fileIndexResultsList);
 		}
 
-		internal async Task> ImportPostBackgroundTask(List tempImportPaths,
+		internal async Task> ImportPostBackgroundTask(
+			List tempImportPaths,
 			ImportSettingsModel importSettings, bool isVerbose = false)
 		{
 			List importedFiles;
-	            
+
 			using ( var scope = _scopeFactory.CreateScope() )
 			{
 				var selectorStorage = scope.ServiceProvider.GetRequiredService();
@@ -114,30 +120,32 @@ internal async Task> ImportPostBackgroundTask(List
 				var exifTool = scope.ServiceProvider.GetRequiredService();
 				var query = scope.ServiceProvider.GetRequiredService();
 				var console = scope.ServiceProvider.GetRequiredService();
-				var metaExifThumbnailService = scope.ServiceProvider.GetRequiredService();
+				var metaExifThumbnailService =
+					scope.ServiceProvider.GetRequiredService();
 				var memoryCache = scope.ServiceProvider.GetRequiredService();
 				var thumbnailQuery = scope.ServiceProvider.GetRequiredService();
 
 				// use of IImport direct does not work
-				importedFiles = await new Import(selectorStorage,_appSettings,
-					importQuery, exifTool, query,console, 
-					metaExifThumbnailService, _logger, thumbnailQuery, memoryCache).Importer(tempImportPaths, importSettings);
+				importedFiles = await new Import(selectorStorage, _appSettings,
+						importQuery, exifTool, query, console,
+						metaExifThumbnailService, _logger, thumbnailQuery, memoryCache)
+					.Importer(tempImportPaths, importSettings);
 			}
-	            
-			if (isVerbose)
+
+			if ( isVerbose )
 			{
-				foreach (var file in importedFiles)
+				foreach ( var file in importedFiles )
 				{
 					_logger.LogInformation(
-							$"[ImportPostBackgroundTask] import {file.Status} " +
-							$"=> {file.FilePath} ~ {file.FileIndexItem?.FilePath}");
+						$"[ImportPostBackgroundTask] import {file.Status} " +
+						$"=> {file.FilePath} ~ {file.FileIndexItem?.FilePath}");
 				}
 			}
-                
+
 			// Remove source files
 			foreach ( var toDelPath in tempImportPaths )
 			{
-				new RemoveTempAndParentStreamFolderHelper(_hostFileSystemStorage,_appSettings)
+				new RemoveTempAndParentStreamFolderHelper(_hostFileSystemStorage, _appSettings)
 					.RemoveTempAndParentStreamFolder(toDelPath);
 			}
 
@@ -156,34 +164,36 @@ internal async Task> ImportPostBackgroundTask(List
 		/// file already imported
 		/// the file url is not found or the domain is not whitelisted
 		[HttpPost("/api/import/fromUrl")]
-		[ProducesResponseType(typeof(List),200)] // yes
-		[ProducesResponseType(typeof(List),206)] // file already imported
+		[ProducesResponseType(typeof(List), 200)] // yes
+		[ProducesResponseType(typeof(List), 206)] // file already imported
 		[ProducesResponseType(404)] // url 404
 		[Produces("application/json")]
 		public async Task FromUrl(string fileUrl, string filename, string structure)
 		{
-			if (filename == null) filename = Base32.Encode(FileHash.GenerateRandomBytes(8)) + ".unknown";
-	        
+			if ( filename == null )
+				filename = Base32.Encode(FileHash.GenerateRandomBytes(8)) + ".unknown";
+
 			// I/O function calls should not be vulnerable to path injection attacks
-			if (!Regex.IsMatch(filename, "^[a-zA-Z0-9_\\s\\.]+$", 
-				RegexOptions.None, TimeSpan.FromMilliseconds(100)) || !FilenamesHelper.IsValidFileName(filename))
+			if ( !Regex.IsMatch(filename, "^[a-zA-Z0-9_\\s\\.]+$",
+				     RegexOptions.None, TimeSpan.FromMilliseconds(100)) ||
+			     !FilenamesHelper.IsValidFileName(filename) )
 			{
 				return BadRequest();
 			}
-	        
+
 			var tempImportFullPath = Path.Combine(_appSettings.TempFolder, filename);
-			var importSettings = new ImportSettingsModel(Request) {Structure = structure};
-			var isDownloaded = await _httpClientHelper.Download(fileUrl,tempImportFullPath);
-			if (!isDownloaded) return NotFound("'file url' not found or domain not allowed " + fileUrl);
+			var importSettings = new ImportSettingsModel(Request) { Structure = structure };
+			var isDownloaded = await _httpClientHelper.Download(fileUrl, tempImportFullPath);
+			if ( !isDownloaded )
+				return NotFound("'file url' not found or domain not allowed " + fileUrl);
 
-			var importedFiles = await _import.Importer(new List{tempImportFullPath}, importSettings);
-			new RemoveTempAndParentStreamFolderHelper(_hostFileSystemStorage,_appSettings)
+			var importedFiles =
+				await _import.Importer(new List { tempImportFullPath }, importSettings);
+			new RemoveTempAndParentStreamFolderHelper(_hostFileSystemStorage, _appSettings)
 				.RemoveTempAndParentStreamFolder(tempImportFullPath);
-			
-			if(importedFiles.Count == 0) Response.StatusCode = 206;
+
+			if ( importedFiles.Count == 0 ) Response.StatusCode = 206;
 			return Json(importedFiles);
 		}
-
-
 	}
 }
diff --git a/starsky/starsky/Controllers/ImportHistoryController.cs b/starsky/starsky/Controllers/ImportHistoryController.cs
index b7c8456c82..6f7b1a78bc 100644
--- a/starsky/starsky/Controllers/ImportHistoryController.cs
+++ b/starsky/starsky/Controllers/ImportHistoryController.cs
@@ -15,14 +15,14 @@ public ImportHistoryController(IImportQuery importQuery)
 		{
 			_importQuery = importQuery;
 		}
-		
+
 		/// 
 		/// Today's imported files
 		/// 
 		/// list of files
 		/// done
 		[HttpGet("/api/import/history")]
-		[ProducesResponseType(typeof(List),200)] // yes
+		[ProducesResponseType(typeof(List), 200)] // yes
 		[Produces("application/json")]
 		public IActionResult History()
 		{
diff --git a/starsky/starsky/Controllers/ImportThumbnailController.cs b/starsky/starsky/Controllers/ImportThumbnailController.cs
index 752cdf422a..d70b3de581 100644
--- a/starsky/starsky/Controllers/ImportThumbnailController.cs
+++ b/starsky/starsky/Controllers/ImportThumbnailController.cs
@@ -31,11 +31,11 @@ public class ImportThumbnailController : Controller
 	private readonly RemoveTempAndParentStreamFolderHelper _removeTempAndParentStreamFolderHelper;
 
 	public ImportThumbnailController(AppSettings appSettings,
-		ISelectorStorage selectorStorage, 
+		ISelectorStorage selectorStorage,
 		IWebLogger logger, IThumbnailQuery thumbnailQuery)
 	{
 		_appSettings = appSettings;
-		_selectorStorage = selectorStorage; 
+		_selectorStorage = selectorStorage;
 		_hostFileSystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem);
 		_thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail);
 		_logger = logger;
@@ -44,7 +44,7 @@ public ImportThumbnailController(AppSettings appSettings,
 			new RemoveTempAndParentStreamFolderHelper(_hostFileSystemStorage,
 				_appSettings);
 	}
-	
+
 	/// 
 	/// Upload thumbnail to ThumbnailTempFolder
 	/// Make sure that the filename is correct, a base32 hash of length 26;
@@ -59,8 +59,8 @@ public ImportThumbnailController(AppSettings appSettings,
 	[Produces("application/json")]
 	[RequestFormLimits(MultipartBodyLengthLimit = 100_000_000)]
 	[RequestSizeLimit(100_000_000)] // in bytes, 100MB
-	[ProducesResponseType(typeof(List),200)] // yes
-	[ProducesResponseType(typeof(List),415)]  // wrong input
+	[ProducesResponseType(typeof(List), 200)] // yes
+	[ProducesResponseType(typeof(List), 415)]  // wrong input
 	public async Task Thumbnail()
 	{
 		var tempImportPaths = await Request.StreamFile(_appSettings, _selectorStorage);
@@ -69,9 +69,9 @@ public async Task Thumbnail()
 
 		// Move the files to the correct location
 		await WriteThumbnails(tempImportPaths, thumbnailNamesWithSuffix);
-		
+
 		// Status if there is nothing uploaded
-		if (tempImportPaths.Count != thumbnailNamesWithSuffix.Count)
+		if ( tempImportPaths.Count != thumbnailNamesWithSuffix.Count )
 		{
 			Response.StatusCode = 415;
 			return Json(thumbnailNamesWithSuffix);
@@ -79,7 +79,7 @@ public async Task Thumbnail()
 
 		var thumbnailItems = MapToTransferObject(thumbnailNamesWithSuffix).ToList();
 		await _thumbnailQuery.AddThumbnailRangeAsync(thumbnailItems);
-		
+
 		return Json(thumbnailNamesWithSuffix);
 	}
 
@@ -91,7 +91,7 @@ internal static IEnumerable MapToTransferObjec
 			var thumb = ThumbnailNameHelper.GetSize(thumbnailNameWithSuffix);
 			var name = ThumbnailNameHelper.RemoveSuffix(thumbnailNameWithSuffix);
 			var item = new ThumbnailResultDataTransferModel(name);
-			item.Change(thumb,true);
+			item.Change(thumb, true);
 			items.Add(item);
 		}
 		return items;
@@ -104,23 +104,23 @@ private List GetThumbnailNamesWithSuffix(List tempImportPaths)
 		foreach ( var tempImportSinglePath in tempImportPaths )
 		{
 			var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(tempImportSinglePath);
-			    
+
 			var thumbToUpperCase = fileNameWithoutExtension.ToUpperInvariant();
-				
-			_logger.LogInformation($"[Import/Thumbnail] - {thumbToUpperCase}" );
+
+			_logger.LogInformation($"[Import/Thumbnail] - {thumbToUpperCase}");
 
 			if ( ThumbnailNameHelper.GetSize(thumbToUpperCase) == ThumbnailSize.Unknown )
 			{
 				continue;
 			}
-			    
+
 			// remove existing thumbnail if exist
-			if (_thumbnailStorage.ExistFile(thumbToUpperCase))
+			if ( _thumbnailStorage.ExistFile(thumbToUpperCase) )
 			{
-				_logger.LogInformation($"[Import/Thumbnail] remove already exists - {thumbToUpperCase}" );
+				_logger.LogInformation($"[Import/Thumbnail] remove already exists - {thumbToUpperCase}");
 				_thumbnailStorage.FileDelete(thumbToUpperCase);
 			}
-			    
+
 			thumbnailNamesWithSuffix.Add(thumbToUpperCase);
 		}
 		return thumbnailNamesWithSuffix;
@@ -128,23 +128,23 @@ private List GetThumbnailNamesWithSuffix(List tempImportPaths)
 
 	internal async Task WriteThumbnails(List tempImportPaths, List thumbnailNames)
 	{
-		if (tempImportPaths.Count != thumbnailNames.Count)
+		if ( tempImportPaths.Count != thumbnailNames.Count )
 		{
 			_removeTempAndParentStreamFolderHelper.RemoveTempAndParentStreamFolder(tempImportPaths);
 			return false;
 		}
-		
+
 		for ( var i = 0; i < tempImportPaths.Count; i++ )
 		{
-			if ( ! _hostFileSystemStorage.ExistFile(tempImportPaths[i]) )
+			if ( !_hostFileSystemStorage.ExistFile(tempImportPaths[i]) )
 			{
 				_logger.LogInformation($"[Import/Thumbnail] ERROR {tempImportPaths[i]} does not exist");
 				continue;
 			}
-				
+
 			await _thumbnailStorage.WriteStreamAsync(
 				_hostFileSystemStorage.ReadStream(tempImportPaths[i]), thumbnailNames[i]);
-				
+
 			// Remove from temp folder to avoid long list of files
 			_removeTempAndParentStreamFolderHelper.RemoveTempAndParentStreamFolder(tempImportPaths[i]);
 		}
diff --git a/starsky/starsky/Controllers/IndexController.cs b/starsky/starsky/Controllers/IndexController.cs
index 1aad51d8ab..b240aee0db 100644
--- a/starsky/starsky/Controllers/IndexController.cs
+++ b/starsky/starsky/Controllers/IndexController.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using starsky.foundation.database.Helpers;
@@ -10,101 +10,103 @@
 
 namespace starsky.Controllers
 {
-    [Authorize]
-    public sealed class IndexController : Controller
-    {
-        private readonly IQuery _query;
-        private readonly AppSettings _appSettings;
+	[Authorize]
+	public sealed class IndexController : Controller
+	{
+		private readonly IQuery _query;
+		private readonly AppSettings _appSettings;
 
-        public IndexController(IQuery query, AppSettings appSettings)
-        {
-            _query = query;
-            _appSettings = appSettings;
-        }
-        
-	    /// 
-	    /// The database-view of a directory
-	    /// 
-	    /// subPath
-	    /// filter on colorClass (use int)
-	    /// to combine files with the same name before the extension
-	    /// ignore deleted files
-	    /// how to orderBy, defaults to fileName
-	    /// 
-	    /// returns a list of items from the database
-	    /// subPath not found in the database
-	    /// User unauthorized
-	    [HttpGet("/api/index")]
+		public IndexController(IQuery query, AppSettings appSettings)
+		{
+			_query = query;
+			_appSettings = appSettings;
+		}
+
+		/// 
+		/// The database-view of a directory
+		/// 
+		/// subPath
+		/// filter on colorClass (use int)
+		/// to combine files with the same name before the extension
+		/// ignore deleted files
+		/// how to orderBy, defaults to fileName
+		/// 
+		/// returns a list of items from the database
+		/// subPath not found in the database
+		/// User unauthorized
+		[HttpGet("/api/index")]
 		[Produces("application/json")]
-		[ProducesResponseType(typeof(ArchiveViewModel),200)]
+		[ProducesResponseType(typeof(ArchiveViewModel), 200)]
 		[ProducesResponseType(404)]
-	    [ProducesResponseType(401)]
-	    public IActionResult Index(
-            string f = "/", 
-            string? colorClass = null,
-            bool collections = true,
-            bool hideDelete = true,
-            SortType sort = SortType.FileName)
-        {
-            // Used in Detail and Index View => does not hide this single item
-            var colorClassActiveList = FileIndexItem.GetColorClassList(colorClass);
+		[ProducesResponseType(401)]
+		public IActionResult Index(
+			string f = "/",
+			string? colorClass = null,
+			bool collections = true,
+			bool hideDelete = true,
+			SortType sort = SortType.FileName)
+		{
+			// Used in Detail and Index View => does not hide this single item
+			var colorClassActiveList = FileIndexItem.GetColorClassList(colorClass);
+
+			var subPath = PathHelper.PrefixDbSlash(f);
+			subPath = PathHelper.RemoveLatestSlash(subPath);
+			if ( string.IsNullOrEmpty(subPath) ) subPath = "/";
+
+			// First check if it is a single Item
+			var singleItem = _query.SingleItem(subPath, colorClassActiveList,
+				collections, hideDelete, sort);
+			// returns no object when it a directory
+
+			if ( singleItem is { IsDirectory: false } )
+			{
+				singleItem.IsReadOnly =
+					_appSettings.IsReadOnly(singleItem.FileIndexItem?.ParentDirectory!);
+				return Json(singleItem);
+			}
 
-            var subPath = PathHelper.PrefixDbSlash(f);
-            subPath = PathHelper.RemoveLatestSlash(subPath);
-            if ( string.IsNullOrEmpty(subPath) ) subPath = "/";
+			var fileIndexItems = SortHelper.Helper(
+				_query.DisplayFileFolders(subPath, colorClassActiveList,
+					collections, hideDelete), sort).ToList();
+			var fileIndexItemsWithoutCollections = _query.DisplayFileFolders(
+				subPath, null, false, hideDelete).ToList();
 
-            // First check if it is a single Item
-            var singleItem = _query.SingleItem(subPath, colorClassActiveList,
-	            collections,hideDelete, sort);
-            // returns no object when it a directory
-            
-            if (singleItem is { IsDirectory: false })
-            {
-	            singleItem.IsReadOnly = _appSettings.IsReadOnly(singleItem.FileIndexItem?.ParentDirectory!);
-                return Json(singleItem);
-            }
+			// (singleItem.IsDirectory) or not found
+			var directoryModel = new ArchiveViewModel
+			{
+				FileIndexItems = fileIndexItems,
+				ColorClassActiveList = colorClassActiveList,
+				RelativeObjects =
+					_query.GetNextPrevInFolder(subPath), // Args are not shown in this view
+				Breadcrumb = Breadcrumbs.BreadcrumbHelper(subPath),
+				SearchQuery = subPath.Split("/").LastOrDefault()!,
+				SubPath = subPath,
+				CollectionsCount =
+					fileIndexItemsWithoutCollections.Count(p => p.IsDirectory == false),
+				// when change colorClass selection you should see all options
+				ColorClassUsage = fileIndexItemsWithoutCollections
+					.Select(p => p.ColorClass).Distinct()
+					.OrderBy(p => ( int )( p )).ToList(),
+				IsReadOnly = _appSettings.IsReadOnly(subPath),
+				Collections = collections,
+			};
 
-            var fileIndexItems = SortHelper.Helper(
-	            _query.DisplayFileFolders(subPath, colorClassActiveList,
-		            collections, hideDelete), sort).ToList();
-            var fileIndexItemsWithoutCollections = _query.DisplayFileFolders(
-	            subPath, null, false, hideDelete).ToList();
-            
-            // (singleItem.IsDirectory) or not found
-            var directoryModel = new ArchiveViewModel
-            {
-                FileIndexItems = fileIndexItems,
-                ColorClassActiveList = 	colorClassActiveList,
-                RelativeObjects = _query.GetNextPrevInFolder(subPath), // Args are not shown in this view
-                Breadcrumb = Breadcrumbs.BreadcrumbHelper(subPath),
-                SearchQuery = subPath.Split("/").LastOrDefault(),
-                SubPath = subPath,
-                CollectionsCount = fileIndexItemsWithoutCollections.
-	                Count(p => p.IsDirectory == false),
-                // when change colorClass selection you should see all options
-                ColorClassUsage = fileIndexItemsWithoutCollections
-	                .Select( p => p.ColorClass).Distinct()
-	                .OrderBy(p => (int) (p)).ToList(),
-                IsReadOnly =  _appSettings.IsReadOnly(subPath),
-                Collections = collections,
-            };
+			// For showing a new database
+			var queryIfFolder = _query.GetObjectByFilePath(subPath);
 
-            // For showing a new database
-            var queryIfFolder = _query.GetObjectByFilePath(subPath);
+			// For showing a new database
+			if ( f == "/" && queryIfFolder == null )
+			{
+				return Json(directoryModel);
+			}
 
-            // For showing a new database
-            if ( f == "/" && queryIfFolder == null )
-            {
-	            return Json(directoryModel);
-            }
+			if ( queryIfFolder != null )
+			{
+				return Json(directoryModel);
+			}
 
-            if ( queryIfFolder != null )
-            {
-	            return Json(directoryModel);
-            }
-            
-            Response.StatusCode = 404;
-            return Json("not found");
-        }
-    }
+			Response.StatusCode = 404;
+			return Json("not found");
+		}
+	}
 }
diff --git a/starsky/starsky/Controllers/MemoryCacheDebugController.cs b/starsky/starsky/Controllers/MemoryCacheDebugController.cs
index a6ae522986..8d151e6d5c 100644
--- a/starsky/starsky/Controllers/MemoryCacheDebugController.cs
+++ b/starsky/starsky/Controllers/MemoryCacheDebugController.cs
@@ -9,7 +9,7 @@
 namespace starsky.Controllers
 {
 	[Authorize]
-	public sealed class MemoryCacheDebugController: Controller
+	public sealed class MemoryCacheDebugController : Controller
 	{
 		private readonly IMemoryCache _memoryCache;
 		private readonly IWebLogger _logger;
@@ -19,7 +19,7 @@ public MemoryCacheDebugController(IMemoryCache memoryCache, IWebLogger logger)
 			_memoryCache = memoryCache;
 			_logger = logger;
 		}
-		
+
 		/// 
 		/// View data from the memory cache - use to debug
 		/// 
diff --git a/starsky/starsky/Controllers/MetaInfoController.cs b/starsky/starsky/Controllers/MetaInfoController.cs
index a2ee4d79e3..14f71761ff 100644
--- a/starsky/starsky/Controllers/MetaInfoController.cs
+++ b/starsky/starsky/Controllers/MetaInfoController.cs
@@ -18,45 +18,45 @@ public MetaInfoController(IMetaInfo metaInfo)
 		{
 			_metaInfo = metaInfo;
 		}
-		
+
 		/// 
-        /// Get realtime (cached a few minutes) about the file
-        /// 
-        /// subPaths split by dot comma
-        /// true is to update files with the same name before the extenstion
-        /// info of object
-        /// the item on disk
-        /// item not found on disk
-        /// you are not allowed to edit this item
-        /// User unauthorized
-        [HttpGet("/api/info")]
-        [ProducesResponseType(typeof(List),200)]
-        [ProducesResponseType(typeof(List),404)]
-        [ProducesResponseType(typeof(List),203)]
-        [Produces("application/json")]
-        public async Task InfoAsync(string f, bool collections = true)
-        {
-            var inputFilePaths = PathHelper.SplitInputFilePaths(f).ToList();
+		/// Get realtime (cached a few minutes) about the file
+		/// 
+		/// subPaths split by dot comma
+		/// true is to update files with the same name before the extenstion
+		/// info of object
+		/// the item on disk
+		/// item not found on disk
+		/// you are not allowed to edit this item
+		/// User unauthorized
+		[HttpGet("/api/info")]
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(typeof(List), 404)]
+		[ProducesResponseType(typeof(List), 203)]
+		[Produces("application/json")]
+		public async Task InfoAsync(string f, bool collections = true)
+		{
+			var inputFilePaths = PathHelper.SplitInputFilePaths(f).ToList();
+
+			var fileIndexResultsList = await _metaInfo.GetInfoAsync(inputFilePaths, collections);
 
-            var fileIndexResultsList = await _metaInfo.GetInfoAsync(inputFilePaths, collections);
-            
-            // returns read only
-            if (fileIndexResultsList.TrueForAll(p => p.Status == FileIndexItem.ExifStatus.ReadOnly))
-            {
-                Response.StatusCode = 203; // is readonly
-                return Json(fileIndexResultsList);
-            }
-                
-            // When all items are not found
-            if ( fileIndexResultsList.TrueForAll(p =>
-	                ( p.Status != FileIndexItem.ExifStatus.Ok &&
-	                  p.Status != FileIndexItem.ExifStatus.Deleted )) )
-            {
-	            return NotFound(fileIndexResultsList);
-            }
-            
-            return Json(fileIndexResultsList);
-        }
+			// returns read only
+			if ( fileIndexResultsList.TrueForAll(p => p.Status == FileIndexItem.ExifStatus.ReadOnly) )
+			{
+				Response.StatusCode = 203; // is readonly
+				return Json(fileIndexResultsList);
+			}
+
+			// When all items are not found
+			if ( fileIndexResultsList.TrueForAll(p =>
+					( p.Status != FileIndexItem.ExifStatus.Ok &&
+					  p.Status != FileIndexItem.ExifStatus.Deleted )) )
+			{
+				return NotFound(fileIndexResultsList);
+			}
+
+			return Json(fileIndexResultsList);
+		}
 
 	}
 }
diff --git a/starsky/starsky/Controllers/MetaReplaceController.cs b/starsky/starsky/Controllers/MetaReplaceController.cs
index a6d8dc7ec5..eebdc6c06a 100644
--- a/starsky/starsky/Controllers/MetaReplaceController.cs
+++ b/starsky/starsky/Controllers/MetaReplaceController.cs
@@ -25,7 +25,7 @@ public sealed class MetaReplaceController : Controller
 		private readonly IWebLogger _logger;
 		private readonly IServiceScopeFactory _scopeFactory;
 
-		public MetaReplaceController(IMetaReplaceService metaReplaceService,  IUpdateBackgroundTaskQueue queue, 
+		public MetaReplaceController(IMetaReplaceService metaReplaceService, IUpdateBackgroundTaskQueue queue,
 			IRealtimeConnectionsService connectionsService, IWebLogger logger, IServiceScopeFactory scopeFactory)
 		{
 			_scopeFactory = scopeFactory;
@@ -48,8 +48,8 @@ public MetaReplaceController(IMetaReplaceService metaReplaceService,  IUpdateBac
 		/// item(s) not found
 		/// User unauthorized
 		[HttpPost("/api/replace")]
-		[ProducesResponseType(typeof(List),200)]
-		[ProducesResponseType(typeof(List),404)]
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(typeof(List), 404)]
 		[Produces("application/json")]
 		public async Task Replace(string f, string fieldName, string search,
 			string replace, bool collections = true)
@@ -58,15 +58,15 @@ public async Task Replace(string f, string fieldName, string sear
 
 			var fileIndexResultsList = await _metaReplaceService
 				.Replace(f, fieldName, search, replace, collections);
-		    
+
 			var resultsOkOrDeleteList = fileIndexResultsList.Where(
-				p => p.Status is FileIndexItem.ExifStatus.Ok 
-					or FileIndexItem.ExifStatus.OkAndSame 
-					or FileIndexItem.ExifStatus.Deleted 
+				p => p.Status is FileIndexItem.ExifStatus.Ok
+					or FileIndexItem.ExifStatus.OkAndSame
+					or FileIndexItem.ExifStatus.Deleted
 					or FileIndexItem.ExifStatus.DeletedAndSame).ToList();
-			
+
 			var changedFileIndexItemName = resultsOkOrDeleteList.
-				ToDictionary(item => item.FilePath!, _ => new List {fieldName});
+				ToDictionary(item => item.FilePath!, _ => new List { fieldName });
 
 			// Update >
 			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
@@ -77,17 +77,17 @@ await metaUpdateService
 					.UpdateAsync(changedFileIndexItemName, resultsOkOrDeleteList,
 						null, collections, false, 0);
 			}, string.Empty);
-			
+
 			// before sending not founds
-			new StopWatchLogger(_logger).StopUpdateReplaceStopWatch("update", 
+			new StopWatchLogger(_logger).StopUpdateReplaceStopWatch("update",
 				fileIndexResultsList.FirstOrDefault()?.FilePath!, collections, stopwatch);
-			
+
 			// When all items are not found
-			if (resultsOkOrDeleteList.Count == 0 )
+			if ( resultsOkOrDeleteList.Count == 0 )
 			{
 				return NotFound(fileIndexResultsList);
 			}
-			
+
 			// Push direct to socket when update or replace to avoid undo after a second
 			var webSocketResponse =
 				new ApiNotificationResponseModel>(resultsOkOrDeleteList, ApiNotificationType.Replace);
diff --git a/starsky/starsky/Controllers/MetaUpdateController.cs b/starsky/starsky/Controllers/MetaUpdateController.cs
index 3b1ea87c8c..ac9eff27d8 100644
--- a/starsky/starsky/Controllers/MetaUpdateController.cs
+++ b/starsky/starsky/Controllers/MetaUpdateController.cs
@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
@@ -13,7 +14,6 @@
 using starsky.foundation.platform.Helpers;
 using starsky.foundation.platform.Interfaces;
 using starsky.foundation.platform.Models;
-using starsky.foundation.webtelemetry.Helpers;
 using starsky.foundation.worker.Interfaces;
 
 namespace starsky.Controllers
@@ -28,10 +28,10 @@ public sealed class MetaUpdateController : Controller
 		private readonly IServiceScopeFactory _scopeFactory;
 		private readonly IMetaUpdateService _metaUpdateService;
 
-		public MetaUpdateController(IMetaPreflight metaPreflight, 
+		public MetaUpdateController(IMetaPreflight metaPreflight,
 			IMetaUpdateService metaUpdateService,
-			IUpdateBackgroundTaskQueue queue, 
-			IWebLogger logger, 
+			IUpdateBackgroundTaskQueue queue,
+			IWebLogger logger,
 			IServiceScopeFactory scopeFactory)
 		{
 			_metaPreflight = metaPreflight;
@@ -40,7 +40,7 @@ public MetaUpdateController(IMetaPreflight metaPreflight,
 			_bgTaskQueue = queue;
 			_logger = logger;
 		}
-	    
+
 		/// 
 		/// Update Exif and Rotation API
 		/// 
@@ -58,12 +58,13 @@ public MetaUpdateController(IMetaPreflight metaPreflight,
 		/// item not found in the database or on disk
 		/// User unauthorized
 		[IgnoreAntiforgeryToken]
-		[ProducesResponseType(typeof(List),200)]
-		[ProducesResponseType(typeof(List),404)]
+		[ProducesResponseType(typeof(List), 200)]
+		[ProducesResponseType(typeof(List), 404)]
 		[ProducesResponseType(typeof(string), 400)]
 		[HttpPost("/api/update")]
 		[Produces("application/json")]
-		public async Task UpdateAsync(FileIndexItem inputModel, string f, bool append, 
+		public async Task UpdateAsync(FileIndexItem inputModel, string f,
+			bool append,
 			bool collections = true, int rotateClock = 0)
 		{
 			var inputFilePaths = PathHelper.SplitInputFilePaths(f);
@@ -71,32 +72,27 @@ public async Task UpdateAsync(FileIndexItem inputModel, string f,
 			{
 				return BadRequest("No input files");
 			}
-			
+
 			var stopwatch = StopWatchLogger.StartUpdateReplaceStopWatch();
 
-			var (fileIndexResultsList, changedFileIndexItemName) = 
-				await _metaPreflight.PreflightAsync(inputModel, 
-				inputFilePaths.ToList(), append, collections, rotateClock);
+			var (fileIndexResultsList, changedFileIndexItemName) =
+				await _metaPreflight.PreflightAsync(inputModel,
+					inputFilePaths.ToList(), append, collections, rotateClock);
 
-			var operationId = HttpContext.GetOperationId();
-			
 			// Update >
 			await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 			{
-				var operationHolder = RequestTelemetryHelper.GetOperationHolder(_scopeFactory,
-					nameof(UpdateAsync), operationId);
-				
 				var metaUpdateService = _scopeFactory.CreateScope()
 					.ServiceProvider.GetRequiredService();
-				
-				var data = await metaUpdateService.UpdateAsync(
+
+				await metaUpdateService.UpdateAsync(
 					changedFileIndexItemName, fileIndexResultsList, null,
 					collections, append, rotateClock);
-				operationHolder.SetData(_scopeFactory, data);
-			}, string.Empty);
+			}, "MetaUpdate", Activity.Current?.Id);
 
 			// before sending not founds
-			new StopWatchLogger(_logger).StopUpdateReplaceStopWatch("update", f,collections, stopwatch);
+			new StopWatchLogger(_logger).StopUpdateReplaceStopWatch("update", f, collections,
+				stopwatch);
 
 			// When all items are not found
 			if ( fileIndexResultsList.TrueForAll(p =>
@@ -108,12 +104,13 @@ await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 
 			// Clone an new item in the list to display
 			var returnNewResultList = fileIndexResultsList.Select(item => item.Clone()).ToList();
-            
+
 			// when switching very fast between images the background task has not run yet
 			_metaUpdateService.UpdateReadMetaCache(returnNewResultList);
-			
+
 			// Push direct to socket when update or replace to avoid undo after a second
-			_logger.LogInformation($"[UpdateController] send to socket {inputFilePaths.FirstOrDefault()}");
+			_logger.LogInformation(
+				$"[UpdateController] send to socket {inputFilePaths.FirstOrDefault()}");
 
 			await Task.Run(async () => await UpdateWebSocketTaskRun(fileIndexResultsList));
 
@@ -123,10 +120,12 @@ await _bgTaskQueue.QueueBackgroundWorkItemAsync(async _ =>
 		private async Task UpdateWebSocketTaskRun(List fileIndexResultsList)
 		{
 			var webSocketResponse =
-				new ApiNotificationResponseModel>(fileIndexResultsList, ApiNotificationType.MetaUpdate);
+				new ApiNotificationResponseModel>(fileIndexResultsList,
+					ApiNotificationType.MetaUpdate);
 			var realtimeConnectionsService = _scopeFactory.CreateScope()
 				.ServiceProvider.GetRequiredService();
-			await realtimeConnectionsService.NotificationToAllAsync(webSocketResponse, CancellationToken.None);
+			await realtimeConnectionsService.NotificationToAllAsync(webSocketResponse,
+				CancellationToken.None);
 		}
 	}
 }
diff --git a/starsky/starsky/Controllers/NotificationController.cs b/starsky/starsky/Controllers/NotificationController.cs
index ede97a118a..64337aac03 100644
--- a/starsky/starsky/Controllers/NotificationController.cs
+++ b/starsky/starsky/Controllers/NotificationController.cs
@@ -16,7 +16,7 @@ public NotificationController(INotificationQuery notificationQuery)
 		{
 			_notificationQuery = notificationQuery;
 		}
-		
+
 		/// 
 		/// Get recent notifications
 		/// Use dateTime 2022-04-16T17:33:10.323974Z to get the latest notifications
@@ -31,8 +31,8 @@ public NotificationController(INotificationQuery notificationQuery)
 		public async Task GetNotifications(string dateTime)
 		{
 			var (parsed, parsedDateTime) = ParseDate(dateTime);
-			
-			if ( !parsed || (DateTime.UtcNow - parsedDateTime ).TotalDays >= 1 )
+
+			if ( !parsed || ( DateTime.UtcNow - parsedDateTime ).TotalDays >= 1 )
 			{
 				return BadRequest("Please enter a valid dateTime");
 			}
@@ -44,7 +44,7 @@ internal static Tuple ParseDate(string dateTime)
 			var isParsed = DateTime.TryParse(dateTime, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsedDateTime);
 			if ( isParsed )
 				return new Tuple(true, parsedDateTime.ToUniversalTime());
-			
+
 			if ( !int.TryParse(dateTime, out var parsedInt) )
 				return new Tuple(false, DateTime.UtcNow);
 
diff --git a/starsky/starsky/Controllers/PublishController.cs b/starsky/starsky/Controllers/PublishController.cs
index bafe38f69c..1d5cb7453e 100644
--- a/starsky/starsky/Controllers/PublishController.cs
+++ b/starsky/starsky/Controllers/PublishController.cs
@@ -85,8 +85,8 @@ public async Task PublishCreateAsync(string f, string itemName,
 			var inputFilePaths = PathHelper.SplitInputFilePaths(f).ToList();
 			var info = await _metaInfo.GetInfoAsync(inputFilePaths, false);
 			if ( info.TrueForAll(p =>
-				    p.Status != FileIndexItem.ExifStatus.Ok &&
-				    p.Status != FileIndexItem.ExifStatus.ReadOnly) )
+					p.Status != FileIndexItem.ExifStatus.Ok &&
+					p.Status != FileIndexItem.ExifStatus.ReadOnly) )
 			{
 				return NotFound(info);
 			}
diff --git a/starsky/starsky/Controllers/RedirectController.cs b/starsky/starsky/Controllers/RedirectController.cs
index 596fe1d8f6..e47de7b215 100644
--- a/starsky/starsky/Controllers/RedirectController.cs
+++ b/starsky/starsky/Controllers/RedirectController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using starsky.foundation.platform.Models;
@@ -18,7 +18,7 @@ public RedirectController(ISelectorStorage selectorStorage, AppSettings appSetti
 			var storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
 			_structureService = new StructureService(storage, appSettings.Structure);
 		}
-			    
+
 		/// 
 		/// Redirect or view path to relative paths using the structure-config (see /api/env)
 		/// 
@@ -33,13 +33,13 @@ public RedirectController(ISelectorStorage selectorStorage, AppSettings appSetti
 		[Produces("application/json")]
 		public IActionResult SubPathRelative(int value, bool json = true)
 		{
-			if(value >= 1) value = value * -1; // always in the past
-			// Fallback for dates older than 24-11-1854 to avoid a exception.
+			if ( value >= 1 ) value = value * -1; // always in the past
+												  // Fallback for dates older than 24-11-1854 to avoid a exception.
 			if ( value < -60000 ) value = 0;
-			
+
 			// expect something like this: /2018/09/2018_09_02/
 			var subPath = _structureService.ParseSubfolders(DateTime.Today.AddDays(value));
-			if(json) return Json(subPath);
+			if ( json ) return Json(subPath);
 			return RedirectToAction("Index", "Home", new { f = subPath });
 		}
 
diff --git a/starsky/starsky/Controllers/SearchController.cs b/starsky/starsky/Controllers/SearchController.cs
index 256257f261..13390a2c69 100644
--- a/starsky/starsky/Controllers/SearchController.cs
+++ b/starsky/starsky/Controllers/SearchController.cs
@@ -26,14 +26,14 @@ public SearchController(ISearch search)
 		/// the search results
 		/// the search results (ActionResult Search)
 		[HttpGet("/api/search")]
-		[ProducesResponseType(typeof(SearchViewModel),200)] // ok
+		[ProducesResponseType(typeof(SearchViewModel), 200)] // ok
 		[Produces("application/json")]
 		public async Task Index(string t, int p = 0)
 		{
 			var model = await _search.Search(t, p);
 			return Json(model);
 		}
-        
+
 		/// 
 		/// Get relative paths in a search query
 		/// Does not cover multiple pages (so it ends within the page)
@@ -44,36 +44,36 @@ public async Task Index(string t, int p = 0)
 		/// Relative object (only this)
 		/// the search results
 		[HttpGet("/api/search/relative-objects")]
-		[ProducesResponseType(typeof(SearchViewModel),200)] // ok
+		[ProducesResponseType(typeof(SearchViewModel), 200)] // ok
 		[Produces("application/json")]
 		public async Task SearchRelative(string f, string t, int p = 0)
 		{
 			// Json api && View()            
-			var searchViewModel =  await _search.Search(t, p);
+			var searchViewModel = await _search.Search(t, p);
 
-			var photoIndexOfQuery = GetIndexFilePathFromSearch(searchViewModel,f);
+			var photoIndexOfQuery = GetIndexFilePathFromSearch(searchViewModel, f);
 			if ( photoIndexOfQuery == -1 ) return NotFound("image not found in search result");
-	        
+
 			var args = new Dictionary
 			{
 				{ "p", p.ToString() },
 				{ "t", t }
 			};
-	        
-			var relativeObject = new RelativeObjects{Args = args};
 
-			if (photoIndexOfQuery != searchViewModel.FileIndexItems?.Count - 1 )
+			var relativeObject = new RelativeObjects { Args = args };
+
+			if ( photoIndexOfQuery != searchViewModel.FileIndexItems?.Count - 1 )
 			{
 				relativeObject.NextFilePath = searchViewModel.FileIndexItems?[photoIndexOfQuery + 1].FilePath!;
 				relativeObject.NextHash = searchViewModel.FileIndexItems?[photoIndexOfQuery + 1].FileHash!;
 			}
 
-			if (photoIndexOfQuery >= 1)
+			if ( photoIndexOfQuery >= 1 )
 			{
 				relativeObject.PrevFilePath = searchViewModel.FileIndexItems?[photoIndexOfQuery - 1].FilePath!;
 				relativeObject.PrevHash = searchViewModel.FileIndexItems?[photoIndexOfQuery - 1].FileHash!;
 			}
-	        
+
 			return Json(relativeObject);
 		}
 
@@ -87,10 +87,10 @@ internal static int GetIndexFilePathFromSearch(SearchViewModel searchViewModel,
 		{
 			var result = searchViewModel.FileIndexItems?.Find(p => p.FilePath == f);
 			var photoIndexOfQuery = searchViewModel.FileIndexItems?.IndexOf(result!);
-			if ( result == null || photoIndexOfQuery == null) return -1;
+			if ( result == null || photoIndexOfQuery == null ) return -1;
 			return photoIndexOfQuery.Value;
 		}
-        
+
 		/// 
 		/// List of files with the tag: !delete! (TrashKeyword.TrashKeywordString)
 		/// Caching is disabled on this api call
@@ -99,7 +99,7 @@ internal static int GetIndexFilePathFromSearch(SearchViewModel searchViewModel,
 		/// the delete files results
 		/// the search results
 		[HttpGet("/api/search/trash")]
-		[ProducesResponseType(typeof(SearchViewModel),200)] // ok
+		[ProducesResponseType(typeof(SearchViewModel), 200)] // ok
 		[Produces("application/json")]
 		public async Task Trash(int p = 0)
 		{
@@ -116,9 +116,9 @@ public async Task Trash(int p = 0)
 		/// Cache is disabled in config
 		/// User unauthorized
 		[HttpPost("/api/search/remove-cache")]
-		[Produces("application/json")]	    
-		[ProducesResponseType(typeof(string),200)]
-		[ProducesResponseType(typeof(string),412)]
+		[Produces("application/json")]
+		[ProducesResponseType(typeof(string), 200)]
+		[ProducesResponseType(typeof(string), 412)]
 		[ProducesResponseType(401)]
 		public IActionResult RemoveCache(string t = "")
 		{
@@ -126,7 +126,7 @@ public IActionResult RemoveCache(string t = "")
 
 			if ( cache != null )
 				return Json(cache == false ? "there is no cached item" : "cache cleared");
-		    
+
 			Response.StatusCode = 412;
 			return Json("cache disabled in config");
 		}
diff --git a/starsky/starsky/Controllers/SearchSuggestController.cs b/starsky/starsky/Controllers/SearchSuggestController.cs
index 30d70d2fde..2cbc134d82 100644
--- a/starsky/starsky/Controllers/SearchSuggestController.cs
+++ b/starsky/starsky/Controllers/SearchSuggestController.cs
@@ -11,7 +11,7 @@ public sealed class SearchSuggestController : Controller
 	{
 		private readonly ISearchSuggest _suggest;
 
-		public SearchSuggestController(ISearchSuggest suggest) 
+		public SearchSuggestController(ISearchSuggest suggest)
 		{
 			_suggest = suggest;
 		}
@@ -23,9 +23,9 @@ public SearchSuggestController(ISearchSuggest suggest)
 		/// the search results
 		/// the search results
 		[HttpGet("/api/suggest")]
-		[ProducesResponseType(typeof(SearchViewModel),200)] // ok
-		[Produces("application/json")]	    
-		[Authorize] 
+		[ProducesResponseType(typeof(SearchViewModel), 200)] // ok
+		[Produces("application/json")]
+		[Authorize]
 		// ^ ^ ^ ^ = = = = = = = = = = = = = = = = = =
 		public async Task Suggest(string t)
 		{
@@ -43,9 +43,9 @@ public async Task Suggest(string t)
 		/// a keyList with search suggestions
 		/// the search results
 		[HttpGet("/api/suggest/all")]
-		[ProducesResponseType(typeof(SearchViewModel),200)] // ok
-		[Produces("application/json")]	    
-		[Authorize] 
+		[ProducesResponseType(typeof(SearchViewModel), 200)] // ok
+		[Produces("application/json")]
+		[Authorize]
 		// ^ ^ ^ ^ = = = = = = = = = = = = = = = = = =
 		public async Task All()
 		{
diff --git a/starsky/starsky/Controllers/SynchronizeController.cs b/starsky/starsky/Controllers/SynchronizeController.cs
index c3aa2f0536..a20728c56e 100644
--- a/starsky/starsky/Controllers/SynchronizeController.cs
+++ b/starsky/starsky/Controllers/SynchronizeController.cs
@@ -3,7 +3,6 @@
 using Microsoft.AspNetCore.Mvc;
 using starsky.foundation.database.Models;
 using starsky.foundation.sync.SyncInterfaces;
-using starsky.foundation.webtelemetry.Helpers;
 
 namespace starsky.Controllers
 {
@@ -26,13 +25,12 @@ public SynchronizeController(IManualBackgroundSyncService manualBackgroundSyncSe
 		/// User unauthorized
 		[HttpPost("/api/synchronize")]
 		[HttpGet("/api/synchronize")] // < = = = = = = = = subject to change!
-		[ProducesResponseType(typeof(string),200)]
-		[ProducesResponseType(typeof(string),401)]
-		[Produces("application/json")]	   
+		[ProducesResponseType(typeof(string), 200)]
+		[ProducesResponseType(typeof(string), 401)]
+		[Produces("application/json")]
 		public async Task Index(string f)
 		{
-			var operationId = HttpContext.GetOperationId();
-			var status = await _manualBackgroundSyncService.ManualSync(f,operationId);
+			var status = await _manualBackgroundSyncService.ManualSync(f);
 			switch ( status )
 			{
 				case FileIndexItem.ExifStatus.NotFoundNotInIndex:
diff --git a/starsky/starsky/Controllers/ThumbnailController.cs b/starsky/starsky/Controllers/ThumbnailController.cs
index 1bd82d61a4..45807b871d 100644
--- a/starsky/starsky/Controllers/ThumbnailController.cs
+++ b/starsky/starsky/Controllers/ThumbnailController.cs
@@ -23,7 +23,7 @@ public sealed class ThumbnailController : Controller
 		private readonly IQuery _query;
 		private readonly IStorage _iStorage;
 		private readonly IStorage _thumbnailStorage;
-		
+
 		public ThumbnailController(IQuery query, ISelectorStorage selectorStorage)
 		{
 			_query = query;
@@ -49,36 +49,36 @@ public ThumbnailController(IQuery query, ISelectorStorage selectorStorage)
 		public IActionResult ThumbnailSmallOrTinyMeta(string f)
 		{
 			f = FilenamesHelper.GetFileNameWithoutExtension(f);
-			
+
 			// Restrict the fileHash to letters and digits only
 			// I/O function calls should not be vulnerable to path injection attacks
-			if (!Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", 
+			if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
 			{
 				return BadRequest();
 			}
-			
-			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.Small)) )
+
+			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)) )
 			{
-				var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f,ThumbnailSize.Small) );
+				var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small));
 				Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.Small.ToString()));
 				return File(stream, "image/jpeg");
 			}
 
-			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.TinyMeta) )  )
+			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)) )
 			{
-				var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f,ThumbnailSize.TinyMeta));
+				var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta));
 				Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.TinyMeta.ToString()));
 				return File(stream, "image/jpeg");
 			}
 
-			if ( !_thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.Large)) )
+			if ( !_thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)) )
 			{
 				SetExpiresResponseHeadersToZero();
 				return NotFound("hash not found");
 			}
 
-			var streamDefaultThumbnail = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f,ThumbnailSize.Large));
+			var streamDefaultThumbnail = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large));
 			Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.Large.ToString()));
 			return File(streamDefaultThumbnail, "image/jpeg");
 		}
@@ -106,30 +106,31 @@ public async Task ListSizesByHash(string f)
 		{
 			// For serving jpeg files
 			f = FilenamesHelper.GetFileNameWithoutExtension(f);
-	        
+
 			// Restrict the fileHash to letters and digits only
 			// I/O function calls should not be vulnerable to path injection attacks
-			if (!Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", 
+			if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
 				RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
 			{
 				return BadRequest();
 			}
-			
-			var data = new ThumbnailSizesExistStatusModel{ 
-				TinyMeta = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.TinyMeta)),
-				Small = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.Small)),
-				Large = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.Large)),
-				ExtraLarge = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f,ThumbnailSize.ExtraLarge))
+
+			var data = new ThumbnailSizesExistStatusModel
+			{
+				TinyMeta = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)),
+				Small = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)),
+				Large = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)),
+				ExtraLarge = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.ExtraLarge))
 			};
 
 			// Success has all items (except tinyMeta)
-			if (data.Small && data.Large && data.ExtraLarge )
+			if ( data.Small && data.Large && data.ExtraLarge )
 				return Json(data);
-			
+
 			var sourcePath = await _query.GetSubPathByHashAsync(f);
 			var isThumbnailSupported =
 				ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath);
-			switch ( isThumbnailSupported  )
+			switch ( isThumbnailSupported )
 			{
 				case true when !string.IsNullOrEmpty(sourcePath):
 					Response.StatusCode = 202;
@@ -146,21 +147,21 @@ public async Task ListSizesByHash(string f)
 		private IActionResult ReturnThumbnailResult(string f, bool json, ThumbnailSize size)
 		{
 			Response.Headers.Append("x-image-size", new StringValues(size.ToString()));
-			var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, size),50);
+			var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, size), 50);
 			var imageFormat = ExtensionRolesHelper.GetImageFormat(stream);
 			if ( imageFormat == ExtensionRolesHelper.ImageFormat.unknown )
 			{
 				SetExpiresResponseHeadersToZero();
 				return NoContent(); // 204
 			}
-			
+
 			// When using the api to check using javascript
 			// use the cached version of imageFormat, otherwise you have to check if it deleted
-			if (json) return Json("OK");
+			if ( json ) return Json("OK");
 
 			stream = _thumbnailStorage.ReadStream(
 					ThumbnailNameHelper.Combine(f, size));
-			
+
 			// thumbs are always in jpeg
 			Response.Headers.Append("x-filename", new StringValues(FilenamesHelper.GetFileName(f + ".jpg")));
 			return File(stream, "image/jpeg");
@@ -185,176 +186,176 @@ private IActionResult ReturnThumbnailResult(string f, bool json, ThumbnailSize s
 		/// item not found on disk
 		/// User unauthorized
 		[HttpGet("/api/thumbnail/{f}")]
-        [ProducesResponseType(200)] // file
-        [ProducesResponseType(202)] // thumbnail can be generated "Thumbnail is not ready yet"
-        [ProducesResponseType(204)] // thumbnail is corrupt
-        [ProducesResponseType(210)] // raw
-        [ProducesResponseType(400)] // string (f) input not allowed to avoid path injection attacks
-        [ProducesResponseType(404)] // not found
-        [AllowAnonymous] // <=== ALLOW FROM EVERYWHERE
-        [ResponseCache(Duration = 29030400)] // 4 weeks
-        public async Task Thumbnail(
-            string f, 
-            string? filePath = null,
-            bool isSingleItem = false, 
-            bool json = false,
-            bool extraLarge = true)
-        {
-            // f is Hash
-            // isSingleItem => detailView
-            // Retry thumbnail => is when you press reset thumbnail
-            // json, => to don't waste the users bandwidth.
-
-	        // For serving jpeg files
-	        f = FilenamesHelper.GetFileNameWithoutExtension(f);
-	        
-	        // Get the text before at (@) so replace @2000 with nothing to match  fileHash
-	        var beforeAt = Regex.Match(f, ".*(?=@)", RegexOptions.None, 
-		        TimeSpan.FromSeconds(1)).Value;
-	        if ( !string.IsNullOrEmpty(beforeAt) ) f = beforeAt;
-	        
-	        // Restrict the fileHash to letters and digits only
-	        // I/O function calls should not be vulnerable to path injection attacks
-	        if (!Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", 
-		        RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
-	        {
-		        return BadRequest();
-	        }
-
-	        var preferredSize = ThumbnailSize.ExtraLarge;
-	        var altSize = ThumbnailSize.Large;
-	        if ( !extraLarge )
-	        {
-		        preferredSize = ThumbnailSize.Large;
-		        altSize = ThumbnailSize.ExtraLarge;
-	        }
-	        
-            if (_thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, preferredSize)))
-            {
-                return ReturnThumbnailResult(f, json, preferredSize);
-            }
-
-            if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, altSize)) )
-            {
-	            return ReturnThumbnailResult(f, json, altSize);
-            }
-
-            // Cached view of item
-            // Need to check again for recently moved files
-            var sourcePath = await _query.GetSubPathByHashAsync(f);
-            if ( sourcePath == null )
-            {
-	            // remove from cache
-	            _query.ResetItemByHash(f);
-	            
-	            if (string.IsNullOrEmpty(filePath) || await _query.GetObjectByFilePathAsync(filePath) == null )
-	            {
-		            SetExpiresResponseHeadersToZero();
-		            return NotFound("not in index");
-	            }
-	            
-	            sourcePath = filePath;
-            }
-
-            if ( !_iStorage.ExistFile(sourcePath) )
-            {
-	            return NotFound("There is no thumbnail image " + f + " and no source image " +
-	                            sourcePath);
-            }
-	        
-	        if (!isSingleItem)
-	        {
-		        // "Photo exist in database but " + "isSingleItem flag is Missing"
-		        SetExpiresResponseHeadersToZero();
-		        Response.StatusCode = 202; // A conflict, that the thumb is not generated yet
-		        return Json("Thumbnail is not ready yet");
-	        }
-                
-	        if (ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath))
-	        {
-		        var fs1 = _iStorage.ReadStream(sourcePath);
-
-		        var fileExt = FilenamesHelper.GetFileExtensionWithoutDot(sourcePath);
-		        var fileName = HttpUtility.UrlEncode(FilenamesHelper.GetFileName(sourcePath));
-		        Response.Headers.TryAdd("x-filename", new StringValues(fileName));
-		        return File(fs1, MimeHelper.GetMimeType(fileExt));
-	        }
-	        
-	        Response.StatusCode = 210; // A conflict, that the thumb is not generated yet
-	        return Json("Thumbnail is not supported; for example you try to view a raw file");
-        }
-
-        /// 
-        /// Get zoomed in image by fileHash.
-        /// At the moment this is the source image
-        /// 
-        /// one single fileHash (NOT path)
-        /// zoom factor? 
-        /// fallback filePath
-        /// Image
-        /// returns content of the file or when json is true, "OK"
-        /// string (f) input not allowed to avoid path injection attacks
-        /// item not found on disk
-        /// Conflict, you did try get for example a thumbnail of a raw file
-        /// User unauthorized
-        [HttpGet("/api/thumbnail/zoom/{f}@{z}")]
-        [ProducesResponseType(200)] // file
-        [ProducesResponseType(400)] // string (f) input not allowed to avoid path injection attacks
-        [ProducesResponseType(404)] // not found
-        [ProducesResponseType(210)] // raw
-        public async Task ByZoomFactorAsync(
-	        string f,
-	        int z = 0, 
-	        string filePath = "")
-        {
-	        // For serving jpeg files
-	        f = FilenamesHelper.GetFileNameWithoutExtension(f);
-	        
-	        // Restrict the fileHash to letters and digits only
-	        // I/O function calls should not be vulnerable to path injection attacks
-	        if (!Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", 
-		        RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
-	        {
-		        return BadRequest();
-	        }
-	        
-	        // Cached view of item
-	        var sourcePath = await _query.GetSubPathByHashAsync(f);
-	        if ( sourcePath == null )
-	        {
-		        if ( await _query.GetObjectByFilePathAsync(filePath) == null )
-		        {
-			        return NotFound("not in index");
-		        }
-		        sourcePath = filePath;
-	        }
-	        
-	        if (ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath))
-	        {
-		        var fs1 = _iStorage.ReadStream(sourcePath);
-
-		        var fileExt = FilenamesHelper.GetFileExtensionWithoutDot(sourcePath);
-		        Response.Headers.Append("x-filename", FilenamesHelper.GetFileName(sourcePath));
-		        return File(fs1, MimeHelper.GetMimeType(fileExt));
-	        }
-	        
-	        Response.StatusCode = 210; // A conflict, that the thumb is not generated yet
-	        return Json("Thumbnail is not supported; for example you try to view a raw file");
-        }
-
-        /// 
-        /// Force Http context to no browser cache
-        /// 
-        public void SetExpiresResponseHeadersToZero()
-        {
-	        Request.HttpContext.Response.Headers.Remove("Cache-Control");
-	        Request.HttpContext.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
-
-	        Request.HttpContext.Response.Headers.Remove("Pragma");
-	        Request.HttpContext.Response.Headers.Append("Pragma", "no-cache");
-
-	        Request.HttpContext.Response.Headers.Remove("Expires");
-	        Request.HttpContext.Response.Headers.Append("Expires", "0");
-        }
+		[ProducesResponseType(200)] // file
+		[ProducesResponseType(202)] // thumbnail can be generated "Thumbnail is not ready yet"
+		[ProducesResponseType(204)] // thumbnail is corrupt
+		[ProducesResponseType(210)] // raw
+		[ProducesResponseType(400)] // string (f) input not allowed to avoid path injection attacks
+		[ProducesResponseType(404)] // not found
+		[AllowAnonymous] // <=== ALLOW FROM EVERYWHERE
+		[ResponseCache(Duration = 29030400)] // 4 weeks
+		public async Task Thumbnail(
+			string f,
+			string? filePath = null,
+			bool isSingleItem = false,
+			bool json = false,
+			bool extraLarge = true)
+		{
+			// f is Hash
+			// isSingleItem => detailView
+			// Retry thumbnail => is when you press reset thumbnail
+			// json, => to don't waste the users bandwidth.
+
+			// For serving jpeg files
+			f = FilenamesHelper.GetFileNameWithoutExtension(f);
+
+			// Get the text before at (@) so replace @2000 with nothing to match  fileHash
+			var beforeAt = Regex.Match(f, ".*(?=@)", RegexOptions.None,
+				TimeSpan.FromSeconds(1)).Value;
+			if ( !string.IsNullOrEmpty(beforeAt) ) f = beforeAt;
+
+			// Restrict the fileHash to letters and digits only
+			// I/O function calls should not be vulnerable to path injection attacks
+			if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
+				RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+			{
+				return BadRequest();
+			}
+
+			var preferredSize = ThumbnailSize.ExtraLarge;
+			var altSize = ThumbnailSize.Large;
+			if ( !extraLarge )
+			{
+				preferredSize = ThumbnailSize.Large;
+				altSize = ThumbnailSize.ExtraLarge;
+			}
+
+			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, preferredSize)) )
+			{
+				return ReturnThumbnailResult(f, json, preferredSize);
+			}
+
+			if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, altSize)) )
+			{
+				return ReturnThumbnailResult(f, json, altSize);
+			}
+
+			// Cached view of item
+			// Need to check again for recently moved files
+			var sourcePath = await _query.GetSubPathByHashAsync(f);
+			if ( sourcePath == null )
+			{
+				// remove from cache
+				_query.ResetItemByHash(f);
+
+				if ( string.IsNullOrEmpty(filePath) || await _query.GetObjectByFilePathAsync(filePath) == null )
+				{
+					SetExpiresResponseHeadersToZero();
+					return NotFound("not in index");
+				}
+
+				sourcePath = filePath;
+			}
+
+			if ( !_iStorage.ExistFile(sourcePath) )
+			{
+				return NotFound("There is no thumbnail image " + f + " and no source image " +
+								sourcePath);
+			}
+
+			if ( !isSingleItem )
+			{
+				// "Photo exist in database but " + "isSingleItem flag is Missing"
+				SetExpiresResponseHeadersToZero();
+				Response.StatusCode = 202; // A conflict, that the thumb is not generated yet
+				return Json("Thumbnail is not ready yet");
+			}
+
+			if ( ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath) )
+			{
+				var fs1 = _iStorage.ReadStream(sourcePath);
+
+				var fileExt = FilenamesHelper.GetFileExtensionWithoutDot(sourcePath);
+				var fileName = HttpUtility.UrlEncode(FilenamesHelper.GetFileName(sourcePath));
+				Response.Headers.TryAdd("x-filename", new StringValues(fileName));
+				return File(fs1, MimeHelper.GetMimeType(fileExt));
+			}
+
+			Response.StatusCode = 210; // A conflict, that the thumb is not generated yet
+			return Json("Thumbnail is not supported; for example you try to view a raw file");
+		}
+
+		/// 
+		/// Get zoomed in image by fileHash.
+		/// At the moment this is the source image
+		/// 
+		/// one single fileHash (NOT path)
+		/// zoom factor? 
+		/// fallback filePath
+		/// Image
+		/// returns content of the file or when json is true, "OK"
+		/// string (f) input not allowed to avoid path injection attacks
+		/// item not found on disk
+		/// Conflict, you did try get for example a thumbnail of a raw file
+		/// User unauthorized
+		[HttpGet("/api/thumbnail/zoom/{f}@{z}")]
+		[ProducesResponseType(200)] // file
+		[ProducesResponseType(400)] // string (f) input not allowed to avoid path injection attacks
+		[ProducesResponseType(404)] // not found
+		[ProducesResponseType(210)] // raw
+		public async Task ByZoomFactorAsync(
+			string f,
+			int z = 0,
+			string filePath = "")
+		{
+			// For serving jpeg files
+			f = FilenamesHelper.GetFileNameWithoutExtension(f);
+
+			// Restrict the fileHash to letters and digits only
+			// I/O function calls should not be vulnerable to path injection attacks
+			if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$",
+				RegexOptions.None, TimeSpan.FromMilliseconds(100)) )
+			{
+				return BadRequest();
+			}
+
+			// Cached view of item
+			var sourcePath = await _query.GetSubPathByHashAsync(f);
+			if ( sourcePath == null )
+			{
+				if ( await _query.GetObjectByFilePathAsync(filePath) == null )
+				{
+					return NotFound("not in index");
+				}
+				sourcePath = filePath;
+			}
+
+			if ( ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath) )
+			{
+				var fs1 = _iStorage.ReadStream(sourcePath);
+
+				var fileExt = FilenamesHelper.GetFileExtensionWithoutDot(sourcePath);
+				Response.Headers.Append("x-filename", FilenamesHelper.GetFileName(sourcePath));
+				return File(fs1, MimeHelper.GetMimeType(fileExt));
+			}
+
+			Response.StatusCode = 210; // A conflict, that the thumb is not generated yet
+			return Json("Thumbnail is not supported; for example you try to view a raw file");
+		}
+
+		/// 
+		/// Force Http context to no browser cache
+		/// 
+		public void SetExpiresResponseHeadersToZero()
+		{
+			Request.HttpContext.Response.Headers.Remove("Cache-Control");
+			Request.HttpContext.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
+
+			Request.HttpContext.Response.Headers.Remove("Pragma");
+			Request.HttpContext.Response.Headers.Append("Pragma", "no-cache");
+
+			Request.HttpContext.Response.Headers.Remove("Expires");
+			Request.HttpContext.Response.Headers.Append("Expires", "0");
+		}
 	}
 }
diff --git a/starsky/starsky/Controllers/ThumbnailGenerationController.cs b/starsky/starsky/Controllers/ThumbnailGenerationController.cs
index 28f70fa866..2833bf96cc 100644
--- a/starsky/starsky/Controllers/ThumbnailGenerationController.cs
+++ b/starsky/starsky/Controllers/ThumbnailGenerationController.cs
@@ -20,7 +20,7 @@ public ThumbnailGenerationController(ISelectorStorage selectorStorage,
 			_selectorStorage = selectorStorage;
 			_thumbnailGenerationService = thumbnailGenerationService;
 		}
-		
+
 		/// 
 		/// Create thumbnails for a folder in the background
 		/// 
@@ -34,14 +34,14 @@ public async Task ThumbnailGeneration(string f)
 			var subPath = f != "/" ? PathHelper.RemoveLatestSlash(f) : "/";
 			var subPathStorage = _selectorStorage.Get(SelectorStorage.StorageServices.SubPath);
 
-			if ( !subPathStorage.ExistFolder(subPath))
+			if ( !subPathStorage.ExistFolder(subPath) )
 			{
 				return NotFound("folder not found");
 			}
 
 			// When the CPU is to high its gives a Error 500
 			await _thumbnailGenerationService.ManualBackgroundQueue(subPath);
-			
+
 			return Json("Job started");
 		}
 	}
diff --git a/starsky/starsky/Controllers/TrashController.cs b/starsky/starsky/Controllers/TrashController.cs
index e72a2639cd..555674fc3d 100644
--- a/starsky/starsky/Controllers/TrashController.cs
+++ b/starsky/starsky/Controllers/TrashController.cs
@@ -32,7 +32,7 @@ public IActionResult DetectToUseSystemTrash()
 	{
 		return Json(_moveToTrashService.DetectToUseSystemTrash());
 	}
-	
+
 	/// 
 	/// (beta) Move a file to the trash
 	/// 
@@ -57,7 +57,7 @@ public async Task TrashMoveAsync(string f, bool collections = fal
 		}
 
 		var fileIndexResultsList = await _moveToTrashService.MoveToTrashAsync(inputFilePaths.ToList(), collections);
-		
+
 		return Json(fileIndexResultsList);
 	}
 }
diff --git a/starsky/starsky/Controllers/UploadController.cs b/starsky/starsky/Controllers/UploadController.cs
index 14d440c066..ac09374013 100644
--- a/starsky/starsky/Controllers/UploadController.cs
+++ b/starsky/starsky/Controllers/UploadController.cs
@@ -1,4 +1,3 @@
-#nullable enable
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
@@ -30,12 +29,12 @@ namespace starsky.Controllers
 {
 	[Authorize] // <- should be logged in!
 	[SuppressMessage("Usage", "S5693:Make sure the content " +
-		"length limit is safe here", Justification = "Is checked")]
+							  "length limit is safe here", Justification = "Is checked")]
 	public sealed class UploadController : Controller
 	{
 		private readonly AppSettings _appSettings;
 		private readonly IImport _import;
-		private readonly IStorage _iStorage; 
+		private readonly IStorage _iStorage;
 		private readonly IStorage _iHostStorage;
 		private readonly IQuery _query;
 		private readonly ISelectorStorage _selectorStorage;
@@ -44,10 +43,11 @@ public sealed class UploadController : Controller
 		private readonly IMetaExifThumbnailService _metaExifThumbnailService;
 		private readonly IMetaUpdateStatusThumbnailService _metaUpdateStatusThumbnailService;
 
-		[SuppressMessage("Usage", "S107: Constructor has 8 parameters, which is greater than the 7 authorized")]
-		public UploadController(IImport import, AppSettings appSettings, 
-			ISelectorStorage selectorStorage, IQuery query, 
-			IRealtimeConnectionsService realtimeService, IWebLogger logger, 
+		[SuppressMessage("Usage",
+			"S107: Constructor has 8 parameters, which is greater than the 7 authorized")]
+		public UploadController(IImport import, AppSettings appSettings,
+			ISelectorStorage selectorStorage, IQuery query,
+			IRealtimeConnectionsService realtimeService, IWebLogger logger,
 			IMetaExifThumbnailService metaExifThumbnailService,
 			IMetaUpdateStatusThumbnailService metaUpdateStatusThumbnailService)
 		{
@@ -76,15 +76,16 @@ public UploadController(IImport import, AppSettings appSettings,
 		/// missing 'to' header
 		/// the ImportIndexItem of the imported files 
 		[HttpPost("/api/upload")]
-        [DisableFormValueModelBinding]
+		[DisableFormValueModelBinding]
 		[RequestFormLimits(MultipartBodyLengthLimit = 320_000_000)]
 		[RequestSizeLimit(320_000_000)] // in bytes, 305MB
-		[ProducesResponseType(typeof(List),200)] // yes
-		[ProducesResponseType(typeof(string),400)]
-		[ProducesResponseType(typeof(List),404)]
-		[ProducesResponseType(typeof(List),415)]  // Wrong input (e.g. wrong extenstion type)
-		[Produces("application/json")]	    
-        public async Task UploadToFolder()
+		[ProducesResponseType(typeof(List), 200)] // yes
+		[ProducesResponseType(typeof(string), 400)]
+		[ProducesResponseType(typeof(List), 404)]
+		[ProducesResponseType(typeof(List),
+			415)] // Wrong input (e.g. wrong extenstion type)
+		[Produces("application/json")]
+		public async Task UploadToFolder()
 		{
 			var to = Request.Headers["to"].ToString();
 			if ( string.IsNullOrWhiteSpace(to) ) return BadRequest("missing 'to' header");
@@ -92,13 +93,16 @@ public async Task UploadToFolder()
 			var parentDirectory = GetParentDirectoryFromRequestHeader();
 			if ( parentDirectory == null )
 			{
-				return NotFound(new ImportIndexItem{Status = ImportStatus.ParentDirectoryNotFound});
+				return NotFound(new ImportIndexItem
+				{
+					Status = ImportStatus.ParentDirectoryNotFound
+				});
 			}
-			
-			var tempImportPaths = await Request.StreamFile(_appSettings,_selectorStorage);
-			
-			var fileIndexResultsList = await _import.Preflight(tempImportPaths, 
-				new ImportSettingsModel{IndexMode = false});
+
+			var tempImportPaths = await Request.StreamFile(_appSettings, _selectorStorage);
+
+			var fileIndexResultsList = await _import.Preflight(tempImportPaths,
+				new ImportSettingsModel { IndexMode = false });
 			// fail/pass, right type, string=subPath, string?2= error reason
 			var metaResultsList = new List<(bool, bool, string, string?)>();
 
@@ -108,9 +112,9 @@ public async Task UploadToFolder()
 				{
 					continue;
 				}
-			
+
 				var tempFileStream = _iHostStorage.ReadStream(tempImportPaths[i]);
-				
+
 				var fileName = Path.GetFileName(tempImportPaths[i]);
 
 				// subPath is always unix style
@@ -119,57 +123,60 @@ public async Task UploadToFolder()
 
 				// to get the output in the result right
 				fileIndexResultsList[i].FileIndexItem!.FileName = fileName;
-				fileIndexResultsList[i].FileIndexItem!.ParentDirectory =  parentDirectory;
+				fileIndexResultsList[i].FileIndexItem!.ParentDirectory = parentDirectory;
 				fileIndexResultsList[i].FilePath = subPath;
 				// Do sync action before writing it down
-				fileIndexResultsList[i].FileIndexItem = await SyncItem(fileIndexResultsList[i].FileIndexItem!);
-				
+				fileIndexResultsList[i].FileIndexItem =
+					await SyncItem(fileIndexResultsList[i].FileIndexItem!);
+
 				var writeStatus =
 					await _iStorage.WriteStreamAsync(tempFileStream, subPath + ".tmp");
 				await tempFileStream.DisposeAsync();
-				
+
 				// to avoid partly written stream to be read by an other application
 				_iStorage.FileDelete(subPath);
 				_iStorage.FileMove(subPath + ".tmp", subPath);
 				_logger.LogInformation($"[UploadController] write {subPath} is {writeStatus}");
-				
+
 				// clear directory cache
 				_query.RemoveCacheParentItem(subPath);
 
 				var deleteStatus = _iHostStorage.FileDelete(tempImportPaths[i]);
-				_logger.LogInformation($"[UploadController] delete {tempImportPaths[i]} is {deleteStatus}");
-				
+				_logger.LogInformation(
+					$"[UploadController] delete {tempImportPaths[i]} is {deleteStatus}");
+
 				var parentPath = Directory.GetParent(tempImportPaths[i])?.FullName;
 				if ( !string.IsNullOrEmpty(parentPath) && parentPath != _appSettings.TempFolder )
 				{
 					_iHostStorage.FolderDelete(parentPath);
 				}
 
-				metaResultsList.Add((await _metaExifThumbnailService.AddMetaThumbnail(subPath,
-					fileIndexResultsList[i].FileIndexItem!.FileHash!)));
+				metaResultsList.Add(( await _metaExifThumbnailService.AddMetaThumbnail(subPath,
+					fileIndexResultsList[i].FileIndexItem!.FileHash!) ));
 			}
 
 			// send all uploads as list
 			var socketResult = fileIndexResultsList
 				.Where(p => p.Status == ImportStatus.Ok)
 				.Select(item => item.FileIndexItem).Cast().ToList();
-			
+
 			var webSocketResponse = new ApiNotificationResponseModel>(
-				socketResult,ApiNotificationType.UploadFile);
-			await _realtimeService.NotificationToAllAsync(webSocketResponse, CancellationToken.None);
-			
+				socketResult, ApiNotificationType.UploadFile);
+			await _realtimeService.NotificationToAllAsync(webSocketResponse,
+				CancellationToken.None);
+
 			await _metaUpdateStatusThumbnailService.UpdateStatusThumbnail(metaResultsList);
-			
+
 			// Wrong input (extension is not allowed)
-            if ( fileIndexResultsList.TrueForAll(p => p.Status == ImportStatus.FileError) )
-            {
-	            _logger.LogInformation($"Wrong input extension is not allowed" +
-					$" {string.Join(",",fileIndexResultsList.Select(p => p.FilePath))}");
-	            Response.StatusCode = 415;
-            }
-            
-	        return Json(fileIndexResultsList);
-        }
+			if ( fileIndexResultsList.TrueForAll(p => p.Status == ImportStatus.FileError) )
+			{
+				_logger.LogInformation($"Wrong input extension is not allowed" +
+									   $" {string.Join(",", fileIndexResultsList.Select(p => p.FilePath))}");
+				Response.StatusCode = 415;
+			}
+
+			return Json(fileIndexResultsList);
+		}
 
 		/// 
 		/// Perform database updates
@@ -185,7 +192,7 @@ private async Task SyncItem(FileIndexItem metaDataItem)
 				await _query.AddItemAsync(metaDataItem);
 				return metaDataItem;
 			}
-			
+
 			FileIndexCompareHelper.Compare(itemFromDatabase, metaDataItem);
 			AddOrRemoveXmpSidecarFileToDatabase(metaDataItem);
 
@@ -196,14 +203,15 @@ private async Task SyncItem(FileIndexItem metaDataItem)
 		private void AddOrRemoveXmpSidecarFileToDatabase(FileIndexItem metaDataItem)
 		{
 			if ( _iStorage.ExistFile(ExtensionRolesHelper.ReplaceExtensionWithXmp(metaDataItem
-				.FilePath))	 )
+					.FilePath)) )
 			{
 				metaDataItem.AddSidecarExtension("xmp");
 				return;
 			}
+
 			metaDataItem.RemoveSidecarExtension("xmp");
 		}
-		
+
 		/// 
 		/// Check if xml can be parsed
 		/// Used by sidecar upload
@@ -224,7 +232,7 @@ private bool IsValidXml(string xml)
 				return false;
 			}
 		}
-		
+
 		/// 
 		/// Upload sidecar file to specific folder (does not check if already has been imported)
 		/// Use the header 'to' to determine the location to where to upload
@@ -251,7 +259,7 @@ public async Task UploadToFolderSidecarFile()
 			var to = Request.Headers["to"].ToString();
 			if ( string.IsNullOrWhiteSpace(to) ) return BadRequest("missing 'to' header");
 			_logger.LogInformation($"[UploadToFolderSidecarFile] to:{to}");
-			
+
 			var parentDirectory = GetParentDirectoryFromRequestHeader();
 			if ( parentDirectory == null )
 			{
@@ -266,7 +274,7 @@ public async Task UploadToFolderSidecarFile()
 				var data = await StreamToStringHelper.StreamToStringAsync(
 					_iHostStorage.ReadStream(tempImportSinglePath));
 				if ( !IsValidXml(data) ) continue;
-				
+
 				var tempFileStream = _iHostStorage.ReadStream(tempImportSinglePath);
 				var fileName = Path.GetFileName(tempImportSinglePath);
 
@@ -275,22 +283,23 @@ public async Task UploadToFolderSidecarFile()
 
 				if ( _appSettings.UseDiskWatcher == false )
 				{
-					await new SyncSingleFile(_appSettings, _query, 
+					await new SyncSingleFile(_appSettings, _query,
 						_iStorage, null!, _logger).UpdateSidecarFile(subPath);
 				}
-				
+
 				await _iStorage.WriteStreamAsync(tempFileStream, subPath);
 				await tempFileStream.DisposeAsync();
 				importedList.Add(subPath);
-				
+
 				var deleteStatus = _iHostStorage.FileDelete(tempImportSinglePath);
 				_logger.LogInformation($"delete {tempImportSinglePath} is {deleteStatus}");
 			}
-			
+
 			if ( importedList.Count == 0 )
 			{
 				Response.StatusCode = 415;
 			}
+
 			return Json(importedList);
 		}
 
@@ -300,16 +309,16 @@ public async Task UploadToFolderSidecarFile()
 			if ( to == "/" ) return "/";
 
 			// only used for direct import
-			if ( _iStorage.ExistFolder(FilenamesHelper.GetParentPath(to)) && 
-			     FilenamesHelper.IsValidFileName(FilenamesHelper.GetFileName(to)) )
+			if ( _iStorage.ExistFolder(FilenamesHelper.GetParentPath(to)) &&
+				 FilenamesHelper.IsValidFileName(FilenamesHelper.GetFileName(to)) )
 			{
 				Request.Headers["filename"] = FilenamesHelper.GetFileName(to);
 				return FilenamesHelper.GetParentPath(PathHelper.RemoveLatestSlash(to));
 			}
+
 			// ReSharper disable once ConvertIfStatementToReturnStatement
-			if (!_iStorage.ExistFolder(PathHelper.RemoveLatestSlash(to))) return null;
+			if ( !_iStorage.ExistFolder(PathHelper.RemoveLatestSlash(to)) ) return null;
 			return PathHelper.RemoveLatestSlash(to);
 		}
-		
 	}
 }
diff --git a/starsky/starsky/Helpers/AntiForgeryCookie.cs b/starsky/starsky/Helpers/AntiForgeryCookie.cs
index 8a163bf816..9f6a0c3283 100644
--- a/starsky/starsky/Helpers/AntiForgeryCookie.cs
+++ b/starsky/starsky/Helpers/AntiForgeryCookie.cs
@@ -15,7 +15,7 @@ public AntiForgeryCookie(IAntiforgery antiForgery)
 		{
 			_antiForgery = antiForgery;
 		}
-		
+
 		/// 
 		/// The request token can be sent as a JavaScript-readable cookie, 
 		/// 
@@ -25,9 +25,9 @@ public void SetAntiForgeryCookie(HttpContext httpContext)
 			if ( httpContext == null ) return;
 			var tokens = _antiForgery.GetAndStoreTokens(httpContext).RequestToken;
 			if ( tokens == null ) return;
-			
+
 			httpContext.Response.Cookies.Append(
-				"X-XSRF-TOKEN", tokens, 
+				"X-XSRF-TOKEN", tokens,
 				new CookieOptions()
 				{
 					HttpOnly = false, // need to be false, is needed by the javascript front-end
diff --git a/starsky/starsky/Helpers/CacheControlOverwrite.cs b/starsky/starsky/Helpers/CacheControlOverwrite.cs
index 9505040399..65a7d82d00 100644
--- a/starsky/starsky/Helpers/CacheControlOverwrite.cs
+++ b/starsky/starsky/Helpers/CacheControlOverwrite.cs
@@ -4,7 +4,7 @@ namespace starsky.Helpers
 {
 	public static class CacheControlOverwrite
 	{
-			    
+
 		/// 
 		/// For Performance on slow devices
 		/// 
@@ -14,7 +14,7 @@ public static void SetExpiresResponseHeaders(HttpRequest request, int time = 290
 		{
 			request.HttpContext.Response.Headers.Remove("Cache-Control");
 			request.HttpContext.Response.Headers.Append("Cache-Control", $"private,max-age={time}");
-        
+
 			request.HttpContext.Response.Headers.Remove("Expires");
 			request.HttpContext.Response.Headers.Append("Expires", time.ToString());
 		}
diff --git a/starsky/starsky/Helpers/SwaggerExportHelper.cs b/starsky/starsky/Helpers/SwaggerExportHelper.cs
index 5830ced6e6..4e9c2adf3f 100644
--- a/starsky/starsky/Helpers/SwaggerExportHelper.cs
+++ b/starsky/starsky/Helpers/SwaggerExportHelper.cs
@@ -27,7 +27,7 @@ public SwaggerExportHelper(IServiceScopeFactory serviceScopeFactory, IWebLogger?
 			_serviceScopeFactory = serviceScopeFactory;
 			_logger = logger;
 		}
-		
+
 		/// 
 		/// Running scoped services
 		/// @see: https://thinkrethink.net/2018/07/12/injecting-a-scoped-service-into-ihostedservice/
@@ -36,13 +36,13 @@ public SwaggerExportHelper(IServiceScopeFactory serviceScopeFactory, IWebLogger?
 		/// CompletedTask
 		protected override Task ExecuteAsync(CancellationToken stoppingToken)
 		{
-			using (var scope = _serviceScopeFactory.CreateScope())
+			using ( var scope = _serviceScopeFactory.CreateScope() )
 			{
 				var appSettings = scope.ServiceProvider.GetRequiredService();
 				var selectorStorage = scope.ServiceProvider.GetRequiredService();
 				var swaggerProvider = scope.ServiceProvider.GetRequiredService();
 				var applicationLifetime = scope.ServiceProvider.GetRequiredService();
-				
+
 				Add03AppExport(appSettings, selectorStorage, swaggerProvider);
 				Add04SwaggerExportExitAfter(appSettings, applicationLifetime);
 			}
@@ -57,7 +57,7 @@ internal void ExecuteAsync()
 		{
 			ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
 		}
-		
+
 		/// 
 		/// Export Values to Storage
 		/// 
@@ -68,7 +68,7 @@ internal void ExecuteAsync()
 		public bool Add03AppExport(AppSettings appSettings, ISelectorStorage selectorStorage, ISwaggerProvider swaggerProvider)
 		{
 			if ( appSettings.AddSwagger != true || appSettings.AddSwaggerExport != true ) return false;
-			
+
 			var swaggerJsonText = GenerateSwagger(swaggerProvider, appSettings.Name);
 			if ( string.IsNullOrEmpty(swaggerJsonText) ) throw new ArgumentException("swaggerJsonText = null", nameof(swaggerProvider));
 
@@ -87,9 +87,9 @@ public bool Add03AppExport(AppSettings appSettings, ISelectorStorage selectorSto
 		public bool Add04SwaggerExportExitAfter(AppSettings appSettings, IHostApplicationLifetime applicationLifetime)
 		{
 			if ( appSettings.AddSwagger != true ||
-			     appSettings.AddSwaggerExport != true ||
-			     appSettings.AddSwaggerExportExitAfter != true ) return false;
-			
+				 appSettings.AddSwaggerExport != true ||
+				 appSettings.AddSwaggerExportExitAfter != true ) return false;
+
 			applicationLifetime.StopApplication();
 			return true;
 		}
diff --git a/starsky/starsky/Helpers/SwaggerSetupHelper.cs b/starsky/starsky/Helpers/SwaggerSetupHelper.cs
index 1c2562965d..847b24bd77 100644
--- a/starsky/starsky/Helpers/SwaggerSetupHelper.cs
+++ b/starsky/starsky/Helpers/SwaggerSetupHelper.cs
@@ -15,11 +15,11 @@ public SwaggerSetupHelper(AppSettings appSettings)
 		{
 			_appSettings = appSettings;
 		}
-		
+
 		public void Add01SwaggerGenHelper(IServiceCollection services)
 		{
 			var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
-			
+
 			services.AddSwaggerGen(c =>
 			{
 				c.SwaggerDoc(_appSettings.Name, new OpenApiInfo { Title = _appSettings.Name, Version = version });
@@ -38,7 +38,7 @@ public void Add01SwaggerGenHelper(IServiceCollection services)
 				c.IncludeXmlComments(GetXmlCommentsPath());
 			});
 		}
-		
+
 		/// 
 		/// Expose Swagger to the `/swagger/` endpoint
 		/// 
@@ -57,7 +57,7 @@ public void Add02AppUseSwaggerAndUi(IApplicationBuilder app)
 				options.OAuthAppName(_appSettings.Name + " - Swagger");
 			}); // makes the ui visible    
 		}
-		
+
 		private string GetXmlCommentsPath()
 		{
 			return Path.Combine(_appSettings.BaseDirectoryProject, "starsky.xml");
diff --git a/starsky/starsky/Program.cs b/starsky/starsky/Program.cs
index 763f0d7d65..98863a6f10 100644
--- a/starsky/starsky/Program.cs
+++ b/starsky/starsky/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
@@ -15,19 +15,19 @@ namespace starsky
 	public static class Program
 	{
 		[SuppressMessage("Usage", "S6603: The collection-specific TrueForAll " +
-		                          "method should be used instead of the All extension")]
+								  "method should be used instead of the All extension")]
 		public static async Task Main(string[] args)
 		{
 			var appSettingsPath = Path.Join(
 				new AppSettings().BaseDirectoryProject,
 				"appsettings.json");
-			await PortProgramHelper.SetEnvPortAspNetUrlsAndSetDefault(args,appSettingsPath);
-			
+			await PortProgramHelper.SetEnvPortAspNetUrlsAndSetDefault(args, appSettingsPath);
+
 			var builder = CreateWebHostBuilder(args);
 			var startup = new Startup(args);
 			startup.ConfigureServices(builder.Services);
 			builder.Host.UseWindowsService();
-			
+
 			var app = builder.Build();
 			var hostLifetime = app.Services.GetRequiredService();
 			startup.Configure(app, builder.Environment, hostLifetime);
@@ -42,7 +42,7 @@ internal static async Task RunAsync(WebApplication webApplication,
 			{
 				return false;
 			}
-			
+
 			try
 			{
 				await webApplication.RunAsync();
@@ -53,7 +53,7 @@ internal static async Task RunAsync(WebApplication webApplication,
 			}
 			return true;
 		}
-		
+
 		private static WebApplicationBuilder CreateWebHostBuilder(string[] args)
 		{
 			var settings = new WebApplicationOptions
@@ -67,10 +67,10 @@ private static WebApplicationBuilder CreateWebHostBuilder(string[] args)
 			builder.WebHost.ConfigureKestrel(k =>
 			{
 				k.Limits.MaxRequestLineSize = 65536; //64Kb
-				// AddServerHeader removes the header: Server: Kestrel
+													 // AddServerHeader removes the header: Server: Kestrel
 				k.AddServerHeader = false;
 			});
-			
+
 			builder.WebHost.UseIIS();
 			builder.WebHost.UseIISIntegration();
 
diff --git a/starsky/starsky/Properties/default-init-launchSettings.json b/starsky/starsky/Properties/default-init-launchSettings.json
index c31f279aa8..898032daae 100644
--- a/starsky/starsky/Properties/default-init-launchSettings.json
+++ b/starsky/starsky/Properties/default-init-launchSettings.json
@@ -3,37 +3,36 @@
   "profiles": {
     "starsky": {
       "commandName": "Project",
-        "commandLineArgs": "--urls \"http://localhost:4000\"",
-        "environmentVariables": {
-          "ASPNETCORE_URLS": "http://localhost:4000/",
-          "ASPNETCORE_ENVIRONMENT": "Development",
-          "app__useDiskWatcher": "true",
-          "app__UseRealtime": "true",
-          "app__AddSwagger": "true",
-          "app__AddSwaggerExport": "true",
-          "app__AddSwaggerExportExitAfter": "false",
-          "app__NoAccountLocalhost": "false",
-          "app__IsAccountRegisterOpen": "true", 
-          "app__SyncOnStartup": "false",
-          "___app__storageFolder": "remove prefix underscores ___ and update value",
-          "___app__dependenciesFolder": "remove prefix underscores ___and update value",  
-          "___app__ApplicationInsightsConnectionString": "remove prefix underscores ___ and update value",
-          "___app__DatabaseConnection": "Server=localhost;port=12838;database=starsky;uid=starsky;pwd=ad12dc47-a411-4dce-92b3-2649a755426c;maximumpoolsize=30;",
-          "app__DatabaseType": "Sqlite",
-          "app__ApplicationInsightsDatabaseTracking": "true",
-          "app__ApplicationInsightsLog": "true",
-          "app__EnablePackageTelemetry": "true",
-          "app__EnablePackageTelemetryDebug": "true", 
-          "app__DemoUnsafeDeleteStorageFolder": "false",
-          "app__useSystemTrash": "true",
-          "app__accountRolesByEmailRegisterOverwrite__demo@qdraw.nl": "Administrator",
-          "app__ThumbnailGenerationIntervalInMinutes": "15",
-          "___app__OpenTelemetry__TracesEndpoint": "http://localhost:4318/v1/traces",
-          "___app__OpenTelemetry__MetricsEndpoint": "http://localhost:4318/v1/metrics",
-          "___app__OpenTelemetry__LogsEndpoint": "http://localhost:4318/v1/logs",
-          "___app__OpenTelemetry__Header": "api-key=test",
-          "___app__OpenTelemetry__ServiceName": "starsky-dev"
-        }
+      "commandLineArgs": "--urls \"http://localhost:4000\"",
+      "environmentVariables": {
+        "ASPNETCORE_URLS": "http://localhost:4000/",
+        "ASPNETCORE_ENVIRONMENT": "Development",
+        "app__useDiskWatcher": "true",
+        "app__UseRealtime": "true",
+        "app__AddSwagger": "true",
+        "app__AddSwaggerExport": "true",
+        "app__AddSwaggerExportExitAfter": "false",
+        "app__NoAccountLocalhost": "false",
+        "app__IsAccountRegisterOpen": "true",
+        "app__SyncOnStartup": "false",
+        "___app__storageFolder": "remove prefix underscores ___ and update value",
+        "___app__dependenciesFolder": "remove prefix underscores ___and update value",
+        "___app__DatabaseConnection": "Server=localhost;port=12838;database=starsky;uid=root;pwd=ad12dc47-a411-4dce-92b3-2649a755426c;maximumpoolsize=30;",
+        "app__DatabaseType": "Sqlite",
+        "app__ApplicationInsightsDatabaseTracking": "true",
+        "app__ApplicationInsightsLog": "true",
+        "app__EnablePackageTelemetry": "true",
+        "app__EnablePackageTelemetryDebug": "true",
+        "app__DemoUnsafeDeleteStorageFolder": "false",
+        "app__useSystemTrash": "true",
+        "app__accountRolesByEmailRegisterOverwrite__demo@qdraw.nl": "Administrator",
+        "app__ThumbnailGenerationIntervalInMinutes": "15",
+        "___app__OpenTelemetry__TracesEndpoint": "http://localhost:4318/v1/traces",
+        "___app__OpenTelemetry__MetricsEndpoint": "http://localhost:4318/v1/metrics",
+        "___app__OpenTelemetry__LogsEndpoint": "http://localhost:4318/v1/logs",
+        "___app__OpenTelemetry__Header": "api-key=test",
+        "___app__OpenTelemetry__ServiceName": "starsky-dev"
+      }
     }
   }
 }
diff --git a/starsky/starsky/Startup.cs b/starsky/starsky/Startup.cs
index efd56a86bb..a7dc9f69cd 100644
--- a/starsky/starsky/Startup.cs
+++ b/starsky/starsky/Startup.cs
@@ -7,7 +7,6 @@
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
-using Microsoft.ApplicationInsights.Extensibility;
 using Microsoft.AspNetCore.Authentication;
 using Microsoft.AspNetCore.Authentication.Cookies;
 using Microsoft.AspNetCore.Builder;
@@ -35,12 +34,10 @@
 using starsky.foundation.realtime.Model;
 using starsky.foundation.webtelemetry.Extensions;
 using starsky.foundation.webtelemetry.Helpers;
-using starsky.foundation.webtelemetry.Processor;
 using starsky.Helpers;
 
 namespace starsky
 {
-	// ReSharper disable once ClassNeverInstantiated.Global
 	public sealed class Startup
 	{
 		private readonly IConfigurationRoot _configuration;
@@ -60,7 +57,7 @@ public Startup(string[]? args = null)
 			if ( !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("app__appsettingspath")) )
 			{
 				Console.WriteLine("app__appSettingsPath: " +
-				                  Environment.GetEnvironmentVariable("app__appsettingspath"));
+								  Environment.GetEnvironmentVariable("app__appsettingspath"));
 			}
 
 			_configuration = SetupAppSettings.AppSettingsToBuilder(args).ConfigureAwait(false)
@@ -87,8 +84,6 @@ public void ConfigureServices(IServiceCollection services)
 			services.AddMemoryCache();
 			// this is ignored here: appSettings.AddMemoryCache; but implemented in cache
 
-			// Detect Application Insights (used in next SetupDatabaseTypes)
-			services.AddMonitoring(_appSettings);
 			services.AddOpenTelemetryMonitoring(_appSettings);
 
 			// LoggerFactory
@@ -294,7 +289,7 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
 			app.UseAuthentication();
 			app.UseBasicAuthentication();
 			app.UseNoAccount(_appSettings?.NoAccountLocalhost == true ||
-			                 _appSettings?.DemoUnsafeDeleteStorageFolder == true);
+							 _appSettings?.DemoUnsafeDeleteStorageFolder == true);
 			app.UseCheckIfAccountExist();
 
 			app.UseAuthorization();
@@ -316,19 +311,6 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
 			app.UseWebSockets();
 			app.MapWebSocketConnections("/realtime", new WebSocketConnectionsOptions(),
 				_appSettings?.UseRealtime);
-
-			if ( _appSettings != null && !string.IsNullOrWhiteSpace(_appSettings
-				    .ApplicationInsightsConnectionString) )
-			{
-				var configuration =
-					app.ApplicationServices.GetRequiredService();
-				configuration.TelemetryProcessorChainBuilder.Use(next =>
-					new FilterWebsocketsTelemetryProcessor(next));
-				configuration.TelemetryProcessorChainBuilder.Build();
-
-				var onStoppedSync = new FlushApplicationInsights(app);
-				applicationLifetime.ApplicationStopping.Register(onStoppedSync.Flush);
-			}
 		}
 
 		/// 
@@ -340,7 +322,7 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
 		internal (bool, bool, bool) SetupStaticFiles(IApplicationBuilder app,
 			string assetsName = "assets")
 		{
-			var result = ( false, false, false );
+			var result = (false, false, false);
 
 			// Allow Current Directory and wwwroot in Base Directory
 			// AppSettings can be null when running tests
@@ -369,19 +351,19 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env,
 
 			// Check if clientapp is build and use the assets folder
 			if ( !Directory.Exists(Path.Combine(
-				    _appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)) )
+					_appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)) )
 			{
 				return result;
 			}
 
 			app.UseStaticFiles(new StaticFileOptions
-				{
-					OnPrepareResponse = PrepareResponse,
-					FileProvider = new PhysicalFileProvider(
+			{
+				OnPrepareResponse = PrepareResponse,
+				FileProvider = new PhysicalFileProvider(
 						Path.Combine(_appSettings.BaseDirectoryProject, "clientapp", "build",
 							assetsName)),
-					RequestPath = $"/assets",
-				}
+				RequestPath = $"/assets",
+			}
 			);
 			result.Item3 = true;
 			return result;
diff --git a/starsky/starsky/appsettings.Development.json b/starsky/starsky/appsettings.Development.json
index 433a5fa8eb..d8eec8f307 100644
--- a/starsky/starsky/appsettings.Development.json
+++ b/starsky/starsky/appsettings.Development.json
@@ -5,9 +5,10 @@
       "Default": "Debug",
       "System": "Information",
       "Microsoft": "Information",
-      "Microsoft.AspNetCore.Cors.Infrastructure.CorsService": "Warning", 
-      "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Warning", 
-      "Microsoft.AspNetCore.DataProtection": "Warning"
+      "Microsoft.AspNetCore.Cors.Infrastructure.CorsService": "Warning",
+      "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Warning",
+      "Microsoft.AspNetCore.DataProtection": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
     }
   }
 }
diff --git a/starsky/starsky/appsettings.default.json b/starsky/starsky/appsettings.default.json
index 91f59a10b1..c27d090c8b 100644
--- a/starsky/starsky/appsettings.default.json
+++ b/starsky/starsky/appsettings.default.json
@@ -1,62 +1,62 @@
 {
-    "app": {
-        "demoData": [
-            {
-                "Key": "https://media.qdraw.nl/log/malaga-2021/_settings.json",
-                "Value": "malaga-2021"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/schoorl-boswachterspad-noordzeeroute-2022/_settings.json",
-                "Value": "schoorl-boswachterspad-noordzeeroute-2022"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/kungsleden-etappe-1-heenreis-en-abiskojokk/_settings.json",
-                "Value": "kungsleden"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/kungsleden-etappe-2-van-abiskojokk-naar-grddenvaggi/_settings.json",
-                "Value": "kungsleden"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/kungsleden-etappe-3-van-grddenvaggi-naar-bossosjohka-2022/_settings.json",
-                "Value": "kungsleden"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/kungsleden-etappe-4-van-bossosjohka-naar-kiruna-2022/_settings.json",
-                "Value": "kungsleden"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/kungsleden-etappe-5-van-kiruna-naar-juoksujrvi-2022/_settings.json",
-                "Value": "kungsleden"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/kungsleden-etappe-6-van-kebnekaise-kittelbcken-naar-nikkaluokta-2/_settings.json",
-                "Value": "kungsleden"
-            },
-            {
-                "Key": "https://media.qdraw.nl/download/starsky-sample-photos/_settings.json",
-                "Value": "vsm-en-den-bosch"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/vernant-in-de-franse-alpen-2020/_settings.json",
-                "Value": "vernant-2020"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/gers-in-de-franse-alpen-2020/_settings.json",
-                "Value": "gers-2020"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/hiken-in-estland-rmk-oandu-aegviidu-ikla-wandelroute-van-aegviidu/_settings.json",
-                "Value": "estland-2021"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/hiken-in-estland-rmk-wandelroute-van-paukjarve-naar-kalmeoja-2021/_settings.json",
-                "Value": "estland-2021"
-            },
-            {
-                "Key": "https://media.qdraw.nl/log/hiken-in-estland-rmk-wandelroute-van-kalmeoja-naar-vosu-2021/_settings.json",
-                "Value": "estland-2021"
-            }
-        ]
-    }
-}
\ No newline at end of file
+  "app": {
+    "demoData": [
+      {
+        "Key": "https://media.qdraw.nl/log/malaga-2021/_settings.json",
+        "Value": "malaga-2021"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/schoorl-boswachterspad-noordzeeroute-2022/_settings.json",
+        "Value": "schoorl-boswachterspad-noordzeeroute-2022"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/kungsleden-etappe-1-heenreis-en-abiskojokk/_settings.json",
+        "Value": "kungsleden"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/kungsleden-etappe-2-van-abiskojokk-naar-grddenvaggi/_settings.json",
+        "Value": "kungsleden"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/kungsleden-etappe-3-van-grddenvaggi-naar-bossosjohka-2022/_settings.json",
+        "Value": "kungsleden"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/kungsleden-etappe-4-van-bossosjohka-naar-kiruna-2022/_settings.json",
+        "Value": "kungsleden"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/kungsleden-etappe-5-van-kiruna-naar-juoksujrvi-2022/_settings.json",
+        "Value": "kungsleden"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/kungsleden-etappe-6-van-kebnekaise-kittelbcken-naar-nikkaluokta-2/_settings.json",
+        "Value": "kungsleden"
+      },
+      {
+        "Key": "https://media.qdraw.nl/download/starsky-sample-photos/_settings.json",
+        "Value": "vsm-en-den-bosch"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/vernant-in-de-franse-alpen-2020/_settings.json",
+        "Value": "vernant-2020"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/gers-in-de-franse-alpen-2020/_settings.json",
+        "Value": "gers-2020"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/hiken-in-estland-rmk-oandu-aegviidu-ikla-wandelroute-van-aegviidu/_settings.json",
+        "Value": "estland-2021"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/hiken-in-estland-rmk-wandelroute-van-paukjarve-naar-kalmeoja-2021/_settings.json",
+        "Value": "estland-2021"
+      },
+      {
+        "Key": "https://media.qdraw.nl/log/hiken-in-estland-rmk-wandelroute-van-kalmeoja-naar-vosu-2021/_settings.json",
+        "Value": "estland-2021"
+      }
+    ]
+  }
+}
diff --git a/starsky/starsky/appsettings.json b/starsky/starsky/appsettings.json
index 279fb6552e..c8358f69b0 100644
--- a/starsky/starsky/appsettings.json
+++ b/starsky/starsky/appsettings.json
@@ -1,159 +1,152 @@
 {
-    "Logging": {
-        "LogLevel": {
-            "Default": "Information",
-            "app": "Information",
-            "System": "Warning",
-            "Microsoft": "Warning",
-            "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Warning",
-            "Microsoft.AspNetCore.DataProtection": "Warning"
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "app": "Information",
+      "System": "Warning",
+      "Microsoft": "Warning",
+      "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Warning",
+      "Microsoft.AspNetCore.DataProtection": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    },
+    "EventLog": {
+      "LogLevel": {
+        "Default": "Warning",
+        "app": "Information"
+      }
+    }
+  },
+  "app": {
+    "DatabaseType": "sqlite",
+    "DatabaseConnection": "Data Source=app__data.db",
+    "addMemoryCache": "true",
+    "ExifToolImportXmpCreate": "true",
+    "CameraTimeZone": "Europe/Berlin",
+    "readOnlyFolders": [
+      "/read-only-folder"
+    ],
+    "SyncIgnore": [
+      "/lost+found",
+      "/.stfolder",
+      "/.git",
+      "$RECYCLE.BIN",
+      "Photos Library.photoslibrary"
+    ],
+    "importIgnore": [
+      "lost+found",
+      "/.stfolder",
+      "/.git",
+      "$RECYCLE.BIN",
+      "Photos Library.photoslibrary"
+    ],
+    "VideoUseLocalTime": [
+      {
+        "Make": "Sony",
+        "Model": "A58"
+      }
+    ],
+    "metaThumbnailOnImport": "true",
+    "isAccountRegisterOpen": "false",
+    "accountRegisterDefaultRole": "User",
+    "AccountRolesByEmailRegisterOverwrite": {
+      "demo@qdraw.nl": "Administrator"
+    },
+    "checkForUpdates": "true",
+    "addSwagger": "false",
+    "addSwaggerExport": "false",
+    "SyncOnStartup": "true",
+    "DemoUnsafeDeleteStorageFolder": "false",
+    "useSystemTrash": "true",
+    "OpenTelemetry": {
+      "TracesEndpoint": null,
+      "MetricsEndpoint": null,
+      "LogsEndpoint": null
+    },
+    "publishProfiles": {
+      "_default": [
+        {
+          "ContentType": "html",
+          "SourceMaxWidth": 0,
+          "OverlayMaxWidth": 0,
+          "OverlayFullPath": "",
+          "Path": "index.html",
+          "Template": "Index.cshtml",
+          "Prepend": "",
+          "Copy": "true"
         },
-        "ApplicationInsights": {
-            "LogLevel": {
-                "Default": "Information",
-                "app": "Trace",
-                "System": "Warning",
-                "Microsoft": "Warning",
-                "Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker": "Information",
-                "Microsoft.AspNetCore.DataProtection": "Information"
-            }
+        {
+          "ContentType": "html",
+          "SourceMaxWidth": 0,
+          "OverlayMaxWidth": 0,
+          "OverlayFullPath": "",
+          "Path": "index.web.html",
+          "Template": "Index.cshtml",
+          "Prepend": "https://media.qdraw.nl/log/{name}",
+          "Copy": "true"
         },
-        "EventLog": {
-            "LogLevel": {
-                "Default": "Warning",
-                "app": "Information"
-            }
-        }
-    },
-    "app" :{
-        "DatabaseType": "sqlite",
-        "DatabaseConnection": "Data Source=app__data.db",
-        "addMemoryCache": "true",
-        "ExifToolImportXmpCreate": "true",
-        "CameraTimeZone": "Europe/Berlin",
-        "readOnlyFolders":[
-            "/read-only-folder"
-        ],
-        "SyncIgnore": [
-            "/lost+found",
-            "/.stfolder",
-            "/.git",
-            "$RECYCLE.BIN",
-            "Photos Library.photoslibrary"
-        ],
-        "importIgnore":[
-            "lost+found",
-            "/.stfolder",
-            "/.git",
-            "$RECYCLE.BIN",
-            "Photos Library.photoslibrary"
-        ],
-        "VideoUseLocalTime" : [{
-            "Make": "Sony",
-            "Model": "A58"
-        }],
-        "metaThumbnailOnImport": "true",
-        "isAccountRegisterOpen": "false",
-        "accountRegisterDefaultRole": "User",
-        "AccountRolesByEmailRegisterOverwrite": {
-            "demo@qdraw.nl": "Administrator"
+        {
+          "ContentType": "html",
+          "SourceMaxWidth": 0,
+          "OverlayMaxWidth": 0,
+          "OverlayFullPath": "",
+          "Path": "autopost.txt",
+          "Template": "Autopost.cshtml",
+          "Prepend": "https://media.qdraw.nl/log/{name}",
+          "Copy": "true"
         },
-        "checkForUpdates": "true",
-        "addSwagger": "false",
-        "addSwaggerExport": "false",
-        "SyncOnStartup": "true",
-        "DemoUnsafeDeleteStorageFolder" : "false",
-        "useSystemTrash": "true",
-        "OpenTelemetry": {
-            "TracesEndpoint": null,
-            "MetricsEndpoint": null,
-            "LogsEndpoint": null
+        {
+          "ContentType": "jpeg",
+          "SourceMaxWidth": 1000,
+          "OverlayMaxWidth": 380,
+          "Path": "{AssemblyDirectory}/WebHtmlPublish/EmbeddedViews/qdrawlarge.png",
+          "Folder": "1000",
+          "Append": "_kl1k",
+          "Copy": "true"
         },
-        "publishProfiles": {
-            "_default": [
-                {
-                    "ContentType":  "html",
-                    "SourceMaxWidth":  0,
-                    "OverlayMaxWidth":  0,
-                    "OverlayFullPath": "",
-                    "Path": "index.html",
-                    "Template": "Index.cshtml",
-                    "Prepend": "",
-                    "Copy": "true"
-                },
-                {
-                    "ContentType":  "html",
-                    "SourceMaxWidth":  0,
-                    "OverlayMaxWidth":  0,
-                    "OverlayFullPath": "",
-                    "Path": "index.web.html",
-                    "Template": "Index.cshtml",
-                    "Prepend": "https://media.qdraw.nl/log/{name}",
-                    "Copy": "true"
-                },
-                {
-                    "ContentType":  "html",
-                    "SourceMaxWidth":  0,
-                    "OverlayMaxWidth":  0,
-                    "OverlayFullPath": "",
-                    "Path": "autopost.txt",
-                    "Template": "Autopost.cshtml",
-                    "Prepend": "https://media.qdraw.nl/log/{name}",
-                    "Copy": "true"
-                },
-                {
-                    "ContentType":  "jpeg",
-                    "SourceMaxWidth":  1000,
-                    "OverlayMaxWidth":  380,
-                    "Path": "{AssemblyDirectory}/WebHtmlPublish/EmbeddedViews/qdrawlarge.png",
-                    "Folder": "1000",
-                    "Append": "_kl1k",
-                    "Copy": "true"
-                },
-                {
-                    "ContentType":  "jpeg",
-                    "SourceMaxWidth":  500,
-                    "OverlayMaxWidth":  200,
-                    "Path": "{AssemblyDirectory}/WebHtmlPublish/EmbeddedViews/qdrawsmall.png",
-                    "Folder": "500",
-                    "Append": "_kl",
-                    "Copy": "true",
-                    "MetaData": "false"
-                },
-                {
-                    "ContentType":  "moveSourceFiles",
-                    "Folder": "orgineel",
-                    "Copy": "false"
-                },
-                {
-                    "ContentType":  "publishContent",
-                    "Folder": "",
-                    "Copy": "true"
-                },
-                {
-                    "ContentType": "publishManifest",
-                    "Folder": "",
-                    "Copy": "true"
-                },
-                {
-                    "ContentType":  "onlyFirstJpeg",
-                    "SourceMaxWidth":  213,
-                    "Folder": "",
-                    "Append": "___og_image",
-                    "Copy": "true",
-                    "MetaData": "false"
-                }
-            ],
-            "no_logo_2000px": [
-                {
-                    "ContentType":  "jpeg",
-                    "SourceMaxWidth":  2000,
-                    "OverlayMaxWidth":  0,
-                    "Folder": "",
-                    "Append": "_kl2k",
-                    "Copy": "true"
-                }
-            ]
+        {
+          "ContentType": "jpeg",
+          "SourceMaxWidth": 500,
+          "OverlayMaxWidth": 200,
+          "Path": "{AssemblyDirectory}/WebHtmlPublish/EmbeddedViews/qdrawsmall.png",
+          "Folder": "500",
+          "Append": "_kl",
+          "Copy": "true",
+          "MetaData": "false"
+        },
+        {
+          "ContentType": "moveSourceFiles",
+          "Folder": "orgineel",
+          "Copy": "false"
+        },
+        {
+          "ContentType": "publishContent",
+          "Folder": "",
+          "Copy": "true"
+        },
+        {
+          "ContentType": "publishManifest",
+          "Folder": "",
+          "Copy": "true"
+        },
+        {
+          "ContentType": "onlyFirstJpeg",
+          "SourceMaxWidth": 213,
+          "Folder": "",
+          "Append": "___og_image",
+          "Copy": "true",
+          "MetaData": "false"
+        }
+      ],
+      "no_logo_2000px": [
+        {
+          "ContentType": "jpeg",
+          "SourceMaxWidth": 2000,
+          "OverlayMaxWidth": 0,
+          "Folder": "",
+          "Append": "_kl2k",
+          "Copy": "true"
         }
+      ]
     }
+  }
 }
diff --git a/starsky/starsky/clientapp/public/index.html b/starsky/starsky/clientapp/public/index.html
index 6aeb8a94e4..0246428e49 100644
--- a/starsky/starsky/clientapp/public/index.html
+++ b/starsky/starsky/clientapp/public/index.html
@@ -1,51 +1,45 @@
 
 
-  
-    
-    
-    
-    
-    
+
+    
+    
+    
+    
+    
     
-    
+    
     Starsky App
-  
+
 
-  
-    
-