Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions FmiSrl.FtpServer.Server/Abstractions/IFileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public interface IFileSystemProvider
/// <returns>A task that represents the asynchronous operation. The task result contains <c>true</c> if the directory exists; otherwise, <c>false</c>.</returns>
Task<bool> DirectoryExistsAsync(FtpAuthenticationContext authContext, string path);

/// <summary>
/// Gets a single file system entry for the specified path.
/// </summary>
/// <param name="authContext">The authentication context for the operation.</param>
/// <param name="path">The path to get the entry for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="FileSystemEntry"/> or <c>null</c> if not found.</returns>
Task<FileSystemEntry?> GetEntryAsync(FtpAuthenticationContext authContext, string path);

/// <summary>
/// Renames a file or directory.
/// </summary>
Expand Down
9 changes: 5 additions & 4 deletions FmiSrl.FtpServer.Server/Commands/RetrCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,7 +48,7 @@ private static async Task ProcessRetrieveAsync(FtpCommandContext context)

try
{
await TransferFileAsync(context, targetFile);
await TransferFileAsync(context, targetFile, entry);
}
catch (Exception ex)
{
Expand All @@ -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();

Expand All @@ -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);
}
Expand Down
15 changes: 4 additions & 11 deletions FmiSrl.FtpServer.Server/Commands/SizeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}
19 changes: 19 additions & 0 deletions FmiSrl.FtpServer.Server/Services/PhysicalFileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ public Task<bool> FileExistsAsync(FtpAuthenticationContext authContext, string p
public Task<bool> DirectoryExistsAsync(FtpAuthenticationContext authContext, string path) =>
Task.FromResult(Directory.Exists(GetFullPath(authContext, path)));

/// <inheritdoc/>
public Task<FileSystemEntry?> GetEntryAsync(FtpAuthenticationContext authContext, string path)
{
var fullPath = GetFullPath(authContext, path);
if (File.Exists(fullPath))
{
var fi = new FileInfo(fullPath);
return Task.FromResult<FileSystemEntry?>(new FileSystemEntry(fi.Name, fi.Length, fi.LastWriteTime, false));
}

if (Directory.Exists(fullPath))
{
var di = new DirectoryInfo(fullPath);
return Task.FromResult<FileSystemEntry?>(new FileSystemEntry(di.Name, 0, di.LastWriteTime, true));
}

return Task.FromResult<FileSystemEntry?>(null);
}

/// <inheritdoc/>
public Task RenameAsync(FtpAuthenticationContext authContext, string oldPath, string newPath)
{
Expand Down
8 changes: 5 additions & 3 deletions FmiSrl.FtpServer.Tests.Unit/Commands/RetrCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public async Task ExecuteAsync_WhenFileNotFound_ShouldReturn550()
{
var sut = new RetrCommand();
_session.DataConnection.Returns(Substitute.For<IFtpDataConnection>());
_fileSystem.FileExistsAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Returns(false);
_fileSystem.GetEntryAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Returns((FileSystemEntry?)null);
await sut.ExecuteAsync(_context);
await _session.Received().SendResponseAsync(550, Arg.Any<string>());
}
Expand All @@ -62,7 +62,8 @@ public async Task ExecuteAsync_WhenSuccess_ShouldReturn226()
var dataConn = Substitute.For<IFtpDataConnection>();
dataConn.GetStreamAsync().Returns(new MemoryStream());
_session.DataConnection.Returns(dataConn);
_fileSystem.FileExistsAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Returns(true);
_fileSystem.GetEntryAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt")
.Returns(new FileSystemEntry("file.txt", 10, DateTime.Now, false));
_fileSystem.OpenReadAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Returns(new MemoryStream(new byte[10]));

await sut.ExecuteAsync(_context);
Expand All @@ -78,7 +79,8 @@ public async Task ExecuteAsync_WhenException_ShouldReturn550()
var sut = new RetrCommand();
var dataConn = Substitute.For<IFtpDataConnection>();
_session.DataConnection.Returns(dataConn);
_fileSystem.FileExistsAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Returns(true);
_fileSystem.GetEntryAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt")
.Returns(new FileSystemEntry("file.txt", 10, DateTime.Now, false));
dataConn.GetStreamAsync().Throws(new InvalidOperationException());

await sut.ExecuteAsync(_context);
Expand Down
10 changes: 5 additions & 5 deletions FmiSrl.FtpServer.Tests.Unit/Commands/SizeCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FtpAuthenticationContext>(), "/").Returns(entries);
var entry = new FileSystemEntry("file.txt", 1234, DateTime.Now, false);
_fileSystem.GetEntryAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Returns(entry);
await sut.ExecuteAsync(_context);
await _session.Received().SendResponseAsync(213, "1234");
}
Expand All @@ -49,16 +49,16 @@ public async Task ExecuteAsync_WhenFileExists_ShouldReturnSize()
public async Task ExecuteAsync_WhenFileNotFound_ShouldReturn550()
{
var sut = new SizeCommand();
_fileSystem.GetEntriesAsync(Arg.Any<FtpAuthenticationContext>(), "/").Returns(Array.Empty<FileSystemEntry>());
_fileSystem.GetEntryAsync(Arg.Any<FtpAuthenticationContext>(), "/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<FtpAuthenticationContext>(), "/").Throws(new InvalidOperationException());
_fileSystem.GetEntryAsync(Arg.Any<FtpAuthenticationContext>(), "/file.txt").Throws(new InvalidOperationException());
await sut.ExecuteAsync(_context);
await _session.Received().SendResponseAsync(550, "Error getting file size.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private sealed class DummyFileSystemProvider : IFileSystemProvider
public Task DeleteDirectoryAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException();
public Task<bool> FileExistsAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException();
public Task<bool> DirectoryExistsAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException();
public Task<FileSystemEntry?> GetEntryAsync(FtpAuthenticationContext authContext, string path) => throw new NotImplementedException();
public Task RenameAsync(FtpAuthenticationContext authContext, string oldPath, string newPath) => throw new NotImplementedException();
}

Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.202",
"version": "10.0.203",
"rollForward": "latestMajor",
"allowPrerelease": false
}
Expand Down
Loading