Dieses Informationsfenster platziere ich freiwillig und unentgeldlich zur Unterstützung meiner Schwester bei ihrem Jobwechsel. Es wird nur für eine begrenzte Zeit angezeigt und kann ausgeblendet werden. Ich hoffe, dass die Aktion keinen meiner Leser zu sehr stört. Zum Ausblenden werden Cookies benutzt, diese müssen aktiviert und für diese Seite erlaubt werden. Werden die Cookies gelöscht oder die Seite auf einem anderen Gerät betrachtet, erscheint das Informationsfenster wieder und muss erneut geschlossen werden.

Information

Katja Schurig sucht neue berufliche Herausforderungen!

Liebe Leser, ich habe hier einen Geheimtipp für euch:
Katja Schurig
Katja Schurig, berufserfahren und kampferprobt in den Bereichen Veranstaltungsmanagement und -verkauf, Personalwesen, Sales & Marketing sowie Tourismus, will sich neuen beruflichen Herausforderungen stellen. Wer mit viel Energie umgehen kann und auf der Suche nach einer professionellen Fachkraft ist (oder jemanden kennt, der jemanden kennt), der findet ihr Profil nebst Kontaktdaten auf: Xing
Edit Comment
IT-Stuff, Tutorials, Software, Internet, Unterhaltung Willkommen auf IMA

FTP-Backup-Lösung mit PHP

Dieser Beitrag ist eine Ergänzung bzw. Erweiterung des vorherigen Posts “FTP Backup Skript in PHP”.

Folgende Änderungen werde ich hier besprechen:

  • Erweiterung des Skripts
  • Sicherheit: Absicherung mit .htpasswd
  • Sicherheit: Absicherung mit .htaccess
  • Automatisierung mit All-Inkl Cronjobs

Erweiterung des Backup Skripts

Das neue Skript bietet nun neue Funktionalitäten:

  • Farbliche Hervorhebung
  • Verbesserungen des Backup Prozesses, zusätzliche Überprüfungen und Debug Infos bei Fehlern
  • Verbinden eines externen FTP Server und Kopieren aller neuen Backups

Nun werden also von beliebig vielen Ordner des FTP-Root Backups erstellt, gegebenenfalls aufgeräumt wenn mehr Backups existieren als aufgehoben werden sollen und anschließend alle neuen Backups auf einen externen FTP Server kopiert. Für den Upload wird der passive FTP Modus verwendet, da dieser in den seltensten Fällen Probleme macht. Sollte der Wechsel zum passiven FTP fehlschlagen, wird dennoch aktives FTP probiert.
Alle nötigen Informationen werden in den Variablen in Zeile 16 bis 25 angegeben. Format und Hilfe steht jeweils dabei, eigentlich sollte da alles klar sein.

Die Erweiterungen machen aus dem Skript eine beispielhafte Backup-Lösung für FTP Inhalte. Auch hier am Beispiel von All-Inkl als Hoster. Wer die Lösung unabhängig von All-Inkl einsetzen möchte, wird das Archive_Tar PHP Modul und irgendeine Art von Cronjob-Funktionalität brauchen.

Screenshot

php-ftp-backup-neu

Code

Update 28.05.: verbesserte FTP-URI-Verarbeitung, Portangabe möglich, Verbindung über FTPs (FTP über SSL über Port 21) möglich (mit Fallback zu unsicherem FTP wenn FTPs nicht funktioniert), erweiterte Informationen über die erfolgten Backups in der Benachrichtigungsmail, kleine Optimierungen
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;
$newbackups = array();
$backupinfo = array();
echo "<style>.ok{color:#478F47;}.err{color:#DA3833;}.grey{color:grey;}.warn{color:#f0ad4e;}th,td{border-bottom: 1px solid #aaa;}</style>";

// ########## EDIT THIS VARIABLES ###################
$foldertobackup = array("tools", "reports"); // which root folders should get backed up?
$copytoexternalftp = 1; // copy new backup files to external ftp server? should be 1/"yes"/"ja" or 0/"no"/"nein"
// external (ftp) servers to copy new backups to, format:
// in general: ftp://username:password@url:port/path (port is required!)
// ftp://user:pw@ftp.server.com:21/
// ftp://user:pw@serverurl.com:21/optional/path
// secure sftp connection (port 22) in preparation but not ready yet
$externalftpuri = "ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups";
$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 = "adminmail@barketing.de"; // valid mail address to send the mail to
// ##################################################

