PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.0.0
Backup Migration v2.0.0
2.1.7 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 10 months ago controller.php 10 months ago dropbox.php 10 months ago ftp.php 10 months ago google-drive.php 10 months ago s3.php 10 months ago
ftp.php
709 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 /**
133 * checkForBackupsToUpload - Will check for backups that requires to be in sync with cloud
134 *
135 * @return string[]
136 */
137 public function checkForBackupsToUpload()
138 {
139 $isEnabled = Dashboard\bmi_get_config('STORAGE::EXTERNAL::FTP');
140 if (!($isEnabled === true || $isEnabled === 'true')) {
141 update_option('bmip_to_be_uploaded', ['current_upload' => [], 'queue' => []]);
142 return [];
143 }
144
145 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'scanner' . DIRECTORY_SEPARATOR . 'backups.php';
146
147 // Upload Object
148 $requiresUpload = get_option('bmip_to_be_uploaded', [
149 'current_upload' => [],
150 'queue' => [],
151 'failed' => []
152 ]);
153
154 // Local Backups
155 $backups = new Backups();
156 $backupsAvailable = $backups->getAvailableBackups("local");
157 $localBackups = $backupsAvailable['local'];
158 $localBackups = array_reverse($localBackups);
159
160 // FTP Drive
161 $ftpFailed = false;
162 $ftpBackups = $this->getFtpBackups();
163
164 if ($ftpBackups && isset($ftpBackups['data'])) {
165 $ftpParsed = $this->parseFtpFiles($ftpBackups['data']);
166 } else {
167 return ['status' => 'error']; //Don't requeue FTP if the fetching fails
168 }
169
170 $backupsFiles = isset($ftpParsed['zipFiles']) ? $ftpParsed['zipFiles'] : [];
171 $manifestFiles = isset($ftpParsed['jsonFiles']) ? $ftpParsed['jsonFiles'] : [];
172 $availableManifests = array_column($manifestFiles, 'name');
173 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
174
175 foreach($localBackups as $name => $details) {
176 $md5 = $details[7];
177 if (isset($uploadedBackupStatus[$md5]) && isset($uploadedBackupStatus[$md5]['ftp'])) {
178 continue;
179 }
180 $isBackupNotExists = !in_array($md5 . '.json', $availableManifests) || !in_array($name, array_column($backupsFiles, 'name'));
181 if ($isBackupNotExists && !(isset($requiresUpload['current_upload']['task']) && $requiresUpload['current_upload']['task'] == 'ftp_' . $md5)) {
182 $requiresUpload['queue']['ftp_' . $md5] = [
183 'name' => $name,
184 'md5' => $md5,
185 'json' => $md5 . '.json'
186 ];
187
188 //As it gets queued again remove any failed tasks
189 if (isset($requiresUpload['failed']['ftp_' . $md5])) unset($requiresUpload['failed']['ftp_' . $md5]);
190 }
191 }
192
193 update_option('bmip_to_be_uploaded', $requiresUpload);
194 return ['status' => 'success'];
195 }
196
197 /**
198 * parseFtpFiles - Parses FTp Drive output files
199 *
200 * @param object $files FTP Files of Return
201 *
202 * @return array of parsed files
203 */
204 public function parseFtpFiles(&$files)
205 {
206 $login_result = $this->ftpConnect();
207 if ($login_result === false) {
208 return [];
209 }
210
211 $parsedFiles = [];
212 $zipFiles = [];
213 $jsonFiles = [];
214 foreach ($files as $index => $file) {
215
216 if ($file['type'] !== 'file') continue;
217
218 $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
219
220 if (in_array($ext, ['zip', 'gz', 'tar'])) {
221 $zipFiles[] = $file;
222 } else if ($ext === 'json') {
223 $jsonFiles[] = $file;
224 }
225 }
226
227 return compact('zipFiles', 'jsonFiles');
228 }
229
230 /**
231 * getFtpBackups - Return list of FTP backups and their MD5s
232 *
233 * @return array|bool status
234 */
235 public function getFtpBackups()
236 {
237 // connect and login to FTP server
238 // Update FTP config
239 $ftpConnect = $this->ftpConnect();
240
241 if ($ftpConnect !== false) {
242 //Get detail files
243 $fileList = $this->_custom_ftp_list($ftpConnect, $this->ftp_access_dir);
244 ftp_close($ftpConnect);
245 return ['status' => 'success', 'data' => $fileList];
246 }
247 return false;
248 }
249
250 /**
251 * uploadManifestFile - It will upload manifest file into BMI directory on FTP
252 *
253 * @return array status
254 */
255 private function uploadManifestFile($mdf, $manifestPath)
256 {
257 $ftpConnect= $this->ftpConnect();
258
259 if ($ftpConnect !== false) {
260 ftp_chdir($ftpConnect, $this->ftp_access_dir);
261 if (ftp_put($ftpConnect, $mdf . '.json', $manifestPath, FTP_ASCII)) {
262 ftp_close($ftpConnect);
263 return ['status' => 'success', 'data' => 'ok'];
264 } else {
265 ftp_close($ftpConnect);
266 return ['status' => 'error', 'data' => 'error'];
267 }
268 }
269 return ['status' => 'error', 'data' => 'error'];
270 }
271
272 /**
273 * getFTPFileContents - Gets file body by filename ftp
274 *
275 * @return array status
276 */
277 public function getFtpFileContents($fileName)
278 {
279 $ftpConnect = $this->ftpConnect();
280 if ($ftpConnect === false){
281 return ['status' => 'error', 'data' => 'error'];
282 }
283
284 ftp_chdir($ftpConnect, $this->ftp_access_dir);
285
286 $temp_file = 'temp.json';
287
288 $file = ftp_get($ftpConnect, BMI_BACKUPS . DIRECTORY_SEPARATOR . $temp_file, $fileName, FTP_BINARY);
289 if ($file) {
290 $file = file_get_contents(BMI_BACKUPS . DIRECTORY_SEPARATOR . $temp_file);
291 }
292 ftp_close($ftpConnect);
293 return ['status' => 'success', 'data' => $file];
294 }
295
296 /**
297 * getFtpDriveFileMeta - Gets file meta data by FTP file ID
298 *
299 * @return array|bool status
300 */
301 public function getFtpDriveFileMeta($fileName)
302 {
303 $ftpConnect = $this->ftpConnect();
304 if ($ftpConnect !== false) {
305 $fileList = $this->_custom_ftp_list($ftpConnect, $this->ftp_access_dir);
306 foreach($fileList as $file) {
307 if($file['name'] === $fileName) {
308 ftp_close($ftpConnect);
309 return ['status' => 'success', 'data' => $file];
310 }
311 }
312 ftp_close($ftpConnect);
313 }
314 return false;
315 }
316
317 /**
318 * deleteFtpDriveBackup - Deletes Backup from FTP
319 *
320 * @return bool status
321 */
322 public function deleteFtpDriveBackup($md5)
323 {
324 $files = $this->getFtpBackups();
325
326 if (isset($files['status']) && $files['status'] === 'success') {
327 $files = $files['data'];
328 foreach ($files as $index => $file) {
329 if ($file['name'] === $md5 . '.json') {
330 $content = $this->getFtpFileContents($file['name']);
331 if (!$content['data']) {
332 return false;
333 }
334
335 $data = json_decode($content['data']);
336 if (!isset($data->name)) {
337 return false;
338 }
339
340 $this->deleteFileFtp($data->name);
341 }
342 }
343 return true;
344 }
345 return false;
346 }
347
348 /**
349 * deleteFtpJson - Deletes JSON manifest from FTP
350 *
351 * @return bool status
352 */
353 public function deleteFtpJson($fileName)
354 {
355 $files = $this->getFtpBackups();
356 if (isset($files['status']) && $files['status'] === 'success') {
357 $files = $files['data'];
358 foreach ($files as $index => $file) {
359 if ($file['name'] === $fileName) {
360 $this->deleteFileFtp($fileName);
361 return true;
362 }
363 }
364 }
365 return false;
366 }
367
368 public function getFtpDriveFileContents($fileName, $startRange, $endRange) {
369 $remote_file = $this->ftp_access_dir . DIRECTORY_SEPARATOR . $fileName;
370
371 $ftp_username = urlencode($this->ftp_access_username);
372 $ftp_password = urlencode($this->ftp_access_password);
373 $ftp_port = $this->ftp_access_port;
374 $ftp_server = $this->ftp_access_host;
375
376 $ch = curl_init();
377
378 $url = "ftp://{$ftp_username}:{$ftp_password}@{$ftp_server}:{$ftp_port}/{$remote_file}";
379
380 curl_setopt($ch, CURLOPT_URL, $url);
381 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
382 curl_setopt($ch, CURLOPT_RANGE, "$startRange-$endRange");
383 curl_setopt($ch, CURLOPT_NOPROGRESS, true);
384 curl_setopt($ch, CURLOPT_TIMEOUT, 30);
385
386 $data = curl_exec($ch);
387
388 if(curl_errno($ch)) {
389 $error_message = curl_error($ch);
390 curl_close($ch);
391 error_log('cURL error: ' . $error_message);
392 return false;
393 }
394
395 curl_close($ch);
396
397 return ['status' => 'success', 'data' => $data];
398 }
399
400 /**
401 * uploadFtpDriveFiles - It will upload particular file into BMI directory on FTP
402 *
403 * @return array status
404 */
405 public function uploadFtpDriveFiles($uploadURL, $filePath, $manifestPath, $md5, $batch, $bytesPerRequest)
406 {
407 if (!file_exists($filePath)) {
408 update_option('bmip_to_be_uploaded', [
409 'current_upload' => [],
410 'queue' => [],
411 'failed' => []
412 ]);
413
414 return ['status' => 'error'];
415 }
416
417 set_transient('bmip_upload_ongoing', '1', 31);
418
419 $batchNumber = intval($batch);
420 $maxLength = filesize($filePath);
421
422 $toBeUploaded = get_option('bmip_to_be_uploaded', false);
423
424 //Check Exist file in ftp
425 $exist = $this->isExist(basename($filePath), $maxLength);
426
427
428 if ($exist) {
429 $manifestRes = $this->uploadManifestFile($md5, $manifestPath);
430 if ($manifestRes['status'] === 'success') {
431 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
432 if (!isset($uploadedBackupStatus[$md5])) {
433 $uploadedBackupStatus[$md5] = [];
434 }
435 $uploadedBackupStatus[$md5]['ftp'] = true;
436 update_option('bmi_uploaded_backups_status', $uploadedBackupStatus);
437 }
438
439 $task = $toBeUploaded['current_upload']['task'];
440 $toBeUploaded['current_upload'] = [];
441 if (!isset($toBeUploaded['failed'])) {
442 $toBeUploaded['failed'] = [];
443 }
444
445 if (isset($toBeUploaded['failed'][$task])) {
446 unset($toBeUploaded['failed'][$task]);
447 }
448
449 update_option('bmip_to_be_uploaded', $toBeUploaded);
450 return ['status' => 'success', 'data' => []];
451 }
452
453 $chunkSize = 256 * 1024 * 4 * intval($bytesPerRequest / 1024 / 1024);
454
455 $chunkOffset = (($batchNumber - 1) * $chunkSize);
456 $rangeEnd = (($chunkSize * $batchNumber) - 1);
457
458 if ($rangeEnd >= $maxLength) {
459 $rangeEnd = $maxLength - 1;
460 }
461 if (($chunkSize + $chunkOffset) >= $maxLength) {
462 $chunkSize = $rangeEnd - $chunkOffset + 1;
463 }
464 $nextShouldStartAt = $rangeEnd + 1;
465
466 if ($stream = fopen($filePath, 'r')) {
467 if ($maxLength > $chunkOffset) {
468 $binaryData = stream_get_contents($stream, $chunkSize, $chunkOffset);
469 }
470 fclose($stream);
471 }
472
473
474 $ftpConnect = $this->ftpConnect();
475
476 if ($ftpConnect !== false) {
477 // Login to FTP server
478
479 // Change directory to FTP directory
480 ftp_chdir($ftpConnect, $this->ftp_access_dir);
481
482 // Open local file for reading
483 $localFile = fopen($filePath, 'ab');
484 $remote_file = basename($filePath);
485 $user = $this->ftp_access_username;
486 $pass = $this->ftp_access_password;
487 $ftp_host = $this->ftp_access_host;
488 $ftpDirectory = $this->ftp_access_dir;
489 $ftp_port = $this->ftp_access_port;
490
491 $remote_handle = fopen("ftp://$user:$pass@$ftp_host:$ftp_port/$ftpDirectory/$remote_file", 'ab');
492
493 if ($localFile) {
494 fseek($localFile, $chunkOffset);
495 $upload = fwrite($remote_handle, $binaryData);
496
497 // Check if upload was successful
498 if ($upload) {
499
500 $task = $toBeUploaded['current_upload']['task'];
501 if (!isset($toBeUploaded['failed'])) {
502 $toBeUploaded['failed'] = [];
503 }
504
505 if (isset($toBeUploaded['failed'][$task])) {
506 unset($toBeUploaded['failed'][$task]);
507 }
508
509 // All is good
510 if ($toBeUploaded) {
511 $toBeUploaded['current_upload']['batch'] = intval($batch) + 1;
512 $toBeUploaded['current_upload']['progress'] = number_format(($rangeEnd / $maxLength) * 100, 2) . '%';
513 update_option('bmip_to_be_uploaded', $toBeUploaded);
514 }
515
516 // Check finished
517 if ($chunkSize + $chunkOffset >= $maxLength) {
518
519 $manifestRes = $this->uploadManifestFile($md5, $manifestPath);
520 if ($manifestRes['status'] === 'success') {
521 $uploadedBackupStatus = get_option('bmi_uploaded_backups_status', []);
522 if (!isset($uploadedBackupStatus[$md5])) {
523 $uploadedBackupStatus[$md5] = [];
524 }
525 $uploadedBackupStatus[$md5]['ftp'] = true;
526 update_option('bmi_uploaded_backups_status', $uploadedBackupStatus);
527 }
528
529 $task = $toBeUploaded['current_upload']['task'];
530 $toBeUploaded['current_upload'] = [];
531 if (!isset($toBeUploaded['failed'])) {
532 $toBeUploaded['failed'] = [];
533 }
534
535 if (isset($toBeUploaded['failed'][$task])) {
536 unset($toBeUploaded['failed'][$task]);
537 }
538
539 update_option('bmip_to_be_uploaded', $toBeUploaded);
540 }
541 } else {
542 $this->errorFtp($toBeUploaded, 'Error uploading file to FTP server!');
543 }
544
545 // Close local file
546 fclose($localFile);
547 } else {
548 $this->errorFtp($toBeUploaded, 'Error opening local file!');
549 }
550
551 ftp_close($ftpConnect);
552 } else {
553 $this->errorFtp($toBeUploaded, 'Error connecting to FTP server!');
554 }
555 delete_transient('bmip_upload_ongoing');
556 return ['status' => 'success', 'data' => []];
557 }
558
559 public function errorFtp($toBeUploaded, $message)
560 {
561 Logger::error('[BMI PRO] Error during file upload (FTP) Message:' . $message . '!');
562
563 $task = $toBeUploaded['current_upload']['task'];
564 // Requeueing is handled globally
565 // $toBeUploaded['queue'][$task] = [
566 // 'name' => $toBeUploaded['current_upload']['name'],
567 // 'md5' => $toBeUploaded['current_upload']['md5'],
568 // 'json' => $toBeUploaded['current_upload']['json']
569 // ];
570
571 $toBeUploaded['current_upload'] = [];
572 if (!isset($toBeUploaded['failed'])) {
573 $toBeUploaded['failed'] = [];
574 }
575 if (isset($toBeUploaded['failed'][$task])) {
576 $toBeUploaded['failed'][$task]++;
577 } else {
578 $toBeUploaded['failed'][$task] = 1;
579 }
580
581 update_option('bmip_to_be_uploaded', $toBeUploaded);
582 }
583
584 public function uploadFTPDriveFile($fileName)
585 {
586 // Check enable FTP option
587 $isFtpEnable = Dashboard\bmi_get_config('STORAGE::EXTERNAL::FTP');
588
589 if ($isFtpEnable !== true && $isFtpEnable !== 'true') {
590 return ['status' => 'error'];
591 }
592
593 $toBeUploaded = get_option('bmip_to_be_uploaded', false);
594
595 $storageLocalPath = sanitize_text_field(bmi_get_config('STORAGE::LOCAL::PATH'));
596
597 $backupFile = $storageLocalPath . DIRECTORY_SEPARATOR . 'backups' . DIRECTORY_SEPARATOR . $fileName['filename'];
598
599 $ftpConnect = $this->ftpConnect();
600 if ($ftpConnect === false) {
601 return ['status' => 'error'];
602 }
603
604 $fileSize = filesize($backupFile);
605
606 $latest = BMI_BACKUPS . '/latest.log';
607 $latest_progress = BMI_BACKUPS . '/latest_progress.log';
608
609 if ($upload = ftp_nb_put($ftpConnect, DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR . $fileName['filename'], $backupFile, FTP_BINARY)) {
610 while ($upload !== FTP_FINISHED && $upload !== FTP_FAILED) {
611 $bytesUploaded = ftp_size($ftpConnect, $fileName['filename']);
612
613 if ($bytesUploaded > 0 && $fileSize > 0) {
614 $progress = ($bytesUploaded / $fileSize) * 100;
615
616 error_log("Upload progress: " . round($progress, 2) . "%\n");
617
618 if ($toBeUploaded) {
619 // $toBeUploaded['current_upload']['batch'] = intval($batch) + 1;
620 $toBeUploaded['current_upload']['progress'] = round($progress, 2) . '%';
621 update_option('bmip_to_be_uploaded', $toBeUploaded);
622 }
623
624 $progress1 = fopen($latest_progress, 'w');
625
626 if (!$progress1){
627 update_option('bmip_to_be_uploaded', [
628 'current_upload' => [],
629 'queue' => [],
630 'failed' => []
631 ]);
632
633 ftp_close($ftpConnect);
634 return ['status' => 'error'];
635 }
636 fwrite($progress1, $progress);
637 fclose($progress1);
638
639 Logger::append('Step', "Upload progress: " . round($progress, 2) . "%\n");
640 }
641
642 $upload = ftp_nb_continue($ftpConnect);
643 }
644
645 ftp_close($ftpConnect);
646 if ($upload === FTP_FAILED) {
647 return ['status' => 'error'];
648 } else {
649 return true;
650 }
651 }
652 return ['status' => 'error'];
653
654 }
655
656 private function deleteFileFtp($fileName)
657 {
658 $remote_file = $fileName;
659 $ftpConnect = $this->ftpConnect();
660
661 if ($ftpConnect === false) {
662 return false;
663 }
664
665 ftp_chdir($ftpConnect, $this->ftp_access_dir);
666
667 if (ftp_size($ftpConnect, $remote_file) !== -1) {
668 if (ftp_delete($ftpConnect, $remote_file)) {
669 ftp_close($ftpConnect);
670 return true;
671 } else {
672 ftp_close($ftpConnect);
673 return false;
674 }
675 } else {
676 ftp_close($ftpConnect);
677 return false;
678 }
679 }
680
681 /**
682 * Check file is exist in the ftp
683 *
684 * @param $fileName
685 * @param $fileSize
686 * @return bool|void
687 */
688 private function isExist($fileName, $fileSize)
689 {
690 $ftpConnect = $this->ftpConnect();
691 if ($ftpConnect === false) {
692 return false;
693 }
694
695 $contents_on_server = ftp_nlist($ftpConnect,$this->ftp_access_dir);
696
697 foreach ($contents_on_server as $fileOnServer) {
698 if ($fileName === basename($fileOnServer)) {
699 if (ftp_size($ftpConnect, $fileOnServer) >= $fileSize) {
700 ftp_close($ftpConnect);
701 return true;
702 }
703 }
704 }
705 ftp_close($ftpConnect);
706 return false;
707 }
708 }
709