├── .github └── funding.yml ├── composer.json ├── example.php ├── license.md ├── readme.md └── src └── Ftp.php /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: dg 2 | custom: "https://nette.org/make-donation?to=ftp-php" 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dg/ftp-php", 3 | "description": "Easy-to-use library for accessing FTP servers", 4 | "keywords": ["ftp"], 5 | "homepage": "https://github.com/dg/ftp-php", 6 | "license": ["BSD-3-Clause"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "http://davidgrudl.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=8.1", 15 | "ext-ftp": "*" 16 | }, 17 | "autoload": { 18 | "classmap": ["src/"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | connect('ftp.ed.ac.uk'); 11 | 12 | // Login with username and password 13 | $ftp->login('anonymous', 'example@example.com'); 14 | 15 | // Download file 'README' to local temporary file 16 | $temp = tmpfile(); 17 | $ftp->fget($temp, 'README', Ftp::ASCII); 18 | 19 | // echo file 20 | echo '
'; 21 | fseek($temp, 0); 22 | fpassthru($temp); 23 | 24 | } catch (FtpException $e) { 25 | echo 'Error: ', $e->getMessage(); 26 | } 27 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Copyright (c) 2008 David Grudl 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of David Grudl nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | FTP for PHP 2 | =========== 3 | 4 | [](https://packagist.org/packages/dg/ftp-php) 5 | [](https://github.com/dg/ftp-php/releases) 6 | [](https://github.com/dg/ftp-php/blob/master/license.md) 7 | 8 | FTP for PHP is a very small and easy-to-use library for accessing FTP servers. 9 | 10 | It requires PHP 8.1 or newer and is licensed under the New BSD License. 11 | You can obtain the latest version from our [GitHub repository](https://github.com/dg/ftp-php/releases) 12 | or install it via Composer: 13 | 14 | ``` 15 | php composer.phar require dg/ftp-php 16 | ``` 17 | 18 | If you like it, **[please make a donation now](https://nette.org/make-donation?to=ftp-php)**. Thank you! 19 | 20 | 21 | Usage 22 | ----- 23 | 24 | Opens an FTP connection to the specified host: 25 | 26 | ```php 27 | $ftp = new Ftp; 28 | $host = 'ftp.example.com'; 29 | $ftp->connect($host); 30 | ``` 31 | 32 | Login with username and password 33 | 34 | ```php 35 | $ftp->login($username, $password); 36 | ``` 37 | 38 | Upload the file 39 | 40 | ```php 41 | $ftp->put($destinationFile, $sourceFile, Ftp::Binary); 42 | ``` 43 | 44 | Close the FTP stream 45 | 46 | ```php 47 | $ftp->close(); 48 | // or simply unset($ftp); 49 | ``` 50 | 51 | Ftp throws exception if operation failed. So you can simply do following: 52 | 53 | ```php 54 | try { 55 | $ftp = new Ftp; 56 | $ftp->connect($host); 57 | $ftp->login($username, $password); 58 | $ftp->put($destinationFile, $sourceFile, Ftp::Binary); 59 | 60 | } catch (FtpException $e) { 61 | echo 'Error: ', $e->getMessage(); 62 | } 63 | ``` 64 | 65 | On the other hand, if you'd like the possible exception quietly catch, call methods with the prefix `try`: 66 | 67 | ```php 68 | $ftp->tryDelete($destinationFile); 69 | ``` 70 | 71 | When the connection is accidentally interrupted, you can re-establish it using method `$ftp->reconnect()`. 72 | 73 | 74 | ----- 75 | (c) David Grudl, 2008, 2023 (http://davidgrudl.com) 76 | -------------------------------------------------------------------------------- /src/Ftp.php: -------------------------------------------------------------------------------- 1 | 'ssl_connect', 65 | 'getoption' => 'get_option', 66 | 'setoption' => 'set_option', 67 | 'nbcontinue' => 'nb_continue', 68 | 'nbfget' => 'nb_fget', 69 | 'nbfput' => 'nb_fput', 70 | 'nbget' => 'nb_get', 71 | 'nbput' => 'nb_put', 72 | ]; 73 | 74 | private FTP\Connection $resource; 75 | private array $state = []; 76 | 77 | 78 | public function __construct(?string $url = null, bool $passiveMode = true) 79 | { 80 | if (!extension_loaded('ftp')) { 81 | throw new Exception('PHP extension FTP is not loaded.'); 82 | } 83 | if ($url) { 84 | $parts = parse_url($url); 85 | if (!isset($parts['scheme']) || !in_array($parts['scheme'], ['ftp', 'ftps', 'sftp'], true)) { 86 | throw new InvalidArgumentException('Invalid URL.'); 87 | } 88 | $func = $parts['scheme'] === 'ftp' ? 'connect' : 'ssl_connect'; 89 | $this->$func($parts['host'], empty($parts['port']) ? 0 : (int) $parts['port']); 90 | if (isset($parts['user'])) { 91 | $this->login(urldecode($parts['user']), isset($parts['pass']) ? urldecode($parts['pass']) : ''); 92 | } 93 | $this->pasv((bool) $passiveMode); 94 | if (isset($parts['path'])) { 95 | $this->chdir($parts['path']); 96 | } 97 | } 98 | } 99 | 100 | 101 | /** 102 | * Magic method (do not call directly). 103 | * @throws Exception 104 | * @throws FtpException 105 | */ 106 | public function __call(string $name, array $args): mixed 107 | { 108 | $name = strtolower($name); 109 | $silent = strncmp($name, 'try', 3) === 0; 110 | $func = $silent ? substr($name, 3) : $name; 111 | $func = 'ftp_' . (self::Aliases[$func] ?? $func); 112 | 113 | if (!function_exists($func)) { 114 | throw new Exception("Call to undefined method Ftp::$name()."); 115 | } 116 | 117 | $errorMsg = null; 118 | set_error_handler(function (int $code, string $message) use (&$errorMsg) { 119 | $errorMsg = $message; 120 | }); 121 | 122 | if ($func === 'ftp_connect' || $func === 'ftp_ssl_connect') { 123 | $this->state = [$name => $args]; 124 | $this->resource = $func(...$args); 125 | $res = null; 126 | 127 | } elseif (!$this->resource instanceof FTP\Connection) { 128 | restore_error_handler(); 129 | throw new FtpException('Not connected to FTP server. Call connect() or ssl_connect() first.'); 130 | 131 | } else { 132 | if ($func === 'ftp_login' || $func === 'ftp_pasv') { 133 | $this->state[$name] = $args; 134 | } 135 | 136 | array_unshift($args, $this->resource); 137 | $res = $func(...$args); 138 | 139 | if ($func === 'ftp_chdir' || $func === 'ftp_cdup') { 140 | $this->state['chdir'] = [ftp_pwd($this->resource)]; 141 | } 142 | } 143 | 144 | restore_error_handler(); 145 | if (!$silent && $errorMsg !== null) { 146 | if (ini_get('html_errors')) { 147 | $errorMsg = html_entity_decode(strip_tags($errorMsg)); 148 | } 149 | 150 | if (($a = strpos($errorMsg, ': ')) !== false) { 151 | $errorMsg = substr($errorMsg, $a + 2); 152 | } 153 | 154 | throw new FtpException($errorMsg); 155 | } 156 | 157 | return $res; 158 | } 159 | 160 | 161 | /** 162 | * Reconnects to FTP server. 163 | */ 164 | public function reconnect(): void 165 | { 166 | @ftp_close($this->resource); // intentionally @ 167 | foreach ($this->state as $name => $args) { 168 | [$this, $name](...$args); 169 | } 170 | } 171 | 172 | 173 | /** 174 | * Checks if file or directory exists. 175 | */ 176 | public function fileExists(string $file): bool 177 | { 178 | return (bool) $this->nlist($file); 179 | } 180 | 181 | 182 | /** 183 | * Checks if directory exists. 184 | */ 185 | public function isDir(string $dir): bool 186 | { 187 | $current = $this->pwd(); 188 | try { 189 | $this->chdir($dir); 190 | } catch (FtpException $e) { 191 | } 192 | $this->chdir($current); 193 | return empty($e); 194 | } 195 | 196 | 197 | /** 198 | * Recursive creates directories. 199 | */ 200 | public function mkDirRecursive(string $dir): void 201 | { 202 | $parts = explode('/', $dir); 203 | $path = ''; 204 | while (!empty($parts)) { 205 | $path .= array_shift($parts); 206 | try { 207 | if ($path !== '') { 208 | $this->mkdir($path); 209 | } 210 | } catch (FtpException $e) { 211 | if (!$this->isDir($path)) { 212 | throw new FtpException("Cannot create directory '$path'."); 213 | } 214 | } 215 | $path .= '/'; 216 | } 217 | } 218 | 219 | 220 | /** 221 | * Recursive deletes path. 222 | */ 223 | public function deleteRecursive(string $path): void 224 | { 225 | if (!$this->tryDelete($path)) { 226 | foreach ((array) $this->nlist($path) as $file) { 227 | if ($file !== '.' && $file !== '..') { 228 | $this->deleteRecursive(!str_contains($file, '/') ? "$path/$file" : $file); 229 | } 230 | } 231 | $this->rmdir($path); 232 | } 233 | } 234 | } 235 | 236 | 237 | 238 | class FtpException extends Exception 239 | { 240 | } 241 | --------------------------------------------------------------------------------