| @@ -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 |
| @@ -90,14 +95,44 @@ | ||
| 90 | 95 | |
| 91 | 96 | // Do some pre-update actions |
| 92 | 97 | $this->pre_update_item($source_id, $data, [], $source_type); |
| 93 | 98 | |
| 94 | - if ($wpdb->insert(Db::get_table_name(), $data)) { | |
| 95 | - $item_id = $wpdb->insert_id; | |
| 99 | + // Upsert instead of a plain insert — two independent triggers (this plugin's bulk | |
| 100 | + // sync, WordPress's own metadata hook, Imagify's re-sync) can land on the same | |
| 101 | + // attachment around the same time; this converges on one row instead of erroring. | |
| 102 | + $table = Db::get_table_name(); | |
| 103 | + $inserted = $wpdb->query($wpdb->prepare( | |
| 104 | + "INSERT INTO {$table} | |
| 105 | + (provider, region, storage, source_id, source_path, source_type, url, `key`, original_source_path, original_key, is_private, extra) | |
| 106 | + VALUES (%s, %s, %s, %d, %s, %s, %s, %s, %s, %s, %d, %s) | |
| 107 | + ON DUPLICATE KEY UPDATE | |
| 108 | + id = LAST_INSERT_ID(id), | |
| 109 | + source_path = VALUES(source_path), | |
| 110 | + url = VALUES(url), | |
| 111 | + `key` = VALUES(`key`), | |
| 112 | + original_source_path = VALUES(original_source_path), | |
| 113 | + original_key = VALUES(original_key), | |
| 114 | + is_private = VALUES(is_private), | |
| 115 | + extra = VALUES(extra)", | |
| 116 | + $data['provider'], $data['region'], $data['storage'], $data['source_id'], $data['source_path'], | |
| 117 | + $data['source_type'], $data['url'], $data['key'], $data['original_source_path'], $data['original_key'], | |
| 118 | + $data['is_private'], $data['extra'] | |
| 119 | + )); | |
| 120 | + | |
| 121 | + if ($inserted !== false) { | |
| 122 | + // rows_affected: 1 = new row, 2 = existing row updated. Capture before any | |
| 123 | + // other query on $wpdb overwrites it. | |
| 124 | + $is_new_row = (int) $wpdb->rows_affected === 1; | |
| 125 | + | |
| 126 | + $item_id = (int) $wpdb->insert_id; | |
| 96 | 127 | $data['id'] = $item_id; |
| 97 | 128 | Integration::update_meta($source_id, 'item', $data, false, false, $source_type); |
| 98 | 129 | Cache::update_item_cache($source_id.'_item_'.$source_type, $data); |
| 99 | - Counter::add( 'uploaded', $source_type ); | |
| 130 | + | |
| 131 | + // Only count a genuinely new item, not a duplicate-collision update. | |
| 132 | + if ($is_new_row) { | |
| 133 | + Counter::add( 'uploaded', $source_type ); | |
| 134 | + } | |
| 100 | 135 | } |
| 101 | 136 | |
| 102 | 137 | // Do some post-update actions |
| 103 | 138 | $this->post_update_item($source_id, $data, $source_type); |
| @@ -191,15 +226,20 @@ | ||
| 191 | 226 | |
| 192 | 227 | /** |
| 193 | 228 | * Function to update item in data base |
| 194 | 229 | * @since 1.0.0 |
| 230 | + * @param array|null $target_identity Optional ['provider'=>, 'storage'=>, 'region'=>] to | |
| 231 | + * move the row to a different connection's identity. | |
| 232 | + * The WHERE clause still uses this instance's own | |
| 233 | + * (source) binding to locate the row — only the SET | |
| 234 | + * values change. @since 1.4.0 | |
| 195 | 235 | */ |
| 196 | - public function update($source_id, $data, $source_type = 'media_library') { | |
| 236 | + public function update($source_id, $data, $source_type = 'media_library', $target_identity = null) { | |
| 197 | 237 | global $wpdb; |
| 198 | 238 | if (isset($source_id) && !Utils::is_empty($source_id)) { |
| 199 | - $data['provider'] = $this->service; | |
| 200 | - $data['storage'] = $this->bucket_name; | |
| 201 | - $data['region'] = $this->region; | |
| 239 | + $data['provider'] = $target_identity['provider'] ?? $this->service; | |
| 240 | + $data['storage'] = $target_identity['storage'] ?? $this->bucket_name; | |
| 241 | + $data['region'] = $target_identity['region'] ?? $this->region; | |
| 202 | 242 | |
| 203 | 243 | $old_item = $this->get($source_id, $source_type); |
| 204 | 244 | |
| 205 | 245 | // update data from $old_item if not exists in $data |
| @@ -213,8 +253,32 @@ | ||
| 213 | 253 | |
| 214 | 254 | // Do some pre-update actions |
| 215 | 255 | $this->pre_update_item($source_id, $data, $old_item, $source_type); |
| 216 | 256 | |
| 257 | + // Moving to a different identity (e.g. bucket_to_bucket migration) can collide | |
| 258 | + // with a stale row already sitting at that target identity — e.g. an interrupted | |
| 259 | + // earlier migration attempt, or the same item independently tracked under a | |
| 260 | + // connection this site used previously. uidx_item_source is UNIQUE on | |
| 261 | + // (source_id, source_type, provider, storage, region), so the UPDATE below would | |
| 262 | + // otherwise fail outright. The row being updated here is the current, live one; | |
| 263 | + // a pre-existing row already at the target is stale by definition — clear it | |
| 264 | + // first rather than letting the whole update silently fail. | |
| 265 | + $is_identity_move = $target_identity !== null && ( | |
| 266 | + $data['provider'] !== $this->service || | |
| 267 | + $data['storage'] !== $this->bucket_name || | |
| 268 | + $data['region'] !== $this->region | |
| 269 | + ); | |
| 270 | + if ($is_identity_move) { | |
| 271 | + $target_where = array( | |
| 272 | + 'source_id' => $source_id, | |
| 273 | + 'source_type' => $source_type, | |
| 274 | + 'provider' => $data['provider'], | |
| 275 | + 'storage' => $data['storage'], | |
| 276 | + 'region' => $data['region'], | |
| 277 | + ); | |
| 278 | + $wpdb->delete(Db::get_table_name(), $target_where); | |
| 279 | + } | |
| 280 | + | |
| 217 | 281 | $where = array( |
| 218 | 282 | 'source_id' => $source_id, |
| 219 | 283 | 'source_type' => $source_type, |
| 220 | 284 | 'provider' => $this->service, |
| @@ -230,10 +294,14 @@ | ||
| 230 | 294 | |
| 231 | 295 | Integration::delete_meta($source_id, 'item', false, $source_type); |
| 232 | 296 | Cache::delete_item_cache($source_id.'_item_'.$source_type); |
| 233 | 297 | |
| 234 | - // Reset cache | |
| 235 | - $this->get($source_id, $source_type); | |
| 298 | + if ($target_identity === null) { | |
| 299 | + // Reset cache — skipped when moving to a different identity: this instance's | |
| 300 | + // get() would search under the now-stale source identity and cache a false | |
| 301 | + // negative; the delete_item_cache() above is sufficient on its own there. | |
| 302 | + $this->get($source_id, $source_type); | |
| 303 | + } | |
| 236 | 304 | // Do some post-update actions |
| 237 | 305 | $this->post_update_item($source_id, $data, $source_type); |
| 238 | 306 | |
| 239 | 307 | return true; |
| @@ -314,10 +382,13 @@ | ||
| 314 | 382 | if( |
| 315 | 383 | ($check_rewrite && (isset($this->settings['rewrite_url']) && $this->settings['rewrite_url'])) || |
| 316 | 384 | !$check_rewrite |
| 317 | 385 | ) { |
| 386 | + if ($check_rewrite) { | |
| 387 | + return (bool) apply_filters('wpmcs_is_available_from_provider', true, $attachment_id, $source_type); | |
| 388 | + } | |
| 318 | 389 | return true; |
| 319 | - } | |
| 390 | + } | |
| 320 | 391 | } |
| 321 | 392 | return false; |
| 322 | 393 | } |
| 323 | 394 | |
| @@ -365,9 +436,9 @@ | ||
| 365 | 436 | return []; |
| 366 | 437 | } |
| 367 | 438 | |
| 368 | 439 | // Normalize & deduplicate |
| 369 | - $paths = array_unique( array_map( 'esc_sql', $paths ) ); | |
| 440 | + $paths = array_unique( $paths ); | |
| 370 | 441 | |
| 371 | 442 | $table = Db::get_table_name(); |
| 372 | 443 | |
| 373 | 444 | // Build USE INDEX clause from field map |
| @@ -396,12 +467,15 @@ | ||
| 396 | 467 | */ |
| 397 | 468 | $conditions = []; |
| 398 | 469 | |
| 399 | 470 | if ( $exact_match ) { |
| 400 | - $in = "'" . implode( "','", $paths ) . "'"; | |
| 471 | + $placeholders = implode( ',', array_fill( 0, count( $paths ), '%s' ) ); | |
| 401 | 472 | |
| 402 | 473 | foreach ( array_keys( $fields ) as $column ) { |
| 403 | - $conditions[] = "`{$column}` IN ({$in})"; | |
| 474 | + $conditions[] = "`{$column}` IN ({$placeholders})"; | |
| 475 | + foreach ( $paths as $path ) { | |
| 476 | + $params[] = $path; | |
| 477 | + } | |
| 404 | 478 | } |
| 405 | 479 | } else { |
| 406 | 480 | foreach ( $paths as $path ) { |
| 407 | 481 | $ext = pathinfo( $path, PATHINFO_EXTENSION ); |
| @@ -409,9 +483,10 @@ | ||
| 409 | 483 | ? substr_replace( $path, '%', -strlen( $ext ) - 1 ) |
| 410 | 484 | : $path . '%'; |
| 411 | 485 | |
| 412 | 486 | foreach ( array_keys( $fields ) as $column ) { |
| 413 | - $conditions[] = "`{$column}` LIKE '{$base}'"; | |
| 487 | + $conditions[] = "`{$column}` LIKE %s"; | |
| 488 | + $params[] = $base; | |
| 414 | 489 | } |
| 415 | 490 | } |
| 416 | 491 | } |
| 417 | 492 | |
| @@ -517,11 +592,9 @@ | ||
| 517 | 592 | * @param |
| 518 | 593 | */ |
| 519 | 594 | public function get_url($source_id, $size = 'full', $source_type = 'media_library'){ |
| 520 | 595 | if ($data = $this->get($source_id, $source_type)) { |
| 521 | - $extras = $this->get_extras($source_id, false, $source_type) ?: []; | |
| 522 | - $key = ''; | |
| 523 | - $url = ''; | |
| 596 | + $key = ''; | |
| 524 | 597 | switch($size) { |
| 525 | 598 | case 'full': |
| 526 | 599 | $key = $data['key']; |
| 527 | 600 | break; |
| @@ -530,8 +603,10 @@ | ||
| 530 | 603 | $key = $data['original_key']; |
| 531 | 604 | } |
| 532 | 605 | break; |
| 533 | 606 | default: |
| 607 | + // Only named sizes need extras — skip fetching them for 'full'/'original'. | |
| 608 | + $extras = $this->get_extras($source_id, false, $source_type) ?: []; | |
| 534 | 609 | if( |
| 535 | 610 | isset($extras) && !empty($extras) && |
| 536 | 611 | isset($extras['sizes']) && !empty($extras['sizes']) && |
| 537 | 612 | isset($extras['sizes'][$size]) && !empty($extras['sizes'][$size]) |
| @@ -546,9 +621,12 @@ | ||
| 546 | 621 | if ($privateUrl === false) { |
| 547 | 622 | $new_url = Service::instance()->get_private_url($key); |
| 548 | 623 | |
| 549 | 624 | if (!Utils::is_empty($new_url)) { |
| 550 | - $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 ); | |
| 551 | 629 | $expireMinutes = (int)(isset($this->settings['private_url_expire']) && !empty($this->settings['private_url_expire'])) |
| 552 | 630 | ? $this->settings['private_url_expire'] |
| 553 | 631 | : 20; |
| 554 | 632 | $expireSeconds = $expireMinutes * 60; |
| @@ -578,11 +656,15 @@ | ||
| 578 | 656 | * @param string $size size of the file, default is full |
| 579 | 657 | * @param string $source_type source type of item, default is media_library |
| 580 | 658 | * @param bool $all if true, it will move all files to server |
| 581 | 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. | |
| 582 | 664 | * @return array an array of server file paths |
| 583 | 665 | */ |
| 584 | - 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'){ | |
| 585 | 667 | $server_files = []; |
| 586 | 668 | $server_file = false; |
| 587 | 669 | $source_id = (int)$source_id; |
| 588 | 670 | $item = $this->get($source_id, $source_type); |
| @@ -587,12 +669,12 @@ | ||
| 587 | 669 | $source_id = (int)$source_id; |
| 588 | 670 | $item = $this->get($source_id, $source_type); |
| 589 | 671 | |
| 590 | 672 | // Remove log if exists before move to server |
| 591 | - Logger::instance()->remove_log('restore_to_server', $source_id, $source_type); | |
| 673 | + Logger::instance()->remove_log($log_type, $source_id, $source_type); | |
| 592 | 674 | |
| 593 | 675 | if ( isset($item) && !empty($item) ) { |
| 594 | - $files = $this->moveToServerByItem($item, $size, $all); | |
| 676 | + $files = $this->moveToServerByItem($item, $size, $all, $log_type); | |
| 595 | 677 | if (isset($files) && !empty($files)) { |
| 596 | 678 | $server_files = $all ? array_merge($server_files, $files) : $files; |
| 597 | 679 | } |
| 598 | 680 | } |
| @@ -598,9 +680,9 @@ | ||
| 598 | 680 | } |
| 599 | 681 | if( $all && $backup ) { |
| 600 | 682 | $backupItem = $this->get_backup($source_id, $source_type); |
| 601 | 683 | if (isset($backupItem) && !empty($backupItem)) { |
| 602 | - $files = $this->moveToServerByItem($backupItem, $size, $all); | |
| 684 | + $files = $this->moveToServerByItem($backupItem, $size, $all, $log_type); | |
| 603 | 685 | if (isset($files) && !empty($files)) { |
| 604 | 686 | $server_files['backup'] = $files; |
| 605 | 687 | } |
| 606 | 688 | } |
| @@ -615,11 +697,12 @@ | ||
| 615 | 697 | * @since 1.0.0 |
| 616 | 698 | * @param array $item |
| 617 | 699 | * @param string $size |
| 618 | 700 | * @param bool $all |
| 701 | + * @param string $log_type error-log bucket to write to on failure | |
| 619 | 702 | * @return array|string |
| 620 | 703 | */ |
| 621 | - public function moveToServerByItem( $item = [], $size = 'full', $all = false ) { | |
| 704 | + public function moveToServerByItem( $item = [], $size = 'full', $all = false, $log_type = 'restore_to_server' ) { | |
| 622 | 705 | $source_id = (int) ( $item['source_id'] ?? 0 ); |
| 623 | 706 | // Validate source ID |
| 624 | 707 | if( $source_id <= 0 ) { |
| 625 | 708 | return false; |
| @@ -657,9 +740,10 @@ | ||
| 657 | 740 | if ( $file = $this->move_to_server_by_key_and_path( |
| 658 | 741 | $data['key'], |
| 659 | 742 | $data['path'], |
| 660 | 743 | $source_id, |
| 661 | - $source_type | |
| 744 | + $source_type, | |
| 745 | + $log_type | |
| 662 | 746 | ) ) { |
| 663 | 747 | $results[ $label ] = $file; |
| 664 | 748 | } |
| 665 | 749 | } |
| @@ -672,9 +756,10 @@ | ||
| 672 | 756 | return $this->move_to_server_by_key_and_path( |
| 673 | 757 | $files[ $size ]['key'], |
| 674 | 758 | $files[ $size ]['path'], |
| 675 | 759 | $source_id, |
| 676 | - $source_type | |
| 760 | + $source_type, | |
| 761 | + $log_type | |
| 677 | 762 | ); |
| 678 | 763 | } |
| 679 | 764 | |
| 680 | 765 | return false; |
| @@ -679,11 +764,58 @@ | ||
| 679 | 764 | |
| 680 | 765 | return false; |
| 681 | 766 | } |
| 682 | 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 | + } | |
| 683 | 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 | + } | |
| 684 | 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 | + | |
| 685 | 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 | + /** | |
| 686 | 818 | * Get service path of item from database by source url |
| 687 | 819 | * @since 1.0.0 |
| 688 | 820 | * @param int $source_id |
| 689 | 821 | * @param string $file |
| @@ -763,12 +895,13 @@ | ||
| 763 | 895 | * @param string $key |
| 764 | 896 | * @param string $relative_path |
| 765 | 897 | * @param int $source_id |
| 766 | 898 | * @param string $source_type |
| 899 | + * @param string $log_type error-log bucket to write to on failure | |
| 767 | 900 | * |
| 768 | 901 | * @return string|false |
| 769 | 902 | */ |
| 770 | - 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' ) { | |
| 771 | 904 | if ( empty( $key ) || empty( $relative_path ) ) { |
| 772 | 905 | return false; |
| 773 | 906 | } |
| 774 | 907 | |
| @@ -778,13 +911,18 @@ | ||
| 778 | 911 | if ( file_exists( $file ) ) { |
| 779 | 912 | return $file; |
| 780 | 913 | } |
| 781 | 914 | |
| 782 | - 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 ) ) { | |
| 783 | 921 | return $file; |
| 784 | 922 | } |
| 785 | 923 | |
| 786 | - Logger::instance()->add_log( 'restore_to_server', $source_id, $source_type, [ | |
| 924 | + Logger::instance()->add_log( $log_type, $source_id, $source_type, [ | |
| 787 | 925 | 'message' => __( 'The file could not be copied to the server. Please try again.', 'media-cloud-sync' ), |
| 788 | 926 | 'file' => $key, |
| 789 | 927 | 'code' => 404, |
| 790 | 928 | ] ); |
| @@ -819,8 +957,13 @@ | ||
| 819 | 957 | /** |
| 820 | 958 | * Delete media item |
| 821 | 959 | */ |
| 822 | 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 | + | |
| 823 | 966 | $upload_dir = wp_get_upload_dir(); |
| 824 | 967 | |
| 825 | 968 | if (isset($item['extra']) && !empty($item['extra'])) { |
| 826 | 969 | $extras = Utils::maybe_unserialize($item['extra']); |
| @@ -921,9 +1064,57 @@ | ||
| 921 | 1064 | // May be delete server files |
| 922 | 1065 | $this->may_be_delete_server_files_by_id($source_id, $source_type, true, true); |
| 923 | 1066 | } |
| 924 | 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 | + } | |
| 925 | 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 = []; | |
| 1114 | + } | |
| 1115 | + | |
| 1116 | + | |
| 926 | 1117 | public function may_be_delete_server_files_by_source_paths($source_paths) { |
| 927 | 1118 | if (Utils::is_empty($source_paths) || !is_array($source_paths)) { |
| 928 | 1119 | return false; |
| 929 | 1120 | } |
| @@ -989,9 +1180,9 @@ | ||
| 989 | 1180 | } |
| 990 | 1181 | } |
| 991 | 1182 | } |
| 992 | 1183 | |
| 993 | - $this->may_be_delete_server_files_by_item($item, $delete_main_file, $delete_backup); | |
| 1184 | + $this->may_be_delete_server_files_by_item($item, $delete_main_file); | |
| 994 | 1185 | |
| 995 | 1186 | return true; |
| 996 | 1187 | } |
| 997 | 1188 | |