Loading README.md +2 −2 Original line number Diff line number Diff line Loading @@ -88,11 +88,11 @@ define( 'S3_UPLOADS_CACHE_CONTROL', 30 * 24 * 60 * 60 ); // will expire in 30 days time ``` You can also configure the `Expires` header using the `S3_UPLOADS_EXPIRES` constant You can also configure the `Expires` header using the `S3_UPLOADS_HTTP_EXPIRES` constant For instance if you wanted to set an asset to effectively not expire, you could set the Expires header way off in the future. For example: ```PHP define( 'S3_UPLOADS_EXPIRES', gmdate( 'D, d M Y H:i:s', time() + (10 * 365 * 24 * 60 * 60) ) .' GMT' ); define( 'S3_UPLOADS_HTTP_EXPIRES', gmdate( 'D, d M Y H:i:s', time() + (10 * 365 * 24 * 60 * 60) ) .' GMT' ); // will expire in 10 years time ``` inc/class-s3-uploads-stream-wrapper.php +119 −7 Original line number Diff line number Diff line Loading @@ -7,12 +7,7 @@ class S3_Uploads_Stream_Wrapper extends Aws\S3\StreamWrapper { * * @param S3Client $client Client to use with the stream wrapper */ public static function register( Aws\S3\S3Client $client) { if (in_array('s3', stream_get_wrappers())) { stream_wrapper_unregister('s3'); } public static function register( Aws\S3\S3Client $client) { stream_wrapper_register( 's3', __CLASS__, STREAM_IS_URL ); static::$client = $client; } Loading Loading @@ -47,6 +42,123 @@ class S3_Uploads_Stream_Wrapper extends Aws\S3\StreamWrapper { } /** * @param string $path * @param string $mode * @param int $options * @param string $opened_path * * @return bool */ public function stream_open( $path, $mode, $options, &$opened_path ) { $result = parent::stream_open( $path, $mode, $options, $opened_path ); if ( ! $result ) { return $result; } if ( $mode === 'r' || $mode === 'a' ) { return $result; } /** * As we open a temp stream, we don't actually know if we have writing ability yet. * This means functions like copy() will not fail correctly, as the write to s3 * is only attemped on stream_flush() which is too late to report to copy() * et al that the write has failed. * * As a work around, we attempt to write an empty object. */ try { $p = $this->params; $p['Body'] = ''; static::$client->putObject($p); } catch (\Exception $e) { return $this->triggerError($e->getMessage()); } return $result; } /** * Provides information for is_dir, is_file, filesize, etc. Works on buckets, keys, and prefixes * * This is overrided to handle some optimizations with directories, else wp_upload_dir() causes * a stat() on every page load (atleast once). * * @param string $path * @param int $flags * * @return array Returns an array of stat data * @link http://www.php.net/manual/en/streamwrapper.url-stat.php */ public function url_stat( $path, $flags ) { $extension = pathinfo($path, PATHINFO_EXTENSION); /** * If the file is actually just a path to a directory * then return it as always existing. This is to work * around wp_upload_dir doing file_exists checks on * the uploads directory on every page load */ if ( ! $extension ) { return array ( 0 => 0, 'dev' => 0, 1 => 0, 'ino' => 0, 2 => 16895, 'mode' => 16895, 3 => 0, 'nlink' => 0, 4 => 0, 'uid' => 0, 5 => 0, 'gid' => 0, 6 => -1, 'rdev' => -1, 7 => 0, 'size' => 0, 8 => 0, 'atime' => 0, 9 => 0, 'mtime' => 0, 10 => 0, 'ctime' => 0, 11 => -1, 'blksize' => -1, 12 => -1, 'blocks' => -1, ); } // Check if this path is in the url_stat cache if ( isset ( self::$nextStat[ $path ] ) ) { return self::$nextStat[ $path ]; } $parts = $this->getParams( $path ); // Stat a bucket or just s3:// if ( ! $parts['Key'] && ( ! $parts['Bucket'] || self::$client->doesBucketExist( $parts['Bucket'] ) ) ) { return $this->formatUrlStat( $path ); } // You must pass either a bucket or a bucket + key if ( ! $parts['Key'] ) { return $this->triggerError( "File or directory not found: {$path}", $flags ); } try { // Attempt to stat and cache regular object return $this->formatUrlStat( self::$client->headObject( $parts )->toArray() ); } catch ( Exception $e ) { return $this->triggerError( $e->getMessage(), $flags ); } } public function stream_metadata( $path, $option, $value ) { // not implemented } Loading inc/class-s3-uploads.php +7 −2 Original line number Diff line number Diff line Loading @@ -30,14 +30,19 @@ class S3_Uploads { $this->secret = $secret; $this->bucket_url = $bucket_url; $this->region = $region; } /** * Register the stream wrapper for s3 */ public function register_stream_wrapper() { if ( defined( 'S3_UPLOADS_USE_LOCAL' ) && S3_UPLOADS_USE_LOCAL ) { require_once dirname( __FILE__ ) . '/class-s3-uploads-local-stream-wrapper.php'; stream_wrapper_register( 's3', 'S3_Uploads_Local_Stream_Wrapper', STREAM_IS_URL ); } else { $s3 = $this->s3(); S3_Uploads_Stream_Wrapper::register( $s3 ); stream_context_set_option( stream_context_get_default(), 's3', 'ACL', Aws\S3\Enum\CannedAcl::PUBLIC_READ ); stream_context_set_option( stream_context_get_default(), 's3', 'ACL', 'public-read' ); } stream_context_set_option( stream_context_get_default(), 's3', 'seekable', true ); Loading lib/aws-sdk/Aws/S3/StreamWrapper.php +18 −84 Original line number Diff line number Diff line Loading @@ -182,24 +182,6 @@ class StreamWrapper } elseif ($mode == 'a') { $this->openAppendStream($params, $errors); } else { /** * Modification by Joe Hoyle * * As we open a temp stream, we don't actually know if we have writing ability yet. * This means functions like copy() will not fail correctly, as the write to s3 * is only attemped on stream_flush() which is too late to report to copy() * et al that the write has failed. * * As a work around, we attempt to write an empty object. */ try { $p = $params; $p['Body'] = ''; static::$client->putObject($p); } catch (\Exception $e) { return $this->triggerError($e->getMessage()); } $this->openWriteStream($params, $errors); } } Loading Loading @@ -333,90 +315,42 @@ class StreamWrapper */ public function url_stat($path, $flags) { $extension = pathinfo($path, PATHINFO_EXTENSION); /** * If the file is actually just a path to a directory * then return it as always existing. This is to work * around wp_upload_dir doing file_exists checks on * the uploads directory on every page load */ if ( ! $extension ) { return array ( 0 => 0, 'dev' => 0, 1 => 0, 'ino' => 0, 2 => 16895, 'mode' => 16895, 3 => 0, 'nlink' => 0, 4 => 0, 'uid' => 0, 5 => 0, 'gid' => 0, 6 => -1, 'rdev' => -1, 7 => 0, 'size' => 0, 8 => 0, 'atime' => 0, 9 => 0, 'mtime' => 0, 10 => 0, 'ctime' => 0, 11 => -1, 'blksize' => -1, 12 => -1, 'blocks' => -1, ); } // Check if this path is in the url_stat cache if (isset(self::$nextStat[$path])) { return self::$nextStat[$path]; if (isset(static::$nextStat[$path])) { return static::$nextStat[$path]; } $parts = $this->getParams($path); // Stat a bucket or just s3:// if (!$parts['Key'] && (!$parts['Bucket'] || self::$client->doesBucketExist($parts['Bucket']))) { return $this->formatUrlStat($path); } // You must pass either a bucket or a bucket + key if (!$parts['Key']) { // Stat "directories": buckets, or "s3://" if (!$parts['Bucket'] || static::$client->doesBucketExist($parts['Bucket'])) { return $this->formatUrlStat($path); } else { return $this->triggerError("File or directory not found: {$path}", $flags); } } try { try { $result = static::$client->headObject($parts)->toArray(); if (substr($parts['Key'], -1, 1) == '/' && $result['ContentLength'] == 0) { // Return as if it is a bucket to account for console bucket objects (e.g., zero-byte object "foo/") return $this->formatUrlStat($path); } else { // Attempt to stat and cache regular object return $this->formatUrlStat(self::$client->headObject($parts)->toArray()); return $this->formatUrlStat($result); } } catch (NoSuchKeyException $e) { // Maybe this isn't an actual key, but a prefix. Do a prefix listing of objects to determine. /** * Modification by Joe Hoyle * * If there is an extension, we don't need to check if it's a dir. There is an issue with checking * if it's a dir, as s3 doesn't have true directories. See https://forums.aws.amazon.com/thread.jspa?threadID=142985 * for a more in-depth example. */ if ( $extension ) { return $this->triggerError("File or directory not found: {$path}", $flags); } $result = self::$client->listObjects(array( $result = static::$client->listObjects(array( 'Bucket' => $parts['Bucket'], 'Prefix' => $parts['Key'], 'Prefix' => rtrim($parts['Key'], '/') . '/', 'MaxKeys' => 1 )); if (!$result['Contents'] && !$result['CommonPrefixes']) { return $this->triggerError("File or directory not found: {$path}", $flags); } // This is a directory prefix return $this->formatUrlStat($path); } Loading s3-uploads.php +1 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ function s3_uploads_init() { } $instance = S3_Uploads::get_instance(); $instance->register_stream_wrapper(); add_filter( 'upload_dir', array( $instance, 'filter_upload_dir' ) ); add_filter( 'wp_image_editors', array( $instance, 'filter_editors' ), 9 ); Loading Loading
README.md +2 −2 Original line number Diff line number Diff line Loading @@ -88,11 +88,11 @@ define( 'S3_UPLOADS_CACHE_CONTROL', 30 * 24 * 60 * 60 ); // will expire in 30 days time ``` You can also configure the `Expires` header using the `S3_UPLOADS_EXPIRES` constant You can also configure the `Expires` header using the `S3_UPLOADS_HTTP_EXPIRES` constant For instance if you wanted to set an asset to effectively not expire, you could set the Expires header way off in the future. For example: ```PHP define( 'S3_UPLOADS_EXPIRES', gmdate( 'D, d M Y H:i:s', time() + (10 * 365 * 24 * 60 * 60) ) .' GMT' ); define( 'S3_UPLOADS_HTTP_EXPIRES', gmdate( 'D, d M Y H:i:s', time() + (10 * 365 * 24 * 60 * 60) ) .' GMT' ); // will expire in 10 years time ```
inc/class-s3-uploads-stream-wrapper.php +119 −7 Original line number Diff line number Diff line Loading @@ -7,12 +7,7 @@ class S3_Uploads_Stream_Wrapper extends Aws\S3\StreamWrapper { * * @param S3Client $client Client to use with the stream wrapper */ public static function register( Aws\S3\S3Client $client) { if (in_array('s3', stream_get_wrappers())) { stream_wrapper_unregister('s3'); } public static function register( Aws\S3\S3Client $client) { stream_wrapper_register( 's3', __CLASS__, STREAM_IS_URL ); static::$client = $client; } Loading Loading @@ -47,6 +42,123 @@ class S3_Uploads_Stream_Wrapper extends Aws\S3\StreamWrapper { } /** * @param string $path * @param string $mode * @param int $options * @param string $opened_path * * @return bool */ public function stream_open( $path, $mode, $options, &$opened_path ) { $result = parent::stream_open( $path, $mode, $options, $opened_path ); if ( ! $result ) { return $result; } if ( $mode === 'r' || $mode === 'a' ) { return $result; } /** * As we open a temp stream, we don't actually know if we have writing ability yet. * This means functions like copy() will not fail correctly, as the write to s3 * is only attemped on stream_flush() which is too late to report to copy() * et al that the write has failed. * * As a work around, we attempt to write an empty object. */ try { $p = $this->params; $p['Body'] = ''; static::$client->putObject($p); } catch (\Exception $e) { return $this->triggerError($e->getMessage()); } return $result; } /** * Provides information for is_dir, is_file, filesize, etc. Works on buckets, keys, and prefixes * * This is overrided to handle some optimizations with directories, else wp_upload_dir() causes * a stat() on every page load (atleast once). * * @param string $path * @param int $flags * * @return array Returns an array of stat data * @link http://www.php.net/manual/en/streamwrapper.url-stat.php */ public function url_stat( $path, $flags ) { $extension = pathinfo($path, PATHINFO_EXTENSION); /** * If the file is actually just a path to a directory * then return it as always existing. This is to work * around wp_upload_dir doing file_exists checks on * the uploads directory on every page load */ if ( ! $extension ) { return array ( 0 => 0, 'dev' => 0, 1 => 0, 'ino' => 0, 2 => 16895, 'mode' => 16895, 3 => 0, 'nlink' => 0, 4 => 0, 'uid' => 0, 5 => 0, 'gid' => 0, 6 => -1, 'rdev' => -1, 7 => 0, 'size' => 0, 8 => 0, 'atime' => 0, 9 => 0, 'mtime' => 0, 10 => 0, 'ctime' => 0, 11 => -1, 'blksize' => -1, 12 => -1, 'blocks' => -1, ); } // Check if this path is in the url_stat cache if ( isset ( self::$nextStat[ $path ] ) ) { return self::$nextStat[ $path ]; } $parts = $this->getParams( $path ); // Stat a bucket or just s3:// if ( ! $parts['Key'] && ( ! $parts['Bucket'] || self::$client->doesBucketExist( $parts['Bucket'] ) ) ) { return $this->formatUrlStat( $path ); } // You must pass either a bucket or a bucket + key if ( ! $parts['Key'] ) { return $this->triggerError( "File or directory not found: {$path}", $flags ); } try { // Attempt to stat and cache regular object return $this->formatUrlStat( self::$client->headObject( $parts )->toArray() ); } catch ( Exception $e ) { return $this->triggerError( $e->getMessage(), $flags ); } } public function stream_metadata( $path, $option, $value ) { // not implemented } Loading
inc/class-s3-uploads.php +7 −2 Original line number Diff line number Diff line Loading @@ -30,14 +30,19 @@ class S3_Uploads { $this->secret = $secret; $this->bucket_url = $bucket_url; $this->region = $region; } /** * Register the stream wrapper for s3 */ public function register_stream_wrapper() { if ( defined( 'S3_UPLOADS_USE_LOCAL' ) && S3_UPLOADS_USE_LOCAL ) { require_once dirname( __FILE__ ) . '/class-s3-uploads-local-stream-wrapper.php'; stream_wrapper_register( 's3', 'S3_Uploads_Local_Stream_Wrapper', STREAM_IS_URL ); } else { $s3 = $this->s3(); S3_Uploads_Stream_Wrapper::register( $s3 ); stream_context_set_option( stream_context_get_default(), 's3', 'ACL', Aws\S3\Enum\CannedAcl::PUBLIC_READ ); stream_context_set_option( stream_context_get_default(), 's3', 'ACL', 'public-read' ); } stream_context_set_option( stream_context_get_default(), 's3', 'seekable', true ); Loading
lib/aws-sdk/Aws/S3/StreamWrapper.php +18 −84 Original line number Diff line number Diff line Loading @@ -182,24 +182,6 @@ class StreamWrapper } elseif ($mode == 'a') { $this->openAppendStream($params, $errors); } else { /** * Modification by Joe Hoyle * * As we open a temp stream, we don't actually know if we have writing ability yet. * This means functions like copy() will not fail correctly, as the write to s3 * is only attemped on stream_flush() which is too late to report to copy() * et al that the write has failed. * * As a work around, we attempt to write an empty object. */ try { $p = $params; $p['Body'] = ''; static::$client->putObject($p); } catch (\Exception $e) { return $this->triggerError($e->getMessage()); } $this->openWriteStream($params, $errors); } } Loading Loading @@ -333,90 +315,42 @@ class StreamWrapper */ public function url_stat($path, $flags) { $extension = pathinfo($path, PATHINFO_EXTENSION); /** * If the file is actually just a path to a directory * then return it as always existing. This is to work * around wp_upload_dir doing file_exists checks on * the uploads directory on every page load */ if ( ! $extension ) { return array ( 0 => 0, 'dev' => 0, 1 => 0, 'ino' => 0, 2 => 16895, 'mode' => 16895, 3 => 0, 'nlink' => 0, 4 => 0, 'uid' => 0, 5 => 0, 'gid' => 0, 6 => -1, 'rdev' => -1, 7 => 0, 'size' => 0, 8 => 0, 'atime' => 0, 9 => 0, 'mtime' => 0, 10 => 0, 'ctime' => 0, 11 => -1, 'blksize' => -1, 12 => -1, 'blocks' => -1, ); } // Check if this path is in the url_stat cache if (isset(self::$nextStat[$path])) { return self::$nextStat[$path]; if (isset(static::$nextStat[$path])) { return static::$nextStat[$path]; } $parts = $this->getParams($path); // Stat a bucket or just s3:// if (!$parts['Key'] && (!$parts['Bucket'] || self::$client->doesBucketExist($parts['Bucket']))) { return $this->formatUrlStat($path); } // You must pass either a bucket or a bucket + key if (!$parts['Key']) { // Stat "directories": buckets, or "s3://" if (!$parts['Bucket'] || static::$client->doesBucketExist($parts['Bucket'])) { return $this->formatUrlStat($path); } else { return $this->triggerError("File or directory not found: {$path}", $flags); } } try { try { $result = static::$client->headObject($parts)->toArray(); if (substr($parts['Key'], -1, 1) == '/' && $result['ContentLength'] == 0) { // Return as if it is a bucket to account for console bucket objects (e.g., zero-byte object "foo/") return $this->formatUrlStat($path); } else { // Attempt to stat and cache regular object return $this->formatUrlStat(self::$client->headObject($parts)->toArray()); return $this->formatUrlStat($result); } } catch (NoSuchKeyException $e) { // Maybe this isn't an actual key, but a prefix. Do a prefix listing of objects to determine. /** * Modification by Joe Hoyle * * If there is an extension, we don't need to check if it's a dir. There is an issue with checking * if it's a dir, as s3 doesn't have true directories. See https://forums.aws.amazon.com/thread.jspa?threadID=142985 * for a more in-depth example. */ if ( $extension ) { return $this->triggerError("File or directory not found: {$path}", $flags); } $result = self::$client->listObjects(array( $result = static::$client->listObjects(array( 'Bucket' => $parts['Bucket'], 'Prefix' => $parts['Key'], 'Prefix' => rtrim($parts['Key'], '/') . '/', 'MaxKeys' => 1 )); if (!$result['Contents'] && !$result['CommonPrefixes']) { return $this->triggerError("File or directory not found: {$path}", $flags); } // This is a directory prefix return $this->formatUrlStat($path); } Loading
s3-uploads.php +1 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ function s3_uploads_init() { } $instance = S3_Uploads::get_instance(); $instance->register_stream_wrapper(); add_filter( 'upload_dir', array( $instance, 'filter_upload_dir' ) ); add_filter( 'wp_image_editors', array( $instance, 'filter_editors' ), 9 ); Loading