-
Notifications
You must be signed in to change notification settings - Fork 0
SimpleFTP #4
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
base: main
Are you sure you want to change the base?
SimpleFTP #4
Changes from all commits
5ce7997
22a344b
d0094cf
0cba9cc
b6be475
50de4d2
4724a02
141d4fe
e1618c2
d2f4869
e46dfc0
ae803ab
5dbf9f0
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,134 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace SimpleFTP.Client | ||
| { | ||
| class _ | ||
| { | ||
| } | ||
| } | ||
|
|
||
| [*.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 | ||
| 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
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> | ||||||
| /// 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()) | ||||||
|
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.
Suggested change
|
||||||
| { | ||||||
| try | ||||||
| { | ||||||
| await client.ConnectAsync(IP, PORT); | ||||||
| Console.WriteLine($"Connected at {IP}"); | ||||||
|
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. Стоит всегда разделять 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) | ||||||
|
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. Опечатка |
||||||
| { | ||||||
| Console.WriteLine($"Error. {excetpion.Message}"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Console.WriteLine("Disconnected from the server"); | ||||||
| } | ||||||
|
|
||||||
| private static async Task CommandHandler(NetworkStream stream, StreamReader reader, StreamWriter writer) | ||||||
|
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. Методы по традиции именуются глаголами в повелительной форме, даже если это обработчики. "HandleCommand", например. |
||||||
| { | ||||||
| Console.WriteLine("Enter commands ('list <path>' or 'get <path> <local path>' or 'exit'"); | ||||||
| while (true) | ||||||
| { | ||||||
| string? input = Console.ReadLine(); | ||||||
|
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. Тут даже настоящий 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) | ||||||
|
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. Асинхронные операции лучше сразу делать все прерываемыми. Тем более потенциально длительные. Принимайте CancellationToken в любой async-метод и передавайте его в любой библиотечный async-метод, который его принимает (таких большинство). |
||||||
| { | ||||||
| await writer.WriteLineAsync($"1 {path}"); | ||||||
| await writer.FlushAsync(); | ||||||
|
|
||||||
| string? response = await reader.ReadLineAsync(); | ||||||
|
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. Вот тут, например, если сервер ответит некорректно, мы будем без 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}"); | ||||||
|
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. Касательно разделения бизнес-логики и 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]; | ||||||
|
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.
Suggested change
|
||||||
| 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)) | ||||||
|
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.
Suggested change
|
||||||
| { | ||||||
| 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); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| 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(); | ||
|
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. Вот сюда надо бы вынести все Console.WriteLine из клиента, и вместо ReadLine принимать параметры из args |
||
| 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> |
| 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" | ||
| } | ||
| } | ||
| } |
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.
Хм