v1.1.0
This commit is contained in:
parent
e6299a3e14
commit
47885eb7c3
6 changed files with 279 additions and 45 deletions
|
|
@ -1,9 +1,11 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: iDrivee2 Media Upload
|
||||
* Plugin URI: https://github.com/javiercasares/idrivee2-media-upload
|
||||
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload
|
||||
* 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.0.0
|
||||
* Version: 1.1.0
|
||||
* Requires at least: 6.8
|
||||
* Requires PHP: 8.2
|
||||
* Author: Javier Casares
|
||||
|
|
@ -59,3 +61,7 @@ require_once __DIR__ . '/includes/class-plugin.php';
|
|||
* @since 0.3.0
|
||||
*/
|
||||
Plugin::get_instance( __FILE__ )->init();
|
||||
|
||||
// Initialize ROBOTSTXT updater (auto-configures from plugin headers).
|
||||
require_once __DIR__ . '/robotstxt-updater.php';
|
||||
Robotstxt_Updater::init( __FILE__ );
|
||||
|
|
|
|||
|
|
@ -69,9 +69,18 @@ class Media_Uploader {
|
|||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
add_filter( 'wp_generate_attachment_metadata', array( $this, 'upload_attachment_to_idrivee2' ), 10, 2 );
|
||||
add_filter( 'wp_update_attachment_metadata', array( $this, 'upload_attachment_to_idrivee2' ), 10, 2 );
|
||||
// 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_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' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -110,7 +119,19 @@ class Media_Uploader {
|
|||
}
|
||||
}
|
||||
|
||||
// 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'] ?? array() ),
|
||||
)
|
||||
);
|
||||
|
||||
$object_url = '';
|
||||
$s3_base_url = '';
|
||||
$upload_count = 0;
|
||||
|
||||
// Load and initialise WP_Filesystem.
|
||||
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||
|
|
@ -123,7 +144,13 @@ class Media_Uploader {
|
|||
foreach ( $files as $key => $local_path ) {
|
||||
// Skip if file doesn't exist.
|
||||
if ( ! $wp_filesystem->exists( $local_path ) ) {
|
||||
$this->logger->warning( 'Media file not found', array( 'path' => $local_path ) );
|
||||
$this->logger->warning(
|
||||
'File does not exist, skipping upload',
|
||||
array(
|
||||
'key' => $key,
|
||||
'path' => $local_path,
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -132,14 +159,58 @@ class Media_Uploader {
|
|||
? $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 ) {
|
||||
// Skip this file if it can't be read.
|
||||
$this->logger->error( 'Failed to read media file', array( 'path' => $local_path ) );
|
||||
$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(
|
||||
|
|
@ -154,13 +225,18 @@ class Media_Uploader {
|
|||
// Log successful upload.
|
||||
$this->logger->s3_operation( 'putObject', true, basename( $object_key ) );
|
||||
|
||||
// Capture the ObjectURL for the original image.
|
||||
if ( 'original' === $key && ! empty( $result['ObjectURL'] ) ) {
|
||||
// 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 ( ! empty( $result['ObjectURL'] ) ) {
|
||||
$object_url = $result['ObjectURL'];
|
||||
$s3_base_url = dirname( $result['ObjectURL'] );
|
||||
}
|
||||
}
|
||||
|
||||
// Delete local file via WP_Filesystem.
|
||||
$wp_filesystem->delete( $local_path );
|
||||
$upload_count++;
|
||||
|
||||
} catch ( \Aws\Exception\AwsException $e ) {
|
||||
// Log failed upload.
|
||||
|
|
@ -170,26 +246,48 @@ class Media_Uploader {
|
|||
}
|
||||
}
|
||||
|
||||
// Preserve relative path in database.
|
||||
update_post_meta( $attachment_id, '_wp_attached_file', $meta['file'] );
|
||||
|
||||
if ( $object_url ) {
|
||||
// Update the GUID in wp_posts to the S3 URL.
|
||||
wp_update_post(
|
||||
// Log upload summary.
|
||||
$this->logger->info(
|
||||
sprintf( 'Upload complete: %d of %d files uploaded to S3', $upload_count, count( $files ) ),
|
||||
array(
|
||||
'ID' => $attachment_id,
|
||||
'guid' => $object_url,
|
||||
'attachment_id' => $attachment_id,
|
||||
'uploaded' => $upload_count,
|
||||
'expected' => count( $files ),
|
||||
)
|
||||
);
|
||||
|
||||
// Override front-end URL to use the S3 ObjectURL.
|
||||
add_filter(
|
||||
'wp_get_attachment_url',
|
||||
function ( string $url, int $id ) use ( $object_url, $attachment_id ): string {
|
||||
return ( $id === $attachment_id ) ? $object_url : $url;
|
||||
},
|
||||
10,
|
||||
2
|
||||
// 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 ) );
|
||||
}
|
||||
|
||||
// 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'];
|
||||
} elseif ( $object_url ) {
|
||||
// Use S3 ObjectURL.
|
||||
$public_url = $object_url;
|
||||
} else {
|
||||
// Fallback: construct from base URL.
|
||||
$public_url = $s3_base_url . '/' . $file_name;
|
||||
}
|
||||
|
||||
// Update the GUID to the public URL.
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $attachment_id,
|
||||
'guid' => $public_url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -210,4 +308,103 @@ class Media_Uploader {
|
|||
$this->upload_attachment_to_idrivee2( $meta, $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @since 1.0.1
|
||||
*
|
||||
* @param array<string> $files Array of file paths to delete.
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_files_for_deletion( array $files ): void {
|
||||
$queue = get_option( 'idrivee2_deletion_queue', array() );
|
||||
|
||||
foreach ( $files as $file_path ) {
|
||||
$queue[] = array(
|
||||
'path' => $file_path,
|
||||
'timestamp' => time(),
|
||||
);
|
||||
}
|
||||
|
||||
update_option( 'idrivee2_deletion_queue', $queue, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up local files that were uploaded to S3.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @since 1.0.1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cleanup_local_files(): void {
|
||||
$queue = get_option( 'idrivee2_deletion_queue', array() );
|
||||
|
||||
if ( empty( $queue ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load and initialise WP_Filesystem.
|
||||
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
WP_Filesystem();
|
||||
global $wp_filesystem;
|
||||
|
||||
$current_time = time();
|
||||
$new_queue = array();
|
||||
$deleted = 0;
|
||||
|
||||
foreach ( $queue as $item ) {
|
||||
$file_path = $item['path'];
|
||||
$timestamp = $item['timestamp'];
|
||||
|
||||
// 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 ) ) {
|
||||
$result = $wp_filesystem->delete( $file_path );
|
||||
if ( $result ) {
|
||||
$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 )
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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 ),
|
||||
array( 'remaining' => count( $new_queue ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,12 +138,31 @@ class Plugin {
|
|||
// Load text domain for translations.
|
||||
add_action( 'plugins_loaded', array( $this, 'load_textdomain' ), 20 );
|
||||
|
||||
// Add custom cron interval.
|
||||
add_filter( 'cron_schedules', array( $this, 'add_cron_intervals' ) );
|
||||
|
||||
// Register component hooks.
|
||||
$this->admin_page->register();
|
||||
$this->media_uploader->register();
|
||||
$this->url_rewriter->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom cron intervals.
|
||||
*
|
||||
* @since 1.0.1
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $schedules Existing schedules.
|
||||
* @return array<string, array<string, mixed>> Modified schedules.
|
||||
*/
|
||||
public function add_cron_intervals( array $schedules ): array {
|
||||
$schedules['every_five_minutes'] = array(
|
||||
'interval' => 300,
|
||||
'display' => __( 'Every 5 Minutes', 'idrivee2-media-upload' ),
|
||||
);
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the plugin text domain for translations.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ msgid "iDrivee2 Media Upload"
|
|||
msgstr ""
|
||||
|
||||
#. Plugin URI of the plugin/theme
|
||||
msgid "https://github.com/javiercasares/idrivee2-media-upload"
|
||||
msgid "https://git.robotstxt.es/ROBOTSTXT/idrivee2-media-upload"
|
||||
msgstr ""
|
||||
|
||||
#. Description of the plugin/theme
|
||||
|
|
|
|||
|
|
@ -20,35 +20,47 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
|||
/**
|
||||
* Clean up plugin data.
|
||||
*
|
||||
* Note: This plugin does not create any custom database tables or options.
|
||||
* All data is stored in standard WordPress post meta and attachment metadata.
|
||||
* This removes all custom post meta, options, and scheduled cron events
|
||||
* created by the plugin.
|
||||
*
|
||||
* If you want to delete all attachment metadata created by this plugin,
|
||||
* uncomment the code below. WARNING: This cannot be undone.
|
||||
* WARNING: This action cannot be undone.
|
||||
*/
|
||||
|
||||
// phpcs:disable Squiz.PHP.CommentedOutCode.Found
|
||||
|
||||
/*
|
||||
// Delete all _wp_attached_file meta that was preserved by this plugin.
|
||||
// This is optional as WordPress manages this meta normally.
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
|
||||
// Delete all _idrivee2_last_upload post meta.
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->postmeta}
|
||||
WHERE meta_key = '_wp_attached_file'
|
||||
AND post_id IN (
|
||||
SELECT ID FROM {$wpdb->posts}
|
||||
WHERE post_type = 'attachment'
|
||||
)"
|
||||
WHERE meta_key = '_idrivee2_last_upload'"
|
||||
);
|
||||
|
||||
// Delete all _idrivee2_s3_base_url post meta.
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->postmeta}
|
||||
WHERE meta_key = '_idrivee2_s3_base_url'"
|
||||
);
|
||||
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
*/
|
||||
|
||||
// Delete the deletion queue option.
|
||||
delete_option( 'idrivee2_deletion_queue' );
|
||||
|
||||
// Unschedule the cleanup cron event.
|
||||
$timestamp = wp_next_scheduled( 'idrivee2_cleanup_local_files' );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, 'idrivee2_cleanup_local_files' );
|
||||
}
|
||||
|
||||
// Clear all hooks for this action to prevent any remaining schedules.
|
||||
wp_clear_scheduled_hook( 'idrivee2_cleanup_local_files' );
|
||||
|
||||
/**
|
||||
* Note: Files uploaded to S3 are NOT deleted by this uninstall script.
|
||||
* If you want to delete files from S3, you must do so manually using
|
||||
* your S3 management console or AWS CLI.
|
||||
*
|
||||
* Local files in wp-content/uploads/ are also NOT deleted, as they are
|
||||
* part of WordPress's standard media library structure.
|
||||
*/
|
||||
|
|
|
|||
4
vendor/composer/installed.php
vendored
4
vendor/composer/installed.php
vendored
|
|
@ -3,7 +3,7 @@
|
|||
'name' => 'javiercasares/idrivee2-media-upload',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '758c667aed0ce75fdafa62357cf9c5d8b6d1beae',
|
||||
'reference' => 'ce690d2ae28f4364878953c73e3aa3c88642d599',
|
||||
'type' => 'wordpress-plugin',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
'javiercasares/idrivee2-media-upload' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '758c667aed0ce75fdafa62357cf9c5d8b6d1beae',
|
||||
'reference' => 'ce690d2ae28f4364878953c73e3aa3c88642d599',
|
||||
'type' => 'wordpress-plugin',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue