Achtung: Diese Backup Lösung habe ich im nächsten Artikel noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen und die darin enthaltenen Sicherheitshinweise zu beachten.

FTP Backups (bisher)

Mein Blog läuft auf dem Hoster ALL-INKL.com, den ich nebenbei gesagt nach mehreren Jahren immernoch empfehlen kann.
Bisher habe ich halbwegs (un)regelmäßig manuell, von Hand, FTP Backups erstellt. Also alle paar Monate habe ich über einen FTP Tool wie FileZilla bestimmte Ordner heruntergeladen, Datei für Datei, anschließend in ein Archiv gepackt und irgendwo verstaut. Dieser Prozess ist natürlich in vielerlei Hinsicht nicht zuverlässig, zeitintensiv und aufwändig.
Auch das Sichern der Ordner automatisiert mit Tools wie SyncBack (mein Artikel dazu) wird immer zeitintensiver, je größer die zu sichernde Datenmenge wird. Außerdem muss immer ein PC, auf dem die Tools laufen, an sein.

FTP Backups via PHP Skript

Sinnvoller ist es, diese Backups auf dem Server des Webhosters erstellen zu lassen. So wird der heimische PC nicht belastet.
All-Inkl bietet dafür ein recht einfaches PHP Skript zum Sichern eines einzelnen FTP Ordners oder des gesamten All-Inkl FTP Accounts.

Ich habe dieses Skript ausgebaut und um verschiedene Funktionen ergänzt:

  • beliebig viele Ordner des All-Inkl Accounts in einzelne .tar.gz Archive sichern
  • Einschränkung der Anzahl aufgehobener Backups – älteste Backups werden automatisch gelöscht
  • detaillierte Ausgabe inklusive benötigter Zeit
  • E-Mail Benachrichtigung

Screenshot

Das Bild zeigt die Ausgaben des Backup Skripts und die versendete E-Mail Benachrichtigung

Code

Achtung: Dieses Backup Skript habe ich in Version 1.1 noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen. Besser wäre sogar die Version 1.2, mit der sich zusätzlich MySQL Datenbanken mitsichern lassen.

Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

