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.');
		}
	}
 
}
 
?>

Download der Referenzimplementierung inklusive Beispielen

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');
?>