PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.2
Backup Migration v2.1.2
2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / external / ftp.php
backup-backup / includes / external Last commit date
backupbliss.php 3 months ago controller.php 3 months ago dropbox.php 3 months ago ftp.php 3 months ago google-drive.php 3 months ago s3.php 3 months ago
ftp.php
722 lines
1 <?php
2
3 // Namespace
4 namespace BMI\Plugin\External;
5
6 // Use
7 use BMI\Plugin\Backup_Migration_Plugin as BMP;
8 use BMI\Plugin\BMI_Logger as Logger;
9 use BMI\Plugin\BMI_Pro_Core;
10 use BMI\Plugin\BMProAjax as BMProAjax;
11 use BMI\Plugin\Progress\BMI_MigrationProgress as MigrationProgress;
12 use BMI\Plugin\Scanner\BMI_BackupsScanner as Backups;
13 use BMI\Plugin\Dashboard as Dashboard;
14 use function BMI\Plugin\Dashboard\bmi_get_config;
15
16 // Exit on direct access
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * BMI_External_FTP
23 */
24 class BMI_External_FTP
25 {
26 private $ftp_access_username = false;
27 private $ftp_access_password = false;
28 private $ftp_access_host = false;
29 private $ftp_access_dir = false;
30 private $ftp_access_port = false;
31
32 public function __construct()
33 {
34 // Update FTP config
35 $this->ftp_access_host = get_option('bmi_pro_ftp_host');
36 $this->ftp_access_username = get_option('bmi_pro_ftp_username');
37 $this->ftp_access_password = get_option('bmi_pro_ftp_password');
38 $this->ftp_access_dir = get_option('bmi_pro_ftp_backup_dir');
39 $this->ftp_access_port = get_option('bmi_pro_ftp_port');
40
41 // Delete files
42 add_action('bmi_premium_remove_backup_file', [&$this, 'deleteFtpDriveBackup']);
43 add_action('bmi_premium_remove_backup_json_file', [&$this, 'deleteFtpJson']);
44 }
45
46 private function _custom_ftp_list($ftp_connection, $directory) {
47 // Get the list of files and directories
48 $file_list = ftp_nlist($ftp_connection, $directory);
49
50 $result = [];
51
52 if ($file_list === false) {
53 return $result; // Error occurred during listing
54 }
55
56 foreach ($file_list as $file) {
57 $item_path = $file;
58
59 // Get the file size
60 $size = ftp_size($ftp_connection, $item_path);
61
62 // Only include files (ignore directories)
63 if ($size >= 0) { // Size >= 0 indicates a file
64 $result[] = [
65 'name' => basename($file),
66 'size' => $size,
67 'type' => 'file'
68 ];
69 }
70 }
71
72 return $result;
73 }
74
75 public function get_host() {
76 return $this->ftp_access_host;
77 }
78
79 public function get_user_name() {
80 return $this->ftp_access_username;
81 }
82
83 public function get_password() {
84 return $this->ftp_access_password;
85 }
86
87 public function get_dir() {
88 return $this->ftp_access_dir;
89 }
90
91 public function get_port() {
92 return $this->ftp_access_port;
93 }
94
95 /**
96 * ftpConnect - Connects to FTP Server
97 *
98 * @return bool|\FTP\Connection
99 */
100 public function ftpConnect()
101 {
102 $ftp_server = $this->ftp_access_host;
103 $ftp_username = $this->ftp_access_username;
104 $ftp_password = $this->ftp_access_password;
105 $ftp_port = $this->ftp_access_port;
106
107 if (!$ftp_server || !$ftp_username || !$ftp_password) {
108 return false;
109 }
110
111 if (!function_exists('ftp_connect')) {
112 return false;
113 }
114
115 $ftp_conn = ftp_connect($ftp_server, $ftp_port);
116
117 if (!$ftp_conn) {
118 return false;
119 }
120
121 $login = ftp_login($ftp_conn, $ftp_username, $ftp_password);
122 if ($login) {
123 ftp_pasv($ftp_conn, true);
124 } else {
125 ftp_close($ftp_conn);
126 return false;
127 }
128
129 return $ftp_conn;
130 }
131
132 public function verifyConnection()
133 {
134 $conn_id = $this->ftpConnect();
135
136 $res = false;
137 if ($conn_id !== false) {
138 $res = 'connected';
139 }
140
141 return ['status' => 'success', 'result' => $res];
142
143 }
144
145 /**
146 * checkForBackupsToUpload - Will check for backups that requires to be in sync with cloud
147 *
148 * @return string[]
149 */
150 public function checkForBackupsToUpload()
151 {
152 $isEnabled = Dashboard\bmi_get_config('STORAGE::EXTERNAL::FTP');
153 if (!($isEnabled === true || $isEnabled === 'true')) {
154 update_option('bmip_to_be_uploaded', ['current_upload' => [], 'queue' => []]);
155 return [];
156 }
157
158 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'scanner' . DIRECTORY_SEPARATOR . 'backups.php';
159
160 // Upload Object
161 $requiresUpload = get_option('bmip_to_be_uploaded', [
162 'current_upload' => [],
163 'queue' => [],
164 'failed' => []
165 ]);
166
167 // Local Backups
168 $backups = new Backups();
169 $backupsAvailable = $backups->getAvailableBackups("local");
170 $localBackups = $backupsAvailable['local'];
171 $localBackups = array_reverse($localBackups);
172
173 // FTP Drive
174 $ftpFailed = false;
175 $ftpBackups = $this->getFtpBackups();
176
177 if ($ftpBackups && isset($ftpBackups['data'])) {
178 $ftpParsed = $this->parseFtpFiles($ftpBackups['data']);
179 } else {
180 return ['status' => 'error']; //Don't requeue FTP if the fetching fails
181 }
182
183 $backupsFiles = isset($ftpParsed['zipFiles']) ? $ftpParsed['zipFiles'] : [];
184 $manifestFiles = isset($ftpParsed['jsonFiles']) ? $ftpParsed['jsonFiles'] : [];
185 $availableManifests = array_column($manifestFiles, 'name');
186 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
187
188 foreach($localBackups as $name => $details) {
189 $md5 = $details[7];
190 if (isset($uploadedBackupStatus[$md5]) && isset($uploadedBackupStatus[$md5]['ftp'])) {
191 continue;
192 }
193 $isBackupNotExists = !in_array($md5 . '.json', $availableManifests) || !in_array($name, array_column($backupsFiles, 'name'));
194 if ($isBackupNotExists && !(isset($requiresUpload['current_upload']['task']) && $requiresUpload['current_upload']['task'] == 'ftp_' . $md5)) {
195 $requiresUpload['queue']['ftp_' . $md5] = [
196 'name' => $name,
197 'md5' => $md5,
198 'json' => $md5 . '.json'
199 ];
200
201 //As it gets queued again remove any failed tasks
202 if (isset($requiresUpload['failed']['ftp_' . $md5])) unset($requiresUpload['failed']['ftp_' . $md5]);
203 }
204 }
205
206 update_option('bmip_to_be_uploaded', $requiresUpload);
207 return ['status' => 'success'];
208 }
209
210 /**
211 * parseFtpFiles - Parses FTp Drive output files
212 *
213 * @param object $files FTP Files of Return
214 *
215 * @return array of parsed files
216 */
217 public function parseFtpFiles(&$files)
218 {
219 $login_result = $this->ftpConnect();
220 if ($login_result === false) {
221 return [];
222 }
223
224 $parsedFiles = [];
225 $zipFiles = [];
226 $jsonFiles = [];
227 foreach ($files as $index => $file) {
228
229 if ($file['type'] !== 'file') continue;
230
231 $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
232
233 if (in_array($ext, ['zip', 'gz', 'tar'])) {
234 $zipFiles[] = $file;
235 } else if ($ext === 'json') {
236 $jsonFiles[] = $file;
237 }
238 }
239
240 return compact('zipFiles', 'jsonFiles');
241 }
242
243 /**
244 * getFtpBackups - Return list of FTP backups and their MD5s
245 *
246 * @return array|bool status
247 */
248 public function getFtpBackups()
249 {
250 // connect and login to FTP server
251 // Update FTP config
252 $ftpConnect = $this->ftpConnect();
253
254 if ($ftpConnect !== false) {
255 //Get detail files
256 $fileList = $this->_custom_ftp_list($ftpConnect, $this->ftp_access_dir);
257 ftp_close($ftpConnect);
258 return ['status' => 'success', 'data' => $fileList];
259 }
260 return false;
261 }
262
263 /**
264 * uploadManifestFile - It will upload manifest file into BMI directory on FTP
265 *
266 * @return array status
267 */
268 private function uploadManifestFile($mdf, $manifestPath)
269 {
270 $ftpConnect= $this->ftpConnect();
271
272 if ($ftpConnect !== false) {
273 ftp_chdir($ftpConnect, $this->ftp_access_dir);
274 if (ftp_put($ftpConnect, $mdf . '.json', $manifestPath, FTP_ASCII)) {
275 ftp_close($ftpConnect);
276 return ['status' => 'success', 'data' => 'ok'];
277 } else {
278 ftp_close($ftpConnect);
279 return ['status' => 'error', 'data' => 'error'];
280 }
281 }
282 return ['status' => 'error', 'data' => 'error'];
283 }
284
285 /**
286 * getFTPFileContents - Gets file body by filename ftp
287 *
288 * @return array status
289 */
290 public function getFtpFileContents($fileName)
291 {
292 $ftpConnect = $this->ftpConnect();
293 if ($ftpConnect === false){
294 return ['status' => 'error', 'data' => 'error'];
295 }
296
297 ftp_chdir($ftpConnect, $this->ftp_access_dir);
298
299 $temp_file = 'temp.json';
300
301 $file = ftp_get($ftpConnect, BMI_BACKUPS . DIRECTORY_SEPARATOR . $temp_file, $fileName, FTP_BINARY);
302 if ($file) {
303 $file = file_get_contents(BMI_BACKUPS . DIRECTORY_SEPARATOR . $temp_file);
304 }
305 ftp_close($ftpConnect);
306 return ['status' => 'success', 'data' => $file];
307 }
308
309 /**
310 * getFtpDriveFileMeta - Gets file meta data by FTP file ID
311 *
312 * @return array|bool status
313 */
314 public function getFtpDriveFileMeta($fileName)
315 {
316 $ftpConnect = $this->ftpConnect();
317 if ($ftpConnect !== false) {
318 $fileList = $this->_custom_ftp_list($ftpConnect, $this->ftp_access_dir);
319 foreach($fileList as $file) {
320 if($file['name'] === $fileName) {
321 ftp_close($ftpConnect);
322 return ['status' => 'success', 'data' => $file];
323 }
324 }
325 ftp_close($ftpConnect);
326 }
327 return false;
328 }
329
330 /**
331 * deleteFtpDriveBackup - Deletes Backup from FTP
332 *
333 * @return bool status
334 */
335 public function deleteFtpDriveBackup($md5)
336 {
337 $files = $this->getFtpBackups();
338
339 if (isset($files['status']) && $files['status'] === 'success') {
340 $files = $files['data'];
341 foreach ($files as $index => $file) {
342 if ($file['name'] === $md5 . '.json') {
343 $content = $this->getFtpFileContents($file['name']);
344 if (!$content['data']) {
345 return false;
346 }
347
348 $data = json_decode($content['data']);
349 if (!isset($data->name)) {
350 return false;
351 }
352
353 $this->deleteFileFtp($data->name);
354 }
355 }
356 return true;
357 }
358 return false;
359 }
360
361 /**
362 * deleteFtpJson - Deletes JSON manifest from FTP
363 *
364 * @return bool status
365 */
366 public function deleteFtpJson($fileName)
367 {
368 $files = $this->getFtpBackups();
369 if (isset($files['status']) && $files['status'] === 'success') {
370 $files = $files['data'];
371 foreach ($files as $index => $file) {
372 if ($file['name'] === $fileName) {
373 $this->deleteFileFtp($fileName);
374 return true;
375 }
376 }
377 }
378 return false;
379 }
380
381 public function getFtpDriveFileContents($fileName, $startRange, $endRange) {
382 $remote_file = $this->ftp_access_dir . DIRECTORY_SEPARATOR . $fileName;
383
384 $ftp_username = urlencode($this->ftp_access_username);
385 $ftp_password = urlencode($this->ftp_access_password);
386 $ftp_port = $this->ftp_access_port;
387 $ftp_server = $this->ftp_access_host;
388
389 $ch = curl_init();
390
391 $url = "ftp://{$ftp_username}:{$ftp_password}@{$ftp_server}:{$ftp_port}/{$remote_file}";
392
393 curl_setopt($ch, CURLOPT_URL, $url);
394 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
395 curl_setopt($ch, CURLOPT_RANGE, "$startRange-$endRange");
396 curl_setopt($ch, CURLOPT_NOPROGRESS, true);
397 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
398
399 $data = curl_exec($ch);
400
401 if(curl_errno($ch)) {
402 $error_message = curl_error($ch);
403 curl_close($ch);
404 error_log('cURL error: ' . $error_message);
405 return false;
406 }
407
408 curl_close($ch);
409
410 return ['status' => 'success', 'data' => $data];
411 }
412
413 /**
414 * uploadFtpDriveFiles - It will upload particular file into BMI directory on FTP
415 *
416 * @return array status
417 */
418 public function uploadFtpDriveFiles($uploadURL, $filePath, $manifestPath, $md5, $batch, $bytesPerRequest)
419 {
420 if (!file_exists($filePath)) {
421 update_option('bmip_to_be_uploaded', [
422 'current_upload' => [],
423 'queue' => [],
424 'failed' => []
425 ]);
426
427 return ['status' => 'error'];
428 }
429
430 set_transient('bmip_upload_ongoing', '1', 31);
431
432 $batchNumber = intval($batch);
433 $maxLength = filesize($filePath);
434
435 $toBeUploaded = get_option('bmip_to_be_uploaded', false);
436
437 //Check Exist file in ftp
438 $exist = $this->isExist(basename($filePath), $maxLength);
439
440
441 if ($exist) {
442 $manifestRes = $this->uploadManifestFile($md5, $manifestPath);
443 if ($manifestRes['status'] === 'success') {
444 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
445 if (!isset($uploadedBackupStatus[$md5])) {
446 $uploadedBackupStatus[$md5] = [];
447 }
448 $uploadedBackupStatus[$md5]['ftp'] = true;
449 update_option('bmi_uploaded_backups_status', $uploadedBackupStatus);
450 }
451
452 $task = $toBeUploaded['current_upload']['task'];
453 $toBeUploaded['current_upload'] = [];
454 if (!isset($toBeUploaded['failed'])) {
455 $toBeUploaded['failed'] = [];
456 }
457
458 if (isset($toBeUploaded['failed'][$task])) {
459 unset($toBeUploaded['failed'][$task]);
460 }
461
462 update_option('bmip_to_be_uploaded', $toBeUploaded);
463 return ['status' => 'success', 'data' => []];
464 }
465
466 $chunkSize = 256 * 1024 * 4 * intval($bytesPerRequest / 1024 / 1024);
467
468 $chunkOffset = (($batchNumber - 1) * $chunkSize);
469 $rangeEnd = (($chunkSize * $batchNumber) - 1);
470
471 if ($rangeEnd >= $maxLength) {
472 $rangeEnd = $maxLength - 1;
473 }
474 if (($chunkSize + $chunkOffset) >= $maxLength) {
475 $chunkSize = $rangeEnd - $chunkOffset + 1;
476 }
477 $nextShouldStartAt = $rangeEnd + 1;
478
479 if ($stream = fopen($filePath, 'r')) {
480 if ($maxLength > $chunkOffset) {
481 $binaryData = stream_get_contents($stream, $chunkSize, $chunkOffset);
482 }
483 fclose($stream);
484 }
485
486
487 $ftpConnect = $this->ftpConnect();
488
489 if ($ftpConnect !== false) {
490 // Login to FTP server
491
492 // Change directory to FTP directory
493 ftp_chdir($ftpConnect, $this->ftp_access_dir);
494
495 // Open local file for reading
496 $localFile = fopen($filePath, 'ab');
497 $remote_file = basename($filePath);
498 $user = $this->ftp_access_username;
499 $pass = $this->ftp_access_password;
500 $ftp_host = $this->ftp_access_host;
501 $ftpDirectory = $this->ftp_access_dir;
502 $ftp_port = $this->ftp_access_port;
503
504 $remote_handle = fopen("ftp://$user:$pass@$ftp_host:$ftp_port/$ftpDirectory/$remote_file", 'ab');
505
506 if ($localFile) {
507 fseek($localFile, $chunkOffset);
508 $upload = fwrite($remote_handle, $binaryData);
509
510 // Check if upload was successful
511 if ($upload) {
512
513 $task = $toBeUploaded['current_upload']['task'];
514 if (!isset($toBeUploaded['failed'])) {
515 $toBeUploaded['failed'] = [];
516 }
517
518 if (isset($toBeUploaded['failed'][$task])) {
519 unset($toBeUploaded['failed'][$task]);
520 }
521
522 // All is good
523 if ($toBeUploaded) {
524 $toBeUploaded['current_upload']['batch'] = intval($batch) + 1;
525 $toBeUploaded['current_upload']['progress'] = number_format(($rangeEnd / $maxLength) * 100, 2) . '%';
526 update_option('bmip_to_be_uploaded', $toBeUploaded);
527 }
528
529 // Check finished
530 if ($chunkSize + $chunkOffset >= $maxLength) {
531
532 $manifestRes = $this->uploadManifestFile($md5, $manifestPath);
533 if ($manifestRes['status'] === 'success') {
534 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
535 if (!isset($uploadedBackupStatus[$md5])) {
536 $uploadedBackupStatus[$md5] = [];
537 }
538 $uploadedBackupStatus[$md5]['ftp'] = true;
539 update_option('bmi_uploaded_backups_status', $uploadedBackupStatus);
540 }
541
542 $task = $toBeUploaded['current_upload']['task'];
543 $toBeUploaded['current_upload'] = [];
544 if (!isset($toBeUploaded['failed'])) {
545 $toBeUploaded['failed'] = [];
546 }
547
548 if (isset($toBeUploaded['failed'][$task])) {
549 unset($toBeUploaded['failed'][$task]);
550 }
551
552 update_option('bmip_to_be_uploaded', $toBeUploaded);
553 }
554 } else {
555 $this->errorFtp($toBeUploaded, 'Error uploading file to FTP server!');
556 }
557
558 // Close local file
559 fclose($localFile);
560 } else {
561 $this->errorFtp($toBeUploaded, 'Error opening local file!');
562 }
563
564 ftp_close($ftpConnect);
565 } else {
566 $this->errorFtp($toBeUploaded, 'Error connecting to FTP server!');
567 }
568 delete_transient('bmip_upload_ongoing');
569 return ['status' => 'success', 'data' => []];
570 }
571
572 public function errorFtp($toBeUploaded, $message)
573 {
574 Logger::error('[BMI PRO] Error during file upload (FTP) Message:' . $message . '!');
575
576 $task = $toBeUploaded['current_upload']['task'];
577 // Requeueing is handled globally
578 // $toBeUploaded['queue'][$task] = [
579 // 'name' => $toBeUploaded['current_upload']['name'],
580 // 'md5' => $toBeUploaded['current_upload']['md5'],
581 // 'json' => $toBeUploaded['current_upload']['json']
582 // ];
583
584 $toBeUploaded['current_upload'] = [];
585 if (!isset($toBeUploaded['failed'])) {
586 $toBeUploaded['failed'] = [];
587 }
588 if (isset($toBeUploaded['failed'][$task])) {
589 $toBeUploaded['failed'][$task]++;
590 } else {
591 $toBeUploaded['failed'][$task] = 1;
592 }
593
594 update_option('bmip_to_be_uploaded', $toBeUploaded);
595 }
596
597 public function uploadFTPDriveFile($fileName)
598 {
599 // Check enable FTP option
600 $isFtpEnable = Dashboard\bmi_get_config('STORAGE::EXTERNAL::FTP');
601
602 if ($isFtpEnable !== true && $isFtpEnable !== 'true') {
603 return ['status' => 'error'];
604 }
605
606 $toBeUploaded = get_option('bmip_to_be_uploaded', false);
607
608 $storageLocalPath = sanitize_text_field(bmi_get_config('STORAGE::LOCAL::PATH'));
609
610 $backupFile = $storageLocalPath . DIRECTORY_SEPARATOR . 'backups' . DIRECTORY_SEPARATOR . $fileName['filename'];
611
612 $ftpConnect = $this->ftpConnect();
613 if ($ftpConnect === false) {
614 return ['status' => 'error'];
615 }
616
617 $fileSize = filesize($backupFile);
618
619 $latest = BMI_BACKUPS . '/latest.log';
620 $latest_progress = BMI_BACKUPS . '/latest_progress.log';
621
622 if ($upload = ftp_nb_put($ftpConnect, DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR . $fileName['filename'], $backupFile, FTP_BINARY)) {
623 while ($upload !== FTP_FINISHED && $upload !== FTP_FAILED) {
624 $bytesUploaded = ftp_size($ftpConnect, $fileName['filename']);
625
626 if ($bytesUploaded > 0 && $fileSize > 0) {
627 $progress = ($bytesUploaded / $fileSize) * 100;
628
629 error_log("Upload progress: " . round($progress, 2) . "%\n");
630
631 if ($toBeUploaded) {
632 // $toBeUploaded['current_upload']['batch'] = intval($batch) + 1;
633 $toBeUploaded['current_upload']['progress'] = round($progress, 2) . '%';
634 update_option('bmip_to_be_uploaded', $toBeUploaded);
635 }
636
637 $progress1 = fopen($latest_progress, 'w');
638
639 if (!$progress1){
640 update_option('bmip_to_be_uploaded', [
641 'current_upload' => [],
642 'queue' => [],
643 'failed' => []
644 ]);
645
646 ftp_close($ftpConnect);
647 return ['status' => 'error'];
648 }
649 fwrite($progress1, $progress);
650 fclose($progress1);
651
652 Logger::append('Step', "Upload progress: " . round($progress, 2) . "%\n");
653 }
654
655 $upload = ftp_nb_continue($ftpConnect);
656 }
657
658 ftp_close($ftpConnect);
659 if ($upload === FTP_FAILED) {
660 return ['status' => 'error'];
661 } else {
662 return true;
663 }
664 }
665 return ['status' => 'error'];
666
667 }
668
669 private function deleteFileFtp($fileName)
670 {
671 $remote_file = $fileName;
672 $ftpConnect = $this->ftpConnect();
673
674 if ($ftpConnect === false) {
675 return false;
676 }
677
678 ftp_chdir($ftpConnect, $this->ftp_access_dir);
679
680 if (ftp_size($ftpConnect, $remote_file) !== -1) {
681 if (ftp_delete($ftpConnect, $remote_file)) {
682 ftp_close($ftpConnect);
683 return true;
684 } else {
685 ftp_close($ftpConnect);
686 return false;
687 }
688 } else {
689 ftp_close($ftpConnect);
690 return false;
691 }
692 }
693
694 /**
695 * Check file is exist in the ftp
696 *
697 * @param $fileName
698 * @param $fileSize
699 * @return bool|void
700 */
701 private function isExist($fileName, $fileSize)
702 {
703 $ftpConnect = $this->ftpConnect();
704 if ($ftpConnect === false) {
705 return false;
706 }
707
708 $contents_on_server = ftp_nlist($ftpConnect,$this->ftp_access_dir);
709
710 foreach ($contents_on_server as $fileOnServer) {
711 if ($fileName === basename($fileOnServer)) {
712 if (ftp_size($ftpConnect, $fileOnServer) >= $fileSize) {
713 ftp_close($ftpConnect);
714 return true;
715 }
716 }
717 }
718 ftp_close($ftpConnect);
719 return false;
720 }
721 }
722