<?
  // PHP-Konfiguration optimieren
  // if no errors are shown, please check htaccess restrictions by "php_flag display_errors off"
  // in this or parent folders
  @error_reporting(E_ALL);
  @ini_set("max_execution_time", 300);
  @ini_set("memory_limit", "256M");
  header('Content-Type: text/html; charset=utf-8');
  include "Archive/Tar.php";
  $pfad = preg_replace('/(\/www\/htdocs\/\w+\/).*/', '$1', realpath(__FILE__));
  $alltime = 0;

  // ########## EDIT THIS VARIABLES ###################
  $foldertobackup = array("bonnie", "tests", "locationmap", "blog"); // which root folders should get backed up?
  $backupfilemaximum = 2; // how many archives should be stored?
  $dir = $pfad."backup/"; // in which subfolder is this backup php file? this would be: "root/backup/"
  $sendmail = 1; // send notification mail when all backups are done - should be 1/"yes"/"ja" or 0/"no"/"nein"
  $sendmailto = "admin@yourdomain.com"; // valid mail address to send the mail to
  // ##################################################

  foreach ($foldertobackup as $verzeichnis) {
    $jobtime = time();
    echo "<br><br>########################################<br>";
    echo "<strong>Verzeichnis ".$verzeichnis." wird gesichert...</strong><br>";
    flush();

    // Name: [verzeichnis]_[Datum]_[Uhrzeit].tar.gz
    $archivname = $verzeichnis.date('_Y-m-d_His').".tar.gz";
    // Name: [All-Inkl-Accountname]_[Datum]_[Uhrzeit].tar.gz
    //$archivname = preg_replace('/.+\/(.+)\/$/', '$1', $pfad).date('_Y-m-d_His').".tar.gz";

    // Auszuschließende Ressourcen
    $ignorieren = array("*.sql.gz", "*.tar.gz", "usage", "logs");

    // ######### create backup
    $archiv = new Archive_Tar($archivname, true);
    $archiv->setIgnoreList($ignorieren);
    $archiv->createModify($pfad.$verzeichnis, "", $pfad);
    $backuptime = time() - $jobtime;
    if (is_int($backuptime)) {
      echo "Backup fertig: ".$archivname." (Dauer: ".$backuptime." Sekunden)<br>";
    } else {
      echo "Backup fertig: ".$archivname."<br>";
    }

    echo "Aufräumen der Backups...<br>";
    flush();
    // integer starts at 0 before counting
    $i = 0;
    $backupfiles = array();
    // ######### collect valid backup files
    if ($handle = opendir($dir)) {
      while (($file = readdir($handle)) !== false) {
        if (  is_int(strpos($file, $verzeichnis)) == true &&
              preg_match('/\.tar.gz$/i', $file) &&
              !in_array($file, array('.', '..')) &&
              !is_dir($dir.$file)
        ) {
          $backupfiles[$dir.$file] = filectime($dir.$file);
        }
      }
    }
    echo count($backupfiles)." valide Backups dieses Ordners gefunden, ";
    echo $backupfilemaximum." Backups sollen behalten werden. ";
    $backupcountdif = count($backupfiles)-$backupfilemaximum;
    if ($backupcountdif<=0) {
      echo "Kein Backup wird gelöscht.<br>";
    } else if ($backupcountdif==1) {
      echo "1 Backup wird gelöscht:<br>";
    } else if ($backupcountdif>=2) {
      echo $backupcountdif." Backups werden gelöscht:<br>";
    }
    flush();

    // ######### sort and delete oldest backups
    // sort backup files by date
    arsort($backupfiles);
    // reset counter variable
    $i = 0;
    // delete oldest files
    foreach ($backupfiles as $key => $value) {
      if($i>=$backupfilemaximum) {
        echo $key." wird gelöscht...<br>";
        if (unlink($key)) {
          echo "Datei erfolgreich gelöscht.<br>";
        } else {
          echo "Fehler beim Löschen der Datei.<br>";
        }
      }
      $i++;
    }
    $jobendtime = time() - $jobtime;
    if (is_int($jobendtime)) {
      echo "######################################## (Dauer: ".$jobendtime." Sekunden)<br>";
      $alltime += $jobendtime;
    } else {
      echo "########################################<br>";
    }
  }

  echo "<br><br>Die automatische Sicherung des FTP-PHP-Backup-Skripts '".pathinfo(__FILE__, PATHINFO_BASENAME)."' hat ".count($foldertobackup)." Verzeichnisse in insgesamt ".$alltime." Sekunden gesichert.<br><br>";

  // ######### send mail
  if (!isset($sendmail) || $sendmail== 0 && in_array($sendmail, array("no", "nein"))) {
    echo "Benachrichtigungsmail wurde nicht verschickt.";
  } else {
    if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $sendmailto)) {
      echo "FEHLER: Mail konnte nicht versendet werden, da die Adresse ungültig ist!";
    } else {
      mail(
        $sendmailto,
        "Automatische FTP Sicherung abgeschlossen",
        "Die automatische Sicherung des FTP-PHP-Backup-Skripts ".pathinfo(__FILE__, PATHINFO_BASENAME)." hat ".count($foldertobackup)." Verzeichnisse in insgesamt ".$alltime." Sekunden gesichert.",
        "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
      ) or die("FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden");
      echo "Benachrichtigungsmail wurde erfolgreich verschickt!";
    }
  }
?>

Wichtige Anmerkungen

Bitte beachtet, dass einige Zeilen angepasst werden müssen und der Code nur bei dem Webhoster All-Inkl getestet wurde. Die Zeile

include "Archive/Tar.php"

