Skip to content
Open
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
134 changes: 134 additions & 0 deletions SimpleFTP/Client/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleFTP.Client
{
class _
{
}
}
Comment on lines +1 to +12

Choose a reason for hiding this comment

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

Хм


[*.cs]
dotnet_diagnostic.SA0001.severity = none

[*.cs]
#### Стили именования ####

# Правила именования

dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i

dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case

dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case

# Спецификации символов

dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =

dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =

dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =

# Стили именования

dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case

dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
csharp_indent_labels = one_less_than_current
csharp_style_throw_expression = true:suggestion

[*.vb]
#### Стили именования ####

# Правила именования

dotnet_naming_rule.interface_should_be_начинается_с_i.severity = suggestion
dotnet_naming_rule.interface_should_be_начинается_с_i.symbols = interface
dotnet_naming_rule.interface_should_be_начинается_с_i.style = начинается_с_i

dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.severity = suggestion
dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.symbols = типы
dotnet_naming_rule.типы_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы

dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.severity = suggestion
dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.symbols = не_являющиеся_полем_члены
dotnet_naming_rule.не_являющиеся_полем_члены_should_be_всечастиспрописнойбуквы.style = всечастиспрописнойбуквы

# Спецификации символов

dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =

dotnet_naming_symbols.типы.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.типы.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.типы.required_modifiers =

dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_kinds = property, event, method
dotnet_naming_symbols.не_являющиеся_полем_члены.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.не_являющиеся_полем_члены.required_modifiers =

# Стили именования

dotnet_naming_style.начинается_с_i.required_prefix = I
dotnet_naming_style.начинается_с_i.required_suffix =
dotnet_naming_style.начинается_с_i.word_separator =
dotnet_naming_style.начинается_с_i.capitalization = pascal_case

dotnet_naming_style.всечастиспрописнойбуквы.required_prefix =
dotnet_naming_style.всечастиспрописнойбуквы.required_suffix =
dotnet_naming_style.всечастиспрописнойбуквы.word_separator =
dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case

dotnet_naming_style.всечастиспрописнойбуквы.required_prefix =
dotnet_naming_style.всечастиспрописнойбуквы.required_suffix =
dotnet_naming_style.всечастиспрописнойбуквы.word_separator =
dotnet_naming_style.всечастиспрописнойбуквы.capitalization = pascal_case

[*.{cs,vb}]
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
172 changes: 172 additions & 0 deletions SimpleFTP/Client/Client.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// <copyright file="Client.cs" company="Kalinin Andrew">
// Copyright (c) Kalinin Andrew. All rights reserved.
// </copyright>

namespace SimpleFTP.Client;

using System.Net.Sockets;
using System.Text;

