diff --git a/FmiSrl.FtpServer.Server/Abstractions/IFileSystemProvider.cs b/FmiSrl.FtpServer.Server/Abstractions/IFileSystemProvider.cs index 96baebd..67add4f 100644 --- a/FmiSrl.FtpServer.Server/Abstractions/IFileSystemProvider.cs +++ b/FmiSrl.FtpServer.Server/Abstractions/IFileSystemProvider.cs @@ -69,6 +69,14 @@ public interface IFileSystemProvider /// A task that represents the asynchronous operation. The task result contains true if the directory exists; otherwise, false. Task DirectoryExistsAsync(FtpAuthenticationContext authContext, string path); + /// + /// Gets a single file system entry for the specified path. + /// + /// The authentication context for the operation. + /// The path to get the entry for. + /// A task that represents the asynchronous operation. The task result contains the or null if not found. + Task GetEntryAsync(FtpAuthenticationContext authContext, string path); + /// /// Renames a file or directory. /// diff --git a/FmiSrl.FtpServer.Server/Commands/RetrCommand.cs b/FmiSrl.FtpServer.Server/Commands/RetrCommand.cs index 8535eb4..4ea3b7f 100644 --- a/FmiSrl.FtpServer.Server/Commands/RetrCommand.cs +++ b/FmiSrl.FtpServer.Server/Commands/RetrCommand.cs @@ -36,8 +36,9 @@ public async Task ExecuteAsync(FtpCommandContext context) private static async Task ProcessRetrieveAsync(FtpCommandContext context) { var targetFile = PathHelper.NormalizePath(context.Session.CurrentDirectory, context.Arguments); + var entry = await context.FileSystem.GetEntryAsync(context.AuthContext, targetFile); - if (!await context.FileSystem.FileExistsAsync(context.AuthContext, targetFile)) + if (entry == null || entry.IsDirectory) { await context.Session.SendResponseAsync(550, "File not found."); return; @@ -47,7 +48,7 @@ private static async Task ProcessRetrieveAsync(FtpCommandContext context) try { - await TransferFileAsync(context, targetFile); + await TransferFileAsync(context, targetFile, entry); } catch (Exception ex) { @@ -61,7 +62,7 @@ private static async Task ProcessRetrieveAsync(FtpCommandContext context) } } - private static async Task TransferFileAsync(FtpCommandContext context, string targetFile) + private static async Task TransferFileAsync(FtpCommandContext context, string targetFile, FileSystemEntry entry) { var dataStream = await context.Session.DataConnection!.GetStreamAsync(); @@ -70,7 +71,7 @@ private static async Task TransferFileAsync(FtpCommandContext context, string ta context.Logger.LogInformation( "Starting transfer of {TargetFile} ({Length} bytes)...", targetFile, - fileStream.Length); + entry.Size); await fileStream.CopyToAsync(dataStream); context.Logger.LogInformation("Finished copying {TargetFile} to data stream.", targetFile); } diff --git a/FmiSrl.FtpServer.Server/Commands/SizeCommand.cs b/FmiSrl.FtpServer.Server/Commands/SizeCommand.cs index 6467412..0fa49f2 100644 --- a/FmiSrl.FtpServer.Server/Commands/SizeCommand.cs +++ b/FmiSrl.FtpServer.Server/Commands/SizeCommand.cs @@ -42,21 +42,14 @@ private static async Task ProcessSizeRequestAsync(FtpCommandContext context) private static async Task TryGetFileSizeAsync(FtpCommandContext context, string targetFile) { - var directory = Path.GetDirectoryName(targetFile)?.Replace('\\', '/') ?? "/"; - if (directory == string.Empty) - { - directory = "/"; - } - - var entries = await context.FileSystem.GetEntriesAsync(context.AuthContext, directory); - var file = entries.FirstOrDefault(e => !e.IsDirectory && e.Name == Path.GetFileName(targetFile)); + var entry = await context.FileSystem.GetEntryAsync(context.AuthContext, targetFile); - if (file != null) + if (entry != null && !entry.IsDirectory) { - await context.Session.SendResponseAsync(213, file.Size.ToString(CultureInfo.InvariantCulture)); + await context.Session.SendResponseAsync(213, entry.Size.ToString(CultureInfo.InvariantCulture)); return; } - await context.Session.SendResponseAsync(550, "File not found."); + await context.Session.SendResponseAsync(550, "File not found or is a directory."); } } diff --git a/FmiSrl.FtpServer.Server/Services/PhysicalFileSystemProvider.cs b/FmiSrl.FtpServer.Server/Services/PhysicalFileSystemProvider.cs index 2afb501..edaa863 100644 --- a/FmiSrl.FtpServer.Server/Services/PhysicalFileSystemProvider.cs +++ b/FmiSrl.FtpServer.Server/Services/PhysicalFileSystemProvider.cs @@ -99,6 +99,25 @@ public Task FileExistsAsync(FtpAuthenticationContext authContext, string p public Task DirectoryExistsAsync(FtpAuthenticationContext authContext, string path) => Task.FromResult(Directory.Exists(GetFullPath(authContext, path))); + /// + public Task GetEntryAsync(FtpAuthenticationContext authContext, string path) + { + var fullPath = GetFullPath(authContext, path); + if (File.Exists(fullPath)) + { + var fi = new FileInfo(fullPath); + return Task.FromResult(new FileSystemEntry(fi.Name, fi.Length, fi.LastWriteTime, false)); + } + + if (Directory.Exists(fullPath)) + { + var di = new DirectoryInfo(fullPath); + return Task.FromResult(new FileSystemEntry(di.Name, 0, di.LastWriteTime, true)); + } + + return Task.FromResult(null); + } + /// public Task RenameAsync(FtpAuthenticationContext authContext, string oldPath, string newPath) { diff --git a/FmiSrl.FtpServer.Tests.Unit/Commands/RetrCommandTests.cs b/FmiSrl.FtpServer.Tests.Unit/Commands/RetrCommandTests.cs index e7a0ae8..174da0a 100644 --- a/FmiSrl.FtpServer.Tests.Unit/Commands/RetrCommandTests.cs +++ b/FmiSrl.FtpServer.Tests.Unit/Commands/RetrCommandTests.cs @@ -50,7 +50,7 @@ public async Task ExecuteAsync_WhenFileNotFound_ShouldReturn550() { var sut = new RetrCommand(); _session.DataConnection.Returns(Substitute.For()); - _fileSystem.FileExistsAsync(Arg.Any(), "/file.txt").Returns(false); + _fileSystem.GetEntryAsync(Arg.Any(), "/file.txt").Returns((FileSystemEntry?)null); await sut.ExecuteAsync(_context); await _session.Received().SendResponseAsync(550, Arg.Any()); } @@ -62,7 +62,8 @@ public async Task ExecuteAsync_WhenSuccess_ShouldReturn226() var dataConn = Substitute.For(); dataConn.GetStreamAsync().Returns(new MemoryStream()); _session.DataConnection.Returns(dataConn); - _fileSystem.FileExistsAsync(Arg.Any(), "/file.txt").Returns(true); + _fileSystem.GetEntryAsync(Arg.Any(), "/file.txt") + .Returns(new FileSystemEntry("file.txt", 10, DateTime.Now, false)); _fileSystem.OpenReadAsync(Arg.Any(), "/file.txt").Returns(new MemoryStream(new byte[10])); await sut.ExecuteAsync(_context); @@ -78,7 +79,8 @@ public async Task ExecuteAsync_WhenException_ShouldReturn550() var sut = new RetrCommand(); var dataConn = Substitute.For(); _session.DataConnection.Returns(dataConn); - _fileSystem.FileExistsAsync(Arg.Any(), "/file.txt").Returns(true); + _fileSystem.GetEntryAsync(Arg.Any(), "/file.txt") + .Returns(new FileSystemEntry("file.txt", 10, DateTime.Now, false)); dataConn.GetStreamAsync().Throws(new InvalidOperationException()); await sut.ExecuteAsync(_context); diff --git a/FmiSrl.FtpServer.Tests.Unit/Commands/SizeCommandTests.cs b/FmiSrl.FtpServer.Tests.Unit/Commands/SizeCommandTests.cs index 9e7e55a..b17f9fd 100644 --- a/FmiSrl.FtpServer.Tests.Unit/Commands/SizeCommandTests.cs +++ b/FmiSrl.FtpServer.Tests.Unit/Commands/SizeCommandTests.cs @@ -39,8 +39,8 @@ public async Task ExecuteAsync_WhenArgsEmpty_ShouldReturn501() public async Task ExecuteAsync_WhenFileExists_ShouldReturnSize() { var sut = new SizeCommand(); - var entries = new[] { new FileSystemEntry("file.txt", 1234, DateTime.Now, false) }; - _fileSystem.GetEntriesAsync(Arg.Any(), "/").Returns(entries); + var entry = new FileSystemEntry("file.txt", 1234, DateTime.Now, false); + _fileSystem.GetEntryAsync(Arg.Any(), "/file.txt").Returns(entry); await sut.ExecuteAsync(_context); await _session.Received().SendResponseAsync(213, "1234"); } @@ -49,16 +49,16 @@ public async Task ExecuteAsync_WhenFileExists_ShouldReturnSize() public async Task ExecuteAsync_WhenFileNotFound_ShouldReturn550() { var sut = new SizeCommand(); - _fileSystem.GetEntriesAsync(Arg.Any(), "/").Returns(Array.Empty()); + _fileSystem.GetEntryAsync(Arg.Any(), "/file.txt").Returns((FileSystemEntry?)null); await sut.ExecuteAsync(_context); - await _session.Received().SendResponseAsync(550, "File not found."); + await _session.Received().SendResponseAsync(550, "File not found or is a directory."); } [Fact] public async Task ExecuteAsync_WhenException_ShouldReturn550() { var sut = new SizeCommand(); - _fileSystem.GetEntriesAsync(Arg.Any(), "/").Throws(new InvalidOperationException()); + _fileSystem.GetEntryAsync(Arg.Any(), "/file.txt").Throws(new InvalidOperationException()); await sut.ExecuteAsync(_context); await _session.Received().SendResponseAsync(550, "Error getting file size."); } diff --git a/FmiSrl.FtpServer.Tests.Unit/DependencyInjection/DependencyInjectionTests.cs b/FmiSrl.FtpServer.Tests.Unit/DependencyInjection/DependencyInjectionTests.cs index 491a45e..00937dd 100644 --- a/FmiSrl.FtpServer.Tests.Unit/DependencyInjection/DependencyInjectionTests.cs +++ b/FmiSrl.FtpServer.Tests.Unit/DependencyInjection/DependencyInjectionTests.cs @@ -65,6 +65,7 @@ private sealed class DummyFileSystemProvider : IFileSystemProvider public Task DeleteDirectoryAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException(); public Task FileExistsAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException(); public Task DirectoryExistsAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException(); + public Task GetEntryAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException(); public Task RenameAsync(FtpAuthenticationContext authContext, string oldPath, string newPath) => throw new NotImplementedException(); } diff --git a/global.json b/global.json index 6b350c5..95365e3 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.202", + "version": "10.0.203", "rollForward": "latestMajor", "allowPrerelease": false }