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 + + +