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/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/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
);
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
}