1.Introduction

Vous êtes un développeur C#, vous travaillez avec Silverlight et vous souhaitez pouvoir faire de l'upload de fichier sur un serveur PHP ?
Alors ce tutoriel est pour vous.

A travers cet article, nous allons présenter comment créer et utiliser un web service développé avec NuSOAP permettant l'envoi de fichier sur un serveur, ainsi que sa consommation par une application Silverlight.
Tout au long de ce cours, je vais utiliser Visual C# 2008.

2.Préambule


Dans ce tutoriel, je considère que vous avez quelques notions de Silverlight et les outils correctement installés.
Pour plus de renseignements sur l'installation des outils Silverlight, vous pouvez aller consulter l'introduction à Silverlight 2 de Benjamin Roux.

3.Silverlight et l'envoi de fichier

3.1.Envoi de fichier et sécurité

Les applications Silverlight n'ont pas accès aux ressources de l'ordinateur client. Elles sont en effet exécutées dans une sandbox (bac à sable) ce qui permet notamment d'empêcher l'appel à des API du système d'exploitation, d'empêcher l'accès à la base de registre, ... Bref, de protéger l'utilisateur contre tout code malicieux.

Par contre, le contrôle OpenFileDialog permet d'accéder en lecture à des fichiers (avec l'accord de l'utilisateur, puisque c'est lui qui va sélectionner un fichier).

Grâce à ce contrôle, Silverlight peut donc accéder à un fichier. Cependant, ce n'est pas suffisant, nous sommes encore côté client. Pour uploader un fichier sur un serveur, on va donc devoir l'envoyer sur celui-ci.
Une solution est de transmettre le contenu de ce fichier à un web service afin qu'il puisse recréer un fichier identique sur le serveur.

3.2.Préambule sur la création de web service avec NuSOAP

3.2.1.NuSoap

NuSOAP est un ensemble de classes PHP qui permet à un développeur de créer des web services SOAP. Un des intérêts de cette bibliothèque est qu'elle fonctionne simplement en copiant les fichiers sur son serveur. Aucune extension n'est requise.

Plus d'infos sur NuSOAP ici. Vous pouvez télécharger NuSOAP à cet emplacement.

Ensuite, il suffira juste de copier les classes qui sont dans le répertoire lib de l'archive sur notre serveur.

NB : à l'heure de l'écriture de cet article, la dernière version est la 0.7.3.

Pour faire marcher NuSOAP avec Silverlight, il va falloir opérer 2 petites modifications.
Dans le fichier nusoap.php, recherchez les lignes :

 
Sélectionnez

var $soap_defencoding = 'ISO-8859-1';
//var $soap_defencoding = 'UTF-8';

Commentez la première et décommentez la deuxième pour avoir

 
Sélectionnez

//var $soap_defencoding = 'ISO-8859-1';
var $soap_defencoding = 'UTF-8';

Faire de même dans le fichier class.nusoap_base.php

3.2.2.Créer un web service avec NuSoap, l'exemple de l'Hello World

Créer un fichier helloworld.php qui va contenir ce code :

helloworld.php
Sélectionnez

<?php
require_once('nusoap/nusoap.php');

$server = new soap_server();
// Initialize WSDL support
$server->configureWSDL('helloworldwsdl', 'urn:helloworldwsdl');
// Register the method to expose
$server->register('gethelloworld',		// method name
	array('nom' => 'xsd:string'),	// input parameters
	array('return' => 'xsd:string'),	// output parameters
	'urn:helloworldwsdl',		// namespace
	'urn:helloworldwsdl#addliste',	// soapaction
	'rpc',				// style
	'literal',			// use
	'Le traditionnel Hello World'	// documentation
);
// Define the method as a PHP function
function gethelloworld($nom)
{
	return "Hello ".$nom;
}

// Use the request to (try to) invoke the service
$HTTP_RAW_POST_DATA = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : '';
$server->service($HTTP_RAW_POST_DATA);

?>

