fix some spacing
[lhc/web/wiklou.git] / includes / filebackend / FileBackendStore.php
index 7e91949..35384c7 100644 (file)
@@ -48,6 +48,8 @@ abstract class FileBackendStore extends FileBackend {
 
        protected $maxFileSize = 4294967296; // integer bytes (4GiB)
 
+       const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
+
        /**
         * @see FileBackend::__construct()
         *
@@ -55,8 +57,8 @@ abstract class FileBackendStore extends FileBackend {
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
-               $this->memCache       = new EmptyBagOStuff(); // disabled by default
-               $this->cheapCache     = new ProcessCacheLRU( 300 );
+               $this->memCache = new EmptyBagOStuff(); // disabled by default
+               $this->cheapCache = new ProcessCacheLRU( 300 );
                $this->expensiveCache = new ProcessCacheLRU( 5 );
        }
 
@@ -88,12 +90,15 @@ abstract class FileBackendStore extends FileBackend {
         * Do not call this function from places outside FileBackend and FileOp.
         *
         * $params include:
-        *   - content       : the raw file contents
-        *   - dst           : destination storage path
-        *   - disposition   : Content-Disposition header value for the destination
-        *   - async         : Status will be returned immediately if supported.
-        *                     If the status is OK, then its value field will be
-        *                     set to a FileBackendStoreOpHandle object.
+        *   - content     : the raw file contents
+        *   - dst         : destination storage path
+        *   - disposition : Content-Disposition header value for the destination
+        *   - headers     : HTTP header name/value map
+        *   - async       : Status will be returned immediately if supported.
+        *                   If the status is OK, then its value field will be
+        *                   set to a FileBackendStoreOpHandle object.
+        *   - dstExists   : Whether a file exists at the destination (optimization).
+        *                   Callers can use "false" if no existing file is being changed.
         *
         * @param $params Array
         * @return Status
@@ -107,7 +112,9 @@ abstract class FileBackendStore extends FileBackend {
                } else {
                        $status = $this->doCreateInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
-                       $this->deleteFileCache( $params['dst'] ); // persistent cache
+                       if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                               $this->deleteFileCache( $params['dst'] ); // persistent cache
+                       }
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -116,6 +123,7 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * @see FileBackendStore::createInternal()
+        * @return Status
         */
        abstract protected function doCreateInternal( array $params );
 
@@ -125,12 +133,15 @@ abstract class FileBackendStore extends FileBackend {
         * Do not call this function from places outside FileBackend and FileOp.
         *
         * $params include:
-        *   - src           : source path on disk
-        *   - dst           : destination storage path
-        *   - disposition   : Content-Disposition header value for the destination
-        *   - async         : Status will be returned immediately if supported.
-        *                     If the status is OK, then its value field will be
-        *                     set to a FileBackendStoreOpHandle object.
+        *   - src         : source path on disk
+        *   - dst         : destination storage path
+        *   - disposition : Content-Disposition header value for the destination
+        *   - headers     : HTTP header name/value map
+        *   - async       : Status will be returned immediately if supported.
+        *                   If the status is OK, then its value field will be
+        *                   set to a FileBackendStoreOpHandle object.
+        *   - dstExists   : Whether a file exists at the destination (optimization).
+        *                   Callers can use "false" if no existing file is being changed.
         *
         * @param $params Array
         * @return Status
@@ -144,7 +155,9 @@ abstract class FileBackendStore extends FileBackend {
                } else {
                        $status = $this->doStoreInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
-                       $this->deleteFileCache( $params['dst'] ); // persistent cache
+                       if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                               $this->deleteFileCache( $params['dst'] ); // persistent cache
+                       }
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -153,6 +166,7 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * @see FileBackendStore::storeInternal()
+        * @return Status
         */
        abstract protected function doStoreInternal( array $params );
 
@@ -169,6 +183,8 @@ abstract class FileBackendStore extends FileBackend {
         *   - async               : Status will be returned immediately if supported.
         *                           If the status is OK, then its value field will be
         *                           set to a FileBackendStoreOpHandle object.
+        *   - dstExists           : Whether a file exists at the destination (optimization).
+        *                           Callers can use "false" if no existing file is being changed.
         *
         * @param $params Array
         * @return Status
@@ -178,7 +194,9 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doCopyInternal( $params );
                $this->clearCache( array( $params['dst'] ) );
-               $this->deleteFileCache( $params['dst'] ); // persistent cache
+               if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
+               }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -186,6 +204,7 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * @see FileBackendStore::copyInternal()
+        * @return Status
         */
        abstract protected function doCopyInternal( array $params );
 
@@ -216,6 +235,7 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * @see FileBackendStore::deleteInternal()
+        * @return Status
         */
        abstract protected function doDeleteInternal( array $params );
 
@@ -232,6 +252,8 @@ abstract class FileBackendStore extends FileBackend {
         *   - async               : Status will be returned immediately if supported.
         *                           If the status is OK, then its value field will be
         *                           set to a FileBackendStoreOpHandle object.
+        *   - dstExists           : Whether a file exists at the destination (optimization).
+        *                           Callers can use "false" if no existing file is being changed.
         *
         * @param $params Array
         * @return Status
@@ -242,7 +264,9 @@ abstract class FileBackendStore extends FileBackend {
                $status = $this->doMoveInternal( $params );
                $this->clearCache( array( $params['src'], $params['dst'] ) );
                $this->deleteFileCache( $params['src'] ); // persistent cache
-               $this->deleteFileCache( $params['dst'] ); // persistent cache
+               if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
+               }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -264,6 +288,40 @@ abstract class FileBackendStore extends FileBackend {
                return $status;
        }
 
+       /**
+        * Alter metadata for a file at the storage path.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * $params include:
+        *   - src           : source storage path
+        *   - disposition   : Content-Disposition header value for the destination
+        *   - headers       : HTTP header name/value map
+        *   - async         : Status will be returned immediately if supported.
+        *                     If the status is OK, then its value field will be
+        *                     set to a FileBackendStoreOpHandle object.
+        *
+        * @param $params Array
+        * @return Status
+        */
+       final public function describeInternal( array $params ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+               $status = $this->doDescribeInternal( $params );
+               $this->clearCache( array( $params['src'] ) );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
+               return $status;
+       }
+
+       /**
+        * @see FileBackendStore::describeInternal()
+        * @return Status
+        */
+       protected function doDescribeInternal( array $params ) {
+               return Status::newGood();
+       }
+
        /**
         * No-op file operation that does nothing.
         * Do not call this function from places outside FileBackend and FileOp.
@@ -505,6 +563,7 @@ abstract class FileBackendStore extends FileBackend {
                                        $subDir = $params['dir'] . "/{$subDirRel}"; // full path
                                        $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
                                }
+                               unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
                        }
                }
 
@@ -601,17 +660,25 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( !$this->cheapCache->has( $path, 'stat' ) ) {
+               if ( !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
                        $this->primeFileCache( array( $path ) ); // check persistent cache
                }
-               if ( $this->cheapCache->has( $path, 'stat' ) ) {
+               if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
                        $stat = $this->cheapCache->get( $path, 'stat' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $stat['latest'] ) {
-                               wfProfileOut( __METHOD__ . '-' . $this->name );
-                               wfProfileOut( __METHOD__ );
-                               return $stat;
+                       if ( is_array( $stat ) ) {
+                               if ( !$latest || $stat['latest'] ) {
+                                       wfProfileOut( __METHOD__ . '-' . $this->name );
+                                       wfProfileOut( __METHOD__ );
+                                       return $stat;
+                               }
+                       } elseif ( in_array( $stat, array( 'NOT_EXIST', 'NOT_EXIST_LATEST' ) ) ) {
+                               if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
+                                       wfProfileOut( __METHOD__ . '-' . $this->name );
+                                       wfProfileOut( __METHOD__ );
+                                       return false;
+                               }
                        }
                }
                wfProfileIn( __METHOD__ . '-miss' );
@@ -619,7 +686,7 @@ abstract class FileBackendStore extends FileBackend {
                $stat = $this->doGetFileStat( $params );
                wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
-               if ( is_array( $stat ) ) { // don't cache negatives
+               if ( is_array( $stat ) ) { // file exists
                        $stat['latest'] = $latest;
                        $this->cheapCache->set( $path, 'stat', $stat );
                        $this->setFileCache( $path, $stat ); // update persistent cache
@@ -627,8 +694,11 @@ abstract class FileBackendStore extends FileBackend {
                                $this->cheapCache->set( $path, 'sha1',
                                        array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
                        }
-               } else {
+               } elseif ( $stat === false ) { // file does not exist
+                       $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
                        wfDebug( __METHOD__ . ": File $path does not exist.\n" );
+               } else { // an error occurred
+                       wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -682,7 +752,7 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( $this->cheapCache->has( $path, 'sha1' ) ) {
+               if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
                        $stat = $this->cheapCache->get( $path, 'sha1' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
@@ -697,10 +767,7 @@ abstract class FileBackendStore extends FileBackend {
                $hash = $this->doGetFileSha1Base36( $params );
                wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
-               if ( $hash ) { // don't cache negatives
-                       $this->cheapCache->set( $path, 'sha1',
-                               array( 'hash' => $hash, 'latest' => $latest ) );
-               }
+               $this->cheapCache->set( $path, 'sha1', array( 'hash' => $hash, 'latest' => $latest ) );
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $hash;
@@ -804,6 +871,14 @@ abstract class FileBackendStore extends FileBackend {
         */
        abstract protected function doGetLocalCopyMulti( array $params );
 
+       /**
+        * @see FileBackend::getFileHttpUrl()
+        * @return string|null
+        */
+       public function getFileHttpUrl( array $params ) {
+               return null; // not supported
+       }
+
        /**
         * @see FileBackend::streamFile()
         * @return Status
@@ -980,12 +1055,13 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function getOperationsInternal( array $ops ) {
                $supportedOps = array(
-                       'store'       => 'StoreFileOp',
-                       'copy'        => 'CopyFileOp',
-                       'move'        => 'MoveFileOp',
-                       'delete'      => 'DeleteFileOp',
-                       'create'      => 'CreateFileOp',
-                       'null'        => 'NullFileOp'
+                       'store'    => 'StoreFileOp',
+                       'copy'     => 'CopyFileOp',
+                       'move'     => 'MoveFileOp',
+                       'delete'   => 'DeleteFileOp',
+                       'create'   => 'CreateFileOp',
+                       'describe' => 'DescribeFileOp',
+                       'null'     => 'NullFileOp'
                );
 
                $performOps = array(); // array of FileOp objects
@@ -1051,6 +1127,9 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
+               // Fix up custom header name/value pairs...
+               $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
+
                // Build up a list of FileOps...
                $performOps = $this->getOperationsInternal( $ops );
 
@@ -1100,6 +1179,12 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
+               // Fix up custom header name/value pairs...
+               $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
+
+               // Clear any file cache entries
+               $this->clearCache();
+
                $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
                $async = ( $this->parallelize === 'implicit' );
                $maxConcurrency = $this->concurrency; // throttle
@@ -1191,6 +1276,26 @@ abstract class FileBackendStore extends FileBackend {
                return array();
        }
 
+       /**
+        * Strip long HTTP headers from a file operation
+        *
+        * @param $op array Same format as doOperation()
+        * @return Array
+        */
+       protected function stripInvalidHeadersFromOp( array $op ) {
+               if ( isset( $op['headers'] ) ) {
+                       foreach ( $op['headers'] as $name => $value ) {
+                               if ( strlen( $name ) > 255 || strlen( $value ) > 255 ) {
+                                       trigger_error( "Header '$name: $value' is too long." );
+                                       unset( $op['headers'][$name] );
+                               } elseif ( !strlen( $value ) ) {
+                                       $op['headers'][$name] = ''; // null/false => ""
+                               }
+                       }
+               }
+               return $op;
+       }
+
        /**
         * @see FileBackend::preloadCache()
         */
@@ -1562,6 +1667,8 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Delete the cached stat info for a file path.
         * The cache key is salted for a while to prevent race conditions.
+        * Since negatives (404s) are not cached, this does not need to be called when
+        * a file is created at a path were there was none before.
         *
         * @param $path string Storage path
         */