Parent5446 has uploaded a new change for review.
https://gerrit.wikimedia.org/r/52089
Change subject: Added S3 Backend (do not merge).
......................................................................
Added S3 Backend (do not merge).
Change-Id: Id2f1492fa7dc8d1467fcf0856080e4d12fd964cc
---
M AWS.php
A s3/AmazonS3FileBackend.php
A s3/AmazonS3Repo.php
3 files changed, 487 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/AWS
refs/changes/89/52089/1
diff --git a/AWS.php b/AWS.php
index 6ab567e..9a92986 100644
--- a/AWS.php
+++ b/AWS.php
@@ -43,6 +43,20 @@
*/
$wgAWSRegion = false;
+$wgAutoloadClasses['AmazonS3Repo'] = __DIR__ . '/s3/AmazonS3Repo.php';
+$wgAutoloadClasses['AmazonS3FileBackend'] = __DIR__ .
'/s3/AmazonS3FileBackend.php';
+
$wgExtensionMessagesFiles['AWS'] = __DIR__ . '/AWS.i18n.php';
+$wgLockManagers[] = array(
+ 'name' => 'memcLockManager',
+ 'class' => 'MemcLockManager'
+);
+
+$wgFileBackends[] = array(
+ 'name' => 'amazons3-backend',
+ 'class' => 'AmazonS3FileBackend',
+ 'lockManager' => 'memcLockManager',
+);
+
require_once( __DIR__ . '/vendor/autoload.php' );
diff --git a/s3/AmazonS3FileBackend.php b/s3/AmazonS3FileBackend.php
new file mode 100644
index 0000000..333cab4
--- /dev/null
+++ b/s3/AmazonS3FileBackend.php
@@ -0,0 +1,458 @@
+<?php
+
+use Aws\S3\S3Client;
+
+class AmazonS3FileBackend extends FileBackendStore {
+ function __construct( array $config ) {
+ global $wgAWSCredentials, $wgAWSRegion;
+
+ parent::__construct( $config );
+
+ $this->encryption = $wgAWSServerSideEncryption ? 'AES256' :
null;
+ $this->client = S3Client::factory( array(
+ 'key' => $wgAWSCredentials['key'],
+ 'secret' => $wgAWSCredentials['secret'],
+ 'region' => $wgAWSRegion,
+ 'scheme' => WebRequest::detectProtocol(),
+ 'ssl.certificate_authority' => true
+ ) );
+ }
+
+ function directoriesAreVirtual() {
+ return true;
+ }
+
+ function isPathUsableInternal( $storagePath ) {
+ return count( $storagePath ) <= 1024;
+ }
+
+ function resolveContainerPath( $container, $relStoragePath ) {
+ $validLabel = '[a-z0-9]+([a-z0-9\-]+[a-z0-9])?';
+ if(
+ $this->client->isValidBucketName( $container ) &&
+ $this->isPathUsableInternal( $relStoragePath )
+ ) {
+ return $relStoragePath;
+ } else {
+ return null;
+ }
+ }
+
+
+
+ function doCreateInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $container, $key ) = $this->resolveStoragePathReal(
$params['dst'] );
+ if( $container === null || $key == null ) {
+ $status->fatal( 'backend-fail-invalidpath',
$params['dst'] );
+ return $status;
+ }
+
+ $params['headers'] = isset( $params['headers'] ) ?
$params['headers'] : array();
+ $params['headers'] += array_fill_keys( array(
+ 'Cache-Control' => null,
+ 'Content-Encoding' => null,
+ 'Content-Language' => null,
+ 'Content-Type' => null,
+ 'Expires' => null
+ ), null );
+
+ $res = $this->client->putObject( array(
+ 'ACL' => $this->isSecure( $container ) ? 'private' :
'public-read',
+ 'Body' => $params['content'],
+ 'Bucket' => $container,
+ 'CacheControl' => $params['headers']['Cache-Control'],
+ 'ContentDisposition' => $this->truncDisp( $disposition
),
+ 'ContentEncoding' =>
$params['headers']['ContentEncoding'],
+ 'ContentLanguage' =>
$params['headers']['ContentLanguage'],
+ 'ContentType' => $params['headers']['ContentType'],
+ 'Expires' => $params['headers']['Expires'],
+ 'Key' => $this->client->encodeKey( $key ),
+ 'ServerSideEncryption' => $this->encryption,
+ 'ContentMD5' => md5( $params['content'] ),
+ ) );
+
+ if( !$res['RequestId'] ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ }
+
+ return $status;
+ }
+
+ function doStoreInternal( array $params ) {
+ $params['content'] = fopen( $params['src'], 'r' );
+ return $this->doCreateInternal( $params );
+ }
+
+ function doCopyInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $srcContainer, $srcKey ) = $this->resolveStoragePathReal(
$params['src'] );
+ list( $dstContainer, $dstKey ) = $this->resolveStoragePathReal(
$params['dst'] );
+ if( $srcContainer === null || $srcKey == null ) {
+ $status->fatal( 'backend-fail-invalidpath',
$params['src'] );
+ }
+ if( $dstContainer === null || $dstKey == null ) {
+ $status->fatal( 'backend-fail-invalidpath',
$params['dst'] );
+ }
+
+ if( !$status->isOK() ) {
+ return $status;
+ }
+
+ $params['headers'] += array_fill_keys( array(
+ 'Cache-Control',
+ 'Content-Encoding',
+ 'Content-Language',
+ 'Content-Type',
+ 'Expires'
+ ), null );
+
+ if( $srcContainer == $dstContainer ) {
+ $res = $this->client->copyObject( array_filter( array(
+ 'ACL' => $this->isSecure( $container ) ?
'private' : 'public-read',
+ 'Bucket' => $srcContainer,
+ 'CacheControl' =>
$params['headers']['Cache-Control'],
+ 'ContentDisposition' => $this->truncDisp(
$disposition ),
+ 'ContentEncoding' =>
$params['headers']['ContentEncoding'],
+ 'ContentLanguage' =>
$params['headers']['ContentLanguage'],
+ 'ContentType' =>
$params['headers']['ContentType'],
+ 'CopySource' => urlencode( $srcKey ),
+ 'CopySourceIfMatch' =>
$params['headers']['ETag'],
+ 'CopySourceIfModifiedSince' =>
$params['headers']['If-Modified-Since'],
+ 'Expires' => $params['headers']['Expires'],
+ 'Key' => $this->client->encodeKey( $key ),
+ 'ServerSideEncryption' => $this->encryption
+ ) ) );
+
+ if( !$res['RequestId'] ) {
+ $status->fatal( 'backend-fail-create',
$params['dst'] );
+ }
+ } else {
+ // Not in the same bucket, so a manual copy/paste has
to be done.
+ $params['content'] = $this->getFileContents( array(
'src' => $params['src'] ) );
+ $status->merge( $this->createInternal( $params ) );
+ }
+
+ return $status;
+ }
+
+ function doDeleteInternal( array $params ) {
+ $status = Status::newGood();
+
+ list( $container, $key ) = $this->resolveStoragePathReal(
$params['src'] );
+ if( $container === null || $key == null ) {
+ $status->fatal( 'backend-fail-invalidpath',
$params['src'] );
+ return $status;
+ }
+
+ $this->client->deleteObject( array(
+ 'Bucket' => $container,
+ 'Key' => $this->client->encodeKey( $key )
+ ) );
+
+ if( !$res['RequestId'] ) {
+ $status->fatal( 'backend-fail-create', $params['dst'] );
+ }
+
+ return $status;
+ }
+
+
+
+ function doDirectoryExists( $container, $dir, array $params ) {
+ // See if at least one file is in the directory.
+ $it = new AmazonS3FileIterator( $this->client, $container,
$dir, array( 'topOnly' ), 1 );
+ return $it->valid();
+ }
+
+ function doGetFileStat( array $params ) {
+ list( $container, $key ) = $this->resolveStoragePathReal(
$params['src'] );
+ if( $container === null || $key == null ) {
+ return null;
+ } elseif( !$this->client->doesObjectExist( $container, $key ) )
{
+ return false;
+ }
+
+ $res = $this->client->headObject( array(
+ 'Bucket' => $container,
+ 'Key' => $this->client->encodeKey( $key )
+ ) );
+
+ return array(
+ 'mtime' => wfTimestamp( TS_MW, $res['LastModified'] ),
+ 'size' => $res['ContentLength'],
+ 'etag' => $res['Etag']
+ );
+ }
+
+ function getFileHttpUrl( array $params ) {
+ list( $container, $key ) = $this->resolveStoragePathReal(
$params['src'] );
+ if( $container === null ) {
+ return null;
+ } elseif ( $key ) {
+ $key = "/$key";
+ }
+
+ $scheme = WebRequest::detectProtocol();
+ return "$scheme://$container.s3.amazonaws.com$key";
+ }
+
+ function getDirectoryListInternal( $container, $dir, array $params ) {
+ return new AmazonS3DirectoryIterator( $this->client,
$container, $dir, $params );
+ }
+
+ function getFileListInternal( $container, $dir, array $params ) {
+ return new AmazonS3FileIterator( $this->client, $container,
$dir, $params );
+ }
+
+
+
+ function doGetLocalCopyMulti( array $params ) {
+ $fsFiles = array();
+ if ( !isset( $params['srcs'] ) ) {
+ $params['srcs'] = array( $params['src'] );
+ }
+ foreach( $params['srcs'] as $src ) {
+ list( $container, $key ) =
$this->resolveStoragePathReal( $src );
+ if( $container === null || $key === null ) {
+ $fsFiles[$src] = null;
+ } else {
+ $ext = self::extensionFromPath( $src );
+ $tmpFile = TempFSFile::factory( 'localcopy_',
$ext );
+ if( !$tmpFile ) {
+ $fsFiles[$src] = null;
+ } else {
+ $srcPath = $this->getFileHttpUrl(
array( 'src' => $src ) );
+ $dstPath = $tmpFile->getPath();
+
+ wfSuppressWarnings();
+ $ok = copy( $srcPath, $dstPath );
+ wfRestoreWarnings();
+
+ if( !$ok ) {
+ $fsFiles[$src] = null;
+ } else {
+ $fsFiles[$src] = $tmpFile;
+ }
+ }
+ }
+ }
+ return $fsFiles;
+ }
+
+ function doGetLocalReferenceMulti( array $params ) {
+ $fsFiles = array();
+ if ( !isset( $params['srcs'] ) ) {
+ $params['srcs'] = array( $params['src'] );
+ }
+ foreach( $params['srcs'] as $src ) {
+ list( $container, $key ) =
$this->resolveStoragePathReal( $src );
+ if( $container === null || $key === null ) {
+ $fsFiles[$src] = null;
+ } else {
+ $fsFiles[$src] = new FSFile(
$this->getFileHttpUrl( array( 'src' => $src ) ) );
+ }
+ }
+ return $fsFiles;
+ }
+
+
+
+ function doPrepareInternal( $container, $dir, array $params ) {
+ $status = Status::newGood();
+
+ if( $this->client->doesBucketExist( $container ) ) {
+ return $status;
+ }
+
+ $res = $this->client->createBucket( array(
+ 'ACL' => isset( $params['noListing'] ) ? 'private' :
'public-read',
+ 'Bucket' => $container
+ ) );
+
+ if( !$res['Location'] ) {
+ $status->fatal( 'directorycreateerror', $container );
+ }
+
+ $this->client->waitUntilBucketExists( array( 'Bucket' =>
$container ) );
+
+ $status->merge( $this->doSecureInternal( $container, $dir,
$params ) );
+
+ return $status;
+ }
+
+ function doPublishInternal( $container, $dir, array $params ) {
+ $status = Status::newGood();
+
+ if( !empty( $params['listing'] ) ) {
+ $res = $this->client->putBucketAcl( array(
+ 'ACL' => 'public-read',
+ 'Bucket' => $container
+ ) );
+
+ if( !$res['RequestId'] ) {
+ $status->fatal( 'backend-fail-delete',
$container );
+ }
+ }
+
+ if( !empty( $params['access'] ) ) {
+ foreach( new AmazonS3FileIterator( $this->client,
$container, $dir, $params ) as $key ) {
+ $res = $this->client->putObjectAcl( array(
+ 'ACL' => 'public-read',
+ 'Bucket' => $container,
+ 'Key' => $key
+ ) );
+
+ if( !$res['RequestID'] ) {
+ $status->fatal( 'backend-fail-delete',
$key );
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ function doSecureInternal( $container, $dir, array $params ) {
+ $status = Status::newGood();
+
+ if( !empty( $params['noListing'] ) ) {
+ $res = $this->client->putBucketAcl( array(
+ 'ACL' => 'private',
+ 'Bucket' => $container
+ ) );
+
+ if( !$res['RequestId'] ) {
+ $status->fatal( 'backend-fail-create',
$container );
+ }
+ }
+
+ if( !empty( $params['noAccess'] ) ) {
+ foreach( new AmazonS3FileIterator( $this->client,
$container, $dir, $params ) as $key ) {
+ $res = $this->client->putObjectAcl( array(
+ 'ACL' => 'private',
+ 'Bucket' => $container,
+ 'Key' => $key
+ ) );
+
+ if( !$res['RequestID'] ) {
+ $status->fatal( 'backend-fail-create',
$key );
+ }
+ }
+ }
+
+ return $status;
+ }
+
+ private function isSecure( $container ) {
+ static $pubUrl =
"http://acs.amazonaws.com/groups/global/AllUsers";
+ $acl = $this->client->getBucketAcl( array( 'Bucket' =>
$container ) );
+ foreach( $acl['Grants'] as $grant ) {
+ if( $grant['Grantee']['URI'] == $pubUrl ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private function truncDisp( $disposition ) {
+ $res = '';
+ foreach( explode( ';', $disposition ) as $part ) {
+ $part = trim( $part );
+ $new = $res ? $part : "{$res};{$part}";
+ if( strlen( $new ) <= 255 ) {
+ $res = $new;
+ } else {
+ break;
+ }
+ }
+ return $res;
+ }
+}
+
+class AmazonS3FileIterator implements Iterator {
+ private $client, $container, $dir, $topOnly, $limit;
+ private $index, $results, $market, $finished;
+
+ public function __construct( S3Client $client, $container, $dir, array
$params, $limit = 500 ) {
+ $this->client = $client;
+ $this->container = $container;
+ $this->dir = $dir;
+ $this->limit = $limit;
+ $this->topOnly = $params['topOnly'];
+
+ $this->rewind();
+ }
+
+ public function key() {
+ $this->init();
+ return $this->index;
+ }
+
+ public function current() {
+ $this->init();
+ return $this->results['Contents'][$this->index]['Key'];
+ }
+
+ public function next() {
+ if( $this->topOnly ) {
+ do {
+ ++$this->index;
+ } while( strpos( $this->current(), '/', strlen(
$this->dir ) ) !== false );
+ } else {
+ ++$this->index;
+ }
+ }
+
+ public function rewind() {
+ $this->results = null;
+ $this->marker = null;
+ $this->index = 0;
+ $this->finished = false;
+ }
+
+ public function valid() {
+ $this->init();
+ return !$this->finished || $this->index < count(
$this->results['Contents'] );
+ }
+
+ private function init() {
+ if(
+ (
+ $this->results === null ||
+ $this->index >= count(
$this->results['Contents'] )
+ ) &&
+ !$this->finished
+ ) {
+ $this->results = $this->client->listObjects( array(
+ 'Bucket' => $this->container,
+ 'Delimiter' => '/',
+ 'Marker' => $this->marker,
+ 'MaxKeys' => $this->limit,
+ 'Prefix' => $this->dir
+ ) );
+
+ $this->index = 0;
+ $this->marker = $this->results['Marker'];
+ $this->finished = !$this->results['IsTruncated'];
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
+
+class AmazonS3DirectoryIterator extends AmazonS3FileIterator {
+ private $directories = array();
+
+ function current() {
+ return dirname( parent::current() );
+ }
+
+ function next() {
+ do {
+ parent::next();
+ } while( array_key_exists( $this->current(), $this->directories
) );
+ }
+}
diff --git a/s3/AmazonS3Repo.php b/s3/AmazonS3Repo.php
new file mode 100644
index 0000000..074211b
--- /dev/null
+++ b/s3/AmazonS3Repo.php
@@ -0,0 +1,15 @@
+<?php
+
+class AmazonS3Repo extends LocalRepo {
+ function __construct( array $info = null ) {
+ settype( $info, 'array' );
+ $info['backend'] = 'amazons3-backend';
+
+ parent::__construct( $info );
+ }
+
+ function getZoneUrl( $zone, $ext = null ) {
+ $params = array( 'src' => $this->getZonePath( $zone ) );
+ return $this->backend->getFileHttpUrl( $params );
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/52089
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Id2f1492fa7dc8d1467fcf0856080e4d12fd964cc
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/AWS
Gerrit-Branch: master
Gerrit-Owner: Parent5446 <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits