From ed456c1ee84a5ddba5b61e78ad0a913926a4505f Mon Sep 17 00:00:00 2001 From: savedo Date: Mon, 18 May 2026 16:01:06 +0200 Subject: [PATCH 1/2] feat: add event handler support for file uploads - Introduced `IFtpServerEventHandler` to handle server events. - Updated `StorCommand` to invoke `OnFileUploadedAsync` on file upload. - Enhanced dependency injection to register event handlers. - Modified `README` and tests to include event handler configuration. --- .../Abstractions/FtpCommandContext.cs | 4 +++- .../Abstractions/IFtpServerEventHandler.cs | 15 +++++++++++++++ FmiSrl.FtpServer.Server/Commands/StorCommand.cs | 15 +++++++++++++++ .../DependencyInjection/FtpServerBuilder.cs | 6 ++++++ .../DependencyInjection/IFtpServerBuilder.cs | 7 +++++++ FmiSrl.FtpServer.Server/FtpServer.cs | 6 +++++- .../FtpServerTests.cs | 8 ++++---- README.md | 2 ++ 8 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 FmiSrl.FtpServer.Server/Abstractions/IFtpServerEventHandler.cs diff --git a/FmiSrl.FtpServer.Server/Abstractions/FtpCommandContext.cs b/FmiSrl.FtpServer.Server/Abstractions/FtpCommandContext.cs index 8e2dd56..d1c2c2b 100644 --- a/FmiSrl.FtpServer.Server/Abstractions/FtpCommandContext.cs +++ b/FmiSrl.FtpServer.Server/Abstractions/FtpCommandContext.cs @@ -12,6 +12,7 @@ namespace FmiSrl.FtpServer.Server.Abstractions; /// The authentication provider to use. /// The server configuration options. /// The logger to use for recording command execution details. +/// The collection of registered FTP server event handlers. public record FtpCommandContext( IFtpSession Session, string Verb, @@ -19,7 +20,8 @@ public record FtpCommandContext( IFileSystemProvider FileSystem, IAuthenticationProvider Authenticator, FtpServerConfigurationOptions Configuration, - ILogger Logger + ILogger Logger, + IEnumerable? EventHandlers = null ) { /// diff --git a/FmiSrl.FtpServer.Server/Abstractions/IFtpServerEventHandler.cs b/FmiSrl.FtpServer.Server/Abstractions/IFtpServerEventHandler.cs new file mode 100644 index 0000000..7482ab3 --- /dev/null +++ b/FmiSrl.FtpServer.Server/Abstractions/IFtpServerEventHandler.cs @@ -0,0 +1,15 @@ +namespace FmiSrl.FtpServer.Server.Abstractions; + +/// +/// Defines event handlers for FTP server events. +/// +public interface IFtpServerEventHandler +{ + /// + /// Invoked when a file is successfully uploaded to the server. + /// + /// The FTP session that uploaded the file. + /// The full path of the uploaded file. + /// A task that represents the asynchronous operation. + Task OnFileUploadedAsync(IFtpSession session, string targetFile); +} diff --git a/FmiSrl.FtpServer.Server/Commands/StorCommand.cs b/FmiSrl.FtpServer.Server/Commands/StorCommand.cs index e104a48..9fa0788 100644 --- a/FmiSrl.FtpServer.Server/Commands/StorCommand.cs +++ b/FmiSrl.FtpServer.Server/Commands/StorCommand.cs @@ -77,5 +77,20 @@ private static async Task ReceiveFileAsync(FtpCommandContext context, string tar } await context.Session.SendResponseAsync(226, "Transfer complete."); + + if (context.EventHandlers != null) + { + foreach (var handler in context.EventHandlers) + { + try + { + await handler.OnFileUploadedAsync(context.Session, targetFile); + } + catch (Exception ex) + { + context.Logger.LogError(ex, "Error executing event handler {HandlerType} for file {TargetFile}", handler.GetType().Name, targetFile); + } + } + } } } diff --git a/FmiSrl.FtpServer.Server/DependencyInjection/FtpServerBuilder.cs b/FmiSrl.FtpServer.Server/DependencyInjection/FtpServerBuilder.cs index dc345ce..e5d528a 100644 --- a/FmiSrl.FtpServer.Server/DependencyInjection/FtpServerBuilder.cs +++ b/FmiSrl.FtpServer.Server/DependencyInjection/FtpServerBuilder.cs @@ -12,4 +12,10 @@ public IFtpServerBuilder AddMiddleware() where TMiddleware : class, Services.AddTransient(); return this; } + + public IFtpServerBuilder AddEventHandler() where TEventHandler : class, IFtpServerEventHandler + { + Services.AddTransient(); + return this; + } } diff --git a/FmiSrl.FtpServer.Server/DependencyInjection/IFtpServerBuilder.cs b/FmiSrl.FtpServer.Server/DependencyInjection/IFtpServerBuilder.cs index a85fdfb..68a3074 100644 --- a/FmiSrl.FtpServer.Server/DependencyInjection/IFtpServerBuilder.cs +++ b/FmiSrl.FtpServer.Server/DependencyInjection/IFtpServerBuilder.cs @@ -20,4 +20,11 @@ public interface IFtpServerBuilder /// The type of the middleware to add. /// The instance. IFtpServerBuilder AddMiddleware() where TMiddleware : class, IFtpCommandMiddleware; + + /// + /// Adds an event handler to the FTP server. + /// + /// The type of the event handler to add. + /// The instance. + IFtpServerBuilder AddEventHandler() where TEventHandler : class, IFtpServerEventHandler; } diff --git a/FmiSrl.FtpServer.Server/FtpServer.cs b/FmiSrl.FtpServer.Server/FtpServer.cs index 2dc5e3e..265656a 100644 --- a/FmiSrl.FtpServer.Server/FtpServer.cs +++ b/FmiSrl.FtpServer.Server/FtpServer.cs @@ -23,6 +23,7 @@ public class FtpServer( IFileSystemProvider fileSystemProvider, IAuthenticationProvider authenticationProvider, IEnumerable middlewares, + IEnumerable eventHandlers, IOptions configurationOptions, ILogger? logger = null ) @@ -35,6 +36,8 @@ public class FtpServer( private readonly IAuthenticationProvider _authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); + private readonly IEnumerable _eventHandlers = eventHandlers; + private readonly ILogger _logger = logger ?? NullLogger.Instance; private readonly FtpCommandHandler _commandHandler = new(middlewares); private readonly Dictionary _sessions = []; @@ -177,7 +180,8 @@ private async Task ExecuteCommandAsync(NetClient client, FtpSession session, str _fileSystemProvider, _authenticationProvider, _configurationOptions, - _logger); + _logger, + _eventHandlers); await _commandHandler.HandleCommandAsync(context); diff --git a/FmiSrl.FtpServer.Tests.Integration/FtpServerTests.cs b/FmiSrl.FtpServer.Tests.Integration/FtpServerTests.cs index f53aa35..3487ae8 100644 --- a/FmiSrl.FtpServer.Tests.Integration/FtpServerTests.cs +++ b/FmiSrl.FtpServer.Tests.Integration/FtpServerTests.cs @@ -43,7 +43,7 @@ public FtpServerTests() ServerName = "TestServer" }); - _server = new FmiSrl.FtpServer.Server.FtpServer(fileSystem, authenticator, Enumerable.Empty(), serverOptions, NullLogger.Instance); + _server = new FmiSrl.FtpServer.Server.FtpServer(fileSystem, authenticator, Enumerable.Empty(), Enumerable.Empty(), serverOptions, NullLogger.Instance); } public async ValueTask DisposeAsync() @@ -67,14 +67,14 @@ public async Task When_server_starts_should_be_able_to_login_and_list_files() { // Arrange await _server.StartAsync(); - + // Create a test file for the user var userRoot = Path.Combine(_rootPath, "test"); Directory.CreateDirectory(userRoot); File.WriteAllText(Path.Combine(userRoot, "test.txt"), "Hello World"); using var client = new AsyncFtpClient("127.0.0.1", "test", "password", _port); - + // Act await client.Connect(); var currentDir = await client.GetWorkingDirectory(); @@ -116,7 +116,7 @@ public async Task When_uploading_file_should_save_to_disk() // Assert Assert.Equal(FtpStatus.Success, status); - + var filePath = Path.Combine(_rootPath, "test", "uploaded.txt"); Assert.True(File.Exists(filePath)); Assert.Equal(testContent, await File.ReadAllTextAsync(filePath)); diff --git a/README.md b/README.md index 4c19206..b28a82a 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ var authOptions = Options.Create(new SimpleAuthenticationProviderOptions var ftpServer = new FtpServer( new PhysicalFileSystemProvider(fsOptions), new SimpleAuthenticationProvider(authOptions), + Enumerable.Empty(), + Enumerable.Empty(), serverOptions, NullLogger.Instance ); From 7fbaf40506d8bdeea8b577730ff72654304c96bf Mon Sep 17 00:00:00 2001 From: savedo Date: Mon, 18 May 2026 16:01:31 +0200 Subject: [PATCH 2/2] chore: bump `next-version` to 1.3.0 and update .NET SDK to 10.0.203 --- GitVersion.yml | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index 3f148fe..16b4bcf 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -next-version: 1.2.0 +next-version: 1.3.0 branches: main: regex: ^master$|^main$ diff --git a/global.json b/global.json index 43a2c0d..95365e3 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.101", + "version": "10.0.203", "rollForward": "latestMajor", "allowPrerelease": false }