| @@ -18,8 +18,17 @@ | ||
| 18 | 18 | protected $bucket_name; |
| 19 | 19 | protected $region = ''; |
| 20 | 20 | |
| 21 | 21 | /** |
| 22 | + * Absolute paths restored from cloud (by any integration) pending removal | |
| 23 | + * again — fed into the pre-update pipeline if a save happens this request, | |
| 24 | + * with a shutdown fallback otherwise. See track_restored_for_cleanup(). | |
| 25 | + * @since 1.4.0 | |
| 26 | + */ | |
| 27 | + protected $pending_restored_files = []; | |
| 28 | + protected $pending_cleanup_hooked = false; | |
| 29 | + | |
| 30 | + /** | |
| 22 | 31 | * Admin constructor. |
| 23 | 32 | * @since 1.0.0 |
| 24 | 33 | */ |
| 25 | 34 | public function __construct() { |
| @@ -46,14 +55,10 @@ | ||
| 46 | 55 | $this->service = isset($this->credentials['service']) && !empty($this->credentials['service']) |
| 47 | 56 | ? $this->credentials['service'] |
| 48 | 57 | : ''; |
| 49 | 58 | |
| 50 | - if (isset($this->bucketConfig['bucket_name'])) { | |
| 51 | - $this->bucket_name = $this->bucketConfig['bucket_name']; | |
| 52 | - } | |
| 53 | - if (isset($this->config['region'])) { | |
| 54 | - $this->region = $this->config['region']; | |
| 55 | - } | |
| 59 | + $this->bucket_name = isset($this->bucketConfig['bucket_name']) ? $this->bucketConfig['bucket_name'] : ''; | |
| 60 | + $this->region = isset($this->config['region']) ? $this->config['region'] : ''; | |
| 56 | 61 | } |
| 57 | 62 | |
| 58 | 63 | /** |
| 59 | 64 | * Add Item In database |
| @@ -377,10 +382,13 @@ | ||
| 377 | 382 | if( |
| 378 | 383 | ($check_rewrite && (isset($this->settings['rewrite_url']) && $this->settings['rewrite_url'])) || |
| 379 | 384 | !$check_rewrite |
| 380 | 385 | ) { |
| 386 | + if ($check_rewrite) { | |
| 387 | + return (bool) apply_filters('wpmcs_is_available_from_provider', true, $attachment_id, $source_type); | |
| 388 | + } | |
| 381 | 389 | return true; |
| 382 | - } | |
| 390 | + } | |
| 383 | 391 | } |
| 384 | 392 | return false; |
| 385 | 393 | } |
| 386 | 394 | |
| @@ -613,9 +621,12 @@ | ||
| 613 | 621 | if ($privateUrl === false) { |
| 614 | 622 | $new_url = Service::instance()->get_private_url($key); |
| 615 | 623 | |
| 616 | 624 | if (!Utils::is_empty($new_url)) { |
| 617 | - $privateUrl = Cdn::may_generate_cdn_url($new_url, $key); | |
| 625 | + // No hook (Pro inactive, or the current delivery provider hasn't | |
| 626 | + // implemented one) means passthrough — same URL, unmodified. Real | |
| 627 | + // per-CDN rewriting (e.g. CloudFront signed URLs) is a Pro concern. | |
| 628 | + $privateUrl = apply_filters( 'wpmcs_generate_private_url', $new_url, $key ); | |
| 618 | 629 | $expireMinutes = (int)(isset($this->settings['private_url_expire']) && !empty($this->settings['private_url_expire'])) |
| 619 | 630 | ? $this->settings['private_url_expire'] |
| 620 | 631 | : 20; |
| 621 | 632 | $expireSeconds = $expireMinutes * 60; |
| @@ -645,11 +656,15 @@ | ||
| 645 | 656 | * @param string $size size of the file, default is full |
| 646 | 657 | * @param string $source_type source type of item, default is media_library |
| 647 | 658 | * @param bool $all if true, it will move all files to server |
| 648 | 659 | * @param bool $backup if true, it will move backup file to server |
| 660 | + * @param string $log_type error-log bucket to write to on failure — lets a caller other | |
| 661 | + * than the "Restore to Server" job (e.g. "Remove from Cloud", | |
| 662 | + * which also restores as a safety step) attribute failures to | |
| 663 | + * its own error list instead of Restore to Server's. | |
| 649 | 664 | * @return array an array of server file paths |
| 650 | 665 | */ |
| 651 | - public function moveToServer($source_id, $size = 'full', $source_type = 'media_library', $all = false, $backup = false){ | |
| 666 | + public function moveToServer($source_id, $size = 'full', $source_type = 'media_library', $all = false, $backup = false, $log_type = 'restore_to_server'){ | |
| 652 | 667 | $server_files = []; |
| 653 | 668 | $server_file = false; |
| 654 | 669 | $source_id = (int)$source_id; |
| 655 | 670 | $item = $this->get($source_id, $source_type); |
| @@ -654,12 +669,12 @@ | ||
| 654 | 669 | $source_id = (int)$source_id; |
| 655 | 670 | $item = $this->get($source_id, $source_type); |
| 656 | 671 | |
| 657 | 672 | // Remove log if exists before move to server |
| 658 | - Logger::instance()->remove_log('restore_to_server', $source_id, $source_type); | |
| 673 | + Logger::instance()->remove_log($log_type, $source_id, $source_type); | |
| 659 | 674 | |
| 660 | 675 | if ( isset($item) && !empty($item) ) { |
| 661 | - $files = $this->moveToServerByItem($item, $size, $all); | |
| 676 | + $files = $this->moveToServerByItem($item, $size, $all, $log_type); | |
| 662 | 677 | if (isset($files) && !empty($files)) { |
| 663 | 678 | $server_files = $all ? array_merge($server_files, $files) : $files; |
| 664 | 679 | } |
| 665 | 680 | } |
| @@ -665,9 +680,9 @@ | ||
| 665 | 680 | } |
| 666 | 681 | if( $all && $backup ) { |
| 667 | 682 | $backupItem = $this->get_backup($source_id, $source_type); |
| 668 | 683 | if (isset($backupItem) && !empty($backupItem)) { |
| 669 | - $files = $this->moveToServerByItem($backupItem, $size, $all); | |
| 684 | + $files = $this->moveToServerByItem($backupItem, $size, $all, $log_type); | |
| 670 | 685 | if (isset($files) && !empty($files)) { |
| 671 | 686 | $server_files['backup'] = $files; |
| 672 | 687 | } |
| 673 | 688 | } |
| @@ -682,11 +697,12 @@ | ||
| 682 | 697 | * @since 1.0.0 |
| 683 | 698 | * @param array $item |
| 684 | 699 | * @param string $size |
| 685 | 700 | * @param bool $all |
| 701 | + * @param string $log_type error-log bucket to write to on failure | |
| 686 | 702 | * @return array|string |
| 687 | 703 | */ |
| 688 | - public function moveToServerByItem( $item = [], $size = 'full', $all = false ) { | |
| 704 | + public function moveToServerByItem( $item = [], $size = 'full', $all = false, $log_type = 'restore_to_server' ) { | |
| 689 | 705 | $source_id = (int) ( $item['source_id'] ?? 0 ); |
| 690 | 706 | // Validate source ID |
| 691 | 707 | if( $source_id <= 0 ) { |
| 692 | 708 | return false; |
| @@ -724,9 +740,10 @@ | ||
| 724 | 740 | if ( $file = $this->move_to_server_by_key_and_path( |
| 725 | 741 | $data['key'], |
| 726 | 742 | $data['path'], |
| 727 | 743 | $source_id, |
| 728 | - $source_type | |
| 744 | + $source_type, | |
| 745 | + $log_type | |
| 729 | 746 | ) ) { |
| 730 | 747 | $results[ $label ] = $file; |
| 731 | 748 | } |
| 732 | 749 | } |
| @@ -739,9 +756,10 @@ | ||
| 739 | 756 | return $this->move_to_server_by_key_and_path( |
| 740 | 757 | $files[ $size ]['key'], |
| 741 | 758 | $files[ $size ]['path'], |
| 742 | 759 | $source_id, |
| 743 | - $source_type | |
| 760 | + $source_type, | |
| 761 | + $log_type | |
| 744 | 762 | ); |
| 745 | 763 | } |
| 746 | 764 | |
| 747 | 765 | return false; |
| @@ -746,11 +764,58 @@ | ||
| 746 | 764 | |
| 747 | 765 | return false; |
| 748 | 766 | } |
| 749 | 767 | |
| 768 | + /** | |
| 769 | + * Whether a moveToServer(..., $all=true, $backup=true) result actually restored | |
| 770 | + * everything this item is expected to have (every size, the original if present, and | |
| 771 | + * the backup entry if one exists) — moveToServer()'s return silently drops any single | |
| 772 | + * file that failed, so a plain non-empty check on it isn't enough to safely delete the | |
| 773 | + * cloud copies afterward. | |
| 774 | + * @since 1.4.1 | |
| 775 | + */ | |
| 776 | + public function verify_full_restore( $source_id, $source_type, $moved ) { | |
| 777 | + $row = $this->get( $source_id, $source_type ); | |
| 778 | + if ( empty( $row ) ) { | |
| 779 | + return false; | |
| 780 | + } | |
| 750 | 781 | |
| 782 | + $expected = $this->expected_restore_labels( $row ); | |
| 783 | + $restored = array_diff( array_keys( (array) $moved ), [ 'backup' ] ); | |
| 784 | + if ( ! empty( array_diff( $expected, $restored ) ) ) { | |
| 785 | + return false; | |
| 786 | + } | |
| 751 | 787 | |
| 788 | + $backup_item = $this->get_backup( $source_id, $source_type ); | |
| 789 | + if ( empty( $backup_item ) ) { | |
| 790 | + return true; | |
| 791 | + } | |
| 792 | + | |
| 793 | + $expected_backup = $this->expected_restore_labels( $backup_item ); | |
| 794 | + $restored_backup = ! empty( $moved['backup'] ) ? array_keys( $moved['backup'] ) : []; | |
| 795 | + return empty( array_diff( $expected_backup, $restored_backup ) ); | |
| 796 | + } | |
| 797 | + | |
| 752 | 798 | /** |
| 799 | + * File labels (full, original, each named size) a given item row is expected to have. | |
| 800 | + */ | |
| 801 | + private function expected_restore_labels( $item_row ) { | |
| 802 | + $expected = [ 'full' ]; | |
| 803 | + if ( ! empty( $item_row['original_key'] ) || ! empty( $item_row['original_source_path'] ) ) { | |
| 804 | + $expected[] = 'original'; | |
| 805 | + } | |
| 806 | + | |
| 807 | + $extras = ! empty( $item_row['extra'] ) ? Utils::maybe_unserialize( $item_row['extra'] ) : []; | |
| 808 | + if ( ! empty( $extras['sizes'] ) ) { | |
| 809 | + $expected = array_merge( $expected, array_keys( $extras['sizes'] ) ); | |
| 810 | + } | |
| 811 | + | |
| 812 | + return $expected; | |
| 813 | + } | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + /** | |
| 753 | 818 | * Get service path of item from database by source url |
| 754 | 819 | * @since 1.0.0 |
| 755 | 820 | * @param int $source_id |
| 756 | 821 | * @param string $file |
| @@ -830,12 +895,13 @@ | ||
| 830 | 895 | * @param string $key |
| 831 | 896 | * @param string $relative_path |
| 832 | 897 | * @param int $source_id |
| 833 | 898 | * @param string $source_type |
| 899 | + * @param string $log_type error-log bucket to write to on failure | |
| 834 | 900 | * |
| 835 | 901 | * @return string|false |
| 836 | 902 | */ |
| 837 | - protected function move_to_server_by_key_and_path( $key, $relative_path, $source_id = 0, $source_type = 'media_library' ) { | |
| 903 | + protected function move_to_server_by_key_and_path( $key, $relative_path, $source_id = 0, $source_type = 'media_library', $log_type = 'restore_to_server' ) { | |
| 838 | 904 | if ( empty( $key ) || empty( $relative_path ) ) { |
| 839 | 905 | return false; |
| 840 | 906 | } |
| 841 | 907 | |
| @@ -845,13 +911,18 @@ | ||
| 845 | 911 | if ( file_exists( $file ) ) { |
| 846 | 912 | return $file; |
| 847 | 913 | } |
| 848 | 914 | |
| 849 | - if ( Service::instance()->object_to_server( $key, $file ) ) { | |
| 915 | + // Checked on disk rather than trusting the return value alone — at least one | |
| 916 | + // provider (Cloudflare R2) has been observed writing the file successfully while | |
| 917 | + // still reporting failure (an SDK-level error thrown after the save completes). | |
| 918 | + Service::instance()->object_to_server( $key, $file ); | |
| 919 | + | |
| 920 | + if ( file_exists( $file ) ) { | |
| 850 | 921 | return $file; |
| 851 | 922 | } |
| 852 | 923 | |
| 853 | - Logger::instance()->add_log( 'restore_to_server', $source_id, $source_type, [ | |
| 924 | + Logger::instance()->add_log( $log_type, $source_id, $source_type, [ | |
| 854 | 925 | 'message' => __( 'The file could not be copied to the server. Please try again.', 'media-cloud-sync' ), |
| 855 | 926 | 'file' => $key, |
| 856 | 927 | 'code' => 404, |
| 857 | 928 | ] ); |
| @@ -886,8 +957,13 @@ | ||
| 886 | 957 | /** |
| 887 | 958 | * Delete media item |
| 888 | 959 | */ |
| 889 | 960 | public function delete_attachments_by_item($item, $delete_backup = true) { |
| 961 | + // Lets an integration veto the delete when another row still relies on the same key. | |
| 962 | + if (!apply_filters('wpmcs_should_delete_cloud_files', true, $item)) { | |
| 963 | + return; | |
| 964 | + } | |
| 965 | + | |
| 890 | 966 | $upload_dir = wp_get_upload_dir(); |
| 891 | 967 | |
| 892 | 968 | if (isset($item['extra']) && !empty($item['extra'])) { |
| 893 | 969 | $extras = Utils::maybe_unserialize($item['extra']); |
| @@ -986,8 +1062,56 @@ | ||
| 986 | 1062 | do_action('wpmcs_post_update_item', $source_id, $data, $source_type); |
| 987 | 1063 | |
| 988 | 1064 | // May be delete server files |
| 989 | 1065 | $this->may_be_delete_server_files_by_id($source_id, $source_type, true, true); |
| 1066 | + } | |
| 1067 | + | |
| 1068 | + /** | |
| 1069 | + * Track paths restored from cloud (by any integration) so they get removed | |
| 1070 | + * again later, honoring "Remove from server" the way the normal sync pipeline | |
| 1071 | + * would. Fed into the pre-update pipeline if a save happens this request (fast | |
| 1072 | + * path — matches how the item's own pending removals already work), with a | |
| 1073 | + * shutdown fallback (priority 1, ahead of most other plugins' shutdown hooks) | |
| 1074 | + * for requests where nothing ever triggers a save. | |
| 1075 | + * | |
| 1076 | + * @param string[] $paths Absolute paths of the restored files. | |
| 1077 | + * @return void | |
| 1078 | + * @since 1.4.0 | |
| 1079 | + */ | |
| 1080 | + public function track_restored_for_cleanup(array $paths) { | |
| 1081 | + foreach ($paths as $path) { | |
| 1082 | + if (!in_array($path, $this->pending_restored_files, true)) { | |
| 1083 | + $this->pending_restored_files[] = $path; | |
| 1084 | + } | |
| 1085 | + } | |
| 1086 | + | |
| 1087 | + if ($this->pending_cleanup_hooked) { | |
| 1088 | + return; | |
| 1089 | + } | |
| 1090 | + $this->pending_cleanup_hooked = true; | |
| 1091 | + | |
| 1092 | + add_filter('wpmcs_pre_update_item_additional_files_to_remove_from_server', function ($files_to_remove) { | |
| 1093 | + $files_to_remove = array_merge((array) $files_to_remove, $this->pending_restored_files); | |
| 1094 | + $this->pending_restored_files = []; | |
| 1095 | + return $files_to_remove; | |
| 1096 | + }); | |
| 1097 | + | |
| 1098 | + add_action('shutdown', array($this, 'flush_pending_restored_files'), 1); | |
| 1099 | + } | |
| 1100 | + | |
| 1101 | + /** | |
| 1102 | + * Shutdown fallback for track_restored_for_cleanup() — removes anything the | |
| 1103 | + * pre-update pipeline didn't already pick up this request. | |
| 1104 | + * | |
| 1105 | + * @return void | |
| 1106 | + * @since 1.4.0 | |
| 1107 | + */ | |
| 1108 | + public function flush_pending_restored_files() { | |
| 1109 | + if (empty($this->pending_restored_files)) { | |
| 1110 | + return; | |
| 1111 | + } | |
| 1112 | + $this->may_be_delete_server_files_by_source_paths($this->pending_restored_files); | |
| 1113 | + $this->pending_restored_files = []; | |
| 990 | 1114 | } |
| 991 | 1115 | |
| 992 | 1116 | |
| 993 | 1117 | public function may_be_delete_server_files_by_source_paths($source_paths) { |