, die ein externes Modul einbindet, könnte auf anderen Websern zu Problemen führen. Am besten testet ihr es einfach.
Thema Sicherheit: Wie Kenny in den Kommentaren korrekt angemerkt hat, müssen noch Anpassungen erfolgen, wenn das Skript als tatsächliche Backup-Lösung zum Einsatz kommen soll.

  • Die Sicherungen landen bei diesem Skript direkt in dem Skript-Ordner. Die Sicherung(en) sollte(n) nach der Sicherung an einen anderen Ort, beispielsweise auf einen anderen Server oder ein NAS, kopiert werden. Oder ihr automatisiert den Download der Sicherungen nach dem Backupprozess.
  • Sowohl das Skript als auch die Sicherungen können ohne weitere Maßnahmen direkt angesprochen werden. Ihr solltet natürlich den Backup Ordner beispielsweise durch eine htpasswd absichern und den Zugriff auf die Backups über die htaccess einschränken.

Ich werde dieses Online-Backup Thema in Zukunft vermutlich nochmal aufgreifen, erweitern und optimieren. Ich verlinke das dann hier.

Dieser Beitrag soll nur alle meine Batch-Softwaredeployments in einer Übersicht zusammenfassen:

Firefox

Stand: 19.09.2016 – Version: 48.0.2 getestet und läuft
Beitrag

Flash

Stand: 19.09.2016 – Version: 23.0 getestet und läuft
Beitrag

Java

Stand: 16.08.2016 – Version: 8u101 getestet und läuft
Beitrag

Neu: Reader DC

Stand: Feb 2018 – Version 2018.009 getestet und läuft
Neue Methode: super-simple Deployment-Guide
AIP Methode: Deployment-Guide

Reader (oldschool, Reader XI)

Stand: 28.07.2015 – Version: 11.0.12 getestet und läuft (deaktiviert)
Beitrag, Update-Kurzfassund 1 und 2

Skype

Stand: 17.03.2016 – Version: 7.21 getestet und läuft (deaktiviert)
Beitrag, Update-Kurzfassung

KeePass 2

Stand: 11.07.2016 – Version: 2.34 getestet und läuft
Beitrag

HipChat (inkl. Uninstaller)

Stand: 19.09.2016 – Version: 4.27.1.1658 getestet und läuft
Beitrag, Uninstaller

Google scannt die meisten Webseiten täglich mehrmals. Wenn dabei Malware gefunden wird, wird diese Webseite auf eine Blacklist gesetzt, auf die alle größeren Browser zugreifen. So kann es schnell passieren, dass die eiene Seite bzw. eine bestimmte Unterseite so aussieht:
website-von-google-malware-warnung-befreien-warnung

Wenn nicht die komplette Webseite sondern nur eine Unterseite betroffen ist, sieht die detaillierte Google Malware Warnung vielleicht so aus:
website-von-google-malware-warnung-befreien-detaillierte-warnung

[…] 131 Seiten der Webseite überprüft. Dabei haben wir auf 1 Seite(n) festgestellt, dass Malware […]

Natürlich hat das Bereinigen der Seite, Entfernen von Schadcode und Schließen von Sicherheitslücken die oberste Priorität. Sollte das aber länger dauern oder die Unterseite muss am besten sofort wieder ohne Malware-Warnung erreichbar sein, muss eine schnelle Alternative her.

In diesem Fall ist es eventuell möglich die Malware Meldung mit einem Trick zu umgehen, ohne dass der Nutzer das merkt und ohne ihn zu gefährden.

How-To

1) Erstellt eine Kopie der Unterseite, also eine neue Seite mit exakt den gleichen Inhalten. Diese Seite sieht nun genauso aus wie die Malware-betroffene Seite, ist Malware-frei (wenn tatsächlich nur die eine Unterseite und nicht alle Seiten von der Malware betroffen war), allerdings unter einer anderen URL erreichbar.

2) Richtet eine Weierleitung der URL der beschädigten Seite zur URL der neuen Seite ein. Nutzt dazu entweder die Möglichkeiten eures CMS, eures DNS/Domain-Anbieters oder benutzt .htaccess Weiterleitungen:

Redirect 301 /about /z-portable

Dies würde die Kontaktseite meines Blogs direkt auf die Z-Portable Seite umleiten, mit dem HTTP Statuscode 301.

