Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple FTP #5

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.x'
dotnet-version: '7.x'
- name: Build
run: for dir in */; do cd $dir; for sln in *.sln; do dotnet build $sln; cd ..; done; done
shell: bash
Expand Down
142 changes: 142 additions & 0 deletions SimpleFTP/Client/Client.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
namespace Client;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Надо бы линтер настроить, очень ему Ваш код не нравится


using Exceptions;
using System.Net.Sockets;

/// <summary>
/// FTP client, that can process get and list queries.
/// </summary>
public class Client : IDisposable
{
private readonly TcpClient _client;

private readonly NetworkStream _networkStream;
private readonly StreamWriter _writer;
private readonly StreamReader _reader;

/// <summary>
/// Initializes a new instance of the <see cref="Client"/> class.
/// </summary>
/// <param name="port">The port number of the remote host to which you intend to connect.</param>
/// <param name="host">The DNS name of the remote host to which you intend to connect.</param>
public Client(int port, string host)
{
_client = new TcpClient(host, port);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это инициирует синхронное подключение. Тут надо было использовать конструктор без параметров и потом ConnectToAsync


_networkStream = _client.GetStream();
_writer = new StreamWriter(_networkStream);
_reader = new StreamReader(_networkStream);
}

/// <summary>
/// Gets a value indicating whether <see cref="Client"/> is disposed.
/// </summary>
public bool IsDisposed { get; private set; }

/// <summary>
/// Releases all resources used by the current instance of the <see cref="Client"/> class.
/// </summary>
public void Dispose()
{
if (IsDisposed)
{
return;
}

_reader.Dispose();
_writer.Dispose();
_networkStream.Dispose();
_client.Dispose();

IsDisposed = true;
}

/// <summary>
/// Gets list of files containing in the specific directory.
/// </summary>
/// <param name="pathToDirectory">Path to the needed directory.</param>
/// <returns>
/// Returns pair where first item is the number of elements contained in the directory,
/// second item is the list of elements contained in the directory.
/// List also consists of pairs, where first item is the name of the element,
/// second item is a boolean value indicating, whether the element is folder or not.
/// </returns>
public async Task<(int, List<(string, bool)>)> ListAsync(string pathToDirectory)
{
var query = $"1 {pathToDirectory}";
await _writer.WriteLineAsync(query);
await _writer.FlushAsync();

var response = await _reader.ReadLineAsync();

return response is null or "-1" ? (-1, new List<(string, bool)>()) : ParseResponse(response);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Длину списка можно было не возвращать, он её сам знает

}

/// <summary>
/// Downloads specific file from the server.
/// </summary>
/// <param name="pathToFile">Path to the needed file.</param>
/// <param name="destinationStream">Stream to which file bytes will be moved.</param>
/// <returns>Size of downloaded file.</returns>
/// <exception cref="DataLossException">Throws if some bytes were lost while downloading.</exception>
public async Task<long> GetAsync(string pathToFile, Stream destinationStream)
{
var query = $"2 {pathToFile}";
await _writer.WriteLineAsync(query);
await _writer.FlushAsync();

var sizeInBytes = new byte[8];
if (await _networkStream.ReadAsync(sizeInBytes.AsMemory(0, 8)) != 8)
{
throw new DataLossException("Some bytes were lost.");
}

var size = BitConverter.ToInt64(sizeInBytes);
if (size == -1)
{
return -1;
}

await CopyStream(destinationStream, size);

return size;
}

private async Task CopyStream(Stream destinationStream, long size)
{
var bytesLeft = size;
var chunkSize = Math.Min(1024, bytesLeft);
var chunkBuffer = new byte[chunkSize];

while (bytesLeft > 0)
{
var readBytesCount = await _networkStream.ReadAsync(chunkBuffer, 0, (int)chunkSize);

if (readBytesCount != chunkSize)
{
throw new DataLossException("Data loss during transmission and reception.");
}

await destinationStream.WriteAsync(chunkBuffer, 0, (int)chunkSize);
await destinationStream.FlushAsync();

bytesLeft -= chunkSize;
chunkSize = Math.Min(chunkSize, bytesLeft);
}
}

private (int, List<(string, bool)>) ParseResponse(string response)
{
var splitResponse = response.Split(" ");
var numberOfElements = int.Parse(splitResponse[0]);
var listOfElements = new List<(string, bool)>();

for (var i = 1; i < numberOfElements * 2; i += 2)
{
var pair = (splitResponse[i], bool.Parse(splitResponse[i + 1]));
listOfElements.Add(pair);
}

return (numberOfElements, listOfElements);
}
}
18 changes: 18 additions & 0 deletions SimpleFTP/Client/Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
34 changes: 34 additions & 0 deletions SimpleFTP/Client/Exceptions/DataLossException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Client.Exceptions;

