FTP клиент на C#

192

Представленный в данной статье пример FTP клиента можно загрузить по ссылке - FtpClient.rar.

Обзор FTP

File Transfer Protocol (FTP) — это протокол прикладного уровня, построенный поверх протокола транспортного уровня, обычно поверх TCP. Он используется для копирования файлов с удаленного сервера и на удаленный сервер.

Принцип работы FTP заключается в следующем: сначала открывается соединение TCP с сервером, отправляются текстовые команды для выполнения таких действий, как извлечение файла с сервера, и сервер возвращает трехразрядный код (вместе с сообщением, удобным для восприятия человеком), чтобы показать состояние запрошенного действия.

В FTP используются два разных соединения — управляющее соединение, на котором посылаются команды и получаются ответы сервера, и соединение для данных, используемое для самой передачи файлов с сервера или на сервер. По умолчанию сервер слушает команды от клиента на порту 21, а когда нужно отправлять данные, открывает второе соединение с портом 20 клиента.

Активный и пассивный режимы

Соединение для данных открывается, если только отправлена команда копирования файла. В активном режиме (который установлен по умолчанию) клиент должен слушать соединения. Когда потребуется отправить данные, FTP-сервер откроет соединение с этим сокетом и передаст данные к клиенту.

При таком подходе проблема заключается в том, что большинство конфигураций брандмауэров не позволит извне установить соединения с машинами за брандмауэром, а разрешит только те соединения, которые были инициализированы из-за брандмауэра. В FTP эта проблема решается вводом пассивного режима. В этом случае клиент отправляет команду, указывающую, что используется пассивный режим, и на нее сервер отвечает номером порта, на котором он слушает. Когда нужно отправить данные, вместо того чтобы слушать запрос от сервера, клиент может открыть соединение с указанным портом. В нашей реализации будет использоваться пассивный режим.

FTP-команды

Спецификация FTP определяет несколько команд для аутентификации, копирования файлов на сервер и с сервера и изменения каталога на сервере. В нашем коде не будем пользоваться всеми командами, поэтому рассмотрим только те, которые нам понадобятся:

Команды FTP
Команда Описание
USER <имя пользователя> Имя пользователя, которое нужно удостоверить на сервере
PASS <пароль> Пароль, связанный с именем пользователя
RETR <имя файла> Скопировать с сервера указанный файл
ST0R <имя файла> Скопировать файл на сервер и сохранить его в указанном месте
TYPE <индикатор типа> Формат данных. Может принимать одно значение из следующего перечня: A — ASCII, Е — EBCDIC, I — изображение (двоичные данные), L <размер байта> — локальный размер байта
PASV Использовать пассивный режим
STAT Принуждает сервер отправить сообщение состояния клиенту. Команда может использоваться в ходе передачи данных, чтобы указать состояние операции
QUIT Закрывает соединение с сервером

Коды состояния FTP

Трехразрядные коды FTP упорядочены в соответствии со степенью детализации, предоставленной разрядом кода: в первом разряде дается общее указание о состоянии команды; второй разряд указывает общий тип возникшей ошибки; в третьем разряде содержится более конкретная информация. Первый разряд может принимать следующие значения:

Вот некоторые конкретные ответы, которые будут обрабатываться:

Код Описание
125 Открыто соединение для данных — начинается передача.
150 Готовность к открытию соединения для данных.
200 Команда принята
220 Обслуживание готово для нового пользователя.
227 Вход в пассивный режим.
230 Пользователь вошел в систему.
331 Имя пользователя принято — отправляйте пароль.

Кодирование FTP-клиента

Ниже представлен код FTP-клиента, реализованного на WPF с использованием собственной реализации класса FtpWebRequest в виде класса Client:

// Реализация класса FtpWebRequest
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;

namespace FtpClient
{
	public class Client {
		private string password;
		private string userName;
		private string uri;
		private int bufferSize = 1024;
 
		public bool Passive = true;
		public bool Binary = true;
		public bool EnableSsl = false;
		public bool Hash = false;
		
		public Client(string uri, string userName, string password) {
			this.uri = uri;
			this.userName = userName;
			this.password = password;
		}
 
		public string ChangeWorkingDirectory(string path) {
			uri = combine(uri, path);
 
			return PrintWorkingDirectory();
		}
 
		public string DeleteFile(string fileName) {
			var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.DeleteFile);
			
			return getStatusDescription(request);
		}
 
		public string DownloadFile(string source, string dest) {
			var request = createRequest(combine(uri, source), WebRequestMethods.Ftp.DownloadFile);
			
			byte[] buffer = new byte[bufferSize];
 
			using (var response = (FtpWebResponse)request.GetResponse()) {
				using (var stream = response.GetResponseStream()) {
					using (var fs = new FileStream(dest, FileMode.OpenOrCreate)) {
						int readCount = stream.Read(buffer, 0, bufferSize);
 
						while (readCount > 0) {
							if (Hash)
								Console.Write("#");
 
							fs.Write(buffer, 0, readCount);
							readCount = stream.Read(buffer, 0, bufferSize);
						}
					}
				}
 
				return response.StatusDescription;
			}
		}
 