3) Nun wird der Besucher bei Eingabe der üblichen Malware-URL auf die Malware-freie Seite geleitet. Der Nutzer merkt das nur noch an der falschen URL, das verwirrt natürlich. Also sollte man noch die URL anpassen, damit für den Nutzer nicht mehr ersichtlich ist, dass er auf der Kopie-Seite befindet.
Am einfachsten geht das mit der Browser history API, um genau zu sein

history.pushState

:

// URL Fake
if(window.location.pathname=="/neue-url/") history.pushState({id: 'SOME ID'}, '', '/alte-url');

Damit wird, wenn im Browser die URL domain.tld/neu-url aufgerufn wird, die URL in domain.tld/alte-url in die URL Zeile gepackt. Nun ist nur noch mit Tools ersichtlich, dass eine andere URL aufgerufen wurde.

Beispiel

Die Unterseite meines Blogs /about/, also die Über Mich Seite, wurde von Malware infiziert; jedoch nur diese Unterseite.
Ich erstelle eine Kopie dieser Seite (exemplarisch nehme ich jetzt die Z-Portable Seite als „Kopie“) und leite die About Seite per htaccess darauf um. Nach einem Klick auf „About“ im Menü muss der Inhalt der Z-Portable Seite erscheinen, jedoch muss in der URL /about/ stehen, nicht /z-portable/.
website-von-google-malware-warnung-befreien-trick
Sichtbar ist ein anderer Inhalt, die URL passt.

Anschließen kann man über die Google Webmaster Tools die Neubewertung der Seite initiieren und sollte spätestens 12 Stunden später eigentlich diese Meldung erhalten:

Congratulations! Google has received and processed your malware review request. We did not detect any malware on your site.

Hier noch ein Guide zum Thema Google Malware Warnung: Link.

Ein Leser berichtete mir von seinen Erfahrungen mit Kommentaren in Batch:
REM ist gut, :: ist BÖSE!

Seine Erklärung dazu:

Grund: die ‚::‘-Kommentare sind syntaktisch anscheinend nichts anderes als Sprungmarken (die eben nie angesprungen werden). Funktioniert ja auch immer schön, und man kann sogar (vermeintlich) diese Sprungmarken doppelt definieren, also mehrfach denselben „Kommentar“ schreiben.
Probleme macht das aber in for-Schleifen. Konkret schicke ich dir mal ein Beispiel, bei dem die Meldung „Das System kann das angegebene Laufwerk nicht finden.“ erscheint.
Das wird noch viel lustiger, wenn man ein längeres Skript in dieser for-Schleife hat. Ich habe schon drei verschiedene sinnlose Fehlermeldungen wegen dieser Sache erhalten. War nicht leicht herauszufinden, was die Ursache war. Umstellen auf REM hat alle Probleme auf einmal behoben.

Hier der Start-Code für den Test:

@echo off
for /f "tokens=*" %%a in (test.bat) do (
    ::test
    ::echo
    echo %%a
)

Diesen Code habe ich Stück für Stück verändert und mir die Resultate angesehen. Und diese waren, wie vom Leser berichtet, erstaunlich fehlerhaft:
batch-kommentare-rem-statt-doppelpunkt-test-cmd
Ausführungen:

  1. Start-Code wie oben mit ::test und ::echo
  2. nur ::echo
  3. nur ::test
  4. ohne ::test und ::echo
  5. ::test an verschiedenen Stellen zw. echo %%a und der schließenden Klammer
  6. ::test außerhalb der for Schleife
  7. ::test2 innerhalb der for Schleife
  8. ::test2 in ::test umbenannt
  9. ::test und eine leere Zeile davor
  10. ::echo hinzugefügt, ::test und ::echo innerhalb, Start-Code
  11. ::echo wieder entfernt, nur ::test

Also bei :: Kommentaren innerhalb der for Schleife kommt es unter Umständen zu Fehlern. Mal triggert ein :: Kommentar („test“ in diesem Fall) einen Fehler, mal nicht. Zukünftig werde ich alles mit REM auskommentieren.

