10 * Class for a Swift based file backend.
11 * Status messages should avoid mentioning the Swift account name
12 * Likewise, error suppression should be used to avoid path disclosure.
14 * This requires that the php-cloudfiles library is present,
15 * which is available at https://github.com/rackspace/php-cloudfiles.
16 * All of the library classes must be registed in $wgAutoloadClasses.
18 * @ingroup FileBackend
20 class SwiftFileBackend
extends FileBackend
{
21 /** @var CF_Authentication */
22 protected $auth; // swift authentication handler
23 /** @var CF_Connection */
24 protected $conn; // swift connection handle
25 protected $connStarted = 0; // integer UNIX timestamp
27 protected $swiftProxyUser; // string
28 protected $connTTL = 60; // integer seconds
31 * @see FileBackend::__construct()
32 * Additional $config params include:
33 * swiftAuthUrl : Swift authentication server URL
34 * swiftUser : Swift user used by MediaWiki
35 * swiftKey : Swift authentication key for the above user
36 * swiftProxyUser : Swift user used for end-user hits to proxy server
37 * shardViaHashLevels : Map of container names to the number of hash levels
39 public function __construct( array $config ) {
40 parent
::__construct( $config );
42 $this->auth
= new CF_Authentication(
43 $config['swiftUser'], $config['swiftKey'], null, $config['swiftAuthUrl'] );
45 $this->connTTL
= isset( $config['connTTL'] )
47 : 60; // some sane number
48 $this->swiftProxyUser
= isset( $config['swiftProxyUser'] )
49 ?
$config['swiftProxyUser']
51 $this->shardViaHashLevels
= isset( $config['shardViaHashLevels'] )
52 ?
$config['shardViaHashLevels']
57 * @see FileBackend::resolveContainerPath()
59 protected function resolveContainerPath( $container, $relStoragePath ) {
60 if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
61 return null; // too long for swift
63 return $relStoragePath;
67 * @see FileBackend::doCopyInternal()
69 protected function doCreateInternal( array $params ) {
70 $status = Status
::newGood();
72 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
73 if ( $destRel === null ) {
74 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
78 // (a) Get a swift proxy connection
79 $conn = $this->getConnection();
81 $status->fatal( 'backend-fail-connect', $this->name
);
85 // (b) Check the destination container
87 $dContObj = $conn->get_container( $dstCont );
88 } catch ( NoSuchContainerException
$e ) {
89 $status->fatal( 'backend-fail-create', $params['dst'] );
91 } catch ( InvalidResponseException
$e ) {
92 $status->fatal( 'backend-fail-connect', $this->name
);
94 } catch ( Exception
$e ) { // some other exception?
95 $status->fatal( 'backend-fail-internal', $this->name
);
96 $this->logException( $e, __METHOD__
, $params );
100 // (c) Check if the destination object already exists
102 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
103 // NoSuchObjectException not thrown: file must exist
104 if ( empty( $params['overwriteDest'] ) ) {
105 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
108 } catch ( NoSuchObjectException
$e ) {
109 // NoSuchObjectException thrown: file does not exist
110 } catch ( InvalidResponseException
$e ) {
111 $status->fatal( 'backend-fail-connect', $this->name
);
113 } catch ( Exception
$e ) { // some other exception?
114 $status->fatal( 'backend-fail-internal', $this->name
);
115 $this->logException( $e, __METHOD__
, $params );
119 // (d) Get a SHA-1 hash of the object
120 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
122 // (e) Actually create the object
124 $obj = $dContObj->create_object( $destRel );
125 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
126 $obj->metadata
= array( 'Sha1base36' => $sha1Hash );
127 $obj->write( $params['content'] );
128 } catch ( BadContentTypeException
$e ) {
129 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
130 } catch ( InvalidResponseException
$e ) {
131 $status->fatal( 'backend-fail-connect', $this->name
);
132 } catch ( Exception
$e ) { // some other exception?
133 $status->fatal( 'backend-fail-internal', $this->name
);
134 $this->logException( $e, __METHOD__
, $params );
141 * @see FileBackend::doStoreInternal()
143 protected function doStoreInternal( array $params ) {
144 $status = Status
::newGood();
146 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
147 if ( $destRel === null ) {
148 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
152 // (a) Get a swift proxy connection
153 $conn = $this->getConnection();
155 $status->fatal( 'backend-fail-connect', $this->name
);
159 // (b) Check the destination container
161 $dContObj = $conn->get_container( $dstCont );
162 } catch ( NoSuchContainerException
$e ) {
163 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
165 } catch ( InvalidResponseException
$e ) {
166 $status->fatal( 'backend-fail-connect', $this->name
);
168 } catch ( Exception
$e ) { // some other exception?
169 $status->fatal( 'backend-fail-internal', $this->name
);
170 $this->logException( $e, __METHOD__
, $params );
174 // (c) Check if the destination object already exists
176 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
177 // NoSuchObjectException not thrown: file must exist
178 if ( empty( $params['overwriteDest'] ) ) {
179 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
182 } catch ( NoSuchObjectException
$e ) {
183 // NoSuchObjectException thrown: file does not exist
184 } catch ( InvalidResponseException
$e ) {
185 $status->fatal( 'backend-fail-connect', $this->name
);
187 } catch ( Exception
$e ) { // some other exception?
188 $status->fatal( 'backend-fail-internal', $this->name
);
189 $this->logException( $e, __METHOD__
, $params );
193 // (d) Get a SHA-1 hash of the object
194 $sha1Hash = sha1_file( $params['src'] );
195 if ( $sha1Hash === false ) { // source doesn't exist?
196 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
199 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
201 // (e) Actually store the object
203 $obj = $dContObj->create_object( $destRel );
204 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
205 $obj->metadata
= array( 'Sha1base36' => $sha1Hash );
206 $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
207 } catch ( BadContentTypeException
$e ) {
208 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
209 } catch ( IOException
$e ) {
210 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
211 } catch ( InvalidResponseException
$e ) {
212 $status->fatal( 'backend-fail-connect', $this->name
);
213 } catch ( Exception
$e ) { // some other exception?
214 $status->fatal( 'backend-fail-internal', $this->name
);
215 $this->logException( $e, __METHOD__
, $params );
222 * @see FileBackend::doCopyInternal()
224 protected function doCopyInternal( array $params ) {
225 $status = Status
::newGood();
227 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
228 if ( $srcRel === null ) {
229 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
233 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
234 if ( $destRel === null ) {
235 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
239 // (a) Get a swift proxy connection
240 $conn = $this->getConnection();
242 $status->fatal( 'backend-fail-connect', $this->name
);
246 // (b) Check the source and destination containers
248 $sContObj = $conn->get_container( $srcCont );
249 $dContObj = $conn->get_container( $dstCont );
250 } catch ( NoSuchContainerException
$e ) {
251 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
253 } catch ( InvalidResponseException
$e ) {
254 $status->fatal( 'backend-fail-connect', $this->name
);
256 } catch ( Exception
$e ) { // some other exception?
257 $status->fatal( 'backend-fail-internal', $this->name
);
258 $this->logException( $e, __METHOD__
, $params );
262 // (c) Check if the destination object already exists
264 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
265 // NoSuchObjectException not thrown: file must exist
266 if ( empty( $params['overwriteDest'] ) ) {
267 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
270 } catch ( NoSuchObjectException
$e ) {
271 // NoSuchObjectException thrown: file does not exist
272 } catch ( InvalidResponseException
$e ) {
273 $status->fatal( 'backend-fail-connect', $this->name
);
275 } catch ( Exception
$e ) { // some other exception?
276 $status->fatal( 'backend-fail-internal', $this->name
);
277 $this->logException( $e, __METHOD__
, $params );
281 // (d) Actually copy the file to the destination
283 $sContObj->copy_object_to( $srcRel, $dContObj, $destRel );
284 } catch ( NoSuchObjectException
$e ) { // source object does not exist
285 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
286 } catch ( InvalidResponseException
$e ) {
287 $status->fatal( 'backend-fail-connect', $this->name
);
288 } catch ( Exception
$e ) { // some other exception?
289 $status->fatal( 'backend-fail-internal', $this->name
);
290 $this->logException( $e, __METHOD__
, $params );
297 * @see FileBackend::doDeleteInternal()
299 protected function doDeleteInternal( array $params ) {
300 $status = Status
::newGood();
302 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
303 if ( $srcRel === null ) {
304 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
308 // (a) Get a swift proxy connection
309 $conn = $this->getConnection();
311 $status->fatal( 'backend-fail-connect', $this->name
);
315 // (b) Check the source container
317 $sContObj = $conn->get_container( $srcCont );
318 } catch ( NoSuchContainerException
$e ) {
319 $status->fatal( 'backend-fail-delete', $params['src'] );
321 } catch ( InvalidResponseException
$e ) {
322 $status->fatal( 'backend-fail-connect', $this->name
);
324 } catch ( Exception
$e ) { // some other exception?
325 $status->fatal( 'backend-fail-internal', $this->name
);
326 $this->logException( $e, __METHOD__
, $params );
330 // (c) Actually delete the object
332 $sContObj->delete_object( $srcRel );
333 } catch ( NoSuchObjectException
$e ) {
334 if ( empty( $params['ignoreMissingSource'] ) ) {
335 $status->fatal( 'backend-fail-delete', $params['src'] );
337 } catch ( InvalidResponseException
$e ) {
338 $status->fatal( 'backend-fail-connect', $this->name
);
339 } catch ( Exception
$e ) { // some other exception?
340 $status->fatal( 'backend-fail-internal', $this->name
);
341 $this->logException( $e, __METHOD__
, $params );
348 * @see FileBackend::doPrepareInternal()
350 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
351 $status = Status
::newGood();
353 // (a) Get a swift proxy connection
354 $conn = $this->getConnection();
356 $status->fatal( 'backend-fail-connect', $this->name
);
360 // (b) Create the destination container
362 $conn->create_container( $fullCont );
363 } catch ( InvalidResponseException
$e ) {
364 $status->fatal( 'backend-fail-connect', $this->name
);
365 } catch ( Exception
$e ) { // some other exception?
366 $status->fatal( 'backend-fail-internal', $this->name
);
367 $this->logException( $e, __METHOD__
, $params );
374 * @see FileBackend::doSecureInternal()
376 protected function doSecureInternal( $fullCont, $dir, array $params ) {
377 $status = Status
::newGood();
378 // @TODO: restrict container from $this->swiftProxyUser
383 * @see FileBackend::doFileExists()
385 protected function doGetFileStat( array $params ) {
386 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
387 if ( $srcRel === null ) {
388 return false; // invalid storage path
391 $conn = $this->getConnection();
398 $container = $conn->get_container( $srcCont );
399 // @TODO: handle 'latest' param as "X-Newest: true"
400 $obj = $container->get_object( $srcRel );
401 // Convert "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
402 $date = DateTime
::createFromFormat( 'D, d F Y G:i:s e', $obj->last_modified
);
405 'mtime' => $date->format( 'YmdHis' ),
406 'size' => $obj->content_length
,
407 'sha1' => $obj->metadata
['Sha1base36']
409 } else { // exception will be caught below
410 throw new Exception( "Could not parse date for object {$srcRel}" );
412 } catch ( NoSuchContainerException
$e ) {
413 } catch ( NoSuchObjectException
$e ) {
414 } catch ( InvalidResponseException
$e ) {
416 } catch ( Exception
$e ) { // some other exception?
418 $this->logException( $e, __METHOD__
, $params );
425 * @see FileBackendBase::getFileContents()
427 public function getFileContents( array $params ) {
428 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
429 if ( $srcRel === null ) {
430 return false; // invalid storage path
433 $conn = $this->getConnection();
440 $container = $conn->get_container( $srcCont );
441 $obj = $container->get_object( $srcRel );
442 $data = $obj->read( $this->headersFromParams( $params ) );
443 } catch ( NoSuchContainerException
$e ) {
444 } catch ( NoSuchObjectException
$e ) {
445 } catch ( InvalidResponseException
$e ) {
446 } catch ( Exception
$e ) { // some other exception?
447 $this->logException( $e, __METHOD__
, $params );
454 * @see FileBackend::getFileListInternal()
456 public function getFileListInternal( $fullCont, $dir, array $params ) {
457 return new SwiftFileIterator( $this, $fullCont, $dir );
461 * Do not call this function outside of SwiftFileIterator
463 * @param $fullCont string Resolved container name
464 * @param $dir string Resolved storage directory with no trailing slash
465 * @param $after string Storage path of file to list items after
466 * @param $limit integer Max number of items to list
469 public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
470 $conn = $this->getConnection();
477 $container = $conn->get_container( $fullCont );
478 $files = $container->list_objects( $limit, $after, "{$dir}/" );
479 } catch ( NoSuchContainerException
$e ) {
480 } catch ( NoSuchObjectException
$e ) {
481 } catch ( InvalidResponseException
$e ) {
482 } catch ( Exception
$e ) { // some other exception?
483 $this->logException( $e, __METHOD__
, $params );
490 * @see FileBackend::doGetFileSha1base36()
492 public function doGetFileSha1base36( array $params ) {
493 $stat = $this->getFileStat( $params );
495 return $stat['sha1'];
502 * @see FileBackend::doStreamFile()
504 protected function doStreamFile( array $params ) {
505 $status = Status
::newGood();
507 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
508 if ( $srcRel === null ) {
509 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
512 $conn = $this->getConnection();
514 $status->fatal( 'backend-fail-connect', $this->name
);
518 $cont = $conn->get_container( $srcCont );
519 $obj = $cont->get_object( $srcRel );
520 } catch ( NoSuchContainerException
$e ) {
521 $status->fatal( 'backend-fail-stream', $params['src'] );
523 } catch ( NoSuchObjectException
$e ) {
524 $status->fatal( 'backend-fail-stream', $params['src'] );
526 } catch ( IOException
$e ) {
527 $status->fatal( 'backend-fail-stream', $params['src'] );
529 } catch ( Exception
$e ) { // some other exception?
530 $status->fatal( 'backend-fail-stream', $params['src'] );
531 $this->logException( $e, __METHOD__
, $params );
536 $output = fopen( "php://output", "w" );
537 $obj->stream( $output, $this->headersFromParams( $params ) );
538 } catch ( InvalidResponseException
$e ) {
539 $status->fatal( 'backend-fail-connect', $this->name
);
540 } catch ( Exception
$e ) { // some other exception?
541 $status->fatal( 'backend-fail-stream', $params['src'] );
542 $this->logException( $e, __METHOD__
, $params );
549 * @see FileBackend::getLocalCopy()
551 public function getLocalCopy( array $params ) {
552 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
553 if ( $srcRel === null ) {
557 $conn = $this->getConnection();
562 // Get source file extension
563 $ext = FileBackend
::extensionFromPath( $srcRel );
564 // Create a new temporary file...
565 $tmpFile = TempFSFile
::factory( wfBaseName( $srcRel ) . '_', $ext );
571 $cont = $conn->get_container( $srcCont );
572 $obj = $cont->get_object( $srcRel );
573 $handle = fopen( $tmpFile->getPath(), 'w' );
575 $obj->stream( $handle, $this->headersFromParams( $params ) );
578 $tmpFile = null; // couldn't open temp file
580 } catch ( NoSuchContainerException
$e ) {
582 } catch ( NoSuchObjectException
$e ) {
584 } catch ( InvalidResponseException
$e ) {
586 } catch ( Exception
$e ) { // some other exception?
588 $this->logException( $e, __METHOD__
, $params );
595 * Get headers to send to Swift when reading a file based
596 * on a FileBackend params array, e.g. that of getLocalCopy().
597 * $params is currently only checked for a 'latest' flag.
599 * @param $params Array
602 protected function headersFromParams( array $params ) {
604 if ( !empty( $params['latest'] ) ) {
605 $hdrs[] = 'X-Newest: true';
611 * Get a connection to the swift proxy
613 * @return CF_Connection|false
615 protected function getConnection() {
616 if ( $this->conn
=== false ) {
617 return false; // failed last attempt
619 // Authenticate with proxy and get a session key.
620 // Session keys expire after a while, so we renew them periodically.
621 if ( $this->conn
=== null ||
( time() - $this->connStarted
) > $this->connTTL
) {
623 $this->auth
->authenticate();
624 $this->conn
= new CF_Connection( $this->auth
);
625 $this->connStarted
= time();
626 } catch ( AuthenticationException
$e ) {
627 $this->conn
= false; // don't keep re-trying
628 } catch ( InvalidResponseException
$e ) {
629 $this->conn
= false; // don't keep re-trying
636 * Log an unexpected exception for this backend
638 * @param $e Exception
639 * @param $func string
640 * @param $params Array
643 protected function logException( Exception
$e, $func, array $params ) {
644 wfDebugLog( 'SwiftBackend',
645 get_class( $e ) . " in '{$this->name}': '{$func}' with " . serialize( $params )
651 * SwiftFileBackend helper class to page through object listings.
652 * Swift also has a listing limit of 10,000 objects for sanity.
654 * @ingroup FileBackend
656 class SwiftFileIterator
implements Iterator
{
658 protected $bufferIter = array();
659 protected $bufferAfter = null; // string; list items *after* this path
660 protected $pos = 0; // integer
662 /** @var SwiftFileBackend */
664 protected $container; //
665 protected $dir; // string storage directory
666 protected $suffixStart; // integer
668 const PAGE_SIZE
= 5000; // file listing buffer size
671 * Get an FSFileIterator from a file system directory
673 * @param $backend SwiftFileBackend
674 * @param $fullCont string Resolved container name
675 * @param $dir string Resolved relative directory
677 public function __construct( SwiftFileBackend
$backend, $fullCont, $dir ) {
678 $this->backend
= $backend;
679 $this->container
= $fullCont;
681 if ( substr( $this->dir
, -1 ) === '/' ) {
682 $this->dir
= substr( $this->dir
, 0, -1 ); // remove trailing slash
684 $this->suffixStart
= strlen( $dir ) +
1; // size of "path/to/dir/"
687 public function current() {
688 return substr( current( $this->bufferIter
), $this->suffixStart
);
691 public function key() {
695 public function next() {
696 // Advance to the next file in the page
697 next( $this->bufferIter
);
699 // Check if there are no files left in this page and
700 // advance to the next page if this page was not empty.
701 if ( !$this->valid() && count( $this->bufferIter
) ) {
702 $this->bufferAfter
= end( $this->bufferIter
);
703 $this->bufferIter
= $this->backend
->getFileListPageInternal(
704 $this->container
, $this->dir
, $this->bufferAfter
, self
::PAGE_SIZE
709 public function rewind() {
711 $this->bufferAfter
= null;
712 $this->bufferIter
= $this->backend
->getFileListPageInternal(
713 $this->container
, $this->dir
, $this->bufferAfter
, self
::PAGE_SIZE
717 public function valid() {
718 return ( current( $this->bufferIter
) !== false ); // no paths can have this value