PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
← All changes | includes/base/item.php +142 -18 1.3.121.4.1 View file →
@@ -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) {