protected $authTTL; // integer seconds
protected $swiftAnonUser; // string; username to handle unauthenticated requests
protected $swiftUseCDN; // boolean; whether CloudFiles CDN is enabled
+ protected $swiftCDNExpiry; // integer; how long to cache things in the CDN
+ protected $swiftCDNPurgable; // boolean; whether object CDN purging is enabled
+
protected $maxContCacheSize = 300; // integer; max containers with entries
/** @var CF_Connection */
protected $conn; // Swift connection handle
protected $connStarted = 0; // integer UNIX timestamp
protected $connContainers = array(); // container object cache
+ protected $connException; // CloudFiles exception
/**
* @see FileBackendStore::__construct()
* swiftAuthTTL : Swift authentication TTL (seconds)
* swiftAnonUser : Swift user used for end-user requests (account:username)
* swiftUseCDN : Whether a Cloud Files Content Delivery Network is set up
+ * swiftCDNExpiry : How long (in seconds) to store content in the CDN.
+ * If files may likely change, this should probably not exceed
+ * a few days. For example, deletions may take this long to apply.
+ * If object purging is enabled, however, this is not an issue.
+ * swiftCDNPurgable : Whether object purge requests are allowed by the CDN.
* shardViaHashLevels : Map of container names to sharding config with:
* 'base' : base of hash characters, 16 or 36
* 'levels' : the number of hash levels (and digits)
$this->swiftUseCDN = isset( $config['swiftUseCDN'] )
? $config['swiftUseCDN']
: false;
+ $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] )
+ ? $config['swiftCDNExpiry']
+ : 3600; // hour
+ $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] )
+ ? $config['swiftCDNPurgable']
+ : true;
// Cache container info to mask latency
$this->memCache = wfGetMainCache();
}
) );
}
if ( $this->swiftUseCDN ) { // Rackspace style CDN
- $contObj->make_public();
+ $contObj->make_public( $this->swiftCDNExpiry );
}
} catch ( CDNNotEnabledException $e ) {
// CDN not enabled; nothing to see here
$status = Status::newGood();
$scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
if ( $status->isOK() ) {
- $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) );
+ # Do not stat the file in getLocalCopy() to avoid infinite loops
+ $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1, 'nostat' => 1 ) );
if ( $tmpFile ) {
$hash = $tmpFile->getSha1Base36();
if ( $hash !== false ) {
*/
public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
$dirs = array();
+ if ( $after === INF ) {
+ return $dirs; // nothing more
+ }
+ wfProfileIn( __METHOD__ . '-' . $this->name );
try {
$container = $this->getContainer( $fullCont );
if ( substr( $object, -1 ) === '/' ) {
$dirs[] = $object; // directories end in '/'
}
- $after = $object; // update last item
}
// Recursive: list all dirs under $dir and its subdirs
} else {
}
$lastDir = $objectDir;
}
- $after = $object; // update last item
}
}
+ if ( count( $objects ) < $limit ) {
+ $after = INF; // avoid a second RTT
+ } else {
+ $after = end( $objects ); // update last item
+ }
} catch ( NoSuchContainerException $e ) {
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, null, __METHOD__,
array( 'cont' => $fullCont, 'dir' => $dir ) );
}
+ wfProfileOut( __METHOD__ . '-' . $this->name );
return $dirs;
}
*/
public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
$files = array();
+ if ( $after === INF ) {
+ return $files; // nothing more
+ }
+ wfProfileIn( __METHOD__ . '-' . $this->name );
try {
$container = $this->getContainer( $fullCont );
}
// Recursive: list all files under $dir and its subdirs
} else { // files
- $files = $container->list_objects( $limit, $after, $prefix );
+ $objects = $container->list_objects( $limit, $after, $prefix );
+ $files = $objects;
+ }
+ if ( count( $objects ) < $limit ) {
+ $after = INF; // avoid a second RTT
+ } else {
+ $after = end( $objects ); // update last item
}
- $after = end( $files ); // update last item
- reset( $files ); // reset pointer
} catch ( NoSuchContainerException $e ) {
} catch ( CloudFilesException $e ) { // some other exception?
$this->handleException( $e, null, __METHOD__,
array( 'cont' => $fullCont, 'dir' => $dir ) );
}
+ wfProfileOut( __METHOD__ . '-' . $this->name );
return $files;
}
return null;
}
- if ( !$this->fileExists( $params ) ) {
+ # Check the recursion guard to avoid loops when filling metadata
+ if ( empty( $params['nostat'] ) && !$this->fileExists( $params ) ) {
return null;
}
$statuses[$index] = $status;
}
- foreach ( $fileOpHandles as $fileOpHandle ) {
- $fileOpHandle->closeResources();
- }
-
return $statuses;
}
}
/**
- * Purge the CDN cache of affected objects if CDN caching is enabled
+ * Purge the CDN cache of affected objects if CDN caching is enabled.
+ * This is for Rackspace/Akamai CDNs.
*
* @param $objects Array List of CF_Object items
* @return void
*/
public function purgeCDNCache( array $objects ) {
- if ( $this->swiftUseCDN ) { // Rackspace style CDN
+ if ( $this->swiftUseCDN && $this->swiftCDNPurgable ) {
foreach ( $objects as $object ) {
try {
$object->purge_from_cdn();
* Get a connection to the Swift proxy
*
* @return CF_Connection|bool False on failure
- * @throws InvalidResponseException
+ * @throws CloudFilesException
*/
protected function getConnection() {
- if ( $this->conn === false ) {
- throw new InvalidResponseException; // failed last attempt
+ if ( $this->connException instanceof Exception ) {
+ throw $this->connException; // failed last attempt
}
// Session keys expire after a while, so we renew them periodically
if ( $this->conn && ( time() - $this->connStarted ) > $this->authTTL ) {
$this->conn = null;
}
// Authenticate with proxy and get a session key...
- if ( $this->conn === null ) {
+ if ( !$this->conn ) {
+ $this->connStarted = 0;
$this->connContainers = array();
try {
$this->auth->authenticate();
$this->conn = new CF_Connection( $this->auth );
$this->connStarted = time();
- } catch ( AuthenticationException $e ) {
- $this->conn = false; // don't keep re-trying
- } catch ( InvalidResponseException $e ) {
- $this->conn = false; // don't keep re-trying
+ } catch ( CloudFilesException $e ) {
+ $this->connException = $e; // don't keep re-trying
+ throw $e; // throw it back
}
}
- if ( !$this->conn ) {
- throw new InvalidResponseException; // auth/connection problem
- }
return $this->conn;
}
* @param $container string Container name
* @param $bypassCache bool Bypass all caches and load from Swift
* @return CF_Container
- * @throws NoSuchContainerException
- * @throws InvalidResponseException
+ * @throws CloudFilesException
*/
protected function getContainer( $container, $bypassCache = false ) {
$conn = $this->getConnection(); // Swift proxy connection
*
* @param $container string Container name
* @return CF_Container
- * @throws InvalidResponseException
+ * @throws CloudFilesException
*/
protected function createContainer( $container ) {
$conn = $this->getConnection(); // Swift proxy connection
*
* @param $container string Container name
* @return void
- * @throws InvalidResponseException
+ * @throws CloudFilesException
*/
protected function deleteContainer( $container ) {
$conn = $this->getConnection(); // Swift proxy connection
- $conn->delete_container( $container );
unset( $this->connContainers[$container] ); // purge cache
+ $conn->delete_container( $container );
}
/**