		public DateTime GetDateTimestamp(string fileName) {
			var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.GetDateTimestamp);
			
			using (var response = (FtpWebResponse)request.GetResponse()) {
				return response.LastModified;
			}
		}
 
		public long GetFileSize(string fileName) {
			var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.GetFileSize);
			
			using (var response = (FtpWebResponse)request.GetResponse()) {
				return response.ContentLength;
			}
		}
 
		public string[] ListDirectory() {
			var list = new List<string>();
 
			var request = createRequest(WebRequestMethods.Ftp.ListDirectory);
			
			using (var response = (FtpWebResponse)request.GetResponse()) {
				using (var stream = response.GetResponseStream()) {
					using (var reader = new StreamReader(stream, true)) {
						while (!reader.EndOfStream) {
							list.Add(reader.ReadLine());
						}
					}
				}
			}
 
			return list.ToArray();
		}
 
		public string[] ListDirectoryDetails() {
			var list = new List<string>();
 
			var request = createRequest(WebRequestMethods.Ftp.ListDirectoryDetails);
			
			using (var response = (FtpWebResponse)request.GetResponse()) {
				using (var stream = response.GetResponseStream()) {
					using (var reader = new StreamReader(stream, true)) {
						while (!reader.EndOfStream) {
							list.Add(reader.ReadLine());
						}
					}
				}
			}
 
			return list.ToArray();
		}
 
		public string MakeDirectory(string directoryName) {
			var request = createRequest(combine(uri, directoryName), WebRequestMethods.Ftp.MakeDirectory);
			
			return getStatusDescription(request);
		}
 
		public string PrintWorkingDirectory() {
			var request = createRequest(WebRequestMethods.Ftp.PrintWorkingDirectory);
 
			return getStatusDescription(request);
		}
 
		public string RemoveDirectory(string directoryName) {
			var request = createRequest(combine(uri, directoryName), WebRequestMethods.Ftp.RemoveDirectory);
			
			return getStatusDescription(request);
		}
 
		public string Rename(string currentName, string newName) {
			var request = createRequest(combine(uri, currentName), WebRequestMethods.Ftp.Rename);
			
			request.RenameTo = newName;
 
			return getStatusDescription(request);
		}
 
		public string UploadFile(string source, string destination) {
			var request = createRequest(combine(uri, destination), WebRequestMethods.Ftp.UploadFile);
			
			using (var stream = request.GetRequestStream()) {
				using (var fileStream = System.IO.File.Open(source, FileMode.Open)) {
					int num;
 
					byte[] buffer = new byte[bufferSize];
 
					while ((num = fileStream.Read(buffer, 0, buffer.Length)) > 0) {
						if (Hash)
							Console.Write("#");
 
						stream.Write(buffer, 0, num);
					}
				}
			}
 
			return getStatusDescription(request);
		}
 
		public string UploadFileWithUniqueName(string source) {
			var request = createRequest(WebRequestMethods.Ftp.UploadFileWithUniqueName);
			
			using (var stream = request.GetRequestStream()) {
				using (var fileStream = System.IO.File.Open(source, FileMode.Open)) {
					int num;
 
					byte[] buffer = new byte[bufferSize];
 
					while ((num = fileStream.Read(buffer, 0, buffer.Length)) > 0) {
						if (Hash)
							Console.Write("#");
 
						stream.Write(buffer, 0, num);
					}
				}
			}
 
			using (var response = (FtpWebResponse)request.GetResponse()) {
				return Path.GetFileName(response.ResponseUri.ToString());
			}
		}
 
		private FtpWebRequest createRequest(string method) {
			return createRequest(uri, method);
		}
 
		private FtpWebRequest createRequest(string uri, string method) {
			var r = (FtpWebRequest)WebRequest.Create(uri);
 
			r.Credentials = new NetworkCredential(userName, password);
			r.Method = method;
			r.UseBinary = Binary;
			r.EnableSsl = EnableSsl;
			r.UsePassive = Passive;
 
			return r;
		}
 
		private string getStatusDescription(FtpWebRequest request) {
			using (var response = (FtpWebResponse)request.GetResponse()) {
				return response.StatusDescription;
			}
		}
 
		private string combine(string path1, string path2) {
			return Path.Combine(path1, path2).Replace("\\", "/");
		}
	}
}
<!-- Разметка окна -->
<Window x:Class="FtpClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="FTP клиент" Style="{DynamicResource MainWindowStyle}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Border BorderThickness="0,0,1,0" BorderBrush="White" Margin="5">
            <Grid Width="300">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="0.4*"/>
                    <ColumnDefinition Width="0.6*"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TextBlock Text="Адрес сервера" />
                <TextBox x:Name="txt_adres" Grid.Column="1" Text="ftp://77.222.61.135/"/>
                <TextBlock Text="Логин" Grid.Row="1"/>
                <TextBox x:Name="txt_login" Grid.Row="1" Grid.Column="1" Text="MyLogin"/>
                <TextBlock Text="Пароль" Grid.Row="2"/>
                <PasswordBox x:Name="txt_password" Grid.Row="2" Grid.Column="1" Password="123456"/>
                <Button x:Name="btn_connect" Content="Соединиться по FTP" Padding="10" Margin="0,10" 
                        Grid.Row="3" Grid.ColumnSpan="2" Width="180" HorizontalAlignment="Left" Click="btn_connect_Click_1" />
            </Grid>
        </Border>
        <ListView Grid.Column="1" Margin="5" x:Name="lbx_files" ItemsSource="{Binding}">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Файл/папка" Width="500">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal" Height="36" MouseLeftButtonDown="folder_Click">
                                        <Image Width="32" Height="32" Source="{Binding Type}" />
                                        <TextBlock Foreground="#DDD" Text="{Binding Name}" Margin="12,0" />
                                    </StackPanel>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Header="Размер" Width="200" DisplayMemberBinding="{Binding FileSize}"/>
                        <GridViewColumn Header="Дата создания" Width="200" DisplayMemberBinding="{Binding Date}"/>
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Net;
using System.IO;
using System.Text.RegularExpressions;