/// <summary>
/// The exception that is thrown in case of data loss.
/// </summary>
[Serializable]
public class DataLossException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="DataLossException"/> class.
/// </summary>
public DataLossException()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DataLossException"/> class.
/// </summary>
/// <param name="message">Exception message.</param>
public DataLossException(string message)
: base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DataLossException"/> class.
/// </summary>
/// <param name="message">Exception message.</param>
/// <param name="inner">Inner exception.</param>
public DataLossException(string message, Exception inner)
: base(message, inner)
{
}
}
9 changes: 9 additions & 0 deletions SimpleFTP/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using var client = new Client.Client(8888, "localhost");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это стоило принимать как параметры командной строки


var elementsInDirectory = await client.ListAsync("../../../");

Console.WriteLine(elementsInDirectory.Item1);
foreach (var element in elementsInDirectory.Item2)
{
Console.WriteLine(element);
}
131 changes: 131 additions & 0 deletions SimpleFTP/FTPTests/FTPTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
namespace Tests;

using NUnit.Framework;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using Server;
using Client;

[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "<Pending>")]
public class Tests
{
private const int Port = 8888;

private const int SizeOfFile = 1024 * 64;
private const int CountOfNumbersInFile = SizeOfFile / 4;

private const string DirectoryPath = "testDirectory";
private readonly string _subdirectoryPath1 = Path.Combine(DirectoryPath, "testSubdirectory1");
private readonly string _subdirectoryPath2 = Path.Combine(DirectoryPath, "testSubdirectory2");
private readonly string _fileName1 = Path.Combine(DirectoryPath, "file1.txt");
private readonly string _fileName2 = Path.Combine(DirectoryPath, "file2.txt");

private readonly Server _server = new (Port);

[OneTimeSetUp]
public void OneTimeSetUp()
{
Task.Run(async () => await _server.RunAsync());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Надо иметь возможность дождаться корректной остановки

Directory.CreateDirectory(DirectoryPath);
Directory.CreateDirectory(_subdirectoryPath1);
Directory.CreateDirectory(_subdirectoryPath2);
File.Create(_fileName1);
File.Create(_fileName2);
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
File.Delete(_fileName1);
File.Delete(_fileName2);
Directory.Delete(_subdirectoryPath1);
Directory.Delete(_subdirectoryPath2);
Directory.Delete(DirectoryPath);
_server.Dispose();
}

[Test]
public async Task GetShouldWorkProperlyWithCorrectQuery()
{
const string sourceFileName = "source.txt";
CreateFileAndGenerateSomeData(sourceFileName);

using var client = new Client(Port, "localhost");

await using var destinationStream = new MemoryStream();
var size = client.GetAsync(sourceFileName, destinationStream).Result;

await using var sourceStream = OpenWithDelay(sourceFileName);
destinationStream.Seek(0, SeekOrigin.Begin);
while (true)
{
var expectedByte = sourceStream.ReadByte();
var actualByte = destinationStream.ReadByte();
Assert.That(expectedByte, Is.EqualTo(actualByte));
if (expectedByte == -1)
{
break;
}
}
}

[Test]
public async Task ListShouldWorkProperlyWithCorrectQuery()
{
using var client = new Client(Port, "localhost");

var (count, elements) = await client.ListAsync(DirectoryPath);
Assert.That(count, Is.EqualTo(4));
Assert.That(elements.Contains(("file1.txt", false)));
Assert.That(elements.Contains(("file2.txt", false)));
Assert.That(elements.Contains(("testSubdirectory1", true)));
Assert.That(elements.Contains(("testSubdirectory2", true)));
}

[Test]
public async Task GetShouldReturnNegativeSizeInCaseOfIncorrectInput()
{
using var client = new Client(Port, "localhost");

await using var destinationStream = new MemoryStream();
var size = client.GetAsync(Path.Combine(DirectoryPath, "notFile"), destinationStream).Result;
Assert.That(size, Is.EqualTo(-1L));
}

[Test]
public async Task ListShouldReturnNegativeValueInCaseOfIncorrectInput()
{
using var client = new Client(Port, "localhost");

var size = (await client.ListAsync(Path.Combine(DirectoryPath, @"nonExistentPath"))).Item1;
Assert.That(size, Is.EqualTo(-1));
}

private void CreateFileAndGenerateSomeData(string fileName)
{
using var writer = new StreamWriter(File.Create(fileName));

var rnd = new Random();
for (var i = 0; i < CountOfNumbersInFile; i++)
{
writer.Write(rnd.Next());
}
}

private FileStream OpenWithDelay(string sourceFileName)
{
while (true)
{
try
{
var stream = File.OpenRead(sourceFileName);
return stream;
}
catch (IOException)
{
}
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Неплохо бы проверить ещё одновременное подключение несколькими клиентами

30 changes: 30 additions & 0 deletions SimpleFTP/FTPTests/FTPTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>

<RootNamespace>Tests</RootNamespace>

<LangVersion>11</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Client\Client.csproj" />
<ProjectReference Include="..\Server\Server.csproj" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions SimpleFTP/Server/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
using var server = new Server.Server(8888);
await server.RunAsync();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Надо реализовать какой-то механизм остановки, типа по нажатию перевода строки

Loading