From 31be5438e75485749a78c0eaebc36131027a84b9 Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Tue, 2 Jun 2026 19:06:05 +0000 Subject: [PATCH] v1.2.0 --- changelog.txt | 28 +++ idrivee2-media-upload.php | 4 +- includes/class-logger.php | 67 +++++- includes/class-media-uploader.php | 367 ++++++++++++++---------------- readme.txt | 36 ++- update.json | 8 +- vendor/composer/installed.php | 4 +- 7 files changed, 306 insertions(+), 208 deletions(-) diff --git a/changelog.txt b/changelog.txt index 82d30a2..af31936 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,33 @@ == 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 = _Release date: 2026-06-02_ diff --git a/idrivee2-media-upload.php b/idrivee2-media-upload.php index adde65a..74cf5c9 100644 --- a/idrivee2-media-upload.php +++ b/idrivee2-media-upload.php @@ -5,7 +5,7 @@ * Gitea Plugin URI: https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload * Primary Branch: main * 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 PHP: 8.1 * Author: ROBOTSTXT @@ -36,7 +36,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @since 1.1.4 */ -define( 'IDRIVEE2_MEDIA_VERSION', '1.1.4' ); +define( 'IDRIVEE2_MEDIA_VERSION', '1.2.0' ); /** * Load Composer autoloader if available. diff --git a/includes/class-logger.php b/includes/class-logger.php index ee18053..e234a4a 100644 --- a/includes/class-logger.php +++ b/includes/class-logger.php @@ -198,6 +198,69 @@ class Logger { $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> $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. * @@ -280,7 +343,7 @@ class Logger { */ private function track_s3_operation( string $operation ): void { $option_key = 'idrivee2_s3_operations'; - $raw = get_option( $option_key, array() ); + $raw = get_option( $option_key, array() ); /** * Daily S3 operation counts, keyed by date and operation name. * @@ -321,7 +384,7 @@ class Logger { */ public function get_s3_stats( int $days = 7 ): array { $days = min( $days, 30 ); - $raw = get_option( 'idrivee2_s3_operations', array() ); + $raw = get_option( 'idrivee2_s3_operations', array() ); /** * Daily S3 operation counts, keyed by date and operation name. * diff --git a/includes/class-media-uploader.php b/includes/class-media-uploader.php index 6c24008..b7f653d 100644 --- a/includes/class-media-uploader.php +++ b/includes/class-media-uploader.php @@ -19,7 +19,8 @@ if ( ! defined( 'ABSPATH' ) ) { /** * 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. * * @since 0.3.0 @@ -69,232 +70,226 @@ class Media_Uploader { * @return void */ public function register(): void { - // Use wp_update_attachment_metadata with high priority to ensure thumbnails are generated. - // 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_filter( 'wp_update_attachment_metadata', array( $this, 'upload_attachment_to_idrivee2' ), 10, 2 ); 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' ) ); - // Schedule cron if not already scheduled. if ( ! wp_next_scheduled( '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 - * configured S3-compatible host, captures the returned ObjectURL for the - * original file, deletes the local copies, updates the attachment's GUID, - * and filters the front-end URL to use the S3 ObjectURL. + * Uses the AWS CommandPool to upload the original file and all generated + * thumbnail sizes in parallel, streaming each file directly from disk. + * No headObject pre-check is performed — files are assumed to be new. + * + * Concurrency defaults to 5 and can be overridden via the + * IDRIVEE2_UPLOAD_CONCURRENCY constant in wp-config.php. * * @since 0.3.0 * - * @param array $meta Attachment metadata, including 'file' and 'sizes'. - * @param int $attachment_id Attachment post ID. - * @return array Unchanged metadata array. + * @param array $meta Attachment metadata, including 'file' and 'sizes'. + * @param int $attachment_id Attachment post ID. + * @return array Unchanged metadata array. */ public function upload_attachment_to_idrivee2( array $meta, int $attachment_id ): array { - // Bail if configuration is incomplete. if ( ! $this->config->is_configured() ) { 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'] : ''; if ( '' === $meta_file ) { return $meta; } + // Build list of files: original + each thumbnail. $upload_dir = wp_upload_dir(); $basedir = $upload_dir['basedir']; $base_path = path_join( $basedir, $meta_file ); - $files = array( - 'original' => $base_path, - ); - $meta_sizes = isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ? $meta['sizes'] : array(); + + /** @var array $files */ + $files = array( 'original' => $base_path ); foreach ( $meta_sizes as $size ) { if ( is_array( $size ) && isset( $size['file'] ) && is_string( $size['file'] ) ) { $files[ $size['file'] ] = path_join( dirname( $base_path ), $size['file'] ); } } - // Log file list for debugging. - $this->logger->info( - sprintf( 'Preparing to upload %d files to S3', count( $files ) ), - array( - 'attachment_id' => $attachment_id, - 'original' => basename( $meta_file ), - 'sizes_count' => count( $meta_sizes ), - ) - ); + // Open a stream for each file and build the AWS command list. + $client = $this->client_factory->create(); + $bucket = $this->config->get_bucket(); + $commands = array(); + $key_map = array(); // int index -> file metadata + $handles = array(); // int index -> resource - $object_url = ''; - $s3_base_url = ''; - $upload_count = 0; + $real_basedir = realpath( $basedir ); + $idx = 0; - // Load and initialise WP_Filesystem. - if ( ! function_exists( 'WP_Filesystem' ) ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; + foreach ( $files as $file_key => $local_path ) { + if ( ! file_exists( $local_path ) || ! is_readable( $local_path ) ) { + continue; + } + + // Security: refuse to read files outside the uploads directory. + $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 + : dirname( $meta_file ) . '/' . $file_key; + + $fh = fopen( $real_path, 'rb' ); + if ( false === $fh ) { + $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( + 'Bucket' => $bucket, + 'Key' => $object_key, + 'Body' => $fh, + 'ACL' => 'public-read', + ) + ); + ++$idx; } - WP_Filesystem(); - global $wp_filesystem; - if ( ! ( $wp_filesystem instanceof \WP_Filesystem_Base ) ) { - $this->logger->error( 'WP_Filesystem not available, aborting S3 upload' ); + if ( empty( $commands ) ) { 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; - } + // Run all uploads concurrently. + $concurrency = defined( 'IDRIVEE2_UPLOAD_CONCURRENCY' ) + ? max( 1, (int) IDRIVEE2_UPLOAD_CONCURRENCY ) + : 5; + $uploaded_paths = array(); + $failed_count = 0; + $object_url = ''; + $s3_base_url = ''; + $has_domain = $this->config->has_domain(); + $cdn_domain = $this->config->get_domain(); + $logger = $this->logger; - // Determine S3 object key. - $object_key = ( 'original' === $key ) - ? $meta_file - : dirname( $meta_file ) . '/' . $key; - - // Check if file already exists in S3. - try { - $exists = $client->headObject( - array( - 'Bucket' => $this->config->get_bucket(), - 'Key' => $object_key, - ) - ); - // 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', - ) - ); - - // Log successful upload. - $this->logger->s3_operation( 'putObject', true, basename( $object_key ) ); - - // Capture the base URL for constructing CDN URLs. - if ( 'original' === $key ) { - // Build CDN URL if domain configured, otherwise use S3 URL. - if ( $this->config->has_domain() ) { - $s3_base_url = trailingslashit( $this->config->get_domain() ) . dirname( $meta_file ); - } elseif ( isset( $result['ObjectURL'] ) && is_string( $result['ObjectURL'] ) && '' !== $result['ObjectURL'] ) { - $object_url = $result['ObjectURL']; - $s3_base_url = dirname( $result['ObjectURL'] ); - } - } - - ++$upload_count; - - } catch ( \Aws\Exception\AwsException $e ) { - // Log failed upload. - $this->logger->s3_operation( 'putObject', false, basename( $object_key ), $e->getAwsErrorMessage() ?? '' ); - // Continue to next file on error. - continue; - } - } - - // Log upload summary. - $this->logger->info( - sprintf( 'Upload complete: %d of %d files uploaded to S3', $upload_count, count( $files ) ), + $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']; + $s3_base_url = dirname( $result['ObjectURL'] ); + } + } + }, + 'rejected' => function ( + mixed $reason, + int|string $cmd_idx + ) use ( + &$failed_count, + $key_map, + $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 ) + ); + }, + ) + ); + + 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( + sprintf( 'Batch upload complete: %d/%d files uploaded to S3', $upload_count, $total ), + array( 'attachment_id' => $attachment_id, - 'uploaded' => $upload_count, - 'expected' => count( $files ), + 'concurrency' => $concurrency, + 'failed' => $failed_count, ) ); - // Update metadata and schedule deletion if files were uploaded. if ( $upload_count > 0 ) { update_post_meta( $attachment_id, '_idrivee2_s3_base_url', $s3_base_url ); update_post_meta( $attachment_id, '_idrivee2_last_upload', time() ); - - // Schedule local files for deletion after 3 minutes. - $this->schedule_files_for_deletion( array_values( $files ) ); + $this->schedule_files_for_deletion( $uploaded_paths ); } - // Preserve relative path in database. 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 ) { - $file_name = basename( $meta_file ); - if ( $this->config->has_domain() ) { - // Use CDN domain. - $public_url = trailingslashit( $this->config->get_domain() ) . $meta_file; + if ( $has_domain ) { + $public_url = trailingslashit( $cdn_domain ) . $meta_file; } elseif ( $object_url ) { - // Use S3 ObjectURL. $public_url = $object_url; } else { - // Fallback: construct from base URL. - $public_url = $s3_base_url . '/' . $file_name; + $public_url = $s3_base_url . '/' . basename( $meta_file ); } - // Update the GUID to the public URL. wp_update_post( array( 'ID' => $attachment_id, @@ -322,24 +317,22 @@ class Media_Uploader { } /** - * Schedule files for deletion. - * - * 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. + * Schedule files for deletion via the cron queue. * * @since 1.0.1 * - * @param array $files Array of file paths to delete. + * @param array $files Array of local file paths to delete. * @return void */ private function schedule_files_for_deletion( array $files ): void { $raw_q = get_option( 'idrivee2_deletion_queue', array() ); $queue = is_array( $raw_q ) ? $raw_q : array(); + $now = time(); foreach ( $files as $file_path ) { $queue[] = array( '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: - * - Were uploaded to S3 successfully - * - Have been in the queue for at least 3 minutes - * - * The 3-minute delay ensures WordPress has time to display thumbnails - * in the admin before files are removed. + * Runs via WP-Cron every 5 minutes. Deletes files that have been in the + * queue for at least 3 minutes, giving WordPress time to serve thumbnails + * from the local copy before removal. * * @since 1.0.1 * @@ -367,7 +357,6 @@ class Media_Uploader { return; } - // Load and initialise WP_Filesystem. if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } @@ -395,46 +384,32 @@ class Media_Uploader { continue; } - // Requeue items with a malformed timestamp rather than deleting immediately. if ( 0 === $timestamp ) { $new_queue[] = $item; continue; } - // Only delete files older than 3 minutes. if ( ( $current_time - $timestamp ) < 180 ) { $new_queue[] = $item; continue; } - // Delete the file if it exists. if ( $wp_filesystem->exists( $file_path ) ) { $deleted_ok = $wp_filesystem->delete( $file_path ); if ( $deleted_ok ) { ++$deleted; - $this->logger->info( - 'Local file deleted after S3 upload', - array( 'path' => basename( $file_path ) ) - ); } else { - // Keep in queue to retry later. $new_queue[] = $item; - $this->logger->warning( - 'Failed to delete local file, will retry', - array( 'path' => $file_path ) - ); + $this->logger->warning( '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 ); - // Log cleanup summary if any files were deleted. if ( $deleted > 0 ) { $this->logger->info( - sprintf( 'Cleanup completed: %d files deleted', $deleted ), + sprintf( 'Cleanup: %d local files deleted', $deleted ), array( 'remaining' => count( $new_queue ) ) ); } diff --git a/readme.txt b/readme.txt index 79f4159..b1ec8b4 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: robotstxt, javiercasares Tags: media, upload, s3, cdn, storage, idrivee2, cloud Requires at least: 4.1 Tested up to: 7.1 -Stable tag: 1.1.4 +Stable tag: 1.2.0 Requires PHP: 8.1 -Version: 1.1.4 +Version: 1.2.0 License: GPL-3.0-or-later 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');` 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:** 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 == += 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 = _Release date: 2026-06-02_ diff --git a/update.json b/update.json index ea1c942..607df8c 100644 --- a/update.json +++ b/update.json @@ -1,8 +1,8 @@ { "name": "iDrivee2 Media Upload", "slug": "idrivee2-media-upload", - "version": "1.1.4", - "download_url": "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload/releases/download/1.1.4/idrivee2-media-upload-1.1.4.zip", + "version": "1.2.0", + "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_php": "8.1", "tested": "7.1", @@ -11,10 +11,10 @@ "author_profile": "https://www.robotstxt.es/", "homepage": "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload", "description": "Uploads media files to iDrivee2 (S3-compatible) with enterprise-grade security and logging.", - "changelog": "

1.1.4 - 2026-06-02

  • Added: Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)
  • Fixed: WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version
  • Changed: Tested up to WordPress 7.1, PHP 8.2-8.5

1.1.3 - 2026-02-04

  • Fixed: Critical namespace issue with Robotstxt_Updater class causing fatal error
  • Fixed: Plugin now loads correctly without PHP fatal errors

1.1.2 - 2026-02-04

  • Changed: Deployment script updated to use PHP 8.2 as platform base for production builds
  • Changed: Now uses composer update --no-dev for consistent dependency resolution
  • Improved: Production packages guarantee PHP 8.2+ compatibility regardless of dev environment

1.1.1 - 2026-02-04

  • Fixed: Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)
  • Improved: Production packages now contain all files required for automatic updates from Gitea

1.1.0 - 2026-02-04

  • Changed: Added explicit PHP version requirement (>=8.2) to composer.json
  • Changed: Updated update.json with correct plugin information
  • Changed: Fixed Text Domain in robotstxt-updater.php to match plugin slug
  • Fixed: Composer now validates PHP version during dependency installation
  • Fixed: Plugin update system correctly identifies the plugin
  • Fixed: Translations properly loaded for updater error messages
  • Improved: All text domains now consistently use 'idrivee2-media-upload'

1.0.0 - 2026-02-03

  • Release: First stable release
  • Feature: Automatic upload of media files to iDrivee2 (S3-compatible storage)
  • Feature: URL rewriting to serve media from CDN
  • Feature: Local file deletion after successful upload
  • Feature: Admin interface with connection and upload testing
  • Security: Enterprise-grade security with nonce validation
  • Architecture: Class-based modular architecture with dependency injection
  • Testing: PHPUnit test structure and PHPStan static analysis
  • Compatibility: WordPress 6.8+ and PHP 8.2+
", + "changelog": "

1.2.0 - 2026-06-02

  • Performance: Concurrent S3 uploads via AWS CommandPool (default 5, tunable via IDRIVEE2_UPLOAD_CONCURRENCY)
  • Performance: Stream files directly from disk — no full load into memory
  • Performance: Removed per-file headObject pre-check — single batch DB write for stats
  • Changed: Hook priority lowered from 999 to 10

1.1.4 - 2026-06-02

  • Added: Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)
  • Fixed: WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version
  • Changed: Tested up to WordPress 7.1, PHP 8.1-8.5, Requires at least 4.1

