\

Školení Návrhové vzory, OOP a UML


 Wednesday, 20 January 2010
Ukázka práce s Posterous API – zálohování blogu

Stáhnout výsledné exe -RSPosterousBackup.

Po jednoduchém přehledu možností mého C# Posterous API wrapperu se nyní podíváme, jak se dá API použít k zálohování vašeho blogu. Pro účely tohoto článku předpokládám, že jste úvodní článek o API wrapperu četli.

Zálohovač blogu (RSPosterousBackup.exe) je jednoduchá konzolová aplikace, které stačí předat uživatelské jméno (parametr –u)  a heslo (parametr –p)  vašeho účtu na Posterous a také adresář vašem počítači (parametr bd), do kterého chcete blog zazálohovat.

Jednoduchá ukázka:

RSPosterousBackup.exe -u:rene@renestein.net -p:mojeheslo -bd:c:\_Archiv\PosterousBackup

Blog uživatelského účtu reneATrenestein.net s heslem mojeheslo bude zazálohován do adresáře c:\_Archiv\PosterousBackup.

Program referencuje samozřejmě knihovnu RStein.Posterous.API a pro (své, uznávám :-)) pobavení jsem také použil RX for .Net Framework 3.5 SP1. Z RX Frameworku jsou referencovány assembly System.CoreEx, System.Interactive a System.Threading. Nejzajímavější na RX Frameworku je, že pro verzi 3.5 .Net Frameworku zpřístupňuje Parallel Linq – PLINQ, který je součástí připravovaného .Net Frameworku 4.0.

Ještě upozornění – v žádném případě nechci tvrdit, že kód, který uvidíte, využívá PLINQ správným způsobem. Program je jen pískovištěm, na kterém jsem si zkoušel a ověřoval, co RX a PLINQ umí a jak vypadá výsledný kód.

Po spuštění RSPosterousBackup.exe funkce Main pouze předá argumenty z příkazové řádky metodě BackupData v třídě BackupEngine, která představuje výkonný mozek celého zálohovače Posterous blogu.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace RSPosterousBackup
{
    class Program
    {
        static void Main(string[] args)
        {
            var engine = new BackupEngine();
            engine.BackupData(args);
            Console.ReadLine();
        }
    }
}

 

Zde je třída BackupEngine

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using RStein.Posterous.API;

namespace RSPosterousBackup
{
    public class BackupEngine
    {
        #region constants
        public static readonly string POSTEROUS_USER = "-u";
        public static readonly string POSTEROUS_PASSWORD = "-p";
        public static readonly string BACKUP_DIRECTORY= "-bd";
        public static readonly string HELP_KEY = "-?";
        public static readonly string HELP_ALT_KEY = "-h";
        public static readonly string POSTEROUS_FILE_EXTENSION = ".posterous";
        public static readonly string POSTEROUS_MEDIA_FORMAT = "Media_{0}";
        public static readonly string POSTEROUS_COMMENT_FORMAT = "Comment_{0}";
        public static readonly string POSTEROUS_SITE_FORMAT = "Site_{0}";
        #endregion constants
        #region private variables        
        #endregion private variables

        #region constructors
        public BackupEngine()
        {
        }
        #endregion constructors
        
        public void BackupData(string[] commandLine)
        {
            if (commandLine == null)
            {
                throw new ArgumentNullException("commandLine");
            }

            var parser = new CommandLineParser();
            Dictionary<string, string> cmSwitches = parser.Parse(commandLine);

            Debug.Assert(cmSwitches != null);

            if (cmSwitches.Keys.Any(key => (key.Equals(HELP_KEY, StringComparison.OrdinalIgnoreCase)) ||
                                       (key.Equals(HELP_ALT_KEY,StringComparison.OrdinalIgnoreCase))))
            {
                logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
                return;
            }

            if (!checkSwitches(cmSwitches))
            {
                logMessage(GlobalConstants.CMDLINE_SHOW_USAGE);
                return;                
            }

            backupDataInner(cmSwitches);
        }