echo "<span class='ok'>".date("d.m.Y G:i:s")."</span><br>";

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

   // check if folder exists
   if(!file_exists($pfad.$verzeichnis)) {
      echo "<span class='err'>Sicherung fehlgeschlagen. Zu sichernder Ordner $pfad$verzeichnis existiert nicht.</span>";
      continue;
   }

   // 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;
   $archivsize = round(filesize($dir.$archivname)/1000000);
   if (!is_numeric($archivsize)) {
      $archivsize = "filesize error";
   }
   if (is_int($backuptime)) {
      echo "<span class='ok'>Backup fertig: $archivname (Größe: $archivsize MB, Dauer: $backuptime Sekunden)</span><br>";
   } else {
      echo "<span class='ok'>Backup fertig: $archivname</span><br>";
   }

   // check created archive
   if (!is_object($archiv)==1 || !is_numeric($archivsize) || !$archivsize>50) {
      // abort process due to wrong type or too small filesize (which likely is an error)
      echo "<span class='err'>Sicherung fehlgeschlagen. Erstelltes Archiv ist fehlerhaft.</span><br>Mehr Infos <a href='http://hannes-schurig.de/09/05/2015/ftp-backup-skript-in-php/' target='blank'>hier</a><br>";
      // debug
      echo "<p class='grey'>Debug:<br>";
      echo "Pfad: $dir.$archivname<br>";
      echo "Typ: ".gettype($archiv)."<br>";
      echo "Größe: $archivsize MB</p>";
      continue;
   }
   $newbackups[$archivname] = $dir.$archivname;

   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 $filepath => $value) {
      if($i>=$backupfilemaximum) {
         echo "$filepath wird gelöscht...<br>";
         if (unlink($filepath)) {
            echo "<span class='ok'>Datei erfolgreich gelöscht.</span><br>";
         } else {
            echo "<span class='err'>Fehler beim Löschen der Datei.</span><br>";
         }
      }
      $i++;
   }
   $jobendtime = time() - $jobtime;
   array_push($backupinfo, array($verzeichnis, $archivname, $archivsize." MB", $jobendtime." Sekunden", date("d.m.Y G:i:s")));
   echo "<span class='ok'>Backup für Verzeichnis $verzeichnis abgeschlossen.</span><br>";
   if (is_int($jobendtime)) {
      echo "######################################## (Dauer: $jobendtime Sekunden)<br>";
      $alltime += $jobendtime;
   } else {
      echo "########################################<br>";
   }
}

if(count($newbackups)>0) {
   echo "<br><span class='ok'>Die automatische Sicherung des Skripts '".pathinfo(__FILE__, PATHINFO_BASENAME)."' hat ".count($newbackups)." Verzeichnisse in insgesamt $alltime Sekunden gesichert.</span><br><br>";
} else {
   echo "<br><span class='err'>Es scheint leider so als wenn keine Backups erfolgreich erstellt wurden.</span><br><br>";
}
flush();

// ######### copy backups to external storages
if (!isset($copytoexternalftp) || $copytoexternalftp== 0 && in_array($copytoexternalftp, array("no", "nein"))) {
   echo "<span class='warn'>Backups werden nicht auf einen externen FTP kopiert. Option ist deaktiviert.</span><br>";
} else {
   $ftptime = time();
   echo "########################################<br>";
   echo "<strong>".count($newbackups)." Backups werden auf externen FTP kopiert...</strong><br>";
   flush();
   $ftpcon = getFtpConnectionByURI($externalftpuri);
   flush();
   if(gettype($ftpcon)=="resource") {
      foreach ($newbackups as $filename => $fullpath) {
         $uploadtime = time();
         echo "Kopiere .$filename. (Größe: $archivsize MB) auf den FTP...<br>";
         flush();
         if (ftp_put($ftpcon, $filename, $filename, FTP_ASCII)) {
            $uploadendtime = time() - $uploadtime;
            if (is_int($uploadendtime)) {
               echo "<span class='ok'>Backup erfolgreich kopiert. (Dauer: $uploadendtime Sekunden)</span><br>";
            } else {
               echo "<span class='ok'>Backup erfolgreich kopiert.</span><br>";
            }
         } else {
            echo "<span class='err'>Fehler beim Kopieren des Backups.</span><br>";
         }
         flush();
      }
   }
   $ftpendtime = time() - $ftptime;
   if (is_int($ftptime)) {
      echo "######################################## (Dauer: $ftpendtime Sekunden)<br>";
   } else {
      echo "########################################<br>";
   }
}
flush();

