feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)
Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
This commit is contained in:
174
classes/items/download-handler.php
Normal file
174
classes/items/download-handler.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use DeliciousBrains\WP_Offload_Media\Providers\Storage\Storage_Provider;
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
class Download_Handler extends Item_Handler {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static $item_handler_key = 'download';
|
||||
|
||||
/**
|
||||
* The default options that should be used if none supplied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function default_options() {
|
||||
return array(
|
||||
'full_source_paths' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a manifest based on the item.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $options
|
||||
*
|
||||
* @return Manifest
|
||||
*/
|
||||
protected function pre_handle( Item $as3cf_item, array $options ) {
|
||||
$manifest = new Manifest();
|
||||
$file_paths = array();
|
||||
|
||||
foreach ( $as3cf_item->objects() as $object_key => $object ) {
|
||||
$file = $as3cf_item->full_source_path( $object_key );
|
||||
|
||||
if ( 0 < count( $options['full_source_paths'] ) && ! in_array( $file, $options['full_source_paths'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_paths[ $object_key ] = $file;
|
||||
}
|
||||
|
||||
$file_paths = array_unique( $file_paths );
|
||||
|
||||
foreach ( $file_paths as $object_key => $file_path ) {
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
$manifest->objects[] = array(
|
||||
'args' => array(
|
||||
'Bucket' => $as3cf_item->bucket(),
|
||||
'Key' => $as3cf_item->provider_key( $object_key ),
|
||||
'SaveAs' => $file_path,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the downloads.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
if ( ! empty( $manifest->objects ) ) {
|
||||
// This test is "late" so that we don't raise the error if the local files exist anyway.
|
||||
// If the provider of this item is different from what's currently configured,
|
||||
// we'll return an error.
|
||||
$current_provider = $this->as3cf->get_storage_provider();
|
||||
if ( ! is_null( $current_provider ) && $current_provider::get_provider_key_name() !== $as3cf_item->provider() ) {
|
||||
$error_msg = sprintf(
|
||||
__( '%1$s with ID %2$d is offloaded to a different provider than currently configured', 'amazon-s3-and-cloudfront' ),
|
||||
$this->as3cf->get_source_type_name( $as3cf_item->source_type() ),
|
||||
$as3cf_item->source_id()
|
||||
);
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
} else {
|
||||
$provider_client = $this->as3cf->get_provider_client( $as3cf_item->region() );
|
||||
|
||||
foreach ( $manifest->objects as &$manifest_object ) {
|
||||
// Save object to a file.
|
||||
$result = $this->download_object( $provider_client, $manifest_object['args'] );
|
||||
|
||||
$manifest_object['download_result']['status'] = self::STATUS_OK;
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
$manifest_object['download_result']['status'] = self::STATUS_FAILED;
|
||||
$manifest_object['download_result']['message'] = $result->get_error_message();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform post handle tasks. Log errors, update filesize totals etc.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
$error_count = 0;
|
||||
|
||||
foreach ( $manifest->objects as $manifest_object ) {
|
||||
if ( self::STATUS_OK !== $manifest_object['download_result']['status'] ) {
|
||||
$error_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $error_count > 0 ) {
|
||||
$error_message = sprintf(
|
||||
__( 'There were %1$d errors downloading files for %2$s ID %3$d from bucket', 'amazon-s3-and-cloudfront' ),
|
||||
$error_count,
|
||||
$this->as3cf->get_source_type_name( $as3cf_item->source_type() ),
|
||||
$as3cf_item->source_id()
|
||||
);
|
||||
|
||||
return new WP_Error( 'download-error', $error_message );
|
||||
}
|
||||
|
||||
$as3cf_item->update_filesize_after_download_local();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an object from provider.
|
||||
*
|
||||
* @param Storage_Provider $provider_client
|
||||
* @param array $object
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function download_object( $provider_client, $object ) {
|
||||
// Make sure the local directory exists.
|
||||
$dir = dirname( $object['SaveAs'] );
|
||||
if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
|
||||
$error_msg = sprintf( __( 'The local directory %s does not exist and could not be created.', 'amazon-s3-and-cloudfront' ), $dir );
|
||||
$error_msg = sprintf( __( 'There was an error attempting to download the file %1$s from the bucket: %2$s', 'amazon-s3-and-cloudfront' ), $object['Key'], $error_msg );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
try {
|
||||
$provider_client->get_object( $object );
|
||||
} catch ( Exception $e ) {
|
||||
// If storage provider file doesn't exist, an empty local file will be created, clean it up.
|
||||
@unlink( $object['SaveAs'] ); //phpcs:ignore
|
||||
|
||||
$error_msg = sprintf( __( 'Error downloading %1$s from bucket: %2$s', 'amazon-s3-and-cloudfront' ), $object['Key'], $e->getMessage() );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
258
classes/items/item-handler.php
Normal file
258
classes/items/item-handler.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use Amazon_S3_And_CloudFront;
|
||||
use AS3CF_Error;
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Class Item_Handler
|
||||
*
|
||||
* Base class for item handler classes.
|
||||
*
|
||||
* @package DeliciousBrains\WP_Offload_Media\Items
|
||||
*/
|
||||
abstract class Item_Handler {
|
||||
/**
|
||||
* Status codes
|
||||
*/
|
||||
const STATUS_OK = 'ok';
|
||||
const STATUS_FAILED = 'failed';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static $item_handler_key;
|
||||
|
||||
/**
|
||||
* @var Amazon_S3_And_CloudFront
|
||||
*/
|
||||
protected $as3cf;
|
||||
|
||||
/**
|
||||
* AS3CF_Item_Handler constructor.
|
||||
*
|
||||
* @param Amazon_S3_And_CloudFront $as3cf
|
||||
*/
|
||||
public function __construct( Amazon_S3_And_CloudFront $as3cf ) {
|
||||
$this->as3cf = $as3cf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item handler key name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_item_handler_key_name() {
|
||||
return static::$item_handler_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default options that should be used if none supplied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function default_options() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entrypoint for handling an item.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $options
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public function handle( Item $as3cf_item, array $options = array() ) {
|
||||
// Merge supplied option values into the defaults as long as supplied options are recognised.
|
||||
if ( empty( $options ) || ! is_array( $options ) ) {
|
||||
$options = array();
|
||||
}
|
||||
$options = array_merge( $this->default_options(), array_intersect_key( $options, $this->default_options() ) );
|
||||
|
||||
try {
|
||||
/**
|
||||
* Filter fires before handling an action on an item, allows action to be cancelled.
|
||||
*
|
||||
* This is a generic handler filter that includes the handler's key name as the last param.
|
||||
*
|
||||
* @param bool $cancel Should the action on the item be cancelled?
|
||||
* @param Item $as3cf_item The item that the action is being handled for.
|
||||
* @param array $options Handler dependent options that may have been set for the action.
|
||||
* @param array $handler_key_name The handler's key name as per `Item_Handler::get_item_handler_key_name()`.
|
||||
*
|
||||
* @see Item_Handler::get_item_handler_key_name()
|
||||
*/
|
||||
$cancel = apply_filters(
|
||||
'as3cf_pre_handle_item',
|
||||
/**
|
||||
* Filter fires before handling an action on an item, allows action to be cancelled.
|
||||
*
|
||||
* This is a handler specific filter whose name ends with the handler's key name.
|
||||
* Format is `as3cf_pre_handle_item_{item-handler-key-name}`.
|
||||
*
|
||||
* Example filter names:
|
||||
*
|
||||
* as3cf_pre_handle_item_upload
|
||||
* as3cf_pre_handle_item_download
|
||||
* as3cf_pre_handle_item_remove-local
|
||||
* as3cf_pre_handle_item_remove-provider
|
||||
* as3cf_pre_handle_item_update-acl
|
||||
*
|
||||
* For a more generic filter, use `as3cf_pre_handle_item`.
|
||||
*
|
||||
* @param bool $cancel Should the action on the item be cancelled?
|
||||
* @param Item $as3cf_item The item that the action is being handled for.
|
||||
* @param array $options Handler dependent options that may have been set for the action.
|
||||
*
|
||||
* @see Item_Handler::get_item_handler_key_name()
|
||||
*/
|
||||
apply_filters( 'as3cf_pre_handle_item_' . static::get_item_handler_key_name(), false, $as3cf_item, $options ),
|
||||
$as3cf_item,
|
||||
$options,
|
||||
static::get_item_handler_key_name()
|
||||
);
|
||||
} catch ( Exception $e ) {
|
||||
return $this->return_result( new WP_Error( $e->getMessage() ), $as3cf_item, $options );
|
||||
}
|
||||
|
||||
// Cancelled, let caller know that request was not handled.
|
||||
if ( false !== $cancel ) {
|
||||
// If something unexpected happened, let the caller know.
|
||||
if ( is_wp_error( $cancel ) ) {
|
||||
return $this->return_result( $cancel, $as3cf_item, $options );
|
||||
}
|
||||
|
||||
return $this->return_result( false, $as3cf_item, $options );
|
||||
}
|
||||
|
||||
$manifest = $this->pre_handle( $as3cf_item, $options );
|
||||
if ( is_wp_error( $manifest ) ) {
|
||||
return $this->return_result( $manifest, $as3cf_item, $options );
|
||||
}
|
||||
|
||||
// Nothing to do, let caller know that request was not handled.
|
||||
if ( empty( $manifest ) || empty( $manifest->objects ) ) {
|
||||
return $this->return_result( false, $as3cf_item, $options );
|
||||
}
|
||||
|
||||
$result = $this->handle_item( $as3cf_item, $manifest, $options );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $this->return_result( $result, $as3cf_item, $options );
|
||||
}
|
||||
|
||||
$result = $this->post_handle( $as3cf_item, $manifest, $options );
|
||||
|
||||
return $this->return_result( $result, $as3cf_item, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an Item and options to generate a Manifest for `handle_item`.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $options
|
||||
*
|
||||
* @return Manifest|WP_Error
|
||||
*/
|
||||
abstract protected function pre_handle( Item $as3cf_item, array $options );
|
||||
|
||||
/**
|
||||
* Perform action for Item using given Manifest.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
abstract protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options );
|
||||
|
||||
/**
|
||||
* Process results of `handle_item` as appropriate.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
abstract protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options );
|
||||
|
||||
/**
|
||||
* Helper to record errors and return them or optional supplied value.
|
||||
*
|
||||
* @param string|WP_Error $error_msg An error message or already constructed WP_Error.
|
||||
* @param mixed|null $return Optional return value instead of WP_Error.
|
||||
*
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
protected function return_handler_error( $error_msg, $return = null ) {
|
||||
if ( is_wp_error( $error_msg ) ) {
|
||||
foreach ( $error_msg->get_error_messages() as $msg ) {
|
||||
AS3CF_Error::Log( $msg );
|
||||
}
|
||||
} else {
|
||||
AS3CF_Error::log( $error_msg );
|
||||
}
|
||||
|
||||
if ( is_null( $return ) ) {
|
||||
return is_wp_error( $error_msg ) ? $error_msg : new WP_Error( 'exception', $error_msg );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires a couple of actions to let interested parties know that a handler has returned a result.
|
||||
*
|
||||
* @param bool|WP_Error $result Result for the action, either handled (true/false), or an error.
|
||||
* @param Item $as3cf_item The item that the action was being handled for.
|
||||
* @param array $options Handler dependent options that may have been set for the action.
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function return_result( $result, Item $as3cf_item, array $options ) {
|
||||
/**
|
||||
* Action fires after attempting to handle an action on an item.
|
||||
*
|
||||
* This is a handler specific action whose name ends with the handler's key name.
|
||||
* Format is `as3cf_post_handle_item_{item-handler-key-name}`.
|
||||
*
|
||||
* Example filter names:
|
||||
*
|
||||
* as3cf_post_handle_item_upload
|
||||
* as3cf_post_handle_item_download
|
||||
* as3cf_post_handle_item_remove-local
|
||||
* as3cf_post_handle_item_remove-provider
|
||||
* as3cf_post_handle_item_update-acl
|
||||
*
|
||||
* For a more generic filter, use `as3cf_post_handle_item`.
|
||||
*
|
||||
* @param bool|WP_Error $result Result for the action, either handled (true/false), or an error.
|
||||
* @param Item $as3cf_item The item that the action was being handled for.
|
||||
* @param array $options Handler dependent options that may have been set for the action.
|
||||
*
|
||||
* @see Item_Handler::get_item_handler_key_name()
|
||||
*/
|
||||
do_action( 'as3cf_post_handle_item_' . static::get_item_handler_key_name(), $result, $as3cf_item, $options );
|
||||
|
||||
/**
|
||||
* Action fires after attempting to handle an action on an item.
|
||||
*
|
||||
* This is a generic handler action that includes the handler's key name as the last param.
|
||||
*
|
||||
* @param bool|WP_Error $result Result for the action, either handled (true/false), or an error.
|
||||
* @param Item $as3cf_item The item that the action was being handled for.
|
||||
* @param array $options Handler dependent options that may have been set for the action.
|
||||
* @param array $handler_key_name The handler's key name as per `Item_Handler::get_item_handler_key_name()`.
|
||||
*
|
||||
* @see Item_Handler::get_item_handler_key_name()
|
||||
*/
|
||||
do_action( 'as3cf_post_handle_item', $result, $as3cf_item, $options, static::get_item_handler_key_name() );
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
2197
classes/items/item.php
Normal file
2197
classes/items/item.php
Normal file
File diff suppressed because it is too large
Load Diff
10
classes/items/manifest.php
Normal file
10
classes/items/manifest.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
class Manifest {
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $objects = array();
|
||||
}
|
||||
837
classes/items/media-library-item.php
Normal file
837
classes/items/media-library-item.php
Normal file
@@ -0,0 +1,837 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use Amazon_S3_And_CloudFront;
|
||||
use AS3CF_Utils;
|
||||
use WP_Error;
|
||||
|
||||
class Media_Library_Item extends Item {
|
||||
/**
|
||||
* Source type name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $source_type_name = 'Media Library Item';
|
||||
|
||||
/**
|
||||
* Internal source type identifier
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $source_type = 'media-library';
|
||||
|
||||
/**
|
||||
* Table that corresponds to this item type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $source_table = 'posts';
|
||||
|
||||
/**
|
||||
* Foreign key (if any) in the $source_table
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $source_fk = 'id';
|
||||
|
||||
/**
|
||||
* Item's summary type name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $summary_type_name = 'Media Library';
|
||||
|
||||
/**
|
||||
* Item's summary type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $summary_type = 'media-library';
|
||||
|
||||
/**
|
||||
* Item constructor.
|
||||
*
|
||||
* @param string $provider Storage provider key name, e.g. "aws".
|
||||
* @param string $region Region for item's bucket.
|
||||
* @param string $bucket Bucket for item.
|
||||
* @param string $path Key path for item (full sized if type has thumbnails etc).
|
||||
* @param bool $is_private Is the object private in the bucket.
|
||||
* @param int $source_id ID that source has.
|
||||
* @param string $source_path Path that source uses, could be relative or absolute depending on source.
|
||||
* @param string $original_filename An optional filename with no path that was previously used for the item.
|
||||
* @param array $extra_info An optional associative array of extra data to be associated with the item.
|
||||
* Recognised keys:
|
||||
* 'objects' => array of ...
|
||||
* -- 'thumbnail' => array of ...
|
||||
* -- -- 'source_file' => 'image-150x150.png'
|
||||
* -- -- 'is_private' => false
|
||||
* 'private_prefix' => 'private/'
|
||||
* For backwards compatibility, if a simple array is supplied it is treated as
|
||||
* private thumbnail sizes that should be private objects in the bucket.
|
||||
* @param int $id Optional Item record ID.
|
||||
* @param int $originator Optional originator of record from ORIGINATORS const.
|
||||
* @param bool $is_verified Optional flag as to whether Item's objects are known to exist.
|
||||
* @param bool $use_object_versioning Optional flag as to whether path prefix should use Object Versioning if type allows it.
|
||||
*/
|
||||
public function __construct(
|
||||
$provider,
|
||||
$region,
|
||||
$bucket,
|
||||
$path,
|
||||
$is_private,
|
||||
$source_id,
|
||||
$source_path,
|
||||
$original_filename = null,
|
||||
$extra_info = array(),
|
||||
$id = null,
|
||||
$originator = 0,
|
||||
$is_verified = true,
|
||||
$use_object_versioning = self::CAN_USE_OBJECT_VERSIONING
|
||||
) {
|
||||
// For Media Library items, the source path should be relative to the Media Library's uploads directory.
|
||||
$uploads = wp_upload_dir();
|
||||
|
||||
if ( false === $uploads['error'] && 0 === strpos( $source_path, $uploads['basedir'] ) ) {
|
||||
$source_path = AS3CF_Utils::unleadingslashit( substr( $source_path, strlen( $uploads['basedir'] ) ) );
|
||||
}
|
||||
|
||||
$objects = array();
|
||||
$private_prefix = null;
|
||||
|
||||
// Ensure re-hydration is clean.
|
||||
if ( ! empty( $extra_info ) && is_array( $extra_info ) ) {
|
||||
if ( isset( $extra_info['private_prefix'] ) ) {
|
||||
$private_prefix = $extra_info['private_prefix'];
|
||||
}
|
||||
if ( isset( $extra_info['objects'] ) ) {
|
||||
$objects = $extra_info['objects'];
|
||||
}
|
||||
}
|
||||
|
||||
$extra_info = array(
|
||||
'objects' => $objects,
|
||||
'private_prefix' => $private_prefix,
|
||||
);
|
||||
|
||||
parent::__construct( $provider, $region, $bucket, $path, $is_private, $source_id, $source_path, $original_filename, $extra_info, $id, $originator, $is_verified, $use_object_versioning );
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize a data struct to be used when passing information
|
||||
* about the current item to filters that assume the item is a
|
||||
* media library item.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function item_data_for_acl_filter() {
|
||||
$item_data = parent::item_data_for_acl_filter();
|
||||
$media_library_item_data = wp_get_attachment_metadata( $this->source_id(), true );
|
||||
|
||||
// Copy over specific elements only as i.e. 'size' may not be populated yet in $media_library_item_data
|
||||
foreach ( array( 'file', 'original_image', 'image_meta' ) as $element ) {
|
||||
if ( isset( $media_library_item_data[ $element ] ) ) {
|
||||
$item_data[ $element ] = $media_library_item_data[ $element ];
|
||||
}
|
||||
}
|
||||
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new item from the source id.
|
||||
*
|
||||
* @param int $source_id
|
||||
* @param array $options
|
||||
*
|
||||
* @return Item|WP_Error
|
||||
*/
|
||||
public static function create_from_source_id( $source_id, $options = array() ) {
|
||||
if ( empty( $source_id ) ) {
|
||||
return new WP_Error(
|
||||
'exception',
|
||||
__( 'Empty Attachment ID passed to ' . __FUNCTION__, 'amazon-s3-and-cloudfront' )
|
||||
);
|
||||
}
|
||||
|
||||
$default_options = array(
|
||||
'originator' => Item::ORIGINATORS['standard'],
|
||||
'is_verified' => true,
|
||||
'use_object_versioning' => static::can_use_object_versioning(),
|
||||
);
|
||||
|
||||
$options = array_merge( $default_options, $options );
|
||||
|
||||
if ( ! in_array( $options['originator'], self::ORIGINATORS ) ) {
|
||||
return new WP_Error(
|
||||
'exception',
|
||||
__( 'Invalid Originator passed to ' . __FUNCTION__, 'amazon-s3-and-cloudfront' )
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Derive local path.
|
||||
*/
|
||||
|
||||
// Verify that get_attached_file will not blow up as it does not check the data it manipulates.
|
||||
$attached_file_meta = get_post_meta( $source_id, '_wp_attached_file', true );
|
||||
if ( ! is_string( $attached_file_meta ) ) {
|
||||
return new WP_Error(
|
||||
'exception',
|
||||
sprintf( __( 'Media Library item with ID %d has damaged meta data', 'amazon-s3-and-cloudfront' ), $source_id )
|
||||
);
|
||||
}
|
||||
unset( $attached_file_meta );
|
||||
|
||||
$source_path = get_attached_file( $source_id, true );
|
||||
|
||||
// Check for valid "full" file path otherwise we'll not be able to create offload path or download in the future.
|
||||
if ( empty( $source_path ) ) {
|
||||
return new WP_Error(
|
||||
'exception',
|
||||
sprintf( __( 'Media Library item with ID %d does not have a valid file path', 'amazon-s3-and-cloudfront' ), $source_id )
|
||||
);
|
||||
}
|
||||
|
||||
/** @var array|false|WP_Error $attachment_metadata */
|
||||
$attachment_metadata = wp_get_attachment_metadata( $source_id, true );
|
||||
if ( is_wp_error( $attachment_metadata ) ) {
|
||||
return $attachment_metadata;
|
||||
}
|
||||
|
||||
// Initialize extra info array with empty values
|
||||
$extra_info = array(
|
||||
'private_prefix' => null,
|
||||
'objects' => array(),
|
||||
);
|
||||
|
||||
// There may be an original image that can override the default original filename.
|
||||
$original_filename = empty( $attachment_metadata['original_image'] ) ? null : $attachment_metadata['original_image'];
|
||||
|
||||
$file_paths = AS3CF_Utils::get_attachment_file_paths( $source_id, false, $attachment_metadata );
|
||||
foreach ( $file_paths as $size => $size_file_path ) {
|
||||
if ( $size === 'file' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_object = array(
|
||||
'source_file' => wp_basename( $size_file_path ),
|
||||
'is_private' => false,
|
||||
);
|
||||
|
||||
$extra_info['objects'][ $size ] = $new_object;
|
||||
|
||||
if ( empty( $original_filename ) && 'full-orig' === $size ) {
|
||||
$original_filename = $new_object['source_file'];
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
$source_id,
|
||||
$source_path,
|
||||
$original_filename,
|
||||
$extra_info,
|
||||
null,
|
||||
$options['originator'],
|
||||
$options['is_verified'],
|
||||
$options['use_object_versioning']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment local URL.
|
||||
*
|
||||
* This is partly a direct copy of wp_get_attachment_url() from /wp-includes/post.php
|
||||
* as we filter the URL in AS3CF and can't remove this filter using the current implementation
|
||||
* of globals for class instances.
|
||||
*
|
||||
* @param string|null $object_key
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function get_local_url( $object_key = null ) {
|
||||
/** @var Amazon_S3_And_CloudFront $as3cf */
|
||||
global $as3cf;
|
||||
$url = '';
|
||||
|
||||
// Get attached file.
|
||||
if ( $file = get_post_meta( $this->source_id(), '_wp_attached_file', true ) ) {
|
||||
// Get upload directory.
|
||||
if ( ( $uploads = wp_upload_dir() ) && false === $uploads['error'] ) {
|
||||
// Check that the upload base exists in the file location.
|
||||
if ( 0 === strpos( $file, $uploads['basedir'] ) ) {
|
||||
// Replace file location with url location.
|
||||
$url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file );
|
||||
} elseif ( false !== strpos( $file, 'wp-content/uploads' ) ) {
|
||||
$url = $uploads['baseurl'] . substr( $file, strpos( $file, 'wp-content/uploads' ) + 18 );
|
||||
} else {
|
||||
// It's a newly-uploaded file, therefore $file is relative to the basedir.
|
||||
$url = $uploads['baseurl'] . "/$file";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $url ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $as3cf->maybe_fix_local_subsite_url( $url );
|
||||
|
||||
if ( ! empty( $object_key ) ) {
|
||||
$meta = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true );
|
||||
if ( empty( $meta['sizes'][ $object_key ]['file'] ) ) {
|
||||
// No alternative sizes available, return
|
||||
return $url;
|
||||
}
|
||||
|
||||
$url = str_replace( wp_basename( $url ), $meta['sizes'][ $object_key ]['file'], $url );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item based on source id.
|
||||
*
|
||||
* @param int $source_id
|
||||
*
|
||||
* @return bool|Media_Library_Item
|
||||
*/
|
||||
public static function get_by_source_id( $source_id ) {
|
||||
$as3cf_item = parent::get_by_source_id( $source_id );
|
||||
|
||||
if ( ! $as3cf_item ) {
|
||||
$provider_object = static::_legacy_get_attachment_provider_info( $source_id );
|
||||
|
||||
if ( is_array( $provider_object ) ) {
|
||||
$as3cf_item = static::_legacy_provider_info_to_item( $source_id, $provider_object );
|
||||
}
|
||||
}
|
||||
|
||||
return $as3cf_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full key (path) for given file that belongs to offloaded attachment.
|
||||
*
|
||||
* If no filename given, full sized path returned.
|
||||
* Path is prepended with private prefix if size associated with filename is private,
|
||||
* and a private prefix has been assigned to offload.
|
||||
*
|
||||
* @param string|null $filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key( $filename = null ) {
|
||||
// Public full path.
|
||||
if ( empty( $filename ) && empty( $this->private_prefix() ) ) {
|
||||
return parent::path();
|
||||
}
|
||||
|
||||
if ( empty( $filename ) ) {
|
||||
$filename = wp_basename( parent::path() );
|
||||
}
|
||||
|
||||
if ( ! empty( $this->private_prefix() ) ) {
|
||||
$size = $this->get_object_key_from_filename( $filename );
|
||||
|
||||
// Private path.
|
||||
if ( $this->is_private( $size ) ) {
|
||||
return $this->private_prefix() . $this->normalized_path_dir() . $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// Public path.
|
||||
return $this->normalized_path_dir() . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute source file paths for offloaded files.
|
||||
*
|
||||
* @return array Associative array of object_key => path
|
||||
*/
|
||||
public function full_source_paths() {
|
||||
return array_intersect_key( AS3CF_Utils::get_attachment_file_paths( $this->source_id(), false ), $this->objects() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get size name from file name
|
||||
*
|
||||
* @param string $filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_object_key_from_filename( $filename ) {
|
||||
return AS3CF_Utils::get_intermediate_size_from_filename( $this->source_id(), basename( $filename ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ACL for intermediate size.
|
||||
*
|
||||
* @param string $object_key Size name
|
||||
* @param string|null $bucket Optional bucket that ACL is potentially to be used with.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_acl_for_object_key( $object_key, $bucket = null ) {
|
||||
/** @var Amazon_S3_And_CloudFront $as3cf */
|
||||
global $as3cf;
|
||||
|
||||
return $as3cf->get_acl_for_intermediate_size( $this->source_id(), $object_key, $bucket, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of un-managed source_ids in descending order.
|
||||
*
|
||||
* While source id isn't strictly unique, it is by source type, which is always used in queries based on called class.
|
||||
*
|
||||
* @param int $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound.
|
||||
* @param int $limit Maximum number of source_ids to return. Required if not counting.
|
||||
* @param bool $count Just return a count of matching source_ids? Negates $limit, default false.
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public static function get_missing_source_ids( $upper_bound, $limit, $count = false ) {
|
||||
global $wpdb;
|
||||
|
||||
$args = array( static::$source_type );
|
||||
|
||||
if ( $count ) {
|
||||
$sql = 'SELECT COUNT(DISTINCT posts.ID)';
|
||||
} else {
|
||||
$sql = 'SELECT DISTINCT posts.ID';
|
||||
}
|
||||
|
||||
$sql .= "
|
||||
FROM {$wpdb->posts} AS posts
|
||||
WHERE posts.post_type = 'attachment'
|
||||
AND posts.ID NOT IN (
|
||||
SELECT items.source_id
|
||||
FROM " . static::items_table() . " AS items
|
||||
WHERE items.source_type = %s
|
||||
AND items.source_id = posts.ID
|
||||
)
|
||||
";
|
||||
|
||||
if ( ! empty( $upper_bound ) ) {
|
||||
$sql .= ' AND posts.ID < %d';
|
||||
$args[] = $upper_bound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow users to exclude certain MIME types from attachments to upload.
|
||||
*
|
||||
* @param array
|
||||
*/
|
||||
$ignored_mime_types = apply_filters( 'as3cf_ignored_mime_types', array() );
|
||||
if ( is_array( $ignored_mime_types ) && ! empty( $ignored_mime_types ) ) {
|
||||
$ignored_mime_types = array_map( 'sanitize_text_field', $ignored_mime_types );
|
||||
$sql .= " AND posts.post_mime_type NOT IN ('" . implode( "','", $ignored_mime_types ) . "')";
|
||||
}
|
||||
|
||||
if ( ! $count ) {
|
||||
$sql .= ' ORDER BY posts.ID DESC LIMIT %d';
|
||||
$args[] = $limit;
|
||||
}
|
||||
|
||||
$sql = $wpdb->prepare( $sql, $args );
|
||||
|
||||
if ( $count ) {
|
||||
return (int) $wpdb->get_var( $sql );
|
||||
} else {
|
||||
return array_map( 'intval', $wpdb->get_col( $sql ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds Media Library items with same source_path and sets them as offloaded.
|
||||
*/
|
||||
public function offload_duplicate_items() {
|
||||
global $wpdb;
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"
|
||||
SELECT m.post_id
|
||||
FROM " . $wpdb->postmeta . " AS m
|
||||
LEFT JOIN " . $wpdb->posts . " AS p ON m.post_id = p.ID AND p.`post_type` = 'attachment'
|
||||
WHERE m.meta_key = '_wp_attached_file'
|
||||
AND m.meta_value = %s
|
||||
AND m.post_id != %d
|
||||
AND m.post_id NOT IN (
|
||||
SELECT i.source_id
|
||||
FROM " . static::items_table() . " AS i
|
||||
WHERE i.source_type = %s
|
||||
AND i.source_id = m.post_id
|
||||
)
|
||||
;
|
||||
",
|
||||
$this->source_path(),
|
||||
$this->source_id(),
|
||||
static::$source_type
|
||||
);
|
||||
|
||||
$results = $wpdb->get_results( $sql );
|
||||
|
||||
// Nothing found, shortcut out.
|
||||
if ( 0 === count( $results ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $results as $result ) {
|
||||
$as3cf_item = new Media_Library_Item(
|
||||
$this->provider(),
|
||||
$this->region(),
|
||||
$this->bucket(),
|
||||
$this->path(),
|
||||
$this->is_private(),
|
||||
$result->post_id,
|
||||
$this->source_path(),
|
||||
wp_basename( $this->original_source_path() ),
|
||||
$this->extra_info()
|
||||
);
|
||||
$as3cf_item->save();
|
||||
$as3cf_item->duplicate_filesize_total( $this->source_id() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to the items edit page in WordPress
|
||||
*
|
||||
* @param object $error
|
||||
*
|
||||
* @return object|null Object containing url and link text
|
||||
*/
|
||||
public static function admin_link( $error ) {
|
||||
return (object) array(
|
||||
'url' => get_edit_post_link( $error->source_id, '' ),
|
||||
'text' => __( 'Edit', 'amazon-s3-and-cloudfront' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a year/month string for the item
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_item_time() {
|
||||
return $this->get_attachment_folder_year_month();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the year/month string for attachment's upload.
|
||||
*
|
||||
* Fall back to post date if attached, otherwise current date.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_attachment_folder_year_month( $data = array() ) {
|
||||
if ( empty( $data ) ) {
|
||||
$data = wp_get_attachment_metadata( $this->source_id(), true );
|
||||
}
|
||||
|
||||
if ( isset( $data['file'] ) ) {
|
||||
$time = $this->get_folder_time_from_url( $data['file'] );
|
||||
}
|
||||
|
||||
if ( empty( $time ) && ( $local_url = wp_get_attachment_url( $this->source_id() ) ) ) {
|
||||
$time = $this->get_folder_time_from_url( $local_url );
|
||||
}
|
||||
|
||||
if ( empty( $time ) ) {
|
||||
$time = date( 'Y/m' );
|
||||
|
||||
if ( ! ( $attach = get_post( $this->source_id() ) ) ) {
|
||||
return $time;
|
||||
}
|
||||
|
||||
if ( ! $attach->post_parent ) {
|
||||
return $time;
|
||||
}
|
||||
|
||||
if ( ! ( $post = get_post( $attach->post_parent ) ) ) {
|
||||
return $time;
|
||||
}
|
||||
|
||||
if ( substr( $post->post_date_gmt, 0, 4 ) > 0 ) {
|
||||
return date( 'Y/m', strtotime( $post->post_date_gmt . ' +0000' ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the upload folder time from given URL
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return null|string YYYY/MM format.
|
||||
*/
|
||||
private function get_folder_time_from_url( $url ) {
|
||||
if ( ! is_string( $url ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
preg_match( '@[0-9]{4}/[0-9]{2}/@', $url, $matches );
|
||||
|
||||
if ( isset( $matches[0] ) ) {
|
||||
return untrailingslashit( $matches[0] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns filesize from metadata, if we have it, so that file or stream
|
||||
* wrapper does not need to be hit.
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function get_filesize() {
|
||||
// Prefer the canonical attachment filesize.
|
||||
$metadata = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true );
|
||||
|
||||
if ( ! empty( $metadata['filesize'] ) && is_int( $metadata['filesize'] ) ) {
|
||||
return $metadata['filesize'];
|
||||
}
|
||||
|
||||
// If offloaded and removed, we should have squirreled away the filesize.
|
||||
$filesize = get_post_meta( $this->source_id(), 'as3cf_filesize_total', true );
|
||||
|
||||
if ( ! empty( $filesize ) && is_int( $filesize ) ) {
|
||||
return $filesize;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update filesize and as3cf_filesize_total metadata on the underlying media library item
|
||||
* after removing the local file.
|
||||
*
|
||||
* @param int $original_size
|
||||
* @param int $total_size
|
||||
*/
|
||||
public function update_filesize_after_remove_local( $original_size, $total_size ) {
|
||||
update_post_meta( $this->source_id(), 'as3cf_filesize_total', $total_size );
|
||||
|
||||
if ( 0 < $original_size && ( $data = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true ) ) ) {
|
||||
if ( is_array( $data ) && empty( $data['filesize'] ) ) {
|
||||
$data['filesize'] = $original_size;
|
||||
|
||||
// Update metadata with filesize
|
||||
update_post_meta( $this->source_id(), '_wp_attachment_metadata', $data );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup filesize and as3cf_filesize_total metadata on the underlying media library item
|
||||
* after downloading a file back from the bucket
|
||||
*/
|
||||
public function update_filesize_after_download_local() {
|
||||
$data = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true );
|
||||
|
||||
/*
|
||||
* Audio and video have a filesize added to metadata by default, but images and anything else don't.
|
||||
* Note: Could have used `wp_generate_attachment_metadata` here to test whether default metadata has 'filesize',
|
||||
* but it not only has side effects it also does a lot of work considering it's not a huge deal for this entry to hang around.
|
||||
*/
|
||||
if (
|
||||
! empty( $data ) &&
|
||||
(
|
||||
empty( $data['mime_type'] ) ||
|
||||
0 === strpos( $data['mime_type'], 'image/' ) ||
|
||||
! ( 0 === strpos( $data['mime_type'], 'audio/' ) || 0 === strpos( $data['mime_type'], 'video/' ) )
|
||||
)
|
||||
) {
|
||||
unset( $data['filesize'] );
|
||||
update_post_meta( $this->source_id(), '_wp_attachment_metadata', $data );
|
||||
}
|
||||
|
||||
delete_post_meta( $this->source_id(), 'as3cf_filesize_total' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate 'as3cf_filesize_total' meta if it exists for an attachment.
|
||||
*
|
||||
* @param int $attachment_id
|
||||
*/
|
||||
public function duplicate_filesize_total( $attachment_id ) {
|
||||
if ( ! ( $filesize = get_post_meta( $attachment_id, 'as3cf_filesize_total', true ) ) ) {
|
||||
// No filesize to duplicate.
|
||||
return;
|
||||
}
|
||||
|
||||
update_post_meta( $this->source_id(), 'as3cf_filesize_total', $filesize );
|
||||
}
|
||||
|
||||
/**
|
||||
* If another item in current site shares full size *local* paths, only remove remote files not referenced by duplicates.
|
||||
* We reference local paths as they should be reflected one way or another remotely, including backups.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $paths
|
||||
*/
|
||||
public function remove_duplicate_paths( Item $as3cf_item, $paths ) {
|
||||
$full_size_paths = AS3CF_Utils::fullsize_paths( $as3cf_item->full_source_paths() );
|
||||
$as3cf_items_with_paths = static::get_by_source_path( $full_size_paths, array( $as3cf_item->source_id() ), false );
|
||||
|
||||
$duplicate_paths = array();
|
||||
|
||||
foreach ( $as3cf_items_with_paths as $as3cf_item_with_path ) {
|
||||
/* @var Media_Library_Item $as3cf_item_with_path */
|
||||
$duplicate_paths += array_values( AS3CF_Utils::get_attachment_file_paths( $as3cf_item_with_path->source_id(), false, false, true ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $duplicate_paths ) ) {
|
||||
$paths = array_diff( $paths, $duplicate_paths );
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transient key to be used for storing blog specific item counts.
|
||||
*
|
||||
* @param int $blog_id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function transient_key_for_item_counts( int $blog_id ): string {
|
||||
return 'as3cf_' . absint( $blog_id ) . '_attachment_counts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count total, offloaded and not offloaded items on current site.
|
||||
*
|
||||
* @return array Keys:
|
||||
* total: Total media count for site (current blog id)
|
||||
* offloaded: Count of offloaded media for site (current blog id)
|
||||
* not_offloaded: Difference between total and offloaded
|
||||
*/
|
||||
protected static function get_item_counts(): array {
|
||||
global $wpdb;
|
||||
|
||||
$sql = "SELECT count(id) FROM {$wpdb->posts} WHERE post_type = 'attachment'";
|
||||
$attachment_count = (int) $wpdb->get_var( $sql );
|
||||
|
||||
$sql = 'SELECT count(id) FROM ' . static::items_table() . ' WHERE source_type = %s';
|
||||
$sql = $wpdb->prepare( $sql, static::$source_type );
|
||||
$offloaded_count = (int) $wpdb->get_var( $sql );
|
||||
|
||||
return array(
|
||||
'total' => $attachment_count,
|
||||
'offloaded' => $offloaded_count,
|
||||
'not_offloaded' => max( $attachment_count - $offloaded_count, 0 ),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* >>> LEGACY ROUTINES BEGIN >>>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert the provider info array for an attachment to item object.
|
||||
*
|
||||
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore
|
||||
*
|
||||
* @param int $source_id
|
||||
* @param array $provider_info
|
||||
*
|
||||
* @return bool|Media_Library_Item
|
||||
*/
|
||||
private static function _legacy_provider_info_to_item( $source_id, $provider_info ) {
|
||||
$attached_file = get_post_meta( $source_id, '_wp_attached_file', true );
|
||||
|
||||
if ( is_string( $attached_file ) && ! empty( $attached_file ) ) {
|
||||
$private_sizes = array();
|
||||
|
||||
if ( ! empty( $provider_info['sizes'] ) && is_array( $provider_info['sizes'] ) ) {
|
||||
$private_sizes = array_keys( $provider_info['sizes'] );
|
||||
}
|
||||
|
||||
return new static(
|
||||
$provider_info['provider'],
|
||||
$provider_info['region'],
|
||||
$provider_info['bucket'],
|
||||
$provider_info['key'],
|
||||
isset( $provider_info['acl'] ) && false !== strpos( $provider_info['acl'], 'private' ),
|
||||
$source_id,
|
||||
$attached_file,
|
||||
wp_basename( $attached_file ),
|
||||
$private_sizes
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment provider info
|
||||
*
|
||||
* @param int $post_id
|
||||
*
|
||||
* @return bool|array
|
||||
*/
|
||||
private static function _legacy_get_attachment_provider_info( $post_id ) {
|
||||
$provider_object = get_post_meta( $post_id, 'amazonS3_info', true );
|
||||
|
||||
if ( ! empty( $provider_object ) && is_array( $provider_object ) && ! empty( $provider_object['bucket'] ) && ! empty( $provider_object['key'] ) ) {
|
||||
global $as3cf;
|
||||
|
||||
$provider_object = array_merge( array(
|
||||
'provider' => $as3cf::get_default_storage_provider(),
|
||||
), $provider_object );
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provider_object['region'] = static::_legacy_get_provider_object_region( $provider_object );
|
||||
|
||||
if ( is_wp_error( $provider_object['region'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provider_object = apply_filters( 'as3cf_get_attachment_s3_info', $provider_object, $post_id ); // Backwards compatibility
|
||||
|
||||
return apply_filters( 'as3cf_get_attachment_provider_info', $provider_object, $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the region of the bucket stored in the provider metadata.
|
||||
*
|
||||
* @param array $provider_object
|
||||
*
|
||||
* @return string|WP_Error - region name
|
||||
*/
|
||||
private static function _legacy_get_provider_object_region( $provider_object ) {
|
||||
if ( ! isset( $provider_object['region'] ) ) {
|
||||
/** @var Amazon_S3_And_CloudFront $as3cf */
|
||||
global $as3cf;
|
||||
|
||||
// If region hasn't been stored in the provider metadata, retrieve using the bucket.
|
||||
$region = $as3cf->get_bucket_region( $provider_object['bucket'] );
|
||||
|
||||
// Could just return $region here regardless, but this format is good for debug during legacy migration.
|
||||
if ( is_wp_error( $region ) ) {
|
||||
return $region;
|
||||
}
|
||||
|
||||
$provider_object['region'] = $region;
|
||||
}
|
||||
|
||||
return $provider_object['region'];
|
||||
}
|
||||
|
||||
/*
|
||||
* <<< LEGACY ROUTINES END <<<
|
||||
*/
|
||||
}
|
||||
44
classes/items/provider-test-item.php
Normal file
44
classes/items/provider-test-item.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
class Provider_Test_Item extends Media_Library_Item {
|
||||
/**
|
||||
* Source type name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $source_type_name = 'Provider Test Item';
|
||||
|
||||
/**
|
||||
* Internal source type identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $source_type = 'provider-test';
|
||||
|
||||
/**
|
||||
* Overrides the parent implementation to avoid storing anything in the database.
|
||||
*
|
||||
* @param bool $update_duplicates
|
||||
*
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
public function save( $update_duplicates = true ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent implementation. Return all paths unchanged.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $paths
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function remove_duplicate_paths( Item $as3cf_item, $paths ): array {
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
209
classes/items/remove-local-handler.php
Normal file
209
classes/items/remove-local-handler.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use AS3CF_Error;
|
||||
|
||||
class Remove_Local_Handler extends Item_Handler {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static $item_handler_key = 'remove-local';
|
||||
|
||||
/**
|
||||
* Keep track of individual files we've already attempted to remove.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $remove_blocked = array();
|
||||
|
||||
/**
|
||||
* Keep track of size of individual files we've already attempted to remove.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $removed_size = array();
|
||||
|
||||
/**
|
||||
* If remove the primary file, we want to update the 'filesize'.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $removed_primary_size = array();
|
||||
|
||||
/**
|
||||
* The default options that should be used if none supplied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function default_options() {
|
||||
return array(
|
||||
'verify_exists_on_provider' => false,
|
||||
'provider_keys' => array(),
|
||||
'files_to_remove' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create manifest for local removal.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $options
|
||||
*
|
||||
* @return Manifest
|
||||
*/
|
||||
protected function pre_handle( Item $as3cf_item, array $options ) {
|
||||
$manifest = new Manifest();
|
||||
$source_id = $as3cf_item->source_id();
|
||||
$primary_file = '';
|
||||
$files_to_remove = array();
|
||||
|
||||
// Note: Unable to use Item::full_size_paths() here
|
||||
// as source item's metadata may not be up-to-date yet.
|
||||
foreach ( $as3cf_item->objects() as $object_key => $object ) {
|
||||
$file = $as3cf_item->full_source_path( $object_key );
|
||||
|
||||
if ( in_array( $file, $this->remove_blocked ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 0 < count( $options['files_to_remove'] ) && ! in_array( $file, $options['files_to_remove'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If needed, make sure this item exists among the provider keys.
|
||||
if ( true === $options['verify_exists_on_provider'] ) {
|
||||
if ( empty( $options['provider_keys'][ $source_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! in_array( $as3cf_item->provider_key( $object_key ), $options['provider_keys'][ $source_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ( file_exists( $file ) ) {
|
||||
$files_to_remove[] = $file;
|
||||
|
||||
if ( Item::primary_object_key() === $object_key ) {
|
||||
$primary_file = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters array of local files before being removed from server.
|
||||
*
|
||||
* @param array $files_to_remove Array of paths to be removed
|
||||
* @param Item $as3cf_item The Item object
|
||||
* @param array $item_source Item source descriptor array
|
||||
*/
|
||||
$filtered_files_to_remove = apply_filters( 'as3cf_remove_local_files', $files_to_remove, $as3cf_item, $as3cf_item->get_item_source_array() );
|
||||
|
||||
// Ensure fileset is unique and does not contain files already blocked.
|
||||
$filtered_files_to_remove = array_unique( array_diff( $filtered_files_to_remove, $this->remove_blocked ) );
|
||||
|
||||
// If filter removes files from list, block attempts to remove them in later calls.
|
||||
$this->remove_blocked = array_merge( $this->remove_blocked, array_diff( $files_to_remove, $filtered_files_to_remove ) );
|
||||
|
||||
foreach ( $filtered_files_to_remove as $file ) {
|
||||
// Filter may have added some files to check for existence.
|
||||
if ( ! in_array( $file, $files_to_remove ) ) {
|
||||
if ( ! file_exists( $file ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter individual files that might still be kept local.
|
||||
*
|
||||
* @param bool $preserve Should the file be kept on the server?
|
||||
* @param string $file Full path to the local file
|
||||
*/
|
||||
if ( false !== apply_filters( 'as3cf_preserve_file_from_local_removal', false, $file ) ) {
|
||||
$this->remove_blocked[] = $file;
|
||||
continue;
|
||||
}
|
||||
|
||||
$manifest->objects[] = array(
|
||||
'file' => $file,
|
||||
'size' => filesize( $file ),
|
||||
'is_primary' => $file === $primary_file,
|
||||
);
|
||||
}
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete local files described in the manifest object array.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
foreach ( $manifest->objects as &$file_to_remove ) {
|
||||
$file = $file_to_remove['file'];
|
||||
|
||||
$file_to_remove['remove_result'] = array( 'status' => self::STATUS_OK );
|
||||
|
||||
//phpcs:ignore
|
||||
if ( ! @unlink( $file ) ) {
|
||||
$this->remove_blocked[] = $file;
|
||||
|
||||
$file_to_remove['remove_result']['status'] = self::STATUS_FAILED;
|
||||
$file_to_remove['remove_result']['message'] = "Error removing local file at $file";
|
||||
|
||||
if ( ! file_exists( $file ) ) {
|
||||
$file_to_remove['remove_result']['message'] = "Error removing local file. Couldn't find the file at $file";
|
||||
} elseif ( ! is_writable( $file ) ) {
|
||||
$file_to_remove['remove_result']['message'] = "Error removing local file. Ownership or permissions are mis-configured for $file";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform post handle tasks.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
if ( empty( $manifest->objects ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Assume we didn't touch the primary file.
|
||||
$this->removed_primary_size[ $as3cf_item->source_id() ] = 0;
|
||||
|
||||
foreach ( $manifest->objects as $file_to_remove ) {
|
||||
if ( $file_to_remove['remove_result']['status'] !== self::STATUS_OK ) {
|
||||
AS3CF_Error::log( $file_to_remove['remove_result']['message'] );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( empty( $this->removed_size[ $as3cf_item->source_id() ] ) ) {
|
||||
$this->removed_size[ $as3cf_item->source_id() ] = $file_to_remove['size'];
|
||||
} else {
|
||||
$this->removed_size[ $as3cf_item->source_id() ] += $file_to_remove['size'];
|
||||
}
|
||||
|
||||
if ( $file_to_remove['is_primary'] ) {
|
||||
$this->removed_primary_size[ $as3cf_item->source_id() ] = $file_to_remove['size'];
|
||||
}
|
||||
}
|
||||
|
||||
$as3cf_item->update_filesize_after_remove_local( $this->removed_primary_size[ $as3cf_item->source_id() ], $this->removed_size[ $as3cf_item->source_id() ] );
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
154
classes/items/remove-provider-handler.php
Normal file
154
classes/items/remove-provider-handler.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
class Remove_Provider_Handler extends Item_Handler {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static $item_handler_key = 'remove-provider';
|
||||
|
||||
/**
|
||||
* The default options that should be used if none supplied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function default_options() {
|
||||
return array(
|
||||
'object_keys' => array(),
|
||||
'offloaded_files' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create manifest for removal from provider.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $options
|
||||
*
|
||||
* @return Manifest|WP_Error
|
||||
*/
|
||||
protected function pre_handle( Item $as3cf_item, array $options ) {
|
||||
$manifest = new Manifest();
|
||||
$paths = array();
|
||||
|
||||
if ( ! empty( $options['object_keys'] ) && ! is_array( $options['object_keys'] ) ) {
|
||||
return $this->return_handler_error( __( 'Invalid object_keys option provided.', 'amazon-s3-and-cloudfront' ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $options['offloaded_files'] ) && ! is_array( $options['offloaded_files'] ) ) {
|
||||
return $this->return_handler_error( __( 'Invalid offloaded_files option provided.', 'amazon-s3-and-cloudfront' ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $options['object_keys'] ) && ! empty( $options['offloaded_files'] ) ) {
|
||||
return $this->return_handler_error( __( 'Providing both object_keys and offloaded_files options is not supported.', 'amazon-s3-and-cloudfront' ) );
|
||||
}
|
||||
|
||||
if ( empty( $options['offloaded_files'] ) ) {
|
||||
foreach ( $as3cf_item->objects() as $object_key => $object ) {
|
||||
if ( 0 < count( $options['object_keys'] ) && ! in_array( $object_key, $options['object_keys'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$paths[ $object_key ] = $as3cf_item->full_source_path( $object_key );
|
||||
}
|
||||
} else {
|
||||
foreach ( $options['offloaded_files'] as $filename => $object ) {
|
||||
$paths[ $filename ] = $as3cf_item->full_source_path_for_filename( $filename );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters array of source files before being removed from provider.
|
||||
*
|
||||
* @param array $paths Array of local paths to be removed from provider
|
||||
* @param Item $as3cf_item The Item object
|
||||
* @param array $item_source The item source descriptor array
|
||||
*/
|
||||
$paths = apply_filters( 'as3cf_remove_source_files_from_provider', $paths, $as3cf_item, $as3cf_item->get_item_source_array() );
|
||||
$paths = array_unique( $paths );
|
||||
|
||||
// Remove local source paths that other items may have offloaded.
|
||||
$paths = $as3cf_item->remove_duplicate_paths( $as3cf_item, $paths );
|
||||
|
||||
// Nothing to do, shortcut out.
|
||||
if ( empty( $paths ) ) {
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
if ( empty( $options['offloaded_files'] ) ) {
|
||||
foreach ( $paths as $object_key => $path ) {
|
||||
$manifest->objects[] = array(
|
||||
'Key' => $as3cf_item->provider_key( $object_key ),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
foreach ( $paths as $filename => $path ) {
|
||||
$manifest->objects[] = array(
|
||||
'Key' => $as3cf_item->provider_key_for_filename( $filename, $options['offloaded_files'][ $filename ]['is_private'] ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete provider objects described in the manifest object array
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
// This test is "late" so that we don't raise the error if there is nothing to remove.
|
||||
// If the provider of this item is different from what's currently configured,
|
||||
// we'll return an error.
|
||||
$current_provider = $this->as3cf->get_storage_provider();
|
||||
if ( ! is_null( $current_provider ) && $current_provider::get_provider_key_name() !== $as3cf_item->provider() ) {
|
||||
$error_msg = sprintf(
|
||||
__( '%1$s with ID %2$d is offloaded to a different provider than currently configured', 'amazon-s3-and-cloudfront' ),
|
||||
$this->as3cf->get_source_type_name( $as3cf_item->source_type() ),
|
||||
$as3cf_item->source_id()
|
||||
);
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
$chunks = array_chunk( $manifest->objects, 1000 );
|
||||
$region = $as3cf_item->region();
|
||||
$bucket = $as3cf_item->bucket();
|
||||
|
||||
try {
|
||||
foreach ( $chunks as $chunk ) {
|
||||
$this->as3cf->get_provider_client( $region )->delete_objects( array(
|
||||
'Bucket' => $bucket,
|
||||
'Objects' => $chunk,
|
||||
) );
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
$error_msg = sprintf( __( 'Error removing files from bucket: %s', 'amazon-s3-and-cloudfront' ), $e->getMessage() );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform post handle tasks.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
434
classes/items/upload-handler.php
Normal file
434
classes/items/upload-handler.php
Normal file
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
namespace DeliciousBrains\WP_Offload_Media\Items;
|
||||
|
||||
use AS3CF_Error;
|
||||
use AS3CF_Utils;
|
||||
use DeliciousBrains\WP_Offload_Media\Providers\Storage\Storage_Provider;
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
class Upload_Handler extends Item_Handler {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected static $item_handler_key = 'upload';
|
||||
|
||||
/**
|
||||
* Keep track of individual files we've already attempted to upload
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attempted_upload = array();
|
||||
|
||||
/**
|
||||
* The default options that should be used if none supplied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function default_options() {
|
||||
return array(
|
||||
'offloaded_files' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare item for uploading by running filters, updating
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param array $options
|
||||
*
|
||||
* @return Manifest|WP_Error
|
||||
*/
|
||||
protected function pre_handle( Item $as3cf_item, array $options ) {
|
||||
$manifest = new Manifest();
|
||||
$source_type_name = $this->as3cf->get_source_type_name( $as3cf_item->source_type() );
|
||||
$primary_key = Item::primary_object_key();
|
||||
|
||||
// Check for valid file path before attempting upload
|
||||
if ( empty( $as3cf_item->source_path() ) ) {
|
||||
$error_msg = sprintf( __( '%1$s with id %2$d does not have a valid file path', 'amazon-s3-and-cloudfront' ), $source_type_name, $as3cf_item->source_id() );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
// Ensure path is a string
|
||||
if ( ! is_string( $as3cf_item->source_path() ) ) {
|
||||
$error_msg = sprintf( __( '%1$s with id %2$d. Provided path is not a string', 'amazon-s3-and-cloudfront' ), $source_type_name, $as3cf_item->source_id() );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
// Ensure primary source file exists for new offload.
|
||||
if ( empty( $as3cf_item->id() ) && ! file_exists( $as3cf_item->full_source_path( $primary_key ) ) ) {
|
||||
$error_msg = sprintf( __( 'Primary file %1$s for %2$s with id %3$s does not exist', 'amazon-s3-and-cloudfront' ), $as3cf_item->full_source_path( $primary_key ), $source_type_name, $as3cf_item->source_id() );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
// Get primary file's stats.
|
||||
$file_name = wp_basename( $as3cf_item->source_path() );
|
||||
$file_type = wp_check_filetype_and_ext( $as3cf_item->full_source_path(), $file_name );
|
||||
$allowed_types = $this->as3cf->get_allowed_mime_types();
|
||||
|
||||
// check mime type of file is in allowed provider mime types
|
||||
if ( ! in_array( $file_type['type'], $allowed_types, true ) ) {
|
||||
$error_msg = sprintf( __( 'Mime type "%1$s" is not allowed (%2$s with id %3$s)', 'amazon-s3-and-cloudfront' ), $file_type['type'], $source_type_name, $as3cf_item->source_id() );
|
||||
|
||||
return $this->return_handler_error( $error_msg );
|
||||
}
|
||||
|
||||
$default_acl = $this->as3cf->get_storage_provider()->get_default_acl();
|
||||
$private_acl = $this->as3cf->get_storage_provider()->get_private_acl();
|
||||
|
||||
foreach ( $as3cf_item->objects() as $object_key => $object ) {
|
||||
// Avoid attempting uploading to an item that doesn't have the primary file in place.
|
||||
if ( $primary_key !== $object_key && empty( $as3cf_item->id() ) && ! isset( $manifest->objects[ $primary_key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$source_path = $as3cf_item->full_source_path( $object_key );
|
||||
|
||||
// If the file has already been offloaded,
|
||||
// don't try and (fail to) re-offload if the file isn't available.
|
||||
if ( $this->in_offloaded_files( $object['source_file'], $options ) && ! file_exists( $source_path ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* This filter allows you to change the public/private status of an individual file associated
|
||||
* with an uploaded item before it's uploaded to the provider.
|
||||
*
|
||||
* @param bool $is_private Should the object be private?
|
||||
* @param string $object_key A unique file identifier for a composite item, e.g. image's "size" such as full, small, medium, large.
|
||||
* @param Item $as3cf_item The item being uploaded.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
$is_private = apply_filters( 'as3cf_upload_object_key_as_private', $as3cf_item->is_private( $object_key ), $object_key, $as3cf_item );
|
||||
$as3cf_item->set_is_private( $is_private, $object_key );
|
||||
|
||||
$object_acl = $as3cf_item->is_private( $object_key ) ? $private_acl : $default_acl;
|
||||
|
||||
$args = array(
|
||||
'Bucket' => $as3cf_item->bucket(),
|
||||
'Key' => $as3cf_item->path( $object_key ),
|
||||
'SourceFile' => $source_path,
|
||||
'ContentType' => AS3CF_Utils::get_mime_type( $object['source_file'] ),
|
||||
'CacheControl' => 'max-age=31536000',
|
||||
);
|
||||
|
||||
// Only set ACL if actually required, some storage provider and bucket settings disable changing ACL.
|
||||
if ( ! empty( $object_acl ) && $this->as3cf->use_acl_for_intermediate_size( 0, $object_key, $as3cf_item->bucket(), $as3cf_item ) ) {
|
||||
$args['ACL'] = $object_acl;
|
||||
}
|
||||
|
||||
// TODO: Remove GZIP functionality.
|
||||
// Handle gzip on supported items
|
||||
if (
|
||||
$this->should_gzip_file( $source_path, $as3cf_item->source_type() ) &&
|
||||
false !== ( $gzip_body = gzencode( file_get_contents( $source_path ) ) )
|
||||
) {
|
||||
unset( $args['SourceFile'] );
|
||||
|
||||
$args['Body'] = $gzip_body;
|
||||
$args['ContentEncoding'] = 'gzip';
|
||||
}
|
||||
|
||||
$args = Storage_Provider::filter_object_meta( $args, $as3cf_item, $object_key );
|
||||
|
||||
// If the bucket is changed by the filter while processing the primary object,
|
||||
// we should try and use that bucket for the item.
|
||||
// If the bucket name is invalid, revert to configured bucket but log it.
|
||||
// We don't abort here as ephemeral filesystems need to be accounted for,
|
||||
// and the configured bucket is at least known to work.
|
||||
if ( $primary_key === $object_key && $as3cf_item->bucket() !== $args['Bucket'] && empty( $as3cf_item->id() ) ) {
|
||||
$bucket = $this->as3cf->check_bucket( $args['Bucket'] );
|
||||
|
||||
if ( $bucket ) {
|
||||
$region = $this->as3cf->get_bucket_region( $bucket );
|
||||
|
||||
if ( is_wp_error( $region ) ) {
|
||||
unset( $region );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $bucket ) || empty( $region ) ) {
|
||||
$mesg = sprintf(
|
||||
__( 'Bucket name "%1$s" is invalid, using "%2$s" instead.', 'amazon-s3-and-cloudfront' ),
|
||||
$args['Bucket'],
|
||||
$as3cf_item->bucket()
|
||||
);
|
||||
AS3CF_Error::log( $mesg );
|
||||
$args['Bucket'] = $as3cf_item->bucket();
|
||||
} else {
|
||||
$args['Bucket'] = $bucket;
|
||||
$as3cf_item->set_bucket( $bucket );
|
||||
$as3cf_item->set_region( $region );
|
||||
}
|
||||
|
||||
unset( $bucket, $region );
|
||||
} elseif ( $primary_key === $object_key && $as3cf_item->bucket() !== $args['Bucket'] && ! empty( $as3cf_item->id() ) ) {
|
||||
$args['Bucket'] = $as3cf_item->bucket();
|
||||
AS3CF_Error::log( __( 'The bucket may not be changed via filters for a previously offloaded item.', 'amazon-s3-and-cloudfront' ) );
|
||||
} elseif ( $primary_key !== $object_key && $as3cf_item->bucket() !== $args['Bucket'] ) {
|
||||
$args['Bucket'] = $as3cf_item->bucket();
|
||||
}
|
||||
|
||||
// If the Key has been changed for the primary object key, then that should be reflected in the item.
|
||||
if ( $primary_key === $object_key && $as3cf_item->path( $object_key ) !== $args['Key'] && empty( $as3cf_item->id() ) ) {
|
||||
$prefix = AS3CF_Utils::trailingslash_prefix( dirname( $args['Key'] ) );
|
||||
|
||||
if ( $prefix === '.' ) {
|
||||
$prefix = '';
|
||||
}
|
||||
|
||||
$as3cf_item->update_path_prefix( $prefix );
|
||||
|
||||
// If the filter tried to use a different filename too, log it.
|
||||
if ( wp_basename( $args['Key'] ) !== wp_basename( $as3cf_item->path( $object_key ) ) ) {
|
||||
$mesg = sprintf(
|
||||
__( 'The offloaded filename must not be changed, "%1$s" has been used instead of "%2$s".', 'amazon-s3-and-cloudfront' ),
|
||||
wp_basename( $as3cf_item->path( $object_key ) ),
|
||||
wp_basename( $args['Key'] )
|
||||
);
|
||||
AS3CF_Error::log( $mesg );
|
||||
}
|
||||
} elseif ( $primary_key === $object_key && $as3cf_item->path( $object_key ) !== $args['Key'] && ! empty( $as3cf_item->id() ) ) {
|
||||
$args['Key'] = $as3cf_item->path( $object_key );
|
||||
AS3CF_Error::log( __( 'The key may not be changed via filters for a previously offloaded item.', 'amazon-s3-and-cloudfront' ) );
|
||||
} elseif ( $primary_key !== $object_key && $as3cf_item->path( $object_key ) !== $args['Key'] ) {
|
||||
$args['Key'] = $as3cf_item->path( $object_key );
|
||||
}
|
||||
|
||||
// If ACL has been set, does the object's is_private need updating?
|
||||
$is_private = ! empty( $args['ACL'] ) && $private_acl === $args['ACL'] || $as3cf_item->is_private( $object_key );
|
||||
$as3cf_item->set_is_private( $is_private, $object_key );
|
||||
|
||||
// Protect against filter use and only set ACL if actually required, some storage provider and bucket settings disable changing ACL.
|
||||
if ( isset( $args['ACL'] ) && ! $this->as3cf->use_acl_for_intermediate_size( 0, $object_key, $as3cf_item->bucket(), $as3cf_item ) ) {
|
||||
unset( $args['ACL'] );
|
||||
}
|
||||
|
||||
// Adjust the actual Key to add the private prefix before uploading.
|
||||
if ( $as3cf_item->is_private( $object_key ) ) {
|
||||
$args['Key'] = $as3cf_item->provider_key( $object_key );
|
||||
}
|
||||
|
||||
// If we've already attempted to offload this source file, leave it out of the manifest.
|
||||
if ( in_array( md5( serialize( $args ) ), $this->attempted_upload ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $primary_key === $object_key ) {
|
||||
/**
|
||||
* Actions fires when an Item's primary file might be offloaded.
|
||||
*
|
||||
* This action gives notice that an Item is being processed for upload to a bucket,
|
||||
* and the given arguments represent the primary file's potential offload location.
|
||||
* However, if the current process is for picking up extra files associated with the item,
|
||||
* the indicated primary file may not actually be offloaded if it does not exist
|
||||
* on the server but has already been offloaded.
|
||||
*
|
||||
* @param Item $as3cf_item The Item whose files are being offloaded.
|
||||
* @param array $args The arguments that could be used to offload the primary file.
|
||||
*/
|
||||
do_action( 'as3cf_pre_upload_object', $as3cf_item, $args );
|
||||
}
|
||||
|
||||
$manifest->objects[ $object_key ]['args'] = $args;
|
||||
}
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload item files to remote storage provider
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
$source_type_name = $this->as3cf->get_source_type_name( $as3cf_item->source_type() );
|
||||
|
||||
try {
|
||||
$provider_client = $this->as3cf->get_provider_client( $as3cf_item->region() );
|
||||
} catch ( Exception $e ) {
|
||||
return $this->return_handler_error( $e->getMessage() );
|
||||
}
|
||||
|
||||
foreach ( $manifest->objects as $object_key => &$object ) {
|
||||
$args = $object['args'];
|
||||
|
||||
$object['upload_result'] = array(
|
||||
'status' => null,
|
||||
'message' => null,
|
||||
);
|
||||
|
||||
if ( ! file_exists( $args['SourceFile'] ) ) {
|
||||
$error_msg = sprintf( __( 'File %1$s does not exist (%2$s with id %3$s)', 'amazon-s3-and-cloudfront' ), $args['SourceFile'], $source_type_name, $as3cf_item->source_id() );
|
||||
|
||||
$object['upload_result']['status'] = self::STATUS_FAILED;
|
||||
$object['upload_result']['message'] = $error_msg;
|
||||
|
||||
// If the missing source file is the primary file, abort the whole process.
|
||||
if ( Item::primary_object_key() === $object_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->attempted_upload[] = md5( serialize( $args ) );
|
||||
|
||||
// Try to do the upload
|
||||
try {
|
||||
$provider_client->upload_object( $args );
|
||||
|
||||
$object['upload_result']['status'] = self::STATUS_OK;
|
||||
} catch ( Exception $e ) {
|
||||
$error_msg = sprintf( __( 'Error offloading %1$s to provider: %2$s (%3$s with id %4$s)', 'amazon-s3-and-cloudfront' ), $args['SourceFile'], $e->getMessage(), $source_type_name, $as3cf_item->source_id() );
|
||||
|
||||
$object['upload_result']['status'] = self::STATUS_FAILED;
|
||||
$object['upload_result']['message'] = $error_msg;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle local housekeeping after uploads.
|
||||
*
|
||||
* @param Item $as3cf_item
|
||||
* @param Manifest $manifest
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) {
|
||||
$item_objects = $as3cf_item->objects();
|
||||
$errors = new WP_Error();
|
||||
$i = 1;
|
||||
|
||||
// Reconcile the Item's objects with their manifest status.
|
||||
foreach ( $item_objects as $object_key => $object ) {
|
||||
// If there was no attempt made to offload the file,
|
||||
// then remove it from list of offloaded objects.
|
||||
// However, if the source file has previously been offloaded,
|
||||
// we should just skip any further processing of it
|
||||
// as the associated objects are still offloaded.
|
||||
if ( ! isset( $manifest->objects[ $object_key ]['upload_result']['status'] ) ) {
|
||||
if ( empty( $options['offloaded_files'][ $object['source_file'] ] ) ) {
|
||||
unset( $item_objects[ $object_key ] );
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the upload didn't succeed, we need to remove the object/size from the item.
|
||||
// However, if the source file has previously been offloaded, we should just log the error.
|
||||
if ( $manifest->objects[ $object_key ]['upload_result']['status'] !== self::STATUS_OK ) {
|
||||
if ( empty( $options['offloaded_files'][ $object['source_file'] ] ) ) {
|
||||
unset( $item_objects[ $object_key ] );
|
||||
}
|
||||
$errors->add( 'upload-object-' . ( $i++ ), $manifest->objects[ $object_key ]['upload_result']['message'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Set the potentially changed list of offloaded objects.
|
||||
$as3cf_item->set_objects( $item_objects );
|
||||
|
||||
// Only save if we have the primary file uploaded.
|
||||
if ( isset( $item_objects[ Item::primary_object_key() ] ) ) {
|
||||
$as3cf_item->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires action after uploading finishes
|
||||
*
|
||||
* @param Item $as3cf_item The item that was just uploaded
|
||||
*/
|
||||
do_action( 'as3cf_post_upload_item', $as3cf_item );
|
||||
|
||||
if ( count( $errors->get_error_codes() ) ) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should gzip file
|
||||
*
|
||||
* @param string $file_path
|
||||
* @param string $source_type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_gzip_file( $file_path, $source_type ) {
|
||||
$file_type = wp_check_filetype_and_ext( $file_path, $file_path );
|
||||
$mimes = $this->get_mime_types_to_gzip( $source_type );
|
||||
|
||||
if ( in_array( $file_type, $mimes ) && is_readable( $file_path ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mime types to gzip
|
||||
*
|
||||
* @param string $source_type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_mime_types_to_gzip( $source_type ) {
|
||||
/**
|
||||
* Return array of mime types that needs to be gzipped before upload
|
||||
*
|
||||
* @param array $mime_types The array of mime types
|
||||
* @param bool $media_library If the uploaded file is part of the media library
|
||||
* @param string $source_type The source type of the uploaded item
|
||||
*/
|
||||
return apply_filters(
|
||||
'as3cf_gzip_mime_types',
|
||||
array(
|
||||
'css' => 'text/css',
|
||||
'eot' => 'application/vnd.ms-fontobject',
|
||||
'html' => 'text/html',
|
||||
'ico' => 'image/x-icon',
|
||||
'js' => 'application/javascript',
|
||||
'json' => 'application/json',
|
||||
'otf' => 'application/x-font-opentype',
|
||||
'rss' => 'application/rss+xml',
|
||||
'svg' => 'image/svg+xml',
|
||||
'ttf' => 'application/x-font-ttf',
|
||||
'woff' => 'application/font-woff',
|
||||
'woff2' => 'application/font-woff2',
|
||||
'xml' => 'application/xml',
|
||||
),
|
||||
'media_library' === $source_type,
|
||||
$source_type
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the given file name already been offloaded?
|
||||
*
|
||||
* @param string $filename
|
||||
* @param array $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function in_offloaded_files( $filename, $options ) {
|
||||
if ( empty( $options['offloaded_files'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists( $filename, $options['offloaded_files'] );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user