Der schnelle Weg zum Server

Veröffentlicht: 30. Jan 2006

Von Andreas Müller

Die hier vorgestellte Anwendung erleichtert das Deployment von Webseiten. Sie leert die Zielordner auf dem Server und berücksichtigt beim Kopieren von Dateien die vom Anwender vorgegebenen Kriterien. Zudem lässt sich das Tool für andere Zwecke anpassen, etwa für Projekt-Backups.

dotnetpro


Diesen Artikel können Sie dank freundlicher Unterstützung von dotnetpro auf MSDN Online lesen. dotnetpro ist ein Partner von MSDN Online.
Ausgabe 10/2005


Zur Partnerübersichtsseite von MSDN Online

Es ist geschafft: Alle Änderungen der Homepage sind fertig gestellt. Nun müssen sie noch vom Entwicklungsserver auf den Produktivserver kopiert werden. Dafür bieten sich verschiedene Möglichkeiten an, beispielsweise per Copy-and-Paste mit dem Explorer oder skriptgesteuert mit einer Batch-Datei. Die meisten Verfahren haben allerdings Nachteile. Mit Copy-and-Paste werden häufig auch Dateien kopiert, die gar nicht benötigt werden. Im schlimmsten Fall führt das sogar dazu, dass Unbefugte den Quellcode einsehen können.

Beim Deployment mithilfe einer Batch-Datei werden mitunter Dateien ohne Warnung überschrieben, weil die Batch meist so programmiert ist, dass auftretende Fehler ignoriert werden. Prüft das Skript auf Fehler, muss der Anwender jeden auftretenden Konflikt bestätigen. Das wiederum ist lästig, führt zu Flüchtigkeitsfehlern und ist bei großen Projekten mit mehreren hundert Objekten kaum praktikabel. Das Erstellen eines Skripts für eine FTP-Übertragung ist dagegen alles andere als trivial.

Auf dieser Seite

 FileTransfer kurz vorgestellt
 Transfer mit Filecopy
 Dateien gezielt weglassen
 Transfer mit FTP
 Konfigurationsdateien
 Fazit
 Literatur
 Über den Autor

FileTransfer kurz vorgestellt

FileTransfer bietet die Möglichkeit des Deployments per FileCopy oder FTP an. Es kann das Zielverzeichnis vor dem Kopieren leeren, während des Kopierens darauf achten, dass nur Dateien ab einem bestimmten Datum (mit Uhrzeit) übertragen werden, nach dem Kopieren leere Verzeichnisse löschen, bestimmte Dateien oder Ordner vom Übertragen ausschließen, abhängig von ihrem Namen oder der Dateinamenserweiterung. FileTransfer lässt sich leicht erweitern und an individuelle Wünsche anpassen, zum Beispiel wäre es möglich, Jokerzeichen (Wildcards) einzuführen. Abbildung 1 zeigt die Anwendung in Aktion.

File Transfer in Aktion
Abbildung 1 FileTransfer in Aktion.

 

Transfer mit Filecopy

Das Filecopy-Deployment erfolgt mithilfe der .NET-Objekte aus dem System.IO-Namespace. Vor allem die Klassen File und Directory werden dazu verwendet.

Zum Löschen eines Verzeichnisses genügt es laut Dokumentation, Directory.Delete(folder, true) aufzurufen, wobei das true für rekursiv steht, also alle Unterobjekte einschließt. Die Praxis hat allerdings gezeigt, dass dies bei Netzlaufwerken nicht immer funktioniert. Warum und unter welchen Voraussetzungen die Fehler auftreten, ließ sich nicht ermitteln. Der Verdacht liegt aber nahe, dass es dann nicht klappt, wenn das Read-only-Attribut einer Datei gesetzt ist. Deshalb wurde für die Anwendung eine Routine entwickelt, die jedes Objekt einzeln löscht, wie in Listing 1 zu sehen ist.

Listing 1:
Rekursives Löschen von Dateien und Unterordnern in einem angegebenen Ordner.

