From 499116d8298b128b53ef185a1d9189e4ac969c75 Mon Sep 17 00:00:00 2001 From: "Nicolab.net" Date: Fri, 11 Jul 2014 20:07:54 +0200 Subject: [PATCH] Initial commit --- .gitignore | 6 + LICENSE | 22 + README.md | 209 ++++++ composer.json | 22 + src/FtpClient/FtpClient.php | 842 +++++++++++++++++++++++++ src/FtpClient/FtpException.php | 20 + src/FtpClient/FtpWrapper.php | 117 ++++ tests/.atoum.php | 10 + tests/bootstrap.php | 6 + tests/units/FtpClient/FtpClient.php | 34 + tests/units/FtpClient/FtpException.php | 43 ++ tests/units/FtpClient/FtpWrapper.php | 53 ++ 12 files changed, 1384 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/FtpClient/FtpClient.php create mode 100644 src/FtpClient/FtpException.php create mode 100644 src/FtpClient/FtpWrapper.php create mode 100644 tests/.atoum.php create mode 100644 tests/bootstrap.php create mode 100644 tests/units/FtpClient/FtpClient.php create mode 100644 tests/units/FtpClient/FtpException.php create mode 100644 tests/units/FtpClient/FtpWrapper.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a98419e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +composer.phar +composer.lock +.DS_Store +Thumbs.db +/.Trash-1000 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ce9fd63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Nicolas Tallefourtane dev@nicolab.net + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d0e96f --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# nicolab/php-ftp-client + +A flexible FTP and SSL-FTP client for PHP. +This lib provides helpers easy to use to manage the remote files. + + +## Install + + * Use composer: `composer install nicolab/php-ftp-client` + + * Or use GIT clone command: `git clone git@github.com:Nicolab/php-ftp-client.git` + + * Or download the library, configure your autoloader or include the 3 files of `nicolab/php-ftp-client/src/FtpClient` directory. + + +## Getting Started + +Connect to a server FTP : + +```php + $ftp = new \FtpClient\FtpClient(); + $ftp->connect($host); + $ftp->login($login, $password); +``` + +OR + +Connect to a server FTP via SSL (on port 22 or other port) : + +```php + $ftp = new \FtpClient\FtpClient(); + $ftp->connect($host, true, 22); + $ftp->login($login, $password); +``` + + +### Usage + +Upload all files and all directories is easy : + +```php + // upload with the ASCII mode + $ftp->putAll($source_directory, $target_directory); + + // Is equal to + $ftp->putAll($source_directory, $target_directory, FTP_ASCII); + + // or upload with the binary mode + $ftp->putAll($source_directory, $target_directory, FTP_BINARY); + +``` + +*Note : FTP_ASCII and FTP_BINARY is the predefined PHP internal constant.* + +Get the a directory size : + +```php + + // size of the current directory + $size = $ftp->dirSize(); + + // size of a given directory + $size = $ftp->dirSize('/path/of/directory'); + +``` + +Count the items in a directory : + +```php + + // count in the current directory + $total = $ftp->count(); + + // count in a given directory + $total = $ftp->count('/path/of/directory'); + + // count only the "files" in the current directory + $total_file = $ftp->count('.', 'file'); + + // count only the "files" in a given directory + $total_file = $ftp->count('/path/of/directory', 'file'); + + // count only the "directories" in a given directory + $total_dir = $ftp->count('/path/of/directory', 'directory'); + + // count only the "symbolic links" in a given directory + $total_link = $ftp->count('/path/of/directory', 'link'); + +``` + +List detailed of all files and directories : + +```php + // scan the current directory and returns the details of each item + $items = $ftp->scanDir(); + + // scan the current directory (recursive) and returns the details of each item + var_dump($ftp->scanDir('.', true)); +``` + +Result: + + 'directory#www' => + array (size=10) + 'permissions' => string 'drwx---r-x' (length=10) + 'number' => string '3' (length=1) + 'owner' => string '32385' (length=5) + 'group' => string 'users' (length=5) + 'size' => string '5' (length=1) + 'month' => string 'Nov' (length=3) + 'day' => string '24' (length=2) + 'time' => string '17:25' (length=5) + 'name' => string 'www' (length=3) + 'type' => string 'directory' (length=9) + + 'link#www/index.html' => + array (size=11) + 'permissions' => string 'lrwxrwxrwx' (length=10) + 'number' => string '1' (length=1) + 'owner' => string '0' (length=1) + 'group' => string 'users' (length=5) + 'size' => string '38' (length=2) + 'month' => string 'Nov' (length=3) + 'day' => string '16' (length=2) + 'time' => string '14:57' (length=5) + 'name' => string 'index.html' (length=10) + 'type' => string 'link' (length=4) + 'target' => string '/var/www/shared/index.html' (length=26) + + 'file#www/README' => + array (size=10) + 'permissions' => string '-rw----r--' (length=10) + 'number' => string '1' (length=1) + 'owner' => string '32385' (length=5) + 'group' => string 'users' (length=5) + 'size' => string '0' (length=1) + 'month' => string 'Nov' (length=3) + 'day' => string '24' (length=2) + 'time' => string '17:25' (length=5) + 'name' => string 'README' (length=6) + 'type' => string 'file' (length=4) + + +All FTP PHP functions are supported and some improved : + +```php + + // Requests execution of a command on the FTP server + $ftp->exec($command); + + // Turns passive mode on or off + $ftp->pasv(true); + + // Set permissions on a file via FTP + $ftp->chmod('0777', 'file.php'); + + // Removes a directory + $ftp->rmdir('path/of/directory/to/remove'); + + // Removes a directory (recursive) + $ftp->rmdir('path/of/directory/to/remove', true); + + // Creates a directory + $ftp->mkdir('path/of/directory/to/create'); + + // Creates a directory (recursive), + // creates automaticaly the sub directory if not exist + $ftp->mkdir('path/of/directory/to/create', true); + + // and more ... + +``` + +Get the help information of the remote FTP server : + +```php + var_dump($ftp->help()); +``` + +Result : + + array (size=6) + 0 => string '214-The following SITE commands are recognized' (length=46) + 1 => string ' ALIAS' (length=6) + 2 => string ' CHMOD' (length=6) + 3 => string ' IDLE' (length=5) + 4 => string ' UTIME' (length=6) + 5 => string '214 Pure-FTPd - http://pureftpd.org/' (length=36) + + +_Note : The result depend of the FTP server._ + + +## Testing + +Tested with "atoum" unit testing framework. + + +## License + +[MIT](https://github.com/Nicolab/php-ftp-client/blob/master/LICENSE) + + +## Author + +| [![Nicolas Tallefourtane - Nicolab.net](http://www.gravatar.com/avatar/d7dd0f4769f3aa48a3ecb308f0b457fc?s=64)](http://nicolab.net) | +|---| +| [Nicolas Talle](http://nicolab.net) | +| [![Make a donation via Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PGRH4ZXP36GUC) | \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..39066bb --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "nicolab/php-ftp-client", + "type": "library", + "description": "A flexible FTP and SSL-FTP client for PHP. This lib provides helpers easy to use to manage the remote files.", + "license": "MIT", + "keywords": ["ftp", "sftp", "ssl-ftp", "ssl", "file", "server", "lib", "helper"], + "homepage": "https://github.com/Nicolab/php-ftp-client", + + "authors" : [ + { + "name" : "Nicolas Tallefourtane", + "email" : "dev@nicolab.net", + "homepage" : "http://nicolab.net" + } + ], + "require": { + "php": ">=5.4" + }, + "autoload": { + "psr-0": {"FtpClient": "src/"} + } +} diff --git a/src/FtpClient/FtpClient.php b/src/FtpClient/FtpClient.php new file mode 100644 index 0000000..b1071e4 --- /dev/null +++ b/src/FtpClient/FtpClient.php @@ -0,0 +1,842 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @copyright Nicolas Tallefourtane http://nicolab.net + */ +namespace FtpClient; + +use + \Closure, + \Countable +; + +/** + * The FTP and SSL-FTP client for PHP. + * + * @method bool alloc() alloc(int $filesize, string &$result = null) Allocates space for a file to be uploaded + * @method bool cdup() cdup() Changes to the parent directory + * @method bool chdir() chdir(string $directory) Changes the current directory on a FTP server + * @method int chmod() chmod(int $mode, string $filename) Set permissions on a file via FTP + * @method bool close() close() Closes an FTP connection + * @method bool delete() delete(string $path) Deletes a file on the FTP server + * @method bool exec() exec(string $command) Requests execution of a command on the FTP server + * @method bool fget() fget(resource $handle, string $remote_file, int $mode, int $resumepos = 0) Downloads a file from the FTP server and saves to an open file + * @method bool fput() fput(string $remote_file, resource $handle, int $mode, int $startpos = 0) Uploads from an open file to the FTP server + * @method mixed get_option() get_option(int $option) Retrieves various runtime behaviours of the current FTP stream + * @method bool get() get(string $local_file, string $remote_file, int $mode, int $resumepos = 0) Downloads a file from the FTP server + * @method int mdtm() mdtm(string $remote_file) Returns the last modified time of the given file + * @method int nb_continue() nb_continue() Continues retrieving/sending a file (non-blocking) + * @method int nb_fget() nb_fget(resource $handle, string $remote_file, int $mode, int $resumepos = 0) Retrieves a file from the FTP server and writes it to an open file (non-blocking) + * @method int nb_fput() nb_fput(string $remote_file, resource $handle, int $mode, int $startpos = 0) Stores a file from an open file to the FTP server (non-blocking) + * @method int nb_get() nb_get(string $local_file, string $remote_file, int $mode, int $resumepos = 0) Retrieves a file from the FTP server and writes it to a local file (non-blocking) + * @method int nb_put() nb_put(string $remote_file, string $local_file, int $mode, int $startpos = 0) Stores a file on the FTP server (non-blocking) + * @method bool pasv() pasv(bool $pasv) Turns passive mode on or off + * @method bool put() put(string $remote_file, string $local_file, int $mode, int $startpos = 0) Uploads a file to the FTP server + * @method string pwd() pwd() Returns the current directory name + * @method bool quit() quit() Closes an FTP connection + * @method array raw() raw(string $command) Sends an arbitrary command to an FTP server + * @method array rawlist() rawlist(string $directory, bool $recursive = false) Returns a detailed list of files in the given directory + * @method bool rename() rename(string $oldname, string $newname) Renames a file or a directory on the FTP server + * @method bool set_option() set_option(int $option, mixed $value) Set miscellaneous runtime FTP options + * @method bool site() site(string $command) Sends a SITE command to the server + * @method int size() size(string $remote_file) Returns the size of the given file + * @method string systype() systype() Returns the system type identifier of the remote FTP server + * + * @author Nicolas Tallefourtane + */ +class FtpClient implements Countable +{ + /** + * The connection with the server + * + * @var resource + */ + protected $conn; + + /** + * PHP FTP functions wrapper + * + * @var Functions + */ + private $ftp; + + /** + * Constructor. + * @param resource|null $connection + * @throws FtpException If ftp extension is not loaded. + */ + public function __construct($connection = null) + { + if (!extension_loaded('ftp')) { + throw new FtpException('FTP extension is not loaded!'); + } + + if ($connection) { + $this->conn = $connection; + } + + $this->setWrapper(new FtpWrapper($this->conn)); + } + + /** + * Close the connection when the object is destroyed + */ + public function __destruct() + { + if ($this->conn) { + $this->ftp->close(); + } + } + + /** + * Call the FTP method with the wrapper. + * + * Wrap the FTP PHP functions to call as method of FtpClient object. + * The connection is automaticaly passed to the FTP PHP functions. + * + * @param string $function + * @param array $arguments + * @return mixed + * @throws FtpException When the function is not valid + */ + public function __call($method, array $arguments) + { + return $this->ftp->__call($method, $arguments); + } + + /** + * Set the wrapper which forward the PHP FTP functions to use in FtpClient instance. + * @param FtpWrapper $wrapper + * @return FtpClient + */ + public function setWrapper(FtpWrapper $wrapper) + { + $this->ftp = $wrapper; + + return $this; + } + + /** + * Overwrites the PHP limit + * + * @param string|null $memory The memory limit, if null is not modified + * @param int $time_limit The max execution time, unlimited by default + * @param bool $ignore_user_abort Ignore user abort, true by default + * @return FtpClient + */ + public function setPhpLimit($memory = null, $time_limit = 0, $ignore_user_abort = true) + { + if (null !== $memory) { + ini_set('memory_limit', $memory); + } + + ignore_user_abort(true); + set_time_limit($time_limit); + + return $this; + } + + /** + * Get the help information of the remote FTP server. + * @return array + */ + public function help() + { + return $this->ftp->raw('help'); + } + + /** + * Open a FTP connection + * + * @param string $host + * @param bool $ssl + * @param int $port + * @param int $timeout + * + * @return FTPClient + * @throws FtpException If unable to connect + */ + public function connect($host, $ssl = false, $port = 21, $timeout = 90) + { + if ($ssl) { + $this->conn = @$this->ftp->ssl_connect($host, $port, $timeout); + } else { + $this->conn = @$this->ftp->connect($host, $port, $timeout); + } + + if (!$this->conn) { + throw new FtpException('Unable to connect'); + } + + return $this; + } + + /** + * Get the connection with the server + * + * @return resource + */ + public function getConnection() + { + return $this->conn; + } + + /** + * Get the wrapper + * + * @return FtpWrapper + */ + public function getWrapper() + { + return $this->ftp; + } + + /** + * Logs in to an FTP connection + * + * @param string $username + * @param string $password + * + * @return FTPClient If the login is incorrect + */ + public function login($username = 'anonymous', $password = '') + { + $result = $this->ftp->login($username, $password); + + if ($result === false) { + throw new FtpException('Login incorrect'); + } + + return $this; + } + + /** + * Returns the last modified time of the given file. + * Return -1 on error + * + * @param string $remoteFile + * + * @return int + */ + public function modifiedTime($remoteFile, $format = null) + { + $time = $this->ftp->mdtm($remoteFile); + + if ($time !== -1 && $format !== null) { + return date($format, $time); + } + + return $time; + } + + /** + * Changes to the parent directory + * + * @return FTPClient + */ + public function up() + { + $result = @$this->ftp->cdup(); + + if ($result === false) { + throw new FtpException('Unable to get parent folder'); + } + + return $this; + } + + /** + * Returns a list of files in the given directory + * + * @param string $directory The directory, by default is "." the current directory + * @param bool $recursive + * @param callable $filter A callable to filter the result, by default is asort() PHP function. + * The result is passed in array argument, must take the argument by reference ! + * The callable should proceed with the reference array because is the behavior of several PHP sorting functions (by reference ensure directly the compatibility with all PHP sorting functions). + * + * @return array + * @throws FtpException If unable to list the directory + */ + public function nlist($directory = '.', $recursive = false, $filter = 'sort') + { + if (!$this->isDir($directory)) { + throw new FtpException('"'.$directory.'" is not a directory'); + } + + $files = $this->ftp->nlist($directory); + + if ($files === false) { + throw new FtpException('Unable to list directory'); + } + + $result = array(); + $dir_len = strlen($directory); + + // if it's the current + if (false !== ($kdot = array_search('.', $files))) { + unset($files[$kdot]); + } + + // if it's the parent + if(false !== ($kdot = array_search('..', $files))) + unset($files[$kdot]); + + if (!$recursive) { + + foreach ($files as $file) { + $result[] = $directory.'/'.$file; + } + + // working with the reference (behavior of several PHP sorting functions) + $filter($result); + + return $result; + } + + // utils for recursion + $flatten = function (array $arr) use (&$flatten) { + + $flat = []; + + foreach ($arr as $k => $v) { + + if (is_array($v)) { + $flat = array_merge($flat, $flatten($v)); + } else { + $flat[] = $v; + } + } + + return $flat; + }; + + foreach ($files as $file) { + + $file = $directory.'/'.$file; + + // if contains the root path (behavior of the recursivity) + if (0 === strpos($file, $directory, $dir_len)) { + $file = substr($file, $dir_len); + } + + if ($this->isDir($file)) { + + $result[] = $file; + $items = $flatten($this->nlist($file, true, $filter)); + + foreach ($items as $item) { + $result[] = $item; + } + + } else { + + $result[] = $file; + } + } + + $result = array_unique($result); + + $filter($result); + + return $result; + } + + /** + * Creates a directory + * @see FtpClient::rmdir() + * @see FtpClient::remove() + * @see FtpClient::put() + * @see FtpClient::putAll() + * + * @param string $directory The directory + * @param bool $recursive + * @return array + */ + public function mkdir($directory, $recursive = false) + { + if (!$recursive or $this->isDir($directory)) { + return $this->ftp->mkdir($directory); + } + + $result = false; + $pwd = $this->ftp->pwd(); + $parts = explode('/', $directory); + + foreach ($parts as $part) { + + if (!@$this->ftp->chdir($part)) { + + $result = $this->ftp->mkdir($part); + $this->ftp->chdir($part); + } + } + + $this->ftp->chdir($pwd); + + return $result; + } + + /** + * Remove a directory. + * @see FtpClient::mkdir() + * @see FtpClient::cleanDir() + * @see FtpClient::remove() + * @see FtpClient::delete() + * @param string $directory + * @param bool $recursive Forces deletion if the directory is not empty + * @return bool + * @throws FtpException If unable to list the directory to remove + */ + public function rmdir($directory, $recursive = true) + { + if ($recursive) { + + $files = $this->nlist($directory, false, 'rsort'); + + // remove children + foreach ($files as $file) { + $this->remove($file, true); + } + } + + // remove the directory + return $this->ftp->rmdir($directory); + } + + /** + * Empty directory + * @see FtpClient::remove() + * @see FtpClient::delete() + * @see FtpClient::rmdir() + * + * @param string $directory + * @return bool + */ + public function cleanDir($directory) + { + if(!$files = $this->nlist($directory)) + + return $this->isEmpty($directory); + + // remove children + foreach ($files as $file) { + $this->remove($file, true); + } + + return $this->isEmpty($directory); + } + + /** + * Remove a file or a directory + * @see FtpClient::rmdir() + * @see FtpClient::cleanDir() + * @see FtpClient::delete() + * @param string $path The path of the file or directory to remove + * @param bool $recursive Is effective only if $path is a directory, {@see FtpClient::rmdir()} + * @return bool + */ + public function remove($path, $recursive = false) + { + try { + + if(@$this->ftp->delete($path) + or ($this->isDir($path) + and @$this->rmdir($path, $recursive))) { + return true; + } + + return false; + + } catch (\Exception $e) { + return false; + } + } + + /** + * Check if a directory exist. + * @param $directory + * @return bool + */ + public function isDir($directory) + { + $pwd = $this->ftp->pwd(); + + if ($pwd === false) { + throw new FtpException('Unable to resolve the current directory'); + } + + if (@$this->ftp->chdir($directory)) { + + $this->ftp->chdir($pwd); + + return true; + } + + $this->ftp->chdir($pwd); + + return false; + } + + /** + * Check if a directory is empty + * @param string $directory + * @return bool + */ + public function isEmpty($directory) + { + return $this->count($directory, null, false) === 0 ? true : false; + } + + /** + * Scan a directory and returns the details of each item. + * @see FtpClient::nlist() + * @see FtpClient::rawlist() + * @see FtpClient::parseRawList() + * @see FtpClient::dirSize() + * @param string $directory + * @param bool $recursive + * @return array + */ + public function scanDir($directory = '.', $recursive = false) + { + return $this->parseRawList($this->rawlist($directory, $recursive)); + } + + /** + * Returns the total size of the given directory in bytes + * + * @param string $directory The directory, by default is the current directory. + * @param bool $recursive true by default + * @return int The size in bytes. + */ + public function dirSize($directory = '.', $recursive = true) + { + $items = $this->scanDir($directory, $recursive); + $size = 0; + + foreach ($items as $item) { + $size += (int) $item['size']; + } + + return $size; + } + + /** + * Count the items (file, directory, link, unknown) + * @param string $directory The directory, by default is the current directory. + * @param string|null $type The type of item to count (file, directory, link, unknown) + * @param bool $recursive true by default + * @return int + */ + public function count($directory = '.', $type = null, $recursive = true) + { + $items = ( + null === $type + ? $this->nlist($directory, $recursive) + : $this->scanDir($directory, $recursive) + ); + + $count = 0; + + foreach ($items as $item) { + + if (null === $type or $item['type'] == $type) { + $count++; + } + } + + return $count; + } + + /** + * Uploads a file to the server from a string + * + * @param string $remote_file + * @param string $content + * @return FtpClient + * @throws FtpException When the transfer fails + */ + public function putFromString($remote_file, $content) + { + $handle = fopen('php://temp', 'w'); + + fwrite($handle, $content); + rewind($handle); + + if ($this->ftp->fput($remote_file, $handle, FTP_BINARY)) { + return $this; + } + + throw new FtpException('Unable to put the file "'.$remote_file.'"'); + } + + /** + * Uploads a file to the server + * + * @param string $local_file + * @return FtpClient + * @throws FtpException When the transfer fails + */ + public function putFromPath($local_file) + { + $remote_file = basename($local_file); + $handle = fopen($local_file, 'r'); + + if ($this->ftp->fput($remote_file, $handle, FTP_BINARY)) { + + rewind($handle); + + return $this; + } + + throw new FtpException('Unable to put the remote file from the local file "' + .$local_file.'"'); + } + + /** + * Upload files + * @param string $source_directory + * @param string $target_directory + * @param int $mode + * @return FtpClient + */ + public function putAll($source_directory, $target_directory, $mode = FTP_BINARY) + { + $d = dir($source_directory); + + // do this for each file in the directory + while ($file = $d->read()) { + + // to prevent an infinite loop + if ($file != "." && $file != "..") { + + // do the following if it is a directory + if (is_dir($source_directory.'/'.$file)) { + + if (!@$this->ftp->chdir($target_directory.'/'.$file)) { + + // create directories that do not yet exist + $this->ftp->mkdir($target_directory.'/'.$file); + } + + // recursive part + $this->putAll( + $source_directory.'/'.$file, $target_directory.'/'.$file, + $mode + ); + } else { + + // put the files + $this->ftp->put( + $target_directory.'/'.$file, $source_directory.'/'.$file, + $mode + ); + } + } + } + + return $this; + } + + /** + * Returns a detailed list of files in the given directory. + * @see FtpClient::nlist() + * @see FtpClient::scanDir() + * @see FtpClient::dirSize() + * @param string $directory The directory, by default is the current directory + * @param bool $recursive + * @return array + * @throws FtpException + */ + public function rawlist($directory = '.', $recursive = false) + { + if (!$this->isDir($directory)) { + throw new FtpException('"'.$directory.'" is not a directory.'); + } + + $list = $this->ftp->rawlist($directory); + $items = array(); + + if (false == $recursive) { + + foreach ($list as $path => $item) { + + $chunks = preg_split("/\s+/", $item); + + // if not "name" + if (empty($chunks[8]) || $chunks[8] == '.' || $chunks[8] == '..') { + continue; + } + + $path = $directory.'/'.$chunks[8]; + + if (substr($path, 0, 2) == './') { + $path = substr($path, 2); + } + + $items[ $this->rawToType($item).'#'.$path ] = $item; + } + + return $items; + } + + $path = ''; + + foreach ($list as $item) { + + $len = strlen($item); + + if(!$len + + // "." + || ($item[$len-1] == '.' && $item[$len-2] == ' ' + + // ".." + or $item[$len-1] == '.' && $item[$len-2] == '.' && $item[$len-3] == ' ') + ){ + + continue; + } + + $chunks = preg_split("/\s+/", $item); + + // if not "name" + if (empty($chunks[8]) || $chunks[8] == '.' || $chunks[8] == '..') { + continue; + } + + $path = $directory.'/'.$chunks[8]; + + if (substr($path, 0, 2) == './') { + $path = substr($path, 2); + } + + $items[$this->rawToType($item).'#'.$path] = $item; + + if ($item[0] == 'd') { + + $sublist = $this->rawlist($path, true); + + foreach ($sublist as $subpath => $subitem) { + $items[$subpath] = $subitem; + } + } + } + + return $items; + } + + /** + * Parse raw list + * @see FtpClient::rawlist() + * @see FtpClient::scanDir() + * @see FtpClient::dirSize() + * @param array $rawlist + * @return array + */ + public function parseRawList(array $rawlist) + { + $items = array(); + $path = ''; + + foreach ($rawlist as $key => $child) { + + $chunks = preg_split("/\s+/", $child); + + if (isset($chunks[8]) && ($chunks[8] == '.' or $chunks[8] == '..')) { + continue; + } + + if (count($chunks) === 1) { + + $len = strlen($chunks[0]); + + if ($len && $chunks[0][$len-1] == ':') { + $path = substr($chunks[0], 0, -1); + } + + continue; + } + + $item = [ + 'permissions' => $chunks[0], + 'number' => $chunks[1], + 'owner' => $chunks[2], + 'group' => $chunks[3], + 'size' => $chunks[4], + 'month' => $chunks[5], + 'day' => $chunks[6], + 'time' => $chunks[7], + 'name' => $chunks[8], + 'type' => $this->rawToType($chunks[0]), + ]; + + if ($item['type'] == 'link') { + $item['target'] = $chunks[10]; // 9 is "->" + } + + // if the key is not the path, behavior of ftp_rawlist() PHP function + if (is_int($key) || false === strpos($key, $item['name'])) { + + array_splice($chunks, 0, 8); + + $key = $item['type'].'#'.($path ? $path.'/' : '').implode(" ", $chunks); + + if ($item['type'] == 'link') { + + // get the first part of 'link#the-link.ext -> /path/of/the/source.ext' + $exp = explode(' ->', $key); + $key = rtrim($exp[0]); + } + + $items[$key] = $item; + + } else { + + // the key is the path, behavior of FtpClient::rawlist() method() + $items[$key] = $item; + } + } + + return $items; + } + + /** + * Convert raw info (drwx---r-x ...) to type (file, directory, link, unknown). + * Only the first char is used for resolving. + * @param string $permission Example : drwx---r-x + * @return string The file type (file, directory, link, unknown) + */ + public function rawToType($permission) + { + if (!is_string($permission)) { + throw new FtpException('The "$permission" argument must be a string, "' + .gettype($permission).'" given.'); + } + + if (empty($permission[0])) { + return 'unknown'; + } + + switch ($permission[0]) { + + case '-': + return 'file'; + + case 'd': + return 'directory'; + + case 'l': + return 'link'; + + default: + return 'unknown'; + } + } +} diff --git a/src/FtpClient/FtpException.php b/src/FtpClient/FtpException.php new file mode 100644 index 0000000..f17ed7f --- /dev/null +++ b/src/FtpClient/FtpException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @copyright Nicolas Tallefourtane http://nicolab.net + */ +namespace FtpClient; + +/** + * The FtpException class. + * Exception thrown if an error on runtime of the FTP client occurs. + * @inheritDoc + * @author Nicolas Tallefourtane + */ +class FtpException extends \Exception {} diff --git a/src/FtpClient/FtpWrapper.php b/src/FtpClient/FtpWrapper.php new file mode 100644 index 0000000..be0a596 --- /dev/null +++ b/src/FtpClient/FtpWrapper.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @copyright Nicolas Tallefourtane http://nicolab.net + */ +namespace FtpClient; + +/** + * Wrap the PHP FTP functions + * + * @method bool alloc() alloc(int $filesize, string &$result = null) Allocates space for a file to be uploaded + * @method bool cdup() cdup() Changes to the parent directory + * @method bool chdir() chdir(string $directory) Changes the current directory on a FTP server + * @method int chmod() chmod(int $mode, string $filename) Set permissions on a file via FTP + * @method bool close() close() Closes an FTP connection + * @method bool delete() delete(string $path) Deletes a file on the FTP server + * @method bool exec() exec(string $command) Requests execution of a command on the FTP server + * @method bool fget() fget(resource $handle, string $remote_file, int $mode, int $resumepos = 0) Downloads a file from the FTP server and saves to an open file + * @method bool fput() fput(string $remote_file, resource $handle, int $mode, int $startpos = 0) Uploads from an open file to the FTP server + * @method mixed get_option() get_option(int $option) Retrieves various runtime behaviours of the current FTP stream + * @method bool get() get(string $local_file, string $remote_file, int $mode, int $resumepos = 0) Downloads a file from the FTP server + * @method bool login() login(string $username, string $password) Logs in to an FTP connection + * @method int mdtm() mdtm(string $remote_file) Returns the last modified time of the given file + * @method string mkdir() mkdir(string $directory) Creates a directory + * @method int nb_continue() nb_continue() Continues retrieving/sending a file (non-blocking) + * @method int nb_fget() nb_fget(resource $handle, string $remote_file, int $mode, int $resumepos = 0) Retrieves a file from the FTP server and writes it to an open file (non-blocking) + * @method int nb_fput() nb_fput(string $remote_file, resource $handle, int $mode, int $startpos = 0) Stores a file from an open file to the FTP server (non-blocking) + * @method int nb_get() nb_get(string $local_file, string $remote_file, int $mode, int $resumepos = 0) Retrieves a file from the FTP server and writes it to a local file (non-blocking) + * @method int nb_put() nb_put(string $remote_file, string $local_file, int $mode, int $startpos = 0) Stores a file on the FTP server (non-blocking) + * @method array nlist() nlist(string $directory) Returns a list of files in the given directory + * @method bool pasv() pasv(bool $pasv) Turns passive mode on or off + * @method bool put() put(string $remote_file, string $local_file, int $mode, int $startpos = 0) Uploads a file to the FTP server + * @method string pwd() pwd() Returns the current directory name + * @method bool quit() quit() Closes an FTP connection + * @method array raw() raw(string $command) Sends an arbitrary command to an FTP server + * @method array rawlist() rawlist(string $directory, bool $recursive = false) Returns a detailed list of files in the given directory + * @method bool rename() rename(string $oldname, string $newname) Renames a file or a directory on the FTP server + * @method bool rmdir() rmdir(string $directory) Removes a directory + * @method bool set_option() set_option(int $option, mixed $value) Set miscellaneous runtime FTP options + * @method bool site() site(string $command) Sends a SITE command to the server + * @method int size() size(string $remote_file) Returns the size of the given file + * @method string systype() systype() Returns the system type identifier of the remote FTP server + * + * @author Nicolas Tallefourtane + */ +class FtpWrapper +{ + /** + * The connection with the server + * + * @var resource + */ + protected $conn; + + /** + * Constructor. + * + * @param ressource &$connection The FTP (or SSL-FTP) connection (takes by reference). + */ + public function __construct(&$connection) + { + $this->conn = &$connection; + } + + /** + * Forward the method call to FTP functions + * + * @param string $function + * @param array $arguments + * @return mixed + * @throws FtpException When the function is not valid + */ + public function __call($function, array $arguments) + { + $function = 'ftp_' . $function; + + if (function_exists($function)) { + + array_unshift($arguments, $this->conn); + + return call_user_func_array($function, $arguments); + } + + throw new FtpException("{$function} is not a valid FTP function"); + } + + /** + * Opens a FTP connection + * + * @param string $host + * @param int $port + * @param int $timeout + * @return resource + */ + public function connect($host, $port = 21, $timeout = 90) + { + return ftp_connect($host, $port, $timeout); + } + + /** + * Opens a Secure SSL-FTP connection + * @param string $host + * @param int $port + * @param int $timeout + * @return resource + */ + public function ssl_connect($host, $port = 21, $timeout = 90) + { + return ftp_ssl_connect($host, $port, $timeout); + } +} diff --git a/tests/.atoum.php b/tests/.atoum.php new file mode 100644 index 0000000..cee6cf6 --- /dev/null +++ b/tests/.atoum.php @@ -0,0 +1,10 @@ +addDefaultReport(); + +// This will add a green or red logo after each run depending on its status. +$report->addField(new atoum\report\fields\runner\result\logo()); + +$script->bootstrapFile(__DIR__. '/bootstrap.php'); +$runner->addTestsFromDirectory(__DIR__. '/units'); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..f6ab8c3 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,6 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @copyright Nicolas Tallefourtane http://nicolab.net + */ +namespace tests\units\FtpClient; + +use + mageekguy\atoum, + FtpClient\FtpClient as TestedClass +; + +/** + * Tests the FtpClient\FtpClient class. + * @author Nicolas Tallefourtane + */ +class FtpClient extends atoum\test +{ + + public function test__construct() + { + $this + ->given($ftp = new TestedClass()) + ->object($ftp) + ->isInstanceOf('\FtpClient\FtpClient') + ; + } +} diff --git a/tests/units/FtpClient/FtpException.php b/tests/units/FtpClient/FtpException.php new file mode 100644 index 0000000..7ae56da --- /dev/null +++ b/tests/units/FtpClient/FtpException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @copyright Nicolas Tallefourtane http://nicolab.net + */ +namespace tests\units\FtpClient; + +use + mageekguy\atoum, + FtpClient\FtpException as TestedClass +; + +/** + * Tests the FtpClient\FtpException class. + * @author Nicolas Tallefourtane + */ +class FtpException extends atoum\test +{ + + public function test__instance() + { + $ftp = new \FtpClient\FtpClient(); + + $this + ->given($e = new TestedClass()) + ->object($e) + ->isInstanceOf('\FtpClient\FtpException') + ->isInstanceOf('\Exception') + + ->exception(function () use ($ftp) { + $ftp->doNotExist(); + }) + ->isInstanceOf('\FtpClient\FtpException') + ->isInstanceOf('\Exception') + ; + } +} diff --git a/tests/units/FtpClient/FtpWrapper.php b/tests/units/FtpClient/FtpWrapper.php new file mode 100644 index 0000000..da67e91 --- /dev/null +++ b/tests/units/FtpClient/FtpWrapper.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @copyright Nicolas Tallefourtane http://nicolab.net + */ +namespace tests\units\FtpClient; + +use + mageekguy\atoum, + FtpClient\FtpWrapper as TestedClass +; + +/** + * Tests the FtpClient\FtpWrapper class. + * @author Nicolas Tallefourtane + */ +class FtpWrapper extends atoum\test +{ + + public function test__construct() + { + $conn = null; + + $this + ->given($wrapper = new TestedClass($conn)) + ->object($wrapper) + ->isInstanceOf('\FtpClient\FtpWrapper') + ; + } + + public function test__call() + { + $conn = null; + + $this + ->given($wrapper = new TestedClass($conn)) + ->exception(function () use ($wrapper) { + $wrapper->doNotExist(); + }) + ->isInstanceOf('\FtpClient\FtpException') + ->isInstanceOf('\Exception') + + ->variable(array($wrapper, 'alloc')) + ->isCallable() + ; + } +}