        private void backupDataInner(Dictionary<string, string> cmSwitches)
        {
            string pUser = cmSwitches[POSTEROUS_USER];
            string pPassword = cmSwitches[POSTEROUS_PASSWORD];
            string backupDir = cmSwitches[BACKUP_DIRECTORY];
            try
            {
                IPosterousAccount account = PosterousApplication.Current.GetPosterousAccount(pUser, pPassword);
                if (!Directory.Exists(backupDir))
                {
                    Directory.CreateDirectory(backupDir);
                }

                var currDirBackupName = new StringBuilder(DateTime.Now.ToLocalTime().ToString());
                currDirBackupName.ToSafeFileName();
                
                
                string currentBackupDir = Path.Combine(backupDir, currDirBackupName.ToString());
                account.Sites.Run(site =>
                                     {
                                         string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                         Directory.CreateDirectory(sitePath);
                                     });


                var processedPosts = (from site in account.Sites.AsParallel()
                                      from post in site.Posts.AsParallel()
                                         .Do(postPost =>
                                                  Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
                                                                         postPost.Title,
                                                                         Thread.CurrentThread.ManagedThreadId)))
                                        
                                         .Do(postPost =>
                                                 {                                                  
                                                     string siteDir = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                                     
                                                     var titlBuilder = new StringBuilder(postPost.Title + postPost.Id.ToString());
                                                     titlBuilder.ToSafeFileName();

                                                     string postDirName = Path.Combine(siteDir,
                                                                                       titlBuilder.ToString()
                                                                                       );
                                                     
                                                     Directory.CreateDirectory(postDirName);
                                                     string postFileName = Path.Combine(postDirName, titlBuilder.ToString() + POSTEROUS_FILE_EXTENSION);
                                                     using (var fileStream = File.Open(postFileName, FileMode.Create))
                                                     using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
                                                     {
                                                         stremWriter.Write(postPost.Body);
                                                     }

                                                     postPost.Media
                                                         .AsParallel()
                                                         .Run(media =>
                                                                 {
                                                                                                             
                                                                     var mediaName = new StringBuilder(String.Format(POSTEROUS_MEDIA_FORMAT, Guid.NewGuid()));
                                                                     mediaName.ToSafeFileName();
                                                                     string mediaFile = Path.Combine(postDirName, mediaName.ToString());


                                                                     using (var fileStream = File.Open(mediaFile, FileMode.Create))
                                                                     {


                                                                         media.Content.CopyToStream(fileStream);

                                                                     }
                                                                 });


                                                     postPost.Comments
                                                         .AsParallel()                                                         
                                                         .Run(comment =>
                                                                 {
                                                                                                             
                                                                     var commentFileName = new StringBuilder(String.Format(POSTEROUS_COMMENT_FORMAT, Guid.NewGuid()));
                                                                     commentFileName.ToSafeFileName();
                                                                     string commentFile = Path.Combine(postDirName, commentFileName.ToString());


                                                                     using (var fileStream = File.Open(commentFile, FileMode.Create))
                                                                     using (var stremWriter = new StreamWriter(fileStream, Encoding.UTF8))
                                                                     {


                                                                         stremWriter.WriteLine(comment.Author.Name);
                                                                         stremWriter.Write(comment.Body);

                                                                     }
                                                                 });
                                                 })

                                     select post).ToList();





                logMessage(String.Format(GlobalConstants.FINAL_BACKUP_MESSAGE_FORMAT, processedPosts.Count));
                
                
            }
            catch (Exception e)
            {
                 Trace.WriteLine(e);
                 Console.WriteLine(e);                 
            }
           
            
        }

        private bool checkSwitches(Dictionary<string, string> cmSwitches)
        {            

            return ckeckIfSwitchNullOrEmpty(POSTEROUS_USER, cmSwitches) &&
                    ckeckIfSwitchNullOrEmpty(POSTEROUS_PASSWORD, cmSwitches) &&
                    ckeckIfSwitchNullOrEmpty(BACKUP_DIRECTORY, cmSwitches);
            
        }

        private bool ckeckIfSwitchNullOrEmpty(string switchKey, Dictionary<string, string> cmSwitches)
        {
            string val;
            cmSwitches.TryGetValue(switchKey, out val);
            if (String.IsNullOrEmpty(val))
            {
                logMessage(String.Format(GlobalConstants.INVALID_CML_SWITCH_FORMAT_STRING_EX, switchKey));
                return false;
            }
            return true;
        }

        private void logMessage(string message)
        {
            
            Console.WriteLine(message);
        }
    }
}

