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
}