SL4 XNA Platformer Level Editor pour WP7 : le stockage Azure (3/4)

Nous avons vu dans le billet précédent quelques détails sur la manière dont j’ai créé l’application Silverlight. Voyons voir dans ce 3ème article les choix que j’ai retenu pour communiquer avec le stockage de Windows Azure.

Note : si vous souhaitez en savoir davantage sur Windows Azure, n’hésitez pas à consulter notre coach MSDN : https://msdn.microsoft.com/fr-fr/windowsazure/msdn.coach.azure.aspx . Il y a notamment une partie sur le stockage.

Mon objectif pour le stockage des niveaux dans le “cloud” était d’utiliser un emplacement ne nécessitant pas la mise en place d’une logique serveur de contrôle d’accès et de passerelle pour le téléchargement des fichiers. En gros, je voulais avoir une solution équivalente à une simple opération de type HTTP PUT sur un serveur Web pour l’écriture des niveaux dans le cloud.

Choix effectué côté Windows Azure

Dans Windows Azure, pour pouvoir discuter avec la zone de stockage (blob, queue, table), il faut disposer de la clé d’accès principale que l’on retrouve dans le portail d’administration d’Azure :

AzurePortalArticle3SL4LE

Cette clé est assez sensible, je vous déconseille de la partager avec le reste de la planète. :-) Or, pour pouvoir discuter directement avec le stockage Azure depuis le client Silverlight, il faudrait mettre la clé en dur dans le code de l’application Silverlight. Et cela revient justement à partager sa clé avec le reste de la planète vu la facilité avec laquelle on peut désassembler une application Silverlight.

Donc la solution classique consiste à n’utiliser cette clé que dans un rôle Web Azure côté ASP.NET par exemple. Ce rôle Web étant hébergé dans Azure, on ne craint rien. Voici donc le type d’enchainement que l’on a l’habitude de voir dans Azure pour ce type de besoin

1 – Un Web Role hébergé dans Windows Azure donc
2 – Ce Web Role héberge quand à lui un service WCF utilisant la clé et les APIs de stockage pour pousser des informations dans les blobs Azure via HTTP REST. Ce service WCF expose des méthodes pour récupérer les fichiers et pousser des fichiers vers les blobs. Ce même service peut éventuellement également ajouter de la logique de contrôle d’accès.
3 – Le client Silverlight référence le service WCF, s’authentifie et utilise les méthodes de lecture/ajout de fichiers via des appels HTTP.

ArchitureClassicAzureSLStorageAccess

Il y a donc un intermédiaire que je ne souhaite pas avoir. Donc, on résume ce que je souhaitait: utiliser le stockage Azure directement depuis mon application Silverlight sans passer par un service WCF le tout sans partager pour autant ma clé principale. Alors c’est possible ou pas ? Oui !

Malgré tout, on ne peut pas autoriser l’écriture pour tout le monde sur un container du stockage Azure. Cependant, on peut créer des URLs valides de manière temporaire sur lesquelles on va positionner des droits particuliers parmi cette liste de choix : lister la liste des fichiers, lecture des fichiers présents, écriture ou suppression.

C’est ce que l’on appelle des “Shared Keys”.

Ressources :

- Authentication Schemes : https://msdn.microsoft.com/en-us/library/dd179428.aspx
- Cloud Cover Episode 8 - Shared Access Signatures : https://channel9.msdn.com/shows/Cloud+Cover/Cloud-Cover-Episode-8-Shared-Access-Signatures/
- Ce post explique également bien les différentes options qui s’offrent à vous pour accéder au stockage Azure : https://blogs.msdn.com/b/eugeniop/archive/2010/04/13/windows-azure-guidance-using-shared-key-signatures-for-images-in-a-expense.aspx
- Et surtout l’excellente série de posts de Steve Marx sur ce sujet: https://blog.smarx.com/posts/uploading-windows-azure-blobs-from-silverlight-part-1-shared-access-signatures dont le blog devrait faire parti de vos favoris ! Il présente ici comment générer ces clés de manière dynamique côté serveur pour laisser ensuite un client Silverlight faire du multi-upload vers son stockage à travers ces clés valide temporairement.