In früheren Posts habe ich bereits typische Probleme mit Batch und Umlauten, sowieso Umlauten und dem Copyrightsymbol behandelt. Die Artikel beschreiben aber nur die Darstellung von Umlauten und dem Copyrightsymbol. Probleme können jedoch auch bei der Verarbeitung von Daten mit Umlauten auftreten, beispielsweise beim Kopieren von Dateien mit Umlauten im Dateinamen.

2 Beispielcodes, die nicht funktionieren:

:: 1
xcopy stundenpläne.xlsx /test/

:: 2
for /f "tokens=*" %%a in (files.txt) do (
  xcopy "%~dp0%%a" "%~dp0test\%%a*" /Y
)

batch-umlaute-im-dateinamen-falsche-methode

Thomas, ein Leser, war so freundlich und schickte mir einen Code, der mit Umlauten in Dateinamen problemlos klarkommt.
Dieser Code nutzt ebenfalls Codepage Tricks um mit Umlauten in Dateinamen umgehen zu können:

:: Wechsele die Codepage, sonst kommen wir nicht im Umlauten in Dateinamen klar! (erst alte codepage speichern)
for /f "tokens=2 delims=:." %%a in ('chcp') do set alteCP=%%a
chcp 65001 >NUL

:: Finde alle referenzierten Dateien und lege sie in den Zielordner
for /f "tokens=*" %%a in (files.txt) do (
  :: Jetzt kopieren. Der Asterisk am Ende vermeidet, dass xcopy eine manuelle Eingabe erfordert.
  xcopy /Y "%~dp0%%a" "%~dp0test\%%a*"
)

:: Aufräumen.
chcp %alteCP% >NUL

Das einzig spezielle in dem Code ist der Stern im xcopy Befehl. Dieser verhindert, dass eine Eingabe vom Nutzer gefordert wird, wenn der Zielordner noch nicht existiert.

server-backup-bannerIch poste mal eben ein kleines Update meines Server Backup Scripts. Nichts spannendes, aber der Start des Scripts ist suboptimal.
Dabei kann noch einiges schief gehen und ein Laufwerksbuchstaben zu verbinden ist für Scripts, die mit der Aufgabenplanung ausgeführt werden, besonders sinnlos.

Vorher:

if not exist Z: net use Z: \\server\*** /user:***

Ziel ist das Verbinden des Backup Shares, ein Schreibtest und Logging aller Fehlercodes.
Besser:

set nas=\\serverip\Backup

REM delete and reconnect backup share
net use %nas% /delete
echo %date% %time:~0,8% net use delete %errorlevel% >> %log%

net use %nas% /u:user password
echo %date% %time:~0,8% net use %errorlevel% >> %log%

REM verify share write access
md %nas%\writeaccesstest
set tmpel=%errorlevel%
echo %date% %time:~0,8% test %tmpel% >> %log%

if %tmpel%==0 goto backup
set err=backup destination write access error && goto err

:backup
rd %nas%\%backupdest%\writeaccesstest
...

Long time no see, let’s go again!!

Bei einem Nutzer der Firma kommt es zu Netzwerkproblemen am PC; die Netzwerkverbindung übermittelt irgendwann einfach keine Pakete mehr oder ist anderweitig gestört. Das Internet ist also tot und der Nutzer müsste jetzt selbstständig eine Diagnose des Netzwerkadapters durchführen o.Ä.
Besser ist ein Script, welches die Internetverbindung bei Doppelklick repariert.

Variante 1 – Sehr einfach

Hier ist ein relativ einfaches Script für die oben beschriebene Situation:

ipconfig /release
ipconfig /renew

Dieses Script kann der Nutzer selbstständig ausführen und innerhalb weniger Sekunden ist die Internetverbindung erneuert.

Es soll aber vorkommen, dass diese einfache Lösung das Problem nicht löst. Dann solltet ihr mit den folgenden Varianten fortfahren.

Variante 2 – Standard

Dieser Code enthält einige Reparaturen mehr:

@echo off & setlocal
ipconfig /release
ipconfig /renew
arp -d *
nbtstat -R
nbtstat -RR
ipconfig /flushdns
ipconfig /registerdns
endlocal

