Press "Enter" to skip to content

Creare un client FTP con .net

Per terminare bene il 2018, in cui ho scritto pochissimo sul blog e per iniziare bene il 2019, pubblico un articolo che devo scrivere da novembre, serve a mostrare come utilizzare le classi FTP del framework .Net per produrre un helper che ci permetta di utilizzare un FTP dall’interno di una libreria .Net. La prima versione di questo helper è stata scritta parecchi anni fa a quattro mani da me e da San Luca.

Pertanto, vediamo cosa troverete nel codice a corredo di questo articolo e come funziona

La soluzione

La soluzione contiene 2 progetti, FTPHelper, che è la libreria riutilizzabile con le funzioni del client FTP e FtpMiniClient, che contiene una semplicissima applicazione WPF che mostra come utilizzare la classe FTP per collegarsi ad un server FTP, caricare o scaricare un file.

Se volete testare l’applicazione dovete procurarvi le credenziali di un Server FTP a cui connettervi con Username e Password. Nel mio caso ho fatto due test, il primo su un server FTP di Aruba, collegato ad un web hosting, il secondo l’ho fatto installando Filezilla Server sul mio PC di sviluppo, creando un utente a cui ho assegnato i permessi su una cartella di test ed utilizzando l’FTP per caricare e scaricare il file di test. In questo caso, per il test in locale Filezilla funziona correttamente anche se non ho modificato i permessi sul firewall di Windows 10 che mi gestisce la sicurezza.

Nella cartella Entities, ho inserito 3 classi che uso a supporto dell’helper, ne descrivo l’uso ma non il codice in quanto sono piuttosto semplici e ne trovate parte sugli articoli relativi alle librerie di uso comune Dotnetwork.

  • BaseError.cs – serve per descrivere un eventuale errore e fare in modo di intercettare gli errori e passarli alla user interface evitando eccezioni non gestite nelle classi helper.
  • FtpParametersItem.cs – Serve per fornire in un unico blocco tutti i parametri di connessione al server FTP quando instanziamo la classe helper invece di dover fornire una dozzina di parametri, in questo modo, l’eventuale aggiunta di un parametro non implica la modifica a tutti i metodi (e alle chiamate ai metodi) che lo usano.
  • Result.cs – E’ la classe Generic che ci permette di restituire oltre ai dati previsti da un metodo anche gli eventuali dati di errore.
  • FtpProxyConflictException.cs – Una classe eccezione specifica per gestire un errore sul proxy quando impostato.

La classe helper FTP.cs

La classe che fa tutto il lavoro e parla con il Server FTP.

using DNW.Base.FTP.Entities;
using DNW.Base.FTP.Resources;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;

Per prima cosa le clausole Using, che includono i namespace base utilizzati e quelli interni alla libreria helper, System.Net è il namespace che contiene le classi per la gestione web comprese le classi FTP.

public delegate void FtpProgressUpdatedEventHandler(object senderFtpProgressUpdatedEventArgs e);

Il primo elemento della classe è un delegate che ci permette di creare un handler per l’evento che la classe scatena per informare la user interface del progresso di un operazione di upload o download file.

public class FTP
{
	...
}

Questa è la nostra classe Helper, ed ora vediamo tutto ciò che vi abbiamo inserito.

public FTP(FtpParametersItem ftpConfig)
{
	FtpConfigData = ftpConfig;
}

Il costruttore, a cui passiamo la classe contenente tutti i dati di configurazione per il server FTP da contattare.

public FtpParametersItem FtpConfigData
{
	get
	{
		return mFtpConfigData;
	}
	private set
	{
		mFtpConfigData = value;
	}
}

La property dove vengono mappati i parametri di configurazione dal costruttore.