private void ClearDirectory(string path)
{
    if (transferType == FileTransferType.Copy)
    foreach (string file in Directory.GetFiles(path))
    {
        try
        {
            if ((File.GetAttributes(file) & FileAttributes.ReadOnly) ==
                FileAttributes.ReadOnly)
            {
               File.SetAttributes(file, FileAttributes.Normal);
            }
            File.Delete(file);
        }
        catch (Exception exp) { errors = true; }
    }
    foreach (string folder in Directory.GetDirectories(path))
    {
        StatusText("Leere Ordner " + folder);
        ClearDirectory(folder);
        try
        {
            Directory.Delete(folder);
        }
        catch (Exception exp) { errors = true; }
    }
    else
    {
        // get folder structure
        foreach (FtpDirectoryEntry entry in ftpClient.GetFiles("*"))
        {
            if (entry.EntryType == FtpDirectoryEntryType.File)
            {
                try
                {
                    ftpClient.DeleteFile(entry.Name);
                }
                catch (Exception exp) { errors = true; }
            }
            else
            {
                try
                {
                    ftpClient.CurrentDirectory = entry.Name;
                    StatusText("Leere Ordner " + ftpClient.CurrentDirectory);
                    ClearDirectory("");
                    ftpClient.CurrentDirectory = "..";
                    ftpClient.RemoveDirectory(entry.Name);
                }
                catch (Exception exp) { errors = true; }
            }
        }
    }
} // end ClearDirectory

In ClearDirectory() werden zunächst alle Dateien des aktuellen Ordners durchlaufen, die Read-only-Attribute zurückgesetzt und dann die Datei gelöscht. Anschließend wird ClearDirectory() für jeden Unterordner nochmals aufgerufen, bevor dieser schließlich entfernt wird. Im nächsten Schritt werden die Dateien kopiert. Auch das geschieht rekursiv, diesmal in CopyDirectory(). Ausgehend vom Root-Verzeichis wird zunächst der Zielordner erstellt, sofern er noch nicht existiert:

if (! Directory.Exists(destination))
{
   Directory.CreateDirectory(destination);
}

Ist der Zielordner angelegt, sind die Dateien an der Reihe. Erst wird überprüft, ob die Datei vom Kopieren ausgeschlossen wurde. Ist dies nicht der Fall, wird sie einfach mittels File.Copy(sourceFile, destinationFile,true) übertragen.

Danach wird für jedes Unterverzeichnis wiederum CopyDirectory() ausgeführt, wie in Listing 2 zu sehen ist. Abschließend werden alle Ordner auf dem Zielserver noch einmal durchlaufen und entfernt, falls sich keine Objekte darin befinden. Auch dabei wird wieder rekursiv gearbeitet, wie in Listing 3 nachzuvollziehen ist.

Listing 2:
Rekursives Kopieren von Dateien und Unterordnern eines angegebenen Ordners.

