-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Simple FTP #5
Changes from 10 commits
818450c
d5f8afb
4841b8d
c3a2f88
a477ee5
b51fd93
2ea09be
4ed7c10
0895f08
f9cc0af
ae21f9f
4903049
97bab45
60ba244
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
namespace Client; | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
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> |
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) | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using var client = new Client.Client(8888, "localhost"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
{ | ||
} | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Неплохо бы проверить ещё одновременное подключение несколькими клиентами |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
using var server = new Server.Server(8888); | ||
await server.RunAsync(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Надо реализовать какой-то механизм остановки, типа по нажатию перевода строки |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Надо бы линтер настроить, очень ему Ваш код не нравится