v1.2.0
This commit is contained in:
parent
1dd5809016
commit
31be5438e7
7 changed files with 293 additions and 195 deletions
|
|
@ -1,5 +1,33 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.2.0 =
|
||||||
|
|
||||||
|
_Release date: 2026-06-02_
|
||||||
|
|
||||||
|
**Highlights**
|
||||||
|
|
||||||
|
* Media uploads dramatically faster: concurrent S3 uploads + streaming from disk
|
||||||
|
|
||||||
|
**Performance**
|
||||||
|
|
||||||
|
* Concurrent uploads via AWS CommandPool (default 5, configurable via IDRIVEE2_UPLOAD_CONCURRENCY)
|
||||||
|
* Files now stream directly from disk (fopen + resource) instead of loading fully into memory
|
||||||
|
* Removed per-file headObject pre-check (eliminates N extra HTTP round-trips per image)
|
||||||
|
* Single DB write for upload stats per attachment (replaces one write per file)
|
||||||
|
* Hook priority lowered from 999 to 10
|
||||||
|
|
||||||
|
**Compatibility**
|
||||||
|
|
||||||
|
* WordPress: 4.1 - 7.1
|
||||||
|
* PHP: 8.1 - 8.5
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
|
||||||
|
* PHP Coding Standards: 3.13.5 (0 errors)
|
||||||
|
* WordPress Coding Standards: 3.3.0 (0 violations)
|
||||||
|
* PHPStan: Level 9, 0 errors
|
||||||
|
* PHPUnit: 22 tests, 54 assertions
|
||||||
|
|
||||||
= 1.1.4 =
|
= 1.1.4 =
|
||||||
|
|
||||||
_Release date: 2026-06-02_
|
_Release date: 2026-06-02_
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Gitea Plugin URI: https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload
|
* Gitea Plugin URI: https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload
|
||||||
* Primary Branch: main
|
* Primary Branch: main
|
||||||
* Description: Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging.
|
* Description: Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging.
|
||||||
* Version: 1.1.4
|
* Version: 1.2.0
|
||||||
* Requires at least: 4.1
|
* Requires at least: 4.1
|
||||||
* Requires PHP: 8.1
|
* Requires PHP: 8.1
|
||||||
* Author: ROBOTSTXT
|
* Author: ROBOTSTXT
|
||||||
|
|
@ -36,7 +36,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||||
*
|
*
|
||||||
* @since 1.1.4
|
* @since 1.1.4
|
||||||
*/
|
*/
|
||||||
define( 'IDRIVEE2_MEDIA_VERSION', '1.1.4' );
|
define( 'IDRIVEE2_MEDIA_VERSION', '1.2.0' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load Composer autoloader if available.
|
* Load Composer autoloader if available.
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,69 @@ class Logger {
|
||||||
$this->track_s3_operation( $operation );
|
$this->track_s3_operation( $operation );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log and track the result of a batch S3 upload.
|
||||||
|
*
|
||||||
|
* Performs a single DB write for all uploads in the batch instead of one
|
||||||
|
* write per file, reducing database load on bulk media imports.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $operation S3 operation name (e.g. 'putObject').
|
||||||
|
* @param int $success_count Number of files successfully uploaded.
|
||||||
|
* @param int $fail_count Number of files that failed.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function s3_batch( string $operation, int $success_count, int $fail_count ): void {
|
||||||
|
if ( $success_count > 0 ) {
|
||||||
|
$this->track_bulk_s3_operations( $operation, $success_count );
|
||||||
|
}
|
||||||
|
if ( $fail_count > 0 ) {
|
||||||
|
$this->log(
|
||||||
|
self::LEVEL_ERROR,
|
||||||
|
sprintf( 'S3 %s batch: %d failure(s)', $operation, $fail_count )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment S3 operation counters by an arbitrary count in a single DB write.
|
||||||
|
*
|
||||||
|
* @since 1.2.0
|
||||||
|
*
|
||||||
|
* @param string $operation S3 operation name.
|
||||||
|
* @param int $count Number of operations to add.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function track_bulk_s3_operations( string $operation, int $count ): void {
|
||||||
|
$option_key = 'idrivee2_s3_operations';
|
||||||
|
$raw = get_option( $option_key, array() );
|
||||||
|
/**
|
||||||
|
* Daily S3 operation counts, keyed by date and operation name.
|
||||||
|
*
|
||||||
|
* @var array<string, array<string, int>> $stats
|
||||||
|
*/
|
||||||
|
$stats = is_array( $raw ) ? $raw : array();
|
||||||
|
|
||||||
|
$today = gmdate( 'Y-m-d' );
|
||||||
|
if ( ! isset( $stats[ $today ] ) ) {
|
||||||
|
$stats[ $today ] = array();
|
||||||
|
}
|
||||||
|
if ( ! isset( $stats[ $today ][ $operation ] ) ) {
|
||||||
|
$stats[ $today ][ $operation ] = 0;
|
||||||
|
}
|
||||||
|
$stats[ $today ][ $operation ] += $count;
|
||||||
|
|
||||||
|
$cutoff_date = gmdate( 'Y-m-d', time() - ( 30 * DAY_IN_SECONDS ) );
|
||||||
|
foreach ( array_keys( $stats ) as $date ) {
|
||||||
|
if ( $date < $cutoff_date ) {
|
||||||
|
unset( $stats[ $date ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option( $option_key, $stats );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an authentication failure.
|
* Log an authentication failure.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||||
/**
|
/**
|
||||||
* Media uploader for handling file uploads to iDrivee2.
|
* Media uploader for handling file uploads to iDrivee2.
|
||||||
*
|
*
|
||||||
* Uploads attachment files and all generated sizes to S3, deletes local copies,
|
* Uploads attachment files and all generated sizes to S3 concurrently using
|
||||||
|
* the AWS CommandPool, streams files directly from disk, deletes local copies,
|
||||||
* and updates the attachment GUID to point to the S3 URL.
|
* and updates the attachment GUID to point to the S3 URL.
|
||||||
*
|
*
|
||||||
* @since 0.3.0
|
* @since 0.3.0
|
||||||
|
|
@ -69,27 +70,25 @@ class Media_Uploader {
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function register(): void {
|
public function register(): void {
|
||||||
// Use wp_update_attachment_metadata with high priority to ensure thumbnails are generated.
|
add_filter( 'wp_update_attachment_metadata', array( $this, 'upload_attachment_to_idrivee2' ), 10, 2 );
|
||||||
// Priority 999 ensures this runs AFTER all thumbnail generation is complete.
|
|
||||||
add_filter( 'wp_update_attachment_metadata', array( $this, 'upload_attachment_to_idrivee2' ), 999, 2 );
|
|
||||||
add_action( 'edit_attachment', array( $this, 'handle_edit_attachment' ) );
|
add_action( 'edit_attachment', array( $this, 'handle_edit_attachment' ) );
|
||||||
|
|
||||||
// Register cron job for cleaning up local files.
|
|
||||||
add_action( 'idrivee2_cleanup_local_files', array( $this, 'cleanup_local_files' ) );
|
add_action( 'idrivee2_cleanup_local_files', array( $this, 'cleanup_local_files' ) );
|
||||||
|
|
||||||
// Schedule cron if not already scheduled.
|
|
||||||
if ( ! wp_next_scheduled( 'idrivee2_cleanup_local_files' ) ) {
|
if ( ! wp_next_scheduled( 'idrivee2_cleanup_local_files' ) ) {
|
||||||
wp_schedule_event( time(), 'every_five_minutes', 'idrivee2_cleanup_local_files' );
|
wp_schedule_event( time(), 'every_five_minutes', 'idrivee2_cleanup_local_files' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads attachment files to iDrivee2 after sizes are generated.
|
* Upload attachment files to iDrivee2 concurrently after sizes are generated.
|
||||||
*
|
*
|
||||||
* This function pushes the original file and all generated sizes to the
|
* Uses the AWS CommandPool to upload the original file and all generated
|
||||||
* configured S3-compatible host, captures the returned ObjectURL for the
|
* thumbnail sizes in parallel, streaming each file directly from disk.
|
||||||
* original file, deletes the local copies, updates the attachment's GUID,
|
* No headObject pre-check is performed — files are assumed to be new.
|
||||||
* and filters the front-end URL to use the S3 ObjectURL.
|
*
|
||||||
|
* Concurrency defaults to 5 and can be overridden via the
|
||||||
|
* IDRIVEE2_UPLOAD_CONCURRENCY constant in wp-config.php.
|
||||||
*
|
*
|
||||||
* @since 0.3.0
|
* @since 0.3.0
|
||||||
*
|
*
|
||||||
|
|
@ -98,203 +97,199 @@ class Media_Uploader {
|
||||||
* @return array<string, mixed> Unchanged metadata array.
|
* @return array<string, mixed> Unchanged metadata array.
|
||||||
*/
|
*/
|
||||||
public function upload_attachment_to_idrivee2( array $meta, int $attachment_id ): array {
|
public function upload_attachment_to_idrivee2( array $meta, int $attachment_id ): array {
|
||||||
// Bail if configuration is incomplete.
|
|
||||||
if ( ! $this->config->is_configured() ) {
|
if ( ! $this->config->is_configured() ) {
|
||||||
return $meta;
|
return $meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the S3 client.
|
|
||||||
$client = $this->client_factory->create();
|
|
||||||
|
|
||||||
// Build list of files: original + each image size.
|
|
||||||
$meta_file = isset( $meta['file'] ) && is_string( $meta['file'] ) ? $meta['file'] : '';
|
$meta_file = isset( $meta['file'] ) && is_string( $meta['file'] ) ? $meta['file'] : '';
|
||||||
if ( '' === $meta_file ) {
|
if ( '' === $meta_file ) {
|
||||||
return $meta;
|
return $meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build list of files: original + each thumbnail.
|
||||||
$upload_dir = wp_upload_dir();
|
$upload_dir = wp_upload_dir();
|
||||||
$basedir = $upload_dir['basedir'];
|
$basedir = $upload_dir['basedir'];
|
||||||
$base_path = path_join( $basedir, $meta_file );
|
$base_path = path_join( $basedir, $meta_file );
|
||||||
$files = array(
|
|
||||||
'original' => $base_path,
|
|
||||||
);
|
|
||||||
|
|
||||||
$meta_sizes = isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ? $meta['sizes'] : array();
|
$meta_sizes = isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ? $meta['sizes'] : array();
|
||||||
|
|
||||||
|
/** @var array<string, string> $files */
|
||||||
|
$files = array( 'original' => $base_path );
|
||||||
foreach ( $meta_sizes as $size ) {
|
foreach ( $meta_sizes as $size ) {
|
||||||
if ( is_array( $size ) && isset( $size['file'] ) && is_string( $size['file'] ) ) {
|
if ( is_array( $size ) && isset( $size['file'] ) && is_string( $size['file'] ) ) {
|
||||||
$files[ $size['file'] ] = path_join( dirname( $base_path ), $size['file'] );
|
$files[ $size['file'] ] = path_join( dirname( $base_path ), $size['file'] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log file list for debugging.
|
// Open a stream for each file and build the AWS command list.
|
||||||
$this->logger->info(
|
$client = $this->client_factory->create();
|
||||||
sprintf( 'Preparing to upload %d files to S3', count( $files ) ),
|
$bucket = $this->config->get_bucket();
|
||||||
array(
|
$commands = array();
|
||||||
'attachment_id' => $attachment_id,
|
$key_map = array(); // int index -> file metadata
|
||||||
'original' => basename( $meta_file ),
|
$handles = array(); // int index -> resource
|
||||||
'sizes_count' => count( $meta_sizes ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$object_url = '';
|
$real_basedir = realpath( $basedir );
|
||||||
$s3_base_url = '';
|
$idx = 0;
|
||||||
$upload_count = 0;
|
|
||||||
|
|
||||||
// Load and initialise WP_Filesystem.
|
foreach ( $files as $file_key => $local_path ) {
|
||||||
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
if ( ! file_exists( $local_path ) || ! is_readable( $local_path ) ) {
|
||||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
|
||||||
}
|
|
||||||
WP_Filesystem();
|
|
||||||
global $wp_filesystem;
|
|
||||||
|
|
||||||
if ( ! ( $wp_filesystem instanceof \WP_Filesystem_Base ) ) {
|
|
||||||
$this->logger->error( 'WP_Filesystem not available, aborting S3 upload' );
|
|
||||||
return $meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload each file via WP_Filesystem.
|
|
||||||
foreach ( $files as $key => $local_path ) {
|
|
||||||
// Skip if file doesn't exist.
|
|
||||||
if ( ! $wp_filesystem->exists( $local_path ) ) {
|
|
||||||
$this->logger->warning(
|
|
||||||
'File does not exist, skipping upload',
|
|
||||||
array(
|
|
||||||
'key' => $key,
|
|
||||||
'path' => $local_path,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine S3 object key.
|
// Security: refuse to read files outside the uploads directory.
|
||||||
$object_key = ( 'original' === $key )
|
$real_path = realpath( $local_path );
|
||||||
|
if ( false === $real_path || false === $real_basedir
|
||||||
|
|| 0 !== strpos( $real_path, $real_basedir . DIRECTORY_SEPARATOR ) ) {
|
||||||
|
$this->logger->warning( 'Refusing upload outside uploads dir', array( 'path' => $local_path ) );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$object_key = ( 'original' === $file_key )
|
||||||
? $meta_file
|
? $meta_file
|
||||||
: dirname( $meta_file ) . '/' . $key;
|
: dirname( $meta_file ) . '/' . $file_key;
|
||||||
|
|
||||||
// Check if file already exists in S3.
|
$fh = fopen( $real_path, 'rb' );
|
||||||
try {
|
if ( false === $fh ) {
|
||||||
$exists = $client->headObject(
|
$this->logger->warning( 'Cannot open file for upload', array( 'path' => $local_path ) );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handles[ $idx ] = $fh;
|
||||||
|
$key_map[ $idx ] = array(
|
||||||
|
'key' => $file_key,
|
||||||
|
'local_path' => $local_path,
|
||||||
|
'object_key' => $object_key,
|
||||||
|
);
|
||||||
|
$commands[ $idx ] = $client->getCommand(
|
||||||
|
'PutObject',
|
||||||
array(
|
array(
|
||||||
'Bucket' => $this->config->get_bucket(),
|
'Bucket' => $bucket,
|
||||||
'Key' => $object_key,
|
'Key' => $object_key,
|
||||||
)
|
'Body' => $fh,
|
||||||
);
|
|
||||||
// File exists, skip upload.
|
|
||||||
$this->logger->info(
|
|
||||||
sprintf( 'File already exists in S3, skipping: %s', basename( $object_key ) ),
|
|
||||||
array( 'object_key' => $object_key )
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
} catch ( \Aws\Exception\AwsException $e ) {
|
|
||||||
// File doesn't exist (404), proceed with upload.
|
|
||||||
if ( 404 !== $e->getStatusCode() ) {
|
|
||||||
// Other error, log and skip.
|
|
||||||
$this->logger->warning(
|
|
||||||
sprintf( 'Error checking S3 file existence: %s', basename( $object_key ) ),
|
|
||||||
array(
|
|
||||||
'object_key' => $object_key,
|
|
||||||
'error' => $e->getAwsErrorMessage() ?? '',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve file contents via WP_Filesystem.
|
|
||||||
$content = $wp_filesystem->get_contents( $local_path );
|
|
||||||
if ( false === $content ) {
|
|
||||||
$this->logger->warning(
|
|
||||||
'Failed to read file contents, skipping upload',
|
|
||||||
array(
|
|
||||||
'key' => $key,
|
|
||||||
'path' => $local_path,
|
|
||||||
'object_key' => $object_key,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info(
|
|
||||||
sprintf( 'Uploading to S3: %s', basename( $object_key ) ),
|
|
||||||
array(
|
|
||||||
'object_key' => $object_key,
|
|
||||||
'file_size' => strlen( $content ),
|
|
||||||
'is_original' => ( 'original' === $key ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Upload to S3 from memory.
|
|
||||||
try {
|
|
||||||
$result = $client->putObject(
|
|
||||||
array(
|
|
||||||
'Bucket' => $this->config->get_bucket(),
|
|
||||||
'Key' => $object_key,
|
|
||||||
'Body' => $content,
|
|
||||||
'ACL' => 'public-read',
|
'ACL' => 'public-read',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
++$idx;
|
||||||
|
}
|
||||||
|
|
||||||
// Log successful upload.
|
if ( empty( $commands ) ) {
|
||||||
$this->logger->s3_operation( 'putObject', true, basename( $object_key ) );
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
// Capture the base URL for constructing CDN URLs.
|
// Run all uploads concurrently.
|
||||||
if ( 'original' === $key ) {
|
$concurrency = defined( 'IDRIVEE2_UPLOAD_CONCURRENCY' )
|
||||||
// Build CDN URL if domain configured, otherwise use S3 URL.
|
? max( 1, (int) IDRIVEE2_UPLOAD_CONCURRENCY )
|
||||||
if ( $this->config->has_domain() ) {
|
: 5;
|
||||||
$s3_base_url = trailingslashit( $this->config->get_domain() ) . dirname( $meta_file );
|
$uploaded_paths = array();
|
||||||
} elseif ( isset( $result['ObjectURL'] ) && is_string( $result['ObjectURL'] ) && '' !== $result['ObjectURL'] ) {
|
$failed_count = 0;
|
||||||
|
$object_url = '';
|
||||||
|
$s3_base_url = '';
|
||||||
|
$has_domain = $this->config->has_domain();
|
||||||
|
$cdn_domain = $this->config->get_domain();
|
||||||
|
$logger = $this->logger;
|
||||||
|
|
||||||
|
$pool = new \Aws\CommandPool(
|
||||||
|
$client,
|
||||||
|
$commands,
|
||||||
|
array(
|
||||||
|
'concurrency' => $concurrency,
|
||||||
|
'fulfilled' => function (
|
||||||
|
mixed $result,
|
||||||
|
int|string $cmd_idx
|
||||||
|
) use (
|
||||||
|
&$uploaded_paths,
|
||||||
|
&$object_url,
|
||||||
|
&$s3_base_url,
|
||||||
|
$key_map,
|
||||||
|
$meta_file,
|
||||||
|
$has_domain,
|
||||||
|
$cdn_domain
|
||||||
|
): void {
|
||||||
|
if ( ! is_int( $cmd_idx ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$info = $key_map[ $cmd_idx ];
|
||||||
|
$uploaded_paths[] = $info['local_path'];
|
||||||
|
|
||||||
|
if ( 'original' === $info['key'] ) {
|
||||||
|
if ( $has_domain ) {
|
||||||
|
$s3_base_url = trailingslashit( $cdn_domain ) . dirname( $meta_file );
|
||||||
|
} elseif ( $result instanceof \Aws\ResultInterface
|
||||||
|
&& isset( $result['ObjectURL'] )
|
||||||
|
&& is_string( $result['ObjectURL'] )
|
||||||
|
&& '' !== $result['ObjectURL']
|
||||||
|
) {
|
||||||
$object_url = $result['ObjectURL'];
|
$object_url = $result['ObjectURL'];
|
||||||
$s3_base_url = dirname( $result['ObjectURL'] );
|
$s3_base_url = dirname( $result['ObjectURL'] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
++$upload_count;
|
'rejected' => function (
|
||||||
|
mixed $reason,
|
||||||
} catch ( \Aws\Exception\AwsException $e ) {
|
int|string $cmd_idx
|
||||||
// Log failed upload.
|
) use (
|
||||||
$this->logger->s3_operation( 'putObject', false, basename( $object_key ), $e->getAwsErrorMessage() ?? '' );
|
&$failed_count,
|
||||||
// Continue to next file on error.
|
$key_map,
|
||||||
continue;
|
$logger
|
||||||
|
): void {
|
||||||
|
++$failed_count;
|
||||||
|
if ( ! is_int( $cmd_idx ) ) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
$info = $key_map[ $cmd_idx ];
|
||||||
|
$error = '';
|
||||||
|
if ( $reason instanceof \Aws\Exception\AwsException ) {
|
||||||
|
$error = $reason->getAwsErrorMessage() ?? $reason->getMessage();
|
||||||
|
} elseif ( $reason instanceof \Throwable ) {
|
||||||
|
$error = $reason->getMessage();
|
||||||
}
|
}
|
||||||
|
$logger->error(
|
||||||
|
sprintf( 'S3 upload failed: %s', basename( $info['object_key'] ) ),
|
||||||
|
array( 'error' => $error )
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Log upload summary.
|
try {
|
||||||
|
$pool->promise()->wait();
|
||||||
|
} finally {
|
||||||
|
// Always close file streams, even if wait() throws.
|
||||||
|
foreach ( $handles as $fh ) {
|
||||||
|
fclose( $fh );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$upload_count = count( $uploaded_paths );
|
||||||
|
$total = count( $commands );
|
||||||
|
|
||||||
|
// Single log entry and single DB write for all uploads.
|
||||||
|
$this->logger->s3_batch( 'putObject', $upload_count, $failed_count );
|
||||||
$this->logger->info(
|
$this->logger->info(
|
||||||
sprintf( 'Upload complete: %d of %d files uploaded to S3', $upload_count, count( $files ) ),
|
sprintf( 'Batch upload complete: %d/%d files uploaded to S3', $upload_count, $total ),
|
||||||
array(
|
array(
|
||||||
'attachment_id' => $attachment_id,
|
'attachment_id' => $attachment_id,
|
||||||
'uploaded' => $upload_count,
|
'concurrency' => $concurrency,
|
||||||
'expected' => count( $files ),
|
'failed' => $failed_count,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update metadata and schedule deletion if files were uploaded.
|
|
||||||
if ( $upload_count > 0 ) {
|
if ( $upload_count > 0 ) {
|
||||||
update_post_meta( $attachment_id, '_idrivee2_s3_base_url', $s3_base_url );
|
update_post_meta( $attachment_id, '_idrivee2_s3_base_url', $s3_base_url );
|
||||||
update_post_meta( $attachment_id, '_idrivee2_last_upload', time() );
|
update_post_meta( $attachment_id, '_idrivee2_last_upload', time() );
|
||||||
|
$this->schedule_files_for_deletion( $uploaded_paths );
|
||||||
// Schedule local files for deletion after 3 minutes.
|
|
||||||
$this->schedule_files_for_deletion( array_values( $files ) );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve relative path in database.
|
|
||||||
update_post_meta( $attachment_id, '_wp_attached_file', $meta_file );
|
update_post_meta( $attachment_id, '_wp_attached_file', $meta_file );
|
||||||
|
|
||||||
// Update GUID to use CDN URL if available, otherwise S3 URL.
|
|
||||||
if ( $s3_base_url ) {
|
if ( $s3_base_url ) {
|
||||||
$file_name = basename( $meta_file );
|
if ( $has_domain ) {
|
||||||
if ( $this->config->has_domain() ) {
|
$public_url = trailingslashit( $cdn_domain ) . $meta_file;
|
||||||
// Use CDN domain.
|
|
||||||
$public_url = trailingslashit( $this->config->get_domain() ) . $meta_file;
|
|
||||||
} elseif ( $object_url ) {
|
} elseif ( $object_url ) {
|
||||||
// Use S3 ObjectURL.
|
|
||||||
$public_url = $object_url;
|
$public_url = $object_url;
|
||||||
} else {
|
} else {
|
||||||
// Fallback: construct from base URL.
|
$public_url = $s3_base_url . '/' . basename( $meta_file );
|
||||||
$public_url = $s3_base_url . '/' . $file_name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the GUID to the public URL.
|
|
||||||
wp_update_post(
|
wp_update_post(
|
||||||
array(
|
array(
|
||||||
'ID' => $attachment_id,
|
'ID' => $attachment_id,
|
||||||
|
|
@ -322,24 +317,22 @@ class Media_Uploader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule files for deletion.
|
* Schedule files for deletion via the cron queue.
|
||||||
*
|
|
||||||
* Adds files to a deletion queue with timestamp. Files will be deleted
|
|
||||||
* by the cron job after 3 minutes to give WordPress time to process.
|
|
||||||
*
|
*
|
||||||
* @since 1.0.1
|
* @since 1.0.1
|
||||||
*
|
*
|
||||||
* @param array<string> $files Array of file paths to delete.
|
* @param array<string> $files Array of local file paths to delete.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function schedule_files_for_deletion( array $files ): void {
|
private function schedule_files_for_deletion( array $files ): void {
|
||||||
$raw_q = get_option( 'idrivee2_deletion_queue', array() );
|
$raw_q = get_option( 'idrivee2_deletion_queue', array() );
|
||||||
$queue = is_array( $raw_q ) ? $raw_q : array();
|
$queue = is_array( $raw_q ) ? $raw_q : array();
|
||||||
|
|
||||||
|
$now = time();
|
||||||
foreach ( $files as $file_path ) {
|
foreach ( $files as $file_path ) {
|
||||||
$queue[] = array(
|
$queue[] = array(
|
||||||
'path' => $file_path,
|
'path' => $file_path,
|
||||||
'timestamp' => time(),
|
'timestamp' => $now,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,14 +340,11 @@ class Media_Uploader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up local files that were uploaded to S3.
|
* Clean up local files after successful S3 upload.
|
||||||
*
|
*
|
||||||
* This method runs via WP-Cron every 5 minutes. It deletes files that:
|
* Runs via WP-Cron every 5 minutes. Deletes files that have been in the
|
||||||
* - Were uploaded to S3 successfully
|
* queue for at least 3 minutes, giving WordPress time to serve thumbnails
|
||||||
* - Have been in the queue for at least 3 minutes
|
* from the local copy before removal.
|
||||||
*
|
|
||||||
* The 3-minute delay ensures WordPress has time to display thumbnails
|
|
||||||
* in the admin before files are removed.
|
|
||||||
*
|
*
|
||||||
* @since 1.0.1
|
* @since 1.0.1
|
||||||
*
|
*
|
||||||
|
|
@ -367,7 +357,6 @@ class Media_Uploader {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and initialise WP_Filesystem.
|
|
||||||
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
}
|
}
|
||||||
|
|
@ -395,46 +384,32 @@ class Media_Uploader {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requeue items with a malformed timestamp rather than deleting immediately.
|
|
||||||
if ( 0 === $timestamp ) {
|
if ( 0 === $timestamp ) {
|
||||||
$new_queue[] = $item;
|
$new_queue[] = $item;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only delete files older than 3 minutes.
|
|
||||||
if ( ( $current_time - $timestamp ) < 180 ) {
|
if ( ( $current_time - $timestamp ) < 180 ) {
|
||||||
$new_queue[] = $item;
|
$new_queue[] = $item;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the file if it exists.
|
|
||||||
if ( $wp_filesystem->exists( $file_path ) ) {
|
if ( $wp_filesystem->exists( $file_path ) ) {
|
||||||
$deleted_ok = $wp_filesystem->delete( $file_path );
|
$deleted_ok = $wp_filesystem->delete( $file_path );
|
||||||
if ( $deleted_ok ) {
|
if ( $deleted_ok ) {
|
||||||
++$deleted;
|
++$deleted;
|
||||||
$this->logger->info(
|
|
||||||
'Local file deleted after S3 upload',
|
|
||||||
array( 'path' => basename( $file_path ) )
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Keep in queue to retry later.
|
|
||||||
$new_queue[] = $item;
|
$new_queue[] = $item;
|
||||||
$this->logger->warning(
|
$this->logger->warning( 'Failed to delete local file, will retry', array( 'path' => $file_path ) );
|
||||||
'Failed to delete local file, will retry',
|
|
||||||
array( 'path' => $file_path )
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If file doesn't exist, consider it successfully cleaned up (don't re-add to queue).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the queue.
|
|
||||||
update_option( 'idrivee2_deletion_queue', $new_queue, false );
|
update_option( 'idrivee2_deletion_queue', $new_queue, false );
|
||||||
|
|
||||||
// Log cleanup summary if any files were deleted.
|
|
||||||
if ( $deleted > 0 ) {
|
if ( $deleted > 0 ) {
|
||||||
$this->logger->info(
|
$this->logger->info(
|
||||||
sprintf( 'Cleanup completed: %d files deleted', $deleted ),
|
sprintf( 'Cleanup: %d local files deleted', $deleted ),
|
||||||
array( 'remaining' => count( $new_queue ) )
|
array( 'remaining' => count( $new_queue ) )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
readme.txt
36
readme.txt
|
|
@ -3,9 +3,9 @@ Contributors: robotstxt, javiercasares
|
||||||
Tags: media, upload, s3, cdn, storage, idrivee2, cloud
|
Tags: media, upload, s3, cdn, storage, idrivee2, cloud
|
||||||
Requires at least: 4.1
|
Requires at least: 4.1
|
||||||
Tested up to: 7.1
|
Tested up to: 7.1
|
||||||
Stable tag: 1.1.4
|
Stable tag: 1.2.0
|
||||||
Requires PHP: 8.1
|
Requires PHP: 8.1
|
||||||
Version: 1.1.4
|
Version: 1.2.0
|
||||||
License: GPL-3.0-or-later
|
License: GPL-3.0-or-later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
|
@ -71,6 +71,9 @@ AWS region for the S3 service (e.g., 'us-east-1', 'eu-west-1').
|
||||||
`define('IDRIVEE2_MEDIA_DOMAIN', 'https://cdn.yourdomain.com');`
|
`define('IDRIVEE2_MEDIA_DOMAIN', 'https://cdn.yourdomain.com');`
|
||||||
Custom CDN domain for serving media files. If not defined, files will be served directly from S3 ObjectURL.
|
Custom CDN domain for serving media files. If not defined, files will be served directly from S3 ObjectURL.
|
||||||
|
|
||||||
|
`define('IDRIVEE2_UPLOAD_CONCURRENCY', 5);`
|
||||||
|
Number of simultaneous S3 uploads per attachment. Default: 5. Increase for faster bulk imports on fast connections; decrease if iDrivee2 returns throttling errors. Minimum: 1.
|
||||||
|
|
||||||
**Security Logging:**
|
**Security Logging:**
|
||||||
|
|
||||||
To enable security logging, add these constants:
|
To enable security logging, add these constants:
|
||||||
|
|
@ -196,6 +199,35 @@ PHP 8.2 or higher is required. The plugin uses strict type declarations and is t
|
||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.2.0 =
|
||||||
|
|
||||||
|
_Release date: 2026-06-02_
|
||||||
|
|
||||||
|
**Highlights**
|
||||||
|
|
||||||
|
* Media uploads now run concurrently — dramatically faster for bulk imports
|
||||||
|
* Files stream directly from disk; no full load into memory
|
||||||
|
|
||||||
|
**Performance**
|
||||||
|
|
||||||
|
* Concurrent S3 uploads via AWS `CommandPool` (default 5 simultaneous, tunable via `IDRIVEE2_UPLOAD_CONCURRENCY` in wp-config.php)
|
||||||
|
* Files streamed directly from disk using native PHP streams instead of loading entirely into memory — critical for large images
|
||||||
|
* Removed per-file `headObject` pre-check — files are assumed new (they just came from WordPress thumbnail generation)
|
||||||
|
* Single DB write for upload statistics per attachment instead of one per file
|
||||||
|
* Hook priority lowered from 999 to 10 — no unnecessary delay
|
||||||
|
|
||||||
|
**Compatibility**
|
||||||
|
|
||||||
|
* WordPress: 4.1 - 7.1
|
||||||
|
* PHP: 8.1 - 8.5
|
||||||
|
|
||||||
|
**Tests**
|
||||||
|
|
||||||
|
* PHP Coding Standards: 3.13.5 (0 errors)
|
||||||
|
* WordPress Coding Standards: 3.3.0 (0 violations)
|
||||||
|
* PHPStan: Level 9, 0 errors
|
||||||
|
* PHPUnit: 22 tests, 54 assertions
|
||||||
|
|
||||||
= 1.1.4 =
|
= 1.1.4 =
|
||||||
|
|
||||||
_Release date: 2026-06-02_
|
_Release date: 2026-06-02_
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "iDrivee2 Media Upload",
|
"name": "iDrivee2 Media Upload",
|
||||||
"slug": "idrivee2-media-upload",
|
"slug": "idrivee2-media-upload",
|
||||||
"version": "1.1.4",
|
"version": "1.2.0",
|
||||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload/releases/download/1.1.4/idrivee2-media-upload-1.1.4.zip",
|
"download_url": "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload/releases/download/1.2.0/idrivee2-media-upload-1.2.0.zip",
|
||||||
"requires": "4.1",
|
"requires": "4.1",
|
||||||
"requires_php": "8.1",
|
"requires_php": "8.1",
|
||||||
"tested": "7.1",
|
"tested": "7.1",
|
||||||
|
|
@ -11,10 +11,10 @@
|
||||||
"author_profile": "https://www.robotstxt.es/",
|
"author_profile": "https://www.robotstxt.es/",
|
||||||
"homepage": "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload",
|
"homepage": "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload",
|
||||||
"description": "Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging.",
|
"description": "Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging.",
|
||||||
"changelog": "<h3>1.1.4 - 2026-06-02</h3><ul><li><strong>Added:</strong> Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)</li><li><strong>Fixed:</strong> WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version</li><li><strong>Changed:</strong> Tested up to WordPress 7.1, PHP 8.2-8.5</li></ul><h3>1.1.3 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Critical namespace issue with Robotstxt_Updater class causing fatal error</li><li><strong>Fixed:</strong> Plugin now loads correctly without PHP fatal errors</li></ul><h3>1.1.2 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Deployment script updated to use PHP 8.2 as platform base for production builds</li><li><strong>Changed:</strong> Now uses composer update --no-dev for consistent dependency resolution</li><li><strong>Improved:</strong> Production packages guarantee PHP 8.2+ compatibility regardless of dev environment</li></ul><h3>1.1.1 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)</li><li><strong>Improved:</strong> Production packages now contain all files required for automatic updates from Gitea</li></ul><h3>1.1.0 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Added explicit PHP version requirement (>=8.2) to composer.json</li><li><strong>Changed:</strong> Updated update.json with correct plugin information</li><li><strong>Changed:</strong> Fixed Text Domain in robotstxt-updater.php to match plugin slug</li><li><strong>Fixed:</strong> Composer now validates PHP version during dependency installation</li><li><strong>Fixed:</strong> Plugin update system correctly identifies the plugin</li><li><strong>Fixed:</strong> Translations properly loaded for updater error messages</li><li><strong>Improved:</strong> All text domains now consistently use 'idrivee2-media-upload'</li></ul><h3>1.0.0 - 2026-02-03</h3><ul><li><strong>Release:</strong> First stable release</li><li><strong>Feature:</strong> Automatic upload of media files to iDrivee2 (S3-compatible storage)</li><li><strong>Feature:</strong> URL rewriting to serve media from CDN</li><li><strong>Feature:</strong> Local file deletion after successful upload</li><li><strong>Feature:</strong> Admin interface with connection and upload testing</li><li><strong>Security:</strong> Enterprise-grade security with nonce validation</li><li><strong>Architecture:</strong> Class-based modular architecture with dependency injection</li><li><strong>Testing:</strong> PHPUnit test structure and PHPStan static analysis</li><li><strong>Compatibility:</strong> WordPress 6.8+ and PHP 8.2+</li></ul>",
|
"changelog": "<h3>1.2.0 - 2026-06-02</h3><ul><li><strong>Performance:</strong> Concurrent S3 uploads via AWS CommandPool (default 5, tunable via IDRIVEE2_UPLOAD_CONCURRENCY)</li><li><strong>Performance:</strong> Stream files directly from disk — no full load into memory</li><li><strong>Performance:</strong> Removed per-file headObject pre-check — single batch DB write for stats</li><li><strong>Changed:</strong> Hook priority lowered from 999 to 10</li></ul><h3>1.1.4 - 2026-06-02</h3><ul><li><strong>Added:</strong> Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)</li><li><strong>Fixed:</strong> WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version</li><li><strong>Changed:</strong> Tested up to WordPress 7.1, PHP 8.1-8.5, Requires at least 4.1</li></ul><h3>1.1.3 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Critical namespace issue with Robotstxt_Updater class causing fatal error</li><li><strong>Fixed:</strong> Plugin now loads correctly without PHP fatal errors</li></ul><h3>1.1.2 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Deployment script updated to use PHP 8.2 as platform base for production builds</li><li><strong>Changed:</strong> Now uses composer update --no-dev for consistent dependency resolution</li><li><strong>Improved:</strong> Production packages guarantee PHP 8.2+ compatibility regardless of dev environment</li></ul><h3>1.1.1 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)</li><li><strong>Improved:</strong> Production packages now contain all files required for automatic updates from Gitea</li></ul><h3>1.1.0 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Added explicit PHP version requirement (>=8.2) to composer.json</li><li><strong>Changed:</strong> Updated update.json with correct plugin information</li><li><strong>Changed:</strong> Fixed Text Domain in robotstxt-updater.php to match plugin slug</li><li><strong>Fixed:</strong> Composer now validates PHP version during dependency installation</li><li><strong>Fixed:</strong> Plugin update system correctly identifies the plugin</li><li><strong>Fixed:</strong> Translations properly loaded for updater error messages</li><li><strong>Improved:</strong> All text domains now consistently use 'idrivee2-media-upload'</li></ul><h3>1.0.0 - 2026-02-03</h3><ul><li><strong>Release:</strong> First stable release</li><li><strong>Feature:</strong> Automatic upload of media files to iDrivee2 (S3-compatible storage)</li><li><strong>Feature:</strong> URL rewriting to serve media from CDN</li><li><strong>Feature:</strong> Local file deletion after successful upload</li><li><strong>Feature:</strong> Admin interface with connection and upload testing</li><li><strong>Security:</strong> Enterprise-grade security with nonce validation</li><li><strong>Architecture:</strong> Class-based modular architecture with dependency injection</li><li><strong>Testing:</strong> PHPUnit test structure and PHPStan static analysis</li><li><strong>Compatibility:</strong> WordPress 6.8+ and PHP 8.2+</li></ul>",
|
||||||
"sections": {
|
"sections": {
|
||||||
"description": "Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging. The plugin intercepts WordPress media uploads, pushes files to an S3-compatible bucket, deletes local copies, and rewrites URLs to serve media from the CDN.",
|
"description": "Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging. The plugin intercepts WordPress media uploads, pushes files to an S3-compatible bucket, deletes local copies, and rewrites URLs to serve media from the CDN.",
|
||||||
"changelog": "<h3>1.1.4 - 2026-06-02</h3><ul><li><strong>Added:</strong> Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)</li><li><strong>Fixed:</strong> WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version</li><li><strong>Changed:</strong> Tested up to WordPress 7.1, PHP 8.2-8.5</li></ul><h3>1.1.3 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Critical namespace issue with Robotstxt_Updater class causing fatal error</li><li><strong>Fixed:</strong> Plugin now loads correctly without PHP fatal errors</li></ul><h3>1.1.2 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Deployment script updated to use PHP 8.2 as platform base for production builds</li><li><strong>Changed:</strong> Now uses composer update --no-dev for consistent dependency resolution</li><li><strong>Improved:</strong> Production packages guarantee PHP 8.2+ compatibility regardless of dev environment</li></ul><h3>1.1.1 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)</li><li><strong>Improved:</strong> Production packages now contain all files required for automatic updates from Gitea</li></ul><h3>1.1.0 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Added explicit PHP version requirement (>=8.2) to composer.json</li><li><strong>Changed:</strong> Updated update.json with correct plugin information</li><li><strong>Changed:</strong> Fixed Text Domain in robotstxt-updater.php to match plugin slug</li><li><strong>Fixed:</strong> Composer now validates PHP version during dependency installation</li><li><strong>Fixed:</strong> Plugin update system correctly identifies the plugin</li><li><strong>Fixed:</strong> Translations properly loaded for updater error messages</li><li><strong>Improved:</strong> All text domains now consistently use 'idrivee2-media-upload'</li></ul><h3>1.0.0 - 2026-02-03</h3><ul><li><strong>Release:</strong> First stable release</li><li><strong>Feature:</strong> Automatic upload of media files to iDrivee2 (S3-compatible storage)</li><li><strong>Feature:</strong> URL rewriting to serve media from CDN</li><li><strong>Feature:</strong> Local file deletion after successful upload</li><li><strong>Feature:</strong> Admin interface with connection and upload testing</li><li><strong>Security:</strong> Enterprise-grade security with nonce validation</li><li><strong>Architecture:</strong> Class-based modular architecture with dependency injection</li><li><strong>Testing:</strong> PHPUnit test structure and PHPStan static analysis</li><li><strong>Compatibility:</strong> WordPress 6.8+ and PHP 8.2+</li></ul>"
|
"changelog": "<h3>1.2.0 - 2026-06-02</h3><ul><li><strong>Performance:</strong> Concurrent S3 uploads via AWS CommandPool (default 5, tunable via IDRIVEE2_UPLOAD_CONCURRENCY)</li><li><strong>Performance:</strong> Stream files directly from disk — no full load into memory</li><li><strong>Performance:</strong> Removed per-file headObject pre-check — single batch DB write for stats</li><li><strong>Changed:</strong> Hook priority lowered from 999 to 10</li></ul><h3>1.1.4 - 2026-06-02</h3><ul><li><strong>Added:</strong> Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)</li><li><strong>Fixed:</strong> WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version</li><li><strong>Changed:</strong> Tested up to WordPress 7.1, PHP 8.1-8.5, Requires at least 4.1</li></ul><h3>1.1.3 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Critical namespace issue with Robotstxt_Updater class causing fatal error</li><li><strong>Fixed:</strong> Plugin now loads correctly without PHP fatal errors</li></ul><h3>1.1.2 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Deployment script updated to use PHP 8.2 as platform base for production builds</li><li><strong>Changed:</strong> Now uses composer update --no-dev for consistent dependency resolution</li><li><strong>Improved:</strong> Production packages guarantee PHP 8.2+ compatibility regardless of dev environment</li></ul><h3>1.1.1 - 2026-02-04</h3><ul><li><strong>Fixed:</strong> Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)</li><li><strong>Improved:</strong> Production packages now contain all files required for automatic updates from Gitea</li></ul><h3>1.1.0 - 2026-02-04</h3><ul><li><strong>Changed:</strong> Added explicit PHP version requirement (>=8.2) to composer.json</li><li><strong>Changed:</strong> Updated update.json with correct plugin information</li><li><strong>Changed:</strong> Fixed Text Domain in robotstxt-updater.php to match plugin slug</li><li><strong>Fixed:</strong> Composer now validates PHP version during dependency installation</li><li><strong>Fixed:</strong> Plugin update system correctly identifies the plugin</li><li><strong>Fixed:</strong> Translations properly loaded for updater error messages</li><li><strong>Improved:</strong> All text domains now consistently use 'idrivee2-media-upload'</li></ul><h3>1.0.0 - 2026-02-03</h3><ul><li><strong>Release:</strong> First stable release</li><li><strong>Feature:</strong> Automatic upload of media files to iDrivee2 (S3-compatible storage)</li><li><strong>Feature:</strong> URL rewriting to serve media from CDN</li><li><strong>Feature:</strong> Local file deletion after successful upload</li><li><strong>Feature:</strong> Admin interface with connection and upload testing</li><li><strong>Security:</strong> Enterprise-grade security with nonce validation</li><li><strong>Architecture:</strong> Class-based modular architecture with dependency injection</li><li><strong>Testing:</strong> PHPUnit test structure and PHPStan static analysis</li><li><strong>Compatibility:</strong> WordPress 6.8+ and PHP 8.2+</li></ul>"
|
||||||
},
|
},
|
||||||
"banners": {
|
"banners": {
|
||||||
"low": "",
|
"low": "",
|
||||||
|
|
|
||||||
4
vendor/composer/installed.php
vendored
4
vendor/composer/installed.php
vendored
|
|
@ -3,7 +3,7 @@
|
||||||
'name' => 'robotstxt/idrivee2-media-upload',
|
'name' => 'robotstxt/idrivee2-media-upload',
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '1772e18b481a48415cd6982f7beceb62945db944',
|
'reference' => '51ad14453c553b99ed1481fcaf638827864adc1d',
|
||||||
'type' => 'wordpress-plugin',
|
'type' => 'wordpress-plugin',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
'robotstxt/idrivee2-media-upload' => array(
|
'robotstxt/idrivee2-media-upload' => array(
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '1772e18b481a48415cd6982f7beceb62945db944',
|
'reference' => '51ad14453c553b99ed1481fcaf638827864adc1d',
|
||||||
'type' => 'wordpress-plugin',
|
'type' => 'wordpress-plugin',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue