PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
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 1.3.0 All 34 releases
← All changes | includes/base/item.php +711 -200 1.2.12trunk View file →
@@ -44,9 +44,9 @@
44 44 ? $this->credentials['bucketConfig']
45 45 : [];
46 46 $this->service = isset($this->credentials['service']) && !empty($this->credentials['service'])
47 47 ? $this->credentials['service']
48 - : [];
48 + : '';
49 49
50 50 if (isset($this->bucketConfig['bucket_name'])) {
51 51 $this->bucket_name = $this->bucketConfig['bucket_name'];
52 52 }
@@ -64,15 +64,16 @@
64 64 $source_id,
65 65 $url,
66 66 $key,
67 67 $source_path,
68 - $meta,
68 + $original_source_path = '',
69 + $original_key = '',
70 + $meta = array(),
69 71 $source_type = 'media_library',
70 72 $is_private = 0
71 73 ) {
72 74 global $wpdb;
73 75 $item_id = false;
74 -
75 76 $data = array(
76 77 'provider' => $this->service,
77 78 'region' => $this->region,
78 79 'storage' => $this->bucket_name,
@@ -80,19 +81,58 @@
80 81 'source_path' => $source_path,
81 82 'source_type' => $source_type,
82 83 'url' => $url,
83 84 'key' => $key,
85 + 'original_source_path' => $original_source_path,
86 + 'original_key' => $original_key,
84 87 'is_private' => $is_private,
85 - 'extra' => maybe_serialize($meta),
88 + 'extra' => Utils::maybe_serialize($meta),
86 89 );
87 - if ($wpdb->insert(Db::get_table_name(), $data)) {
88 - $item_id = $wpdb->insert_id;
90 +
91 + // Do some pre-update actions
92 + $this->pre_update_item($source_id, $data, [], $source_type);
93 +
94 + // Upsert instead of a plain insert — two independent triggers (this plugin's bulk
95 + // sync, WordPress's own metadata hook, Imagify's re-sync) can land on the same
96 + // attachment around the same time; this converges on one row instead of erroring.
97 + $table = Db::get_table_name();
98 + $inserted = $wpdb->query($wpdb->prepare(
99 + "INSERT INTO {$table}
100 + (provider, region, storage, source_id, source_path, source_type, url, `key`, original_source_path, original_key, is_private, extra)
101 + VALUES (%s, %s, %s, %d, %s, %s, %s, %s, %s, %s, %d, %s)
102 + ON DUPLICATE KEY UPDATE
103 + id = LAST_INSERT_ID(id),
104 + source_path = VALUES(source_path),
105 + url = VALUES(url),
106 + `key` = VALUES(`key`),
107 + original_source_path = VALUES(original_source_path),
108 + original_key = VALUES(original_key),
109 + is_private = VALUES(is_private),
110 + extra = VALUES(extra)",
111 + $data['provider'], $data['region'], $data['storage'], $data['source_id'], $data['source_path'],
112 + $data['source_type'], $data['url'], $data['key'], $data['original_source_path'], $data['original_key'],
113 + $data['is_private'], $data['extra']
114 + ));
115 +
116 + if ($inserted !== false) {
117 + // rows_affected: 1 = new row, 2 = existing row updated. Capture before any
118 + // other query on $wpdb overwrites it.
119 + $is_new_row = (int) $wpdb->rows_affected === 1;
120 +
121 + $item_id = (int) $wpdb->insert_id;
89 122 $data['id'] = $item_id;
90 123 Integration::update_meta($source_id, 'item', $data, false, false, $source_type);
91 124 Cache::update_item_cache($source_id.'_item_'.$source_type, $data);
92 - Counter::add( 'uploaded', $source_type );
125 +
126 + // Only count a genuinely new item, not a duplicate-collision update.
127 + if ($is_new_row) {
128 + Counter::add( 'uploaded', $source_type );
129 + }
93 130 }
94 131
132 + // Do some post-update actions
133 + $this->post_update_item($source_id, $data, $source_type);
134 +
95 135 return $item_id;
96 136 }
97 137
98 138
@@ -105,9 +145,22 @@
105 145 if($item === false) {
106 146 $item = Integration::get_meta($source_id, 'item', false, false, false, $source_type);
107 147 if($item == false){
108 148 $item_table = Db::get_table_name();
109 - $item = $wpdb->get_row("SELECT * FROM ".$item_table." WHERE source_id = $source_id AND source_type='$source_type'", ARRAY_A);
149 + $query = "SELECT * FROM {$item_table}
150 + WHERE source_id = %d
151 + AND source_type = %s
152 + AND provider = %s
153 + AND storage = %s";
154 +
155 + if(!empty($this->region)) {
156 + $query .= " AND region = %s";
157 + $query = $wpdb->prepare($query, $source_id, $source_type, $this->service, $this->bucket_name, $this->region);
158 + } else {
159 + $query = $wpdb->prepare($query, $source_id, $source_type, $this->service, $this->bucket_name);
160 + }
161 +
162 + $item = $wpdb->get_row( $query, ARRAY_A);
110 163
111 164 if($wpdb->last_error || null === $item || !(isset($item) && !empty($item))) {
112 165 Cache::update_item_cache($source_id.'_item_'.$source_type, '');
113 166 return false;
@@ -118,9 +171,16 @@
118 171 // Update cache
119 172 Cache::update_item_cache($source_id.'_item_'.$source_type, $item == false ? '' : $item);
120 173 }
121 174
122 - return !empty($item) ? $item : false;
175 + /**
176 + * Filter to modify item data when retrieved
177 + * @param array|false $item
178 + * @param int $source_id
179 + * @param string $source_type
180 + * @since 1.3.5
181 + */
182 + return apply_filters( 'wpmcs_get_item' , !empty($item) ? $item : false, $source_id, $source_type );
123 183 }
124 184
125 185 /**
126 186 * Function to delete item from data base
@@ -128,9 +188,22 @@
128 188 */
129 189 public function delete($source_id, $source_type = 'media_library'){
130 190 global $wpdb;
131 191 if (isset($source_id) && !Utils::is_empty($source_id)) {
132 - $rows = $wpdb->delete(Db::get_table_name(), array('source_id' => $source_id, 'source_type' => $source_type));
192 + // Delete attachments by item from cloud
193 + $item = $this->get($source_id, $source_type);
194 + $this->delete_attachments_by_item($item);
195 +
196 + $where = array(
197 + 'source_id' => $source_id,
198 + 'source_type' => $source_type,
199 + 'provider' => $this->service,
200 + 'storage' => $this->bucket_name,
201 + );
202 + if(!empty($this->region)) {
203 + $where['region'] = $this->region;
204 + }
205 + $rows = $wpdb->delete( Db::get_table_name(), $where );
133 206 if ($wpdb->last_error || false === $rows) {
134 207 return false;
135 208 }
136 209 Integration::delete_meta($source_id, 'item', false, $source_type);
@@ -135,8 +208,12 @@
135 208 }
136 209 Integration::delete_meta($source_id, 'item', false, $source_type);
137 210 Cache::delete_item_cache($source_id.'_item_'.$source_type);
138 211 Counter::remove( 'uploaded', $source_type );
212 +
213 + // Remove logs by media ID & source type
214 + Logger::instance()->remove_log_by_media_id($source_id, $source_type);
215 +
139 216 return true;
140 217 }
141 218 return false;
142 219 }
@@ -144,18 +221,69 @@
144 221
145 222 /**
146 223 * Function to update item in data base
147 224 * @since 1.0.0
225 + * @param array|null $target_identity Optional ['provider'=>, 'storage'=>, 'region'=>] to
226 + * move the row to a different connection's identity.
227 + * The WHERE clause still uses this instance's own
228 + * (source) binding to locate the row — only the SET
229 + * values change. @since 1.4.0
148 230 */
149 - public function update($source_id, $data, $source_type = 'media_library') {
231 + public function update($source_id, $data, $source_type = 'media_library', $target_identity = null) {
150 232 global $wpdb;
151 233 if (isset($source_id) && !Utils::is_empty($source_id)) {
234 + $data['provider'] = $target_identity['provider'] ?? $this->service;
235 + $data['storage'] = $target_identity['storage'] ?? $this->bucket_name;
236 + $data['region'] = $target_identity['region'] ?? $this->region;
152 237
153 - $data['provider'] = $this->service;
154 - $data['region'] = $this->region;
155 - $data['storage'] = $this->bucket_name;
238 + $old_item = $this->get($source_id, $source_type);
156 239
157 - $rows = $wpdb->update(Db::get_table_name(), $data, array('source_id' => $source_id, 'source_type' => $source_type));
240 + // update data from $old_item if not exists in $data
241 + if (isset($old_item) && !empty($old_item)) {
242 + foreach ($old_item as $key => $value) {
243 + if (!isset($data[$key])) {
244 + $data[$key] = $value;
245 + }
246 + }
247 + }
248 +
249 + // Do some pre-update actions
250 + $this->pre_update_item($source_id, $data, $old_item, $source_type);
251 +
252 + // Moving to a different identity (e.g. bucket_to_bucket migration) can collide
253 + // with a stale row already sitting at that target identity — e.g. an interrupted
254 + // earlier migration attempt, or the same item independently tracked under a
255 + // connection this site used previously. uidx_item_source is UNIQUE on
256 + // (source_id, source_type, provider, storage, region), so the UPDATE below would
257 + // otherwise fail outright. The row being updated here is the current, live one;
258 + // a pre-existing row already at the target is stale by definition — clear it
259 + // first rather than letting the whole update silently fail.
260 + $is_identity_move = $target_identity !== null && (
261 + $data['provider'] !== $this->service ||
262 + $data['storage'] !== $this->bucket_name ||
263 + $data['region'] !== $this->region
264 + );
265 + if ($is_identity_move) {
266 + $target_where = array(
267 + 'source_id' => $source_id,
268 + 'source_type' => $source_type,
269 + 'provider' => $data['provider'],
270 + 'storage' => $data['storage'],
271 + 'region' => $data['region'],
272 + );
273 + $wpdb->delete(Db::get_table_name(), $target_where);
274 + }
275 +
276 + $where = array(
277 + 'source_id' => $source_id,
278 + 'source_type' => $source_type,
279 + 'provider' => $this->service,
280 + 'storage' => $this->bucket_name,
281 + );
282 + if(!empty($this->region)) {
283 + $where['region'] = $this->region;
284 + }
285 + $rows = $wpdb->update(Db::get_table_name(), $data, $where);
158 286 if ($wpdb->last_error || false === $rows) {
159 287 return false;
160 288 }
161 289
@@ -161,10 +289,17 @@
161 289
162 290 Integration::delete_meta($source_id, 'item', false, $source_type);
163 291 Cache::delete_item_cache($source_id.'_item_'.$source_type);
164 292
165 - // Reset cache
166 - $current_data = $this->get($source_id, $source_type);
293 + if ($target_identity === null) {
294 + // Reset cache — skipped when moving to a different identity: this instance's
295 + // get() would search under the now-stale source identity and cache a false
296 + // negative; the delete_item_cache() above is sufficient on its own there.
297 + $this->get($source_id, $source_type);
298 + }
299 + // Do some post-update actions
300 + $this->post_update_item($source_id, $data, $source_type);
301 +
167 302 return true;
168 303 }
169 304 return false;
170 305 }
@@ -233,9 +368,13 @@
233 368 if(Utils::is_empty($item)) {
234 369 return false;
235 370 }
236 371
237 - if ($item['provider'] == $this->service) {
372 + if (
373 + $item['provider'] == $this->service &&
374 + $item['storage'] == $this->bucket_name &&
375 + $item['source_type'] == $source_type
376 + ) {
238 377 if(
239 378 ($check_rewrite && (isset($this->settings['rewrite_url']) && $this->settings['rewrite_url'])) ||
240 379 !$check_rewrite
241 380 ) {
@@ -245,67 +384,198 @@
245 384 return false;
246 385 }
247 386
248 387 /**
249 - * Get items by paths
388 + * Get items by source paths
389 + *
390 + * @param array|string $paths
391 + * @param bool $exact_match Use exact paths or greedy match
392 + * @param bool $first_only Return only the first matched item
393 + *
394 + * @return array
250 395 */
251 - public function get_items_by_source_paths( $paths = [] ) {
396 + public function get_items_by_paths( $paths, $exact_match = true, $first_only = false, $type = 'source' ) {
252 397 global $wpdb;
253 398
254 - $items = [];
399 + if ( ! is_array( $paths ) && is_string( $paths ) && ! empty( $paths ) ) {
400 + $paths = [ $paths ];
401 + }
255 402
256 - if (!Utils::is_empty($paths)) {
257 - // Ensure paths are properly sanitized and ready for the query
258 - $like_conditions = array();
259 - foreach ($paths as $path) {
260 - // Prepare the SQL conditions for exact match in source_path and partial match in extras column
261 - $like_conditions[] = $wpdb->prepare("(source_path = %s OR extra LIKE %s)", $path, '%' . $wpdb->esc_like($path) . '%');
262 - }
403 + if ( Utils::is_empty( $paths ) ) {
404 + return [];
405 + }
263 406
264 - // Combine the conditions with OR
265 - $where_clause = implode(' OR ', $like_conditions);
407 + /**
408 + * Field => Index mapping
409 + * field_name => index_name
410 + */
411 + switch ( $type ) {
412 + case 'key':
413 + $fields = [
414 + 'key' => 'uidx_key',
415 + 'original_key' => 'uidx_original_key',
416 + ];
417 + break;
266 418
267 - // Execute the SQL query to fetch source_id instead of source_path
268 - $item_table = Db::get_table_name();
269 - $sql = "SELECT DISTINCT source_id, source_type FROM " . $item_table . " WHERE " . $where_clause;
270 - $results = $wpdb->get_results($sql, ARRAY_A);
419 + default:
420 + $fields = [
421 + 'source_path' => 'uidx_source_path',
422 + 'original_source_path' => 'uidx_original_source_path',
423 + ];
424 + break;
425 + }
271 426
272 - if ($wpdb->last_error || Utils::is_empty($results)) {
273 - return [];
427 + if ( empty( $fields ) ) {
428 + return [];
429 + }
430 +
431 + // Normalize & deduplicate
432 + $paths = array_unique( $paths );
433 +
434 + $table = Db::get_table_name();
435 +
436 + // Build USE INDEX clause from field map
437 + $index_list = implode( ', ', array_values( $fields ) );
438 +
439 + $sql = "
440 + SELECT DISTINCT source_id, source_type
441 + FROM {$table} USE INDEX ({$index_list})
442 + WHERE provider = %s
443 + AND storage = %s
444 + ";
445 +
446 + $params = [
447 + $this->service,
448 + $this->bucket_name,
449 + ];
450 +
451 + // Optional region
452 + if ( ! empty( $this->region ) ) {
453 + $sql .= " AND region = %s";
454 + $params[] = $this->region;
455 + }
456 +
457 + /**
458 + * Path conditions
459 + */
460 + $conditions = [];
461 +
462 + if ( $exact_match ) {
463 + $placeholders = implode( ',', array_fill( 0, count( $paths ), '%s' ) );
464 +
465 + foreach ( array_keys( $fields ) as $column ) {
466 + $conditions[] = "`{$column}` IN ({$placeholders})";
467 + foreach ( $paths as $path ) {
468 + $params[] = $path;
469 + }
274 470 }
471 + } else {
472 + foreach ( $paths as $path ) {
473 + $ext = pathinfo( $path, PATHINFO_EXTENSION );
474 + $base = $ext
475 + ? substr_replace( $path, '%', -strlen( $ext ) - 1 )
476 + : $path . '%';
275 477
276 - $source_ids = array();
277 - foreach ($results as $row) {
278 - $item = $this->get((int)$row['source_id'], $row['source_type']);
279 - if(!Utils::is_empty($item)) {
280 - $items[] = $item;
478 + foreach ( array_keys( $fields ) as $column ) {
479 + $conditions[] = "`{$column}` LIKE %s";
480 + $params[] = $base;
281 481 }
282 482 }
283 - return $items;
284 483 }
484 +
485 + if ( ! empty( $conditions ) ) {
486 + $sql .= " AND ( " . implode( ' OR ', $conditions ) . " )";
487 + }
488 +
489 + // First only
490 + if ( $first_only ) {
491 + $sql .= " ORDER BY source_id ASC LIMIT 1";
492 + }
493 +
494 + $prepared = $wpdb->prepare( $sql, $params );
495 + $results = $wpdb->get_results( $prepared, ARRAY_A );
496 +
497 + if ( $wpdb->last_error || empty( $results ) ) {
498 + return [];
499 + }
500 +
501 + // Hydration
502 + if ( $first_only ) {
503 + return $this->get(
504 + (int) $results[0]['source_id'],
505 + $results[0]['source_type']
506 + );
507 + }
508 +
509 + $items = [];
510 +
511 + foreach ( $results as $row ) {
512 + $item = $this->get(
513 + (int) $row['source_id'],
514 + $row['source_type']
515 + );
516 +
517 + if ( ! Utils::is_empty( $item ) ) {
518 + $items[] = $item;
519 + }
520 + }
521 +
285 522 return $items;
286 523 }
287 524
288 525
289 526 /**
290 - * Get similar existing files like origin path
527 + * Get similar existing files by source path prefix
528 + * Searches in source_path and original_source_path
529 + *
291 530 * @since 1.0.0
292 531 */
293 - public function get_similar_files_by_path($path){
532 + public function get_similar_files_by_path( $path ) {
294 533 global $wpdb;
295 - if (isset($path) && !empty($path)) {
296 - $item_table = Db::get_table_name();
297 - $results = $wpdb->get_results("SELECT source_path FROM " . $item_table . " WHERE source_path LIKE '$path%'", ARRAY_A);
298 - if ($wpdb->last_error || null === $results || !(isset($results) && !empty($results))) {
299 - return false;
300 - }
301 - $source_paths = array();
302 - foreach ($results as $row) {
303 - $source_paths[] = $row['source_path'];
304 - }
305 - return $source_paths;
534 +
535 + if ( Utils::is_empty( $path ) ) {
536 + return false;
306 537 }
307 - return false;
538 +
539 + $table = Db::get_table_name();
540 + $like = $wpdb->esc_like( $path ) . '%';
541 +
542 + $base_where = "
543 + provider = %s
544 + AND storage = %s
545 + " . ( ! empty( $this->region ) ? "AND region = %s" : '' );
546 +
547 + $params = [ $this->service, $this->bucket_name ];
548 + if ( ! empty( $this->region ) ) {
549 + $params[] = $this->region;
550 + }
551 +
552 + $sql = "
553 + (
554 + SELECT source_path
555 + FROM {$table} USE INDEX (idx_source_path_provider)
556 + WHERE {$base_where}
557 + AND source_path LIKE %s
558 + )
559 + UNION DISTINCT
560 + (
561 + SELECT original_source_path AS source_path
562 + FROM {$table} USE INDEX (uidx_original_source_path)
563 + WHERE {$base_where}
564 + AND original_source_path LIKE %s
565 + )
566 + ";
567 +
568 + $params = array_merge( $params, [ $like ], $params, [ $like ] );
569 +
570 + $results = $wpdb->get_results(
571 + $wpdb->prepare( $sql, $params ),
572 + ARRAY_A
573 + );
574 +
575 + return ( $wpdb->last_error || empty( $results ) )
576 + ? false
577 + : array_column( $results, 'source_path' );
308 578 }
309 579
310 580
311 581 /**
@@ -314,25 +584,21 @@
314 584 * @param
315 585 */
316 586 public function get_url($source_id, $size = 'full', $source_type = 'media_library'){
317 587 if ($data = $this->get($source_id, $source_type)) {
318 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
319 - $key = '';
320 - $url = '';
588 + $key = '';
321 589 switch($size) {
322 590 case 'full':
323 591 $key = $data['key'];
324 - $url = $data['url'];
325 592 break;
326 593 case 'original':
327 - if(
328 - isset($extras) && !empty($extras) &&
329 - isset($extras['original']) && !empty($extras['original'])
330 - ) {
331 - $key = $extras['original']['key'];
332 - $url = $extras['original']['url'];
594 + if( isset($data['original_key']) && !empty($data['original_key']) ) {
595 + $key = $data['original_key'];
333 596 }
597 + break;
334 598 default:
599 + // Only named sizes need extras — skip fetching them for 'full'/'original'.
600 + $extras = $this->get_extras($source_id, false, $source_type) ?: [];
335 601 if(
336 602 isset($extras) && !empty($extras) &&
337 603 isset($extras['sizes']) && !empty($extras['sizes']) &&
338 604 isset($extras['sizes'][$size]) && !empty($extras['sizes'][$size])
@@ -337,34 +603,33 @@
337 603 isset($extras['sizes']) && !empty($extras['sizes']) &&
338 604 isset($extras['sizes'][$size]) && !empty($extras['sizes'][$size])
339 605 ) {
340 606 $key = $extras['sizes'][$size]['key'];
341 - $url = $extras['sizes'][$size]['url'];
342 607 }
343 608 }
344 609
345 610 if(!empty($key)){
346 - if (
347 - isset($this->settings['enable_presigned']) && $this->settings['enable_presigned'] &&
348 - isset($this->settings['presigned_expire']) && !empty($this->settings['presigned_expire'])
349 - ) {
350 - $preSignedUrl = Integration::get_meta( $source_id, 'presigned_url_'.$size, false, false, false, $source_type );
351 - if ($preSignedUrl === false) {
352 - $new_url = Service::instance()->get_presigned_url($key);
611 + if (isset($data['is_private']) && $data['is_private']) {
612 + $privateUrl = Integration::get_meta( $source_id, 'private_url_'.$size, false, false, false, $source_type );
613 + if ($privateUrl === false) {
614 + $new_url = Service::instance()->get_private_url($key);
353 615
354 616 if (!Utils::is_empty($new_url)) {
355 - $preSignedUrl = Cdn::may_generate_cdn_url($new_url, $key);
356 - $expireMinutes = (int)(isset($this->settings['presigned_expire']) && !empty($this->settings['presigned_expire']))
357 - ? $this->settings['presigned_expire']
617 + $privateUrl = Cdn::may_generate_cdn_url($new_url, $key);
618 + $expireMinutes = (int)(isset($this->settings['private_url_expire']) && !empty($this->settings['private_url_expire']))
619 + ? $this->settings['private_url_expire']
358 620 : 20;
359 621 $expireSeconds = $expireMinutes * 60;
360 622
361 - Integration::update_meta($source_id, 'presigned_url_'.$size, $preSignedUrl, false, $expireSeconds, $source_type);
623 + Integration::update_meta($source_id, 'private_url_'.$size, $privateUrl, false, $expireSeconds, $source_type);
362 624 }
363 625 }
364 - return $preSignedUrl;
626 + return $privateUrl;
365 627 } else {
366 - return Cdn::may_generate_cdn_url($url, $key);
628 + $url = Service::instance()->get_url($key);
629 + if(!Utils::is_empty($url)) {
630 + return Cdn::may_generate_cdn_url($url, $key);
631 + }
367 632 }
368 633 }
369 634 }
370 635 return false;
@@ -370,129 +635,256 @@
370 635 return false;
371 636 }
372 637
373 638
374 - /**
375 - * Get service path of item from database
376 - * @since 1.0.0
377 - * @param
639 +
640 + /**
641 + * Move file to server, given item id and size
642 + * If $all is true, it will move all files to server
643 + * If $backup is true, it will move backup file to server
644 + * @param int $source_id source id of item
645 + * @param string $size size of the file, default is full
646 + * @param string $source_type source type of item, default is media_library
647 + * @param bool $all if true, it will move all files to server
648 + * @param bool $backup if true, it will move backup file to server
649 + * @return array an array of server file paths
378 650 */
379 - public function moveToServer($source_id, $size = 'full', $all = false, $source_type = 'media_library') {
380 - $server_files = array();
651 + public function moveToServer($source_id, $size = 'full', $source_type = 'media_library', $all = false, $backup = false){
652 + $server_files = [];
381 653 $server_file = false;
382 - $upload_dir = wp_get_upload_dir();
383 654 $source_id = (int)$source_id;
655 + $item = $this->get($source_id, $source_type);
384 656
385 - if ($data = $this->get($source_id, $source_type)) {
386 - $wpmcsService = Service::instance();
387 - if($all) {
388 - if (
389 - isset($data['source_path']) && !empty($data['source_path']) &&
390 - isset($data['key']) && !empty($data['key'])
391 - ) {
392 - $file_path = trailingslashit($upload_dir['basedir']) . $data['source_path'];
393 - if(file_exists($file_path) || $wpmcsService->object_to_server($data['key'], $file_path)) {
394 - $server_files['full'] = $file_path;
395 - }
657 + // Remove log if exists before move to server
658 + Logger::instance()->remove_log('restore_to_server', $source_id, $source_type);
659 +
660 + if ( isset($item) && !empty($item) ) {
661 + $files = $this->moveToServerByItem($item, $size, $all);
662 + if (isset($files) && !empty($files)) {
663 + $server_files = $all ? array_merge($server_files, $files) : $files;
664 + }
665 + }
666 + if( $all && $backup ) {
667 + $backupItem = $this->get_backup($source_id, $source_type);
668 + if (isset($backupItem) && !empty($backupItem)) {
669 + $files = $this->moveToServerByItem($backupItem, $size, $all);
670 + if (isset($files) && !empty($files)) {
671 + $server_files['backup'] = $files;
396 672 }
397 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
398 - if (isset($extras['original']) && !empty($extras['original'])) {
399 - $original = $extras['original'];
400 - if(!empty($original)) {
401 - $original_file_path = trailingslashit($upload_dir['basedir']) . $original['source_path'];
402 - if(file_exists($original_file_path) || $wpmcsService->object_to_server($original['key'], $original_file_path)) {
403 - $server_files['original'] = $original_file_path;
404 - }
405 - }
673 + }
674 + }
675 + return $server_files;
676 + }
677 +
678 +
679 + /**
680 + * Copy back an item from the service to the server
681 + *
682 + * @since 1.0.0
683 + * @param array $item
684 + * @param string $size
685 + * @param bool $all
686 + * @return array|string
687 + */
688 + public function moveToServerByItem( $item = [], $size = 'full', $all = false ) {
689 + $source_id = (int) ( $item['source_id'] ?? 0 );
690 + // Validate source ID
691 + if( $source_id <= 0 ) {
692 + return false;
693 + }
694 +
695 + $source_type = $item['source_type'] ?? 'media_library';
696 + $extras = ! empty( $item['extra'] ) ? Utils::maybe_unserialize( $item['extra'] ) : [];
697 +
698 + // Build file map once
699 + $files = [
700 + 'full' => [
701 + 'key' => $item['key'] ?? null,
702 + 'path' => $item['source_path'] ?? null,
703 + ],
704 + 'original' => [
705 + 'key' => $item['original_key'] ?? null,
706 + 'path' => $item['original_source_path'] ?? null,
707 + ],
708 + ];
709 +
710 + if ( ! empty( $extras['sizes'] ) ) {
711 + foreach ( $extras['sizes'] as $name => $data ) {
712 + $files[ $name ] = [
713 + 'key' => $data['key'] ?? null,
714 + 'path' => $data['source_path'] ?? null,
715 + ];
716 + }
717 + }
718 +
719 + // ALL files
720 + if ( $all ) {
721 + $results = [];
722 +
723 + foreach ( $files as $label => $data ) {
724 + if ( $file = $this->move_to_server_by_key_and_path(
725 + $data['key'],
726 + $data['path'],
727 + $source_id,
728 + $source_type
729 + ) ) {
730 + $results[ $label ] = $file;
406 731 }
407 - if (isset($extras['sizes']) && !empty($extras['sizes'])) {
408 - $sizes = $extras['sizes'];
409 - foreach($sizes as $sub_size => $sub_file) {
410 - if(!empty($sub_file)) {
411 - $sub_file_path = trailingslashit($upload_dir['basedir']) . $sub_file['source_path'];
412 - if(file_exists($sub_file_path) || $wpmcsService->object_to_server($sub_file['key'], $sub_file_path)) {
413 - $server_files[$sub_size] = $sub_file_path;
414 - }
415 - }
416 - }
417 - }
418 - return !empty($server_files) ? $server_files : false;
419 - } else {
420 - if($size === 'full') {
421 - if (
422 - isset($data['source_path']) && !empty($data['source_path']) &&
423 - isset($data['key']) && !empty($data['key'])
424 - ) {
425 - $file_path = trailingslashit($upload_dir['basedir']) . $data['source_path'];
426 - if(file_exists($file_path) || $wpmcsService->object_to_server($data['key'], $file_path)) {
427 - $server_file = $file_path;
428 - }
429 - }
430 - } else if($size === 'original') {
431 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
432 - if (isset($extras['original']) && !empty($extras['original'])) {
433 - $original = $extras['original'];
434 - if(!empty($original)) {
435 - $original_file_path = trailingslashit($upload_dir['basedir']) . $original['source_path'];
436 - if(file_exists($original_file_path) || $wpmcsService->object_to_server($original['key'], $original_file_path)) {
437 - $server_file = $original_file_path;
438 - }
439 - }
440 - }
441 - } else {
442 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
443 - if (isset($extras['sizes']) && !empty($extras['sizes'])) {
444 - $sizes = $extras['sizes'];
445 - if(isset($sizes[$size]) && !empty($sizes[$size])) {
446 - $sub_file_path = trailingslashit($upload_dir['basedir']) . $sizes[$size]['source_path'];
447 - if(file_exists($sub_file_path) || $wpmcsService->object_to_server($sizes[$size]['key'], $sub_file_path)) {
448 - $server_file = $sub_file_path;
449 - }
450 - }
451 - }
452 - }
453 - return $server_file;
454 732 }
733 +
734 + return $results;
455 735 }
736 +
737 + // SINGLE file
738 + if ( isset( $files[ $size ] ) ) {
739 + return $this->move_to_server_by_key_and_path(
740 + $files[ $size ]['key'],
741 + $files[ $size ]['path'],
742 + $source_id,
743 + $source_type
744 + );
745 + }
746 +
456 747 return false;
457 748 }
458 -
459 749
750 +
751 +
460 752 /**
461 753 * Get service path of item from database by source url
462 754 * @since 1.0.0
463 - * @param
755 + * @param int $source_id
756 + * @param string $file
757 + * @param string $source_type
758 + * @return bool
464 759 */
465 - public function moveToServerBySourcePath($source_id, $file, $source_type = 'media_library'){
466 - $server_files = array();
467 - $server_file = false;
468 - $upload_dir = wp_get_upload_dir();
469 - $source_id = (int)$source_id;
760 + public function moveToServerBySourcePath( $source_id, $file, $source_type = 'media_library' ) {
761 + $source_id = (int) $source_id;
470 762
471 - if ($data = $this->get($source_id, $source_type)) {
472 - $source_path = Utils::get_attachment_source_path($file);
763 + $item = $this->get( $source_id, $source_type );
764 + if ( Utils::is_empty( $item ) ) {
765 + return false;
766 + }
473 767
474 - if( isset($data['source_path']) && !empty($data['source_path']) && $data['source_path'] == $source_path ) {
475 - $file_path = trailingslashit($upload_dir['basedir']) . $data['source_path'];
476 - if(file_exists($file_path) || Service::instance()->object_to_server($data['key'], $file_path)) {
768 + $source_path = Utils::get_attachment_source_path( $file );
769 + if ( empty( $source_path ) ) {
770 + return false;
771 + }
772 +
773 + // 1. Check main file
774 + if (
775 + isset( $item['source_path'] ) &&
776 + ! empty( $item['source_path'] ) &&
777 + $item['source_path'] === $source_path &&
778 + $this->move_to_server_by_key_and_path(
779 + $item['key'] ?? null,
780 + $item['source_path'],
781 + $source_id,
782 + $source_type
783 + )
784 + ) {
785 + return true;
786 + }
787 +
788 +
789 + // 2. Check original
790 + if (
791 + isset( $item['original_source_path'] ) && ! empty( $item['original_source_path'] ) &&
792 + $item['original_source_path'] === $source_path &&
793 + $this->move_to_server_by_key_and_path(
794 + $item['original_key'] ?? null,
795 + $item['original_source_path'],
796 + $source_id,
797 + $source_type
798 + )
799 + ) {
800 + return true;
801 + }
802 +
803 + $extras = $this->get_extras( $source_id, false, $source_type ) ?: [];
804 +
805 + // 3. Check sizes
806 + if ( ! empty( $extras['sizes'] ) ) {
807 + foreach ( $extras['sizes'] as $size ) {
808 + if (
809 + isset( $size['source_path'] ) &&
810 + ! empty( $size['source_path'] ) &&
811 + $size['source_path'] === $source_path &&
812 + $this->move_to_server_by_key_and_path(
813 + $size['key'] ?? null,
814 + $size['source_path'],
815 + $source_id,
816 + $source_type
817 + )
818 + ) {
477 819 return true;
478 820 }
479 - } else {
480 - $sizes = $this->get_extras($source_id, 'sizes', $source_type) ?: [];
481 - foreach($sizes as $sub_size => $sub_file) {
482 - if(!empty($sub_file) && $sub_file['source_path'] == $source_path) {
483 - $sub_file_path = trailingslashit($upload_dir['basedir']) . $sub_file['source_path'];
484 - if(file_exists($sub_file_path) || Service::instance()->object_to_server($sub_file['key'], $sub_file_path)) {
485 - return true;
486 - }
487 - }
488 - }
489 821 }
490 822 }
823 +
491 824 return false;
492 825 }
493 826
494 827 /**
828 + * Copy back a file from the service to the server
829 + *
830 + * @param string $key
831 + * @param string $relative_path
832 + * @param int $source_id
833 + * @param string $source_type
834 + *
835 + * @return string|false
836 + */
837 + protected function move_to_server_by_key_and_path( $key, $relative_path, $source_id = 0, $source_type = 'media_library' ) {
838 + if ( empty( $key ) || empty( $relative_path ) ) {
839 + return false;
840 + }
841 +
842 + $upload_dir = wp_get_upload_dir();
843 + $file = trailingslashit( $upload_dir['basedir'] ) . $relative_path;
844 +
845 + if ( file_exists( $file ) ) {
846 + return $file;
847 + }
848 +
849 + if ( Service::instance()->object_to_server( $key, $file ) ) {
850 + return $file;
851 + }
852 +
853 + Logger::instance()->add_log( 'restore_to_server', $source_id, $source_type, [
854 + 'message' => __( 'The file could not be copied to the server. Please try again.', 'media-cloud-sync' ),
855 + 'file' => $key,
856 + 'code' => 404,
857 + ] );
858 +
859 + return false;
860 + }
861 +
862 +
863 + /**
864 + * Move original file to server
865 + *
866 + * @param int $source_id
867 + * @param string $source_type
868 + * @return bool
869 + */
870 + public function moveOriginalToServer($source_id, $source_type = 'media_library') {
871 + $data = $this->get($source_id, $source_type);
872 + if ($data) {
873 + $size = 'full';
874 + if (
875 + isset($data['original_source_path']) && !empty($data['original_source_path']) &&
876 + isset($data['original_key']) && !empty($data['original_key'])
877 + ) {
878 + $size = 'original';
879 + }
880 + return $this->moveToServer($source_id, $size, $source_type);
881 + }
882 + return false;
883 + }
884 +
885 +
886 + /**
495 887 * Delete media item
496 888 */
497 889 public function delete_attachments_by_item($item, $delete_backup = true) {
498 890 $upload_dir = wp_get_upload_dir();
@@ -511,15 +903,8 @@
511 903 }
512 904
513 905 if (
514 906 isset($extras) && !empty($extras) &&
515 - isset($extras['original']) && !empty($extras['original'])
516 - ) {
517 - Service::instance()->deleteSingle($extras['original']['key']);
518 - }
519 -
520 - if (
521 - isset($extras) && !empty($extras) &&
522 907 isset($extras['backup']) && !empty($extras['backup']) &&
523 908 $delete_backup
524 909 ) {
525 910 $backup = Utils::maybe_unserialize($extras['backup']);
@@ -527,17 +912,127 @@
527 912 $this->delete_attachments_by_item($backup, false);
528 913 }
529 914 }
530 915 }
916 +
917 + if (
918 + isset($item['original_key']) && !empty($item['original_key'])
919 + ) {
920 + Service::instance()->deleteSingle($item['original_key']);
921 + }
922 +
531 923 if (isset($item['key']) && !empty($item['key'])) {
532 924 Service::instance()->deleteSingle($item['key']);
533 925 }
534 926 }
535 927
536 - /**
928 +
929 + /**
930 + * Delete Cloud Files by Keys
931 + * @since 1.3.6
932 + */
933 + public function delete_cloud_files_by_keys( $keys = [] ) {
934 + if (Utils::is_empty($keys) || !is_array($keys)) {
935 + return false;
936 + }
937 +
938 + foreach ($keys as $key) {
939 + Service::instance()->deleteSingle( $key );
940 + }
941 + return true;
942 + }
943 +
944 +
945 +
946 + /**
947 + * Pre-update item actions
948 + * @since 1.2.13
949 + * @param int $source_id
950 + * @param array $data
951 + * @param string $source_type
952 + */
953 + public function pre_update_item($source_id, $new_item, $old_item = [], $source_type = 'media_library') {
954 + // Hook for pre-update actions
955 + do_action('wpmcs_pre_update_item', $source_id, $new_item, $old_item, $source_type);
956 +
957 + // Additional filter to modify files to be removed from server if needed
958 + $files_to_remove = apply_filters('wpmcs_pre_update_item_additional_files_to_remove_from_server', [], $source_id, $new_item, $old_item, $source_type);
959 +
960 + // Delete files if any
961 + if (!Utils::is_empty($files_to_remove)) {
962 + $this->may_be_delete_server_files_by_source_paths($files_to_remove);
963 + }
964 + }
965 +
966 +
967 + /**
968 + * Post-update item actions
969 + * @since 1.2.13
970 + * @param int $source_id
971 + * @param array $data
972 + * @param string $source_type
973 + * This function is called after an item has been updated in the database.
974 + * It triggers a WordPress action hook 'wpmcs_post_update_item' to allow other functions to hook into this event.
975 + * After that, it calls may_be_delete_server_files_by_id to potentially delete server files associated with the item.
976 + *
977 + * @example
978 + * $item = Item::instance();
979 + * $item->post_update_item(123, $data, 'media_library');
980 + *
981 + * This example will trigger the post-update actions for the item with ID 123.
982 + * It will execute any functions hooked to 'wpmcs_post_update_item' and may delete server files if the settings allow it.
983 + */
984 + public function post_update_item($source_id, $data, $source_type = 'media_library') {
985 + // Hook for post-update actions
986 + do_action('wpmcs_post_update_item', $source_id, $data, $source_type);
987 +
988 + // May be delete server files
989 + $this->may_be_delete_server_files_by_id($source_id, $source_type, true, true);
990 + }
991 +
992 +
993 + public function may_be_delete_server_files_by_source_paths($source_paths) {
994 + if (Utils::is_empty($source_paths) || !is_array($source_paths)) {
995 + return false;
996 + }
997 +
998 + if( !( isset($this->settings['remove_from_server']) && $this->settings['remove_from_server'] ) ) {
999 + return false;
1000 + }
1001 +
1002 + foreach ($source_paths as $path) {
1003 + if(file_exists($path)) {
1004 + wp_delete_file($path, true);
1005 + }
1006 + }
1007 + return true;
1008 + }
1009 +
1010 + /**
537 1011 * Function to remove media from server by id
538 - * @since 1.0.0
539 - *
1012 + * @param int $attachment_id
1013 + * @param string $source_type
1014 + * @param bool $delete_main_file
1015 + * @param bool $delete_backup
1016 + *
1017 + * This function checks if the item exists and if the setting to remove from server is enabled.
1018 + * If so, it deletes the main file and any backup files associated with the item.
1019 + * It also checks if the item has any extra data, and if so, it attempts to delete the backup files if specified.
1020 + * Finally, it deletes the main file associated with the item.
1021 + *
1022 + * @since 1.2.13
1023 + * @return bool Returns true if the deletion process was initiated, false otherwise.
1024 + *
1025 + * @throws \Exception If the item does not exist or if the removal from server setting is not enabled.
1026 + *
1027 + * @example
1028 + * $item = Item::instance();
1029 + * $item->may_be_delete_server_files_by_id(123, 'media_library', true, true);
1030 + *
1031 + * This example will attempt to delete the server files for the attachment with ID 123,
1032 + * including the main file and any backup files, if the settings allow it.
1033 + *
1034 + * @see Item::may_be_delete_server_files_by_item() for the function that actually performs the deletion.
540 1035 */
541 1036 public function may_be_delete_server_files_by_id($attachment_id, $source_type = 'media_library', $delete_main_file=false, $delete_backup = false) {
542 1037 $item = $this->get($attachment_id, $source_type);
543 1038 if(Utils::is_empty($item)) {
@@ -561,9 +1056,9 @@
561 1056 }
562 1057 }
563 1058 }
564 1059
565 - $this->may_be_delete_server_files_by_item($item, $delete_main_file, $delete_backup);
1060 + $this->may_be_delete_server_files_by_item($item, $delete_main_file);
566 1061
567 1062 return true;
568 1063 }
569 1064
@@ -578,10 +1073,19 @@
578 1073 if( !( isset($this->settings['remove_from_server']) && $this->settings['remove_from_server'] ) ) {
579 1074 return false;
580 1075 }
581 1076
1077 + return $this->delete_server_files_by_item( $item, $delete_main_file );
1078 + }
1079 +
1080 +
1081 + /**
1082 + * Function to remove media from server by item
1083 + */
1084 + public function delete_server_files_by_item( $item, $delete_main_file=false ) {
582 1085 $upload_dir = wp_get_upload_dir();
583 1086 $has_original = false;
1087 + $files_to_remove = array();
584 1088
585 1089 $file_path = trailingslashit($upload_dir['basedir']) . $item['source_path'];
586 1090
587 1091 if (isset($item['extra']) && !empty($item['extra'])) {
@@ -593,33 +1097,40 @@
593 1097 foreach ($extras['sizes'] as $sub_image) {
594 1098 if (isset($sub_image['source_path']) && !empty($sub_image['source_path'])) {
595 1099 $file = trailingslashit($upload_dir['basedir']) . $sub_image['source_path'];
596 1100 if(file_exists($file)) {
597 - wp_delete_file($file);
1101 + $files_to_remove[] = $file;
598 1102 }
599 1103 }
600 1104 }
601 1105 }
602 -
603 - if (
604 - isset($extras) && !empty($extras) &&
605 - isset($extras['original']) && !empty($extras['original'])
606 - ) {
607 - $has_original = true;
608 - $file = trailingslashit($upload_dir['basedir']).$extras['original']['source_path'];
609 - if(file_exists($file) && $delete_main_file) {
610 - wp_delete_file($file);
611 - }
1106 + }
1107 + if (
1108 + isset($item['original_source_path']) && !empty($item['original_source_path'])
1109 + ) {
1110 + $has_original = true;
1111 + $file = trailingslashit($upload_dir['basedir']).$item['original_source_path'];
1112 + if(file_exists($file) && $delete_main_file) {
1113 + $files_to_remove[] = $file;
612 1114 }
613 1115 }
614 1116 if(file_exists($file_path)) {
615 1117 if ($has_original || (!$has_original && $delete_main_file)) {
616 - wp_delete_file($file_path);
1118 + $files_to_remove[] = $file_path;
617 1119 }
618 1120 }
1121 +
1122 +
1123 + $files_to_remove = apply_filters('wpmcs_files_to_remove_from_server', array_unique($files_to_remove), $item['source_id'], $item);
1124 +
1125 + if (!Utils::is_empty($files_to_remove)) {
1126 + foreach ($files_to_remove as $file) {
1127 + wp_delete_file($file, true);
1128 + }
1129 + }
1130 +
619 1131 return true;
620 1132 }
621 -
622 1133
623 1134 /**
624 1135 * Ensures only one instance of Class is loaded or can be loaded.
625 1136 *