private void CopyDirectory(string source, string destination)
{
    StatusText("Erstelle Directory " + destination);
    // make directory on destination
    if (transferType == FileTransferType.Copy)
    {
        if (! Directory.Exists(destination))
        {
            Directory.CreateDirectory(destination);
        }
    }
    else
    {
        if (destination.Length > 0)
        {
            ftpClient.MakeDirectory(destination);
            ftpClient.CurrentDirectory = destination;
        }
    }
    
    // copy files
    foreach (string file in Directory.GetFiles(source))
    {
        if (! ExcludeFileOrFolder(FileType.File, file))
        {
            StatusText("Kopiere Datei " + file);
            if (transferType == FileTransferType.Copy)
            {
                string destFile = destination +
                file.Substring(file.LastIndexOf(@"\"));
                File.Copy(file, destFile, true);
            }
            else
            {
                string destFile = file.Substring(file.LastIndexOf(@"\")+1);
                ftpClient.SaveFile(destFile, File.Open
                (file, FileMode.Open));
            }
        }
    }
    
    // copy folders
    foreach (string folder in Directory.GetDirectories(source))
    {
        if (! ExcludeFileOrFolder(FileType.Folder, folder))
        {
            string destFolder = destination +
            folder.Substring(folder.LastIndexOf(@"\"));
            CopyDirectory(folder, destFolder);
        }
    }

    if ((transferType == FileTransferType.Ftp) && (destination
        .Length > 0))
ftpClient.CurrentDirectory = "..";
} // end CopyDirectory

Listing3:
Überprüfen auf leere Ordner (rekursiv).

private void DeleteEmptyFolders(string rootFolder)
{
    if (transferType == FileTransferType.Copy)
    {
        foreach (string folder in Directory.GetDirectories(rootFolder))
        {
             StatusText("Prüfe Ordner " + folder);
             DeleteEmptyFolders(folder);
             DirectoryInfo di = new DirectoryInfo(folder);
             if ((di.GetFiles().Length == 0) && (di.GetDirectories().Length == 0))
             {
                 di = null;
                 Directory.Delete(folder);
             }
        }
    }
    else
    {
        foreach (FtpDirectoryEntry entry in ftpClient.GetFiles("*"))
        {
            if (entry.EntryType == FtpDirectoryEntryType.Folder)
            {
                ftpClient.CurrentDirectory = entry.Name;
                StatusText("Prüfe Ordner " + ftpClient.CurrentDirectory);
                DeleteEmptyFolders("");
                ftpClient.CurrentDirectory = "..";
                try
                {
                    ftpClient.RemoveDirectory(entry.Name);
                }
                catch { }
            }
        }
    }
} // end DeleteEmptyFolders

Wie weiter oben schon erwähnt, sollen nicht in jedem Fall alle Objekte auf den Webserver kopiert werden. In einer Collection mit ExcludeFile-Objekten sind die Ausschlusskriterien definiert.

 

Dateien gezielt weglassen

Die Klasse besteht aus den beiden Properties FileName vom Typ String und FileType (Enumeration FileType), einem Konstruktor und der ToString()-Methode für die Anzeige in einer ListBox. FileType kann die vier folgenden Bedeutungen haben:

  • File: eine Datei,

  • Folder: ein Ordner,

  • Extension: eine Dateinamenserweiterung,

  • Startstring: der Beginn eines Dateibeziehungsweiseeines Ordnernamens.

In ExcludeFileOrFolder(), zu sehen in Listing 4, wird die Collection durchlaufen und dabei getestet, ob der übergebene Name den Ausschlusskriterien entspricht. Ist dies der Fall, wird true zurückgegeben und die Datei beziehungsweise der Ordner nicht übertragen.

Listing4:
Ausschlusskriterien überprüfen.

private bool ExcludeFileOrFolder(FileType fileType, string file)
{
    foreach (ExcludeFile ef in lstExclude.Items)
    {
        if ((ef.FileType == fileType) && (file == ef.FileName)) return true;
        if ((ef.FileType == FileType.Extension) && (file.EndsWith(ef.FileName))) return true;
        if (ef.FileType == FileType.Startstring)
        {
            string fn = file.Substring(file.LastIndexOf(@"\")+1);
            if (fn.StartsWith(ef.FileName)) return true;
        }

        if ((chkFilesFrom.Checked) && (fileType == FileType.File))
        {
            FileInfo fi = new FileInfo(file);
            if (fi.LastWriteTime < dateFilesFrom) return true;
        }
    }
    return false;
} // end ExcludeFileOrFolde

 

Transfer mit FTP

Für das Deployment per FTP stellt .NET keine vorgefertigten Klassen bereit. Dotnetpro hat schon in [1] erläutert, wie ein FTP-Client in Visual Basic 6 funktioniert. Darauf aufbauend wurde nun die Klasse FtpClient entwickelt, welche die folgenden Funktionen bietet:

  • Verbinden mit einem Host über den Standardport 21, entweder anonym oder mit Anmeldedaten;

  • Verbinden mit einem Host über einen angegebenen Port, anonym oder mit Anmeldedaten;

  • Dateistruktur übertragen; die Verzeichniseinträge werden als Collection zurückgegeben, jeder Eintrag ist ein eigenes Objekt vom Typ FtpDirectoryEntry;

  • Verzeichnis anlegen, wechseln und löschen;

  • Datei übertragen;

  • Dateigröße ermitteln;

  • Datei umbenennen;

  • Datei löschen;

  • Ein beliebiges Kommando zum FTPServerschicken;

  • Übertragungsmodus zwischen ASCIIund Binär wechseln.

Beim Anmelden beim Server wird eine Verbindung für den Kommandoaustausch zwischen Client und Server geöffnet. Für das Übertragen der Dateien und auch von Dateilisten (zum Beispiel das List-Kommando) wird eine zusätzliche Socket-Verbindung benötigt, welche die Methode OpenDataSocket() erzeugt. Der Server gibt dabei einen anderen Port vor, den der Client dann benutzen muss.

Im Prinzip werden die Dateien genau so wie mit FileCopy übertragen.

Der C#-Code dazu ist in den Listings 1 bis 3 enthalten, in denen auch der Code für FileCopy zu finden ist. Die Methode ExcludeFileOrFolder() aus Listing 4, wird für beide Übertragungsarten benutzt.

 

Konfigurationsdateien

Die Konfiguration kann gespeichert und später wieder eingelesen werden. Zum Speichern wird eine XML-Datei verwendet, die mithilfe eines XmlTextWriter-Objekts erzeugt wird. Der Aufbau der XMLDatei ist sehr einfach gehalten, da sie nicht viele Daten aufnehmen muss. Das Einlesen der Daten erfolgt dann einfach durch ein XmlDocument, wobei die einzelnen Eigenschaften direkt durch

doc.SelectSingleNode("config/lasttransfer")
    .InnerText

gesetzt werden. Die Ausschlusskriterien werden in einer foreach-Schleife ermittelt und die entsprechenden Objekte in der Collection erzeugt:

foreach (XmlNode item in doc.SelectNodes
    ("config/exclude"))
{
    string type = item.Attributes["type"].Value;
    string name = item.InnerText;
    if (type == "file")
      lstExclude.Items.Add(new ExcludeFile
        (FileType.File, name));
    if (type == "folder")
      lstExclude.Items.Add(new ExcludeFile
        (FileType.Folder, name));
    if (type == "extension")
      lstExclude.Items.Add(new ExcludeFile
        (FileType.Extension, name));
    if (type == "startstring")
      lstExclude.Items.Add(new ExcludeFile
        (FileType.Startstring, name));
}

Das Konfigurations-File kann als Parameter übergeben werden. Dabei wird die Parameterliste in der Main-Methode des Hauptprogramms ausgewertet. Ist zumindest ein Parameter angegeben, wird der erste Parameter an den Konstruktor des Hauptfensters übergeben. Dieser lädt dann die Konfiguration:

static void Main(string[] args)
{
    string fn = "";
    if (args.Length > 0) fn = args[0];
    Application.Run(new frmMain(fn));
}
// end Main

public frmMain(string fn)
{
    InitializeComponent();
    configFileName = fn;
    LoadConfiguration();
    ...
} // end ctor

 

Fazit

Die vorliegende Anwendung FileTransfer erleichtert das Deployment von Websites deutlich. Durch die Verwendung von zwei Übertragungsmethoden - FileCopy und FTP - ist das Programm flexibel einsetzbar und lässt sich darüber hinaus an individuelle Wünsche anpassen, beispielsweise durch ein zusätzliches Ausschlusskriterium, das reguläre Ausdrücke verwendet.

 

Literatur

[1] Lars Günther, FTP-Client in Visual Basic 6, dotnetpro 12/2003|1/2004, Seite 73 ff.

 

Über den Autor

Andreas Müller ist Geschäftsführer der IkarusSoft GmbH, das kleinen und mittleren Unternehmen Hilfestellung rund um die EDV bietet. Sie erreichen ihn unter andreas.mueller@ikarussoft.de.