10 * Class for an OpenStack Swift based file backend.
12 * This requires the SwiftCloudFiles MediaWiki extension, which includes
13 * the php-cloudfiles library (https://github.com/rackspace/php-cloudfiles).
14 * php-cloudfiles requires the curl, fileinfo, and mb_string PHP extensions.
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 FileBackendStore
{
23 /** @var CF_Authentication */
24 protected $auth; // Swift authentication handler
25 protected $authTTL; // integer seconds
26 protected $swiftAnonUser; // string; username to handle unauthenticated requests
27 protected $maxContCacheSize = 20; // integer; max containers with entries
29 /** @var CF_Connection */
30 protected $conn; // Swift connection handle
31 protected $connStarted = 0; // integer UNIX timestamp
32 protected $connContainers = array(); // container object cache
35 * @see FileBackendStore::__construct()
36 * Additional $config params include:
37 * swiftAuthUrl : Swift authentication server URL
38 * swiftUser : Swift user used by MediaWiki (account:username)
39 * swiftKey : Swift authentication key for the above user
40 * swiftAuthTTL : Swift authentication TTL (seconds)
41 * swiftAnonUser : Swift user used for end-user requests (account:username)
42 * shardViaHashLevels : Map of container names to the number of hash levels
44 public function __construct( array $config ) {
45 parent
::__construct( $config );
47 $this->auth
= new CF_Authentication(
50 null, // account; unused
51 $config['swiftAuthUrl']
54 $this->authTTL
= isset( $config['swiftAuthTTL'] )
55 ?
$config['swiftAuthTTL']
56 : 120; // some sane number
57 $this->swiftAnonUser
= isset( $config['swiftAnonUser'] )
58 ?
$config['swiftAnonUser']
60 $this->shardViaHashLevels
= isset( $config['shardViaHashLevels'] )
61 ?
$config['shardViaHashLevels']
66 * @see FileBackendStore::resolveContainerPath()
68 protected function resolveContainerPath( $container, $relStoragePath ) {
69 if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
70 return null; // too long for Swift
72 return $relStoragePath;
76 * @see FileBackendStore::isPathUsableInternal()
78 public function isPathUsableInternal( $storagePath ) {
79 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
80 if ( $rel === null ) {
81 return false; // invalid
85 $this->getContainer( $container );
86 return true; // container exists
87 } catch ( NoSuchContainerException
$e ) {
88 } catch ( InvalidResponseException
$e ) {
89 } catch ( Exception
$e ) { // some other exception?
90 $this->logException( $e, __METHOD__
, array( 'path' => $storagePath ) );
97 * @see FileBackendStore::doCopyInternal()
99 protected function doCreateInternal( array $params ) {
100 $status = Status
::newGood();
102 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
103 if ( $dstRel === null ) {
104 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
108 // (a) Check the destination container and object
110 $dContObj = $this->getContainer( $dstCont );
111 if ( empty( $params['overwrite'] ) &&
112 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
114 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
117 } catch ( NoSuchContainerException
$e ) {
118 $status->fatal( 'backend-fail-create', $params['dst'] );
120 } catch ( InvalidResponseException
$e ) {
121 $status->fatal( 'backend-fail-connect', $this->name
);
123 } catch ( Exception
$e ) { // some other exception?
124 $status->fatal( 'backend-fail-internal', $this->name
);
125 $this->logException( $e, __METHOD__
, $params );
129 // (b) Get a SHA-1 hash of the object
130 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
132 // (c) Actually create the object
134 // Create a fresh CF_Object with no fields preloaded.
135 // We don't want to preserve headers, metadata, and such.
136 $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
137 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
138 $obj->metadata
= array( 'Sha1base36' => $sha1Hash );
139 // Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59).
140 // The MD5 here will be checked within Swift against its own MD5.
141 $obj->set_etag( md5( $params['content'] ) );
142 // Use the same content type as StreamFile for security
143 $obj->content_type
= StreamFile
::contentTypeFromPath( $params['dst'] );
144 // Actually write the object in Swift
145 $obj->write( $params['content'] );
146 } catch ( BadContentTypeException
$e ) {
147 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
148 } catch ( InvalidResponseException
$e ) {
149 $status->fatal( 'backend-fail-connect', $this->name
);
150 } catch ( Exception
$e ) { // some other exception?
151 $status->fatal( 'backend-fail-internal', $this->name
);
152 $this->logException( $e, __METHOD__
, $params );
159 * @see FileBackendStore::doStoreInternal()
161 protected function doStoreInternal( array $params ) {
162 $status = Status
::newGood();
164 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
165 if ( $dstRel === null ) {
166 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
170 // (a) Check the destination container and object
172 $dContObj = $this->getContainer( $dstCont );
173 if ( empty( $params['overwrite'] ) &&
174 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
176 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
179 } catch ( NoSuchContainerException
$e ) {
180 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
182 } catch ( InvalidResponseException
$e ) {
183 $status->fatal( 'backend-fail-connect', $this->name
);
185 } catch ( Exception
$e ) { // some other exception?
186 $status->fatal( 'backend-fail-internal', $this->name
);
187 $this->logException( $e, __METHOD__
, $params );
191 // (b) Get a SHA-1 hash of the object
192 $sha1Hash = sha1_file( $params['src'] );
193 if ( $sha1Hash === false ) { // source doesn't exist?
194 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
197 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
199 // (c) Actually store the object
201 // Create a fresh CF_Object with no fields preloaded.
202 // We don't want to preserve headers, metadata, and such.
203 $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
204 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
205 $obj->metadata
= array( 'Sha1base36' => $sha1Hash );
206 // The MD5 here will be checked within Swift against its own MD5.
207 $obj->set_etag( md5_file( $params['src'] ) );
208 // Use the same content type as StreamFile for security
209 $obj->content_type
= StreamFile
::contentTypeFromPath( $params['dst'] );
210 // Actually write the object in Swift
211 $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
212 } catch ( BadContentTypeException
$e ) {
213 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
214 } catch ( IOException
$e ) {
215 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
216 } catch ( InvalidResponseException
$e ) {
217 $status->fatal( 'backend-fail-connect', $this->name
);
218 } catch ( Exception
$e ) { // some other exception?
219 $status->fatal( 'backend-fail-internal', $this->name
);
220 $this->logException( $e, __METHOD__
, $params );
227 * @see FileBackendStore::doCopyInternal()
229 protected function doCopyInternal( array $params ) {
230 $status = Status
::newGood();
232 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
233 if ( $srcRel === null ) {
234 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
238 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
239 if ( $dstRel === null ) {
240 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
244 // (a) Check the source/destination containers and destination object
246 $sContObj = $this->getContainer( $srcCont );
247 $dContObj = $this->getContainer( $dstCont );
248 if ( empty( $params['overwrite'] ) &&
249 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
251 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
254 } catch ( NoSuchContainerException
$e ) {
255 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
257 } catch ( InvalidResponseException
$e ) {
258 $status->fatal( 'backend-fail-connect', $this->name
);
260 } catch ( Exception
$e ) { // some other exception?
261 $status->fatal( 'backend-fail-internal', $this->name
);
262 $this->logException( $e, __METHOD__
, $params );
266 // (b) Actually copy the file to the destination
268 $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel );
269 } catch ( NoSuchObjectException
$e ) { // source object does not exist
270 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
271 } catch ( InvalidResponseException
$e ) {
272 $status->fatal( 'backend-fail-connect', $this->name
);
273 } catch ( Exception
$e ) { // some other exception?
274 $status->fatal( 'backend-fail-internal', $this->name
);
275 $this->logException( $e, __METHOD__
, $params );
282 * @see FileBackendStore::doDeleteInternal()
284 protected function doDeleteInternal( array $params ) {
285 $status = Status
::newGood();
287 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
288 if ( $srcRel === null ) {
289 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
294 $sContObj = $this->getContainer( $srcCont );
295 $sContObj->delete_object( $srcRel );
296 } catch ( NoSuchContainerException
$e ) {
297 $status->fatal( 'backend-fail-delete', $params['src'] );
298 } catch ( NoSuchObjectException
$e ) {
299 if ( empty( $params['ignoreMissingSource'] ) ) {
300 $status->fatal( 'backend-fail-delete', $params['src'] );
302 } catch ( InvalidResponseException
$e ) {
303 $status->fatal( 'backend-fail-connect', $this->name
);
304 } catch ( Exception
$e ) { // some other exception?
305 $status->fatal( 'backend-fail-internal', $this->name
);
306 $this->logException( $e, __METHOD__
, $params );
313 * @see FileBackendStore::doPrepareInternal()
315 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
316 $status = Status
::newGood();
318 // (a) Check if container already exists
320 $contObj = $this->getContainer( $fullCont );
321 // NoSuchContainerException not thrown: container must exist
322 return $status; // already exists
323 } catch ( NoSuchContainerException
$e ) {
324 // NoSuchContainerException thrown: container does not exist
325 } catch ( InvalidResponseException
$e ) {
326 $status->fatal( 'backend-fail-connect', $this->name
);
328 } catch ( Exception
$e ) { // some other exception?
329 $status->fatal( 'backend-fail-internal', $this->name
);
330 $this->logException( $e, __METHOD__
, $params );
334 // (b) Create container as needed
336 $contObj = $this->createContainer( $fullCont );
337 if ( $this->swiftAnonUser
!= '' ) {
338 // Make container public to end-users...
339 $status->merge( $this->setContainerAccess(
341 array( $this->auth
->username
, $this->swiftAnonUser
), // read
342 array( $this->auth
->username
) // write
345 } catch ( InvalidResponseException
$e ) {
346 $status->fatal( 'backend-fail-connect', $this->name
);
348 } catch ( Exception
$e ) { // some other exception?
349 $status->fatal( 'backend-fail-internal', $this->name
);
350 $this->logException( $e, __METHOD__
, $params );
358 * @see FileBackendStore::doSecureInternal()
360 protected function doSecureInternal( $fullCont, $dir, array $params ) {
361 $status = Status
::newGood();
363 if ( $this->swiftAnonUser
!= '' ) {
364 // Restrict container from end-users...
366 // doPrepareInternal() should have been called,
367 // so the Swift container should already exist...
368 $contObj = $this->getContainer( $fullCont ); // normally a cache hit
369 // NoSuchContainerException not thrown: container must exist
370 if ( !isset( $contObj->mw_wasSecured
) ) {
371 $status->merge( $this->setContainerAccess(
373 array( $this->auth
->username
), // read
374 array( $this->auth
->username
) // write
376 // @TODO: when php-cloudfiles supports container
377 // metadata, we can make use of that to avoid RTTs
378 $contObj->mw_wasSecured
= true; // avoid useless RTTs
380 } catch ( InvalidResponseException
$e ) {
381 $status->fatal( 'backend-fail-connect', $this->name
);
382 } catch ( Exception
$e ) { // some other exception?
383 $status->fatal( 'backend-fail-internal', $this->name
);
384 $this->logException( $e, __METHOD__
, $params );
392 * @see FileBackendStore::doCleanInternal()
394 protected function doCleanInternal( $fullCont, $dir, array $params ) {
395 $status = Status
::newGood();
397 // Only containers themselves can be removed, all else is virtual
399 return $status; // nothing to do
402 // (a) Check the container
404 $contObj = $this->getContainer( $fullCont, true );
405 } catch ( NoSuchContainerException
$e ) {
406 return $status; // ok, nothing to do
407 } catch ( InvalidResponseException
$e ) {
408 $status->fatal( 'backend-fail-connect', $this->name
);
410 } catch ( Exception
$e ) { // some other exception?
411 $status->fatal( 'backend-fail-internal', $this->name
);
412 $this->logException( $e, __METHOD__
, $params );
416 // (b) Delete the container if empty
417 if ( $contObj->object_count
== 0 ) {
419 $this->deleteContainer( $fullCont );
420 } catch ( NoSuchContainerException
$e ) {
421 return $status; // race?
422 } catch ( InvalidResponseException
$e ) {
423 $status->fatal( 'backend-fail-connect', $this->name
);
425 } catch ( Exception
$e ) { // some other exception?
426 $status->fatal( 'backend-fail-internal', $this->name
);
427 $this->logException( $e, __METHOD__
, $params );
436 * @see FileBackendStore::doFileExists()
438 protected function doGetFileStat( array $params ) {
439 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
440 if ( $srcRel === null ) {
441 return false; // invalid storage path
446 $contObj = $this->getContainer( $srcCont );
447 $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) );
448 $this->addMissingMetadata( $srcObj, $params['src'] );
450 // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
451 'mtime' => wfTimestamp( TS_MW
, $srcObj->last_modified
),
452 'size' => $srcObj->content_length
,
453 'sha1' => $srcObj->metadata
['Sha1base36']
455 } catch ( NoSuchContainerException
$e ) {
456 } catch ( NoSuchObjectException
$e ) {
457 } catch ( InvalidResponseException
$e ) {
459 } catch ( Exception
$e ) { // some other exception?
461 $this->logException( $e, __METHOD__
, $params );
468 * Fill in any missing object metadata and save it to Swift
470 * @param $obj CF_Object
471 * @param $path string Storage path to object
472 * @return bool Success
473 * @throws Exception cloudfiles exceptions
475 protected function addMissingMetadata( CF_Object
$obj, $path ) {
476 if ( isset( $obj->metadata
['Sha1base36'] ) ) {
477 return true; // nothing to do
479 $status = Status
::newGood();
480 $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager
::LOCK_UW
, $status );
481 if ( $status->isOK() ) {
482 $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) );
484 $hash = $tmpFile->getSha1Base36();
485 if ( $hash !== false ) {
486 $obj->metadata
['Sha1base36'] = $hash;
487 $obj->sync_metadata(); // save to Swift
488 return true; // success
492 $obj->metadata
['Sha1base36'] = false;
493 return false; // failed
497 * @see FileBackend::getFileContents()
499 public function getFileContents( array $params ) {
500 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
501 if ( $srcRel === null ) {
502 return false; // invalid storage path
505 if ( !$this->fileExists( $params ) ) {
511 $sContObj = $this->getContainer( $srcCont );
512 $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD request
513 $data = $obj->read( $this->headersFromParams( $params ) );
514 } catch ( NoSuchContainerException
$e ) {
515 } catch ( InvalidResponseException
$e ) {
516 } catch ( Exception
$e ) { // some other exception?
517 $this->logException( $e, __METHOD__
, $params );
524 * @see FileBackendStore::getFileListInternal()
526 public function getFileListInternal( $fullCont, $dir, array $params ) {
527 return new SwiftFileBackendFileList( $this, $fullCont, $dir );
531 * Do not call this function outside of SwiftFileBackendFileList
533 * @param $fullCont string Resolved container name
534 * @param $dir string Resolved storage directory with no trailing slash
535 * @param $after string Storage path of file to list items after
536 * @param $limit integer Max number of items to list
539 public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
543 $container = $this->getContainer( $fullCont );
544 $prefix = ( $dir == '' ) ?
null : "{$dir}/";
545 $files = $container->list_objects( $limit, $after, $prefix );
546 } catch ( NoSuchContainerException
$e ) {
547 } catch ( NoSuchObjectException
$e ) {
548 } catch ( InvalidResponseException
$e ) {
549 } catch ( Exception
$e ) { // some other exception?
550 $this->logException( $e, __METHOD__
, array( 'cont' => $fullCont, 'dir' => $dir ) );
557 * @see FileBackendStore::doGetFileSha1base36()
559 public function doGetFileSha1base36( array $params ) {
560 $stat = $this->getFileStat( $params );
562 return $stat['sha1'];
569 * @see FileBackendStore::doStreamFile()
571 protected function doStreamFile( array $params ) {
572 $status = Status
::newGood();
574 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
575 if ( $srcRel === null ) {
576 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
580 $cont = $this->getContainer( $srcCont );
581 } catch ( NoSuchContainerException
$e ) {
582 $status->fatal( 'backend-fail-stream', $params['src'] );
584 } catch ( InvalidResponseException
$e ) {
585 $status->fatal( 'backend-fail-connect', $this->name
);
587 } catch ( Exception
$e ) { // some other exception?
588 $status->fatal( 'backend-fail-stream', $params['src'] );
589 $this->logException( $e, __METHOD__
, $params );
594 $output = fopen( 'php://output', 'w' );
595 $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD request
596 $obj->stream( $output, $this->headersFromParams( $params ) );
597 } catch ( InvalidResponseException
$e ) { // 404? connection problem?
598 $status->fatal( 'backend-fail-stream', $params['src'] );
599 } catch ( Exception
$e ) { // some other exception?
600 $status->fatal( 'backend-fail-stream', $params['src'] );
601 $this->logException( $e, __METHOD__
, $params );
608 * @see FileBackendStore::getLocalCopy()
610 public function getLocalCopy( array $params ) {
611 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
612 if ( $srcRel === null ) {
616 if ( !$this->fileExists( $params ) ) {
622 $sContObj = $this->getContainer( $srcCont );
623 $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
624 // Get source file extension
625 $ext = FileBackend
::extensionFromPath( $srcRel );
626 // Create a new temporary file...
627 $tmpFile = TempFSFile
::factory( wfBaseName( $srcRel ) . '_', $ext );
629 $handle = fopen( $tmpFile->getPath(), 'wb' );
631 $obj->stream( $handle, $this->headersFromParams( $params ) );
634 $tmpFile = null; // couldn't open temp file
637 } catch ( NoSuchContainerException
$e ) {
639 } catch ( InvalidResponseException
$e ) {
641 } catch ( Exception
$e ) { // some other exception?
643 $this->logException( $e, __METHOD__
, $params );
650 * Get headers to send to Swift when reading a file based
651 * on a FileBackend params array, e.g. that of getLocalCopy().
652 * $params is currently only checked for a 'latest' flag.
654 * @param $params Array
657 protected function headersFromParams( array $params ) {
659 if ( !empty( $params['latest'] ) ) {
660 $hdrs[] = 'X-Newest: true';
666 * Set read/write permissions for a Swift container
668 * @param $contObj CF_Container Swift container
669 * @param $readGrps Array Swift users who can read (account:user)
670 * @param $writeGrps Array Swift users who can write (account:user)
673 protected function setContainerAccess(
674 CF_Container
$contObj, array $readGrps, array $writeGrps
676 $creds = $contObj->cfs_auth
->export_credentials();
678 $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name
);
680 // Note: 10 second timeout consistent with php-cloudfiles
681 $req = new CurlHttpRequest( $url, array( 'method' => 'POST', 'timeout' => 10 ) );
682 $req->setHeader( 'X-Auth-Token', $creds['auth_token'] );
683 $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) );
684 $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) );
686 return $req->execute(); // should return 204
690 * Get a connection to the Swift proxy
692 * @return CF_Connection|false
693 * @throws InvalidResponseException
695 protected function getConnection() {
696 if ( $this->conn
=== false ) {
697 throw new InvalidResponseException
; // failed last attempt
699 // Session keys expire after a while, so we renew them periodically
700 if ( $this->conn
&& ( time() - $this->connStarted
) > $this->authTTL
) {
701 $this->conn
->close(); // close active cURL connections
704 // Authenticate with proxy and get a session key...
705 if ( $this->conn
=== null ) {
706 $this->connContainers
= array();
708 $this->auth
->authenticate();
709 $this->conn
= new CF_Connection( $this->auth
);
710 $this->connStarted
= time();
711 } catch ( AuthenticationException
$e ) {
712 $this->conn
= false; // don't keep re-trying
713 } catch ( InvalidResponseException
$e ) {
714 $this->conn
= false; // don't keep re-trying
717 if ( !$this->conn
) {
718 throw new InvalidResponseException
; // auth/connection problem
724 * @see FileBackendStore::doClearCache()
726 protected function doClearCache( array $paths = null ) {
727 $this->connContainers
= array(); // clear container object cache
731 * Get a Swift container object, possibly from process cache.
732 * Use $reCache if the file count or byte count is needed.
734 * @param $container string Container name
735 * @param $reCache bool Refresh the process cache
736 * @return CF_Container
738 protected function getContainer( $container, $reCache = false ) {
739 $conn = $this->getConnection(); // Swift proxy connection
741 unset( $this->connContainers
[$container] ); // purge cache
743 if ( !isset( $this->connContainers
[$container] ) ) {
744 $contObj = $conn->get_container( $container );
745 // NoSuchContainerException not thrown: container must exist
746 if ( count( $this->connContainers
) >= $this->maxContCacheSize
) { // trim cache?
747 reset( $this->connContainers
);
748 $key = key( $this->connContainers
);
749 unset( $this->connContainers
[$key] );
751 $this->connContainers
[$container] = $contObj; // cache it
753 return $this->connContainers
[$container];
757 * Create a Swift container
759 * @param $container string Container name
760 * @return CF_Container
762 protected function createContainer( $container ) {
763 $conn = $this->getConnection(); // Swift proxy connection
764 $contObj = $conn->create_container( $container );
765 $this->connContainers
[$container] = $contObj; // cache it
770 * Delete a Swift container
772 * @param $container string Container name
775 protected function deleteContainer( $container ) {
776 $conn = $this->getConnection(); // Swift proxy connection
777 $conn->delete_container( $container );
778 unset( $this->connContainers
[$container] ); // purge cache
782 * Log an unexpected exception for this backend
784 * @param $e Exception
785 * @param $func string
786 * @param $params Array
789 protected function logException( Exception
$e, $func, array $params ) {
790 wfDebugLog( 'SwiftBackend',
791 get_class( $e ) . " in '{$this->name}': '{$func}' with " . serialize( $params )
797 * SwiftFileBackend helper class to page through object listings.
798 * Swift also has a listing limit of 10,000 objects for sanity.
799 * Do not use this class from places outside SwiftFileBackend.
801 * @ingroup FileBackend
803 class SwiftFileBackendFileList
implements Iterator
{
805 protected $bufferIter = array();
806 protected $bufferAfter = null; // string; list items *after* this path
807 protected $pos = 0; // integer
809 /** @var SwiftFileBackend */
811 protected $container; //
812 protected $dir; // string storage directory
813 protected $suffixStart; // integer
815 const PAGE_SIZE
= 5000; // file listing buffer size
818 * @param $backend SwiftFileBackend
819 * @param $fullCont string Resolved container name
820 * @param $dir string Resolved directory relative to container
822 public function __construct( SwiftFileBackend
$backend, $fullCont, $dir ) {
823 $this->backend
= $backend;
824 $this->container
= $fullCont;
826 if ( substr( $this->dir
, -1 ) === '/' ) {
827 $this->dir
= substr( $this->dir
, 0, -1 ); // remove trailing slash
829 if ( $this->dir
== '' ) { // whole container
830 $this->suffixStart
= 0;
831 } else { // dir within container
832 $this->suffixStart
= strlen( $this->dir
) +
1; // size of "path/to/dir/"
836 public function current() {
837 return substr( current( $this->bufferIter
), $this->suffixStart
);
840 public function key() {
844 public function next() {
845 // Advance to the next file in the page
846 next( $this->bufferIter
);
848 // Check if there are no files left in this page and
849 // advance to the next page if this page was not empty.
850 if ( !$this->valid() && count( $this->bufferIter
) ) {
851 $this->bufferAfter
= end( $this->bufferIter
);
852 $this->bufferIter
= $this->backend
->getFileListPageInternal(
853 $this->container
, $this->dir
, $this->bufferAfter
, self
::PAGE_SIZE
858 public function rewind() {
860 $this->bufferAfter
= null;
861 $this->bufferIter
= $this->backend
->getFileListPageInternal(
862 $this->container
, $this->dir
, $this->bufferAfter
, self
::PAGE_SIZE
866 public function valid() {
867 return ( current( $this->bufferIter
) !== false ); // no paths can have this value