1.1.3 - 2026-02-04

  • Fixed: Critical namespace issue with Robotstxt_Updater class causing fatal error
  • Fixed: Plugin now loads correctly without PHP fatal errors

1.1.2 - 2026-02-04

  • Changed: Deployment script updated to use PHP 8.2 as platform base for production builds
  • Changed: Now uses composer update --no-dev for consistent dependency resolution
  • Improved: Production packages guarantee PHP 8.2+ compatibility regardless of dev environment

1.1.1 - 2026-02-04

  • Fixed: Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)
  • Improved: Production packages now contain all files required for automatic updates from Gitea

1.1.0 - 2026-02-04

  • Changed: Added explicit PHP version requirement (>=8.2) to composer.json
  • Changed: Updated update.json with correct plugin information
  • Changed: Fixed Text Domain in robotstxt-updater.php to match plugin slug
  • Fixed: Composer now validates PHP version during dependency installation
  • Fixed: Plugin update system correctly identifies the plugin
  • Fixed: Translations properly loaded for updater error messages
  • Improved: All text domains now consistently use 'idrivee2-media-upload'

1.0.0 - 2026-02-03

  • Release: First stable release
  • Feature: Automatic upload of media files to iDrivee2 (S3-compatible storage)
  • Feature: URL rewriting to serve media from CDN
  • Feature: Local file deletion after successful upload
  • Feature: Admin interface with connection and upload testing
  • Security: Enterprise-grade security with nonce validation
  • Architecture: Class-based modular architecture with dependency injection
  • Testing: PHPUnit test structure and PHPStan static analysis
  • Compatibility: WordPress 6.8+ and PHP 8.2+