Premièrement, il faut inclure la bibliothèque NuSOAP (que j'ai mise dans le sous-répertoire nusoap).
Ensuite on crée un nouveau serveur soap et on l'initialise.
La méthode register permet d'indiquer le nom de la méthode php qui va être appelée ainsi que les paramètres d'entrée et de sortie (nom et type).

Notez que pour fonctionner avec Silverlight, le style doit être rpc et l'usage literal.

Il ne reste qu'à créer la simple méthode gethelloworld qui, comme on le voit, retourne "hello" concaténé au nom passé en paramètre.

Le web service est consultable à cette adresse : http://nico-pyright.developpez.com/tutoriel/vs2008/csharp/uploadsilverlightandphp/helloworld.php
Le WSDL généré est

 
Sélectionnez
<?xml version="1.0" encoding="ISO-8859-1"?>
<definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
	xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 
	xmlns:tns="urn:helloworldwsdl" 
	xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
	xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
	xmlns="http://schemas.xmlsoap.org/wsdl/" 
	targetNamespace="urn:helloworldwsdl">
<types>
	<xsd:schema targetNamespace="urn:helloworldwsdl">
		<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
		<xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
	</xsd:schema>
</types>
<message name="gethelloworldRequest">
	<part name="nom" type="xsd:string" />
</message>
<message name="gethelloworldResponse">
	<part name="return" type="xsd:string" />
</message>
<portType name="helloworldwsdlPortType">
	<operation name="gethelloworld">
		<documentation>Le traditionnel Hello World</documentation>
		<input message="tns:gethelloworldRequest"/>
		<output message="tns:gethelloworldResponse"/>
	</operation>
</portType>
<binding name="helloworldwsdlBinding" type="tns:helloworldwsdlPortType">
	<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
	<operation name="gethelloworld">
		<soap:operation soapAction="urn:helloworldwsdl#addliste" style="rpc"/>
		<input><soap:body use="literal" namespace="urn:helloworldwsdl"/></input>
		<output><soap:body use="literal" namespace="urn:helloworldwsdl"/></output>
	</operation>
</binding>
<service name="helloworldwsdl">
	<port name="helloworldwsdlPort" binding="tns:helloworldwsdlBinding">
		<soap:address 
location="http://nico-pyright.developpez.com/tutoriel/vs2008/csharp/uploadsilverlightandphp/helloworld.php"/>
	</port>
</service>
</definitions>

3.2.3.Consommer le web service avec Silverlight

Pour utiliser le web service dans une application Silverlight, il faut ajouter une référence service :
Bouton droit sur le projet => Add service reference.
Il faudra renseigner l'url du wsdl et y naviguer comme sur l'image ci-dessous.

Image non disponible

Un click sur le bouton OK permet à Visual Studio de générer toute la mécanique d'appel de ce web service.

L'appel à un tel web service se fait de manière asynchrone. Créons une petite application pour pouvoir tester ce web service.

 
Sélectionnez

<UserControl x:Class="DemoHelloWorld.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Canvas>
        <TextBlock Text="Saisissez votre nom :" Canvas.Top="5" />
        <TextBox x:Name="name" Width="200" Canvas.Left="150"/>
        <Button Content="Appeler le web service" Width="200" Height="35" Canvas.Top="35" Click="Button_Click" />
        <TextBlock x:Name="result" Canvas.Top="45" Canvas.Left="220" Foreground="Red"/>
    </Canvas>
</UserControl>

Ce XAML permet de créer un champ de saisie et un bouton qui va appeler notre web service.

Image non disponible

Le code associé au click du bouton, ci-dessous, permet d'appeler le web service en lui passant le contenu du textbox en paramètre. Il faudra instancier la classe helloworldwsdlPortTypeClient.
On s'abonne ensuite à l'événement client_gethelloworldCompleted afin d'être notifié de la fin de l'appel au web service, ce qui nous permettra notamment de traiter la réponse de celui-ci.

 
Sélectionnez
private void Button_Click(object sender, RoutedEventArgs e)
{
    helloworldwsdlPortTypeClient client = new helloworldwsdlPortTypeClient();
    client.gethelloworldCompleted += client_gethelloworldCompleted;
    client.gethelloworldAsync(name.Text);
}

void client_gethelloworldCompleted(object sender, gethelloworldCompletedEventArgs e)
{
    string toDisplay;
    try
    {
        toDisplay = e.Result;
    }
    catch (Exception ex)
    {
        toDisplay = string.Format("Impossible d'appeler le web service : {0}", ex.Message);
    }
    Dispatcher.BeginInvoke(() => { result.Text = toDisplay; });
}

On peut noter qu'on récupère le résultat de notre web service dans la variable e.Result. On l'affiche, ce qui nous donne :

Image non disponible

Vous pouvez tester cet exemple à cette adresse.

3.3.Le web service d'envoi de fichier

Pour uploader un fichier, on va créer un web service qui prend en paramètre le nom du fichier ainsi que son contenu pour l'écrire sur le disque.
Bien sur, envoyer le fichier en entier est une mauvaise idée, on va l'envoyer petit bout par petit bout en rajoutant un paramètre pour indiquer si on doit créer le fichier ou bien rajouter le contenu à la suite.

Les données envoyées pouvant être de toutes sortes, on va les encoder afin qu'elles puissent transiter tranquillement. A cet effet, j'ai choisi d'utiliser l'encodage en base 64.

Voici le web service qu'on pourrait écrire pour faire une telle chose (fileupload.php) :

 
Sélectionnez

<?php
require_once('nusoap/nusoap.php');

$server = new soap_server();
// Initialize WSDL support
$server->configureWSDL('uploadwsdl', 'urn:uploadwsdl');
// Register the method to expose
$server->register('upload',				// method name
	array('mode' => 'xsd:int', 'filename' => 'xsd:string', 'filecontent' => 'xsd:string'),        // input parameters
	array('return' => 'xsd:string'),		// output parameters
	'urn:uploadwsdl',				// namespace
	'urn:uploadwsdl#upload',			// soapaction
	'rpc',					// style
	'literal',				// use
	'Upload un fichier'			// documentation
);
// Define the method as a PHP function
function upload($mode, $filename, $filecontent)
{
	if ($mode == 0)
	{
		$fh = fopen($filename, 'w') or die("can't open file");
	}
	if ($mode == 1)
	{
		$fh = fopen($filename, 'a') or die("can't open file");
	}
	$stringData = base64_decode($filecontent);
	fwrite($fh, $stringData);
	fclose($fh);

	return 'OK';
}
// Use the request to (try to) invoke the service
$HTTP_RAW_POST_DATA = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : '';
$server->service($HTTP_RAW_POST_DATA);

?>

Rien d'extraordinaire. Juste un détail, on passe un mode afin de lui indiquer si on écrit le fichier (cas du premier packet) ou si on ajoute au fichier (cas des paquets suivants).
L'ajout au fichier à partir du contenu se fait en ayant décodé la chaine passée en base 64.

NB : il pourrait être judicieux d'implémenter une compression pour réduire la taille de transfert du fichier.

3.4.Côté Silverlight

Nous allons donc appeler ce web service depuis Silverlight afin de faire notre upload de fichier. Pour ce faire, créons une petite application de démo :

 
Sélectionnez

<Canvas>
	<TextBlock x:Name="textEnvoyer" Text="Choisir un fichier : " Canvas.Top="5" />
	<Button x:Name="btnParcourir" Content="Parcourir ..." 
		Canvas.Left="120" Width="90" Height="30" Click="Upload_Click" />
	<TextBlock x:Name="result" Canvas.Top="50" />
</Canvas>

Une interface très simple qui lors du click sur le bouton va nous permettre d'utiliser le contrôle OpenFileDialog.

Image non disponible
 
Sélectionnez

public partial class Page : UserControl
{
	private const int _packetSize = 32768;
	private FileStream _fileStream;
	private string _fileName;

	public Page()
	{
		InitializeComponent();
	}

	private void Upload_Click(object sender, RoutedEventArgs e)
	{
		try
		{
			OpenFileDialog dialog = new OpenFileDialog
			{
				Filter = "Image files (*.gif;*.jpg;*.png)|*.gif;*.jpg;*.png",
				Multiselect = false
			};
			if (dialog.ShowDialog() == true)
			{
				FileInfo fichier = dialog.File;
				_fileName = fichier.Name;
				_fileStream = fichier.OpenRead();
				btnParcourir.IsEnabled = false;
				Dispatcher.BeginInvoke(() => { result.Text = "Envoi en cours ..."; });

				uploadwsdlPortTypeClient client = new uploadwsdlPortTypeClient();
				client.uploadCompleted += client_uploadCompleted;
				string toUpload = GetNextPacket();
				client.uploadAsync(0, _fileName, toUpload);
			}
		}
		catch (Exception ex)
		{
			Dispatcher.BeginInvoke(() => { 
				result.Text = string.Format("Erreur d'envoi : {0}", ex.InnerException.Message); });
		}
	}

	private string GetNextPacket()
	{
		int lengthToRead = (int)(_fileStream.Length > _packetSize ? _packetSize : _fileStream.Length);
		byte[] buffer = new byte[lengthToRead];

		for (int i = 0; i < lengthToRead; i++)
		{
			int value = _fileStream.ReadByte();
			if (value == -1)
			{
				return GetEncodedValue(buffer, i);
			}
			buffer[i] = (byte)value;
		}
		return GetEncodedValue(buffer, lengthToRead);
	}

	private static string GetEncodedValue(byte[] buffer, int i)
	{
		byte[] newBuffer = new byte[i];
		Array.Copy(buffer, newBuffer, i);
		return Convert.ToBase64String(newBuffer);
	}

	void client_uploadCompleted(object sender, uploadCompletedEventArgs e)
	{
		try
		{
			if (e.Result != "OK")
			{
				Dispatcher.BeginInvoke(() => { result.Text = "Erreur d'envoi !"; });
			}
			else
			{
				uploadwsdlPortTypeClient client = new uploadwsdlPortTypeClient();
				client.uploadCompleted += client_uploadCompleted;
				string toUpload = GetNextPacket();
				if (!string.IsNullOrEmpty(toUpload))
					client.uploadAsync(1, _fileName, toUpload);
				else
				{
					Dispatcher.BeginInvoke(() => { btnParcourir.IsEnabled = true; });
					Dispatcher.BeginInvoke(() => { result.Text = "Envoi terminé"; });
				}
			}
		}
		catch (Exception ex)
		{
			Dispatcher.BeginInvoke(() => 
			{ result.Text = string.Format("Erreur d'envoi : {0}", ex.InnerException.Message); });
		}
	}
}

Notez que pour l'exemple, j'ai choisi de limiter la sélection des fichiers aux extensions gif, jpg et png. On peut bien sur envoyer tout type de fichier.
Le lecteur attentif aura aussi remarqué que je défini des paquets d'une taille maximale de 32768 octets.

On peut voir dans la méthode Upload_Click, qu'on permet la sélection d'un unique fichier (Multiselect = false).

Ensuite on appelle notre web service avec le premier paquet à envoyer. Notons qu'en paramètre, on lui passe 0 pour dire que c'est le premier paquet et que donc le fichier est à créer. On passe également le nom du fichier et le contenu de ce paquet qui est encodé en base64 grâce à Convert.ToBase64String.

La méthode client_uploadCompleted est appelée après qu'un paquet soit fini d'être envoyé. Tant que ce n'est pas fini, on répète l'envoi. Notez le paramètre à 1 pour indiquer qu'il faut ajouter le contenu au fichier.

3.5.Améliorations

Notre envoi de fichier est désormais fonctionnel. Mais si on le teste un peu en conditions réelles (réseau saturé, fichier volumineux, etc ...) on se rend compte que l'envoi se termine rarement.
De plus, qu'est-ce qui nous permet de vérifier que le fichier a correctement été transmis ?

Ce sont ces améliorations que nous allons prendre en compte.

3.5.1.Ré-envoi de paquet en cas d'erreur de transfert

Ce qu'on peut faire, c'est si le web service renvoi une erreur, ou si une exception est levée, on peut retenter le transfert (en gérant un nombre maximum de tentative).
Par exemple, en cas de retour incorrect (renvoi différent de OK) ou si une exception est catchée, on peut incrémenter un compteur de tentative et retenter un envoi.
Par exemple, vous pouvez voir ci-dessous l'événement uploadCompleted appelé lors de la fin d'envoi d'un paquet. La fonction ManageError va permettre de tenter de ré-envoyer le paquet, en gérant un nombre maximum de tentative.

 
Sélectionnez

private int _nbTry;
public int MaxTryPerPacket { get; set; }

void uploadCompleted(object sender, uploadCompletedEventArgs e)
{
	try
	{
		if (e.Result != "OK")
			ManageError(e, null);
		else
		{
			// [...] un peu de code enlevé pour plus de clarté [...]
			UploadPacket();
		}
	}
	catch (Exception ex)
	{
		ManageError(null, ex);
	}
}
			
private void ManageError(uploadCompletedEventArgs e, Exception ex)
{
	_nbTry++;
	if (_nbTry <= MaxTryPerPacket)
		UploadPacket(_toReUploadIfError);
	else
	{
		if (ex != null)
			UploadError(ex);
		else
			UploadError(new Exception(e.Result));
	}
}

3.5.2.Vérification d'upload de fichier

Une fois le fichier complètement envoyé, on peut vérifier qu'il est bien conforme à celui envoyé, soit en le re-téléchargeant et en le comparant à l'original, soit en comparant un crc.
J'ai choisi d'utiliser la deuxième méthode, en me basant sur l'algorithme Secure Hash Algorithm 1.
Voici la méthode PHP qui calcule le sha1 d'un fichier en utilisant cet algorithme :

 
Sélectionnez

$server->register('getsha',				// method name
	array('filename' => 'xsd:string'),		// input parameters
	array('return' => 'xsd:string'),		// output parameters
	'urn:uploadwsdl',				// namespace
	'urn:uploadwsdl#upload',			// soapaction
	'rpc',					// style
	'literal',				// use
	'Recupere le sha1 du fichier'		// documentation
);

function getsha($filename)
{
	return sha1_file($filename);
}

Pour faire de même en C#, on utilisera

 
Sélectionnez

public static string GetChecksum(string file)
{
    using (FileStream stream = File.OpenRead(file))
    {
        SHA1 sha = new SHA1Managed();
        byte[] checksum = sha.ComputeHash(stream);
        return BitConverter.ToString(checksum).Replace("-", String.Empty);
    }
}

Je propose de permettre la vérification du fichier grâce à un paramètre lorsque l'événement de fin de téléchargement de fichier est levé, soit :

 
Sélectionnez

public delegate void CurrentFileUploadCompletedHanlder(CurrentFileUploadCompletedEventArgs args, ref bool verifyUpload);
private event CurrentFileUploadCompletedHanlder _currentFileUploadCompleted;
public event CurrentFileUploadCompletedHanlder OnCurrentFileUploadCompleted
{
	add { _currentFileUploadCompleted += value; }
	remove { _currentFileUploadCompleted -= value; }
}

private void UploadPacket(string toUpload)
{
	// [...] un peu de code enlevé pour plus de clarté [...]
	// envoi du fichier courant terminé
	if (_currentFileUploadCompleted != null)
	{
		bool verifyUpload = false;
		_currentFileUploadCompleted(new CurrentFileUploadCompletedEventArgs 
		{ FileName = _currentFileName, FileSize = _currentFileSize }, ref verifyUpload);
		if (verifyUpload)
		{
			uploadwsdlPortTypeClient service = Client;
			service.getshaCompleted += service_getshaCompleted;
			service.getshaAsync(_currentFileName);
		}
		// [...] un peu de code enlevé pour plus de clarté [...]
	}
	// [...] un peu de code enlevé pour plus de clarté [...]
}

void service_getshaCompleted(object sender, getshaCompletedEventArgs e)
{
	if (_currentFileVerifyUpload != null)
	{
		try
		{
			string sha = GetChecksum(_currentFileStream);
			_currentFileVerifyUpload(new VerifyUploadEventArgs 
			{ ResultOK = string.Compare(e.Result, sha, 
			StringComparison.InvariantCultureIgnoreCase) == 0, FileName = _currentFileName });
		}
		catch (Exception)
		{
			_currentFileVerifyUpload(new VerifyUploadEventArgs 
				{ ResultOK = false, FileName = _currentFileName });
		}
	}
	// [...] un peu de code enlevé pour plus de clarté [...]
}

Pour s'abonner à cet événement :

 
Sélectionnez

WSUploader wsUploader = new WSUploader();
wsUploader.OnCurrentFileUploadCompleted += wsUploader_OnCurrentFileUploadCompleted;

// [...] un peu de code enlevé pour plus de clarté [...]

void wsUploader_OnCurrentFileUploadCompleted(CurrentFileUploadCompletedEventArgs args, ref bool verifyUpload)
{
	verifyUpload = true;
}

Donc, si on s'est abonné à l'événement de fin de téléchargement du fichier courant, et qu'on passe la variable verifyUpload à true, alors on appelle le web service qui permet de calculer le sha1 du fichier.
On le compare ainsi avec celui écrit en C# et basé sur le FileStream du fichier qu'on envoi afin de vérifier que le fichier a été correctement reçu.

3.5.3.Upload de plusieurs fichiers

Il pourrait aussi être intéressant de pouvoir uploader plusieurs fichiers d'un coup, et d'être notifié par événement lorsqu'un fichier est terminé ou que tout est terminé.
Il faudra prendre garde à la synchronisation. En effet, les appels au web service étant asynchrone, on risque d'envoyer tous les fichiers en même temps et de faire n'importe quoi.
On gérera la synchronisation avec un AutoResetEvent.

Je ne vais pas rentrer dans le détail dans cet article, mais sachez qu'on pourra utiliser cet objet pour attendre qu'un envoi de fichier soit terminé.
Ainsi, l'envoi de plusieurs fichiers pourra s'écrire ainsi (on passe en paramètre un dictionnaire contenant le stream et le nom de chaque fichier) :

 
Sélectionnez

private static AutoResetEvent autoEvent;
					
public void Upload(Dictionary<FileStream, string> fileDico)
{
	// [...] un peu de code enlevé pour plus de clarté [...]
	Thread thread = new Thread(delegate()
	{
		foreach (KeyValuePair<FileStream, string> element in fileDico)
		{
			autoEvent = new AutoResetEvent(false);

			// [...] un peu de code enlevé pour plus de clarté [...]
			UploadPacket();

			autoEvent.WaitOne();
		}
		// [...] un peu de code enlevé pour plus de clarté [...]
	});
	thread.Start();
}

Ainsi, on démarre un thread qui va boucler sur tous les éléments du dictionnaire.

On déclare un AutoResetEvent et on envoi les paquets.
autoEvent.WaitOne(); va permettre d'attendre que l'opération soit complètement terminée. Plus précisément, WaitOne va attendre qu'une action se passe sur cet objet. Ceci implique que l'on fasse une action quand le fichier est terminé.
On utilisera :

 
Sélectionnez

autoEvent.Set();

Ceci implique qu'il faudra informer de la fin de l'envoi dans tous les cas possibles, c'est à dire lorsque le fichier est terminé ou qu'il y a eu une erreur, ou qu'on a choisi de prolonger le traitement avec une vérification de fichier, etc ... (vous verrez ces différents cas d'utilisation dans le code complet plus bas).

