Archivformat (Zweiter Entwurf)
cms -db wird bald ein spezielles Archivformat zum Speichern von Sicherheitskopien und dem Standardinhalt verwenden.
DIES IST DER ZWEITE ENTWURF. (06.08.2009)
Allgemeines
Als Zeilenumbruch wird der Zeilenvorschub (LF) verwendet. Als Zeichensatz kommt bedingt durch dessen Verwendung im CMS UTF-8 zum Einsatz.
Aufbau des Archives
Kompression
Die Kompression des kompletten Archives mit gzip ist möglich.
Beginn
Das Archiv muss mit der folgenden Zeichenfolge beginnen, gefolgt von einem Zeilenumbruch:
== cms -db archive ==
Kommentar
Nach der Einleitung folgt ein beliebig langer Kommentar, mehrere Zeilen sind möglich. Der Kommentar wird durch einen Zeilenumbruch beendet.
Ordner
Nach dem Kommentar sind alle im Archiv enthaltenen Ordner aufgelistet. Die Auflistung beginnt mit folgender Zeichenkette, gefolgt von einem Zeilenumbruch:
=== directories ===
Danach werden die Ordner zeilenweise aufgelistet. Alle Ordnernamen müssen mit einem Schrägstrich (/) beginnen und enden. Dabei müssen sie in einer Reihenfolge aufgelistet sein, in der sie auch erstellt werden können. (Zum Beispiel kommt /ordner/unterordner/ nach /ordner/). Die Auflistung wird mit einem Zeilenumbruch beendet.
Der Wurzelordner / wird dabei nicht aufgelistet.
Dateien
Die Dateiauflistung wird mit einer Zeichenkette eingeleitet, gefolgt von einem Zeilenumbruch:
=== files ===
Pro Zeile steht eine Datei. Jede Zeile beginnt dabei mit dem Dateinamen, der mit einem Schrägstrich beginnen muss. Der Dateiname wird gefolgt von zwei Schrägstrichen (//). Danach steht der Inhalt der Datei, der base64-enkodiert sein muss. (RFC 2045 section 6.8)
Beispiel
Alle mit dem Zeichen ⏎ markierten Zeilenumbrüche dienen nur zur besseren Lesbarkeit.
== cms -db archive == Das ist ein Archiv === directories === /ordner/ /ordner/unterordner/ /ohne_inhalt/ === files === /text.txt//RGFzIGlzdCBlaW4gVGVzdC4= /ordner/datei.txt//RGllc2UgRGF0ZWkgYmVmaW5kZXQgc2ljaCBpbiBla⏎ W5lbSBPcmRuZXIu /ordner/unterordner/image.png//iVBORw0KGgoAAAANSUhEUgAAAAUAA⏎ AAFCAIAAAACDbGyAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcG⏎ AAAAAd0SU1FB9gBDxQLDz/ewvIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3a⏎ XRoIEdJTVBXgQ4XAAAAQklEQVQI12O8/OLn8oufr7z8pSPOFqnPyyzoWnDl5⏎ S8GBoZXX/+++vKXCcKBgCsvfzHpiLPB+TribEyR+rwQIYh+AHNxGa/EIPCNA⏎ AAAAElFTkSuQmCC
Referenzimplementierung
<?php class Archive { protected $comment; protected $dirs = array(); protected $files = array(); /* * Class constructor * Parameters: * - $comment: optional comment for the archive */ function __construct( $comment = '' ) { $this->comment = $comment; } /* * Changes the comment of the archive * Parameters: * - $comment: the new comment */ public function setComment( $comment ) { $this->comment = $comment; } /* * Adds an empty folder to the archive * Parameters: * - $dest: path of the folder in the archive */ public function addEmptyFolder( $dest ) { if (substr($dest, 0, 1) == '/' && substr($dest, -1, 1) == '/' && $dest != '/') { $this->dirs[] = $dest; } else { die('Can\'t add folder.'); } } /* * Adds a folder * Parameters: * - $src: path to the folder to add * - $dest: path of the folder in the archive */ public function addFolder( $src, $dest ) { if (substr($dest, 0, 1) == '/' && substr($dest, -1, 1) == '/' && is_dir($src)) { if ($dest != '/') { $this->dirs[] = $dest; } $handle = opendir($src); while (false !== ($file = readdir($handle))) { if ($file != '.' && $file != '..') { if (is_file($src.'/'.$file)) { $this->files[] = array($src.'/'.$file, $dest.$file); } else { $this->addFolder($src.'/'.$file, $dest.$file.'/'); } } } closedir($handle); } else { die('Can\'t add folder.'); } } /* * Generates the archive * Parameters: * - $filename: filename for the archive * - $compression: whether to compress the archive (default on) */ public function getArchive( $filename, $compression = true ) { if ($compression && function_exists('gzopen')) { $fn_open = 'gzopen'; $fn_write = 'gzwrite'; $fn_gets = 'gzgets'; $fn_close = 'gzclose'; } else { $fn_open = 'fopen'; $fn_write = 'fwrite'; $fn_gets = 'fgets'; $fn_close = 'fclose'; } $n = "\n"; $handle = call_user_func_array($fn_open, array($filename, 'wb')); call_user_func_array($fn_write, array($handle, '== cms -db archive =='.$n)); call_user_func_array($fn_write, array($handle, $this->comment.$n)); call_user_func_array($fn_write, array($handle, '=== directories ==='.$n)); natcasesort($this->dirs); foreach($this->dirs as $dir) { call_user_func_array($fn_write, array($handle, $dir.$n)); } call_user_func_array($fn_write, array($handle, '=== files ==='.$n)); foreach($this->files as $file) { $content = base64_encode(file_get_contents($file[0])); call_user_func_array($fn_write, array($handle, $file[1].'//'.$content.$n)); } call_user_func_array($fn_close, array($handle)); } } class unArchive { public $comment = ''; protected $dirs; protected $files; protected $handle; protected $fn_open; protected $fn_write; protected $fn_gets; protected $fn_close; /* * Class constructor (reads the archive header) * Parameters: * - $archive: path to the archive */ function __construct( $archive ) { if (file_exists($archive)) { $n = "\n"; if (file_get_contents($archive, NULL, NULL, 0, 21) == '== cms -db archive ==') { $this->fn_open = 'fopen'; $this->fn_write = 'fwrite'; $this->fn_gets = 'fgets'; $this->fn_close = 'fclose'; } else { $this->fn_open = 'gzopen'; $this->fn_write = 'gzwrite'; $this->fn_gets = 'gzgets'; $this->fn_close = 'gzclose'; } $this->handle = call_user_func_array($this->fn_open, array($archive, 'rb')); if (call_user_func_array($this->fn_gets, array($this->handle)) == '== cms -db archive =='.$n) { while ($line = call_user_func_array($this->fn_gets, array($this->handle))) { if ($line == '=== directories ==='.$n) break; $this->comment .= $line; } } else { die('Unsupported file.'); } } else { die('Can\'t open archive.'); } } /* * Extracts the archive * Parameters: * - $dest: folder where the archive is extracted to */ public function extract( $dest ) { $n = "\n"; if (is_dir($dest)) { while ($line = call_user_func_array($this->fn_gets, array($this->handle))) { if ($line == '=== files ==='.$n) break; $line = trim($line); if (!is_dir($dest.$line)) { mkdir($dest.$line); } } while ($line = call_user_func_array($this->fn_gets, array($this->handle))) { $line = trim($line); $line = explode('//', $line, 2); //$filename = $line[0]; //$content = $line[1]; if ($line[0] != '') { $line[1] = base64_decode($line[1]); file_put_contents($dest.$line[0], $line[1]); } } call_user_func_array($this->fn_close, array($this->handle)); } else { die('Not a directory.'); } } } ?>
Beispiele zur Verwendung
Wenn das CMS im Ordner ./cms installiert ist, lässt sich mit folgendem Code ein Backup erstellen:
<?php include('archive.php'); $a = new Archive('cms -db complete backup vX.X'); $a->addFolder('./cms/data', '/'); $a->getArchive('archive.cdba'); ?>
Mit dem folgendem Code lässt sich ein Archiv, das unter ./archive gespeichert ist, in den Ordner ./dest entpacken. Der Kommentar des Archives wird ausgegeben.
<?php include('archive.php'); $a = new unArchive('./archive.cdba'); echo $a->comment; $a->extract('./dest'); ?>