", "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.", - "changelog": "

1.1.4 - 2026-06-02

  • Added: Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)
  • Fixed: WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version
  • Changed: Tested up to WordPress 7.1, PHP 8.2-8.5

1.1.3 - 2026-02-04

  • Fixed: Critical namespace issue with Robotstxt_Updater class causing fatal error
  • Fixed: Plugin now loads correctly without PHP fatal errors

1.1.2 - 2026-02-04

  • Changed: Deployment script updated to use PHP 8.2 as platform base for production builds
  • Changed: Now uses composer update --no-dev for consistent dependency resolution
  • Improved: Production packages guarantee PHP 8.2+ compatibility regardless of dev environment

1.1.1 - 2026-02-04

  • Fixed: Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)
  • Improved: Production packages now contain all files required for automatic updates from Gitea

1.1.0 - 2026-02-04

  • Changed: Added explicit PHP version requirement (>=8.2) to composer.json
  • Changed: Updated update.json with correct plugin information
  • Changed: Fixed Text Domain in robotstxt-updater.php to match plugin slug
  • Fixed: Composer now validates PHP version during dependency installation
  • Fixed: Plugin update system correctly identifies the plugin
  • Fixed: Translations properly loaded for updater error messages
  • Improved: All text domains now consistently use 'idrivee2-media-upload'

