diff --git a/SimpleFTP/Client/.editorconfig b/SimpleFTP/Client/.editorconfig
new file mode 100644
index 0000000..6cd746e
--- /dev/null
+++ b/SimpleFTP/Client/.editorconfig
@@ -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
\ No newline at end of file
diff --git a/SimpleFTP/Client/Client.cs b/SimpleFTP/Client/Client.cs
new file mode 100644
index 0000000..a6f3e87
--- /dev/null
+++ b/SimpleFTP/Client/Client.cs
@@ -0,0 +1,172 @@
+//
+// Copyright (c) Kalinin Andrew. All rights reserved.
+//
+
+namespace SimpleFTP.Client;
+
+using System.Net.Sockets;
+using System.Text;
+
+///
+/// A class that implements the client's functionality.
+///
+public class Client
+{
+ private const string IP = "127.0.0.1";
+ private const int PORT = 8888;
+
+ ///
+ /// Launches the client and connects to the server.
+ ///
+ /// Task representing the operation.
+ public static async Task StartClient()
+ {
+ using (TcpClient client = new TcpClient())
+ {
+ try
+ {
+ await client.ConnectAsync(IP, PORT);
+ Console.WriteLine($"Connected at {IP}");
+ 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)
+ {
+ Console.WriteLine($"Error. {excetpion.Message}");
+ }
+ }
+
+ Console.WriteLine("Disconnected from the server");
+ }
+
+ private static async Task CommandHandler(NetworkStream stream, StreamReader reader, StreamWriter writer)
+ {
+ Console.WriteLine("Enter commands ('list ' or 'get ' or 'exit'");
+ while (true)
+ {
+ string? input = Console.ReadLine();
+ 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 '");
+ }
+
+ break;
+ case "get":
+ if (parts.Length == 3)
+ {
+ await HandleGetRequest(stream, writer, parts[1], parts[2]);
+ }
+ else
+ {
+ Console.WriteLine("Incorrect input. Enter 'get '");
+ }
+
+ break;
+
+ default:
+ Console.WriteLine("Unknown command");
+ break;
+ }
+ }
+ }
+
+ private static async Task HandleListRequest(StreamReader reader, StreamWriter writer, string path)
+ {
+ await writer.WriteLineAsync($"1 {path}");
+ await writer.FlushAsync();
+
+ string? response = await reader.ReadLineAsync();
+ 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}");
+ }
+ }
+
+ 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];
+ 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))
+ {
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SimpleFTP/Client/Program.cs b/SimpleFTP/Client/Program.cs
new file mode 100644
index 0000000..e1cb7af
--- /dev/null
+++ b/SimpleFTP/Client/Program.cs
@@ -0,0 +1,7 @@
+//
+// Copyright (c) Kalinin Andrew. All rights reserved.
+//
+
+using SimpleFTP.Client;
+
+await Client.StartClient();
\ No newline at end of file
diff --git a/SimpleFTP/Client/SimpleFTP.Client.csproj b/SimpleFTP/Client/SimpleFTP.Client.csproj
new file mode 100644
index 0000000..d764014
--- /dev/null
+++ b/SimpleFTP/Client/SimpleFTP.Client.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/SimpleFTP/Client/stylecop.json b/SimpleFTP/Client/stylecop.json
new file mode 100644
index 0000000..c7ed419
--- /dev/null
+++ b/SimpleFTP/Client/stylecop.json
@@ -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"
+ }
+ }
+}
diff --git a/SimpleFTP/Server/.editorconfig b/SimpleFTP/Server/.editorconfig
new file mode 100644
index 0000000..0e1810b
--- /dev/null
+++ b/SimpleFTP/Server/.editorconfig
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SimpleFTP.Server
+{
+ 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
+
+[*.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_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
\ No newline at end of file
diff --git a/SimpleFTP/Server/LogicOfServer.cs b/SimpleFTP/Server/LogicOfServer.cs
new file mode 100644
index 0000000..9e92107
--- /dev/null
+++ b/SimpleFTP/Server/LogicOfServer.cs
@@ -0,0 +1,114 @@
+//
+// Copyright (c) Kalinin Andrew. All rights reserved.
+//
+
+namespace SimpleFTP.Server;
+
+using System.Text;
+
+///
+/// A class with query processing logic.
+///
+public class LogicOfServer
+{
+ private string baseDirectory;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The base directory for which requests will be processed.
+ public LogicOfServer(string baseDirectory)
+ {
+ this.baseDirectory = Path.GetFullPath(baseDirectory);
+ }
+
+ ///
+ /// Checks whether the requested path is located inside the base directory.
+ ///
+ /// The path from the client's request.
+ /// Full path.
+ /// True if the path is safe and located inside the base directory, otherwise False.
+ public bool IsPathSafe(string requestedPath, out string? fullPath)
+ {
+ string combinedPath = Path.Combine(this.baseDirectory, requestedPath);
+ fullPath = Path.GetFullPath(combinedPath);
+ return fullPath.StartsWith(this.baseDirectory, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Processes the List command, compiles a list of files/folders and sends it to the client.
+ ///
+ /// StreamWriter for sending a text response.
+ /// The full path to the directory for listing.
+ /// Task representing the operation.
+ public async Task List(StreamWriter streamwriter, string directoryPath)
+ {
+ DirectoryInfo infoAboutDirectory = new DirectoryInfo(directoryPath);
+
+ if (!infoAboutDirectory.Exists)
+ {
+ await streamwriter.WriteLineAsync("-1");
+ return;
+ }
+
+ try
+ {
+ FileSystemInfo[] filesAndFolders = infoAboutDirectory.GetFileSystemInfos();
+ StringBuilder response = new StringBuilder();
+
+ response.Append(filesAndFolders.Length);
+
+ foreach (var data in filesAndFolders)
+ {
+ response.Append($" {data.Name} {(data is DirectoryInfo ? "true" : "false")}");
+ }
+
+ await streamwriter.WriteLineAsync(response.ToString());
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine($"Error in 'List': {exception.Message}");
+ await streamwriter.WriteLineAsync("-1");
+ }
+ }
+
+ ///
+ /// Processes the Get command, sends the file size and its contents to the client.
+ ///
+ /// A network stream for sending data.
+ /// The full path to the download file.
+ /// Task representing the operation.
+ public async Task Get(Stream stream, string filePath)
+ {
+ FileInfo file = new FileInfo(filePath);
+
+ if (!file.Exists)
+ {
+ byte[] errorInBytes = BitConverter.GetBytes(-1L);
+ await stream.WriteAsync(errorInBytes);
+ await stream.FlushAsync();
+ return;
+ }
+
+ try
+ {
+ long fileSize = file.Length;
+ byte[] sizeBytes = BitConverter.GetBytes(fileSize);
+ await stream.WriteAsync(sizeBytes);
+ await stream.FlushAsync();
+
+ await using (FileStream fileStream = file.OpenRead())
+ {
+ await fileStream.CopyToAsync(stream);
+ }
+
+ await stream.FlushAsync();
+
+ Console.WriteLine($"Sent file ({filePath}) ({fileSize} bytes)");
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine($"Error in 'Get': {exception.Message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/SimpleFTP/Server/Program.cs b/SimpleFTP/Server/Program.cs
new file mode 100644
index 0000000..7e93339
--- /dev/null
+++ b/SimpleFTP/Server/Program.cs
@@ -0,0 +1,10 @@
+//
+// Copyright (c) Kalinin Andrew. All rights reserved.
+//
+
+using SimpleFTP.Server;
+
+string baseDirectory = Directory.GetCurrentDirectory();
+Console.WriteLine($"Starting server. Base directory: {baseDirectory}");
+Server fileServer = new Server(baseDirectory);
+await fileServer.Start();
\ No newline at end of file
diff --git a/SimpleFTP/Server/Server.cs b/SimpleFTP/Server/Server.cs
new file mode 100644
index 0000000..98f86cd
--- /dev/null
+++ b/SimpleFTP/Server/Server.cs
@@ -0,0 +1,160 @@
+//
+// Copyright (c) Kalinin Andrew. All rights reserved.
+//
+
+namespace SimpleFTP.Server;
+
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+///
+/// The main server class that manages listening for connections and client processing.
+///
+public class Server
+{
+ private const int Port = 8888;
+ private readonly TcpListener listener;
+ private readonly LogicOfServer logicOfServer;
+ private CancellationTokenSource cts;
+ private bool isWorking;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The base directory for file operations.
+ public Server(string baseDirectory)
+ {
+ this.listener = new TcpListener(IPAddress.Any, Port);
+ this.logicOfServer = new LogicOfServer(baseDirectory);
+ this.cts = new CancellationTokenSource();
+ }
+
+ ///
+ /// Starts listening for incoming connections and creates tasks for processing each client.
+ ///
+ /// A task representing an asynchronous server operation.
+ public async Task Start()
+ {
+ this.listener.Start();
+ this.isWorking = true;
+ Console.WriteLine($"Listening on port {Port}...");
+
+ while (this.isWorking)
+ {
+ try
+ {
+ TcpClient client = await this.listener.AcceptTcpClientAsync(this.cts.Token);
+ Task clientTask = Task.Run(() => this.ClientHandler(client));
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception exception)
+ {
+ if (!this.isWorking)
+ {
+ break;
+ }
+
+ Console.WriteLine($"Error accepting client {exception.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Initiates the server shutdown process by closing listener and canceling all pending operations.
+ ///
+ public void Stop()
+ {
+ this.isWorking = false;
+ this.cts?.Cancel();
+ this.listener?.Stop();
+ }
+
+ ///
+ /// Frees up resources.
+ ///
+ public void Dispose()
+ {
+ this.Stop();
+ this.listener?.Dispose();
+ this.cts?.Dispose();
+ }
+
+ private async Task ClientHandler(TcpClient client)
+ {
+ Console.WriteLine("Client connected");
+ try
+ {
+ await using NetworkStream stream = client.GetStream();
+ {
+ string? line = string.Empty;
+ while (true)
+ {
+ using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
+ await using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
+ line = await reader.ReadLineAsync();
+ if (line == null)
+ {
+ break;
+ }
+
+ Console.WriteLine($"Request: {line}");
+ string[] parts = line.Split(' ', 2);
+ if (parts.Length < 2)
+ {
+ continue;
+ }
+
+ string command = parts[0];
+ string path = parts[1];
+
+ if (!this.logicOfServer.IsPathSafe(path, out string? fullPath) || fullPath == null)
+ {
+ Console.WriteLine($"access denied {path}");
+ switch (command)
+ {
+ case "1":
+ await writer.WriteLineAsync("-1");
+ await writer.FlushAsync();
+ break;
+ case "2":
+ byte[] errorBytes = BitConverter.GetBytes(-1L);
+ await stream.WriteAsync(errorBytes);
+ await stream.FlushAsync();
+ break;
+ }
+
+ continue;
+ }
+
+ switch (command)
+ {
+ case "1":
+ await this.logicOfServer.List(writer, fullPath);
+ await writer.FlushAsync();
+ break;
+ case "2":
+ await this.logicOfServer.Get(stream, fullPath);
+ break;
+ default:
+ await writer.WriteLineAsync("-1");
+ await writer.FlushAsync();
+ break;
+ }
+ }
+ }
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine($"Error handling client {exception.Message}");
+ }
+ finally
+ {
+ client.Close();
+ Console.WriteLine("Client disconnected");
+ }
+ }
+}
\ No newline at end of file
diff --git a/SimpleFTP/Server/SimpleFTP.Server.csproj b/SimpleFTP/Server/SimpleFTP.Server.csproj
new file mode 100644
index 0000000..d764014
--- /dev/null
+++ b/SimpleFTP/Server/SimpleFTP.Server.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/SimpleFTP/Server/stylecop.json b/SimpleFTP/Server/stylecop.json
new file mode 100644
index 0000000..c7ed419
--- /dev/null
+++ b/SimpleFTP/Server/stylecop.json
@@ -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"
+ }
+ }
+}
diff --git a/SimpleFTP/SimpleFTP.Tests/.editorconfig b/SimpleFTP/SimpleFTP.Tests/.editorconfig
new file mode 100644
index 0000000..e44bd88
--- /dev/null
+++ b/SimpleFTP/SimpleFTP.Tests/.editorconfig
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SimpleFTP.Tests
+{
+ class _
+ {
+ }
+}
+
+[*.cs]
+dotnet_diagnostic.SA0001.severity = none
+
+[*.cs]
+dotnet_diagnostic.SA1600.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
+
+[*.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_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
\ No newline at end of file
diff --git a/SimpleFTP/SimpleFTP.Tests/ServerTests.cs b/SimpleFTP/SimpleFTP.Tests/ServerTests.cs
new file mode 100644
index 0000000..4a290e9
--- /dev/null
+++ b/SimpleFTP/SimpleFTP.Tests/ServerTests.cs
@@ -0,0 +1,183 @@
+//
+// Copyright (c) Kalinin Andrew. All rights reserved.
+//
+
+namespace SimpleFTP.Tests;
+
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+using SimpleFTP.Server;
+using static System.Net.Mime.MediaTypeNames;
+
+public class ServerTests
+{
+ private const string IP = "127.0.0.1";
+ private const int PORT = 8888;
+ private const string TestTextData = "Abcd,Efg";
+ private readonly string baseDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestServerBase");
+ private readonly int[] testIntData = new int[] { 59, -1, 10000, 52 };
+ private Server? server;
+ private Task? serverTask;
+ private byte[] testBinaryData = Array.Empty();
+
+ [OneTimeSetUp]
+ public void OneTimeSetUp()
+ {
+ this.testBinaryData = this.IntToBytes(this.testIntData);
+
+ if (Directory.Exists(this.baseDirectory))
+ {
+ Directory.Delete(this.baseDirectory, true);
+ }
+
+ Directory.CreateDirectory(this.baseDirectory);
+ Directory.CreateDirectory(Path.Combine(this.baseDirectory, "Folder"));
+ Directory.CreateDirectory(Path.Combine(this.baseDirectory, "EmptyFolder"));
+
+ File.WriteAllText(Path.Combine(this.baseDirectory, "file.txt"), TestTextData);
+ File.WriteAllBytes(Path.Combine(this.baseDirectory, "file.bin"), this.testBinaryData);
+ File.WriteAllText(Path.Combine(this.baseDirectory, "Folder", "FileInFolder.txt"), "qwerty");
+
+ this.server = new Server(this.baseDirectory);
+ this.serverTask = this.server.Start();
+
+ Thread.Sleep(300);
+ }
+
+ [OneTimeTearDown]
+ public void OneTimeTearDown()
+ {
+ this.server?.Stop();
+ try
+ {
+ this.serverTask?.Wait(TimeSpan.FromSeconds(5));
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine($"Error during server task teardown: {exception.Message}");
+ }
+
+ if (Directory.Exists(this.baseDirectory))
+ {
+ Directory.Delete(this.baseDirectory, true);
+ }
+ }
+
+ [Test]
+ public async Task List_ValidDirectory_ShouldReturnCorrectList()
+ {
+ var response = await this.PerformList(".");
+
+ Assert.That(response, Is.Not.Null);
+ Assert.That(response, Does.StartWith("4"));
+ Assert.That(response, Contains.Substring(" file.txt false"));
+ Assert.That(response, Contains.Substring(" file.bin false"));
+ Assert.That(response, Contains.Substring(" Folder true"));
+ Assert.That(response, Contains.Substring(" EmptyFolder true"));
+ }
+
+ [Test]
+ public async Task List_EmptyDirectory_ShouldReturnCorrectList()
+ {
+ var response = await this.PerformList("EmptyFolder");
+ Assert.That(response, Is.EqualTo("0"));
+ }
+
+ [Test]
+ public async Task List_NonExistentDirectory_ShouldReturnException()
+ {
+ var response = await this.PerformList("kmnds");
+ Assert.That(response, Is.EqualTo("-1"));
+ }
+
+ [Test]
+ public async Task Get_ValidTextFile_ShouldReturnCorrectContent()
+ {
+ var (size, data) = await this.PerformGet("file.txt");
+ string received = Encoding.UTF8.GetString(data);
+ Assert.That(received, Is.EqualTo(TestTextData));
+ }
+
+ [Test]
+ public async Task Get_ValidBinaryFile_ShouldReturnCorrectContent()
+ {
+ var (size, data) = await this.PerformGet("file.bin");
+ Assert.That(data, Is.EqualTo(this.IntToBytes(this.testIntData)));
+ }
+
+ [Test]
+ public async Task Get_FileInSubFolder_ShouldReturnCorrectContent()
+ {
+ var (size, data) = await this.PerformGet("Folder/FileInFolder.txt");
+ Assert.That(data, Is.EqualTo("qwerty"));
+ }
+
+ private async Task PerformList(string path)
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync(IP, PORT);
+ await using var stream = client.GetStream();
+ await using var writer = new StreamWriter(stream) { AutoFlush = true };
+ using var reader = new StreamReader(stream);
+
+ await writer.WriteLineAsync($"1 {path}");
+ return await reader.ReadLineAsync();
+ }
+
+ private async Task<(long Size, byte[] Data)> PerformGet(string path)
+ {
+ using var client = new TcpClient();
+ await client.ConnectAsync(IP, PORT);
+ await using var stream = client.GetStream();
+
+ string command = $"2 {path}\n";
+ byte[] commandBytes = Encoding.UTF8.GetBytes(command);
+ await stream.WriteAsync(commandBytes);
+ await stream.FlushAsync();
+
+ await Task.Delay(100);
+
+ byte[] sizeBuffer = new byte[8];
+
+ await stream.ReadExactlyAsync(sizeBuffer, 0, 8);
+ long fileSize = BitConverter.ToInt64(sizeBuffer, 0);
+
+ if (fileSize == -1)
+ {
+ return (-1L, Array.Empty());
+ }
+
+ using var memoryStream = new MemoryStream();
+ byte[] buffer = new byte[81920];
+ long totalBytesRead = 0;
+ while (totalBytesRead < fileSize)
+ {
+ int bytesToRead = (int)Math.Min(fileSize - totalBytesRead, buffer.Length);
+ int bytesRead = await stream.ReadAsync(buffer, 0, bytesToRead);
+ if (bytesRead == 0)
+ {
+ throw new EndOfStreamException("Connection closed");
+ }
+
+ await memoryStream.WriteAsync(buffer, 0, bytesRead);
+ totalBytesRead += bytesRead;
+ }
+
+ return (fileSize, memoryStream.ToArray());
+ }
+
+ private byte[] IntToBytes(int[] data)
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ foreach (int num in this.testIntData)
+ {
+ byte[] bytes = BitConverter.GetBytes(num);
+ memoryStream.Write(bytes, 0, bytes.Length);
+ }
+
+ return memoryStream.ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/SimpleFTP/SimpleFTP.Tests/SimpleFTP.Tests.csproj b/SimpleFTP/SimpleFTP.Tests/SimpleFTP.Tests.csproj
new file mode 100644
index 0000000..8363e19
--- /dev/null
+++ b/SimpleFTP/SimpleFTP.Tests/SimpleFTP.Tests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net9.0
+ latest
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SimpleFTP/SimpleFTP.Tests/stylecop.json b/SimpleFTP/SimpleFTP.Tests/stylecop.json
new file mode 100644
index 0000000..c7ed419
--- /dev/null
+++ b/SimpleFTP/SimpleFTP.Tests/stylecop.json
@@ -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"
+ }
+ }
+}
diff --git a/SimpleFTP/SimpleFTP.sln b/SimpleFTP/SimpleFTP.sln
new file mode 100644
index 0000000..d12ff67
--- /dev/null
+++ b/SimpleFTP/SimpleFTP.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.13.35806.99
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFTP.Client", "Client\SimpleFTP.Client.csproj", "{A1377BAB-DB67-4A74-A7D0-86B07890FC3F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFTP.Tests", "SimpleFTP.Tests\SimpleFTP.Tests.csproj", "{CE04E890-DE80-431B-B53E-07A74FA6AEF9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFTP", "SimpleFTP\SimpleFTP.csproj", "{1B9979DB-E126-40BB-B693-2E5BAE60DAF3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleFTP.Server", "Server\SimpleFTP.Server.csproj", "{FDD87F48-4BC5-B87E-9D30-B23C96243D6D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1377BAB-DB67-4A74-A7D0-86B07890FC3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1377BAB-DB67-4A74-A7D0-86B07890FC3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1377BAB-DB67-4A74-A7D0-86B07890FC3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1377BAB-DB67-4A74-A7D0-86B07890FC3F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE04E890-DE80-431B-B53E-07A74FA6AEF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE04E890-DE80-431B-B53E-07A74FA6AEF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE04E890-DE80-431B-B53E-07A74FA6AEF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE04E890-DE80-431B-B53E-07A74FA6AEF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1B9979DB-E126-40BB-B693-2E5BAE60DAF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1B9979DB-E126-40BB-B693-2E5BAE60DAF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1B9979DB-E126-40BB-B693-2E5BAE60DAF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1B9979DB-E126-40BB-B693-2E5BAE60DAF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FDD87F48-4BC5-B87E-9D30-B23C96243D6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FDD87F48-4BC5-B87E-9D30-B23C96243D6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FDD87F48-4BC5-B87E-9D30-B23C96243D6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FDD87F48-4BC5-B87E-9D30-B23C96243D6D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F88013D2-30E3-4487-AFAE-0A91EEBCAB90}
+ EndGlobalSection
+EndGlobal
diff --git a/SimpleFTP/SimpleFTP/SimpleFTP.csproj b/SimpleFTP/SimpleFTP/SimpleFTP.csproj
new file mode 100644
index 0000000..3b8f954
--- /dev/null
+++ b/SimpleFTP/SimpleFTP/SimpleFTP.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Library
+ net9.0
+ enable
+ enable
+
+
+