10 * Class for an OpenStack Swift based file backend.
12 * This requires that the php-cloudfiles library is present,
13 * which is available at https://github.com/rackspace/php-cloudfiles.
14 * All of the library classes must be registed in $wgAutoloadClasses.
16 * Status messages should avoid mentioning the Swift account name
17 * Likewise, error suppression should be used to avoid path disclosure.
19 * @ingroup FileBackend
22 class SwiftFileBackend
extends FileBackend
{
23 /** @var CF_Authentication */
24 protected $auth; // Swift authentication handler
26 /** @var CF_Connection */
27 protected $conn; // Swift connection handle
28 protected $connStarted = 0; // integer UNIX timestamp
29 protected $connContainers = array(); // container object cache
30 protected $connTTL = 120; // integer seconds
32 protected $swiftProxyUser; // string
35 * @see FileBackend::__construct()
36 * Additional $config params include:
37 * swiftAuthUrl : Swift authentication server URL
38 * swiftUser : Swift user used by MediaWiki
39 * swiftKey : Swift authentication key for the above user
40 * swiftProxyUser : Swift user used for end-user hits to proxy server
41 * shardViaHashLevels : Map of container names to the number of hash levels
43 public function __construct( array $config ) {
44 parent
::__construct( $config );
46 $this->auth
= new CF_Authentication(
47 $config['swiftUser'], $config['swiftKey'], null, $config['swiftAuthUrl'] );
49 $this->connTTL
= isset( $config['connTTL'] )
51 : 60; // some sane number
52 $this->swiftProxyUser
= isset( $config['swiftProxyUser'] )
53 ?
$config['swiftProxyUser']
55 $this->shardViaHashLevels
= isset( $config['shardViaHashLevels'] )
56 ?
$config['shardViaHashLevels']
61 * @see FileBackend::resolveContainerPath()
63 protected function resolveContainerPath( $container, $relStoragePath ) {
64 if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
65 return null; // too long for Swift
67 return $relStoragePath;
71 * @see FileBackend::doCopyInternal()
73 protected function doCreateInternal( array $params ) {
74 $status = Status
::newGood();
76 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
77 if ( $destRel === null ) {
78 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
82 // (a) Check the destination container
84 $dContObj = $this->getContainer( $dstCont );
85 } catch ( NoSuchContainerException
$e ) {
86 $status->fatal( 'backend-fail-create', $params['dst'] );
88 } catch ( InvalidResponseException
$e ) {
89 $status->fatal( 'backend-fail-connect', $this->name
);
91 } catch ( Exception
$e ) { // some other exception?
92 $status->fatal( 'backend-fail-internal', $this->name
);
93 $this->logException( $e, __METHOD__
, $params );
97 // (b) Check if the destination object already exists
99 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
100 // NoSuchObjectException not thrown: file must exist
101 if ( empty( $params['overwriteDest'] ) ) {
102 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
105 } catch ( NoSuchObjectException
$e ) {
106 // NoSuchObjectException thrown: file does not exist
107 } catch ( InvalidResponseException
$e ) {
108 $status->fatal( 'backend-fail-connect', $this->name
);
110 } catch ( Exception
$e ) { // some other exception?
111 $status->fatal( 'backend-fail-internal', $this->name
);
112 $this->logException( $e, __METHOD__
, $params );
116 // (c) Get a SHA-1 hash of the object
117 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
119 // (d) Actually create the object
121 $obj = $dContObj->create_object( $destRel );
122 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
123 $obj->metadata
= array( 'Sha1base36' => $sha1Hash );
124 $obj->write( $params['content'] );
125 } catch ( BadContentTypeException
$e ) {
126 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
127 } catch ( InvalidResponseException
$e ) {
128 $status->fatal( 'backend-fail-connect', $this->name
);
129 } catch ( Exception
$e ) { // some other exception?
130 $status->fatal( 'backend-fail-internal', $this->name
);
131 $this->logException( $e, __METHOD__
, $params );
138 * @see FileBackend::doStoreInternal()
140 protected function doStoreInternal( array $params ) {
141 $status = Status
::newGood();
143 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
144 if ( $destRel === null ) {
145 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
149 // (a) Check the destination container
151 $dContObj = $this->getContainer( $dstCont );
152 } catch ( NoSuchContainerException
$e ) {
153 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
155 } catch ( InvalidResponseException
$e ) {
156 $status->fatal( 'backend-fail-connect', $this->name
);
158 } catch ( Exception
$e ) { // some other exception?
159 $status->fatal( 'backend-fail-internal', $this->name
);
160 $this->logException( $e, __METHOD__
, $params );
164 // (b) Check if the destination object already exists
166 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
167 // NoSuchObjectException not thrown: file must exist
168 if ( empty( $params['overwriteDest'] ) ) {
169 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
172 } catch ( NoSuchObjectException
$e ) {
173 // NoSuchObjectException thrown: file does not exist
174 } catch ( InvalidResponseException
$e ) {
175 $status->fatal( 'backend-fail-connect', $this->name
);
177 } catch ( Exception
$e ) { // some other exception?
178 $status->fatal( 'backend-fail-internal', $this->name
);
179 $this->logException( $e, __METHOD__
, $params );
183 // (c) Get a SHA-1 hash of the object
184 $sha1Hash = sha1_file( $params['src'] );
185 if ( $sha1Hash === false ) { // source doesn't exist?
186 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
189 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
191 // (d) Actually store the object
193 $obj = $dContObj->create_object( $destRel );
194 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
195 $obj->metadata
= array( 'Sha1base36' => $sha1Hash );
196 $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
197 } catch ( BadContentTypeException
$e ) {
198 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
199 } catch ( IOException
$e ) {
200 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
201 } catch ( InvalidResponseException
$e ) {
202 $status->fatal( 'backend-fail-connect', $this->name
);
203 } catch ( Exception
$e ) { // some other exception?
204 $status->fatal( 'backend-fail-internal', $this->name
);
205 $this->logException( $e, __METHOD__
, $params );
212 * @see FileBackend::doCopyInternal()
214 protected function doCopyInternal( array $params ) {
215 $status = Status
::newGood();
217 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
218 if ( $srcRel === null ) {
219 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
223 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
224 if ( $destRel === null ) {
225 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
229 // (a) Check the source and destination containers
231 $sContObj = $this->getContainer( $srcCont );
232 $dContObj = $this->getContainer( $dstCont );
233 } catch ( NoSuchContainerException
$e ) {
234 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
236 } catch ( InvalidResponseException
$e ) {
237 $status->fatal( 'backend-fail-connect', $this->name
);
239 } catch ( Exception
$e ) { // some other exception?
240 $status->fatal( 'backend-fail-internal', $this->name
);
241 $this->logException( $e, __METHOD__
, $params );
245 // (b) Check if the destination object already exists
247 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
248 // NoSuchObjectException not thrown: file must exist
249 if ( empty( $params['overwriteDest'] ) ) {
250 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
253 } catch ( NoSuchObjectException
$e ) {
254 // NoSuchObjectException thrown: file does not exist
255 } catch ( InvalidResponseException
$e ) {
256 $status->fatal( 'backend-fail-connect', $this->name
);
258 } catch ( Exception
$e ) { // some other exception?
259 $status->fatal( 'backend-fail-internal', $this->name
);
260 $this->logException( $e, __METHOD__
, $params );
264 // (c) Actually copy the file to the destination
266 $sContObj->copy_object_to( $srcRel, $dContObj, $destRel );
267 } catch ( NoSuchObjectException
$e ) { // source object does not exist
268 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
269 } catch ( InvalidResponseException
$e ) {
270 $status->fatal( 'backend-fail-connect', $this->name
);
271 } catch ( Exception
$e ) { // some other exception?
272 $status->fatal( 'backend-fail-internal', $this->name
);
273 $this->logException( $e, __METHOD__
, $params );
280 * @see FileBackend::doDeleteInternal()
282 protected function doDeleteInternal( array $params ) {
283 $status = Status
::newGood();
285 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
286 if ( $srcRel === null ) {
287 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
291 // (a) Check the source container
293 $sContObj = $this->getContainer( $srcCont );
294 } catch ( NoSuchContainerException
$e ) {
295 $status->fatal( 'backend-fail-delete', $params['src'] );
297 } catch ( InvalidResponseException
$e ) {
298 $status->fatal( 'backend-fail-connect', $this->name
);
300 } catch ( Exception
$e ) { // some other exception?
301 $status->fatal( 'backend-fail-internal', $this->name
);
302 $this->logException( $e, __METHOD__
, $params );
306 // (b) Actually delete the object
308 $sContObj->delete_object( $srcRel );
309 } catch ( NoSuchObjectException
$e ) {
310 if ( empty( $params['ignoreMissingSource'] ) ) {
311 $status->fatal( 'backend-fail-delete', $params['src'] );
313 } catch ( InvalidResponseException
$e ) {
314 $status->fatal( 'backend-fail-connect', $this->name
);
315 } catch ( Exception
$e ) { // some other exception?
316 $status->fatal( 'backend-fail-internal', $this->name
);
317 $this->logException( $e, __METHOD__
, $params );
324 * @see FileBackend::doPrepareInternal()
326 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
327 $status = Status
::newGood();
330 $this->createContainer( $fullCont );
331 } catch ( InvalidResponseException
$e ) {
332 $status->fatal( 'backend-fail-connect', $this->name
);
333 } catch ( Exception
$e ) { // some other exception?
334 $status->fatal( 'backend-fail-internal', $this->name
);
335 $this->logException( $e, __METHOD__
, $params );
342 * @see FileBackend::doSecureInternal()
344 protected function doSecureInternal( $fullCont, $dir, array $params ) {
345 $status = Status
::newGood();
346 // @TODO: restrict container from $this->swiftProxyUser
351 * @see FileBackend::doCleanInternal()
353 protected function doCleanInternal( $fullCont, $dir, array $params ) {
354 $status = Status
::newGood();
356 // (a) Check the container
358 $contObj = $this->getContainer( $fullCont, true );
359 } catch ( NoSuchContainerException
$e ) {
360 return $status; // ok, nothing to do
361 } catch ( InvalidResponseException
$e ) {
362 $status->fatal( 'backend-fail-connect', $this->name
);
364 } catch ( Exception
$e ) { // some other exception?
365 $status->fatal( 'backend-fail-internal', $this->name
);
366 $this->logException( $e, __METHOD__
, $params );
370 // (c) Delete the container if empty
371 if ( $contObj->object_count
== 0 ) {
373 $this->deleteContainer( $fullCont );
374 } catch ( NoSuchContainerException
$e ) {
375 return $status; // race?
376 } catch ( InvalidResponseException
$e ) {
377 $status->fatal( 'backend-fail-connect', $this->name
);
379 } catch ( Exception
$e ) { // some other exception?
380 $status->fatal( 'backend-fail-internal', $this->name
);
381 $this->logException( $e, __METHOD__
, $params );
390 * @see FileBackend::doFileExists()
392 protected function doGetFileStat( array $params ) {
393 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
394 if ( $srcRel === null ) {
395 return false; // invalid storage path
400 $container = $this->getContainer( $srcCont );
401 // @TODO: handle 'latest' param as "X-Newest: true"
402 $obj = $container->get_object( $srcRel );
403 // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
404 $date = DateTime
::createFromFormat( 'D, d F Y G:i:s e', $obj->last_modified
);
407 'mtime' => $date->format( 'YmdHis' ),
408 'size' => $obj->content_length
,
409 'sha1' => $obj->metadata
['Sha1base36']
411 } else { // exception will be caught below
412 throw new Exception( "Could not parse date for object {$srcRel}" );
414 } catch ( NoSuchContainerException
$e ) {
415 } catch ( NoSuchObjectException
$e ) {
416 } catch ( InvalidResponseException
$e ) {
418 } catch ( Exception
$e ) { // some other exception?
420 $this->logException( $e, __METHOD__
, $params );
427 * @see FileBackendBase::getFileContents()
429 public function getFileContents( array $params ) {
430 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
431 if ( $srcRel === null ) {
432 return false; // invalid storage path
437 $container = $this->getContainer( $srcCont );
438 $obj = $container->get_object( $srcRel );
439 $data = $obj->read( $this->headersFromParams( $params ) );
440 } catch ( NoSuchContainerException
$e ) {
441 } catch ( NoSuchObjectException
$e ) {
442 } catch ( InvalidResponseException
$e ) {
443 } catch ( Exception
$e ) { // some other exception?
444 $this->logException( $e, __METHOD__
, $params );
451 * @see FileBackend::getFileListInternal()
453 public function getFileListInternal( $fullCont, $dir, array $params ) {
454 return new SwiftFileBackendFileList( $this, $fullCont, $dir );
458 * Do not call this function outside of SwiftFileBackendFileList
460 * @param $fullCont string Resolved container name
461 * @param $dir string Resolved storage directory with no trailing slash
462 * @param $after string Storage path of file to list items after
463 * @param $limit integer Max number of items to list
466 public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
469 $container = $this->getContainer( $fullCont );
470 $files = $container->list_objects( $limit, $after, "{$dir}/" );
471 } catch ( NoSuchContainerException
$e ) {
472 } catch ( NoSuchObjectException
$e ) {
473 } catch ( InvalidResponseException
$e ) {
474 } catch ( Exception
$e ) { // some other exception?
475 $this->logException( $e, __METHOD__
, $params );
482 * @see FileBackend::doGetFileSha1base36()
484 public function doGetFileSha1base36( array $params ) {
485 $stat = $this->getFileStat( $params );
487 return $stat['sha1'];
494 * @see FileBackend::doStreamFile()
496 protected function doStreamFile( array $params ) {
497 $status = Status
::newGood();
499 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
500 if ( $srcRel === null ) {
501 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
505 $cont = $this->getContainer( $srcCont );
506 $obj = $cont->get_object( $srcRel );
507 } catch ( NoSuchContainerException
$e ) {
508 $status->fatal( 'backend-fail-stream', $params['src'] );
510 } catch ( NoSuchObjectException
$e ) {
511 $status->fatal( 'backend-fail-stream', $params['src'] );
513 } catch ( IOException
$e ) {
514 $status->fatal( 'backend-fail-stream', $params['src'] );
516 } catch ( Exception
$e ) { // some other exception?
517 $status->fatal( 'backend-fail-stream', $params['src'] );
518 $this->logException( $e, __METHOD__
, $params );
523 $output = fopen( 'php://output', 'w' );
524 $obj->stream( $output, $this->headersFromParams( $params ) );
525 } catch ( InvalidResponseException
$e ) {
526 $status->fatal( 'backend-fail-connect', $this->name
);
527 } catch ( Exception
$e ) { // some other exception?
528 $status->fatal( 'backend-fail-stream', $params['src'] );
529 $this->logException( $e, __METHOD__
, $params );
536 * @see FileBackend::getLocalCopy()
538 public function getLocalCopy( array $params ) {
539 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
540 if ( $srcRel === null ) {
544 // Get source file extension
545 $ext = FileBackend
::extensionFromPath( $srcRel );
546 // Create a new temporary file...
547 $tmpFile = TempFSFile
::factory( wfBaseName( $srcRel ) . '_', $ext );
553 $cont = $this->getContainer( $srcCont );
554 $obj = $cont->get_object( $srcRel );
555 $handle = fopen( $tmpFile->getPath(), 'w' );
557 $obj->stream( $handle, $this->headersFromParams( $params ) );
560 $tmpFile = null; // couldn't open temp file
562 } catch ( NoSuchContainerException
$e ) {
564 } catch ( NoSuchObjectException
$e ) {
566 } catch ( InvalidResponseException
$e ) {
568 } catch ( Exception
$e ) { // some other exception?
570 $this->logException( $e, __METHOD__
, $params );
577 * Get headers to send to Swift when reading a file based
578 * on a FileBackend params array, e.g. that of getLocalCopy().
579 * $params is currently only checked for a 'latest' flag.
581 * @param $params Array
584 protected function headersFromParams( array $params ) {
586 if ( !empty( $params['latest'] ) ) {
587 $hdrs[] = 'X-Newest: true';
593 * Get a connection to the Swift proxy
595 * @return CF_Connection|false
596 * @throws InvalidResponseException
598 protected function getConnection() {
599 if ( $this->conn
=== false ) {
600 return false; // failed last attempt
602 // Authenticate with proxy and get a session key.
603 // Session keys expire after a while, so we renew them periodically.
604 if ( $this->conn
=== null ||
( time() - $this->connStarted
) > $this->connTTL
) {
605 $this->connContainers
= array();
607 $this->auth
->authenticate();
608 $this->conn
= new CF_Connection( $this->auth
);
609 $this->connStarted
= time();
610 } catch ( AuthenticationException
$e ) {
611 $this->conn
= false; // don't keep re-trying
612 } catch ( InvalidResponseException
$e ) {
613 $this->conn
= false; // don't keep re-trying
616 if ( !$this->conn
) {
617 throw new InvalidResponseException
; // auth/connection problem
623 * Get a Swift container object, possibly from process cache.
624 * Use $reCache if the file count or byte count is needed.
626 * @param $container string Container name
627 * @param $reCache bool Refresh the process cache
628 * @return CF_Container
630 protected function getContainer( $container, $reCache = false ) {
631 $conn = $this->getConnection(); // Swift proxy connection
633 unset( $this->connContainers
[$container] ); // purge cache
635 if ( !isset( $this->connContainers
[$container] ) ) {
636 $contObj = $conn->get_container( $container );
637 // Exception not thrown: container must exist
638 $this->connContainers
[$container] = $contObj; // cache it
640 return $this->connContainers
[$container];
644 * Create a Swift container
646 * @param $container string Container name
647 * @return CF_Container
649 protected function createContainer( $container ) {
650 $conn = $this->getConnection(); // Swift proxy connection
651 $contObj = $conn->create_container( $container );
652 $this->connContainers
[$container] = $contObj; // cache it
657 * Delete a Swift container
659 * @param $container string Container name
662 protected function deleteContainer( $container ) {
663 $conn = $this->getConnection(); // Swift proxy connection
664 $conn->delete_container( $container );
665 unset( $this->connContainers
[$container] ); // purge cache
669 * Log an unexpected exception for this backend
671 * @param $e Exception
672 * @param $func string
673 * @param $params Array
676 protected function logException( Exception
$e, $func, array $params ) {
677 wfDebugLog( 'SwiftBackend',
678 get_class( $e ) . " in '{$this->name}': '{$func}' with " . serialize( $params )
684 * SwiftFileBackend helper class to page through object listings.
685 * Swift also has a listing limit of 10,000 objects for sanity.
686 * Do not use this class from places outside SwiftFileBackend.
688 * @ingroup FileBackend
690 class SwiftFileBackendFileList
implements Iterator
{
692 protected $bufferIter = array();
693 protected $bufferAfter = null; // string; list items *after* this path
694 protected $pos = 0; // integer
696 /** @var SwiftFileBackend */
698 protected $container; //
699 protected $dir; // string storage directory
700 protected $suffixStart; // integer
702 const PAGE_SIZE
= 5000; // file listing buffer size
705 * @param $backend SwiftFileBackend
706 * @param $fullCont string Resolved container name
707 * @param $dir string Resolved directory relative to container
709 public function __construct( SwiftFileBackend
$backend, $fullCont, $dir ) {
710 $this->backend
= $backend;
711 $this->container
= $fullCont;
713 if ( substr( $this->dir
, -1 ) === '/' ) {
714 $this->dir
= substr( $this->dir
, 0, -1 ); // remove trailing slash
716 $this->suffixStart
= strlen( $dir ) +
1; // size of "path/to/dir/"
719 public function current() {
720 return substr( current( $this->bufferIter
), $this->suffixStart
);
723 public function key() {
727 public function next() {
728 // Advance to the next file in the page
729 next( $this->bufferIter
);
731 // Check if there are no files left in this page and
732 // advance to the next page if this page was not empty.
733 if ( !$this->valid() && count( $this->bufferIter
) ) {
734 $this->bufferAfter
= end( $this->bufferIter
);
735 $this->bufferIter
= $this->backend
->getFileListPageInternal(
736 $this->container
, $this->dir
, $this->bufferAfter
, self
::PAGE_SIZE
741 public function rewind() {
743 $this->bufferAfter
= null;
744 $this->bufferIter
= $this->backend
->getFileListPageInternal(
745 $this->container
, $this->dir
, $this->bufferAfter
, self
::PAGE_SIZE
749 public function valid() {
750 return ( current( $this->bufferIter
) !== false ); // no paths can have this value