arp -d *“ löscht die gesamte lokale ARP Tabelle, wodurch diese daraufhin neu erstellt wird. „nbtstat -R“ löscht die NetBT Tabelle und der Parameter -RR startet eine Aktualisierung der neu erstellten Tabelle. Und die letzten 2 Befehle löschen den DNS Cache und erstellen ihn neu.
Hier werden also verschiedene andere Problemstellen angegangen und die Wahrscheinlichkeit, das Netzwerkproblem damit zu beheben, ist etwas höher.
Jedoch benötigen all diese neuen Befehle Administratorrechte. Hier müsst ihr das Script also als Administrator ausführen (lassen). Wie das geht erfahrt ihr am Ende des Posts.

Variante 3 – Sehr ausführlich

Mit diesen Befehlen gebt ihr den Netzwerkproblemen die volle Ladung Reset:

@echo off
ipconfig /release
ipconfig /renew
arp -d *
nbtstat -R
nbtstat -RR
:: adjust interface name here and remove "rem"
rem netsh interface set interface "LAN-Verbindung" DISABLED DISCONNECTED
:: service names should work in both german and english windows
net stop "dhcp" /y
net stop "dnscache" /y
net stop "netman" /y
net start "dhcp" 
net start "dnscache" 
net start "netman" 
:: adjust interface name here and remove "rem"
rem netsh interface set interface "LAN-Verbindung" ENABLED CONNECTED
ipconfig /flushdns
ipconfig /registerdns

Hier werden zusätzlich einige Dienste neu gestartet und der Adapter (wenn ihr den Adapternamen einfügt und die Kommentierung entfernt) zurückgesetzt.
Natürlich werden auch hierfür Administratorrechte benötigt. Mehr dazu im nächsten Abschnitt.

Script als Administrator ausführen

Natürlich kann man das Script einfach als Administrator ausführen, wenn man die Rechte oder die Login Daten hat. Aber wir gehen mal davon aus, dass ein eingeschränkter Nutzer völlig unabhängig von euch diese Reparaturen zu jeder Zeit ausführen können muss.
Dafür gibt es einen relativ simplen Workaround über die Aufgabensteuerung.

Erstellt auf dem PC des Nutzers eine neue einfache Aufgabe in der Aufgabenplanung, wählt einen beliebigen Trigger (wird eh wieder gelöscht), als Aktion wählt ihr „Programm starten“ und sucht das lokal kopierte Reset Script von oben. Öffnet dann die Eigenschaften der Aufgabe, löscht den Trigger und wählt im „Allgemein“ Tab einen lokalen Administratorbenutzer aus. Aktiviert „Unabhängig von der Benutzeranmeldung ausführen“ und „Mit höchsten Privilegien ausführen“ und speichert die Änderungen. In diesem Moment werdet ihr nach dem Passwort zu dem ausgewählten Nutzer gefragt, gebt das ein. Danach habt ihr eine Aufgabe erstellt, die auf Bedarf ausgeführt werden kann und mit erhöhten Adminrechten das Script ausführt.

Nun müsst ihr noch dem Nutzer die Möglichkeit geben, diese Aufgabe – am besten einfach per Doppelklick – auszuführen. Merkt euch dazu den Namen der gerade erstellten Aufgabe und erstellt auf dem Desktop des Nutzers, oder wo auch immer das Script zum Starten des Netzwerk-Resets liegen soll, ein neues Batch Script mit dieser einen Zeile:

schtasks /run /tn "reset-network"

In die Anführungszeichen kommt natürlich der Name eurer Aufgabe.

Nun kann der Nutzer per Doppelklick dieses Scripts die Netzwerkerneuerung starten, selbst wenn er keine Administratorrechte hat.

Natürlich sind diese Scripte nur Workarounds und beheben vermutlich nicht die Ursache der Netzwerkstörung, um die ihr euch natürlich kümmern solltet. Aber es verschafft euch mehr Zeit fürs Troubleshooting (viel Erfolg dabei!) weil der Nutzers damit erstmal sehr selbstständig, schnell und einfach das Problem beheben kann.

Ich habe die Einrichtung dieses Workrounds nochmal als Video festgehalten:

via, via, via