// ######### send mail
if (!isset($sendmail) || $sendmail== 0 && in_array($sendmail, array("no", "nein"))) {
   echo "<br><span class='warn'>Benachrichtigungsmail wurde nicht verschickt. Option ist deaktiviert.</span><br>";
} else {
   if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $sendmailto)) {
      echo "<br><span class='err'>FEHLER: Mail konnte nicht versendet werden, da die Adresse ungültig ist!</span><br>";
   } else {
      $mailsubject = "Automatische FTP Sicherung abgeschlossen";
      $mailtext = "Die automatische Sicherung des FTP-PHP-Backup-Skripts ".pathinfo(__FILE__, PATHINFO_BASENAME)." hat ".count($foldertobackup)." Verzeichnisse in insgesamt $alltime Sekunden gesichert.<br><br>";
      
      // add backup informations as table to mailtext
      $mailtext .= "<table border=0 style='border-spacing:0;'><thead><tr><th>Verzeichnis</th><th>Dateiname</th><th>Größe</th><th>Dauer</th><th>Timestamp</th></tr></thead><tbody>";
      for($i=0;$i<count($backupinfo);$i++) {
         $mailtext .= "<tr>";
         for($j=0;$j<count($backupinfo[$i]);$j++) {
            $mailtext .= "<td>" . $backupinfo[$i][$j] . "</td>";
         } 
         $mailtext .="</tr>";
      }
      $mailtext .= "</tbody></table>";
      mail(
         $sendmailto,
         $mailsubject,
         $mailtext,
         "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
      ) or die("<br><span class='err'>FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden.</span><br>");
      echo "<br><span class='ok'>Benachrichtigungsmail wurde erfolgreich verschickt!</span><br>";
   }
}
flush();

// function to get ftp connection object from URI
// basics were from: http://php.net/manual/de/function.ftp-connect.php#89811
function getFtpConnectionByURI($uri)
{
   // Split FTP URI into:
   // $match[0] = ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups
   // $match[1] = ftp
   // $match[2] = admin
   // $match[3] = password
   // $match[4] = barketing.dns.com
   // $match[5] = 21
   // $match[6] = /Data/FTP-Backups
   preg_match("/([a-z]*?):\/\/(.*?):(.*?)@(.*?):(.*?)(\/.*)/i", $uri, $match);
   
   $ftpcon = null;

   // check if port is set and uri is formatted correctly
   if(is_int(intval($match[5]))) {
      $port = intval($match[5]);

      // check if ftp(s) or sftp is chosen
      if($match[1]=="ftp") {
         // set up and ftp(s) connection, login
         echo "Stelle (FTPs über SSL - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
         // try ftps over ssl, usally through  port 21
         $ftpcon = ftp_ssl_connect($match[4], $port, 30);
         if (!gettype($ftpcon)=="resource") {
            echo "<span class='warn'>FTP über SSL - Port $port - Verbindung fehlgeschlagen!</span><br>";
            echo "Stelle (unsicheres FTP - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
            // try normal insecure ftp
            $ftpcon = ftp_connect($match[4], $port, 30);
         }
         if (gettype($ftpcon)=="resource") {
            $login = ftp_login($ftpcon, $match[2], $match[3]);
            $pasv = ftp_pasv($ftpcon, true);
         }
      } else if($match[1]=="sftp") {
         echo "<span class='err'>SFTP Unterstützung noch nicht implementiert.</span><br>";
         // if(!$port==22) {
         //    echo "<span class='warn'>SFTP Übertragung aber Port ist nicht 22. Ist der gewählte Port $port korrekt?</span><br>";
         // }
         // echo "Stelle (sichere sFTP - Port $port) Verbindung zu FTP Server $match[4]$match[5]:$match[6] her...<br>";
         // $ftpcon = ssh2_connect($match[4], $port, 30);
         // ssh2_auth_password($ftpcon, $match[2], $match[3]);
         // $sftp = ssh2_sftp($ftpcon);
      } else {
         echo "<span class='err'>Kein gültiger Verbindungstyp (ftp/sftp) angegeben.</span><br>";
      }
   } else {
      echo "<span class='err'>Der Port ist fehlerhaft angegeben. Bitte URI prüfen.</span><br>";
   }
   
   if ($ftpcon && gettype($ftpcon)=="resource")
   {
      echo "<span class='ok'>Verbindung hergestellt</span>";
      if ($login) {
         echo "<span class='ok'>, Login erfolgreich</span>";
         if ($pasv) {
            echo "<span class='ok'>, passiver Modus aktiviert</span>";
            if(!isset($match[6]) || $match[6] == "") {
               echo ".<br>";
               return $ftpcon;
            } else if (ftp_chdir($ftpcon, $match[6])) {
               echo "<span class='ok'>, Verzeichniswechsel zu $match[6] erfolgreich.</span><br>";
               return $ftpcon;
            } else {
               echo "<span class='err'>, Verzeichniswechsel zu $match[6] fehlerhaft.</span><br>";
               return null;
            }
         } else {
            echo "<span class='err'>, passiver Modus konnte nicht aktiviert werden. Upload wird trotzdem probiert.</span><br>";
            return $ftpcon;
         }
      } else {
         echo "<span class='err'>, Login fehlgeschlagen.</span><br>";
         return null;
      }
   }
   echo "<span class='err'>Fehler beim Verbinden mit dem FTP Server $match[4]$match[5]$match[6].</span><br>";
   echo "<p class='grey'>Debug:<br>";
   echo "URI (komplett): $match[0]<br>";
   echo "Typ: $match[0]<br>";
   echo "URI ohne Typ: $match[4]<br>";
   echo "Username: $match[2]<br>";
   echo "Passwort: $match[3]<br>";
   echo "Port: $match[5]<br>";
   echo "Unterordner: $match[6]<br>";
   echo "</p>";
   // Or retun null
   return null;
}
// from: http://stackoverflow.com/a/10473026/516047
function startsWith($haystack, $needle) {
   // search backwards starting from haystack length characters from the end
   return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}

