PluginProbe
JetBackup – Backup, Restore & Migrate / 3.1.23.5
JetBackup – Backup, Restore & Migrate v3.1.23.5
3.1.23.6 3.1.23.5 3.1.23.3 3.1.22.4 3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 All 82 releases
backup / src / JetBackup / Destination / Vendors / FTP / FTP.php
FTP.php
515 lines 14.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 *
4 * JetBackup @ package
5 * Created By Idan Ben-Ezra
6 *
7 * Copyrights @ JetApps
8 * https://www.jetapps.com
9 *
10 **/
11 namespace JetBackup\Destination\Vendors\FTP;
12
13 use Exception;
14 use JetBackup\Destination\DestinationFile;
15 use JetBackup\Destination\DestinationWrapper;
16 use JetBackup\Destination\Integration\DestinationChunkedDownload;
17 use JetBackup\Destination\Integration\DestinationChunkedUpload;
18 use JetBackup\Destination\Integration\DestinationDirIterator;
19 use JetBackup\Destination\Integration\DestinationDiskUsage;
20 use JetBackup\Destination\Integration\DestinationFile as iDestinationFile;
21 use JetBackup\Exception\ConnectionException;
22 use JetBackup\Exception\FieldsValidationException;
23 use JetBackup\Exception\IOException;
24
25 defined( '__JETBACKUP__' ) or die( 'Restricted access' );
26
27 class FTP extends DestinationWrapper {
28
29 const TYPE = 'FTP';
30
31 private ?FTPClient $_connection=null;
32
33 /**
34 * @return string[]
35 */
36 public function protectedFields():array { return ['password']; }
37
38 /**
39 * @param string $path
40 *
41 * @return string
42 */
43 public function getRealPath(string $path): string { return rtrim(parent::getRealPath($path), '/'); }
44
45 /**
46 * @return string
47 */
48 public function getServer():string { return $this->getOptions()->get('server'); }
49
50 /**
51 * @param string $server
52 *
53 * @return void
54 */
55 private function setServer(string $server):void { $this->getOptions()->set('server', trim($server)); }
56
57 /**
58 * @return int
59 */
60 public function getPort():int { return (int) $this->getOptions()->get('port', 21); }
61
62 /**
63 * @param int $port
64 *
65 * @return void
66 */
67 private function setPort(int $port):void { $this->getOptions()->set('port', $port); }
68
69 /**
70 * @return string
71 */
72 public function getUsername():string { return $this->getOptions()->get('username'); }
73
74 /**
75 * @param string $username
76 *
77 * @return void
78 */
79 private function setUsername(string $username):void { $this->getOptions()->set('username', $username); }
80
81 /**
82 * @return string
83 */
84 public function getPassword():string { return $this->getOptions()->get('password'); }
85
86 /**
87 * @param string $password
88 *
89 * @return void
90 */
91 private function setPassword(string $password):void { $this->getOptions()->set('password', $password); }
92
93 /**
94 * @return int
95 */
96 public function getConnectionTimeout():int { return (int) $this->getOptions()->get('timeout', 30); }
97
98 /**
99 * @param int $timeout
100 *
101 * @return void
102 */
103 private function setConnectionTimeout(int $timeout):void { $this->getOptions()->set('timeout', $timeout); }
104
105 /**
106 * @return int
107 */
108 public function getRetries():int { return (int) $this->getOptions()->get('retries', 0); }
109
110 /**
111 * @param int $retries
112 *
113 * @return void
114 */
115 private function setRetries(int $retries):void { $this->getOptions()->set('retries', $retries); }
116
117 /**
118 * @return bool
119 */
120 public function getSecureSSL():bool { return !!$this->getOptions()->get('ssl'); }
121 public function getIgnoreSelfSigned():bool { return !!$this->getOptions()->get('ignore_self_signed'); }
122
123 /**
124 * @param bool $secure
125 *
126 * @return void
127 */
128 private function setSecureSSL(bool $secure):void { $this->getOptions()->set('ssl', $secure); }
129
130 private function setIgnoreSelfSigned(bool $ignore_self_signed):void { $this->getOptions()->set('ignore_self_signed', $ignore_self_signed); }
131
132 /**
133 * @return bool
134 */
135 public function getPassive():bool { return !!$this->getOptions()->get('passive_mode'); }
136
137 /**
138 * @param bool $passive
139 *
140 * @return void
141 */
142 private function setPassive(bool $passive):void { $this->getOptions()->set('passive_mode', $passive); }
143
144 /**
145 * @return int
146 */
147 public function getPreferIp():int { return (int) $this->getOptions()->get('prefer_ip', 4); }
148
149 /**
150 * @param int $prefer_ip
151 *
152 * @return void
153 */
154 private function setPreferIp(int $prefer_ip):void { $this->getOptions()->set('prefer_ip', $prefer_ip); }
155
156 /**
157 * @return void
158 * @throws ConnectionException
159 */
160 public function connect():void {
161
162 if($this->_connection) return;
163
164 $connection = new FTPClient(
165 $this->getServer(),
166 $this->getUsername(),
167 $this->getPassword(),
168 $this->getPort(),
169 $this->getPassive(),
170 $this->getSecureSSL(),
171 $this->getConnectionTimeout(),
172 $this->getPreferIp(),
173 $this->getIgnoreSelfSigned()
174 );
175
176 try {
177 $connection->chdir('/');
178 } catch(Exception $e) {
179 throw new ConnectionException("Failed connecting FTP server {$this->getServer()} on port {$this->getPort()}. Error: {$e->getMessage()}");
180 }
181
182 $this->_connection = $connection;
183 }
184
185 /**
186 * @param string $function
187 * @param ...$args
188 *
189 * @return mixed
190 * @throws IOException
191 */
192 public function _client(string $function, ...$args) {
193 $waittime = 333000;
194 $tries = 0;
195
196 while(true) {
197 try {
198 $this->connect();
199 return $this->_connection->{$function}(...$args);
200 } catch(Exception $e) {
201 if($tries >= $this->getRetries() || $e->getCode() == 9 || $e->getCode() == FTPClient::CODE_TRUNCATED) throw new IOException($e->getMessage(), $e->getCode());
202 if ($waittime > 60000000) $waittime = 60000000;
203 usleep($waittime);
204 $waittime *= 2;
205 $tries++;
206 $this->getLogController()->logDebug("Retry $tries/{$this->getRetries()} {$e->getMessage()}");
207 }
208 }
209 }
210
211 /**
212 * @return void
213 */
214 public function disconnect():void {
215 if(!$this->_connection) return;
216 $this->_connection->close();
217 $this->_connection = null;
218 }
219
220 /**
221 * @return void
222 */
223 public function register():void {}
224
225 /**
226 * @return void
227 */
228 public function unregister():void {}
229
230 /**
231 * @return void
232 * @throws FieldsValidationException
233 */
234 public function validateFields():void {
235 if(!$this->getPath()) throw new FieldsValidationException("No path provided");
236 if(str_starts_with($this->getPath(), './')) throw new FieldsValidationException("Path cannot start with \"./\"");
237 if(!preg_match("/^[\/a-zA-Z0-9\-_.]+$/", $this->getPath())) throw new FieldsValidationException("Invalid path provided (Allowed characters A-Z a-z 0-9 _ - . and /)");
238
239 if(!$this->getServer()) throw new FieldsValidationException("No server address provided");
240 if(!$this->getPort()) throw new FieldsValidationException("No port provided");
241 if(!$this->getUsername()) throw new FieldsValidationException("No username provided");
242 if(!$this->getPassword()) throw new FieldsValidationException("No password provided");
243 if($this->getConnectionTimeout() < 10 || $this->getConnectionTimeout() > 300) throw new FieldsValidationException("Invalid connection timeout provided. valid value is between 10 to 300 seconds");
244 if($this->getRetries() < 0 || $this->getRetries() > 10) throw new FieldsValidationException("Invalid connection retries provided. valid value is between 0 to 10 retries");
245 if(!in_array($this->getPreferIp(), [0, 4, 6])) throw new FieldsValidationException("Invalid Prefer IP version provided. valid values are: 0 - for default route, 4 - for using IPv4, and 6 - for using IPv6");
246 }
247
248 /**
249 * @param object $data
250 *
251 * @return void
252 */
253 public function setData(object $data):void {
254 if(isset($data->server)) $this->setServer($data->server);
255 if(isset($data->port)) $this->setPort($data->port);
256 if(isset($data->username)) $this->setUsername($data->username);
257 if(isset($data->password)) $this->setPassword($data->password);
258 if(isset($data->timeout)) $this->setConnectionTimeout($data->timeout);
259 if(isset($data->retries)) $this->setRetries($data->retries);
260 if(isset($data->ssl)) $this->setSecureSSL($data->ssl);
261 if(isset($data->ignore_self_signed)) $this->setIgnoreSelfSigned($data->ignore_self_signed);
262 if(isset($data->passive_mode)) $this->setPassive($data->passive_mode);
263 if(isset($data->prefer_ip)) $this->setPreferIp($data->prefer_ip);
264 }
265
266 /**
267 * @return array
268 */
269 public function getData(): array {
270 return $this->getOptions()->getData();
271 }
272
273 /**
274 * @param string $directory
275 * @param ?string $data
276 *
277 * @return bool
278 * @throws IOException
279 */
280 public function dirExists(string $directory, ?string $data=null): bool {
281 $this->getLogController()->logDebug("[dirExists] Checking dirExists for $directory");
282
283 // Check the cache before listing the directory
284 if(preg_replace("#/+#", '/', $directory) == '/') return true;
285
286 $dir = $this->listDir(dirname($directory));
287 while($dir->hasNext()) {
288 $details = $dir->getNext();
289 if($details->getType() == iDestinationFile::TYPE_DIRECTORY && $details->getName() == basename($directory)) return true;
290 }
291 return false;
292 }
293
294 /**
295 * @param string $file
296 * @param ?string $data
297 *
298 * @return bool
299 * @throws IOException
300 */
301 public function fileExists(string $file, ?string $data=null): bool {
302 $this->getLogController()->logDebug("[fileExists] Checking file exists for $file");
303 return $this->_client('fileExists', $this->getRealPath($file));
304 }
305
306 /**
307 * @param string $directory
308 * @param bool $recursive
309 * @param ?string $data
310 *
311 * @return string|null
312 * @throws IOException
313 */
314 public function createDir(string $directory, bool $recursive, ?string $data=null):? string {
315 try {
316 $this->getLogController()->logDebug("[createDir] Creating folder $directory");
317 $this->_client('mkdir', $this->getRealPath($directory));
318 } catch(IOException $e) {
319 throw new IOException("Failed creating dir '{$this->getRealPath($directory)}'. Error: {$e->getMessage()}");
320 }
321
322 return null;
323 }
324
325 /**
326 * @param string $directory
327 * @param ?string $data
328 *
329 * @return void
330 * @throws IOException
331 */
332 public function removeDir(string $directory, ?string $data=null):void {
333
334 $this->getLogController()->logDebug("[removeDir] Removing $directory");
335 if(!$this->dirExists($directory)) return;
336
337 $list = $this->listDir($directory);
338
339 $stack = [];
340 $stack[] = [$list, $directory];
341
342 while (!empty($stack)) {
343 list($dir_iterator, $dir_path) = array_pop($stack);
344
345 while ($dir_iterator->hasNext()) {
346 $file = $dir_iterator->getNext();
347 $filename = $file->getName();
348 $path = sprintf("%s/%s", $dir_path, $filename);
349 if ($file->getType() == iDestinationFile::TYPE_DIRECTORY) {
350 // Add the current dir iterator to the stack and continue with the new dir iterator.
351 $stack[] = [$dir_iterator, $dir_path];
352 // Update the 'dir_path' to the new path and use 'listDir' to get a new dir iterator.
353 $dir_path = $path;
354 $dir_iterator = $this->listDir($dir_path);
355 }
356 else $this->removeFile($path);
357 }
358 try {
359 $this->_client('rmdir', $this->getRealPath($dir_path));
360 } catch(IOException $e) {
361 throw new IOException("Failed deleting dir '{$this->getRealPath($dir_path)}'. Error: {$e->getMessage()}");
362 }
363 }
364 }
365
366 /**
367 * @param string $source
368 * @param string $destination
369 * @param ?string $data
370 *
371 * @return void
372 * @throws IOException
373 */
374 public function copyFileToLocal(string $source, string $destination, ?string $data=null):void {
375 $this->getLogController()->logDebug("[copyFileToLocal] Checking destination: $destination");
376 if (file_exists($destination)) {
377 $this->getLogController()->logDebug("[copyFileToLocal] Destination exists already");
378 // The destination is a directory to copy the file to (using the original file name).
379 if (is_dir($destination)) {
380 $this->getLogController()->logDebug("[copyFileToLocal] Destination is a directory");
381 $destination .= "/" . basename($source);
382 }
383 } else if(!file_exists(dirname($destination)) &&
384 !@mkdir(dirname($destination), 0755, true))
385 throw new IOException("Failed creating local dir '" . dirname($destination) . "'");
386
387 if(!is_dir(dirname($destination))) throw new IOException("Local path '" . dirname($destination) . "' isn't a directory");
388
389 try {
390 $this->getLogController()->logDebug("[copyFileToLocal] Starting download {$this->getRealPath($source)} -> $destination");
391 $this->_client('download', $this->getRealPath($source), $destination);
392 } catch(IOException $e) {
393 throw new IOException("Failed downloading file '$source'. Error: {$e->getMessage()}");
394 }
395 }
396
397 /**
398 * @param string $source
399 * @param string $destination
400 * @param string|null $data
401 *
402 * @return DestinationChunkedDownload
403 * @throws ConnectionException
404 */
405 public function copyFileToLocalChunked( string $source, string $destination, ?string $data = null ): DestinationChunkedDownload {
406 $this->connect();
407 $source = $this->getRealPath($source);
408 $this->getLogController()->logDebug("[copyFileToRemote] $source -> $destination");
409 return new ChunkedDownload($this, $source, $destination);
410 }
411
412 /**
413 * @param string $source
414 * @param string $destination
415 * @param ?string $data
416 *
417 * @return string|null
418 * @throws IOException
419 */
420 public function copyFileToRemote(string $source, string $destination, ?string $data=null):? string {
421 try {
422 $destination = $this->getRealPath($destination);
423 $this->getLogController()->logDebug("[copyFileToRemote] $source -> $destination");
424 $this->_client('upload', $source, $destination);
425 } catch(IOException $e) {
426 throw new IOException("Failed uploading file '$source' to '{$destination}'. Error: {$e->getMessage()}");
427 }
428 return null;
429 }
430
431 /**
432 * @param string $source
433 * @param string $destination
434 * @param string|null $data
435 *
436 * @return DestinationChunkedUpload
437 */
438 public function copyFileToRemoteChunked( string $source, string $destination, ?string $data = null ): DestinationChunkedUpload {
439 $this->getLogController()->logDebug("[copyFileToRemoteChunked] $source -> $destination");
440 return new ChunkedUpload($this, $this->getRealPath($destination));
441 }
442
443 /**
444 * @param string $directory
445 * @param ?string $data
446 *
447 * @return DestinationDirIterator
448 * @throws IOException
449 */
450 public function listDir(string $directory, ?string $data=null): DestinationDirIterator {
451 $this->getLogController()->logDebug("[listDir] $directory");
452 return new DirIterator($this, $directory);
453 }
454
455 /**
456 * @param string $directory
457 *
458 * @return iDestinationFile[]
459 * @throws IOException
460 */
461 public function _listDir(string $directory): array {
462 $directory = $this->getRealPath($directory);
463 $this->getLogController()->logDebug("[_listDir] $directory");
464 return $this->_client('listDir', $directory);
465 }
466
467 /**
468 * @return DestinationDiskUsage|null
469 */
470 public function getDiskInfo():?DestinationDiskUsage { return null; }
471
472 /**
473 * @param string $file
474 * @param ?string $data
475 *
476 * @return void
477 * @throws IOException
478 */
479 public function removeFile(string $file, ?string $data=null):void {
480 try {
481 $file = $this->getRealPath($file);
482 $this->getLogController()->logDebug("[removeFile] $file");
483
484 $this->_client('delete', $file);
485 } catch(IOException $e) {
486 throw new IOException("Failed deleting file '$file'. Error: {$e->getMessage()}");
487 }
488 }
489
490 /**
491 * @param string $file
492 *
493 * @return iDestinationFile|null
494 * @throws IOException
495 */
496 public function getFileStat(string $file):?iDestinationFile {
497 $this->getLogController()->logDebug("[getFileStat] $file");
498 $dir = $this->listDir(dirname($file));
499
500 while($dir->hasNext()) {
501 $details = $dir->getNext();
502 if($details->getType() == DestinationFile::TYPE_FILE && $details->getName() == basename($file)) return $details;
503 }
504
505 return null;
506
507 }
508
509 /**
510 *
511 */
512 public function __destruct() {
513 $this->disconnect();
514 }
515 }