Commit 4be50809 authored by Joe Hoyle's avatar Joe Hoyle
Browse files

Move the aws-sdk modifications to our own class

This moves the hacks we did have to the sdk into our own subclass, which
makes upgrading the SDK a lot simpler.
parent dddd001b
Loading
Loading
Loading
Loading
+144 −0
Original line number Diff line number Diff line
@@ -47,6 +47,150 @@ 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 {
			try {
				// Attempt to stat and cache regular object
				return $this->formatUrlStat(self::$client->headObject($parts)->toArray());
			} 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(
					'Bucket'  => $parts['Bucket'],
					'Prefix'  => $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);
			}
		} catch (\Exception $e) {
			return $this->triggerError($e->getMessage(), $flags);
		}
	}

	public function stream_metadata( $path, $option, $value ) {
		// not implemented
	}
+18 −84
Original line number Diff line number Diff line
@@ -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);
            }
        }
@@ -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);
            }
+25 −0
Original line number Diff line number Diff line
@@ -63,4 +63,29 @@ class Test_S3_Uploads_Stream_Wrapper extends WP_UnitTestCase {

		$this->assertFalse( $result );
	}

	/**
	 * As s3 doesn't have directories, we expect that mkdir does not cause any s3
	 * connectivity.
	 */
	public function test_file_exists_on_dir_does_not_cause_network_activity() {

		stream_wrapper_unregister( 's3' );

		// incorrect secret so we'll ato fail if any writing / reading is attempted
		$params = array( 'key' => S3_UPLOADS_KEY, 'secret' => 123 );
		$s3 = Aws\Common\Aws::factory( $params )->get( 's3' );

		S3_Uploads_Stream_Wrapper::register( $s3 );
		stream_context_set_option( stream_context_get_default(), 's3', 'ACL', Aws\S3\Enum\CannedAcl::PUBLIC_READ );

		$bucket_root = strtok( S3_UPLOADS_BUCKET, '/' );

		// result would fail as we don't have permission to write here.
		$result = file_exists( 's3://' . $bucket_root . '/some_dir' );
		$this->assertTrue( $result );

		$result = is_dir( 's3://' . $bucket_root . '/some_dir' );
		$this->assertTrue( $result );
	}
}
 No newline at end of file