Photoshop
Cinema 4d
Fotografie
Weitere Grafiksoftware
HTML / CSS
JavaScript
Flash
PHP
Webserver
Sonstige
PHP & OOP - Datei-Verwaltung via FTP (PHP Tutorial)
Tutorial erstellt von Andreas W. in 5.x, letzte Änderung am 22.06.2010
In dem nachfolgenden Tutorial möchte ich euch zeigen, wie man eine Klasse programmiert und nutzt, die das File Transfere Protocol (FTP), das zur Verwaltung von Dateien auf einem Server genutzt wird, implementiert. Am Ende des Tutorials wird ein kleines, aber dennoch erhellendes Beispiel zu finden sein, das den Einsatz dieser Implementation in der Praxis demonstriert. Theorie und Geschichte sowie die gezeigten Anwendungen und Methoden werde ich dabei ausführlich erläutern.
Das File Transfere Protocol ist ein textbasiertes Protokoll, welches zur Verwaltung von Dateien verwendet werden kann. Dazu wird zwischen Server und Client eine Verbindung aufgebaut, über welche Dateien vom Server zum Client heruntergeladen oder vom Client zum Server hochgeladen werden können. Zudem ist es dem Client möglich die gängigen CRUD-Operationen, also das Anlegen (Create), Lesen (Read), Verändern (Update) und Löschen (Delete) von Ordnern vorzunehmen.
Die Verbindung, die dabei zwischen Client und Server zur Datenübertragung genutzt wird, verwendet dabei im Regelfall den Standard-Port 21. Zur Steuerung wird nicht nur eine seperate Verbindung aufgebaut, sondern auch ein anderer Port, im Regelfall Port 20, der auch Control Port genannt wird, verwendet. Dies kann sich jedoch von Server zu Server unterscheiden.
1985 wurde das File Transfere Protocol unter RFC 959 veröffentlicht. Da FTP, wie auch HTTP und SMTP, ein textbasiertes Protokoll ist, kann die Datei-verwaltung auch über einen telnet-Client vorgenommen werden. Dies ist jedoch nicht üblich. Die eigentliche Arbeit (die richtige Verwendung des Protokolls) übernimmt normalerweise ein FTP-Client, wie zum Beispiel Filezilla.
Da PHP zwar eigene ftp-Funktionen bereit stellt, diese jedoch oftmals aufgrund der vorliegenden PHP-Installation nicht verfügbar sind, werden wir uns das FTP-Protokoll etwas genauer ansehen und eine eigene FTP-Klasse auf Basis dieses Protokolls entwickeln. Dabei werden wir auch den generellen Umgang bzw. die Implementierung einer Protokoll basierten Klasse erlernen. In diesem Rahmen wird es auch einen Ausblick auf mögliche Abstraktionsebenen geben, die es beispielsweise ermöglichen eine eigene HTTP- oder SMTP-Klasse zu entwickeln, wie es auch bereits in den entsprechenden Tutorials PHP & OOP - Webdienste mit PHP nutzen und PHP & OOP - Mailversand via SMTP beschrieben wurde.
Eine Datei lässt sich in Header und Body teilen. Im Header findet man alle wichtigen Daten, wie zum Beispiel den Namen, die Größe und den Typ der zu übertragenden Datei. Im Body findet man den eigentlichen Inhalt der Datei. Anders, als jedoch bei Email müssen wir diese Struktur nicht erst erzeugen, sondern wir können einfach die durch ein anderes Programm geschaffene und uns gegebene Struktur verwenden.
Deutlich komplizierter, als bei dem Versenden einer Email gestaltet sich jedoch die Kommunikation mit dem Server. Um eine solche überhaupt zu ermöglichen muss natürlich erst eine Socket-Verbindung aufgebaut werden, was mit der Funktion fsockopen kein Problem darstellen sollte.
Die Kommunikation mit dem Server läuft nun über das Senden einfacher Standard-Befehle, die immer mit der Zeichenfolge \r\n (CRLF - Carriage Return Line Feed) abgeschlossen werden. Zur Anmeldung am Server werden die Befehle USER, gefolgt vom Namen des FTP-Benutzers, und PASS, gefolgt vom Passwort dieses Benutzers benötigt. Ist der Benutzer erst einmal am Server angemeldet, besteht nur die verbindung, die zur Steuerung benötigt wird. Nun muss eine zweite verbindung zur Übertragung der Daten etabliert werden. Dies über nimmt der Befehl PASV. Nachdem auch diese Verbindung eingerichtet wurde, kann man mit der eigentlichen Verwaltung der auf dem Server liegenden Dateien beginnen. Der Befehl LIST, gefolgt von einem relativen Pfad, hilft dabei sich einen Überblick über die Verzeichnis-Struktur auf dem Server zu verschaffen. Möchte man sich nun zunächst etwas orientieren, kann man mit Hilfe von PWD seinen aktuellen Standort ermitteln und mit CDUP in das übergeordnete und mit CWD gefolgt von einer relativen Pfadangabe in ein bestimmtes Verzeichnis wechseln. Dort angekommen kann man nun mit MKD, gefolgt von einem Namen, ein neues Verzeichnis erstellen und dieses mit RMD, gefolgt vom selben Namen, gleich wieder entfernen. Möchte man nun eine Datei auf den Server laden geschieht dies mit Hilfe des Befehls STOR, der diesmal von zwei Parametern und zwar dem Ursprungs- und dem Zielort gefolgt wird. Beim Herunterladen via RETR reicht jedoch die Angabe der gewünschten Datei. Möchte man die Datei jedoch vor dem herunterladen umbenennen, muss man sie zuerst mit RNFR Dateiname als umzubenennende Datei markieren und ihr dann mit RNTO den neuen Dateinamen zuweisen. Diese Trennung ermöglicht es im übrigen auch eine Datei von Punkt A nach Punkt B zu verschieben, indem man folgendes an den Server sendet.
Code:
RNFR /https/html/dir_1/file.txt \r\n
RNTO /https/html/dir_2/file.txt \r\n
Entscheidet man sich dann, dass man die Datei vielleicht einfach löschen sollte, ist dies ganz einfach mit DELE /https/html/dir_2/file.txt CRLF möglich. Da man nicht die ganze Zeit immer nur arbeiten kann, die Verbindung zum Server jedoch nicht durch ein Timeout verloren gehen soll, gibt es auch einen Befehl, der einfach nichts tut. Dieser Befehl heißt NOOP, wie No Operation. Sollte man sich Fragen, ob es auch einen anderen Port gibt, auf dem der Server über FTP erreichbar ist oder welche Operationen noch unterstützt werden, kann einem der befehl HELP diese Fragen beantworten. Da jede Verbindung auch mal beendet werden muss und wir nun auch mit den wichtigsten Befehlen durch sind, können wir unsere Verbindung auch einfach mit QUIT schließen.
Wie bei vielen Kommunikationsarten mit einem Server liefert dieser auch während der Verbindung Antworten, die der Client überprüfen muss, um auftretende Fehler frühzeitig zu erkennen. Alle Antworten vom Server setzen sich dabei aus einem festgelegten, dreistelligen Zahlencode und einer Server spezifischen Textnachricht zusammen. Eine verkürzte Zusammenstellung der verschiedenen Fehlercodes findet ihr nachfolgend.
1XX: Anforderung akzeptiert, weitere Aktionen nötig.
2XX: Anforderung erfolgreich ausgeführt.
3XX: Anforderung verstanden, weitere Informationen benötigt.
4XX: Temporärer Fehler, erneuter Versuch kann Fehler beheben.
5XX: Fataler Fehler, Anforderung kann nicht verarbeitet werden.
Die zweite Ziffer gibt weiteren Aufschluss.
x0x: Syntax Fehler
x1x: Antwortinformation (z.B. beim Senden des Befehls HELP)
x2x: Status der Verbindung
x3x: Kontroll und Daten Verbindung betreffend
x4x: Nicht benutzt
x5x: Aktionen des Datei-Systems betreffend
Diese Fehlercodes gilt es in unserer Klasse abzufragen und korrekt zu verarbeiten.
Da wir nun die wichtigsten Befehle und Fehlercodes, sowie den allgemeinen Aufbau einer FTP-Anfrage kennengelernt haben, könnten wir uns eigentlich dem Aufbau unserer FTP-Klasse widmen, bevor wir dies jedoch tun, werden wir noch ein kleines Skript schreiben, dass das eben gelernte vertiefen soll.
Eine Verbindung zu einem Server über einen bestimmten Port lässt sich in PHP relativ einfach mit den Socket-Funktionen (man denke nun an fsockopen) herstellen. Abgesehen davon, dass man das Ergebnis der Funktion auf Erfolg prüfen sollte, kann man nach der Verbindungsaufnahme direkt mit dem Senden von Befehlen anfangen.
Code:
<?php
// Create a connection to the ftp-server ...
$sock = fsockopen("ftp://www.example.de", 20, $errno, $errstr, 30);
// ... and check if the connection was established.
if (!$sock) {
// Otherwise show an error message.
die("$errstr ($errno)");
}
else {
// Send the username ...
fwrite($sock, "USER ftp_user\r\n");
// ... and password to the server ...
fwrite($sock, "PASS ftp_pass\r\n");
// ... and switch to type I.
fwrite($sock, "TYPE I\r\n");
// Now ask for help, ...
fwrite($sock, "HELP\r\n");
// ... wait a moment ...
fwrite($sock, "NOOP\r\n");
// ... and quit.
fwrite($sock, "QUIT\r\n");
}
?>
Soeben haben wir also eine Verbindung mit dem Server unter www.example.de aufgebaut, uns an diesem angemeldet, nach Hilfe gefragt und uns wieder verabschiedet. Ihr könnt ja mal die verwendeten Befehle durch andere ersetzen und ein paar Tests durchführen, wobei ihr dies besser mit einem Telnet-Client machen solltet, da dieser euch auch die Antworten des Servers ausgibt, was in unserer PHP-Version noch nicht der Fall ist.
Wir wollen nun einige Methoden implementieren, die uns diese Kommunikation mit dem Server erleichtern.
Wir benötigen also eine Möglichkeit eine Socket-Verbindung aufzubauen und innerhalb des Objekts zugänglich abzuspeichern. Diese Aufgabe können wir einfach dem Konstruktor unserer Klasse überlassen. Da dann der Aufruf in PHP nicht ganz den tatsächlichen Sitzungsverlauf wiederspiegeln würde, implementieren wir außerdem die Methoden connect(), die die Verbidung zum Server herstellt, und login(), die uns an diesem authentifiziert.
Bei der Implementierung dieser Methoden wird man recht schnell merken, dass es sich lohnt einige private Hilfsmethoden einzuführen, die zum Beispiel das Senden eines Befehls (sendCmd()), das Abfragen der Server-Antwort (getReply()) und das Überprüfen der Kontroll-Verbindung sowie der Statuscodes übernehmen (checkControl() und checkData()). Da es sonst schwierig ist die Kommunikation mit dem Server nachzuvollziehen und eventuelle Fehler bei der Übermittlung von Daten festzustellen, ist es außerdem sinnvoll Methoden zur Verwaltung von Logging bzw Debugging-Ausgaben einzuführen.
Basierend auf diesen paar Methoden ist die Implementierung der restlichen Methoden eigentlich ein Kinderspiel. Wir implementieren also für die wichtigsten Befehle jeweils eine Funktion. Ich denke die Erläuterungen im Quellcode reichen aus, um die einzelnen Schritte in der Entwicklung nachvollziehen zu können.
Code:
<?php
error_reporting(E_ALL);
/**
* @class FtpConnect
*
* The FtpConnect class enables you to log onto a ftp server
* to manage your files. It is not very easily operated, but that is not required,
* because this class should just be the base for a comfortable
* class that can be used to manage the filesystem of a webserver.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
class FtpConnect {
/**
* @private
* The host's name.
*/
private $host;
/**
* @private
* The port of the ftp-server.
*/
private $port;
/**
* @private
* The controll connection handle.
*/
private $control;
/**
* @private
* The data connection handle.
*/
private $data;
/**
* @private
* The last server response.
*/
private $response;
/**
* @private
* The logging status.
*/
private $logStats;
/**
* @private
* The logging data.
*/
private $log;
/**
* @public
*
* The constructor sets up the used connection.
*
* @param String $host The name of the host.
* @param Integer $port The used port.
* @param String $name The user's name.
* @param String $pwd The user's password.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function __construct($host='localhost', $port=21, $user, $pwd, $log=false) {
// Declare if the connection with the ftp-server should be logged or not.
$this->logStats = $log;
// Store the host name and password.
$this->host = $host;
$this->port = $port;
// Now establish a connection to the server ...
$this->control = @fsockopen($host, $port);
// ... and check if everything is alright.
if (!$this->checkControl()) {
throw new Exception("Connection failed.\n");
}
// Switch to non-blocking mode ...
@set_socket_blocking($this->control, true);
// ... and set the timeout of the control.
@stream_set_timeout($this->control, 0, 200000);
// Check if the connection was established, ...
if($this->control) {
// ... send username, ...
$this->sendCmd("USER " . $user);
if (!$this->checkControl()) {
throw new Exception("User not accepted: " . $user);
}
// ... and password to the server ...
$this->sendCmd("PASS " . $pwd);
if (!$this->checkControl()) {
throw new Exception("Wrong password.");
}
// ... and set the connection type.
$this->sendCmd("TYPE I");
if (!$this->checkControl()) {
throw new Exception("Cannot switch to TYPE I.");
}
}
}
/**
* @public
*
* Creates a connection to the ftp server.
*
* @return Returns the control handle.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function connect() {
// Establish a connection to the server ...
$this->control = @fsockopen($host, $port);
// ... and check if everything is alright.
if (!$this->checkControl()) {
throw new Exception("Connection failed.\n");
}
// Switch to non-blocking mode ...
@set_socket_blocking($this->control, true);
// ... and set the timeout of the control.
@stream_set_timeout($this->control, 0, 200000);
return $this->control;
}
/**
* @public
*
* Logs a user onto the server.
*
* @param String $user The user's name.
* @param String $pwd The user's password.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function login($user, $password) {
// Send username, ...
$this->sendCmd("USER " . $user);
if (!$this->checkControl()) {
throw new Exception("User not accepted: " . $user);
}
// ... and password to the server ...
$this->sendCmd("PASS " . $pwd);
if (!$this->checkControl()) {
throw new Exception("Wrong password.");
}
// ... and set the connection type.
$this->sendCmd("TYPE I");
if (!$this->checkControl()) {
throw new Exception("Cannot switch to TYPE I.");
}
}
/**
* @public
*
* Closes the current connection to the ftp-server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function quit()
{
if($this->control) {
$this->sendCmd("QUIT");
}
}
/**
* @private
*
* Returns the reply of the ftp-server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
* Version 0.2, 28.03.2008<br />
*/
private function getReply($i = 0) {
// Read the server response from the socket handle, ...
$message = fgets($this->control);
// ... check if there was a response, ...
if( $message == '' ) {
// ... check if max depth is reached ...
if( $i==30 ) {
return "";
}
else {
// ... and wait a moment.
sleep(1);
return $this->getReply($i+1);
}
}
/* The following code is an alternativ way to read
data from a socket stream.
$go = true;
$message = "";
do
{
$tmp = @fgets($this->control, 512);
if($tmp === false)
{
$go = false;
}
else
{
$message .= $tmp;
if( preg_match('/^([0-9]{3})(-(.*[\r\n]{1,2})+\\1)? [^\r\n]+[\r\n]{1,2}$/', $message) ) $go = false;
}
} while($go);*/
// Write server-response to connection-log.
$this->log($message);
return $message;
}
/**
* @private
*
* Checks if the response of a command is ok.
*
* @return Returns if a valid response was given or not.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function checkControl() {
// get the response from the server ...
$this->response = $this->getReply();
// ... and check if everything is alright.
return (empty($this->response) || preg_match('/^[5]/', $this->response)) ? false : true;
}
/**
* @private
*
* Checks if the response of the data-connection after a command is ok.
*
* @param String $code The response code of the server.
* @return Returns if a given code matches the received error code.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function checkData($code) {
// If code 230 was returned the user is logged in and there is nothing to do.
if($code == '230') {
while(true) {
if($this->checkControl() && preg_match('/^230 /',$this->response)) {
return true;
}
else {
return false;
}
}
}
// Check if an other code was received.
else {
if($this->checkControl()) {
$pat = '/^'. $code .'/';
if( preg_match($pat, $this->response)) {
return true;
}
}
return false;
}
}
/**
* @private
*
* Sends a ftp-command given by the user to the server.
*
* @param String $cmd The command which should be transmit to the server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function sendCmd($cmd) {
// Send the command to the server ...
fputs($this->control, "$cmd\r\n");
// ... and create a log entry.
$this->log("> $cmd");
}
/**
* @public
*
* Opens a connection in passive mode for file transferes.
*
* @return Returns the data connection handle.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function pasv() {
// Send the command for entering passive mode ...
$this->sendCmd("PASV");
// ... and check the controll connection.
if($this->checkControl()) {
// Then get the response for PASV.
$offset = strpos($this->response, "(");
$res = substr($this->response, ++$offset, strlen($this->response)-2);
$parts = split(",", trim($res));
// Get the host and port for passive connection ...
$host = "$parts[0].$parts[1].$parts[2].$parts[3]";
$port = ((int)$parts[4] << 8) + (int) $parts[5];
// ... and store the data-connection handle.
$this->data = @fsockopen($host, $port);
return $this->data;
}
}
/**
* @public
*
* Lists the given directory.
*
* @param String $path The path to the directory that should be listed.
* @return The directory listing.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function listPath($path = null) {
// Initialize the path listing ...
$list = "";
// ... and check if we are already running passive mode.
if($this->pasv())
{
if( empty($path) ) {
// Ask for a directory listing for the current path.
$this->sendCmd("LIST " . $this->pwd());
}
else {
// Ask for a directory listing for the specified path.
$this->sendCmd("LIST " . $path);
}
// Check for a response ...
if($this->checkControl()) {
// ... and add the response to the result list.
while(true) {
$line = fgets($this->data);
$list .= $line;
if($line == '') {
break;
}
}
}
}
return $list;
}
/**
* @public
*
* Returns a download handle.
*
* @param String $path The path to the file that should be downloaded.
* @return A download handle.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function retr($path) {
// Check if we are already running passive mode, ...
if($this->pasv()) {
// ... send the command, ...
$this->sendCmd("RETR " . $path);
// ... check for a result ...
if($this->checkControl()) {
// ... and return it.
return $this->data;
}
}
return false;
}
/**
* @public
*
* Allowes to upload a file from local to remote path.
*
* @param String $local The local path.
* @param String $remote The remote path.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function stor($local, $remote) {
// Check if we are already running passive mode, ...
if($this->pasv()) {
// ... send the command, ...
$this->sendCmd("STOR " . $remote);
// ... check for a result ...
if($this->checkControl()) {
// ... and write the data to the target.
$fp = fopen($local, "rb");
while(!feof($fp)) {
$line = fread($fp, 4096);
fwrite($this->data, $line);
}
}
}
}
/**
* @public
*
* Moves to the parent directory.
*
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function cdup() {
// Send the command ...
$this->sendCmd("CDUP");
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* Moves to the specified directory.
*
* @param String $path The path where to move.
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function cwd($path) {
// Send the command ...
$this->sendCmd("CWD " . $path);
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* Creates the specified directory.
*
* @param String $path The name of the directory to be created.
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function mkdir($path) {
// Send the command ...
$this->sendCmd("MKD " . $path);
// ... and return the result of the error-code check.
return $this->checkData("257");
}
/**
* @public
*
* Removes the specified directory.
*
* @param String $path The name of the directory to be removed.
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function rmdir($path) {
// Send the command ...
$this->sendCmd("RMD " . $path);
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* Returns the current working directory.
*
* @return The absolute path to the current directory.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function pwd() {
// Send the command, ...
$this->sendCmd("PWD");
// ... check for some status codes ...
if($this->checkData("257") || $this->checkData("226")) {
// ... and return the result.
$parts = split(" ",$this->response);
return preg_replace('/\"/',"",$parts[1]);
}
}
/**
* @public
*
* Marks the file to be renamed.
*
* @param String $file The file to be renamed.
* @return Returns if operation was successful.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function rnfr($file) {
// Send the command, ...
$this->sendCmd("RNFR $file");
// ... and validates the result.
if($this->checkControl()) {
return true;
}
else {
return false;
}
}
/**
* @public
*
* Renames a marked file.
*
* @param String $file The new name of the file.
* @return Returns if operation was successful.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function rnto($file) {
// Send the command, ...
$this->sendCmd("RNTO $file");
// ... and validates the result.
if($this->checkControl()) {
return true;
}
else {
return false;
}
}
/**
* @public
*
* Deletes the specified file.
*
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function delete($path) {
// Send the command, ...
$this->sendCmd("DELE " . $path);
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* No operation - just wait.
*
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function noop() {
// Send the command, ...
$this->sendCmd("NOOP");
// ... and return the result of the error-code check.
return $this->checkData("200");
}
/**
* @public
*
* Returns some useful information.
*
* @return The help message of the server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function help() {
// Initialize the help message, ...
$help = "";
// ... ask for some help ...
$this->sendCmd("HELP");
// ... and save the response of the server.
while(true) {
$line = fgets($this->control);
$help .= $line;
if($line == '')
break;
}
return $help;
}
/**
* @private
*
* Saves all request and responses to and from the server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function log($str) {
// Check if data should be logged ...
if($this->logStats) {
// ... and log them if needed.
$this->log .= "$str<br>";
}
}
/**
* @public
*
* Returns the currently logged data.
*
* @return The logging output.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function __toString() {
return $this->log;
}
}
?>
Auch wenn der Quellcode sich eigentlich von selbst erklärt, möchte ich noch einmal kurz auf die Methode getReply() eingehen.
Die Methode getReply() nimmt, wie der Name schon sagt, die Antwort des Servers entgegen. Da der Server nicht immer sofort antwortet, muss sichergestellt werden, dass die Antwort des Servers auf einen vorangegangenen Befehl auch wirklich abgefangen wird. Dies geschieht indem man immer und immer wieder eine Zeile aus dem Antwort-Buffer des Servers ließt. Da wir wissen, dass eine Antwort nie länger als eine Zeile ist und zumeist nur wenige Zeichen enthält, reicht es wenn wir immer nur 1024 Zeichen einlesen. Die eingelesenen Zeichen überprüfen wir auf ihre Syntax. Entspricht diese der einer Server-Antwort können wir die Schleife verlassen und die Antwort zurückgeben.
Abschließend wollen wir uns noch ansehen, wie man mit Hilfe dieser Klasse noch einen eigenen Download-Link anbieten kann. Im einfachsten Fall geht dies natürlich über einen Link auf die Datei, die zum Download angeboten werden soll. Handelt es sich dabei jedoch um eine Text-Datei, wie zum Beispiel eine *.html, *.php, *.css oder auch *.csv-Datei, so wird diese entweder ausgeführt, falls es sich um eine ausführbare Datei handelt, interpretiert, falls es sich zum Beispiel um eine *.html-Datei handelt, oder einfach angezeigt. Das ist ja aber im Falle eines Download-Links nicht erwünscht. Daher möchten wir den Browser dazu veranlassen, einen Download-Dialog anzuzeigen. Es ist hierfür nur nötig eine Verbindung zum Zielserver mit Hilfe unserer FTP-Klasse herzustellen und den entsprechenden MIME-Type zu setzen, wobei man hier, wie so oft, Rücksicht auf den Internet Explorer nehmen muss. Diese Aufgaben erledigt der nachfolgende Code-Schnipsel für uns.
getfile.php
Code:
<?php
try {
// Create a connection to the server ...
$ftp = new FtpConnect(
'ftp_host',
'ftp_port',
'ftp_user',
'ftp_pwd',
true
);
// ... and switch to the working directory.
$ftp->cwd("/downloads/");
// Now get the request parameter and call the ftp retrieve command.
$sock = $ftp->retr(trim($_REQUEST['file']));
// Check if a connection was established ...
if($sock) {
// ... and set the mime-type if everything is alright.
$mime_type = (strstr($HTTP_USER_AGENT,'IE') ||
strstr($HTTP_USER_AGENT,'OPERA'))
? 'application/octetstream'
: 'application/octet-stream';
// Finally send the headers and the file.
header('Content-Type: ' . $mime_type);
header('Expires: ' . date("Y/m/d H:i:s"));
// Fix problems with the IE.
if( strstr($HTTP_USER_AGENT, 'IE') ) {
header('Content-Disposition: inline; filename="' . $_REQUEST['file'] . '"');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
}
else {
header('Content-Disposition: attachment; filename="'. $_REQUEST['file'] . '"');
header('Pragma: no-cache');
}
fpassthru($sock);
exit();
}
else {
throw new Exception('File could not be retrieved.');
}
}
catch(Exception $e) {
echo $e->getMessage();
}
?>
Das sieht doch schon recht einfach aus. Es ist uns nun also möglich, unseren Benutzern durch Einbinden eines einfachen Links auf deine-domain.tld/getfile.php?dateiname.end die Datei dateiname.end zum Download anzubieten. Dabei kann die Datei auch auf einem ganz anderen Server liegen, als das Skript, dass den Download bereitstellt. Dazu müssen uns nur die entsprechenden Verbindungs-Daten vorliegen.
Fragen und Antworten findet ihr selbstverständlich bei uns im Forum. Ich möchte euch auch darum bitten, keine Fragen an mich persönlich zu senden aus dem einfachen Grund, dass andere Menschen dann nichts von den gegebenen Antworten haben. Zudem können euch im Forum auch viele andere Leute, die echt Ahnung haben, helfen. Natürlich freue ich mich über ein kurzes Feedback zu diesem Tutorial. Ich bedanke mich für eure Aufmerksamkeit und hoffe, dass euch mein kurzer Exkurs gefallen hat.
Liebe Grüße,
Andreas
http://www.avedo.net
Twittern
>> Allgemeine Fragen oder Probleme mit dem Tutorial? Hier gehts zum Forum!
Ein Überblick
Das File Transfere Protocol ist ein textbasiertes Protokoll, welches zur Verwaltung von Dateien verwendet werden kann. Dazu wird zwischen Server und Client eine Verbindung aufgebaut, über welche Dateien vom Server zum Client heruntergeladen oder vom Client zum Server hochgeladen werden können. Zudem ist es dem Client möglich die gängigen CRUD-Operationen, also das Anlegen (Create), Lesen (Read), Verändern (Update) und Löschen (Delete) von Ordnern vorzunehmen.
Die Verbindung, die dabei zwischen Client und Server zur Datenübertragung genutzt wird, verwendet dabei im Regelfall den Standard-Port 21. Zur Steuerung wird nicht nur eine seperate Verbindung aufgebaut, sondern auch ein anderer Port, im Regelfall Port 20, der auch Control Port genannt wird, verwendet. Dies kann sich jedoch von Server zu Server unterscheiden.
1985 wurde das File Transfere Protocol unter RFC 959 veröffentlicht. Da FTP, wie auch HTTP und SMTP, ein textbasiertes Protokoll ist, kann die Datei-verwaltung auch über einen telnet-Client vorgenommen werden. Dies ist jedoch nicht üblich. Die eigentliche Arbeit (die richtige Verwendung des Protokolls) übernimmt normalerweise ein FTP-Client, wie zum Beispiel Filezilla.
Da PHP zwar eigene ftp-Funktionen bereit stellt, diese jedoch oftmals aufgrund der vorliegenden PHP-Installation nicht verfügbar sind, werden wir uns das FTP-Protokoll etwas genauer ansehen und eine eigene FTP-Klasse auf Basis dieses Protokolls entwickeln. Dabei werden wir auch den generellen Umgang bzw. die Implementierung einer Protokoll basierten Klasse erlernen. In diesem Rahmen wird es auch einen Ausblick auf mögliche Abstraktionsebenen geben, die es beispielsweise ermöglichen eine eigene HTTP- oder SMTP-Klasse zu entwickeln, wie es auch bereits in den entsprechenden Tutorials PHP & OOP - Webdienste mit PHP nutzen und PHP & OOP - Mailversand via SMTP beschrieben wurde.
Das Protokoll im Detail
Eine Datei lässt sich in Header und Body teilen. Im Header findet man alle wichtigen Daten, wie zum Beispiel den Namen, die Größe und den Typ der zu übertragenden Datei. Im Body findet man den eigentlichen Inhalt der Datei. Anders, als jedoch bei Email müssen wir diese Struktur nicht erst erzeugen, sondern wir können einfach die durch ein anderes Programm geschaffene und uns gegebene Struktur verwenden.
Deutlich komplizierter, als bei dem Versenden einer Email gestaltet sich jedoch die Kommunikation mit dem Server. Um eine solche überhaupt zu ermöglichen muss natürlich erst eine Socket-Verbindung aufgebaut werden, was mit der Funktion fsockopen kein Problem darstellen sollte.
Die Kommunikation mit dem Server läuft nun über das Senden einfacher Standard-Befehle, die immer mit der Zeichenfolge \r\n (CRLF - Carriage Return Line Feed) abgeschlossen werden. Zur Anmeldung am Server werden die Befehle USER, gefolgt vom Namen des FTP-Benutzers, und PASS, gefolgt vom Passwort dieses Benutzers benötigt. Ist der Benutzer erst einmal am Server angemeldet, besteht nur die verbindung, die zur Steuerung benötigt wird. Nun muss eine zweite verbindung zur Übertragung der Daten etabliert werden. Dies über nimmt der Befehl PASV. Nachdem auch diese Verbindung eingerichtet wurde, kann man mit der eigentlichen Verwaltung der auf dem Server liegenden Dateien beginnen. Der Befehl LIST, gefolgt von einem relativen Pfad, hilft dabei sich einen Überblick über die Verzeichnis-Struktur auf dem Server zu verschaffen. Möchte man sich nun zunächst etwas orientieren, kann man mit Hilfe von PWD seinen aktuellen Standort ermitteln und mit CDUP in das übergeordnete und mit CWD gefolgt von einer relativen Pfadangabe in ein bestimmtes Verzeichnis wechseln. Dort angekommen kann man nun mit MKD, gefolgt von einem Namen, ein neues Verzeichnis erstellen und dieses mit RMD, gefolgt vom selben Namen, gleich wieder entfernen. Möchte man nun eine Datei auf den Server laden geschieht dies mit Hilfe des Befehls STOR, der diesmal von zwei Parametern und zwar dem Ursprungs- und dem Zielort gefolgt wird. Beim Herunterladen via RETR reicht jedoch die Angabe der gewünschten Datei. Möchte man die Datei jedoch vor dem herunterladen umbenennen, muss man sie zuerst mit RNFR Dateiname als umzubenennende Datei markieren und ihr dann mit RNTO den neuen Dateinamen zuweisen. Diese Trennung ermöglicht es im übrigen auch eine Datei von Punkt A nach Punkt B zu verschieben, indem man folgendes an den Server sendet.
Code:
RNFR /https/html/dir_1/file.txt \r\n
RNTO /https/html/dir_2/file.txt \r\n
Entscheidet man sich dann, dass man die Datei vielleicht einfach löschen sollte, ist dies ganz einfach mit DELE /https/html/dir_2/file.txt CRLF möglich. Da man nicht die ganze Zeit immer nur arbeiten kann, die Verbindung zum Server jedoch nicht durch ein Timeout verloren gehen soll, gibt es auch einen Befehl, der einfach nichts tut. Dieser Befehl heißt NOOP, wie No Operation. Sollte man sich Fragen, ob es auch einen anderen Port gibt, auf dem der Server über FTP erreichbar ist oder welche Operationen noch unterstützt werden, kann einem der befehl HELP diese Fragen beantworten. Da jede Verbindung auch mal beendet werden muss und wir nun auch mit den wichtigsten Befehlen durch sind, können wir unsere Verbindung auch einfach mit QUIT schließen.
Wie bei vielen Kommunikationsarten mit einem Server liefert dieser auch während der Verbindung Antworten, die der Client überprüfen muss, um auftretende Fehler frühzeitig zu erkennen. Alle Antworten vom Server setzen sich dabei aus einem festgelegten, dreistelligen Zahlencode und einer Server spezifischen Textnachricht zusammen. Eine verkürzte Zusammenstellung der verschiedenen Fehlercodes findet ihr nachfolgend.
1XX: Anforderung akzeptiert, weitere Aktionen nötig.
2XX: Anforderung erfolgreich ausgeführt.
3XX: Anforderung verstanden, weitere Informationen benötigt.
4XX: Temporärer Fehler, erneuter Versuch kann Fehler beheben.
5XX: Fataler Fehler, Anforderung kann nicht verarbeitet werden.
Die zweite Ziffer gibt weiteren Aufschluss.
x0x: Syntax Fehler
x1x: Antwortinformation (z.B. beim Senden des Befehls HELP)
x2x: Status der Verbindung
x3x: Kontroll und Daten Verbindung betreffend
x4x: Nicht benutzt
x5x: Aktionen des Datei-Systems betreffend
Diese Fehlercodes gilt es in unserer Klasse abzufragen und korrekt zu verarbeiten.
Eine erste Verbindung
Da wir nun die wichtigsten Befehle und Fehlercodes, sowie den allgemeinen Aufbau einer FTP-Anfrage kennengelernt haben, könnten wir uns eigentlich dem Aufbau unserer FTP-Klasse widmen, bevor wir dies jedoch tun, werden wir noch ein kleines Skript schreiben, dass das eben gelernte vertiefen soll.
Eine Verbindung zu einem Server über einen bestimmten Port lässt sich in PHP relativ einfach mit den Socket-Funktionen (man denke nun an fsockopen) herstellen. Abgesehen davon, dass man das Ergebnis der Funktion auf Erfolg prüfen sollte, kann man nach der Verbindungsaufnahme direkt mit dem Senden von Befehlen anfangen.
Code:
<?php
// Create a connection to the ftp-server ...
$sock = fsockopen("ftp://www.example.de", 20, $errno, $errstr, 30);
// ... and check if the connection was established.
if (!$sock) {
// Otherwise show an error message.
die("$errstr ($errno)");
}
else {
// Send the username ...
fwrite($sock, "USER ftp_user\r\n");
// ... and password to the server ...
fwrite($sock, "PASS ftp_pass\r\n");
// ... and switch to type I.
fwrite($sock, "TYPE I\r\n");
// Now ask for help, ...
fwrite($sock, "HELP\r\n");
// ... wait a moment ...
fwrite($sock, "NOOP\r\n");
// ... and quit.
fwrite($sock, "QUIT\r\n");
}
?>
Soeben haben wir also eine Verbindung mit dem Server unter www.example.de aufgebaut, uns an diesem angemeldet, nach Hilfe gefragt und uns wieder verabschiedet. Ihr könnt ja mal die verwendeten Befehle durch andere ersetzen und ein paar Tests durchführen, wobei ihr dies besser mit einem Telnet-Client machen solltet, da dieser euch auch die Antworten des Servers ausgibt, was in unserer PHP-Version noch nicht der Fall ist.
Die Implementierung
Wir wollen nun einige Methoden implementieren, die uns diese Kommunikation mit dem Server erleichtern.
Wir benötigen also eine Möglichkeit eine Socket-Verbindung aufzubauen und innerhalb des Objekts zugänglich abzuspeichern. Diese Aufgabe können wir einfach dem Konstruktor unserer Klasse überlassen. Da dann der Aufruf in PHP nicht ganz den tatsächlichen Sitzungsverlauf wiederspiegeln würde, implementieren wir außerdem die Methoden connect(), die die Verbidung zum Server herstellt, und login(), die uns an diesem authentifiziert.
Bei der Implementierung dieser Methoden wird man recht schnell merken, dass es sich lohnt einige private Hilfsmethoden einzuführen, die zum Beispiel das Senden eines Befehls (sendCmd()), das Abfragen der Server-Antwort (getReply()) und das Überprüfen der Kontroll-Verbindung sowie der Statuscodes übernehmen (checkControl() und checkData()). Da es sonst schwierig ist die Kommunikation mit dem Server nachzuvollziehen und eventuelle Fehler bei der Übermittlung von Daten festzustellen, ist es außerdem sinnvoll Methoden zur Verwaltung von Logging bzw Debugging-Ausgaben einzuführen.
Basierend auf diesen paar Methoden ist die Implementierung der restlichen Methoden eigentlich ein Kinderspiel. Wir implementieren also für die wichtigsten Befehle jeweils eine Funktion. Ich denke die Erläuterungen im Quellcode reichen aus, um die einzelnen Schritte in der Entwicklung nachvollziehen zu können.
Code:
<?php
error_reporting(E_ALL);
/**
* @class FtpConnect
*
* The FtpConnect class enables you to log onto a ftp server
* to manage your files. It is not very easily operated, but that is not required,
* because this class should just be the base for a comfortable
* class that can be used to manage the filesystem of a webserver.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
class FtpConnect {
/**
* @private
* The host's name.
*/
private $host;
/**
* @private
* The port of the ftp-server.
*/
private $port;
/**
* @private
* The controll connection handle.
*/
private $control;
/**
* @private
* The data connection handle.
*/
private $data;
/**
* @private
* The last server response.
*/
private $response;
/**
* @private
* The logging status.
*/
private $logStats;
/**
* @private
* The logging data.
*/
private $log;
/**
* @public
*
* The constructor sets up the used connection.
*
* @param String $host The name of the host.
* @param Integer $port The used port.
* @param String $name The user's name.
* @param String $pwd The user's password.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function __construct($host='localhost', $port=21, $user, $pwd, $log=false) {
// Declare if the connection with the ftp-server should be logged or not.
$this->logStats = $log;
// Store the host name and password.
$this->host = $host;
$this->port = $port;
// Now establish a connection to the server ...
$this->control = @fsockopen($host, $port);
// ... and check if everything is alright.
if (!$this->checkControl()) {
throw new Exception("Connection failed.\n");
}
// Switch to non-blocking mode ...
@set_socket_blocking($this->control, true);
// ... and set the timeout of the control.
@stream_set_timeout($this->control, 0, 200000);
// Check if the connection was established, ...
if($this->control) {
// ... send username, ...
$this->sendCmd("USER " . $user);
if (!$this->checkControl()) {
throw new Exception("User not accepted: " . $user);
}
// ... and password to the server ...
$this->sendCmd("PASS " . $pwd);
if (!$this->checkControl()) {
throw new Exception("Wrong password.");
}
// ... and set the connection type.
$this->sendCmd("TYPE I");
if (!$this->checkControl()) {
throw new Exception("Cannot switch to TYPE I.");
}
}
}
/**
* @public
*
* Creates a connection to the ftp server.
*
* @return Returns the control handle.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function connect() {
// Establish a connection to the server ...
$this->control = @fsockopen($host, $port);
// ... and check if everything is alright.
if (!$this->checkControl()) {
throw new Exception("Connection failed.\n");
}
// Switch to non-blocking mode ...
@set_socket_blocking($this->control, true);
// ... and set the timeout of the control.
@stream_set_timeout($this->control, 0, 200000);
return $this->control;
}
/**
* @public
*
* Logs a user onto the server.
*
* @param String $user The user's name.
* @param String $pwd The user's password.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function login($user, $password) {
// Send username, ...
$this->sendCmd("USER " . $user);
if (!$this->checkControl()) {
throw new Exception("User not accepted: " . $user);
}
// ... and password to the server ...
$this->sendCmd("PASS " . $pwd);
if (!$this->checkControl()) {
throw new Exception("Wrong password.");
}
// ... and set the connection type.
$this->sendCmd("TYPE I");
if (!$this->checkControl()) {
throw new Exception("Cannot switch to TYPE I.");
}
}
/**
* @public
*
* Closes the current connection to the ftp-server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function quit()
{
if($this->control) {
$this->sendCmd("QUIT");
}
}
/**
* @private
*
* Returns the reply of the ftp-server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
* Version 0.2, 28.03.2008<br />
*/
private function getReply($i = 0) {
// Read the server response from the socket handle, ...
$message = fgets($this->control);
// ... check if there was a response, ...
if( $message == '' ) {
// ... check if max depth is reached ...
if( $i==30 ) {
return "";
}
else {
// ... and wait a moment.
sleep(1);
return $this->getReply($i+1);
}
}
/* The following code is an alternativ way to read
data from a socket stream.
$go = true;
$message = "";
do
{
$tmp = @fgets($this->control, 512);
if($tmp === false)
{
$go = false;
}
else
{
$message .= $tmp;
if( preg_match('/^([0-9]{3})(-(.*[\r\n]{1,2})+\\1)? [^\r\n]+[\r\n]{1,2}$/', $message) ) $go = false;
}
} while($go);*/
// Write server-response to connection-log.
$this->log($message);
return $message;
}
/**
* @private
*
* Checks if the response of a command is ok.
*
* @return Returns if a valid response was given or not.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function checkControl() {
// get the response from the server ...
$this->response = $this->getReply();
// ... and check if everything is alright.
return (empty($this->response) || preg_match('/^[5]/', $this->response)) ? false : true;
}
/**
* @private
*
* Checks if the response of the data-connection after a command is ok.
*
* @param String $code The response code of the server.
* @return Returns if a given code matches the received error code.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function checkData($code) {
// If code 230 was returned the user is logged in and there is nothing to do.
if($code == '230') {
while(true) {
if($this->checkControl() && preg_match('/^230 /',$this->response)) {
return true;
}
else {
return false;
}
}
}
// Check if an other code was received.
else {
if($this->checkControl()) {
$pat = '/^'. $code .'/';
if( preg_match($pat, $this->response)) {
return true;
}
}
return false;
}
}
/**
* @private
*
* Sends a ftp-command given by the user to the server.
*
* @param String $cmd The command which should be transmit to the server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function sendCmd($cmd) {
// Send the command to the server ...
fputs($this->control, "$cmd\r\n");
// ... and create a log entry.
$this->log("> $cmd");
}
/**
* @public
*
* Opens a connection in passive mode for file transferes.
*
* @return Returns the data connection handle.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function pasv() {
// Send the command for entering passive mode ...
$this->sendCmd("PASV");
// ... and check the controll connection.
if($this->checkControl()) {
// Then get the response for PASV.
$offset = strpos($this->response, "(");
$res = substr($this->response, ++$offset, strlen($this->response)-2);
$parts = split(",", trim($res));
// Get the host and port for passive connection ...
$host = "$parts[0].$parts[1].$parts[2].$parts[3]";
$port = ((int)$parts[4] << 8) + (int) $parts[5];
// ... and store the data-connection handle.
$this->data = @fsockopen($host, $port);
return $this->data;
}
}
/**
* @public
*
* Lists the given directory.
*
* @param String $path The path to the directory that should be listed.
* @return The directory listing.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function listPath($path = null) {
// Initialize the path listing ...
$list = "";
// ... and check if we are already running passive mode.
if($this->pasv())
{
if( empty($path) ) {
// Ask for a directory listing for the current path.
$this->sendCmd("LIST " . $this->pwd());
}
else {
// Ask for a directory listing for the specified path.
$this->sendCmd("LIST " . $path);
}
// Check for a response ...
if($this->checkControl()) {
// ... and add the response to the result list.
while(true) {
$line = fgets($this->data);
$list .= $line;
if($line == '') {
break;
}
}
}
}
return $list;
}
/**
* @public
*
* Returns a download handle.
*
* @param String $path The path to the file that should be downloaded.
* @return A download handle.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function retr($path) {
// Check if we are already running passive mode, ...
if($this->pasv()) {
// ... send the command, ...
$this->sendCmd("RETR " . $path);
// ... check for a result ...
if($this->checkControl()) {
// ... and return it.
return $this->data;
}
}
return false;
}
/**
* @public
*
* Allowes to upload a file from local to remote path.
*
* @param String $local The local path.
* @param String $remote The remote path.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function stor($local, $remote) {
// Check if we are already running passive mode, ...
if($this->pasv()) {
// ... send the command, ...
$this->sendCmd("STOR " . $remote);
// ... check for a result ...
if($this->checkControl()) {
// ... and write the data to the target.
$fp = fopen($local, "rb");
while(!feof($fp)) {
$line = fread($fp, 4096);
fwrite($this->data, $line);
}
}
}
}
/**
* @public
*
* Moves to the parent directory.
*
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function cdup() {
// Send the command ...
$this->sendCmd("CDUP");
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* Moves to the specified directory.
*
* @param String $path The path where to move.
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function cwd($path) {
// Send the command ...
$this->sendCmd("CWD " . $path);
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* Creates the specified directory.
*
* @param String $path The name of the directory to be created.
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function mkdir($path) {
// Send the command ...
$this->sendCmd("MKD " . $path);
// ... and return the result of the error-code check.
return $this->checkData("257");
}
/**
* @public
*
* Removes the specified directory.
*
* @param String $path The name of the directory to be removed.
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function rmdir($path) {
// Send the command ...
$this->sendCmd("RMD " . $path);
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* Returns the current working directory.
*
* @return The absolute path to the current directory.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function pwd() {
// Send the command, ...
$this->sendCmd("PWD");
// ... check for some status codes ...
if($this->checkData("257") || $this->checkData("226")) {
// ... and return the result.
$parts = split(" ",$this->response);
return preg_replace('/\"/',"",$parts[1]);
}
}
/**
* @public
*
* Marks the file to be renamed.
*
* @param String $file The file to be renamed.
* @return Returns if operation was successful.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function rnfr($file) {
// Send the command, ...
$this->sendCmd("RNFR $file");
// ... and validates the result.
if($this->checkControl()) {
return true;
}
else {
return false;
}
}
/**
* @public
*
* Renames a marked file.
*
* @param String $file The new name of the file.
* @return Returns if operation was successful.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function rnto($file) {
// Send the command, ...
$this->sendCmd("RNTO $file");
// ... and validates the result.
if($this->checkControl()) {
return true;
}
else {
return false;
}
}
/**
* @public
*
* Deletes the specified file.
*
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function delete($path) {
// Send the command, ...
$this->sendCmd("DELE " . $path);
// ... and return the result of the error-code check.
return $this->checkData("250");
}
/**
* @public
*
* No operation - just wait.
*
* @return The result of the error-code check.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function noop() {
// Send the command, ...
$this->sendCmd("NOOP");
// ... and return the result of the error-code check.
return $this->checkData("200");
}
/**
* @public
*
* Returns some useful information.
*
* @return The help message of the server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function help() {
// Initialize the help message, ...
$help = "";
// ... ask for some help ...
$this->sendCmd("HELP");
// ... and save the response of the server.
while(true) {
$line = fgets($this->control);
$help .= $line;
if($line == '')
break;
}
return $help;
}
/**
* @private
*
* Saves all request and responses to and from the server.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
private function log($str) {
// Check if data should be logged ...
if($this->logStats) {
// ... and log them if needed.
$this->log .= "$str<br>";
}
}
/**
* @public
*
* Returns the currently logged data.
*
* @return The logging output.
*
* @author Andreas Wilhelm
* @version
* Version 0.1, 13.02.2008<br />
*/
public function __toString() {
return $this->log;
}
}
?>
Auch wenn der Quellcode sich eigentlich von selbst erklärt, möchte ich noch einmal kurz auf die Methode getReply() eingehen.
Die Methode getReply() nimmt, wie der Name schon sagt, die Antwort des Servers entgegen. Da der Server nicht immer sofort antwortet, muss sichergestellt werden, dass die Antwort des Servers auf einen vorangegangenen Befehl auch wirklich abgefangen wird. Dies geschieht indem man immer und immer wieder eine Zeile aus dem Antwort-Buffer des Servers ließt. Da wir wissen, dass eine Antwort nie länger als eine Zeile ist und zumeist nur wenige Zeichen enthält, reicht es wenn wir immer nur 1024 Zeichen einlesen. Die eingelesenen Zeichen überprüfen wir auf ihre Syntax. Entspricht diese der einer Server-Antwort können wir die Schleife verlassen und die Antwort zurückgeben.
Anwendung der FTP-Klasse
Abschließend wollen wir uns noch ansehen, wie man mit Hilfe dieser Klasse noch einen eigenen Download-Link anbieten kann. Im einfachsten Fall geht dies natürlich über einen Link auf die Datei, die zum Download angeboten werden soll. Handelt es sich dabei jedoch um eine Text-Datei, wie zum Beispiel eine *.html, *.php, *.css oder auch *.csv-Datei, so wird diese entweder ausgeführt, falls es sich um eine ausführbare Datei handelt, interpretiert, falls es sich zum Beispiel um eine *.html-Datei handelt, oder einfach angezeigt. Das ist ja aber im Falle eines Download-Links nicht erwünscht. Daher möchten wir den Browser dazu veranlassen, einen Download-Dialog anzuzeigen. Es ist hierfür nur nötig eine Verbindung zum Zielserver mit Hilfe unserer FTP-Klasse herzustellen und den entsprechenden MIME-Type zu setzen, wobei man hier, wie so oft, Rücksicht auf den Internet Explorer nehmen muss. Diese Aufgaben erledigt der nachfolgende Code-Schnipsel für uns.
getfile.php
Code:
<?php
try {
// Create a connection to the server ...
$ftp = new FtpConnect(
'ftp_host',
'ftp_port',
'ftp_user',
'ftp_pwd',
true
);
// ... and switch to the working directory.
$ftp->cwd("/downloads/");
// Now get the request parameter and call the ftp retrieve command.
$sock = $ftp->retr(trim($_REQUEST['file']));
// Check if a connection was established ...
if($sock) {
// ... and set the mime-type if everything is alright.
$mime_type = (strstr($HTTP_USER_AGENT,'IE') ||
strstr($HTTP_USER_AGENT,'OPERA'))
? 'application/octetstream'
: 'application/octet-stream';
// Finally send the headers and the file.
header('Content-Type: ' . $mime_type);
header('Expires: ' . date("Y/m/d H:i:s"));
// Fix problems with the IE.
if( strstr($HTTP_USER_AGENT, 'IE') ) {
header('Content-Disposition: inline; filename="' . $_REQUEST['file'] . '"');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
}
else {
header('Content-Disposition: attachment; filename="'. $_REQUEST['file'] . '"');
header('Pragma: no-cache');
}
fpassthru($sock);
exit();
}
else {
throw new Exception('File could not be retrieved.');
}
}
catch(Exception $e) {
echo $e->getMessage();
}
?>
Das sieht doch schon recht einfach aus. Es ist uns nun also möglich, unseren Benutzern durch Einbinden eines einfachen Links auf deine-domain.tld/getfile.php?dateiname.end die Datei dateiname.end zum Download anzubieten. Dabei kann die Datei auch auf einem ganz anderen Server liegen, als das Skript, dass den Download bereitstellt. Dazu müssen uns nur die entsprechenden Verbindungs-Daten vorliegen.
Fragen und Antworten
Fragen und Antworten findet ihr selbstverständlich bei uns im Forum. Ich möchte euch auch darum bitten, keine Fragen an mich persönlich zu senden aus dem einfachen Grund, dass andere Menschen dann nichts von den gegebenen Antworten haben. Zudem können euch im Forum auch viele andere Leute, die echt Ahnung haben, helfen. Natürlich freue ich mich über ein kurzes Feedback zu diesem Tutorial. Ich bedanke mich für eure Aufmerksamkeit und hoffe, dass euch mein kurzer Exkurs gefallen hat.
Liebe Grüße,
Andreas
http://www.avedo.net
>> Allgemeine Fragen oder Probleme mit dem Tutorial? Hier gehts zum Forum!