diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b29be29..7412df8 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "dotnet-ef": { - "version": "7.0.3", + "version": "7.0.10", "commands": [ "dotnet-ef" ] diff --git a/PiwigoDirectorySync.sln.DotSettings b/PiwigoDirectorySync.sln.DotSettings index fe19aae..f2694d2 100644 --- a/PiwigoDirectorySync.sln.DotSettings +++ b/PiwigoDirectorySync.sln.DotSettings @@ -72,6 +72,7 @@ True True True + True diff --git a/PiwigoDirectorySync/AppSettings.cs b/PiwigoDirectorySync/AppSettings.cs new file mode 100644 index 0000000..130b9b3 --- /dev/null +++ b/PiwigoDirectorySync/AppSettings.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; + +namespace PiwigoDirectorySync; + +public static class AppSettings +{ + public static IConfigurationRoot Config { get; } = new ConfigurationBuilder().SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", true) + .AddJsonFile(Path.Combine(Environment.CurrentDirectory, "appsettings.json"), true) + .AddJsonFile("/etc/PiwigoDirectorySync/appsettings.json", true) + .AddEnvironmentVariables() + .Build(); + + public static Settings Settings { get; } = Config.GetSection("settings").Get() ?? throw new InvalidOperationException("Could not parse settings"); + + public static string ConnectionString => Config.GetConnectionString(Settings.DbProvider) ?? throw new InvalidOperationException($"Could not find connection string for provider {Settings.DbProvider}"); +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Commands/ScanCommand.cs b/PiwigoDirectorySync/Commands/ScanCommand.cs new file mode 100644 index 0000000..d0865e8 --- /dev/null +++ b/PiwigoDirectorySync/Commands/ScanCommand.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using System.Threading.Channels; +using NLog; +using PiwigoDirectorySync.Service; +using Spectre.Console.Cli; + +namespace PiwigoDirectorySync.Commands; + +public class ScanCommand : AsyncCommand +{ + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + public override async Task ExecuteAsync(CommandContext context, ScanSettings settings) + { + Logger.Info("Starting scanner and remover"); + var stopWatch = Stopwatch.StartNew(); + + var cancellationTokenSource = new CancellationTokenSource(); + + var fileQueue = Channel.CreateUnbounded(); + + var indexer = new FileIndexer(fileQueue); + var indexerTask = indexer.StartProcessingAsync(cancellationTokenSource.Token); + + var scanner = new FileScanner(fileQueue); + await scanner.ScanAsync(cancellationTokenSource.Token); + + fileQueue.Writer.Complete(); + + await Task.WhenAll(fileQueue.Reader.Completion, indexerTask); + + stopWatch.Stop(); + Logger.Info($"Processed {indexer.TotalFilesScanned} image files in {stopWatch.Elapsed.TotalSeconds} seconds"); + + return 0; + } +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Commands/ScanSettings.cs b/PiwigoDirectorySync/Commands/ScanSettings.cs new file mode 100644 index 0000000..bf7c7e0 --- /dev/null +++ b/PiwigoDirectorySync/Commands/ScanSettings.cs @@ -0,0 +1,7 @@ +using Spectre.Console.Cli; + +namespace PiwigoDirectorySync.Commands; + +public class ScanSettings : CommandSettings +{ +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Persistence/AlbumEntity.cs b/PiwigoDirectorySync/Persistence/AlbumEntity.cs new file mode 100644 index 0000000..28ceb69 --- /dev/null +++ b/PiwigoDirectorySync/Persistence/AlbumEntity.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace PiwigoDirectorySync.Persistence; + +public class AlbumEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public int? ParentId { get; set; } + public AlbumEntity? Parent { get; set; } + public required int ServerId { get; set; } + public PiwigoServerEntity Server { get; set; } = null!; + public required string Name { get; set; } + public required string DirectoryName { get; set; } + public string FullDirectory => Parent is not null ? $"{Parent.FullDirectory}{Path.DirectorySeparatorChar}{DirectoryName}" : DirectoryName; + + public int? PiwigoAlbumId { get; set; } +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Persistence/ImageEntity.cs b/PiwigoDirectorySync/Persistence/ImageEntity.cs new file mode 100644 index 0000000..201ae8b --- /dev/null +++ b/PiwigoDirectorySync/Persistence/ImageEntity.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace PiwigoDirectorySync.Persistence; + +public class ImageEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + public required string Filename { get; set; } + public DateTime LastChange { get; set; } + public string? Md5Sum { get; set; } + public required int AlbumId { get; set; } + public AlbumEntity Album { get; set; } = null!; + public int ServerImageId { get; set; } + public bool UploadRequired { get; set; } + public bool DeleteRequired { get; set; } +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Persistence/PersistenceContext.cs b/PiwigoDirectorySync/Persistence/PersistenceContext.cs new file mode 100644 index 0000000..58672c8 --- /dev/null +++ b/PiwigoDirectorySync/Persistence/PersistenceContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace PiwigoDirectorySync.Persistence; + +public class PersistenceContext : DbContext +{ + public DbSet PiwigoServers { get; set; } + public DbSet PiwigoAlbums { get; set; } + public DbSet PiwigoImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + switch (AppSettings.Settings.DbProvider) + { + case "Sqlite": + options.UseSqlite(AppSettings.ConnectionString); + break; + case "InMemory": + options.UseInMemoryDatabase(AppSettings.ConnectionString); + break; + default: + throw new InvalidOperationException($"DbProvider {AppSettings.Settings.DbProvider} is not supported"); + } + } +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Persistence/PiwigoServerEntity.cs b/PiwigoDirectorySync/Persistence/PiwigoServerEntity.cs new file mode 100644 index 0000000..c51668e --- /dev/null +++ b/PiwigoDirectorySync/Persistence/PiwigoServerEntity.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace PiwigoDirectorySync.Persistence; + +[Index(nameof(Name), IsUnique = true)] +public class PiwigoServerEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public required string Name { get; set; } + public required string Url { get; set; } + public required string Username { get; set; } + public required string Password { get; set; } + public required string RootDirectory { get; set; } +} \ No newline at end of file diff --git a/PiwigoDirectorySync/PiwigoDirectorySync.csproj b/PiwigoDirectorySync/PiwigoDirectorySync.csproj index 0619d80..0968c8f 100644 --- a/PiwigoDirectorySync/PiwigoDirectorySync.csproj +++ b/PiwigoDirectorySync/PiwigoDirectorySync.csproj @@ -1,17 +1,46 @@ - + Exe net7.0 enable enable + false + true Linux + c68c0447-8c7d-4e88-bcc6-96a9853828c7 - - .dockerignore - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + .dockerignore + + + + + + Always + + + + diff --git a/PiwigoDirectorySync/Program.cs b/PiwigoDirectorySync/Program.cs index 2d6e81e..f62bf05 100644 --- a/PiwigoDirectorySync/Program.cs +++ b/PiwigoDirectorySync/Program.cs @@ -1,3 +1,15 @@ -// See https://aka.ms/new-console-template for more information +using NLog; +using NLog.Extensions.Logging; +using PiwigoDirectorySync; +using PiwigoDirectorySync.Commands; +using Spectre.Console.Cli; -Console.WriteLine("Hello, World!"); \ No newline at end of file +LogManager.Configuration = new NLogLoggingConfiguration(AppSettings.Config.GetSection("NLog")); +var logFactory = LogManager.Setup().LogFactory; +var logger = logFactory.GetCurrentClassLogger(); + +logger.Info("Starting command app"); +var app = new CommandApp(); +app.Configure(c => { c.AddCommand("scan"); }); + +return app.Run(args); \ No newline at end of file diff --git a/PiwigoDirectorySync/Service/FileIndexer.cs b/PiwigoDirectorySync/Service/FileIndexer.cs new file mode 100644 index 0000000..37af38d --- /dev/null +++ b/PiwigoDirectorySync/Service/FileIndexer.cs @@ -0,0 +1,17 @@ +using System.Threading.Channels; + +namespace PiwigoDirectorySync.Service; + +public class FileIndexer +{ + private readonly Channel _fileQueue; + + public FileIndexer(Channel fileQueue) + { + _fileQueue = fileQueue ?? throw new ArgumentNullException(nameof(fileQueue)); + } + + public int TotalFilesScanned { get; } + + public Task StartProcessingAsync(CancellationToken token) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Service/FileScanner.cs b/PiwigoDirectorySync/Service/FileScanner.cs new file mode 100644 index 0000000..80bc9dd --- /dev/null +++ b/PiwigoDirectorySync/Service/FileScanner.cs @@ -0,0 +1,15 @@ +using System.Threading.Channels; + +namespace PiwigoDirectorySync.Service; + +public class FileScanner +{ + private readonly Channel _fileQueue; + + public FileScanner(Channel fileQueue) + { + _fileQueue = fileQueue ?? throw new ArgumentNullException(nameof(fileQueue)); + } + + public async Task ScanAsync(CancellationToken token) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/PiwigoDirectorySync/Settings.cs b/PiwigoDirectorySync/Settings.cs new file mode 100644 index 0000000..2134014 --- /dev/null +++ b/PiwigoDirectorySync/Settings.cs @@ -0,0 +1,9 @@ +namespace PiwigoDirectorySync; + +public class Settings +{ + public string DbProvider { get; set; } = null!; + public string ImageRootDirectory { get; set; } = null!; + + public bool HasErrors => string.IsNullOrEmpty(DbProvider) || string.IsNullOrEmpty(ImageRootDirectory); +} \ No newline at end of file diff --git a/PiwigoDirectorySync/addMigration.ps1 b/PiwigoDirectorySync/addMigration.ps1 new file mode 100644 index 0000000..5fd66a3 --- /dev/null +++ b/PiwigoDirectorySync/addMigration.ps1 @@ -0,0 +1,5 @@ +$comment=$args[0] + +write-host "adding migration for Sqlite" +dotnet ef migrations add --project PiwigoDirectorySync.csproj --startup-project PiwigoDirectorySync.csproj --context PiwigoDirectorySync.Persistence.PersistenceContext "$comment" --output-dir Migrations -- --DbProvider Sqlite +#dotnet ef migrations add --project PiwigoDirectorySync/PiwigoDirectorySync.csproj --startup-project PiwigoDirectorySync/PiwigoDirectorySync.csproj --context PiwigoDirectorySync.Persistence.Persistence.PersistenceContext "$comment" --output-dir Migrations -- --DbProvider Sqlite diff --git a/PiwigoDirectorySync/appsettings.json b/PiwigoDirectorySync/appsettings.json new file mode 100644 index 0000000..3393ae2 --- /dev/null +++ b/PiwigoDirectorySync/appsettings.json @@ -0,0 +1,68 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "ConnectionStrings": { + "Sqlite": "Data Source=piwigoSync.db", + "InMemory": "InMemorySyncDb" + }, + "Settings": { + "DbProvider": "Sqlite", + "ImageRootDirectory": ".\\" + }, + "NLog": { + "autoReload": true, + "throwConfigExceptions": true, + "default-wrapper": { + "type": "AsyncWrapper", + "overflowAction": "Block" + }, + "targets": { + "cli-console": { + "type": "ColoredConsole", + "layout": "${longdate} | ${uppercase:${level}} | ${logger} | ${message} ${exception:format=tostring}", + "rowHighlightingRules": [ + { + "condition": "level == LogLevel.Trace", + "foregroundColor": "DarkGray" + }, + { + "condition": "level == LogLevel.Debug", + "foregroundColor": "White" + }, + { + "condition": "level == LogLevel.Info", + "foregroundColor": "DarkGreen" + }, + { + "condition": "level == LogLevel.Warn", + "foregroundColor": "Yellow" + }, + { + "condition": "level == LogLevel.Error", + "foregroundColor": "DarkMagenta" + }, + { + "condition": "level == LogLevel.Fatal", + "foregroundColor": "DarkRed" + } + ] + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Info", + "writeTo": "cli-console" + }, + { + "logger": "Microsoft.*", + "maxLevel": "Info" + } + ] + } +} \ No newline at end of file diff --git a/README.md b/README.md index f8d21e1..b620a34 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ # PiwigoDirectorySync +This application synchronizes the local directory structure with piwigo servers. +Each directory level gets created as album or sub album in piwigo. + +## Building / tooling + +### Restoring dotnet tools + +In the root path you may just run the command ``dotnet tool restore`` to install all dotnet tools and extensions used in this project. + +### Handle development secrets + +To make sure no local development db configuration gets committed, we use the dotnet-user-secrets tool. + +When you configure your environment for the first time and you do not like the default settings, use the following commands to set the secrets: + +`` +cd PiwigoDirectorySync +dotnet user-secrets set "DbProvider" "MariaDb" +dotnet user-secrets set "ConnectionStrings:MariaDb" "Server=localhost;User Id=photowfdev;Password=password123;Database=photowfdev" +`` + +You'll find your secrets under `~/.microsoft/usersecrets/c68c0447-8c7d-4e88-bcc6-96a9853828c7/secrets.json` + +### Docker + +Build the application and docker image for hosting using docker. + +TODO: add some docker build details + +### Publish + +Build the application for manual installation using publish. + +TODO: add some publish details \ No newline at end of file