Cette clé ressemble à cela :

image_thumb_5

Pour générer cette clé, plutôt que de passer par le SDK d’Azure et par un petit morceau de code .NET comme on peut le voir dans le sample à télécharger sur le blog de Steve Marx, j’ai préféré passer par un super outil nommé Cloud Storage Studio pour le faire. C’est un soft payant écrit en WPF avec une période d’évaluation de 30 jours. Il y a même une version amusante en Siverlight à tester en ligne : https://onlinedemo.cerebrata.com/cerebrata.cloudstorage/ mais pour l’instant beaucoup plus limitée.

Voici les étapes à suivre pour créer cette URL magique depuis cet outil:

1 – Sur l’un de vos containers, faites bouton droit –> “Container Access Policy” pour afficher les droits affectés à ce dernier :

CssSharedKeyStep001

2 – Créez une nouvelle politique temporaire en cochant la case, en choisissant la période de validité et en choisissant les différents droits :

CssSharedKeyStep002 

3 – Après avoir cliqué sur “Generate Signed URL”, vous arriverez sur cette fenêtre :

CssSharedKeyStep003 

Choisissez dans la combobox la politique que vous avez créée précédemment puis cliquez sur “Generate Signed URL” et vous obtiendrez une URL comme affichée au bas de la copie d’écran.

Le code côté Silverlight

Il y a 2 parties à gérer côté Silverlight. Dans un premier temps, il faut effectuer une requête vers le container Azure pour connaître la liste des fichiers disponibles puis dans un second temps lancer des requêtes pour télécharger l’ensemble de ces fichiers et charger les niveaux correspondant en mémoire.

Lister l’ensemble des fichiers disponibles

Pour lister l’ensemble des fichiers, rien de plus simple. Comme le décrit l’article suivant du SDK Windows Azure :

- List Blobs : https://msdn.microsoft.com/en-us/library/dd135734.aspx

Il faut envoyer une requête REST une l’URL particulière et on obtient un flux XML en retour. Par exemple, dans mon cas, voici l’URL à attaquer : https://david.blob.core.windows.net/slplatformer?restype=container&comp=list 

Et voici le type de retour XML :

 <?xml version="1.0" encoding="utf-8" ?>
<EnumerationResults ContainerName="https://david.blob.core.windows.net/slplatformer">
  - <Blobs>
    - <Blob>
      <Name>0.txt</Name>
      <Url>https://david.blob.core.windows.net/slplatformer/0.txt</Url>
      - <Properties>
        <Last-Modified>Tue, 27 Jul 2010 18:32:03 GMT</Last-Modified>
        ...
      </Properties>
    </Blob>
    + <Blob>
      <Name>1.txt</Name>
      <Url>https://david.blob.core.windows.net/slplatformer/1.txt</Url>
      - <Properties>
        <Last-Modified>Thu, 29 Jul 2010 01:12:23 GMT</Last-Modified>
        ...
      </Properties>
    </Blob>
  </Blobs>
  <NextMarker />
</EnumerationResults>

Nous n’avons plus qu’à faire un petit coup de mapping avec LINQ to XML. Dans l’exemple de code que vous pouvez télécharger dans l’article précédent, nous partons de la classe suivante :

 // Little class used for the XML object mapping
// during the REST list request to the Azure Blob
public class CloudLevel
{
    public string Name { get; set; }
    public string Url { get; set; }
    public int ContentLength { get; set; }
}

Voici le code pour télécharger la liste des niveaux :

 string UrlListLevels = @"https://david.blob.core.windows.net/slplatformer?restype=container&comp=list";

CloudLevelsListLoader.DownloadStringCompleted += new DownloadStringCompletedEventHandler(LevelsLoader_DownloadStringCompleted);
CloudLevelsListLoader.DownloadStringAsync(new Uri(UrlListLevels));

Et on effectue alors le mapping via ce code :

 // We're going to user Linq to XML to parse the result
// and do an object mapping with the little class named CloudLevel
XElement xmlLevelsList = XElement.Parse(e.Result);

