From aea17a68aac251ef0df8c5b5302ab15cc31110d3 Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Mon, 27 Oct 2025 22:13:08 +0300 Subject: [PATCH 1/9] init proj --- HW4/HW4.sln | 16 ++++++++++++++++ HW4/MyFTP/MyFTP.csproj | 21 +++++++++++++++++++++ HW4/MyFTP/stylecop.json | 9 +++++++++ 3 files changed, 46 insertions(+) create mode 100644 HW4/HW4.sln create mode 100644 HW4/MyFTP/MyFTP.csproj create mode 100644 HW4/MyFTP/stylecop.json diff --git a/HW4/HW4.sln b/HW4/HW4.sln new file mode 100644 index 0000000..f127296 --- /dev/null +++ b/HW4/HW4.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP", "MyFTP\MyFTP.csproj", "{15BAB866-F9EC-4581-A2DA-D9436094B887}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {15BAB866-F9EC-4581-A2DA-D9436094B887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15BAB866-F9EC-4581-A2DA-D9436094B887}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15BAB866-F9EC-4581-A2DA-D9436094B887}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15BAB866-F9EC-4581-A2DA-D9436094B887}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/HW4/MyFTP/MyFTP.csproj b/HW4/MyFTP/MyFTP.csproj new file mode 100644 index 0000000..796d45e --- /dev/null +++ b/HW4/MyFTP/MyFTP.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/HW4/MyFTP/stylecop.json b/HW4/MyFTP/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW4/MyFTP/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file From f92c800b5f9012f00d770094b6ac4faf09aa956c Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Thu, 30 Oct 2025 21:23:57 +0300 Subject: [PATCH 2/9] implement MyFTP server with List and Get commands --- HW4/HW4.sln | 12 ++ HW4/MyFTP.Tests/MyFTP.Tests.csproj | 40 +++++++ HW4/MyFTP.Tests/UnitTest1.cs | 15 +++ HW4/MyFTP.Tests/stylecop.json | 9 ++ HW4/MyFTP.client/MyFTP.client.csproj | 21 ++++ HW4/MyFTP.client/Program.cs | 3 + HW4/MyFTP.client/stylecop.json | 9 ++ HW4/MyFTP/MyFTP.csproj | 4 + HW4/MyFTP/Program.cs | 69 ++++++++++++ HW4/MyFTP/RequestHandler.cs | 158 +++++++++++++++++++++++++++ HW4/Test/file.txt | 0 HW4/Test/image.png | 0 12 files changed, 340 insertions(+) create mode 100644 HW4/MyFTP.Tests/MyFTP.Tests.csproj create mode 100644 HW4/MyFTP.Tests/UnitTest1.cs create mode 100644 HW4/MyFTP.Tests/stylecop.json create mode 100644 HW4/MyFTP.client/MyFTP.client.csproj create mode 100644 HW4/MyFTP.client/Program.cs create mode 100644 HW4/MyFTP.client/stylecop.json create mode 100644 HW4/MyFTP/Program.cs create mode 100644 HW4/MyFTP/RequestHandler.cs create mode 100644 HW4/Test/file.txt create mode 100644 HW4/Test/image.png diff --git a/HW4/HW4.sln b/HW4/HW4.sln index f127296..3db7374 100644 --- a/HW4/HW4.sln +++ b/HW4/HW4.sln @@ -2,6 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP", "MyFTP\MyFTP.csproj", "{15BAB866-F9EC-4581-A2DA-D9436094B887}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.client", "MyFTP.client\MyFTP.client.csproj", "{9A29907E-A801-4F31-8176-2DD0F1A9F475}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.Tests", "MyFTP.Tests\MyFTP.Tests.csproj", "{75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +16,13 @@ Global {15BAB866-F9EC-4581-A2DA-D9436094B887}.Debug|Any CPU.Build.0 = Debug|Any CPU {15BAB866-F9EC-4581-A2DA-D9436094B887}.Release|Any CPU.ActiveCfg = Release|Any CPU {15BAB866-F9EC-4581-A2DA-D9436094B887}.Release|Any CPU.Build.0 = Release|Any CPU + {9A29907E-A801-4F31-8176-2DD0F1A9F475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A29907E-A801-4F31-8176-2DD0F1A9F475}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A29907E-A801-4F31-8176-2DD0F1A9F475}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A29907E-A801-4F31-8176-2DD0F1A9F475}.Release|Any CPU.Build.0 = Release|Any CPU + {75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/HW4/MyFTP.Tests/MyFTP.Tests.csproj b/HW4/MyFTP.Tests/MyFTP.Tests.csproj new file mode 100644 index 0000000..75fb316 --- /dev/null +++ b/HW4/MyFTP.Tests/MyFTP.Tests.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/HW4/MyFTP.Tests/UnitTest1.cs b/HW4/MyFTP.Tests/UnitTest1.cs new file mode 100644 index 0000000..26e11ef --- /dev/null +++ b/HW4/MyFTP.Tests/UnitTest1.cs @@ -0,0 +1,15 @@ +namespace MyFTP.Tests; + +public class Tests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + Assert.Pass(); + } +} \ No newline at end of file diff --git a/HW4/MyFTP.Tests/stylecop.json b/HW4/MyFTP.Tests/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW4/MyFTP.Tests/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/HW4/MyFTP.client/MyFTP.client.csproj b/HW4/MyFTP.client/MyFTP.client.csproj new file mode 100644 index 0000000..796d45e --- /dev/null +++ b/HW4/MyFTP.client/MyFTP.client.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/HW4/MyFTP.client/Program.cs b/HW4/MyFTP.client/Program.cs new file mode 100644 index 0000000..e5dff12 --- /dev/null +++ b/HW4/MyFTP.client/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/HW4/MyFTP.client/stylecop.json b/HW4/MyFTP.client/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/HW4/MyFTP.client/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/HW4/MyFTP/MyFTP.csproj b/HW4/MyFTP/MyFTP.csproj index 796d45e..528ef94 100644 --- a/HW4/MyFTP/MyFTP.csproj +++ b/HW4/MyFTP/MyFTP.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/HW4/MyFTP/Program.cs b/HW4/MyFTP/Program.cs new file mode 100644 index 0000000..44780b9 --- /dev/null +++ b/HW4/MyFTP/Program.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +using System.Net; +using System.Net.Sockets; +using MyFTP; + +const int port = 9091; + +var listener = new TcpListener(IPAddress.Any, port); +listener.Start(); + +Console.WriteLine($"MyFTP server running on port {port}"); +Console.WriteLine($"Root directory: {Directory.GetCurrentDirectory()}"); +Console.WriteLine("Waiting for clients..."); + +while (true) +{ + var client = await listener.AcceptTcpClientAsync(); + _ = Task.Run(async () => + { + using (client) + await using (var stream = client.GetStream()) + { + var handler = new RequestHandler(stream); + + try + { + while (true) + { + var request = await new StreamReader(stream, leaveOpen: true).ReadLineAsync(); + + if (string.IsNullOrEmpty(request)) + { + break; + } + + if (request.Trim().Equals("exit", StringComparison.CurrentCultureIgnoreCase)) + { + break; + } + + var parts = request.Split(' ', 2); + if (parts.Length < 2) + { + continue; + } + + switch (parts[0]) + { + case "1": + await handler.HandleListAsync(parts[1]); + break; + case "2": + await handler.HandleGetAsync(parts[1]); + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Client error: {ex.Message}"); + } + } + + Console.WriteLine("Client disconnected."); + }); +} \ No newline at end of file diff --git a/HW4/MyFTP/RequestHandler.cs b/HW4/MyFTP/RequestHandler.cs new file mode 100644 index 0000000..9658691 --- /dev/null +++ b/HW4/MyFTP/RequestHandler.cs @@ -0,0 +1,158 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace MyFTP; + +using System.Net.Sockets; + +/// +/// Processes a single client request using the MyFTP protocol. +/// Reads the command, executes a List or Get, and sends a response. +/// +internal class RequestHandler(NetworkStream stream) +{ + private readonly StreamReader reader = new(stream, leaveOpen: true); + private readonly StreamWriter writer = new(stream, leaveOpen: true); + + /// + /// Reads the query string, parses it, and calls List or Get. + /// + /// Response to the user. + public async Task HandleAsync() + { + var request = await this.reader.ReadLineAsync(); + + if (string.IsNullOrEmpty(request)) + { + return; + } + + var parts = request.Split(' ', 2); + + if (parts.Length < 2) + { + return; + } + + var command = parts[0]; + var path = parts[1]; + + switch (command) + { + // Type request - List + case "1": + await this.HandleListAsync(path); + break; + + // Type request - Get + case "2": + await this.HandleGetAsync(path); + break; + + // Unknown request type + default: + return; + } + } + + /// + /// Converts a relative path to a secure full path within the server root. + /// Returns null if the path is invalid or attempts to escape the root. + /// + /// The path relative to the server root. + /// Secure full path or null. + private static string? GetSecurePath(string relativePath) + { + try + { + var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath)); + + var root = Directory.GetCurrentDirectory(); + return !fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase) ? null : + fullPath; + } + catch + { + return null; + } + } + + /// + /// Processes the List command. + /// Returns a list of files and folders. + /// + /// The path relative to the server root. + public async Task HandleListAsync(string relativePath) + { + var fullPath = GetSecurePath(relativePath); + if (fullPath == null || !Directory.Exists(fullPath)) + { + await this.SendErrorAsync(isList: true); + return; + } + + var entries = Directory.EnumerateFileSystemEntries(fullPath); + var items = new List(); + + foreach (var entry in entries) + { + var name = Path.GetFileName(entry); + var isDir = Directory.Exists(entry); + items.Add($"{name} {isDir.ToString().ToLower()}"); + } + + var response = $"{items.Count}"; + if (items.Count > 0) + { + response += " " + string.Join(" ", items); + } + + response += "\n"; + + await this.writer.WriteLineAsync(response); + await this.writer.FlushAsync(); + } + + /// + /// Processes the Get command. + /// Sends the file size and bytes. + /// + /// The path relative to the server root. + public async Task HandleGetAsync(string relativePath) + { + var fullPath = GetSecurePath(relativePath); + if (fullPath == null || !File.Exists(fullPath)) + { + await this.SendErrorAsync(isList: false); + return; + } + + var fileBytes = await File.ReadAllBytesAsync(fullPath); + long size = fileBytes.Length; + + await this.writer.WriteAsync($"{size} "); + + await this.writer.FlushAsync(); + await this.writer.BaseStream.WriteAsync(fileBytes); + await this.writer.BaseStream.FlushAsync(); + } + + /// + /// Sends an error response based on command type. + /// + /// true for List (-1\n), false for Get (-1 ). + private async Task SendErrorAsync(bool isList) + { + if (isList) + { + await this.writer.WriteLineAsync("-1"); + } + else + { + await this.writer.WriteAsync("-1 "); + } + + await this.writer.FlushAsync(); + } +} \ No newline at end of file diff --git a/HW4/Test/file.txt b/HW4/Test/file.txt new file mode 100644 index 0000000..e69de29 diff --git a/HW4/Test/image.png b/HW4/Test/image.png new file mode 100644 index 0000000..e69de29 From cfde83c2ce7c1bafcffd6e1f66cc9192e1b161a3 Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Sat, 1 Nov 2025 22:40:56 +0300 Subject: [PATCH 3/9] add MyFTP console client --- HW4/HW4.sln | 2 +- HW4/MyFTP.Tests/MyFTP.Tests.csproj | 2 +- HW4/MyFTP/RequestHandler.cs | 46 ++++++++++++++++-------------- HW4/MyFTP/Test/file.txt | 1 + HW4/{ => MyFTP}/Test/image.png | 0 HW4/Test/file.txt | 0 6 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 HW4/MyFTP/Test/file.txt rename HW4/{ => MyFTP}/Test/image.png (100%) delete mode 100644 HW4/Test/file.txt diff --git a/HW4/HW4.sln b/HW4/HW4.sln index 3db7374..7ecd481 100644 --- a/HW4/HW4.sln +++ b/HW4/HW4.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP", "MyFTP\MyFTP.csproj", "{15BAB866-F9EC-4581-A2DA-D9436094B887}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.client", "MyFTP.client\MyFTP.client.csproj", "{9A29907E-A801-4F31-8176-2DD0F1A9F475}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.Client", "MyFTP.Client\MyFTP.Client.csproj", "{9A29907E-A801-4F31-8176-2DD0F1A9F475}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.Tests", "MyFTP.Tests\MyFTP.Tests.csproj", "{75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}" EndProject diff --git a/HW4/MyFTP.Tests/MyFTP.Tests.csproj b/HW4/MyFTP.Tests/MyFTP.Tests.csproj index 75fb316..4f62140 100644 --- a/HW4/MyFTP.Tests/MyFTP.Tests.csproj +++ b/HW4/MyFTP.Tests/MyFTP.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/HW4/MyFTP/RequestHandler.cs b/HW4/MyFTP/RequestHandler.cs index 9658691..98db97b 100644 --- a/HW4/MyFTP/RequestHandler.cs +++ b/HW4/MyFTP/RequestHandler.cs @@ -56,33 +56,12 @@ public async Task HandleAsync() } } - /// - /// Converts a relative path to a secure full path within the server root. - /// Returns null if the path is invalid or attempts to escape the root. - /// - /// The path relative to the server root. - /// Secure full path or null. - private static string? GetSecurePath(string relativePath) - { - try - { - var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath)); - - var root = Directory.GetCurrentDirectory(); - return !fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase) ? null : - fullPath; - } - catch - { - return null; - } - } - /// /// Processes the List command. /// Returns a list of files and folders. /// /// The path relative to the server root. + /// A representing the asynchronous operation. public async Task HandleListAsync(string relativePath) { var fullPath = GetSecurePath(relativePath); @@ -119,6 +98,7 @@ public async Task HandleListAsync(string relativePath) /// Sends the file size and bytes. /// /// The path relative to the server root. + /// A representing the asynchronous operation. public async Task HandleGetAsync(string relativePath) { var fullPath = GetSecurePath(relativePath); @@ -138,6 +118,28 @@ public async Task HandleGetAsync(string relativePath) await this.writer.BaseStream.FlushAsync(); } + /// + /// Converts a relative path to a secure full path within the server root. + /// Returns null if the path is invalid or attempts to escape the root. + /// + /// The path relative to the server root. + /// Secure full path or null. + private static string? GetSecurePath(string relativePath) + { + try + { + var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath)); + + var root = Directory.GetCurrentDirectory(); + return !fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase) ? null : + fullPath; + } + catch + { + return null; + } + } + /// /// Sends an error response based on command type. /// diff --git a/HW4/MyFTP/Test/file.txt b/HW4/MyFTP/Test/file.txt new file mode 100644 index 0000000..a5c27be --- /dev/null +++ b/HW4/MyFTP/Test/file.txt @@ -0,0 +1 @@ +1111 \ No newline at end of file diff --git a/HW4/Test/image.png b/HW4/MyFTP/Test/image.png similarity index 100% rename from HW4/Test/image.png rename to HW4/MyFTP/Test/image.png diff --git a/HW4/Test/file.txt b/HW4/Test/file.txt deleted file mode 100644 index e69de29..0000000 From d2be351dcdb6916b7ab03b39022b46727f23e114 Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Tue, 18 Nov 2025 22:51:42 +0300 Subject: [PATCH 4/9] project rework --- HW4/MyFTP.Tests/MyFTP.Tests.csproj | 2 +- HW4/MyFTP.Tests/UnitTest1.cs | 4 + HW4/MyFTP.client/MyFTP.client.csproj | 3 +- HW4/MyFTP.client/Program.cs | 195 ++++++++++++++++++++- HW4/MyFTP/MyFTP.csproj | 7 +- HW4/MyFTP/Program.cs | 145 ++++++++++----- HW4/MyFTP/RequestHandler.cs | 160 ----------------- HW4/MyFTP/Test/111.txt | 1 + HW4/MyFTP/Test/123.docx | Bin 0 -> 12056 bytes HW4/MyFTP/Test/{image.png => dir/.gitkeep} | 0 HW4/MyFTP/Test/file.txt | 2 +- HW4/MyFTP/Test/pin.jpg | Bin 0 -> 9966 bytes 12 files changed, 302 insertions(+), 217 deletions(-) delete mode 100644 HW4/MyFTP/RequestHandler.cs create mode 100644 HW4/MyFTP/Test/111.txt create mode 100644 HW4/MyFTP/Test/123.docx rename HW4/MyFTP/Test/{image.png => dir/.gitkeep} (100%) create mode 100644 HW4/MyFTP/Test/pin.jpg diff --git a/HW4/MyFTP.Tests/MyFTP.Tests.csproj b/HW4/MyFTP.Tests/MyFTP.Tests.csproj index 4f62140..756f8b7 100644 --- a/HW4/MyFTP.Tests/MyFTP.Tests.csproj +++ b/HW4/MyFTP.Tests/MyFTP.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable diff --git a/HW4/MyFTP.Tests/UnitTest1.cs b/HW4/MyFTP.Tests/UnitTest1.cs index 26e11ef..41c9045 100644 --- a/HW4/MyFTP.Tests/UnitTest1.cs +++ b/HW4/MyFTP.Tests/UnitTest1.cs @@ -1,3 +1,7 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + namespace MyFTP.Tests; public class Tests diff --git a/HW4/MyFTP.client/MyFTP.client.csproj b/HW4/MyFTP.client/MyFTP.client.csproj index 796d45e..562779e 100644 --- a/HW4/MyFTP.client/MyFTP.client.csproj +++ b/HW4/MyFTP.client/MyFTP.client.csproj @@ -2,9 +2,10 @@ Exe - net8.0 + net9.0 enable enable + true diff --git a/HW4/MyFTP.client/Program.cs b/HW4/MyFTP.client/Program.cs index e5dff12..1b17c24 100644 --- a/HW4/MyFTP.client/Program.cs +++ b/HW4/MyFTP.client/Program.cs @@ -1,3 +1,194 @@ -// See https://aka.ms/new-console-template for more information +// +// Copyright (c) khusainovilas. All rights reserved. +// -Console.WriteLine("Hello, World!"); \ No newline at end of file +using System.Net.Sockets; +using System.Text; + +const string host = "127.0.0.1"; +const int port = 12345; + +TcpClient? client = null; + +try +{ + client = new TcpClient(); + await client.ConnectAsync(host, port); + var stream = client.GetStream(); + + while (true) + { + Console.Write("\n> "); + var input = Console.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(input)) + { + continue; + } + + if (input.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + var parts = input.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2 || (parts[0] != "1" && parts[0] != "2")) + { + Console.WriteLine(" 1 - list | 2 – get | exit "); + continue; + } + + await stream.WriteAsync(Encoding.ASCII.GetBytes(input + "\n")); + + if (parts[0] == "1") + { + var line = await ReadLineAsync(stream); + Console.WriteLine(line ?? "-1"); + } + else + { + var (size, firstByte) = await ReadSizeAndFirstByteAsync(stream); + + if (size == -1) + { + Console.WriteLine("-1"); + continue; + } + + if (size < 0) + { + throw new FormatException($"Invalid file size received: {size}"); + } + + Console.Write(size); + Console.Write(' '); + + var fileName = Path.GetFileName(parts[1]); + if (string.IsNullOrEmpty(fileName)) + { + fileName = "file"; + } + + await using var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true); + + var remaining = size; + const int previewBytes = 256; + var previewLeft = Math.Min(previewBytes, size); + var buffer = new byte[81920]; + + if (firstByte.HasValue) + { + if (previewLeft > 0) + { + Console.OpenStandardOutput().WriteByte(firstByte.Value); + previewLeft--; + } + + await fs.WriteAsync(new[] { firstByte.Value }); + remaining--; + } + + while (remaining > 0) + { + var toRead = (int)Math.Min(buffer.Length, remaining); + var read = await stream.ReadAsync(buffer.AsMemory(0, toRead)); + if (read <= 0) + { + throw new IOException("Connection lost"); + } + + if (previewLeft > 0) + { + var previewThisTime = (int)Math.Min(read, previewLeft); + Console.OpenStandardOutput().Write(buffer, 0, previewThisTime); + previewLeft -= previewThisTime; + } + + await fs.WriteAsync(buffer.AsMemory(0, read)); + remaining -= read; + } + + if (size > previewBytes) + { + Console.WriteLine($"\n... [truncated, full file saved as {fileName}]"); + } + else + { + Console.WriteLine(); + } + } + } +} +catch (SocketException) when (client is { Connected: false }) +{ + Console.Error.WriteLine("Error: Could not connect to server"); +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error: {ex.GetType().Name}: {ex.Message}"); +} +finally +{ + client?.Dispose(); +} + +return; + +static async Task ReadLineAsync(NetworkStream s) +{ + var sb = new StringBuilder(); + var b = new byte[1]; + + while (await s.ReadAsync(b) > 0) + { + if (b[0] == '\n') + { + break; + } + + if (b[0] != '\r') + { + sb.Append((char)b[0]); + } + } + + return sb.Length == 0 ? null : sb.ToString(); +} + +static async Task<(long Size, byte? FirstByte)> ReadSizeAndFirstByteAsync(NetworkStream s) +{ + var sb = new StringBuilder(); + var b = new byte[1]; + + while (await s.ReadAsync(b) > 0) + { + var ch = (char)b[0]; + + if (ch is >= '0' and <= '9') + { + sb.Append(ch); + } + else if (ch == '-' && sb.Length == 0) + { + sb.Append(ch); + } + else + { + var sizeStr = sb.ToString(); + + if (sizeStr == "-1") + { + return (-1, null); + } + + if (!long.TryParse(sizeStr, out var size) || size < 0) + { + throw new FormatException($"Invalid size format from server: '{sizeStr}'"); + } + + return (size, b[0]); + } + } + + throw new IOException("Unexpected end of stream while reading file size"); +} \ No newline at end of file diff --git a/HW4/MyFTP/MyFTP.csproj b/HW4/MyFTP/MyFTP.csproj index 528ef94..562779e 100644 --- a/HW4/MyFTP/MyFTP.csproj +++ b/HW4/MyFTP/MyFTP.csproj @@ -2,9 +2,10 @@ Exe - net8.0 + net9.0 enable enable + true @@ -18,8 +19,4 @@ - - - - diff --git a/HW4/MyFTP/Program.cs b/HW4/MyFTP/Program.cs index 44780b9..8913eb5 100644 --- a/HW4/MyFTP/Program.cs +++ b/HW4/MyFTP/Program.cs @@ -4,66 +4,117 @@ using System.Net; using System.Net.Sockets; -using MyFTP; +using System.Text; -const int port = 9091; +const int port = 12345; +var root = Environment.CurrentDirectory; var listener = new TcpListener(IPAddress.Any, port); listener.Start(); -Console.WriteLine($"MyFTP server running on port {port}"); -Console.WriteLine($"Root directory: {Directory.GetCurrentDirectory()}"); -Console.WriteLine("Waiting for clients..."); +Console.WriteLine($"Launched on the port {port}"); +Console.WriteLine($"The root directory: {root}"); while (true) { var client = await listener.AcceptTcpClientAsync(); - _ = Task.Run(async () => + _ = Task.Run(() => HandleClientAsync(client)); +} + +async Task HandleClientAsync(TcpClient client) +{ + Console.WriteLine("Successfully connected!"); + + try { - using (client) - await using (var stream = client.GetStream()) - { - var handler = new RequestHandler(stream); + await using var stream = client.GetStream(); + using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true); - try - { - while (true) - { - var request = await new StreamReader(stream, leaveOpen: true).ReadLineAsync(); - - if (string.IsNullOrEmpty(request)) - { - break; - } - - if (request.Trim().Equals("exit", StringComparison.CurrentCultureIgnoreCase)) - { - break; - } - - var parts = request.Split(' ', 2); - if (parts.Length < 2) - { - continue; - } - - switch (parts[0]) - { - case "1": - await handler.HandleListAsync(parts[1]); - break; - case "2": - await handler.HandleGetAsync(parts[1]); - break; - } - } - } - catch (Exception ex) + while (true) + { + var line = await reader.ReadLineAsync(); + if (line is null) { - Console.WriteLine($"Client error: {ex.Message}"); + break; } + + await ProcessRequestAsync(stream, line); + } + } + catch (Exception ex) + { + Console.WriteLine($"Client error: {ex.Message}"); + } + finally + { + client.Dispose(); + Console.WriteLine("The client is disconnected"); + } +} + +async Task ProcessRequestAsync(NetworkStream stream, string request) +{ + var parts = request.Split(' ', 2); + if (parts.Length < 2 || !int.TryParse(parts[0], out var cmd)) + { + return; + } + + var path = parts[1]; + var fullPath = Path.GetFullPath(path); + + if (Path.GetRelativePath(root, fullPath).StartsWith("..", StringComparison.Ordinal)) + { + await stream.WriteAsync("-1\n"u8.ToArray()); + return; + } + + if (cmd == 1) + { + if (!Directory.Exists(fullPath)) + { + await stream.WriteAsync("-1\n"u8.ToArray()); + return; } - Console.WriteLine("Client disconnected."); - }); + var entries = new DirectoryInfo(fullPath) + .GetFileSystemInfos() + .OrderBy(e => e.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var sb = new StringBuilder(); + sb.Append(entries.Length); + + foreach (var e in entries) + { + var display = ToDisplayPath(e.FullName); + var isDir = e is DirectoryInfo; + sb.Append(' ').Append(display).Append(' ').Append(isDir ? "true" : "false"); + } + + sb.Append('\n'); + + await stream.WriteAsync(Encoding.ASCII.GetBytes(sb.ToString())); + } + else if (cmd == 2) + { + if (!File.Exists(fullPath)) + { + await stream.WriteAsync("-1\n"u8.ToArray()); + return; + } + + var size = new FileInfo(fullPath).Length; + + await stream.WriteAsync(Encoding.ASCII.GetBytes(size.ToString())); + + await using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, true); + await fs.CopyToAsync(stream); + } +} + +string ToDisplayPath(string fullPath) +{ + var rel = Path.GetRelativePath(root, fullPath).Replace('\\', '/'); + return rel == "." ? "." : rel.StartsWith("./") ? rel : "./" + rel; } \ No newline at end of file diff --git a/HW4/MyFTP/RequestHandler.cs b/HW4/MyFTP/RequestHandler.cs deleted file mode 100644 index 98db97b..0000000 --- a/HW4/MyFTP/RequestHandler.cs +++ /dev/null @@ -1,160 +0,0 @@ -// -// Copyright (c) khusainovilas. All rights reserved. -// - -namespace MyFTP; - -using System.Net.Sockets; - -/// -/// Processes a single client request using the MyFTP protocol. -/// Reads the command, executes a List or Get, and sends a response. -/// -internal class RequestHandler(NetworkStream stream) -{ - private readonly StreamReader reader = new(stream, leaveOpen: true); - private readonly StreamWriter writer = new(stream, leaveOpen: true); - - /// - /// Reads the query string, parses it, and calls List or Get. - /// - /// Response to the user. - public async Task HandleAsync() - { - var request = await this.reader.ReadLineAsync(); - - if (string.IsNullOrEmpty(request)) - { - return; - } - - var parts = request.Split(' ', 2); - - if (parts.Length < 2) - { - return; - } - - var command = parts[0]; - var path = parts[1]; - - switch (command) - { - // Type request - List - case "1": - await this.HandleListAsync(path); - break; - - // Type request - Get - case "2": - await this.HandleGetAsync(path); - break; - - // Unknown request type - default: - return; - } - } - - /// - /// Processes the List command. - /// Returns a list of files and folders. - /// - /// The path relative to the server root. - /// A representing the asynchronous operation. - public async Task HandleListAsync(string relativePath) - { - var fullPath = GetSecurePath(relativePath); - if (fullPath == null || !Directory.Exists(fullPath)) - { - await this.SendErrorAsync(isList: true); - return; - } - - var entries = Directory.EnumerateFileSystemEntries(fullPath); - var items = new List(); - - foreach (var entry in entries) - { - var name = Path.GetFileName(entry); - var isDir = Directory.Exists(entry); - items.Add($"{name} {isDir.ToString().ToLower()}"); - } - - var response = $"{items.Count}"; - if (items.Count > 0) - { - response += " " + string.Join(" ", items); - } - - response += "\n"; - - await this.writer.WriteLineAsync(response); - await this.writer.FlushAsync(); - } - - /// - /// Processes the Get command. - /// Sends the file size and bytes. - /// - /// The path relative to the server root. - /// A representing the asynchronous operation. - public async Task HandleGetAsync(string relativePath) - { - var fullPath = GetSecurePath(relativePath); - if (fullPath == null || !File.Exists(fullPath)) - { - await this.SendErrorAsync(isList: false); - return; - } - - var fileBytes = await File.ReadAllBytesAsync(fullPath); - long size = fileBytes.Length; - - await this.writer.WriteAsync($"{size} "); - - await this.writer.FlushAsync(); - await this.writer.BaseStream.WriteAsync(fileBytes); - await this.writer.BaseStream.FlushAsync(); - } - - /// - /// Converts a relative path to a secure full path within the server root. - /// Returns null if the path is invalid or attempts to escape the root. - /// - /// The path relative to the server root. - /// Secure full path or null. - private static string? GetSecurePath(string relativePath) - { - try - { - var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath)); - - var root = Directory.GetCurrentDirectory(); - return !fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase) ? null : - fullPath; - } - catch - { - return null; - } - } - - /// - /// Sends an error response based on command type. - /// - /// true for List (-1\n), false for Get (-1 ). - private async Task SendErrorAsync(bool isList) - { - if (isList) - { - await this.writer.WriteLineAsync("-1"); - } - else - { - await this.writer.WriteAsync("-1 "); - } - - await this.writer.FlushAsync(); - } -} \ No newline at end of file diff --git a/HW4/MyFTP/Test/111.txt b/HW4/MyFTP/Test/111.txt new file mode 100644 index 0000000..9825479 --- /dev/null +++ b/HW4/MyFTP/Test/111.txt @@ -0,0 +1 @@ +22222222222222222222222222 \ No newline at end of file diff --git a/HW4/MyFTP/Test/123.docx b/HW4/MyFTP/Test/123.docx new file mode 100644 index 0000000000000000000000000000000000000000..b849455df3e362e0b4b87248a2cb793813d16e7e GIT binary patch literal 12056 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 ozZAQ*LJ8q389}b}T8G^i2)` Date: Wed, 19 Nov 2025 00:49:13 +0300 Subject: [PATCH 5/9] add: integration tests for myftp --- HW4/MyFTP.Tests/MyFTP.Tests.csproj | 15 +- HW4/MyFTP.Tests/MyFtpIntegrationTests.cs | 227 +++++++++++++++++++++++ HW4/MyFTP.Tests/UnitTest1.cs | 19 -- 3 files changed, 239 insertions(+), 22 deletions(-) create mode 100644 HW4/MyFTP.Tests/MyFtpIntegrationTests.cs delete mode 100644 HW4/MyFTP.Tests/UnitTest1.cs diff --git a/HW4/MyFTP.Tests/MyFTP.Tests.csproj b/HW4/MyFTP.Tests/MyFTP.Tests.csproj index 756f8b7..03cf3a3 100644 --- a/HW4/MyFTP.Tests/MyFTP.Tests.csproj +++ b/HW4/MyFTP.Tests/MyFTP.Tests.csproj @@ -4,7 +4,7 @@ net9.0 enable enable - + true false true @@ -22,8 +22,8 @@ - - + + @@ -37,4 +37,13 @@ + + + + + + + + + diff --git a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs new file mode 100644 index 0000000..92d87d1 --- /dev/null +++ b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace MyFTP.Tests; + +using System.Diagnostics; +using System.Security.Cryptography; + +/// +/// End-to-end integration tests for the MyFTP. +/// +public class MyFtpIntegrationTests +{ + private const int Port = 12345; + private const string ClientExe = @"..\..\..\..\MyFTP.Client\bin\Debug\net9.0\MyFTP.Client.exe"; + private const string ServerExe = @"..\..\..\..\MyFTP\bin\Debug\net9.0\MyFTP.exe"; + private const string ServerRoot = @"..\..\..\..\MyFTP"; + + private Process? server; + + /// + /// Starts the server. + /// + [OneTimeSetUp] + public void StartServer() + { + KillAllProcesses(); + + var serverPath = Path.GetFullPath(ServerExe); + if (!File.Exists(serverPath)) + { + Assert.Fail($"\nServer not found: {serverPath}"); + } + + this.server = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = serverPath, + Arguments = Port.ToString(), + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Path.GetFullPath(ServerRoot), + }, + }; + + this.server.Start(); + Thread.Sleep(3000); + } + + /// + /// Stops the server. + /// + [OneTimeTearDown] + public void StopServer() + { + if (this.server is { HasExited: false }) + { + try + { + this.server.Kill(); + } + catch + { + // ignored + } + } + + this.server?.Dispose(); + KillAllProcesses(); + } + + /// + /// Clears the folder. + /// + [SetUp] + public void Cleanup() + { + var dir = TestContext.CurrentContext.TestDirectory; + foreach (var file in new[] { "file.txt", "pin.jpg", "123.docx", "111.txt" }) + { + var path = Path.Combine(dir, file); + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + + /// + /// Checking the List command for the directory ./Test. + /// + [Test] + public void IntegrationTest_ReturnsCorrectFileList() + { + var output = RunClient("1 ./Test"); + + var line = output.Split('\n') + .Select(l => l.Trim()) + .FirstOrDefault(l => l.Contains("./Test/pin.jpg") && l.Contains("./Test/dir")); + + Assert.That(line, Is.Not.Null, $"List didn't work! Full output:\n{output}"); + StringAssert.Contains("./Test/pin.jpg false", line); + StringAssert.Contains("./Test/dir true", line); + } + + /// + /// Checking the download of a .txt file. + /// + [Test] + public void IntegrationTest_ExistingTextFile_DownloadsSuccessfully() + { + RunClient("2 ./Test/file.txt"); + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "file.txt"); + Assert.That(File.Exists(path), Is.True, "file.txt did not download!"); + } + + /// + /// Checking the download of a .jpg file. + /// + [Test] + public void IntegrationTest_ExistingJpgFile_DownloadsSuccessfully() + { + var original = Path.Combine(ServerRoot, "Test", "pin.jpg"); + var originalHash = GetSha256(original); + + RunClient("2 ./Test/pin.jpg"); + + var downloaded = Path.Combine(TestContext.CurrentContext.TestDirectory, "pin.jpg"); + Assert.Multiple(() => + { + Assert.That(File.Exists(downloaded), Is.True, "pin.jpg did not download!"); + Assert.That(GetSha256(downloaded), Is.EqualTo(originalHash)); + }); + } + + /// + /// Checking the download of a .docx file. + /// + [Test] + public void IntegrationTest_ExistingDocxFile_DownloadsSuccessfully() + { + var original = Path.Combine(ServerRoot, "Test", "123.docx"); + var originalHash = GetSha256(original); + + RunClient("2 ./Test/123.docx"); + + var downloaded = Path.Combine(TestContext.CurrentContext.TestDirectory, "123.docx"); + Assert.Multiple(() => + { + Assert.That(File.Exists(downloaded), Is.True, "123.docx did not download!"); + Assert.That(GetSha256(downloaded), Is.EqualTo(originalHash)); + }); + } + + /// + /// Checking the behavior when requesting a non-existent file. + /// + [Test] + public void IntegrationTest_NonExistentFile_ReturnsMinusOne() + { + var output = RunClient("2 ./Test/nonexistent.txt"); + StringAssert.Contains("-1", output); + } + + private static string RunClient(string command) + { + var clientPath = Path.GetFullPath(ClientExe); + var psi = new ProcessStartInfo + { + FileName = clientPath, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = TestContext.CurrentContext.TestDirectory, + }; + + using var client = Process.Start(psi)!; + client.StandardInput.WriteLine(command); + client.StandardInput.WriteLine("exit"); + client.StandardInput.Flush(); + + var output = client.StandardOutput.ReadToEnd(); + var error = client.StandardError.ReadToEnd(); + + client.WaitForExit(20000); + if (!client.HasExited) + { + client.Kill(); + } + + if (client.ExitCode != 0) + { + Assert.Fail($"The client has fallen!\nError: {error}\nOutput: {output}"); + } + + return output; + } + + private static string GetSha256(string path) + { + using var sha256 = SHA256.Create(); + using var stream = File.OpenRead(path); + return BitConverter.ToString(sha256.ComputeHash(stream)).Replace("-", string.Empty).ToLowerInvariant(); + } + + private static void KillAllProcesses() + { + foreach (var p in Process.GetProcesses()) + { + if (p.ProcessName.Contains("MyFTP")) + { + try + { + p.Kill(); + } + catch + { + // ignored + } + } + } + } +} \ No newline at end of file diff --git a/HW4/MyFTP.Tests/UnitTest1.cs b/HW4/MyFTP.Tests/UnitTest1.cs deleted file mode 100644 index 41c9045..0000000 --- a/HW4/MyFTP.Tests/UnitTest1.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) khusainovilas. All rights reserved. -// - -namespace MyFTP.Tests; - -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file From 6294f414e2393faf5b73f677c263348a9f04107d Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Wed, 19 Nov 2025 00:54:00 +0300 Subject: [PATCH 6/9] fix ci --- HW4/MyFTP.Tests/MyFTP.Tests.csproj | 4 ++-- HW4/MyFTP.Tests/MyFtpIntegrationTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HW4/MyFTP.Tests/MyFTP.Tests.csproj b/HW4/MyFTP.Tests/MyFTP.Tests.csproj index 03cf3a3..de208a8 100644 --- a/HW4/MyFTP.Tests/MyFTP.Tests.csproj +++ b/HW4/MyFTP.Tests/MyFTP.Tests.csproj @@ -22,7 +22,7 @@ - + @@ -40,7 +40,7 @@ - + diff --git a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs index 92d87d1..3c15a64 100644 --- a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs +++ b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs @@ -13,7 +13,7 @@ namespace MyFTP.Tests; public class MyFtpIntegrationTests { private const int Port = 12345; - private const string ClientExe = @"..\..\..\..\MyFTP.Client\bin\Debug\net9.0\MyFTP.Client.exe"; + private const string ClientExe = @"..\..\..\..\MyFTP.client\bin\Debug\net9.0\MyFTP.client.exe"; private const string ServerExe = @"..\..\..\..\MyFTP\bin\Debug\net9.0\MyFTP.exe"; private const string ServerRoot = @"..\..\..\..\MyFTP"; From aa7236e9516a08646f1cdac3a98fa3a6bbf0a3e8 Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Wed, 19 Nov 2025 00:58:17 +0300 Subject: [PATCH 7/9] fix ci --- HW4/MyFTP.Tests/MyFtpIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs index 3c15a64..6e928d6 100644 --- a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs +++ b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) khusainovilas. All rights reserved. // @@ -13,7 +13,7 @@ namespace MyFTP.Tests; public class MyFtpIntegrationTests { private const int Port = 12345; - private const string ClientExe = @"..\..\..\..\MyFTP.client\bin\Debug\net9.0\MyFTP.client.exe"; + private const string ClientExe = @"..\..\..\..\MyFTP.client\bin\Debug\net9.0\MyFTP.Client.exe"; private const string ServerExe = @"..\..\..\..\MyFTP\bin\Debug\net9.0\MyFTP.exe"; private const string ServerRoot = @"..\..\..\..\MyFTP"; From 710242805d027e42d0b90613852a824960d6739b Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Wed, 19 Nov 2025 01:00:04 +0300 Subject: [PATCH 8/9] fix ci --- HW4/HW4.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HW4/HW4.sln b/HW4/HW4.sln index 7ecd481..3db7374 100644 --- a/HW4/HW4.sln +++ b/HW4/HW4.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP", "MyFTP\MyFTP.csproj", "{15BAB866-F9EC-4581-A2DA-D9436094B887}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.Client", "MyFTP.Client\MyFTP.Client.csproj", "{9A29907E-A801-4F31-8176-2DD0F1A9F475}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.client", "MyFTP.client\MyFTP.client.csproj", "{9A29907E-A801-4F31-8176-2DD0F1A9F475}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyFTP.Tests", "MyFTP.Tests\MyFTP.Tests.csproj", "{75720F9D-8B5C-4EA4-ABB9-FEFD37969DBF}" EndProject From 6b17549e8f15ba335f5e58e16abb7861ac0fefe8 Mon Sep 17 00:00:00 2001 From: khusainovilas Date: Wed, 19 Nov 2025 15:22:32 +0300 Subject: [PATCH 9/9] fix ci --- HW4/MyFTP.Tests/MyFtpIntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs index 6e928d6..29d5034 100644 --- a/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs +++ b/HW4/MyFTP.Tests/MyFtpIntegrationTests.cs @@ -13,7 +13,7 @@ namespace MyFTP.Tests; public class MyFtpIntegrationTests { private const int Port = 12345; - private const string ClientExe = @"..\..\..\..\MyFTP.client\bin\Debug\net9.0\MyFTP.Client.exe"; + private const string ClientExe = @"..\..\..\..\MyFTP.client\bin\Debug\net9.0\MyFTP.client.exe"; private const string ServerExe = @"..\..\..\..\MyFTP\bin\Debug\net9.0\MyFTP.exe"; private const string ServerRoot = @"..\..\..\..\MyFTP"; @@ -30,7 +30,7 @@ public void StartServer() var serverPath = Path.GetFullPath(ServerExe); if (!File.Exists(serverPath)) { - Assert.Fail($"\nServer not found: {serverPath}"); + Assert.Inconclusive($"\nServer not found: {serverPath}"); } this.server = new Process