3.5.4.Paramétrage de l'emplacement du web service

Par défaut, la génération du code utile pour le web service (voir § 3.2.3.Consommer le web service avec Silverlight stocke l'url du web service. Mais il peut être utile de passer cette url par code, si jamais l'url change, on n'a pas besoin de régénérer le code.
Pour ce faire, on pourra instancier le web service de cette façon :

 
Sélectionnez

new uploadwsdlPortTypeClient(new System.ServiceModel.BasicHttpBinding(System.ServiceModel.BasicHttpSecurityMode.None), 
	new System.ServiceModel.EndpointAddress("adresse du web service"));

Sachant que pour ma part, j'ai choisi de paramétrer ceci dans une classe statique.

3.5.5.Suppression du fichier

En cas d'erreur, on peut vouloir laisser le choix à l'utilisateur de supprimer le fichier (mal) uploadé.

 
Sélectionnez

public delegate void CurrentFileErrorUploadHanlder(CurrentFileErrorUploadEventArgs args, ref bool DeleteFileOnServer);
private event CurrentFileErrorUploadHanlder _currentFileUploadError;
public event CurrentFileErrorUploadHanlder OnCurrentFileUploadError
{
	add { _currentFileUploadError += value; }
	remove { _currentFileUploadError -= value; }
}
					
private void UploadError(Exception exception)
{
	if (_currentFileUploadError != null)
	{
		bool deleteFile = false;
		_currentFileUploadError(new CurrentFileErrorUploadEventArgs 
			{ 	InnerException = exception, 
				FileName = _currentFileName 
			}, ref deleteFile);
		if (deleteFile)
			DeleteFile();
	}
	// [...] un peu de code enlevé pour plus de clarté [...]
}
private void DeleteFile()
{
	uploadwsdlPortTypeClient service = Client;
	service.deleteAsync(_currentFileName);
}

Avec un web service pour effacer le fichier :

 
Sélectionnez

$server->register('delete',			// method name
	array('filename' => 'xsd:string'),	// input parameters
	array('return' => 'xsd:string'),	// output parameters
	'urn:uploadwsdl',			// namespace
	'urn:uploadwsdl#upload',		// soapaction
	'rpc',				// style
	'literal',			// use
	'Supprime un fichier'		// documentation
);

function delete($filename)
{
	if (unlink($filename))
		return 'OK';
	return 'KO';
}

3.6.Nouvelles améliorations

Après avoir utilisé l'upload de fichier de manière plus poussée, je me suis rendu compte que tout n'était pas encore parfait. J'ai donc procédé à quelques nouvelles améliorations.

3.6.1.Envoi et vérification

Pourquoi attendre que tout soit envoyé pour vérifier que le transfert est OK ? C'est suite à l'envoi de très gros fichiers que je me suis posé cette question.
Pour ce faire, j'ai rajouté un paramètre lors de l'envoi de données qui contient le checksum des données envoyées. Ainsi, coté serveur on pourra vérifier que le contenu envoyé est bien celui escompté.
La méthode d'upload en php devient donc :

 
Sélectionnez

$server->register('upload',
	array('mode' => 'xsd:int', 'filename' => 'xsd:string', 'filecontent' => 'xsd:string', 'checksum' => 'xsd:string'),
	array('return' => 'xsd:string'),
	'urn:uploadwsdl',
	'urn:uploadwsdl#upload',			// soapaction
	'rpc',					// style
	'literal',				// use
	'Upload un fichier'			// documentation
);

function upload($mode, $filename, $filecontent, $checksum)
{
	$stringData = base64_decode($filecontent);
	if (strcasecmp(sha1($stringData), $checksum) != 0)
		return 'KO';
	
	if ($mode == 0)
		$fh = fopen($filename, 'wb') or die("can't open file");
	if ($mode == 1)
		$fh = fopen($filename, 'ab') or die("can't open file");
	fwrite($fh, $stringData);
	fclose($fh);

	return 'OK';
}

Coté Silverlight, l'envoi pourra se faire ainsi :

 
Sélectionnez

string checkSum = GetChecksum(new MemoryStream(Convert.FromBase64String(toUpload)));
service.uploadAsync((int)_uploadType, _currentFileName, toUpload, checkSum);

3.6.2.Catch d'exception

Je me suis rendu compte aussi empiriquement que lorsque je recevais une exception de type TargetInvocationException qui contenait une InnerException de type ProtocolException après un envoi de données, lors de la lecture du résultat, alors le résultat était correct.
C'est une exception que je ne m'explique pas, mais il se trouve que si je ne l'attrapais pas, alors le client croyait que la donnée était à renvoyer alors qu'elle avait été correctement ajoutée au fichier sur le serveur.
Ce qui faisait que les fichiers uploadés avait parfois des données en double, et ne passaient pas à la vérification.

J'ai donc modifié mon code en conséquence.

3.7.Le WSUploader

Voici la classe WSUploader, avec les nouvelles améliorations, qui fait ce travail d'upload :

 
Sélectionnez
namespace DemoUpload
{
    public class WSUploader
    {
        private enum UploadType
        {
            CREATE = 0,
            APPEND = 1
        }

        private static AutoResetEvent autoEvent;
        private static UploadType _uploadType;
        private string _currentFileName;
        private Stream _currentFileStream;
        private long _currentFileSize;
        private long _totalSizeToUpload;
        private long _currentSizeUploaded;
        private long _totalSizeUploaded;
        private string _toReUploadIfError;
        private int _nbTry;
        private bool _hasError;
        private List<string> _successfullFiles;
        private List<string> _errorFiles;

        #region current file handler

        public delegate void CurrentFileErrorUploadHanlder(CurrentFileErrorUploadEventArgs args, ref bool DeleteFileOnServer);
        private event CurrentFileErrorUploadHanlder _currentFileUploadError;
        public event CurrentFileErrorUploadHanlder OnCurrentFileUploadError
        {
            add { _currentFileUploadError += value; }
            remove { _currentFileUploadError -= value; }
        }

        public delegate void CurrentFileUploadProgressHanlder(CurrentFileProgressEventArgs args);
        private event CurrentFileUploadProgressHanlder _currentFileUploadProgress;
        public event CurrentFileUploadProgressHanlder OnCurrentFileUploadProgress
        {
            add { _currentFileUploadProgress += value; }
            remove { _currentFileUploadProgress -= value; }
        }

        public delegate void CurrentFileUploadCompletedHanlder(CurrentFileUploadCompletedEventArgs args, ref bool verifyUpload);
        private event CurrentFileUploadCompletedHanlder _currentFileUploadCompleted;
        public event CurrentFileUploadCompletedHanlder OnCurrentFileUploadCompleted
        {
            add { _currentFileUploadCompleted += value; }
            remove { _currentFileUploadCompleted -= value; }
        }

        public delegate void CurrentFileVerifyUploadHanlder(VerifyUploadEventArgs args);
        private event CurrentFileVerifyUploadHanlder _currentFileVerifyUpload;
        public event CurrentFileVerifyUploadHanlder OnCurrentFileVerifyUpload
        {
            add { _currentFileVerifyUpload += value; }
            remove { _currentFileVerifyUpload -= value; }
        }

        #endregion

        #region all files handler
        public delegate void ErrorUploadHanlder(ErrorUploadEventArgs args);
        private event ErrorUploadHanlder _uploadError;
        public event ErrorUploadHanlder OnUploadError
        {
            add { _uploadError += value; }
            remove { _uploadError -= value; }
        }

        public delegate void UploadProgressHanlder(ProgressEventArgs args);
        private event UploadProgressHanlder _uploadProgress;
        public event UploadProgressHanlder OnUploadProgress
        {
            add { _uploadProgress += value; }
            remove { _uploadProgress -= value; }
        }

        public delegate void UploadCompletedHanlder(UploadCompletedEventArgs args);
        private event UploadCompletedHanlder _uploadCompleted;
        public event UploadCompletedHanlder OnUploadCompleted
        {
            add { _uploadCompleted += value; }
            remove { _uploadCompleted -= value; }
        }

        #endregion

        public WSUploader()
        {
            MaxTryPerPacket = 5;
            PacketSize = 32768;
        }

        private static uploadwsdlPortTypeClient Client
        {
            get { return new uploadwsdlPortTypeClient(
		new BasicHttpBinding(BasicHttpSecurityMode.None), new EndpointAddress(Config.UPLOAD_WEB_SERVICE_PATH)); }
        }

        public int PacketSize { get; set; }
        public int MaxTryPerPacket { get; set; }

        public void Upload(Dictionary<FileStream, string> fileDico)
        {
            _successfullFiles = new List<string>();
            _errorFiles = new List<string>();
            _totalSizeToUpload = fileDico.Sum(kp => kp.Key.Length);
            Thread thread = new Thread(delegate()
            {
                foreach (KeyValuePair<FileStream, string> element in fileDico)
                {
                    _uploadType = UploadType.CREATE;
                    autoEvent = new AutoResetEvent(false);
                    KeyValuePair<FileStream, string> currentElement = element;
                    _currentSizeUploaded = 0;
                    _currentFileStream = currentElement.Key;
                    _currentFileName = currentElement.Value;
                    _currentFileSize = _currentFileStream.Length;

                    UploadPacket();

                    autoEvent.WaitOne();
                }
                // tous les envois sont terminés
                if (_hasError)
                {
                    if (_uploadError != null)
                        _uploadError(new ErrorUploadEventArgs 
					{ FileNameWithError = _errorFiles, SuccessFullFileName = _successfullFiles });
                }
                else
                {
                    if (_uploadCompleted != null)
                        _uploadCompleted(new UploadCompletedEventArgs 
					{ UploadedFiles = _successfullFiles, TotalUploadedSize = _totalSizeToUpload });
                }
            });
            thread.Start();
        }

        void uploadCompleted(object sender, uploadCompletedEventArgs e)
        {
            try
            {
                if (e.Result != "OK")
                    ManageError(e, null);
                else
                    ContinueUpload();
            }
            catch (TargetInvocationException ex)
            {
                if (ex.InnerException != null && ex.InnerException is ProtocolException)
                    ContinueUpload();
                else
                    ManageError(null, ex);
            }
            catch (Exception ex)
            {
                ManageError(null, ex);
            }
        }

        private void ContinueUpload()
        {
            _uploadType = UploadType.APPEND;
            if (_currentFileUploadProgress != null)
                _currentFileUploadProgress(new CurrentFileProgressEventArgs 
			{ ProgressPercentage = ((double)_currentSizeUploaded * 100) / _currentFileStream.Length, 
			FileName = _currentFileName });
            if (_uploadProgress != null)
                _uploadProgress(new ProgressEventArgs 
			{ ProgressPercentage = ((double)_totalSizeUploaded * 100) / _totalSizeToUpload });

            UploadPacket();
        }

        private void UploadPacket()
        {
            string toUpload = GetNextPacket();
            _toReUploadIfError = toUpload;
            _nbTry = 0;
            UploadPacket(toUpload);
        }

        private void UploadPacket(string toUpload)
        {
            if (!string.IsNullOrEmpty(toUpload))
            {
                try
                {
                    uploadwsdlPortTypeClient service = Client;
                    service.uploadCompleted += uploadCompleted;
                    string checkSum = GetChecksum(new MemoryStream(Convert.FromBase64String(toUpload)));
                    service.uploadAsync((int)_uploadType, _currentFileName, toUpload, checkSum);
                }
                catch (Exception ex)
                {
                    UploadError(ex);
                }
            }
            else
            {
                // envoi du fichier courant terminé
                if (_currentFileUploadCompleted != null)
                {
                    bool verifyUpload = false;
                    _currentFileUploadCompleted(new CurrentFileUploadCompletedEventArgs 
				{ FileName = _currentFileName, FileSize = _currentFileSize }, ref verifyUpload);
                    if (verifyUpload)
                    {
                        _nbTry = 0;
                        DoCheckSum();
                    }
                    else
                    {
                        _successfullFiles.Add(_currentFileName);
                        autoEvent.Set();
                    }
                }
                else
                {
                    _successfullFiles.Add(_currentFileName);
                    autoEvent.Set();
                }
            }
        }

        private void DoCheckSum()
        {
            uploadwsdlPortTypeClient service = Client;
            service.getshaCompleted += service_getshaCompleted;
            service.getshaAsync(_currentFileName);
        }

        private static string GetChecksum(Stream fileStream)
        {
            fileStream.Seek(0, SeekOrigin.Begin);
            SHA1 sha = new SHA1Managed();
            byte[] checksum = sha.ComputeHash(fileStream);
            return BitConverter.ToString(checksum).Replace("-", String.Empty);
        }

        void service_getshaCompleted(object sender, getshaCompletedEventArgs e)
        {
            Stream currentFileStream = _currentFileStream;
            string currentFileName = _currentFileName;
            if (_currentFileVerifyUpload != null)
            {
                try
                {
                    string sha = GetChecksum(currentFileStream);
                    bool resultOK = string.Compare(e.Result, sha, StringComparison.InvariantCultureIgnoreCase) == 0;
                    _currentFileVerifyUpload(new VerifyUploadEventArgs
                                                 {
                                                     ResultOK = resultOK,
                                                     FileName = currentFileName
                                                 });
                    if (resultOK)
                        _successfullFiles.Add(currentFileName);
                    else
                    {
                        _hasError = true;
                        _errorFiles.Add(currentFileName);
                    }
                    autoEvent.Set();
                }
                catch (Exception)
                {
                    _nbTry++;
                    if (_nbTry <= MaxTryPerPacket)
                        DoCheckSum();
                    else
                    {
                        _currentFileVerifyUpload(new VerifyUploadEventArgs 
					{ ResultOK = false, FileName = currentFileName });
                        _errorFiles.Add(currentFileName);
                        _hasError = true;
                        autoEvent.Set();
                    }
                }
            }
            else
            {
                _successfullFiles.Add(currentFileName);
                autoEvent.Set();
            }
        }

        private void ManageError(uploadCompletedEventArgs e, Exception ex)
        {
            _nbTry++;
            if (_nbTry <= MaxTryPerPacket)
                UploadPacket(_toReUploadIfError);
            else
            {
                if (ex != null)
                    UploadError(ex);
                else
                    UploadError(new Exception(string.Format("Nombre maxi de tentatives atteintes : {0}", e.Result)));
            }
        }

        private void UploadError(Exception exception)
        {
            if (_currentFileUploadError != null)
            {
                bool deleteFile = false;
                _currentFileUploadError(new CurrentFileErrorUploadEventArgs 
			{ InnerException = exception, FileName = _currentFileName }, ref deleteFile);
                if (deleteFile)
                    DeleteFile();
            }
            _errorFiles.Add(_currentFileName);
            _hasError = true;
            autoEvent.Set();
        }

        private void DeleteFile()
        {
            uploadwsdlPortTypeClient service = Client;
            service.deleteAsync(_currentFileName);
        }

        private string GetNextPacket()
        {
            int lengthToRead = (int)(_currentFileStream.Length > PacketSize ? PacketSize : _currentFileStream.Length);
            byte[] buffer = new byte[lengthToRead];

            for (int i = 0; i < lengthToRead; i++)
            {
                int value = -1;
                int nbTry = 0;
                bool notok = true;
                while (notok && nbTry < 5)
                {
                    try
                    {
                        value = _currentFileStream.ReadByte();
                        notok = false;
                    }
                    catch (IndexOutOfRangeException)
                    {
                        nbTry++;
                        Thread.Sleep(300);
                    }
                }
                if (nbTry == 5)
                    value = -1;
                
                if (value == -1)
                {
                    _currentSizeUploaded += i;
                    _totalSizeUploaded += i;
                    return GetEncodedValue(buffer, i);
                }
                buffer[i] = (byte)value;
            }
            _currentSizeUploaded += lengthToRead;
            _totalSizeUploaded += lengthToRead;
            return GetEncodedValue(buffer, lengthToRead);
        }

        private static string GetEncodedValue(byte[] buffer, int i)
        {
            byte[] newBuffer = new byte[i];
            Array.Copy(buffer, newBuffer, i);
            return Convert.ToBase64String(newBuffer);
        }
    }

    public class VerifyUploadEventArgs
    {
        public bool ResultOK { get; set; }
        public string FileName { get; set; }
    }

    public class UploadCompletedEventArgs
    {
        public List<string> UploadedFiles { get; set; }
        public long TotalUploadedSize { get; set; }
    }

    public class CurrentFileUploadCompletedEventArgs
    {
        public string FileName { get; set; }
        public long FileSize { get; set; }
    }

    public class CurrentFileProgressEventArgs
    {
        public double ProgressPercentage { get; set; }
        public string FileName { get; set; }
    }

    public class ProgressEventArgs
    {
        public double ProgressPercentage { get; set; }
    }

    public class ErrorUploadEventArgs
    {
        public List<string> SuccessFullFileName { get; set; }
        public List<string> FileNameWithError { get; set; }
    }

    public class CurrentFileErrorUploadEventArgs
    {
        public string FileName { get; set; }
        public Exception InnerException { get; set; }
    }
}

4.Exemple d'application

4.1.Lier l'upload à un contrôle ProgressBar

On pourra désormais utiliser simplement l'upload, et le lier par exemple à une ProgressBar.
Pour cela, rien de plus simple :

 
Sélectionnez

<UserControl x:Class="DemoUpload.Page"
	    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
	    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	    <Canvas>
	        <TextBlock x:Name="textEnvoyer" Text="Choisir un fichier : " Canvas.Top="5" />
	        <Button x:Name="btnParcourir" Content="Parcourir ..." Canvas.Left="120" Width="90" Height="30" Click="Upload_Click" />
	        <TextBlock x:Name="result" Canvas.Top="5" Canvas.Left="250" />
	        <ProgressBar x:Name="GlobalProgressBar" Width="200" Height="20" Canvas.Top="50" Minimum="0" Maximum="100" />
	        <ProgressBar x:Name="IndividualProgressBar" Width="200" Height="20" Canvas.Top="90" Minimum="0" Maximum="100" />
	        <TextBlock x:Name="verif" Canvas.Top="130" />
	    </Canvas>
</UserControl>
 
Sélectionnez

public partial class Page : UserControl
{
	private readonly List<FrameworkElement> controlProgressBar;
	public Page()
	{
		InitializeComponent();
		controlProgressBar = new List<FrameworkElement> { GlobalProgressBar, IndividualProgressBar, verif };
		SetVisibilityOnControls(controlProgressBar, false);
	}

	private void SetVisibilityOnControls(IEnumerable<FrameworkElement> elements, bool visible)
	{
		Dispatcher.BeginInvoke(() =>
		{
			foreach (FrameworkElement element in elements)
				element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
		});
	}

	private void Upload_Click(object sender, RoutedEventArgs e)
	{
		try
		{
			OpenFileDialog dialog = new OpenFileDialog
			{
				Filter = "Image files (*.gif;*.jpg;*.png)|*.gif;*.jpg;*.png",
				Multiselect = true
			};
			if (dialog.ShowDialog() == true)
			{
				var fichiers = dialog.Files;
				GlobalProgressBar.Value = 0;
				IndividualProgressBar.Value = 0;
				SetVisibilityOnControls(controlProgressBar, true);
				btnParcourir.IsEnabled = false;
				Dispatcher.BeginInvoke(() => { verif.Text = ""; });
				Dispatcher.BeginInvoke(() => { result.Text = "Envoi en cours ..."; });

				WSUploader wsUploader = new WSUploader { MaxTryPerPacket = 10 };
				wsUploader.OnCurrentFileUploadCompleted += wsUploader_OnCurrentFileUploadCompleted;
				wsUploader.OnCurrentFileUploadError += wsUploader_OnCurrentFileUploadError;
				wsUploader.OnCurrentFileUploadProgress += wsUploader_OnCurrentFileUploadProgress;
				wsUploader.OnCurrentFileVerifyUpload += wsUploader_OnCurrentFileVerifyUpload;

				wsUploader.OnUploadCompleted += wsUploader_OnUploadCompleted;
				wsUploader.OnUploadError += wsUploader_OnUploadError;
				wsUploader.OnUploadProgress += wsUploader_OnUploadProgress;
				wsUploader.Upload(fichiers.Select(f => f).ToDictionary(f => f.OpenRead(), f => f.Name));
			}
		}
		catch (Exception ex)
		{
			Dispatcher.BeginInvoke(() => { result.Text = ex.InnerException.Message; });
		}
	}

	void wsUploader_OnCurrentFileVerifyUpload(VerifyUploadEventArgs args)
	{
		Dispatcher.BeginInvoke(() => { verif.Text = args.ResultOK ? "Vérification OK" : "Vérification KO"; });
	}

	void wsUploader_OnCurrentFileUploadProgress(CurrentFileProgressEventArgs args)
	{
		Dispatcher.BeginInvoke(() => { IndividualProgressBar.Value = args.ProgressPercentage; });
	}

	void wsUploader_OnCurrentFileUploadError(CurrentFileErrorUploadEventArgs args, ref bool DeleteFileOnServer)
	{
		DeleteFileOnServer = true;
		Dispatcher.BeginInvoke(() => { result.Text = 
			string.Format("Erreur dans l'envoi du fichier {0} => {1}", 
			args.FileName, args.InnerException.Message); });
	}

	void wsUploader_OnCurrentFileUploadCompleted(CurrentFileUploadCompletedEventArgs args, ref bool verifyUpload)
	{
		Dispatcher.BeginInvoke(() => { IndividualProgressBar.Value = 100; });
		verifyUpload = true;
	}

	void wsUploader_OnUploadProgress(ProgressEventArgs args)
	{
		Dispatcher.BeginInvoke(() => { GlobalProgressBar.Value = args.ProgressPercentage; });
	}

	void wsUploader_OnUploadError(ErrorUploadEventArgs args)
	{
		StringBuilder stringBuilder = new StringBuilder();
		foreach (string file in args.FileNameWithError)
		{
			if (stringBuilder.Length != 0)
				stringBuilder.Append(", ");
			stringBuilder.Append(file);
		}
		Dispatcher.BeginInvoke(() => 
			{ result.Text = string.Format("Erreur dans l'envoi des fichiers : {0}", stringBuilder); });
	}

	void wsUploader_OnUploadCompleted(UploadCompletedEventArgs args)
	{
		Dispatcher.BeginInvoke(() => { GlobalProgressBar.Value = 100; });
		Dispatcher.BeginInvoke(() => { IndividualProgressBar.Value = 100; });
		Dispatcher.BeginInvoke(() => { btnParcourir.IsEnabled = true; });
		Dispatcher.BeginInvoke(() => { result.Text = "Envoi terminé"; });
	}
}

Ce qui nous donne :

Image non disponible

4.2.Autoriser le cross domain

Si votre script PHP à appeler est sur un domaine différent de celui où est hébergée votre application Silverlight, il faudra autoriser explicitement l'application à appeler une url externe.
Pour ce faire, le site de destination devra posséder un fichier crossdomain.xml à la racine du serveur, dont le contenu autorise le cross-domain.
L'exemple suivant autorise tous les appels :

 
Sélectionnez
<cross-domain-policy>
	<allow-access-from domain="*"/>
</cross-domain-policy>

Plus d'infos sur msdn.

4.3.Téléchargement

Vous pouvez télécharger ici les sources du projet (ws php inclu) : version rar (180 Ko) , version zip (238 Ko).

5.Conclusion

Cet article montre comment utiliser NuSOAP et Silverlight pour faire de l'upload de fichier par web service sur un serveur utilisant PHP.

Remerciements

Je remercie Djé ainsi que l'équipe Dotnet pour leurs relectures attentives du document.

Contact

Si vous constatez une erreur dans le tutorial, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.