var cloudLevels = from level in xmlLevelsList.Descendants("Blob")
                    .OrderBy(l => l.Element("Name").Value.ToString())
                    select new CloudLevel
                    {
                        Name = level.Element("Name").Value.ToString(),
                        Url = level.Element("Url").Value.ToString(),
                        ContentLength = int.Parse(level.Element("Properties").Element(@"Content-Length").Value)
                    };

Charger les niveaux

Une fois que l’on a la liste des fichiers, il ne nous reste plus qu’à la parcourir. Pour chacun des fichiers, on lance une requête de chargement asynchrone via l’objet WebClient :

 // For each 160 bytes file listed in XML returned by the first REST request
// We're loading them asynchronously via the WebClient object 
foreach (CloudLevel level in filteredCloudLevel)
{
    WebClient levelReader = new WebClient();
    levelReader.OpenReadCompleted += new OpenReadCompletedEventHandler(levelReader_OpenReadCompleted);
    levelReader.OpenReadAsync(new Uri(level.Url), level.Name);
}

Puis ensuite sur le retour, on récupère le stream renvoyé et on charge le niveau en mémoire :

 // Once callbacked by the WebClient
// We're loading the level in memory
// The LoadLevel method is the same one used when the user loads a file from the disk
void levelReader_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    string fileName = e.UserState.ToString();
    StreamReader reader = new StreamReader(e.Result);
    LoadLevel(reader, fileName);
}

Par contre, vous noterez qu’avec cette technique on ne contrôle pas l’ordre dans lequel on va recevoir les fichiers. On envoi en effet quasiment en simultané N requêtes via N WebClient différents. Vous n’aurez donc pas forcément les niveaux qui arriveront dans l’ordre 0.txt, 1.txt, etc. puisque ces requêtes ne sont pas sérialisées.

Sauvegarder les niveaux dans Azure

Il ne nous reste plus qu’à sauvegarder l’un ou l’ensemble des niveaux vers le stockage Azure. Pour cela, il nous faut construire l’URL que l’on a vu plus haut avec l’outil Cloud Storage Studio et l’utiliser pour faire une requête de type PUT. Cela donne ce genre de code :

 // Using part of the code of Steve Marx
// https://blog.smarx.com/posts/uploading-windows-azure-blobs-from-silverlight-part-2-enabling-cross-domain-access-to-blobs
private void SaveThisTextLevelInTheCloud(char[] textLevel, string levelName)
{
    // TO DO: change this 3 values to match your own account
    // You'll need also to build a blob container in Azure with a shared key allowing writing for a specific amount of time
    string account = "david";
    string container = "slplatformer";
    string sas = "&sr=c&si=levels&sig=q7%2FODk%2FfgwKbdAecGto0%2B5HP%2FnRs45VQRrzdO%2FyFZEQ%3D";
    // Building the magic URL allowing someone to write/read into an Azure Blob during a limited amount of time without
    // knowing the private main storage key of your Azure Storage 
    string url = string.Format("https://{0}.blob.core.windows.net/{1}/{2}?{3}", account, container, levelName, sas);

    Uri uri = new Uri(url);

    var webRequest = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(uri);
    webRequest.Method = "PUT";
    webRequest.ContentType = "text/plain";
    try
    {
        webRequest.BeginGetRequestStream((ar) =>
        {
            using (var writer = new StreamWriter(webRequest.EndGetRequestStream(ar)))
            {
                writer.Write(textLevel);
            }
            webRequest.BeginGetResponse((ar2) =>
            {
                if (ar2.AsyncState != null)
                    ((HttpWebRequest)ar2.AsyncState).EndGetResponse(ar2);
            }, null);
        }, null);
    }
    catch (Exception ex)
    {
    }
}

Appels Cross-Domain de Silverlight

Pour que cela fonctionne, il ne faut pas oublier de positionner un fichier ClientAccessPolicy.xml dans le répertoire racine root$ de votre stockage. Vous pouvez le faire également avec l’outil Cloud Storage Studio ou par contre à nouveau.

Nous verrons dans le dernier article la partie concernant le jeu XNA sur Windows Phone 7 dans lequel vous pourrez récupérer son code source.

David