public Result<boolCheckFtpConnection()
{
	Result<boolret = new Result<bool>();
	try
	{
		string fullFtpAddress = BuildFullFtpAddress(string.Emptystring.Empty);
		FtpWebRequest request = (FtpWebRequest)FtpWebRequest.Create(fullFtpAddress);
 
		request.Method = WebRequestMethods.Ftp.PrintWorkingDirectory;
		request.Credentials = new NetworkCredential(FtpConfigData.UserNameFtpConfigData.UserPassword);
		request.UsePassive = FtpConfigData.PassiveMode;
		request.UseBinary = true;
		request.KeepAlive = FtpConfigData.KeepAlive;
 
		using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
		{
			response.Close();
		}
		ret.Data = true;
	}
	catch (System.Net.WebException wEx)
	{
		if (wEx.Status == WebExceptionStatus.ConnectFailure)
		{
			if (FtpConfigData.PassiveMode == false)
			{
				Exception ex = new FtpProxyConflictException(FTPRx.excProxyConflict);
				ret.SetError(ex);
			}
			else
			{
				ret.SetError(wEx);
			}
		}
		else
		{
			ret.SetError(wEx);
		}
	}
	catch (System.InvalidOperationException)
	{
		Exception ex = new FtpProxyConflictException(FTPRx.excProxyConflict);
		ret.SetError(ex);
	}
	catch (Exception nEx)
	{
		ret.SetError(nEx);
	}
 
	return (ret);
}

Questo metodo permette di verificare se la connessione al server può essere stabilita e se il server risponde, in modo da controllare i parametri di configurazione prima di caricare o scaricare file sul sito FTP. Per effettuare la connessione, viene utilizzata la classe FtpWebRequest di .Net che viene inizializzata con uno dei comandi FTP disponibili, ovvero il comando che richiede il nome della cartella di lavoro sul server FTP in caso di errore di connessione viene restituito l’errore rilevato.

public Result<byte[]> DownloadFile(string fileNamestring folderName)
{
	Result<byte[]> downloadedData = new Result<byte[]>();
 
	try
	{
		string fullFtpAddress = BuildFullFtpAddress(fileNamefolderName);
		FtpWebRequest request = (FtpWebRequest)FtpWebRequest.Create(fullFtpAddress);
 
		//Get the file size first (for progress bar)
		request.Method = WebRequestMethods.Ftp.GetFileSize;
		request.Credentials = new NetworkCredential(FtpConfigData.UserNameFtpConfigData.UserPassword);
		request.UsePassive = FtpConfigData.PassiveMode;
		request.UseBinary = true;
		request.KeepAlive = true//don't close the connection
 
		long dataLength = request.GetResponse().ContentLength;
		long dataReaded = 0;
 
		//Downloading File...
 
		//Now get the actual data
		request = (FtpWebRequest)FtpWebRequest.Create(fullFtpAddress);
		request.Method = WebRequestMethods.Ftp.DownloadFile;
		request.Credentials = new NetworkCredential(FtpConfigData.UserNameFtpConfigData.UserPassword);
		request.UsePassive = FtpConfigData.PassiveMode;
		request.UseBinary = true;
		request.KeepAlive = FtpConfigData.KeepAlive//close the connection when done
 
		OnFtpProgressUpdated(new FtpProgressUpdatedEventArgs(dataLengthdataReaded));
 
		//Streams
		using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
		{
			using (Stream reader = response.GetResponseStream())
			{
				//Download to memory
				//Note: adjust the streams here to download directly to the hard drive
				using (MemoryStream memStream = new MemoryStream())
				{
					byte[] buffer = new byte[FtpConfigData.BufferSize]; //downloads in chuncks
 
					while (true)
					{
						//Try to read the data
						int bytesRead = reader.Read(buffer0FtpConfigData.BufferSize);
 
						if (bytesRead == 0)
						{
							OnFtpProgressUpdated(new FtpProgressUpdatedEventArgs(dataLengthdataReaded));
							break;
						}
						else
						{
							//Write the downloaded data
							memStream.Write(buffer0bytesRead);
							dataReaded += bytesRead;
							OnFtpProgressUpdated(new FtpProgressUpdatedEventArgs(dataLengthdataReaded));
						}
					}
 
					//Convert the downloaded stream to a byte array
					downloadedData.Data = memStream.ToArray();
 
					//Clean up
					memStream.Close();
				}
				reader.Close();
			}
			response.Close();
		}
	}
	catch (System.Net.WebException wEx)
	{
		downloadedData.SetError(wEx);
	}
	catch (Exception ex)
	{
		downloadedData.SetError(ex);
	}
	return (downloadedData);
}