?>

Sicherheit: Absicherung mit .htpasswd

Das Verzeichnis, in dem die backup.php und die Backups liegen, sollte natürlich mit einer .htpasswd abgesichert werden. Mit einer eingerichteten .htpasswd Datei ist zuerst ein Login nötig, eh man auf bestimmte Bereiche des Webspaces zugreifen darf:
Das Bild zeigt eine Login Datenabfrage beim Aufruf der Backup URL
Die Datei .htpasswd enthält hierbei die Login Daten und in der .htaccess des Backup Unterordners wird festgelegt, dass eine .htpasswd diesen Ordner schützt. Die .htpasswd generiert ihr euch am besten mit diesem Generator und baut sie dann folgendermaßen in die .htaccess dieses Ordners ein:

AuthType Basic
AuthName "Backups"
AuthUserFile /www/htdocs/all-inkl-account/backup/.htpasswd
Require valid-user

Sicherheit: Absicherung mit .htaccess

Da wir schonmal bei .htaccess sind, erhöhen wir die Sicherheit mit ein paar weiteren grundlegenden Zeilen:

#block access to certain file types
<FilesMatch ".(htaccess|htpasswd|ini|phps|log|sh|tar.gz)$">
 Order Allow,Deny
 Deny from all
</FilesMatch>

# disable directory browsing
Options All -Indexes