1.0.0 - 2026-02-03

  • Release: First stable release
  • Feature: Automatic upload of media files to iDrivee2 (S3-compatible storage)
  • Feature: URL rewriting to serve media from CDN
  • Feature: Local file deletion after successful upload
  • Feature: Admin interface with connection and upload testing
  • Security: Enterprise-grade security with nonce validation
  • Architecture: Class-based modular architecture with dependency injection
  • Testing: PHPUnit test structure and PHPStan static analysis
  • Compatibility: WordPress 6.8+ and PHP 8.2+
" + "changelog": "

1.2.0 - 2026-06-02

  • Performance: Concurrent S3 uploads via AWS CommandPool (default 5, tunable via IDRIVEE2_UPLOAD_CONCURRENCY)
  • Performance: Stream files directly from disk — no full load into memory
  • Performance: Removed per-file headObject pre-check — single batch DB write for stats
  • Changed: Hook priority lowered from 999 to 10

1.1.4 - 2026-06-02

  • Added: Full dev tooling: PHPCS, PHPStan level 9, PHPUnit test suite (22 tests)
  • Fixed: WP_Filesystem null guard, type safety on get_option/get_transient, dynamic asset version
  • Changed: Tested up to WordPress 7.1, PHP 8.1-8.5, Requires at least 4.1