La funzione fondamentale per il download di un file da una cartella del sito FTP, che compone un array di byte contenente il file che poi sarà salvato su disco dal programma chiamante. Utilizza anche in questo caso l’oggetto FtpWebRequest per eseguire una serie di comandi. Il primo comando è la richiesta della dimensione del file. La risposta, tramite la Response fornita dalla chiamata utilizza il metodo ContentLength per stabilire quanto è grande il file da scaricare.

Viene generata una nuova FtpWebRequest con il comando DownloadFile, la FtpWebResponse ottenuta contiene uno Stream che utilizzando una classe Stream ed un MemoryStream viene utilizzata per una lettura ciclica dei dati in forma sequenziale, utilizzando la lunghezza del Buffer impostata nei parametri di configurazione passati al costruttore. Una volta letto tutto lo stream del file tramite il memory stream viene prodotto il Byte array restituito al chiamante per il salvataggio del file.

public Result<List<string>> GetFileList(string folderToRead)
{
	Result<List<string>> ret = Result.Get<List<string>>();
 
	try
	{
		string fullFtpAddress = BuildFullFtpAddress(string.EmptyfolderToRead);
		FtpWebRequest request = (FtpWebRequest)FtpWebRequest.Create(fullFtpAddress);
 
		request.Method = WebRequestMethods.Ftp.ListDirectory;
		request.Credentials = new NetworkCredential(FtpConfigData.UserNameFtpConfigData.UserPassword);
		request.UsePassive = FtpConfigData.PassiveMode;
		request.UseBinary = true;
		request.KeepAlive = FtpConfigData.KeepAlive;
 
		using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
		{
			using (Stream responseStream = response.GetResponseStream())
			{
				using (StreamReader reader = new StreamReader(responseStream))
				{
					while (!reader.EndOfStream)
					{
						ret.Data.Add(reader.ReadLine());
					}
					reader.Close();
				}
				responseStream.Close();
			}
			response.Close();
		}
	}
	catch (System.Net.WebException wEx)
	{
		ret.SetError(wEx);
	}
	catch (Exception ex)
	{
		ret.SetError(ex);
	}
	return (ret);
}

Il terzo metodo che analizziamo, anche se non l’ho inserito nella applicazione di test, è il metodo GetFileList, che richiede al server FTP la lista dei file di una cartella. Restituisce una lista di stringhe con i nomi dei file. Anche in questo caso utilizziamo la classe FtpWebRequest ed il comando ListDirectory. In questo caso, utilizziamo in modo diverso lo Stream restituito con il metodo ReadLine, perché sappiamo che è uno stream in formato testo e che ogni linea di testo è il nome di un file. Componiamo la lista che poi viene restituita al chiamante.

