makes most classes internal and adds a download command to download original images from piwigo to the local folder (can be used for backup of a gallery)
All checks were successful
PiwigoDirectorySync/pipeline/head This commit looks good

This commit is contained in:
Philipp Häfelfinger 2023-09-02 23:44:10 +02:00
parent 303d69efe6
commit 96bce7c83a
29 changed files with 194 additions and 42 deletions

View File

@ -2,7 +2,7 @@ using Microsoft.Extensions.Configuration;
namespace PiwigoDirectorySync; namespace PiwigoDirectorySync;
public static class AppSettings internal static class AppSettings
{ {
public static readonly IReadOnlySet<string> SupportedExtensions = new HashSet<string> { "jpg", "jpeg", "png" }; public static readonly IReadOnlySet<string> SupportedExtensions = new HashSet<string> { "jpg", "jpeg", "png" };

View File

@ -6,7 +6,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Done by parser")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Done by parser")]
public class CommonCommandSettings : CommandSettings internal class CommonCommandSettings : CommandSettings
{ {
[CommandArgument(0, "[PiwigoServerId]")] [CommandArgument(0, "[PiwigoServerId]")]
[DefaultValue(1)] [DefaultValue(1)]

View File

@ -0,0 +1,33 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using PiwigoDirectorySync.Services;
using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands;
internal class DownloadImagesCommand : AsyncCommand<DownloadImagesSettings>
{
private readonly IImageSynchronizer _imageSynchronizer;
private readonly ILogger<DownloadImagesCommand> _logger;
public DownloadImagesCommand(IImageSynchronizer imageSynchronizer, ILogger<DownloadImagesCommand> logger)
{
_imageSynchronizer = imageSynchronizer;
_logger = logger;
}
public override async Task<int> ExecuteAsync(CommandContext context, DownloadImagesSettings settings)
{
_logger.LogInformation("Starting image download for piwigo server {SettingsPiwigoServerId}", settings.PiwigoServerId);
var stopWatch = Stopwatch.StartNew();
var cancellationTokenSource = new CancellationTokenSource();
await _imageSynchronizer.DownloadImagesAsync(settings.PiwigoServerId, cancellationTokenSource.Token);
stopWatch.Stop();
_logger.LogInformation("Synchronized all images with piwigo server {SettingsPiwigoServerId} in {ElapsedTotalSeconds} seconds", settings.PiwigoServerId,
stopWatch.Elapsed.TotalSeconds);
return 0;
}
}

View File

@ -0,0 +1,5 @@
namespace PiwigoDirectorySync.Commands;
internal class DownloadImagesSettings : CommonCommandSettings
{
}

View File

@ -6,7 +6,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
public class ScanCommand : AsyncCommand<ScanSettings> internal class ScanCommand : AsyncCommand<ScanSettings>
{ {
private readonly IFileIndexer _fileIndexer; private readonly IFileIndexer _fileIndexer;
private readonly IFileSystemScanner _fileSystemScanner; private readonly IFileSystemScanner _fileSystemScanner;

View File

@ -5,7 +5,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class ScanSettings : CommonCommandSettings internal class ScanSettings : CommonCommandSettings
{ {
[CommandOption("-d|--mark-for-delete")] [CommandOption("-d|--mark-for-delete")]
[DefaultValue(false)] [DefaultValue(false)]

View File

@ -5,7 +5,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
public class SyncAlbumsCommand : AsyncCommand<SyncAlbumsSettings> internal class SyncAlbumsCommand : AsyncCommand<SyncAlbumsSettings>
{ {
private readonly IAlbumSynchronizer _albumSynchronizer; private readonly IAlbumSynchronizer _albumSynchronizer;
private readonly ILogger<SyncAlbumsCommand> _logger; private readonly ILogger<SyncAlbumsCommand> _logger;
@ -18,7 +18,8 @@ public class SyncAlbumsCommand : AsyncCommand<SyncAlbumsSettings>
public override async Task<int> ExecuteAsync(CommandContext context, SyncAlbumsSettings settings) public override async Task<int> ExecuteAsync(CommandContext context, SyncAlbumsSettings settings)
{ {
_logger.LogInformation("Starting album synchronization"); _logger.LogInformation("Starting album synchronization for piwigo server {SettingsPiwigoServerId}", settings.PiwigoServerId);
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();

View File

@ -3,6 +3,6 @@ using System.Diagnostics.CodeAnalysis;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class SyncAlbumsSettings : CommonCommandSettings internal class SyncAlbumsSettings : CommonCommandSettings
{ {
} }

View File

@ -5,7 +5,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
public class SyncFullCommand : AsyncCommand<SyncFullSettings> internal class SyncFullCommand : AsyncCommand<SyncFullSettings>
{ {
private readonly IAlbumSynchronizer _albumSynchronizer; private readonly IAlbumSynchronizer _albumSynchronizer;
private readonly IFileIndexer _fileIndexer; private readonly IFileIndexer _fileIndexer;
@ -37,7 +37,7 @@ public class SyncFullCommand : AsyncCommand<SyncFullSettings>
await _albumSynchronizer.SynchronizeAlbums(settings.PiwigoServerId, cancellationTokenSource.Token); await _albumSynchronizer.SynchronizeAlbums(settings.PiwigoServerId, cancellationTokenSource.Token);
_logger.LogInformation("running image synchronization"); _logger.LogInformation("running image synchronization");
await _imageSynchronizer.SynchronizeImages(settings.PiwigoServerId, cancellationTokenSource.Token); await _imageSynchronizer.SynchronizeImagesAsync(settings.PiwigoServerId, cancellationTokenSource.Token);
stopWatch.Stop(); stopWatch.Stop();
_logger.LogInformation("Full synchronization for piwigo server {SettingsPiwigoServerId} finished in {ElapsedTotalSeconds} seconds", settings.PiwigoServerId, _logger.LogInformation("Full synchronization for piwigo server {SettingsPiwigoServerId} finished in {ElapsedTotalSeconds} seconds", settings.PiwigoServerId,

View File

@ -1,3 +1,3 @@
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
public class SyncFullSettings : CommonCommandSettings {} internal class SyncFullSettings : CommonCommandSettings {}

View File

@ -5,7 +5,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
public class SyncImagesCommand : AsyncCommand<SyncImagesSettings> internal class SyncImagesCommand : AsyncCommand<SyncImagesSettings>
{ {
private readonly IImageSynchronizer _imageSynchronizer; private readonly IImageSynchronizer _imageSynchronizer;
private readonly ILogger<SyncImagesCommand> _logger; private readonly ILogger<SyncImagesCommand> _logger;
@ -18,11 +18,11 @@ public class SyncImagesCommand : AsyncCommand<SyncImagesSettings>
public override async Task<int> ExecuteAsync(CommandContext context, SyncImagesSettings settings) public override async Task<int> ExecuteAsync(CommandContext context, SyncImagesSettings settings)
{ {
_logger.LogInformation("Starting image synchronization"); _logger.LogInformation("Starting image synchronization of piwigo server {SettingsPiwigoServerId}", settings.PiwigoServerId);
var stopWatch = Stopwatch.StartNew(); var stopWatch = Stopwatch.StartNew();
var cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSource = new CancellationTokenSource();
await _imageSynchronizer.SynchronizeImages(settings.PiwigoServerId, cancellationTokenSource.Token); await _imageSynchronizer.SynchronizeImagesAsync(settings.PiwigoServerId, cancellationTokenSource.Token);
stopWatch.Stop(); stopWatch.Stop();
_logger.LogInformation("Synchronized all images with piwigo server {SettingsPiwigoServerId} in {ElapsedTotalSeconds} seconds", settings.PiwigoServerId, _logger.LogInformation("Synchronized all images with piwigo server {SettingsPiwigoServerId} in {ElapsedTotalSeconds} seconds", settings.PiwigoServerId,

View File

@ -1,5 +1,5 @@
namespace PiwigoDirectorySync.Commands; namespace PiwigoDirectorySync.Commands;
public class SyncImagesSettings : CommonCommandSettings internal class SyncImagesSettings : CommonCommandSettings
{ {
} }

View File

@ -3,7 +3,7 @@ using Spectre.Console.Cli;
namespace PiwigoDirectorySync.Infrastructure; namespace PiwigoDirectorySync.Infrastructure;
public sealed class DependencyInjectionTypeRegistrar : ITypeRegistrar internal sealed class DependencyInjectionTypeRegistrar : ITypeRegistrar
{ {
private readonly IServiceCollection _builder; private readonly IServiceCollection _builder;

View File

@ -2,7 +2,7 @@
namespace PiwigoDirectorySync.Infrastructure; namespace PiwigoDirectorySync.Infrastructure;
public sealed class DependencyTypeResolver : ITypeResolver, IDisposable internal sealed class DependencyTypeResolver : ITypeResolver, IDisposable
{ {
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;

View File

@ -0,0 +1,14 @@
using System.Security.Cryptography;
namespace PiwigoDirectorySync.Infrastructure;
internal static class FilesystemHelpers
{
public static async Task<string> CalculateMd5SumAsync(string imageFileFullPath, CancellationToken stoppingToken)
{
using var md5 = MD5.Create();
await using var stream = File.OpenRead(imageFileFullPath);
var hash = await md5.ComputeHashAsync(stream, stoppingToken);
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}
}

View File

@ -3,7 +3,7 @@ using PiwigoDirectorySync.Persistence;
namespace PiwigoDirectorySync.Infrastructure; namespace PiwigoDirectorySync.Infrastructure;
public interface IPiwigoClientFactory internal interface IPiwigoClientFactory
{ {
Task<IPiwigoClient> GetPiwigoClientAsync(ServerEntity piwigoServer, CancellationToken ct); Task<IPiwigoClient> GetPiwigoClientAsync(ServerEntity piwigoServer, CancellationToken ct);
} }

View File

@ -4,7 +4,7 @@ using PiwigoDirectorySync.Persistence;
namespace PiwigoDirectorySync.Infrastructure; namespace PiwigoDirectorySync.Infrastructure;
public class PiwigoClientFactory : IPiwigoClientFactory internal class PiwigoClientFactory : IPiwigoClientFactory
{ {
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;

View File

@ -12,6 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -45,11 +45,12 @@ app.Configure(config =>
#endif #endif
config.AddCommand<ScanCommand>("scan"); config.AddCommand<ScanCommand>("scan");
config.AddCommand<DownloadImagesCommand>("download");
config.AddBranch("sync", c => config.AddBranch("sync", c =>
{ {
c.AddCommand<SyncFullCommand>("full");
c.AddCommand<SyncAlbumsCommand>("albums"); c.AddCommand<SyncAlbumsCommand>("albums");
c.AddCommand<SyncImagesCommand>("images"); c.AddCommand<SyncImagesCommand>("images");
c.AddCommand<SyncFullCommand>("full");
}); });
}); });

View File

@ -30,6 +30,12 @@
"commandLineArgs": "sync full", "commandLineArgs": "sync full",
"environmentVariables": { "environmentVariables": {
} }
},
"Download": {
"commandName": "Project",
"commandLineArgs": "download",
"environmentVariables": {
}
} }
} }
} }

View File

@ -7,7 +7,7 @@ using PiwigoDirectorySync.Persistence;
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public class AlbumSynchronizer : IAlbumSynchronizer internal class AlbumSynchronizer : IAlbumSynchronizer
{ {
private readonly ILogger<AlbumSynchronizer> _logger; private readonly ILogger<AlbumSynchronizer> _logger;
private readonly PersistenceContext _persistenceContext; private readonly PersistenceContext _persistenceContext;

View File

@ -1,12 +1,12 @@
using System.Security.Cryptography; using System.Threading.Channels;
using System.Threading.Channels;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PiwigoDirectorySync.Infrastructure;
using PiwigoDirectorySync.Persistence; using PiwigoDirectorySync.Persistence;
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public class FileIndexer : IFileIndexer internal class FileIndexer : IFileIndexer
{ {
private readonly IList<string> _failedFiles = new List<string>(); private readonly IList<string> _failedFiles = new List<string>();
private readonly ILogger<FileIndexer> _logger; private readonly ILogger<FileIndexer> _logger;
@ -54,7 +54,7 @@ public class FileIndexer : IFileIndexer
if (image.LastChange != fileInfo.LastWriteTimeUtc) if (image.LastChange != fileInfo.LastWriteTimeUtc)
{ {
image.UploadRequired = true; image.UploadRequired = true;
image.Md5Sum = await CalculateMd5SumAsync(fullFilePath, ct); image.Md5Sum = await FilesystemHelpers.CalculateMd5SumAsync(fullFilePath, ct);
} }
image.DeleteRequired = false; image.DeleteRequired = false;
@ -125,12 +125,4 @@ public class FileIndexer : IFileIndexer
return album; return album;
} }
private static async Task<string> CalculateMd5SumAsync(string imageFileFullPath, CancellationToken stoppingToken)
{
using var md5 = MD5.Create();
await using var stream = File.OpenRead(imageFileFullPath);
var hash = await md5.ComputeHashAsync(stream, stoppingToken);
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}
} }

View File

@ -4,7 +4,7 @@ using PiwigoDirectorySync.Persistence;
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public class FileSystemScanner : IFileSystemScanner internal class FileSystemScanner : IFileSystemScanner
{ {
private readonly ILogger<FileSystemScanner> _logger; private readonly ILogger<FileSystemScanner> _logger;
private readonly PersistenceContext _persistenceContext; private readonly PersistenceContext _persistenceContext;

View File

@ -1,6 +1,6 @@
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public interface IAlbumSynchronizer internal interface IAlbumSynchronizer
{ {
Task SynchronizeAlbums(int piwigoServerId, CancellationToken ct); Task SynchronizeAlbums(int piwigoServerId, CancellationToken ct);
} }

View File

@ -2,7 +2,7 @@ using System.Threading.Channels;
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public interface IFileIndexer internal interface IFileIndexer
{ {
int TotalFilesScanned { get; } int TotalFilesScanned { get; }
IReadOnlyCollection<string> FailedFiles { get; } IReadOnlyCollection<string> FailedFiles { get; }

View File

@ -2,7 +2,7 @@ using System.Threading.Channels;
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public interface IFileSystemScanner internal interface IFileSystemScanner
{ {
Task ScanAsync(Channel<string> fileQueue, int piwigoServerId, CancellationToken ct); Task ScanAsync(Channel<string> fileQueue, int piwigoServerId, CancellationToken ct);
} }

View File

@ -1,6 +1,7 @@
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public interface IImageSynchronizer internal interface IImageSynchronizer
{ {
Task SynchronizeImages(int piwigoServerId, CancellationToken ct); Task SynchronizeImagesAsync(int piwigoServerId, CancellationToken ct);
Task DownloadImagesAsync(int piwigoServerId, CancellationToken ct);
} }

View File

@ -1,13 +1,15 @@
using Microsoft.EntityFrameworkCore; using Flurl.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Piwigo.Client; using Piwigo.Client;
using Piwigo.Client.Albums;
using Piwigo.Client.Images; using Piwigo.Client.Images;
using PiwigoDirectorySync.Infrastructure; using PiwigoDirectorySync.Infrastructure;
using PiwigoDirectorySync.Persistence; using PiwigoDirectorySync.Persistence;
namespace PiwigoDirectorySync.Services; namespace PiwigoDirectorySync.Services;
public class ImageSynchronizer : IImageSynchronizer internal class ImageSynchronizer : IImageSynchronizer
{ {
private readonly ILogger<ImageSynchronizer> _logger; private readonly ILogger<ImageSynchronizer> _logger;
private readonly PersistenceContext _persistenceContext; private readonly PersistenceContext _persistenceContext;
@ -20,7 +22,34 @@ public class ImageSynchronizer : IImageSynchronizer
_persistenceContext = persistenceContext; _persistenceContext = persistenceContext;
} }
public async Task SynchronizeImages(int piwigoServerId, CancellationToken ct) public async Task DownloadImagesAsync(int piwigoServerId, CancellationToken ct)
{
var piwigoServer = await _persistenceContext.PiwigoServers.FindAsync(new object?[] { piwigoServerId }, ct);
if (piwigoServer is null)
{
_logger.LogError("Could not sync images with piwigo server {PiwigoServerId}", piwigoServerId);
return;
}
_logger.LogInformation("Downloading missing images of piwigo server {PiwigoServerName} using base path {PiwigoServerRootDirectory}", piwigoServer.Name,
piwigoServer.RootDirectory);
var piwigoClient = await _piwigoClientFactory.GetPiwigoClientAsync(piwigoServer, ct);
var albumIdsToDownload = await _persistenceContext.PiwigoAlbums.Where(a => a.ServerAlbumId.HasValue).Select(a => a.ServerAlbumId!.Value).Distinct().ToListAsync(ct);
foreach (var albumId in albumIdsToDownload)
{
var albumInfos = await piwigoClient.Album.GetListAsync(albumId, false, false, ThumbnailSize.Thumb, ct);
var albumInfo = albumInfos.First();
_logger.LogInformation("Starting downloads for album {AlbumInfoName}", albumInfo.Name);
await DownloadImagesForAlbumAsync(piwigoClient, piwigoServer, albumId, albumInfo, ct);
}
}
public async Task SynchronizeImagesAsync(int piwigoServerId, CancellationToken ct)
{ {
var piwigoServer = await _persistenceContext.PiwigoServers.FindAsync(new object?[] { piwigoServerId }, ct); var piwigoServer = await _persistenceContext.PiwigoServers.FindAsync(new object?[] { piwigoServerId }, ct);
if (piwigoServer is null) if (piwigoServer is null)
@ -39,6 +68,75 @@ public class ImageSynchronizer : IImageSynchronizer
await UploadChangedImagesToServerAsync(piwigoClient, piwigoServer, ct); await UploadChangedImagesToServerAsync(piwigoClient, piwigoServer, ct);
} }
private async Task DownloadImagesForAlbumAsync(IPiwigoClient piwigoClient, ServerEntity piwigoServer, int albumId, Album albumInfo, CancellationToken ct)
{
if (albumInfo.NbImages is null or <= 0)
{
_logger.LogInformation("No images to download for empty album {AlbumId} / {AlbumInfoName}", albumId, albumInfo.Name);
return;
}
const int pageSize = 100;
var numberOfPages = albumInfo.NbImages.Value / pageSize + 1;
var currentPage = 0;
while (currentPage < numberOfPages)
{
var imagePagingInfo = new ImagePagingInfo(currentPage, pageSize, albumInfo.NbImages.Value);
var images = await piwigoClient.Image.GetImagesAsync(albumId, false, imagePagingInfo, ImageFilter.Empty, ImageOrder.Name, ct);
foreach (var image in images.Images)
{
await DownloadImageAsync(piwigoServer, albumInfo, image, ct);
}
currentPage++;
}
}
private async Task DownloadImageAsync(ServerEntity piwigoServer, Album albumInfo, Image image, CancellationToken ct)
{
var localAlbum = await _persistenceContext.PiwigoAlbums.FindByServerIdAsync(piwigoServer.Id, albumInfo.Id, ct);
if (localAlbum is null)
{
_logger.LogWarning("Could not add image {ImageId} / {ImageFile}: album with server id {AlbumInfoId} / {AlbumInfoName} not found", image.Id, image.File, albumInfo.Id,
albumInfo.Name);
return;
}
var localImage = await GetOrAddImageFromServerAsync(localAlbum, image, ct);
var fileInfo = new FileInfo(Path.Combine(piwigoServer.RootDirectory, localImage.FilePath));
if (fileInfo.Exists)
{
_logger.LogWarning("Tried to download image {ImageFile} but it already exists", image.File);
return;
}
await image.ElementUrl.DownloadFileAsync(fileInfo.Directory!.FullName, fileInfo.Name, cancellationToken: ct);
localImage.Md5Sum = await FilesystemHelpers.CalculateMd5SumAsync(fileInfo.FullName, ct);
await _persistenceContext.SaveChangesAsync(ct);
}
private async Task<ImageEntity> GetOrAddImageFromServerAsync(AlbumEntity album, Image image, CancellationToken ct)
{
var imageEntity = await _persistenceContext.PiwigoImages.Where(i => i.AlbumId == album.Id && i.ServerImageId == image.Id).FirstOrDefaultAsync(ct);
if (imageEntity is null)
{
imageEntity = new ImageEntity
{
AlbumId = album.Id,
Album = album,
FilePath = Path.Combine(album.Path, image.File!),
UploadRequired = false,
DeleteRequired = false
};
_persistenceContext.PiwigoImages.Add(imageEntity);
}
return imageEntity;
}
private async Task UploadChangedImagesToServerAsync(IPiwigoClient piwigoClient, ServerEntity piwigoServer, CancellationToken ct) private async Task UploadChangedImagesToServerAsync(IPiwigoClient piwigoClient, ServerEntity piwigoServer, CancellationToken ct)
{ {
var imagesToUpload = await _persistenceContext.PiwigoImages.Include(i => i.Album) var imagesToUpload = await _persistenceContext.PiwigoImages.Include(i => i.Album)

View File

@ -1,6 +1,6 @@
namespace PiwigoDirectorySync; namespace PiwigoDirectorySync;
public class Settings internal class Settings
{ {
public string DbProvider { get; set; } = null!; public string DbProvider { get; set; } = null!;
public string ImageRootDirectory { get; set; } = null!; public string ImageRootDirectory { get; set; } = null!;