1.1.3 - 2026-02-04

  • Fixed: Critical namespace issue with Robotstxt_Updater class causing fatal error
  • Fixed: Plugin now loads correctly without PHP fatal errors

1.1.2 - 2026-02-04

  • Changed: Deployment script updated to use PHP 8.2 as platform base for production builds
  • Changed: Now uses composer update --no-dev for consistent dependency resolution
  • Improved: Production packages guarantee PHP 8.2+ compatibility regardless of dev environment

1.1.1 - 2026-02-04

  • Fixed: Deployment script now includes essential files (update.json, robotstxt-updater.php, readme.txt, changelog.txt)
  • Improved: Production packages now contain all files required for automatic updates from Gitea

1.1.0 - 2026-02-04

  • Changed: Added explicit PHP version requirement (>=8.2) to composer.json
  • Changed: Updated update.json with correct plugin information
  • Changed: Fixed Text Domain in robotstxt-updater.php to match plugin slug
  • Fixed: Composer now validates PHP version during dependency installation
  • Fixed: Plugin update system correctly identifies the plugin
  • Fixed: Translations properly loaded for updater error messages
  • Improved: All text domains now consistently use 'idrivee2-media-upload'

1.0.0 - 2026-02-03

  • Release: First stable release
  • Feature: Automatic upload of media files to iDrivee2 (S3-compatible storage)
  • Feature: URL rewriting to serve media from CDN
  • Feature: Local file deletion after successful upload
  • Feature: Admin interface with connection and upload testing
  • Security: Enterprise-grade security with nonce validation
  • Architecture: Class-based modular architecture with dependency injection
  • Testing: PHPUnit test structure and PHPStan static analysis
  • Compatibility: WordPress 6.8+ and PHP 8.2+
" }, "banners": { "low": "", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index cc06433..ba21af0 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'robotstxt/idrivee2-media-upload', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '1772e18b481a48415cd6982f7beceb62945db944', + 'reference' => '51ad14453c553b99ed1481fcaf638827864adc1d', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -121,7 +121,7 @@ 'robotstxt/idrivee2-media-upload' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '1772e18b481a48415cd6982f7beceb62945db944', + 'reference' => '51ad14453c553b99ed1481fcaf638827864adc1d', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(),