├── .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 | [![Downloads this Month](https://img.shields.io/packagist/dm/dg/ftp-php.svg)](https://packagist.org/packages/dg/ftp-php)
 5 | [![Latest Stable Version](https://poser.pugx.org/dg/ftp-php/v/stable)](https://github.com/dg/ftp-php/releases)
 6 | [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](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 | 


--------------------------------------------------------------------------------