# prevent basic url hacking stuff
# from: http://www.queness.com/post/5421/17-useful-htaccess-tricks-and-tips
RewriteEngine On
# proc/self/environ? no way!
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
# Block out any script trying to set a mosConfig value through the URL
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|\%3D) [OR]
# Block out any script trying to base64_encode crap to send via URL
RewriteCond %{QUERY_STRING} base64_encode.*(.*) [OR]
# Block out any script that includes a <script> tag in URL
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
# Block out any script trying to set a PHP GLOBALS variable via URL
RewriteCond %{QUERY_STRING} GLOBALS(=|[|\%[0-9A-Z]{0,2}) [OR]
# Block out any script trying to modify a _REQUEST variable via URL
RewriteCond %{QUERY_STRING} _REQUEST(=|[|\%[0-9A-Z]{0,2})
# Send all blocked request to homepage with 403 Forbidden error!
RewriteRule ^(.*)$ /index.htm [F,L]

ErrorDocument 401 /backup/index.htm
ErrorDocument 403 /backup/index.htm
ErrorDocument 404 /backup/index.htm
ErrorDocument 500 /backup/index.htm

Dadurch werden Zugriffe auf bestimmte Dateitypen (auch die Backup Dateien), Verzeichnisse und Zugriffe mit sicherheitskritischen Merkmalen unterbunden. Alle diese nicht validen Zugriffe bekommen die index.htm serviert, welches einfach nur eine leere HTML Datei ist. Somit wird den Abfragenden auch kein detaillierter Grund gegeben, warum der Zugriff fehlschlug.

Automatisierung mit All-Inkl Cronjobs

Zu guter Letzt hilft diese Sicherungslösung natürlich nur, wenn sie automatisiert wird. Auch dies ist stark abhängig von eurem Hoster, System, dem Anwendungsbereich usw.
Im Falle von All-Inkl als Webhoster, könnt ihr die Cronjob Funktionalität im KAS (KAS -> Tools -> Cronjobs) benutzen:
Das Bild zeigt die Cronjob Einrichtungsoberfläche von All-Inkl

All-Inkl bietet sogar das Zusenden aller Skriptausgaben. Ihr erhaltet also zusätzlich zu dem kurzen E-Mail-Bericht, der im Skript generiert werden kann, noch eine weitere Mail mit allen Ausgaben. Diese sind dann zwar nicht mehr farbig, aber was solls:
Das Bild zeigt die Cronjob E-Mail mit den Skriptausgaben

Danke an Kenny für das wertvolle Feedback im letzten Artikel, das dazu beigetragen hat, dass ich diese Erweiterung nochmal gepostet habe. Meinst du, dass das jetzt eine Basic Backup Lösung sein könnte? Das einzige, was noch fehlt, ist das Backup Management auf dem externen FTP Server, damit der nicht überläuft. Aber das ist nun wirklich Aufgabe der Admins zu entscheiden und zu verwalten, wie lange Backups aufgehoben werden sollen.


FTP Backup Skript in PHP

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 im nächsten Artikel noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen.

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 = "schurigh@gmail.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: 17.3.2015 – Version: 36.0.1 getestet und läuft
Beitrag

Flash

Stand: 17.3.2015 – Version: 17.0.0.134 getestet und läuft
Beitrag

Java

Stand: 17.3.2015 – Version: 8u40 getestet und läuft
Beitrag

Reader

Stand: 13.3.2015 – Version: 11.0.10 getestet und läuft
Beitrag, Update-Kurzfassund 1 und 2

Skype

Stand: 13.3.2015 – Version: 7.1.32.105 getestet und läuft
Beitrag, Update-Kurzfassung


Amazon verschenkt 37 Apps bis zum 14.02.

Kurz vorm Valentinstag verteilt Amazon mal wieder Apps for free. In der aktuellen Aktion verschenkt Amazon 37 Apps im Wert von über 100€, bis 14.02. zumindest.
Die Apps können wie immer jetzt einfach gekauft und sofort oder erst später über den Amazon App-Store installiert werden.

Ein paar Apps sind sicher einen Blick wert: Shazam Encore (Premium), OfficeSuite Professional 8 (Alternative zum kostenlosen Polaris Office), Runtastic PRO GPS Running (Lauf-Unterapp der Runtastic App-Gruppe), Sworkit Pro (Freeletics Alternative?), Sleep as Android Unlock, Cut the Rope: Experiments (2. Teil des Kultspiels) und League of Heroes Premium.

Und hier sind sie:
Liste aller kostenlosen Amazon Apps inklusive Icon


Die Aufgabe ist eigentlich trivial: in GMail ein “Senden als”-Konto einzurichten ist kinderleicht, eigentlich. Man benötigt den SMTP Server des Mailservers, inklusive Zugangsdaten. Ist bei 98% der Mailserver und Mailanbieter auch kein Problem, nur die Office365 Kunden müssen aufpassen. Denn Office365 in GMail als “Senden als” Konto einzurichten erfordert jetzt etwas Recherche.

Der Fehler: smtp.office365.com

Wer mal googelt oder einfach öfter mit Office365 arbeitet, kennt einen SMTP Server: smtp.office365.com
Diesen findet man auch auf so ziemlich jeder Internetseite, egal ob von Microsoft oder anderen Seiten: 0 (Office365 Account-interne Hilfe), 1, 2, 3, 4, 5, usw…

Wer es damit versucht, bekommt vielleicht das hier zu sehen:
Das Bild zeigt den GMail Dialog, in dem das Office365 Konto als Senden Als Account hinzugefügt werden soll. Allerdings erscheint der Fehler "Server response: read error: generic::deadline_exceeded: read timeout code(0)"

“Authentication failed. Please check your username/password.
[Server response: read error: generic::deadline_exceeded: read timeout code(0) ]”

Die Nutzerdaten sind natürlich korrekt, der Fehler steckt woanders. Weder ein Mailverkehr mit dem Domainhoster noch ein Telefonat mit Microsoft (und das ist eigentlich das Traurige daran) brachte Erleuchtung.

Die Lösung: Lokalisierte SMTP-Server

Einige Google Anfragen später wurde ich endlich HIER fündig: Microsoft nutzt für sein Office365 zwar den Ober-Alias smtp.office365.com, dahinter stecken aber lokalisierte Kontinent-SMTP-Server mit eigenen Namen. Und aus irgendeinem Grund – ich weiß an der Stelle auch noch nicht ob das ein Problem von GMail, Office365 oder einer Kombination aus GMail-Domainhoster-Office ist – funktioniert das Einrichten in GMail nicht (mehr?) mit diesem SMTP Alias.
Stattdessen brauch man die SMTP Servernamen des passenden lokalisierten Servers, den bekommt ihr so:

Das Bild zeigt die Recherche des lokalisierten SMTP-Servers mit nslookup in der Windows CMD
outlook-emeaeast.office365.com für Deutschland beispielsweise.

Dass der Hinweis nicht mal von einem Microsoft Supportmitarbeiter kommt, wenn man von Fehlern beim Verbinden mit smtp.office365.com erzählt und sogar den Fehlerscreenshot von oben zeigt, ist beunruhigend.


Ihr seid zu Hause, sitzt am PC und habt eigentlich keine Lust ständig auf das Handy zu wechseln und dort zu tippen? Besser wäre es am PC auf Whatsapp und Telegram Nachrichten antworten zu können?
Was vorher nur mit Tools, Hacks und Root funktionierte, wird mit Whatsapp Web und Telegram Web nun super einfach.

Whatsapp Web

Seit Kurzem ist die bereits vor langem geleakte Web-Erweiterung unter web.whatsapp.com erreichbar. Auf der Seite ist ein QR-Code zu sehen, den ihr mit eurer Whatsapp App einscannen müsst. Dazu öffnet ihr Whatsapp in der Chatübersicht, öffnet das Menü und wählt dort “Whatsapp Web” und richtet dann eure Handykamera auf den QR Code.
Info: Der Menüpunkt “Whatsapp Web” ist erst ab der (Android) App-Version 2.11.505 verfügbar.

Download section
Play Store
Download der .apk direkt aus meinem Blog

Sobald euer Handy den QR Code erfasst, müsste der Browser auch schon die Webversion anzeigen, das hat bei mir nicht 1 Sekunden Wartezeit gedauert:
Das Bild zeigt den Whatsapp Web Messenger in seiner vollen Pracht

Das Bild zeigt den Whatsapp Messenger mit geöffneten Smilies

Dabei ist eigentlich alles wie gewohnt, alte Chatnachrichten, Bilder, Medien, Smilies, Einstellungen, alles geht völlig intuitiv und so wie man es kennt von der Bühne.

Nachteil: Der Whatsapp Web Messenger ist nur eine Art umgeleitetes Whatsapp von eurem Handy und keine eigenständige Applikation. Ihr müsst mit eurem Handy also im Internet bleiben, damit es funktioniert. Das hat der unten gezeigte Telegram Messenger anders gelöst.
Vorteil: Alle Nachrichten, die ihr am PC schreibt, sind auch direkt in eurem Handy, schließlich ist der Web Messenger nur eine Kopie am PC. Das heißt, dass ihr auch jegliche Online-Gespräche mit den üblichen Whatsapp Sicherungsmethoden sichern könnt.

Telegram Web

Telegram Web gibt es bereits schon länger aber die Gelegenheit passt ihn mal zum Vergleich zu zeigen.
Auf web.telegram.org gebt ihr eure Handynummer ein, erhaltet einen Auth-Code per Telegram Nachricht und erreicht danach die Weboberfläche. Dieser Messenger ist also keine Kopie eures Handys, er ist ein eigenständiger Telegram-Client eurer Handynummer. Ihr erhaltet in eurem Telegram auch eine Benachrichtung, sollte sich jemand mit eurer Telefonnummer im Web einloggen, selbst wenn ihr das selbst wart.

Das Bild zeigt den Telegram Messenger in seiner vollen Pracht

Das Bild zeigt die persönlichen Einstellungen des Telegram Web Messengers, hier kann viel konfiguriert werden Das Bild zeigt ein Telegram Userprofil mit Informationen und Einstellungen

Man merkt, dass der Telegram Web Messenger schon länger lebt, weil dort einiges mehr möglich ist. Es können viele Einstellungen gemacht werden, sogar seperate Einstellungen für jeden Kontakt, es können mehr Informationen eingesehen und Nachrichten selektiert werden, um sie dann weiterzuleiten oder zu löschen. Das wird bei Whatsapp Web aber sicher auch mit der Zeit noch kommen.

Auf jeden Fall ermöglichen beide Messenger das bequeme Chatten am PC und stören somit nicht mehr so sehr den Workflow der PC-Arbeit. Am besten für den Workflow bleibt aber natürlich das komplette Deaktivieren aller Messenger und Ablenkungen ;)


Diese Themen wurden kürzlich mehrfach an mich herangetragen:

    Banner: Whatsapp Icon mit einer Zange davor
  • Wie kann ich ein bestimmtes Gespräch sichern oder auf ein anderes Gerät schicken?
  • Wie kann ich alle meine Nachrichten sichern?
  • Wie kann ich den aktuellen Stand meiner Whatsapp Chats exportieren um später genau diesen Stand wiederherzustellen?
  • Wie kann ich meine Whatsapp Chats von einem Handy auf ein anderes (neues) Handy übertragen?

Root Zugriff wird nicht benötigt!

Für die folgenden Tipps braucht ihr kein Root Zugriff sondern nur die öffentlich von Whatsapp erstellten Dateien auf eurem internen oder externen Speicher.

Einzelne Gespräche als .txt Datei per Mail schicken

Für diesen äußerst simplen Fall hat Whatsapp bereits ein Funktion eingebaut. Wenn ihr in der Gesprächsübersicht länger auf ein Gespräch drückt, findet ihr im Menü “Chat per E-Mail senden”. Das könnt ihr entweder ohne Medien (nur .txt Datei per Mail), oder mit Medien (in der Mail werden Bilder und Videos angehangen) machen.
Hinweis: Ich habe die Erfahrung gemacht, dass, je nachdem ob man den Chat mit oder ohne Medien sichert, unterschiedliche Teile des Chats exportiert werden. Beispiel: Mit Medien wurden nur Chats ab September gesichert, ohne Medien waren tatsächlich alle Nachrichten ab April in dem .txt Export. Also exportiert mit dieser Funktion den Chat immer mit UND ohne Medien, sonst fehlen euch vielleicht Nachrichten.

Whatsapp Nachrichten-Datenbank sichern

Whatsapp speichert ein Mal täglich die komplette Nachrichtendatenbank in eine Backupdatei. Ihr könnt in Whatsapp auch manuell ein Backup starten. Dies geht während der Nachrichtenübersicht über das Menü (Android drei Punkte oben rechts) -> Einstellungen -> Chat-Einstellungen. Dort seht ihr unter “Backup Chat-Verlauf” den Zeitpunkt des letzten Backups.
Mit einem Tipp auf diese Zeile wird ein Backup gestartet.
whatsapp-chats-sichern-exportieren-am-pc-ansehen-whatsapp-backupwhatsapp-chats-sichern-exportieren-am-pc-ansehen-whatsapp-backup-done

Whatsapp Backup-Dateien finden und exportieren

whatsapp-chats-sichern-exportieren-am-pc-ansehen-database-folder-filesDie Whatsapp Chat-Backups findet ihr auf eurem Smartphone (Android) unter storage/sdcard0/Whatsapp/Databases/ (“sdcard” ist optional, je nachdem wo ihr Whatsapp installiert hat und es seine Daten ablegt).
Dort liegt, unter dem Namen “msgstore.db.crypt8″ (vor 2015 war es noch .crypt7 als Dateiendung), eure Chat-Datenbank, sowie die gesicherten Datenbanken der letzten 8 Tage, beispielsweise als “msgstore-2015-01-04.db.crypt8″. Diese Datenbank enthält alle eure Nachrichten und ist somit das Zentrum aller folgenden Aktionen. Ihr braucht sie also später noch.

whatsapp-chats-sichern-exportieren-am-pc-ansehen-database-folder-files-zip-exportSichert die Datei – vorsichtshalber am besten gleich alle Dateien, falls eine beschädigt sein sollte – an einen sicheren Ort, z.B. euren PC. Das könnt ihr mit Android Software (9 Freeware Tools zeige ich hier) oder über “Teilen” erledigen; per Mail, Bluetooth, Cloudspeicher, as usual.
Wenn das “Teilen” der Dateien nicht funktioniert (war bei mir so), packt am besten alle Dateien mit einem Dateimanager in ein .zip Archiv und probiert es damit, so es sollte klappen.

Whatsapp Chats auf ein anderes/neues Handy übertragen

Ein typischer Fall: neues Handy, Whatsapp wird installiert, alles weg. Hier die Lösung:
1.)
Installiert euch zuerst Whatsapp auf eurem neuen Telefon.
Nur zur Überprüfung: Schaut (ggf. mit einer Dateimanager App) nach, ob ihr den Ordner storage/sdcard0/Whatsapp/Databases/ (“sdcard” ist optional, je nachdem wo ihr Whatsapp installiert hat und es seine Daten ablegt) finden könnt. Dort liegt jetzt eventuell schon eine msgstore.db.crypt8 Datei.
2.)
Deinstalliert jetzt Whatsapp auf dem neuen Telefon wieder. Der Whatsapp Ordner mit dem Datenbankordner sollte erhalten bleiben. Löscht alle enthaltenen Dateien des Databases Ordners.
3.)
Ihr braucht nun die gesicherte Whatsapp Nachrichtendatenbank “msgstore.db.crypt8″ (siehe Punkt 1) eures alten Telefons und kopiert diese in den Ordner Databases/ des neuen Telefons. Wenn diese Datei dort bereits existiert, überschreibt diese.
Auf “Nummer Sicher” gehen: Sicherheitshalber könnt ihr auch 1 ältere Datei (also die aktuellste der “msgstore-2015-01-04.db.crypt8” formatierten Dateien), wenn ihr diese habt, mit hineinkopieren. Alle 7 Sicherungen einzufügen würde ich nicht empfehlen; dies macht den Wiederherstellungsprozess nur langsamer und erhöht aber die Chancen nicht.
4.)
Installiert nun Whatsapp wieder. Nach dem Zustimmen der AGB erscheint nun der Hinweis, dass ein Backup gefunden wurde. Dieses könnt ihr nun wiederherstellen. Der Vorgang kann bis zu 2 Minuten dauern, je nach Größe eurer Nachrichtendatenbank und Handygeschwindigkeit.
whatsapp-chats-sichern-exportieren-am-pc-ansehen-whatsapp-reinstall-backup-found-restorewhatsapp-chats-sichern-exportieren-am-pc-ansehen-whatsapp-reinstall-backup-found