namespace FtpClient
{
    public partial class MainWindow : Window
    {
        string prevAdress = "ftp://";

        public MainWindow()
        {
            InitializeComponent();
        }

        private void btn_connect_Click_1(object sender, RoutedEventArgs e)
        {
            try
            {
                // Создаем объект подключения по FTP
                Client client = new Client(txt_adres.Text, txt_login.Text, txt_password.Password);

                // Регулярное выражение, которое ищет информацию о папках и файлах 
                // в строке ответа от сервера
                Regex regex = new Regex(@"^([d-])([rwxt-]{3}){3}\s+\d{1,}\s+.*?(\d{1,})\s+(\w+\s+\d{1,2}\s+(?:\d{4})?)(\d{1,2}:\d{2})?\s+(.+?)\s?$",
                    RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);

                // Получаем список корневых файлов и папок
                // Используется LINQ to Objects и регулярные выражения
                List<FileDirectoryInfo> list = client.ListDirectoryDetails()
                                                     .Select(s =>
                                                     {
                                                         Match match = regex.Match(s);
                                                         if (match.Length > 5)
                                                         {
                                                             // Устанавливаем тип, чтобы отличить файл от папки (используется также для установки рисунка)
                                                             string type = match.Groups[1].Value == "d" ? "DIR.png" : "FILE.png";

                                                             // Размер задаем только для файлов, т.к. для папок возвращается
                                                             // размер ярлыка 4кб, а не самой папки
                                                             string size = "";
                                                             if (type == "FILE.png")
                                                                 size = (Int32.Parse(match.Groups[3].Value.Trim()) / 1024).ToString() + " кБ";

                                                             return new FileDirectoryInfo(size, type, match.Groups[6].Value, match.Groups[4].Value, txt_adres.Text);
                                                         }
                                                         else return new FileDirectoryInfo();
                                                     }).ToList();

                // Добавить поле, которое будет возвращать пользователя на директорию выше
                list.Add(new FileDirectoryInfo("","DEFAULT.png","...","",txt_adres.Text));
                list.Reverse();

                // Отобразить список в ListView
                lbx_files.DataContext = list;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString() + ": \n" + ex.Message);
            }
        }

        private void folder_Click(object sender, MouseButtonEventArgs e)
        {
            if (e.ClickCount >= 2)
            {
                FileDirectoryInfo fdi = (FileDirectoryInfo)(sender as StackPanel).DataContext;
                if (fdi.Type == "DIR.png")
                {
                    prevAdress = fdi.adress;
                    txt_adres.Text = fdi.adress + fdi.Name + "/";
                    btn_connect_Click_1(null, null);
                }
            }

        }
    }
}
// Вспомогательный класс
public class FileDirectoryInfo
    {
        string fileSize;
        string type;
        string name;
        string date;
        public string adress;

        public string FileSize
        {
            get { return fileSize; }
            set { fileSize = value; }
        }

        public string Type
        {
            get { return type; }
            set { type = value; }
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public string Date
        {
            get { return date; }
            set { date = value; }
        }

        public FileDirectoryInfo() { }

        public FileDirectoryInfo(string fileSize, string type, string name, string date, string adress)
        {
            FileSize = fileSize;
            Type = type;
            Name = name;
            Date = date;
            this.adress = adress;
        }

    }

Обратите внимание, что это простая реализация FTP-клиента, которая просто представляет возможность просматривать файлы и папки на сервере, используя графический интерфейс пользователя WPF. При желании можно расширить функционал данного клиента, используя методы класса Client для загрузки/скачивания файлов, изменения директорий и т.д. Используя возможности WPF (тему DarkBlue и шаблон окна), в конечном итоге можно получить программу следующего вида (здесь я использовал подключение к серверу, на котором хоститься данный сайт):

FTP-клиент
Пройди тесты
Лучший чат для C# программистов