public Result<boolUploadFile(string fileToUploadstring folderWhereUpload)
{
	Result<boolret = new Result<bool>();
	try
	{
		string fullFtpAddress = BuildFullFtpAddress(fileToUploadfolderWhereUpload);
		FtpWebRequest request = (FtpWebRequest)FtpWebRequest.Create(fullFtpAddress);
 
		request.Method = WebRequestMethods.Ftp.UploadFile;
		request.Credentials = new NetworkCredential(FtpConfigData.UserNameFtpConfigData.UserPassword);
		request.UsePassive = FtpConfigData.PassiveMode;
		request.UseBinary = true;
		request.KeepAlive = FtpConfigData.KeepAlive;
 
		//Upload file
		using (Stream reqStream = request.GetRequestStream())
		{
			//Load the file
			using (FileStream stream = File.OpenRead(fileToUpload))
			{
				byte[] buffer = new byte[FtpConfigData.BufferSize];
 
				stream.Seek(0SeekOrigin.End);
				long dataLength = stream.Position;
				long dataReaded = 0;
				stream.Seek(0SeekOrigin.Begin);
 
				while (true)
				{
					//Try to read the data
					int bytesRead = stream.Read(buffer0FtpConfigData.BufferSize);
 
					if (bytesRead == 0)
					{
						OnFtpProgressUpdated(new FtpProgressUpdatedEventArgs(dataLengthdataReaded));
						break;
					}
					else
					{
						//Write the downloaded data
						reqStream.Write(buffer0bytesRead);
						dataReaded += bytesRead;
						OnFtpProgressUpdated(new FtpProgressUpdatedEventArgs(dataLengthdataReaded));
					}
				}
				stream.Close();
			}
			reqStream.Close();
		}
		ret.Data = true;
	}
	catch (System.Net.WebException wEx)
	{
		ret.SetError(wEx);
	}
	catch (Exception nEx)
	{
		ret.SetError(nEx);
	}
	return (ret);
}

Il metodo per caricare un file sul server FTP come i precedenti utilizza la classe FtpWebRequest e in questo caso il comando UploadFile. Utilizziamo il RequestStream per effettuare il ciclo di caricamento del file utilizzando sempre la BufferSize fornita dai parametri di configurazione.

Mi fermo qui nella breve descrizione della classe helper, anche se al suo interno troverete anche ulteriori metodi utili per il vostro Client FTP ovvero il metodo DeleteFile, che permette di cancellare i file sulla cartella FTP, e una serie di overload per i metodi di Upload che permette di caricare una lista di file e permette di caricare i file utilizzando più tentativi in caso di connessione non troppo buona.

Ovviamente questa classe esplora solo una minima parte dell’implementazione del protocollo FTP di .Net che è completa e che implementa non solo la parte client ma anche la parte server FTP ed è una minuscola parte dell’implementazione delle classi Web Request e Web Response che possono essere utilizzate per la chiamata ai servizi Web API Rest o comunque per ottenere contenuti da qualsiasi connessione web.

La classe di test MainWindow.xaml.cs

Non discuterò la parte Xaml della classe perché ho semplicemente creato una Grid in cui ho inserito Testo e Textbox più alcune Checkbox ed infine i bottoni che permettono di:

  • Selezionare il file da caricare
  • Fare il test della connessione
  • Provare a fare un upload
  • Provare a fare un download
La finestra di test

Ho visualizzato la configurazione del mio server installato in locale e la cartella è la seguente:

Ho creato una sottocartella per testare la lettura e scrittura su sottocartella ma ovviamente potete provare anche sulla root del sito semplicemente non mettendo nulla sul campo Cartella.

private void GetUploadFile_click(object senderRoutedEventArgs e)
{
	OpenFileDialog ofd = new OpenFileDialog();
	ofd.Title = "File da caricare";
	ofd.Filter = "Tutti i file (*.*)|*.*";
	ofd.Multiselect = false;
	ofd.FileName = FileToUpload;

	bool? filed = ofd.ShowDialog(this);
	if (filed.HasValue && filed.Value)
	{
		FileToUpload = ofd.FileName;
	}
}

Il pulsantino con i 3 puntini che permette di selezionare il nome del file da caricare utilizza la OpenFileDialog di windows per selezionare il file ovunque sul vostro disco da caricare su FTP.