Třída BackupEngine nejdříve v metodě BackupData pomocí instance třídy CommandLineParser rozpársuje příkazový řádek a voláním pomocné metody checkSwitches zkontroluje, zda byly předány všechny vyžadované parametry. Jestliže nějaký parametr chybí nebo byl zadán parametr pro zobrazení nápovědy (-h, –?), program zobrazí nápovědu a k zálohování nedojde.

Ihned po dokončení všech předběžných kontrol je volána privátní metoda backupDataInner, která je odpovědná za zálohování blogu. Metoda backupDataInner získá odkaz na Posterous účet (PosterousApplication.Current.GetPosterousAccount(pUser, pPassword); a poté pro každou Site (samostatný blog) založí nový podadresář v adresáři, jehož názvem je aktuální lokální datum a čas a který je vytvořen v adresáři předaném v parametru –bd uživatelem. 

Adresářová struktura pro každý zálovaný blog:

<adresář určený –bd přepínačem>\<adresář  - názvem je aktuální datum a čas>\Site_<Site Id>

Příklad adresářové struktury:

"c:\_Archiv\PosterousBackup\20.1.2010 15_57_07\Site_851694"

Zálohován tedy není jeden blog, ale všechny blogy, které jsou asociovány s daným posterous účtem.

Můžete si všimnout, že pro založení podaresáře používvám jednu z RX extenzních metod – metodu Run.

                account.Sites.Run(site =>
                                     {
                                         string sitePath = Path.Combine(currentBackupDir, String.Format(POSTEROUS_SITE_FORMAT, site.Id.ToString()));
                                         Directory.CreateDirectory(sitePath);
                                     });

Metoda Run je náhradou za extenzní metodu ForEach, kterou jste si dříve museli sami dopsat nebo jste byli nuceni použít metodu ForEach ve třídě List takto.

account.Sites.ToList().ForEach(site => {//zbytek kódu lambdy identický s kódem výše…}); 



Zdůrazním, že metoda Run vykoná nějaký vedlejší efekt nad každým elementem v IEnumerable, tedy v našem případě založí adresář, a dále již s elementy nepracuje – vrací void. Za chvíli uvidíme metodu, která nám umožní to samé, co metoda Run, ale elementy pošle po “vykonání vedlejšího efektu nad elementem” (tedy spuštění námi předané funkce jakou je např. lambda  pro založení adresáře) ke zpracování dalším LINQ extenzním metodám.

Signatura metody Run:

public static void Run<TSource>(
    this IEnumerable<TSource> source,
    Action<TSource> action
)

Dále do proměnné processedPosts uložíme pomocí speciálního a pro naši kratochvíli jediného LINQ dotazu všechny zpracované blogposty (instance podporující rozhraní IPosterousPost). Jak vidíte, stačí se přes Posterous API dotázat do kolekce Sites (blogy) a poté z každého blogu zpracovat všechny blogspoty.

from site in account.Sites.AsParallel()
                                      from post in site.Posts.AsParallel()…

Na více místech v dotazu si můžete všimnout volání extenzní metody AsParalllel, kterým dáváte najevo, že zpracování jednotlivých blogpostů a také médií (IPosterousMedium) a komentářů (IPosterousComment) může proběhnout ve více vláknech – o detaily se ale postará PLINQ, vy sami žádná nová vlákna nespouštíte ani nespravujete.

Můžete si také všimnout, že na několika místech volám extenzní metodu Do .Metoda Do pracuje podobně jako před chvílí zmiňovaná metoda Run. Na každý element v zdrojové kolekci aplikuje předanou funkci, ale poté na rozdíl od metody Run element předá k dalšímu zpracování.

Signatura metody Do:

public static IEnumerable<TSource> Do<TSource>(
	this IEnumerable<TSource> source,
	Action<TSource> action
)
 

Zde vypíšeme přes RX extenzní metodu Do titulek právě zpracovávaného blogpostu a Id vlákna, které blogspot zpracovává. Tato metoda je zde jen na ukázku, že každý element ve zdrojové kolekci je metodou Do předán dále ke zpracování

 .Do(postPost =>
                                                  Console.WriteLine(String.Format(GlobalConstants.POST_BACKUP_MESSAGE_FORMAT,
                                                                         postPost.Title,
                                                                         Thread.CurrentThread.ManagedThreadId)))

Na metodu Do navazuje další metoda Do, ve které proběhne zpracování každého blogspotu. V adresáří každé Site (blogu) je vytvořen pro každý blogspot vytvořen nový podadresář, jehož názvem je titulek (vlastnost Title) společně s  Id blogpostu. Do tohoto podadresáře je uložen text blogspotu. Soubor s textem blogspotu má příponu posterous a také jsou do podadresáře uložena média (zvukové, obrazové a video soubory) a všechny komentáře k blogspotu.

Hlavní část programu je za námi. Zde jsou ještě výpisy pomocných tříd.

Třída CommandLineParser pro pársování hodnot předaných uživatelem v příkazovém řádku.

using System;
using System.Collections.Generic;
using System.Linq;

namespace RSPosterousBackup
{
    public class CommandLineParser
    {
        #region constants

        public static readonly char COMMAND_PARTS_SEPARATOR = '-';
        public static readonly char COMMAND_VALUE_SEPARATOR = ':';
        public const int MIN_KEY_PARTS = 1;
        public const int MAX_KEY_PARTS = 2;
        #endregion constants
        
        #region constructors
        public CommandLineParser()
        {
        }
        #endregion constructors
        #region methods
        public virtual Dictionary<string, string> Parse (string[] commandLine)
        {
            if (commandLine == null)
            {
                throw new ArgumentNullException("commandLine");
            }
            
            return parseInner(commandLine);
        }

        private Dictionary<string, string> parseInner(string[] commandLine)
        {            
            var dict = (from part in commandLine
                        let keyValuePair = part.Split(new[] {COMMAND_VALUE_SEPARATOR}, MAX_KEY_PARTS)
                        select new
                                   {
                                       Key = keyValuePair.First().Trim().ToLower(),
                                       Value = keyValuePair.Length > MIN_KEY_PARTS ? keyValuePair.Last().Trim() : String.Empty
                                   }).ToDictionary(kv => kv.Key, kv => kv.Value);

            return dict;
        }

        #endregion methods
    }
}

Konstanty

namespace RSPosterousBackup
{
    public static class GlobalConstants
    {
        public static readonly string INVALID_CML_SWITCH_FORMAT_STRING_EX = "Invalid switch {0}";
        public static readonly char SAFE_FILE_PATH_CHAR = '_';        
        public static readonly string POST_BACKUP_MESSAGE_FORMAT = "Processing post: {0}  - in thread {1}";
        public static readonly string FINAL_BACKUP_MESSAGE_FORMAT = "Total posts: {0}";
        public static readonly string CMDLINE_SHOW_USAGE =
@"Usage: 
RSPosterousBackup.exe -u:<posterous user name> p:<posterous password> -bd:<backup directory>
Example:RSPosterousBackup.exe -u:GIC@Roma.com p:Rubicon -bd:c:\PosterousBackup
RSPosterousBackup.exe -?  - show this help";
                                                                 
    }
}

Třída StringBuilderExtensions s extenzní metodou ToSafeFileName, která v navrhovaném jménu souboru nahradí nepovolené znaky podtržítkem.

using System.IO;
using System.Linq;
using System.Text;

namespace RSPosterousBackup
{
    public static class StringBuilderExtensions
    {
        public static void ToSafeFileName(this StringBuilder builder)
        {
            Path.GetInvalidFileNameChars().Run(ch => builder.Replace(ch, GlobalConstants.SAFE_FILE_PATH_CHAR));
        }
    }
}

Stáhnout výsledné exe -RSPosterousBackup.



Wednesday, 20 January 2010 17:54:21 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  .NET Framework | C# Posterous API | LINQ | RX Extensions