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/services/gcloud.php +365 -152 1.2.12trunk View file →
@@ -3,10 +3,10 @@
3 3
4 4 defined('ABSPATH') || exit;
5 5
6 6 // Libraries
7 -use Dudlewebs\WPMCS\Google\Cloud\Storage\StorageClient;
8 -use Dudlewebs\WPMCS\Google\Cloud\Core\Exception\ServiceException;
7 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Storage\StorageClient;
8 +use Dudlewebs\WPMCS\GCP\Google\Cloud\Core\Exception\ServiceException;
9 9
10 10 use Exception;
11 11
12 12 class GCloud {
@@ -28,23 +28,26 @@
28 28 /**
29 29 * Admin constructor.
30 30 * @since 1.0.0
31 31 */
32 - public function __construct() {
32 + public function __construct($credentials = null) {
33 33 $this->assets_url = WPMCS_ASSETS_URL;
34 34 $this->version = WPMCS_VERSION;
35 35 $this->token = WPMCS_TOKEN;
36 36
37 37 // Initialize setup
38 - $this->init();
38 + $this->init($credentials);
39 39 }
40 40
41 41 /**
42 42 * Initialise Client
43 + *
44 + * @param array|null $credentials Optional explicit credentials; falls back to
45 + * Utils::get_credentials() when omitted.
43 46 */
44 - public function init() {
47 + public function init($credentials = null) {
45 48 $this->settings = Utils::get_settings();
46 - $this->credentials = Utils::get_credentials();
49 + $this->credentials = $credentials !== null ? $credentials : Utils::get_credentials();
47 50 $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
48 51 ? $this->credentials['config']
49 52 : [];
50 53 $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
@@ -93,15 +96,16 @@
93 96 * Verify Credentials
94 97 * @since 1.0.0
95 98 * @return boolean
96 99 */
97 - public function verifyCredentials( $config_json ){
98 - if (isset($config_json) && !empty($config_json)) {
100 + public function verifyCredentials( $config = [] ){
101 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
102 + if (!Service::has_missing_fields([$config_json])) {
99 103 if(!Utils::is_json($config_json)){
100 104 return [
101 105 'success' => false,
102 106 'code' => 200,
103 - 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
107 + 'message' => esc_html__('Invalid JSON configuration, please try again', 'media-cloud-sync'),
104 108 ];
105 109 }
106 110
107 111 try {
@@ -117,9 +121,13 @@
117 121 'message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'),
118 122 ];
119 123 }
120 124
121 -
125 + $result = [
126 + 'success' => false,
127 + 'code' => 200,
128 + 'message' => esc_html__('Please check the authorization details', 'media-cloud-sync'),
129 + ];
122 130
123 131 try {
124 132 $bucket = $googleClient->bucket($this->token . '_dummy-bucket-for-auth-check');
125 133 $exists = $bucket->exists(); // Triggers the API call
@@ -184,13 +192,19 @@
184 192 * Verify Bucket Exists
185 193 * @since 1.0.0
186 194 * @return boolean
187 195 */
188 - public function verifyBucketExist( $config_json, $bucket_name ){
189 - if ( !( isset($config_json) && !empty($config_json) && !empty($bucket_name) ) ) {
196 + public function verifyBucketExist( $config = [], $bucketConfig = [] ){
197 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
198 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
199 + if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
190 200 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
191 201 }
192 202
203 + if ( !Utils::is_json( $config_json ) ) {
204 + return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
205 + }
206 +
193 207 try {
194 208 $config_array = json_decode($config_json, true);
195 209 if (is_array($config_array)) {
196 210 $googleClient = new StorageClient([
@@ -226,15 +240,8 @@
226 240 'code' => 200,
227 241 'success' => false,
228 242 ];
229 243 }
230 -
231 -
232 - if($bucket_found) {
233 - return array('message' => esc_html__('Bucket exist', 'media-cloud-sync'), 'code' => 200, 'success' => true);
234 - } else {
235 - return array('message' => esc_html__("Bucket choosen does not exist / does not have read permission", 'media-cloud-sync'), 'code' => 200, 'success' => false);
236 - }
237 244 } catch (Exception $ex) {
238 245 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
239 246 }
240 247 }
@@ -244,10 +251,13 @@
244 251 * Create Bucket
245 252 * @since 1.0.0
246 253 * @return boolean
247 254 */
248 - public function createBucket( $config_json, $region, $bucket_name ){
249 - if ( !( isset($config_json) && !empty($config_json) && !empty($region) && !empty($bucket_name) ) ) {
255 + public function createBucket( $config = [], $bucketConfig = [] ){
256 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
257 + $region = isset($bucketConfig['region']) ? $bucketConfig['region'] : '';
258 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
259 + if ( Service::has_missing_fields([$config_json, $region, $bucket_name]) ) {
250 260 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
251 261 }
252 262
253 263 if ( !Utils::is_json( $config_json ) ) {
@@ -325,9 +335,9 @@
325 335 'success' => true,
326 336 ];
327 337
328 338 } catch (Exception $ex) {
329 - return ['message' => $e->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
339 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
330 340 }
331 341 }
332 342
333 343 /**
@@ -333,10 +343,12 @@
333 343 /**
334 344 * Check Bucket Write Permission
335 345 * @since 1.0.0
336 346 */
337 - public function verifyObjectWritePermission($config_json, $bucket_name){
338 - if ( !( isset($config_json) && !empty($config_json) && !empty($bucket_name) ) ) {
347 + public function verifyObjectWritePermission( $config = [], $bucketConfig = [] ) {
348 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
349 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
350 + if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
339 351 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
340 352 }
341 353
342 354 if ( !Utils::is_json( $config_json ) ) {
@@ -362,9 +374,9 @@
362 374 } else {
363 375 return ['message' => esc_html__('No Buckets found', 'media-cloud-sync'), 'code' => 200, 'success' => false];
364 376 }
365 377 if ($bucket_found) {
366 - $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
378 + $object_key = Utils::get_permission_check_object_key();
367 379
368 380 // Prepare a temporary file with content to check write permission
369 381 $stream = fopen('php://temp', 'r+');
370 382 fwrite($stream, 'This is a test object to check write permission.');
@@ -376,14 +388,11 @@
376 388 [
377 389 'name' => $object_key,
378 390 ]
379 391 );
380 - if(is_resource($stream)) {
381 - fclose($stream);
382 - }
383 392
384 393 if ($object->exists()) {
385 - return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
394 + return ['message' => esc_html__('Bucket write permission verified successfully', 'media-cloud-sync'), 'code' => 200, 'success' => true];
386 395 } else {
387 396 return ['message' => esc_html__('Bucket write permission not verified', 'media-cloud-sync'), 'code' => 200, 'success' => false];
388 397 }
389 398 }
@@ -388,8 +397,12 @@
388 397 }
389 398 }
390 399 } catch (Exception $ex) {
391 400 return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
401 + } finally {
402 + if (isset($stream) && is_resource($stream)) {
403 + fclose($stream);
404 + }
392 405 }
393 406 }
394 407
395 408 /**
@@ -395,10 +408,13 @@
395 408 /**
396 409 * Check Bucket Delete Permission
397 410 * @since 1.0.0
398 411 */
399 - public function verifyObjectDeletePermission($config_json, $bucket_name){
400 - if ( !( isset($config_json) && !empty($config_json) && !empty($bucket_name) ) ) {
412 + public function verifyObjectDeletePermission( $config = [], $bucketConfig = [] ) {
413 + $config_json = isset($config['config_json']) ? $config['config_json'] : '';
414 + $bucket_name = isset($bucketConfig['bucket_name']) ? $bucketConfig['bucket_name'] : '';
415 +
416 + if ( Service::has_missing_fields([$config_json, $bucket_name]) ) {
401 417 return ['message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'), 'code' => 200, 'success' => false];
402 418 }
403 419 if( !Utils::is_json( $config_json ) ) {
404 420 return ['message' => esc_html__('JSON Configuration is invalid', 'media-cloud-sync'), 'code' => 200, 'success' => false];
@@ -419,9 +435,9 @@
419 435 }
420 436
421 437 $bucket = $googleClient->bucket($bucket_name);
422 438 try {
423 - $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
439 + $object_key = Utils::get_permission_check_object_key();
424 440
425 441 // Try fetching a dummy object to test access
426 442 $object = $bucket->object($object_key);
427 443 if ($object->exists()) {
@@ -447,9 +463,9 @@
447 463 ];
448 464 }
449 465 } catch (Exception $ex) {
450 466 return [
451 - 'message' => esc_html__('Object does not exist or credentials are invalid: ', 'media-cloud-sync') . $e->getMessage(),
467 + 'message' => esc_html__('Object does not exist or credentials are invalid: ', 'media-cloud-sync') . $ex->getMessage(),
452 468 'code' => 200,
453 469 'success' => false,
454 470 ];
455 471 }
@@ -468,11 +484,10 @@
468 484 'status' => false,
469 485 'message' => '',
470 486 'lastChecked' => time(),
471 487 ];
472 - if (empty($this->gcloudClient) || empty($this->bucket_name)) {
488 + if (Service::has_missing_fields([$this->gcloudClient, $this->bucket_name])) {
473 489 $result['message'] = esc_html__('Please check the authorization details', 'media-cloud-sync');
474 - Utils::set_status('cdnRead', $result);
475 490 return [
476 491 'message' => $result['message'],
477 492 'code' => 200,
478 493 'success' => false,
@@ -480,12 +495,12 @@
480 495 ];
481 496 }
482 497
483 498 try {
484 - $object_key = Utils::generate_object_key($this->token . '_dummy-object-for-bucket-permission-check', '');
499 + $object_key = Utils::get_permission_check_object_key();
485 500
486 501 // Check if the object was created successfully
487 - if (!$this->bucket->object($object_key)->exists()) {
502 + if (!$this->exists($object_key)) {
488 503 // Create a dummy object to check write permission
489 504 $stream = fopen('php://temp', 'r+');
490 505 fwrite($stream, 'This is a test object to check read permission.');
491 506 rewind($stream);
@@ -492,18 +507,15 @@
492 507 $this->bucket->upload(
493 508 $stream,
494 509 [
495 510 'name' => $object_key,
511 + 'metadata' => ['cacheControl' => 'no-cache, no-store, must-revalidate'],
496 512 ]
497 513 );
498 - if (is_resource($stream)) {
499 - fclose($stream);
500 - }
501 514 // Re-check if the object was created successfully
502 - if (!$this->bucket->object($object_key)->exists()) {
515 + if (!$this->exists($object_key)) {
503 516 $result['status'] = false;
504 517 $result['message'] = esc_html__('Failed to create an object for read permission check, please check service configuration', 'media-cloud-sync');
505 - Utils::set_status('cdnRead', $result);
506 518 return [
507 519 'message' => $result['message'],
508 520 'code' => 200,
509 521 'success' => false,
@@ -514,24 +526,30 @@
514 526
515 527 $url = $this->generate_file_url($object_key);
516 528 $cdn_url = Cdn::may_generate_cdn_url($url, $object_key);
517 529
518 - $headers = @get_headers($cdn_url);
519 - if (strpos($headers[0], '200') !== false) {
530 + // Never trust a cached response for this fixed, predictable URL — a stale cached
531 + // error would otherwise keep failing the check long after real access is fine.
532 + $no_cache_context = stream_context_create(['http' => ['header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"]]);
533 + $headers = @get_headers($cdn_url, false, $no_cache_context);
534 + $status_code = (is_array($headers) && !empty($headers[0]) && preg_match('/\s(\d{3})\s/', $headers[0], $matches))
535 + ? (int) $matches[1]
536 + : 0;
537 +
538 + if ($status_code === 200) {
520 539 $result['status'] = true;
521 540 $result['message'] = esc_html__('Objects are accessible to Read', 'media-cloud-sync');
522 - } else if (strpos($headers[0], '403') !== false) {
541 + } else if ($status_code === 403) {
523 542 $result['status'] = false;
524 - if($this->cdnConfig['service'] == $this->service) {
543 + if(isset($this->cdnConfig['service']) && $this->cdnConfig['service'] == $this->service) {
525 544 $result['message'] = esc_html__('Access Denied. Please check your bucket policy. Public Read Access is required.', 'media-cloud-sync');
526 545 } else {
527 546 $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
528 547 }
529 - $result['message'] = esc_html__('Access Denied. Please check your bucket policy', 'media-cloud-sync');
530 - } else if (strpos($headers[0], '404') !== false) {
548 + } else if ($status_code === 404) {
531 549 $result['status'] = false;
532 550 $result['message'] = esc_html__('Object not found. Please check your bucket policy', 'media-cloud-sync');
533 - } else if (strpos($headers[0], '500') !== false) {
551 + } else if ($status_code === 500) {
534 552 $result['status'] = false;
535 553 $result['message'] = esc_html__('Internal Server error. Please check your bucket policy', 'media-cloud-sync');
536 554 } else {
537 555 $result['status'] = false;
@@ -536,9 +554,8 @@
536 554 } else {
537 555 $result['status'] = false;
538 556 $result['message'] = esc_html__('Objects are not accessible to read', 'media-cloud-sync');
539 557 }
540 - Utils::set_status('cdnRead', $result);
541 558 $this->deleteSingle($object_key);
542 559 return [
543 560 'message' => $result['message'],
544 561 'code' => 200,
@@ -545,11 +562,17 @@
545 562 'success' => $result['status'],
546 563 'lastChecked' => $result['lastChecked'],
547 564 ];
548 565 } catch (ServiceException $ex) {
549 - return ['message' => $ex->getMessage(), 'code' => 200, 'success' => false];
566 + $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
567 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
550 568 } catch (Exception $ex) {
551 - return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false];
569 + $result['message'] = $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync');
570 + return ['message' => $ex->getMessage() ?? esc_html__('Please check the authorization details', 'media-cloud-sync'), 'code' => 200, 'success' => false, 'lastChecked' => time()];
571 + } finally {
572 + if (isset($stream) && is_resource($stream)) {
573 + fclose($stream);
574 + }
552 575 }
553 576 }
554 577
555 578 /**
@@ -560,9 +583,9 @@
560 583 if ($this->gcloudClient) {
561 584 try {
562 585 $bucket = $this->gcloudClient->bucket($this->token . '_dummy-bucket-for-auth-check');
563 586 $exists = $bucket->exists(); // Triggers the API call
564 - return false;
587 + return true;
565 588 } catch (ServiceException $e) {
566 589 $statusCode = $e->getCode();
567 590
568 591 $validErrors = [200, 403, 404];
@@ -585,8 +608,9 @@
585 608 *
586 609 */
587 610 public function toPrivate($key) {
588 611 if(!$key) return false;
612 + if(!$this->bucket) return false;
589 613
590 614 try {
591 615 $object = $this->bucket->object($key);
592 616 if ($object->exists()) {
@@ -610,8 +634,9 @@
610 634 *
611 635 */
612 636 public function toPublic($key) {
613 637 if(!$key) return false;
638 + if(!$this->bucket) return false;
614 639
615 640 try {
616 641 $object = $this->bucket->object($key);
617 642 if ($object->exists()) {
@@ -632,13 +657,14 @@
632 657 /**
633 658 * Check the object exist
634 659 * @since 1.1.8
635 660 */
636 - public function exists($key) {
661 + public function exists($key, $bucket = null) {
637 662 if(!$key) return false;
638 663
639 664 try {
640 - $object = $this->bucket->object($key);
665 + $bucket = $bucket ?? $this->bucket;
666 + $object = $bucket->object($key);
641 667 if ($object->exists()) {
642 668 return true;
643 669 } else {
644 670 return false;
@@ -655,116 +681,147 @@
655 681 }
656 682
657 683
658 684 /**
685 + * List Objects — $delimiter = null gives a flat/recursive listing instead of one folder level.
686 + * resultLimit=$maxKeys caps the iterator to this page only (Bucket::objects() would otherwise auto-paginate the whole bucket).
687 + * @since 1.3.13
688 + */
689 + public function listObjects($prefix = '', $continuationToken = null, $maxKeys = 1000, $delimiter = '/') {
690 + if (!$this->bucket) {
691 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync'), 'folders' => [], 'objects' => [], 'next_token' => null];
692 + }
693 + try {
694 + $options = [
695 + 'maxResults' => $maxKeys,
696 + 'resultLimit' => $maxKeys,
697 + ];
698 + if (!empty($delimiter)) {
699 + $options['delimiter'] = $delimiter;
700 + }
701 + if (!empty($prefix)) {
702 + $options['prefix'] = $prefix;
703 + }
704 + if (!empty($continuationToken)) {
705 + $options['pageToken'] = $continuationToken;
706 + }
707 +
708 + $iterator = $this->bucket->objects($options);
709 +
710 + $objects = [];
711 + foreach ($iterator as $object) {
712 + $key = $object->name();
713 + if ($key === $prefix) {
714 + continue; // the folder placeholder object itself, not a file
715 + }
716 + $info = $object->info();
717 + $objects[] = [
718 + 'key' => $key,
719 + 'size' => isset($info['size']) ? (int) $info['size'] : 0,
720 + 'last_modified' => isset($info['updated']) ? $info['updated'] : '',
721 + ];
722 + }
723 +
724 + return [
725 + 'success' => true,
726 + 'code' => 200,
727 + 'message' => '',
728 + 'folders' => $iterator->prefixes(),
729 + 'objects' => $objects,
730 + 'next_token' => $iterator->nextResultToken(),
731 + ];
732 + } catch (ServiceException $e) {
733 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
734 + } catch (Exception $e) {
735 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage(), 'folders' => [], 'objects' => [], 'next_token' => null];
736 + }
737 + }
738 +
739 + /**
659 740 * Upload Single
660 741 * @since 1.0.0
661 742 * @return boolean
662 743 */
663 - public function uploadSingle($media_absolute_path, $media_path, $prefix=''){
664 - $result = array();
744 + public function uploadSingle($absolute_source_path, $relative_source_path, $prefix=''){
665 745 if (
666 - isset($media_absolute_path) && !empty($media_absolute_path) &&
667 - isset($media_path) && !empty($media_path)
746 + isset($absolute_source_path) && !empty($absolute_source_path) &&
747 + isset($relative_source_path) && !empty($relative_source_path)
668 748 ) {
669 - $file_name = wp_basename( $media_path );
749 + $file_name = wp_basename( $relative_source_path );
670 750 if ($file_name) {
671 - $upload_path = Utils::generate_object_key($media_path, $prefix);
751 + $upload_path = Utils::generate_object_key($relative_source_path, $prefix);
752 + return $this->execute_upload($absolute_source_path, $upload_path);
753 + }
754 + return [
755 + 'success' => false,
756 + 'code' => 200,
757 + 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync'),
758 + ];
759 + }
760 + return [
761 + 'success' => false,
762 + 'code' => 200,
763 + 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
764 + ];
765 + }
672 766
673 - // Decide Multipart upload or normal put object
674 - if (filesize($media_absolute_path) <= Schema::getConstant('GCLOUD_MULTIPART_MIN_FILE_SIZE')) {
675 - // Upload a publicly accessible file. The file size and type are determined by the SDK.
676 - try {
767 + /**
768 + * Upload a local file to an exact destination key (no Utils::generate_object_key() derivation).
769 + * @since 1.4.0
770 + */
771 + public function uploadObjectAtKey($absolute_source_path, $key) {
772 + return $this->execute_upload($absolute_source_path, $key);
773 + }
677 774
678 - $upload = $this->bucket->upload(
679 - fopen($media_absolute_path, 'r'),
680 - [
681 - 'name' => $upload_path,
682 - ]
683 - );
775 + // Chunked upload above GCLOUD_MULTIPART_MIN_FILE_SIZE, single request below it — same
776 + // threshold uploadSingle() always used, now shared with uploadObjectAtKey().
777 + private function execute_upload($absolute_source_path, $key) {
778 + $options = ['name' => $key];
779 + if (filesize($absolute_source_path) > Schema::getConstant('GCLOUD_MULTIPART_MIN_FILE_SIZE')) {
780 + $options['chunkSize'] = 262144 * 2;
781 + }
782 + $cache_control = Utils::get_cache_control_header();
783 + if ($cache_control) {
784 + $options['cacheControl'] = $cache_control;
785 + }
684 786
685 - $object = $this->bucket->object($upload_path);
787 + try {
788 + $handle = fopen($absolute_source_path, 'rb');
789 + $upload = $this->bucket->upload($handle, $options);
686 790
687 - if ($object->exists()) {
688 - $result = array(
689 - 'success' => true,
690 - 'code' => 200,
691 - 'file_url' => $this->generate_file_url($upload_path),
692 - 'key' => $upload_path,
693 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
694 - );
695 - } else {
696 - $result = array(
697 - 'success' => false,
698 - 'code' => 200,
699 - 'message' => esc_html__('Object not found at server.', 'media-cloud-sync'),
700 - );
701 - }
702 - } catch (Exception $e) {
703 - $result = array(
704 - 'success' => false,
705 - 'code' => 200,
706 - 'message' => $e->getMessage(),
707 - );
708 - }
709 - } else {
710 - try {
711 - $upload = $this->bucket->upload(
712 - fopen($media_absolute_path, 'r'),
713 - [
714 - 'name' => $upload_path,
715 - 'chunkSize' => 262144 * 2,
716 - ]
717 - );
718 -
719 - $object = $this->bucket->object($upload_path);
720 -
721 - if ($object->exists()) {
722 - $result = array(
723 - 'success' => true,
724 - 'code' => 200,
725 - 'file_url' => $this->generate_file_url($upload_path),
726 - 'key' => $upload_path,
727 - 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
728 - );
729 - } else {
730 - $result = array(
731 - 'success' => false,
732 - 'code' => 200,
733 - 'message' => esc_html__('Something happened while uploading to server', 'media-cloud-sync'),
734 - );
735 - }
736 - } catch (Exception $e) {
737 - $result = array(
738 - 'success' => false,
739 - 'code' => 200,
740 - 'message' => $e->getMessage(),
741 - );
742 - }
743 - }
744 - } else {
745 - $result = array(
746 - 'success' => false,
747 - 'code' => 200,
748 - 'message' => esc_html__('Check the file you are trying to upload. Please try again', 'media-cloud-sync'),
749 - );
791 + if ($upload->exists()) {
792 + return [
793 + 'success' => true,
794 + 'code' => 200,
795 + 'file_url' => $this->generate_file_url($key),
796 + 'key' => $key,
797 + 'message' => esc_html__('File Uploaded Successfully', 'media-cloud-sync'),
798 + ];
750 799 }
751 - } else {
752 - $result = array(
753 - 'success' => false,
754 - 'code' => 200,
755 - 'message' => esc_html__('Insufficient Data. Please try again', 'media-cloud-sync'),
756 - );
800 + return [
801 + 'success' => false,
802 + 'code' => 200,
803 + 'message' => esc_html__('Object not found at server.', 'media-cloud-sync'),
804 + ];
805 + } catch (Exception $e) {
806 + return [
807 + 'success' => false,
808 + 'code' => 200,
809 + 'message' => $e->getMessage(),
810 + ];
811 + } finally {
812 + if (isset($handle) && is_resource($handle)) {
813 + fclose($handle);
814 + }
757 815 }
758 - return $result;
759 816 }
760 817
761 -
762 818 /**
763 819 * Save object to server
764 820 * @since 1.0.0
765 821 */
766 822 public function object_to_server($key, $save_path){
823 + if(!$this->bucket) return false;
767 824 try {
768 825 $object = $this->bucket->object($key);
769 826 if ($object->exists()) {
770 827 $object->downloadToFile($save_path);
@@ -777,9 +834,143 @@
777 834 }
778 835 return false;
779 836 }
780 837
838 + /**
839 + * Object bytes in memory, no local file — for callers (e.g. zip download) that need
840 + * the content itself rather than a copy on the server's filesystem.
841 + * @since 1.3.13
842 + */
843 + public function get_object_content($key) {
844 + if(!$this->bucket) return false;
845 + try {
846 + $object = $this->bucket->object($key);
847 + if ($object->exists()) {
848 + return $object->downloadAsString();
849 + }
850 + } catch (Exception $e) {
851 + return false;
852 + }
853 + return false;
854 + }
781 855
856 + /**
857 + * Deletes the live generation, then best-effort purges every prior generation too — a
858 + * bucket with Object Versioning enabled otherwise keeps old generations (and the storage
859 + * they use) around at the old key. The live delete happens unconditionally first, in its
860 + * own try/catch, so the object still ends up gone even if the generation-listing call
861 + * below fails for any reason.
862 + * @since 1.3.14
863 + */
864 + public function purge_all_versions($key) {
865 + if (!$this->bucket) {
866 + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')];
867 + }
868 +
869 + try {
870 + $this->bucket->object($key)->delete();
871 + } catch (ServiceException $e) {
872 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
873 + } catch (\Exception $e) {
874 + return ['success' => false, 'code' => 200, 'message' => $e->getMessage()];
875 + }
876 +
877 + // Best-effort only from here — the live copy above is already gone regardless of
878 + // whether this bucket has Object Versioning enabled or this call succeeds.
879 + try {
880 + foreach ($this->bucket->objects(['prefix' => $key, 'versions' => true]) as $object) {
881 + if ($object->name() === $key) {
882 + $object->delete();
883 + }
884 + }
885 + } catch (ServiceException $e) {
886 + // Generation history cleanup failed — not fatal, live object is gone.
887 + } catch (\Exception $e) {
888 + // Generation history cleanup failed — not fatal, live object is gone.
889 + }
890 +
891 + return ['success' => true, 'code' => 200, 'message' => esc_html__('Purged Successfully', 'media-cloud-sync')];
892 + }
893 +
894 +
895 + /**
896 + * Copy an object to a new path in Google Cloud Storage
897 + *
898 + * @param string $key Original object key (path in bucket)
899 + * @param string $new_path Destination object key
900 + * @return bool True if object was copied successfully, false otherwise
901 + * @since 1.3.4
902 + */
903 + // Trusts copy()'s own success/failure rather than pre/post-verifying with extra
904 + // exists() calls — each one is a full network round-trip, and with move/copy processing
905 + // keys sequentially, extra round-trips per file add up fast on a folder with many files.
906 + // copy() itself throws (caught below) if the source is missing or the copy otherwise
907 + // fails, so nothing is lost by not checking first.
908 + public function copy_to_new_path($key, $new_path) {
909 + if (!$this->bucket) {
910 + return [
911 + 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
912 + 'code' => 200,
913 + 'success' => false
914 + ];
915 + }
916 + try {
917 + $sourceObject = $this->bucket->object($key);
918 + $sourceObject->copy($this->bucket, ['name' => $new_path]);
919 + return [
920 + 'success' => true,
921 + 'code' => 200,
922 + 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
923 + ];
924 + } catch (ServiceException $e) {
925 + return [
926 + 'success' => false,
927 + 'code' => 200,
928 + 'message' => $e->getMessage()
929 + ];
930 + } catch (\Exception $e) {
931 + return [
932 + 'success' => false,
933 + 'code' => 200,
934 + 'message' => $e->getMessage()
935 + ];
936 + }
937 + }
938 +
939 + // Like copy_to_new_path() but into an explicit (possibly different) bucket — needs write
940 + // access there too, so callers should fall back to download+upload on failure.
941 + public function copy_to_bucket($key, $new_key, $dest_bucket) {
942 + if (!$this->bucket || !$this->gcloudClient) {
943 + return [
944 + 'message' => esc_html__('Client not configured', 'media-cloud-sync'),
945 + 'code' => 200,
946 + 'success' => false
947 + ];
948 + }
949 + try {
950 + $sourceObject = $this->bucket->object($key);
951 + $sourceObject->copy($this->gcloudClient->bucket($dest_bucket), ['name' => $new_key]);
952 + return [
953 + 'success' => true,
954 + 'code' => 200,
955 + 'message' => esc_html__('File copied successfully', 'media-cloud-sync')
956 + ];
957 + } catch (ServiceException $e) {
958 + return [
959 + 'success' => false,
960 + 'code' => 200,
961 + 'message' => $e->getMessage()
962 + ];
963 + } catch (\Exception $e) {
964 + return [
965 + 'success' => false,
966 + 'code' => 200,
967 + 'message' => $e->getMessage()
968 + ];
969 + }
970 + }
971 +
972 +
782 973 /**
783 974 * Delete Single
784 975 * @since 1.0.0
785 976 * @return boolean
@@ -785,8 +976,15 @@
785 976 * @return boolean
786 977 */
787 978 public function deleteSingle($key){
788 979 $result = array();
980 + if (!$this->bucket) {
981 + return array(
982 + 'success' => false,
983 + 'code' => 200,
984 + 'message' => esc_html__('Client not configured', 'media-cloud-sync')
985 + );
986 + }
789 987 if (isset($key) && !empty($key)) {
790 988 try {
791 989 $object = $this->bucket->object($key);
792 990 $object->delete();
@@ -822,34 +1020,41 @@
822 1020 }
823 1021
824 1022
825 1023 /**
826 - * get presigned URL
1024 + * get private URL
827 1025 * @since 1.0.0
828 1026 * @return boolean
829 1027 */
830 - public function get_presigned_url($key) {
1028 + public function get_private_url($key) {
831 1029 $result = array();
1030 + if (!$this->bucket) {
1031 + return array(
1032 + 'success' => false,
1033 + 'code' => 200,
1034 + 'message' => esc_html__('Client not configured', 'media-cloud-sync')
1035 + );
1036 + }
832 1037 if (isset($key) && !empty($key)) {
833 1038 try {
834 1039 $object = $this->bucket->object($key);
835 1040
836 - $expires = isset($this->settings['presigned_expire']) ? $this->settings['presigned_expire'] : 20;
1041 + $expires = isset($this->settings['private_url_expire']) ? $this->settings['private_url_expire'] : 20;
837 1042
838 - $presignedUrl = $object->signedUrl(new \DateTime(sprintf('+%s minutes', $expires)));
1043 + $privateUrl = $object->signedUrl(new \DateTime(sprintf('+%s minutes', $expires)));
839 1044
840 - if ($presignedUrl) {
1045 + if ($privateUrl) {
841 1046 $result = array(
842 1047 'success' => true,
843 1048 'code' => 200,
844 - 'file_url' => $presignedUrl,
845 - 'message' => esc_html__('Got Presigned URL Successfully', 'media-cloud-sync'),
1049 + 'file_url' => $privateUrl,
1050 + 'message' => esc_html__('Got Private URL Successfully', 'media-cloud-sync'),
846 1051 );
847 1052 } else {
848 1053 $result = array(
849 1054 'success' => false,
850 1055 'code' => 200,
851 - 'message' => esc_html__('Error getting presigned URL', 'media-cloud-sync'),
1056 + 'message' => esc_html__('Error getting private URL', 'media-cloud-sync'),
852 1057 );
853 1058 }
854 1059 } catch (Exception $e) {
855 1060 $result = array(
@@ -871,9 +1076,9 @@
871 1076
872 1077 /**
873 1078 * Generate file URL
874 1079 */
875 - private function generate_file_url($key){
1080 + public function generate_file_url($key){
876 1081 $domain = $this->get_domain();
877 1082
878 1083 return apply_filters('wpmcs_generate_google_file_url',
879 1084 $domain . '/' . $this->bucket_name . '/' . $key,
@@ -881,8 +1086,16 @@
881 1086 $this->bucket_name
882 1087 );
883 1088 }
884 1089
1090 + /**
1091 + * Is provider URL
1092 + * @since 1.3.6
1093 + */
1094 + public function is_provider_url($url) {
1095 + $domain = $this->get_domain();
1096 + return (strpos($url, $domain . '/' . $this->bucket_name . '/') !== false);
1097 + }
885 1098
886 1099 /**
887 1100 * Get domain URL
888 1101 */