/// <summary>
/// A class that implements the client's functionality.
/// </summary>
public class Client
{
private const string IP = "127.0.0.1";
private const int PORT = 8888;
Comment on lines +15 to +16

Choose a reason for hiding this comment

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

Это надо было бы передавать как параметры конструктора


/// <summary>
/// Launches the client and connects to the server.
/// </summary>
/// <returns>Task representing the operation.</returns>
public static async Task StartClient()
{
using (TcpClient client = new TcpClient())

Choose a reason for hiding this comment

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

Suggested change
using (TcpClient client = new TcpClient())
using (TcpClient client = new())

{
try
{
await client.ConnectAsync(IP, PORT);
Console.WriteLine($"Connected at {IP}");

Choose a reason for hiding this comment

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

Стоит всегда разделять UI и бизнес-логику, особенно такую низкоуровневую, как скачивание чего-то по сети. Так будет гораздо проще переиспользовать этот код в другом приложении, например, UI-клиенте (такая задача реально была когда-то в прошлые годы). Клиент не должен вообще ничего на консоль выводить, это дело Main-а.

await using NetworkStream stream = client.GetStream();
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
await using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
await CommandHandler(stream, reader, writer);
}
catch (Exception excetpion)

Choose a reason for hiding this comment

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

Опечатка

{
Console.WriteLine($"Error. {excetpion.Message}");
}
}

Console.WriteLine("Disconnected from the server");
}

private static async Task CommandHandler(NetworkStream stream, StreamReader reader, StreamWriter writer)

Choose a reason for hiding this comment

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

Методы по традиции именуются глаголами в повелительной форме, даже если это обработчики. "HandleCommand", например.

{
Console.WriteLine("Enter commands ('list <path>' or 'get <path> <local path>' or 'exit'");
while (true)
{
string? input = Console.ReadLine();

Choose a reason for hiding this comment

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

Тут даже настоящий REPL есть. Это ещё и делает почти невозможным использование клиента из скриптов. Думаю, что команда должна приниматься как аргументы командной строки в Main, как и IP-адрес/порт, и потом просто вызываться метод Get или List клииента.

if (string.IsNullOrWhiteSpace(input))
{
continue;
}

string[] parts = input.Split(' ');
string command = parts[0].ToLower();

if (command == "exit")
{
break;
}

switch (command)
{
case "list":
if (parts.Length == 2)
{
await HandleListRequest(reader, writer, parts[1]);
}
else
{
Console.WriteLine("Incorrect input. Enter 'list <path>'");
}

break;
case "get":
if (parts.Length == 3)
{
await HandleGetRequest(stream, writer, parts[1], parts[2]);
}
else
{
Console.WriteLine("Incorrect input. Enter 'get <path> <local path>'");
}

break;

default:
Console.WriteLine("Unknown command");
break;
}
}
}

private static async Task HandleListRequest(StreamReader reader, StreamWriter writer, string path)

Choose a reason for hiding this comment

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

Асинхронные операции лучше сразу делать все прерываемыми. Тем более потенциально длительные. Принимайте CancellationToken в любой async-метод и передавайте его в любой библиотечный async-метод, который его принимает (таких большинство).

{
await writer.WriteLineAsync($"1 {path}");
await writer.FlushAsync();

string? response = await reader.ReadLineAsync();

Choose a reason for hiding this comment

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

Вот тут, например, если сервер ответит некорректно, мы будем без CancellationToken вечно ждать перевода строки или разрыва соединения

if (response == "-1" || response is null)
{
Console.WriteLine("Error. Directory was not found or access to it was denied");
return;
}

string[] parts = response!.Split(' ');
if (!int.TryParse(parts[0], out int count) || count < 0)
{
Console.WriteLine("invalid response from the server");
return;
}

if (count == 0)
{
Console.WriteLine("Directory is empty");
return;
}

Console.WriteLine($"Directory contains {count} items:");
for (int i = 0; i < count; ++i)
{
string name = parts[(i * 2) + 1];
string isDirectory = parts[(i * 2) + 2] == "true" ? "[dir]" : "[file]";
Console.WriteLine($" {isDirectory} {name}");

Choose a reason for hiding this comment

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

Касательно разделения бизнес-логики и UI, вот эта строчка вообще лишает шанса использовать Client для чего-то кроме того, для чего конкретно он был написан. На самом деле, так его даже перевести на другой язык почти невозможно.

}
}

private static async Task HandleGetRequest(NetworkStream stream, StreamWriter writer, string path, string localPath)
{
await writer.WriteLineAsync($"2 {path}");
await writer.FlushAsync();
byte[] sizeBuffer = new byte[8];

Choose a reason for hiding this comment

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

Suggested change
byte[] sizeBuffer = new byte[8];
var sizeBuffer = new byte[8];

await stream.ReadExactlyAsync(sizeBuffer, 0, 8);
long fileSize = BitConverter.ToInt64(sizeBuffer, 0);

if (fileSize == -1)
{
Console.WriteLine("File not found or access denied");
return;
}

Console.WriteLine($"Downloading file {path} to {localPath}");
long totalBytesRead = 0;

try
{
await using (FileStream fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write))

Choose a reason for hiding this comment

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

Suggested change
await using (FileStream fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write))
await using (FileStream fileStream = new(localPath, FileMode.Create, FileAccess.Write))

{
byte[] buffer = new byte[81920];
while (totalBytesRead < fileSize)
{
int bytesToRead = (int)Math.Min(fileSize - totalBytesRead, buffer.Length);
int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, bytesToRead));

await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
totalBytesRead += bytesRead;
}
}

Console.WriteLine("Download finish");
}
catch (Exception excetpion)
{
Console.WriteLine($"Error loading. {excetpion.Message}");
if (File.Exists(localPath))
{
File.Delete(localPath);
}
}
}
}
7 changes: 7 additions & 0 deletions SimpleFTP/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// <copyright file="Program.cs" company="Kalinin Andrew">
// Copyright (c) Kalinin Andrew. All rights reserved.
// </copyright>

using SimpleFTP.Client;

await Client.StartClient();

Choose a reason for hiding this comment

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

Вот сюда надо бы вынести все Console.WriteLine из клиента, и вместо ReadLine принимать параметры из args

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

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

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

<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions SimpleFTP/Client/stylecop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "Kalinin Andrew"
}
}
}
Loading