Probleme bei der Wiederherstellung

Sollte es bei der Wiederherstellung der Nachrichten zu Problemen und Fehlern kommen, typischerweise mit “Entschuldigung, wir konnten leider keiner ihrer Backups wiederherstellen” kommentiert, solltet ihr diesen Absatz lesen.
Vor allem wenn der Zeitpunkt der Sicherung bereits Wochen oder Monate her ist, kann es Probleme geben.

Lösung 1: Whatsapp Version der Sicherung ist älter als aktell

Die Wiederherstellung der Sicherung einer älteren Whatsapp Version kann Probleme bereiten. Ich empfehle, Whatsapp in der Version, mit der die Sicherung erstellt wurde, zu installieren.

Beispiel:
Am 16.09.2014 habt ich meine Whatsapp Dateien (msgstore.db.crypt7) gesichert. Zu diesem Zeitpunkt war Whatsapp mit der Version 2.11.387 aktuell. Das erfahrt ihr beispielweise auf der apk4fun.com Seite.
Alte Whatsapp Versionen als .apk Download gibt es dort oder alternativ auch auf uptodown.com (hier aber ohne Datum).
Nach der Installation meckert die alte Whatsapp Version eventuell, dass das “Verfallsdatum” der App abgelaufen ist. Dazu müsst ihr einfach nur in euren Android Einstellungen das Datum auf ein früheres Datum ändern und schon solltet ihr zum Wiederherstellungsdialog gelangen.
Das Bild zeigt einen Whatsapp Dialog, der warnt, dass das "Ablaufdatum" dieser (veralteten) Whatsapp Version abgelaufen ist und diese deswegen nicht mehr benutzt werden kann. Das folgende Bild zeigt, wie sich dieser Dialog umgehen lässt. Das Bild zeigt die Android Einstellungen für "Datum & Zeit". "Autom. Datum/Uhrzeit" wurde deaktiviert und ein falsches (früheres) Datum wurde eingestellt, um die "out of date" Warnung von Whatsapp zu umgehen.

Lösung 2: Telefonnummer der Sicherung ist anders als aktuell

Außerdem soll es wohl Probleme geben, wenn man Whatsapp mit einer anderen Nummer verifiziert als der, die man zur Sicherung hatte. Also versucht das frisch installierte Whatsapp am besten mit der Telefonnummer, mit der die Sicherung gemacht wurde, zu verifizieren.
Am einfachsten ist es, wenn die SIM Karte mit dieser Telefonnummer im Handy steckt, das ist aber nicht zwingend nötig. Ihr könnt auch den Bestätigungscode auf ein anderes Handy mit dieser SIM/Nummer schicken lassen und damit das andere Handy ohne SIM verifizieren.

Die Kombination der beiden oberen Ansätze hat es mir nach vielen Fehlversuchen erlaubt, Nachrichten einer alten .crypt7 Datenbank von September 2014 erfolgreich wiederherzustellen.




Admin

Sie möchten hier werben oder Sponsor auf längere Zeit werden? Ich garantiere Ihnen unschlagbare Preise bei meinen Besucherzahlen. Egal ob Banner, Textlinks oder andere Werbeformen. Informieren Sie sich bei mir unter werbung@hannes-schurig.de