private void CheckConnection_click(object senderRoutedEventArgs e)
{
	try
	{
		FTP ftpClient = new FTP(FtpConfigData);
		Result<boolftpChecked = ftpClient.CheckFtpConnection();
		if (ftpChecked.HasError)
		{
			MessageBox.Show(ftpChecked.Error.ToString());
			return;
		}
		ResultText = "Controllo connessione effettuato correttamente";
	}
	catch (Exception ex)
	{
		
		MessageBox.Show(ex.Message);
	}
}

Il metodo di test della connessione utilizza i dati mappati sui controlli XAML e genera un istanza della nostra classe helper FTP per poi chiamare il metodo CheckFtpConnection.

private void UploadFile_click(object senderRoutedEventArgs e)
{
	try
	{
		if (FileToUpload != null && FileToUpload.Trim().Length > 0)
		{
			FTP ftpHelper = new FTP(FtpConfigData);
			Result<booluploaded = ftpHelper.UploadFile(FileToUploadFolderForDownload);
			if (uploaded.HasError)
			{
				MessageBox.Show(uploaded.Error.ToString());
				return;
			}
			ResultText = "File caricato con successo.";
		}
		else
		{
			MessageBox.Show("Indicare il file da usare x l'upload test");
		}
	}
	catch (Exception ex)
	{
 
		MessageBox.Show(ex.Message);
	}
}

Il caricamento del file di test è semplice quanto il precedente test, infatti cambia solo il metodo che viene chiamato. Lascio a voi effettuare il test dei metodi che caricano più file e quelli che inseriscono più tentativi in caso uno di essi fallisca per problemi di connettività lenta o non troppo buona.

private void DownloadFile_click(object senderRoutedEventArgs e)
{
	try
	{
		if (FileToDownload != null && FileToDownload.Trim().Length > 0)
		{
			FTP ftpHelper = new FTP(FtpConfigData);
			Result<List<string>> getFiles = ftpHelper.GetFileList(FolderForDownload);
			if (getFiles.HasError)
			{
				MessageBox.Show(getFiles.Error.ToString());
				return;
			}
			Result<byte[]> downloaded = ftpHelper.DownloadFile(FileToDownloadFolderForDownload);
			if (downloaded.HasError)
			{
				MessageBox.Show(downloaded.Error.ToString());
				return;
			}
			SaveFileDialog sfd = new SaveFileDialog();
			sfd.FileName = FileToDownload;
			sfd.Filter = "Tutti i file (*.*)|*.*";
			bool? saved = sfd.ShowDialog(this);
			if (saved.HasValue && saved.Value)
			{
				File.WriteAllBytes(sfd.FileNamedownloaded.Data);
			}

			ResultText = "File scaricato con successo.";
			if (MessageBox.Show("Vuoi aprire il file?""Apri file"MessageBoxButton.YesNo== MessageBoxResult.Yes)
			{
				Process.Start(sfd.FileName);
			}

		}
		else
		{
			MessageBox.Show("Indicare il nome del file da usare x il download test");
		}
	}
	catch (Exception ex)
	{

		MessageBox.Show(ex.Message);
	}
}

Anche il metodo di test per il download è semplice, ha solo in più due cose, l’uso di una SaveFileDialog per memorizzare il file scaricato su disco e l’uso di una MessageBox per permettere all’utente di aprire il file scaricato.

Potete aggiungere un test per il metodo per effettuare la Lista dei file e scaricare più file invece che uno solo, e potete provare a cancellare un file dopo averlo caricato con il metodo DeleteFile della classe Helper.

Si tratta solo di una panoramica di base sulla classe FTP di .Net ma sicuramente può essere uno spunto per la costruzione di applicazioni non banali che utilizzano un client FTP per fornire servizi ad applicazioni windows.

Il codice a corredo per questo articolo può essere scaricato al seguente link:

Se avete dubbi, domande, critiche, approfondimenti in merito all’articolo, usate pure l’icona a forma di bustina in alto per mandarmi un messaggio.