43e6cb3ed84aa364548f28872a18b3547f202b9a
[lhc/web/wiklou.git] / includes / filebackend / FileBackendStore.php
1 <?php
2 /**
3 * Base class for all backends using particular storage medium.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup FileBackend
22 * @author Aaron Schulz
23 */
24
25 /**
26 * @brief Base class for all backends using particular storage medium.
27 *
28 * This class defines the methods as abstract that subclasses must implement.
29 * Outside callers should *not* use functions with "Internal" in the name.
30 *
31 * The FileBackend operations are implemented using basic functions
32 * such as storeInternal(), copyInternal(), deleteInternal() and the like.
33 * This class is also responsible for path resolution and sanitization.
34 *
35 * @ingroup FileBackend
36 * @since 1.19
37 */
38 abstract class FileBackendStore extends FileBackend {
39 /** @var BagOStuff */
40 protected $memCache;
41 /** @var ProcessCacheLRU */
42 protected $cheapCache; // Map of paths to small (RAM/disk) cache items
43 /** @var ProcessCacheLRU */
44 protected $expensiveCache; // Map of paths to large (RAM/disk) cache items
45
46 /** @var Array Map of container names to sharding settings */
47 protected $shardViaHashLevels = array(); // (container name => config array)
48
49 protected $maxFileSize = 4294967296; // integer bytes (4GiB)
50
51 const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
52
53 /**
54 * @see FileBackend::__construct()
55 *
56 * @param $config Array
57 */
58 public function __construct( array $config ) {
59 parent::__construct( $config );
60 $this->memCache = new EmptyBagOStuff(); // disabled by default
61 $this->cheapCache = new ProcessCacheLRU( 300 );
62 $this->expensiveCache = new ProcessCacheLRU( 5 );
63 }
64
65 /**
66 * Get the maximum allowable file size given backend
67 * medium restrictions and basic performance constraints.
68 * Do not call this function from places outside FileBackend and FileOp.
69 *
70 * @return integer Bytes
71 */
72 final public function maxFileSizeInternal() {
73 return $this->maxFileSize;
74 }
75
76 /**
77 * Check if a file can be created or changed at a given storage path.
78 * FS backends should check if the parent directory exists, files can be
79 * written under it, and that any file already there is writable.
80 * Backends using key/value stores should check if the container exists.
81 *
82 * @param $storagePath string
83 * @return bool
84 */
85 abstract public function isPathUsableInternal( $storagePath );
86
87 /**
88 * Create a file in the backend with the given contents.
89 * This will overwrite any file that exists at the destination.
90 * Do not call this function from places outside FileBackend and FileOp.
91 *
92 * $params include:
93 * - content : the raw file contents
94 * - dst : destination storage path
95 * - disposition : Content-Disposition header value for the destination
96 * - async : Status will be returned immediately if supported.
97 * If the status is OK, then its value field will be
98 * set to a FileBackendStoreOpHandle object.
99 *
100 * @param $params Array
101 * @return Status
102 */
103 final public function createInternal( array $params ) {
104 wfProfileIn( __METHOD__ );
105 wfProfileIn( __METHOD__ . '-' . $this->name );
106 if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
107 $status = Status::newFatal( 'backend-fail-maxsize',
108 $params['dst'], $this->maxFileSizeInternal() );
109 } else {
110 $status = $this->doCreateInternal( $params );
111 $this->clearCache( array( $params['dst'] ) );
112 $this->deleteFileCache( $params['dst'] ); // persistent cache
113 }
114 wfProfileOut( __METHOD__ . '-' . $this->name );
115 wfProfileOut( __METHOD__ );
116 return $status;
117 }
118
119 /**
120 * @see FileBackendStore::createInternal()
121 */
122 abstract protected function doCreateInternal( array $params );
123
124 /**
125 * Store a file into the backend from a file on disk.
126 * This will overwrite any file that exists at the destination.
127 * Do not call this function from places outside FileBackend and FileOp.
128 *
129 * $params include:
130 * - src : source path on disk
131 * - dst : destination storage path
132 * - disposition : Content-Disposition header value for the destination
133 * - async : Status will be returned immediately if supported.
134 * If the status is OK, then its value field will be
135 * set to a FileBackendStoreOpHandle object.
136 *
137 * @param $params Array
138 * @return Status
139 */
140 final public function storeInternal( array $params ) {
141 wfProfileIn( __METHOD__ );
142 wfProfileIn( __METHOD__ . '-' . $this->name );
143 if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
144 $status = Status::newFatal( 'backend-fail-maxsize',
145 $params['dst'], $this->maxFileSizeInternal() );
146 } else {
147 $status = $this->doStoreInternal( $params );
148 $this->clearCache( array( $params['dst'] ) );
149 $this->deleteFileCache( $params['dst'] ); // persistent cache
150 }
151 wfProfileOut( __METHOD__ . '-' . $this->name );
152 wfProfileOut( __METHOD__ );
153 return $status;
154 }
155
156 /**
157 * @see FileBackendStore::storeInternal()
158 */
159 abstract protected function doStoreInternal( array $params );
160
161 /**
162 * Copy a file from one storage path to another in the backend.
163 * This will overwrite any file that exists at the destination.
164 * Do not call this function from places outside FileBackend and FileOp.
165 *
166 * $params include:
167 * - src : source storage path
168 * - dst : destination storage path
169 * - ignoreMissingSource : do nothing if the source file does not exist
170 * - disposition : Content-Disposition header value for the destination
171 * - async : Status will be returned immediately if supported.
172 * If the status is OK, then its value field will be
173 * set to a FileBackendStoreOpHandle object.
174 *
175 * @param $params Array
176 * @return Status
177 */
178 final public function copyInternal( array $params ) {
179 wfProfileIn( __METHOD__ );
180 wfProfileIn( __METHOD__ . '-' . $this->name );
181 $status = $this->doCopyInternal( $params );
182 $this->clearCache( array( $params['dst'] ) );
183 $this->deleteFileCache( $params['dst'] ); // persistent cache
184 wfProfileOut( __METHOD__ . '-' . $this->name );
185 wfProfileOut( __METHOD__ );
186 return $status;
187 }
188
189 /**
190 * @see FileBackendStore::copyInternal()
191 */
192 abstract protected function doCopyInternal( array $params );
193
194 /**
195 * Delete a file at the storage path.
196 * Do not call this function from places outside FileBackend and FileOp.
197 *
198 * $params include:
199 * - src : source storage path
200 * - ignoreMissingSource : do nothing if the source file does not exist
201 * - async : Status will be returned immediately if supported.
202 * If the status is OK, then its value field will be
203 * set to a FileBackendStoreOpHandle object.
204 *
205 * @param $params Array
206 * @return Status
207 */
208 final public function deleteInternal( array $params ) {
209 wfProfileIn( __METHOD__ );
210 wfProfileIn( __METHOD__ . '-' . $this->name );
211 $status = $this->doDeleteInternal( $params );
212 $this->clearCache( array( $params['src'] ) );
213 $this->deleteFileCache( $params['src'] ); // persistent cache
214 wfProfileOut( __METHOD__ . '-' . $this->name );
215 wfProfileOut( __METHOD__ );
216 return $status;
217 }
218
219 /**
220 * @see FileBackendStore::deleteInternal()
221 */
222 abstract protected function doDeleteInternal( array $params );
223
224 /**
225 * Move a file from one storage path to another in the backend.
226 * This will overwrite any file that exists at the destination.
227 * Do not call this function from places outside FileBackend and FileOp.
228 *
229 * $params include:
230 * - src : source storage path
231 * - dst : destination storage path
232 * - ignoreMissingSource : do nothing if the source file does not exist
233 * - disposition : Content-Disposition header value for the destination
234 * - async : Status will be returned immediately if supported.
235 * If the status is OK, then its value field will be
236 * set to a FileBackendStoreOpHandle object.
237 *
238 * @param $params Array
239 * @return Status
240 */
241 final public function moveInternal( array $params ) {
242 wfProfileIn( __METHOD__ );
243 wfProfileIn( __METHOD__ . '-' . $this->name );
244 $status = $this->doMoveInternal( $params );
245 $this->clearCache( array( $params['src'], $params['dst'] ) );
246 $this->deleteFileCache( $params['src'] ); // persistent cache
247 $this->deleteFileCache( $params['dst'] ); // persistent cache
248 wfProfileOut( __METHOD__ . '-' . $this->name );
249 wfProfileOut( __METHOD__ );
250 return $status;
251 }
252
253 /**
254 * @see FileBackendStore::moveInternal()
255 * @return Status
256 */
257 protected function doMoveInternal( array $params ) {
258 unset( $params['async'] ); // two steps, won't work here :)
259 // Copy source to dest
260 $status = $this->copyInternal( $params );
261 if ( $status->isOK() ) {
262 // Delete source (only fails due to races or medium going down)
263 $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
264 $status->setResult( true, $status->value ); // ignore delete() errors
265 }
266 return $status;
267 }
268
269 /**
270 * No-op file operation that does nothing.
271 * Do not call this function from places outside FileBackend and FileOp.
272 *
273 * @param $params Array
274 * @return Status
275 */
276 final public function nullInternal( array $params ) {
277 return Status::newGood();
278 }
279
280 /**
281 * @see FileBackend::concatenate()
282 * @return Status
283 */
284 final public function concatenate( array $params ) {
285 wfProfileIn( __METHOD__ );
286 wfProfileIn( __METHOD__ . '-' . $this->name );
287 $status = Status::newGood();
288
289 // Try to lock the source files for the scope of this function
290 $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
291 if ( $status->isOK() ) {
292 // Actually do the file concatenation...
293 $start_time = microtime( true );
294 $status->merge( $this->doConcatenate( $params ) );
295 $sec = microtime( true ) - $start_time;
296 if ( !$status->isOK() ) {
297 wfDebugLog( 'FileOperation', get_class( $this ) . " failed to concatenate " .
298 count( $params['srcs'] ) . " file(s) [$sec sec]" );
299 }
300 }
301
302 wfProfileOut( __METHOD__ . '-' . $this->name );
303 wfProfileOut( __METHOD__ );
304 return $status;
305 }
306
307 /**
308 * @see FileBackendStore::concatenate()
309 * @return Status
310 */
311 protected function doConcatenate( array $params ) {
312 $status = Status::newGood();
313 $tmpPath = $params['dst']; // convenience
314 unset( $params['latest'] ); // sanity
315
316 // Check that the specified temp file is valid...
317 wfSuppressWarnings();
318 $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
319 wfRestoreWarnings();
320 if ( !$ok ) { // not present or not empty
321 $status->fatal( 'backend-fail-opentemp', $tmpPath );
322 return $status;
323 }
324
325 // Get local FS versions of the chunks needed for the concatenation...
326 $fsFiles = $this->getLocalReferenceMulti( $params );
327 foreach ( $fsFiles as $path => &$fsFile ) {
328 if ( !$fsFile ) { // chunk failed to download?
329 $fsFile = $this->getLocalReference( array( 'src' => $path ) );
330 if ( !$fsFile ) { // retry failed?
331 $status->fatal( 'backend-fail-read', $path );
332 return $status;
333 }
334 }
335 }
336 unset( $fsFile ); // unset reference so we can reuse $fsFile
337
338 // Get a handle for the destination temp file
339 $tmpHandle = fopen( $tmpPath, 'ab' );
340 if ( $tmpHandle === false ) {
341 $status->fatal( 'backend-fail-opentemp', $tmpPath );
342 return $status;
343 }
344
345 // Build up the temp file using the source chunks (in order)...
346 foreach ( $fsFiles as $virtualSource => $fsFile ) {
347 // Get a handle to the local FS version
348 $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
349 if ( $sourceHandle === false ) {
350 fclose( $tmpHandle );
351 $status->fatal( 'backend-fail-read', $virtualSource );
352 return $status;
353 }
354 // Append chunk to file (pass chunk size to avoid magic quotes)
355 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
356 fclose( $sourceHandle );
357 fclose( $tmpHandle );
358 $status->fatal( 'backend-fail-writetemp', $tmpPath );
359 return $status;
360 }
361 fclose( $sourceHandle );
362 }
363 if ( !fclose( $tmpHandle ) ) {
364 $status->fatal( 'backend-fail-closetemp', $tmpPath );
365 return $status;
366 }
367
368 clearstatcache(); // temp file changed
369
370 return $status;
371 }
372
373 /**
374 * @see FileBackend::doPrepare()
375 * @return Status
376 */
377 final protected function doPrepare( array $params ) {
378 wfProfileIn( __METHOD__ );
379 wfProfileIn( __METHOD__ . '-' . $this->name );
380
381 $status = Status::newGood();
382 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
383 if ( $dir === null ) {
384 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
385 wfProfileOut( __METHOD__ . '-' . $this->name );
386 wfProfileOut( __METHOD__ );
387 return $status; // invalid storage path
388 }
389
390 if ( $shard !== null ) { // confined to a single container/shard
391 $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
392 } else { // directory is on several shards
393 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
394 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
395 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
396 $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
397 }
398 }
399
400 wfProfileOut( __METHOD__ . '-' . $this->name );
401 wfProfileOut( __METHOD__ );
402 return $status;
403 }
404
405 /**
406 * @see FileBackendStore::doPrepare()
407 * @return Status
408 */
409 protected function doPrepareInternal( $container, $dir, array $params ) {
410 return Status::newGood();
411 }
412
413 /**
414 * @see FileBackend::doSecure()
415 * @return Status
416 */
417 final protected function doSecure( array $params ) {
418 wfProfileIn( __METHOD__ );
419 wfProfileIn( __METHOD__ . '-' . $this->name );
420 $status = Status::newGood();
421
422 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
423 if ( $dir === null ) {
424 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
425 wfProfileOut( __METHOD__ . '-' . $this->name );
426 wfProfileOut( __METHOD__ );
427 return $status; // invalid storage path
428 }
429
430 if ( $shard !== null ) { // confined to a single container/shard
431 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
432 } else { // directory is on several shards
433 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
434 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
435 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
436 $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
437 }
438 }
439
440 wfProfileOut( __METHOD__ . '-' . $this->name );
441 wfProfileOut( __METHOD__ );
442 return $status;
443 }
444
445 /**
446 * @see FileBackendStore::doSecure()
447 * @return Status
448 */
449 protected function doSecureInternal( $container, $dir, array $params ) {
450 return Status::newGood();
451 }
452
453 /**
454 * @see FileBackend::doPublish()
455 * @return Status
456 */
457 final protected function doPublish( array $params ) {
458 wfProfileIn( __METHOD__ );
459 wfProfileIn( __METHOD__ . '-' . $this->name );
460 $status = Status::newGood();
461
462 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
463 if ( $dir === null ) {
464 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
465 wfProfileOut( __METHOD__ . '-' . $this->name );
466 wfProfileOut( __METHOD__ );
467 return $status; // invalid storage path
468 }
469
470 if ( $shard !== null ) { // confined to a single container/shard
471 $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
472 } else { // directory is on several shards
473 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
474 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
475 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
476 $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
477 }
478 }
479
480 wfProfileOut( __METHOD__ . '-' . $this->name );
481 wfProfileOut( __METHOD__ );
482 return $status;
483 }
484
485 /**
486 * @see FileBackendStore::doPublish()
487 * @return Status
488 */
489 protected function doPublishInternal( $container, $dir, array $params ) {
490 return Status::newGood();
491 }
492
493 /**
494 * @see FileBackend::doClean()
495 * @return Status
496 */
497 final protected function doClean( array $params ) {
498 wfProfileIn( __METHOD__ );
499 wfProfileIn( __METHOD__ . '-' . $this->name );
500 $status = Status::newGood();
501
502 // Recursive: first delete all empty subdirs recursively
503 if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
504 $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) );
505 if ( $subDirsRel !== null ) { // no errors
506 foreach ( $subDirsRel as $subDirRel ) {
507 $subDir = $params['dir'] . "/{$subDirRel}"; // full path
508 $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
509 }
510 }
511 }
512
513 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
514 if ( $dir === null ) {
515 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
516 wfProfileOut( __METHOD__ . '-' . $this->name );
517 wfProfileOut( __METHOD__ );
518 return $status; // invalid storage path
519 }
520
521 // Attempt to lock this directory...
522 $filesLockEx = array( $params['dir'] );
523 $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
524 if ( !$status->isOK() ) {
525 wfProfileOut( __METHOD__ . '-' . $this->name );
526 wfProfileOut( __METHOD__ );
527 return $status; // abort
528 }
529
530 if ( $shard !== null ) { // confined to a single container/shard
531 $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
532 $this->deleteContainerCache( $fullCont ); // purge cache
533 } else { // directory is on several shards
534 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
535 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
536 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
537 $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
538 $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
539 }
540 }
541
542 wfProfileOut( __METHOD__ . '-' . $this->name );
543 wfProfileOut( __METHOD__ );
544 return $status;
545 }
546
547 /**
548 * @see FileBackendStore::doClean()
549 * @return Status
550 */
551 protected function doCleanInternal( $container, $dir, array $params ) {
552 return Status::newGood();
553 }
554
555 /**
556 * @see FileBackend::fileExists()
557 * @return bool|null
558 */
559 final public function fileExists( array $params ) {
560 wfProfileIn( __METHOD__ );
561 wfProfileIn( __METHOD__ . '-' . $this->name );
562 $stat = $this->getFileStat( $params );
563 wfProfileOut( __METHOD__ . '-' . $this->name );
564 wfProfileOut( __METHOD__ );
565 return ( $stat === null ) ? null : (bool)$stat; // null => failure
566 }
567
568 /**
569 * @see FileBackend::getFileTimestamp()
570 * @return bool
571 */
572 final public function getFileTimestamp( array $params ) {
573 wfProfileIn( __METHOD__ );
574 wfProfileIn( __METHOD__ . '-' . $this->name );
575 $stat = $this->getFileStat( $params );
576 wfProfileOut( __METHOD__ . '-' . $this->name );
577 wfProfileOut( __METHOD__ );
578 return $stat ? $stat['mtime'] : false;
579 }
580
581 /**
582 * @see FileBackend::getFileSize()
583 * @return bool
584 */
585 final public function getFileSize( array $params ) {
586 wfProfileIn( __METHOD__ );
587 wfProfileIn( __METHOD__ . '-' . $this->name );
588 $stat = $this->getFileStat( $params );
589 wfProfileOut( __METHOD__ . '-' . $this->name );
590 wfProfileOut( __METHOD__ );
591 return $stat ? $stat['size'] : false;
592 }
593
594 /**
595 * @see FileBackend::getFileStat()
596 * @return bool
597 */
598 final public function getFileStat( array $params ) {
599 $path = self::normalizeStoragePath( $params['src'] );
600 if ( $path === null ) {
601 return false; // invalid storage path
602 }
603 wfProfileIn( __METHOD__ );
604 wfProfileIn( __METHOD__ . '-' . $this->name );
605 $latest = !empty( $params['latest'] ); // use latest data?
606 if ( !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
607 $this->primeFileCache( array( $path ) ); // check persistent cache
608 }
609 if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
610 $stat = $this->cheapCache->get( $path, 'stat' );
611 // If we want the latest data, check that this cached
612 // value was in fact fetched with the latest available data
613 // (the process cache is ignored if it contains a negative).
614 if ( !$latest || ( is_array( $stat ) && $stat['latest'] ) ) {
615 wfProfileOut( __METHOD__ . '-' . $this->name );
616 wfProfileOut( __METHOD__ );
617 return $stat;
618 }
619 }
620 wfProfileIn( __METHOD__ . '-miss' );
621 wfProfileIn( __METHOD__ . '-miss-' . $this->name );
622 $stat = $this->doGetFileStat( $params );
623 wfProfileOut( __METHOD__ . '-miss-' . $this->name );
624 wfProfileOut( __METHOD__ . '-miss' );
625 if ( is_array( $stat ) ) { // file exists
626 $stat['latest'] = $latest;
627 $this->cheapCache->set( $path, 'stat', $stat );
628 $this->setFileCache( $path, $stat ); // update persistent cache
629 if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
630 $this->cheapCache->set( $path, 'sha1',
631 array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
632 }
633 } elseif ( $stat === false ) { // file does not exist
634 $this->cheapCache->set( $path, 'stat', false );
635 wfDebug( __METHOD__ . ": File $path does not exist.\n" );
636 } else { // an error occurred
637 wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
638 }
639 wfProfileOut( __METHOD__ . '-' . $this->name );
640 wfProfileOut( __METHOD__ );
641 return $stat;
642 }
643
644 /**
645 * @see FileBackendStore::getFileStat()
646 */
647 abstract protected function doGetFileStat( array $params );
648
649 /**
650 * @see FileBackend::getFileContentsMulti()
651 * @return Array
652 */
653 public function getFileContentsMulti( array $params ) {
654 wfProfileIn( __METHOD__ );
655 wfProfileIn( __METHOD__ . '-' . $this->name );
656
657 $params = $this->setConcurrencyFlags( $params );
658 $contents = $this->doGetFileContentsMulti( $params );
659
660 wfProfileOut( __METHOD__ . '-' . $this->name );
661 wfProfileOut( __METHOD__ );
662 return $contents;
663 }
664
665 /**
666 * @see FileBackendStore::getFileContentsMulti()
667 * @return Array
668 */
669 protected function doGetFileContentsMulti( array $params ) {
670 $contents = array();
671 foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
672 wfSuppressWarnings();
673 $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
674 wfRestoreWarnings();
675 }
676 return $contents;
677 }
678
679 /**
680 * @see FileBackend::getFileSha1Base36()
681 * @return bool|string
682 */
683 final public function getFileSha1Base36( array $params ) {
684 $path = self::normalizeStoragePath( $params['src'] );
685 if ( $path === null ) {
686 return false; // invalid storage path
687 }
688 wfProfileIn( __METHOD__ );
689 wfProfileIn( __METHOD__ . '-' . $this->name );
690 $latest = !empty( $params['latest'] ); // use latest data?
691 if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
692 $stat = $this->cheapCache->get( $path, 'sha1' );
693 // If we want the latest data, check that this cached
694 // value was in fact fetched with the latest available data.
695 if ( !$latest || $stat['latest'] ) {
696 wfProfileOut( __METHOD__ . '-' . $this->name );
697 wfProfileOut( __METHOD__ );
698 return $stat['hash'];
699 }
700 }
701 wfProfileIn( __METHOD__ . '-miss' );
702 wfProfileIn( __METHOD__ . '-miss-' . $this->name );
703 $hash = $this->doGetFileSha1Base36( $params );
704 wfProfileOut( __METHOD__ . '-miss-' . $this->name );
705 wfProfileOut( __METHOD__ . '-miss' );
706 if ( $hash ) { // don't cache negatives
707 $this->cheapCache->set( $path, 'sha1',
708 array( 'hash' => $hash, 'latest' => $latest ) );
709 }
710 wfProfileOut( __METHOD__ . '-' . $this->name );
711 wfProfileOut( __METHOD__ );
712 return $hash;
713 }
714
715 /**
716 * @see FileBackendStore::getFileSha1Base36()
717 * @return bool|string
718 */
719 protected function doGetFileSha1Base36( array $params ) {
720 $fsFile = $this->getLocalReference( $params );
721 if ( !$fsFile ) {
722 return false;
723 } else {
724 return $fsFile->getSha1Base36();
725 }
726 }
727
728 /**
729 * @see FileBackend::getFileProps()
730 * @return Array
731 */
732 final public function getFileProps( array $params ) {
733 wfProfileIn( __METHOD__ );
734 wfProfileIn( __METHOD__ . '-' . $this->name );
735 $fsFile = $this->getLocalReference( $params );
736 $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
737 wfProfileOut( __METHOD__ . '-' . $this->name );
738 wfProfileOut( __METHOD__ );
739 return $props;
740 }
741
742 /**
743 * @see FileBackend::getLocalReferenceMulti()
744 * @return Array
745 */
746 final public function getLocalReferenceMulti( array $params ) {
747 wfProfileIn( __METHOD__ );
748 wfProfileIn( __METHOD__ . '-' . $this->name );
749
750 $params = $this->setConcurrencyFlags( $params );
751
752 $fsFiles = array(); // (path => FSFile)
753 $latest = !empty( $params['latest'] ); // use latest data?
754 // Reuse any files already in process cache...
755 foreach ( $params['srcs'] as $src ) {
756 $path = self::normalizeStoragePath( $src );
757 if ( $path === null ) {
758 $fsFiles[$src] = null; // invalid storage path
759 } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
760 $val = $this->expensiveCache->get( $path, 'localRef' );
761 // If we want the latest data, check that this cached
762 // value was in fact fetched with the latest available data.
763 if ( !$latest || $val['latest'] ) {
764 $fsFiles[$src] = $val['object'];
765 }
766 }
767 }
768 // Fetch local references of any remaning files...
769 $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
770 foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
771 $fsFiles[$path] = $fsFile;
772 if ( $fsFile ) { // update the process cache...
773 $this->expensiveCache->set( $path, 'localRef',
774 array( 'object' => $fsFile, 'latest' => $latest ) );
775 }
776 }
777
778 wfProfileOut( __METHOD__ . '-' . $this->name );
779 wfProfileOut( __METHOD__ );
780 return $fsFiles;
781 }
782
783 /**
784 * @see FileBackendStore::getLocalReferenceMulti()
785 * @return Array
786 */
787 protected function doGetLocalReferenceMulti( array $params ) {
788 return $this->doGetLocalCopyMulti( $params );
789 }
790
791 /**
792 * @see FileBackend::getLocalCopyMulti()
793 * @return Array
794 */
795 final public function getLocalCopyMulti( array $params ) {
796 wfProfileIn( __METHOD__ );
797 wfProfileIn( __METHOD__ . '-' . $this->name );
798
799 $params = $this->setConcurrencyFlags( $params );
800 $tmpFiles = $this->doGetLocalCopyMulti( $params );
801
802 wfProfileOut( __METHOD__ . '-' . $this->name );
803 wfProfileOut( __METHOD__ );
804 return $tmpFiles;
805 }
806
807 /**
808 * @see FileBackendStore::getLocalCopyMulti()
809 * @return Array
810 */
811 abstract protected function doGetLocalCopyMulti( array $params );
812
813 /**
814 * @see FileBackend::getFileHttpUrl()
815 * @return string|null
816 */
817 public function getFileHttpUrl( array $params ) {
818 return null; // not supported
819 }
820
821 /**
822 * @see FileBackend::streamFile()
823 * @return Status
824 */
825 final public function streamFile( array $params ) {
826 wfProfileIn( __METHOD__ );
827 wfProfileIn( __METHOD__ . '-' . $this->name );
828 $status = Status::newGood();
829
830 $info = $this->getFileStat( $params );
831 if ( !$info ) { // let StreamFile handle the 404
832 $status->fatal( 'backend-fail-notexists', $params['src'] );
833 }
834
835 // Set output buffer and HTTP headers for stream
836 $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
837 $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
838 if ( $res == StreamFile::NOT_MODIFIED ) {
839 // do nothing; client cache is up to date
840 } elseif ( $res == StreamFile::READY_STREAM ) {
841 wfProfileIn( __METHOD__ . '-send' );
842 wfProfileIn( __METHOD__ . '-send-' . $this->name );
843 $status = $this->doStreamFile( $params );
844 wfProfileOut( __METHOD__ . '-send-' . $this->name );
845 wfProfileOut( __METHOD__ . '-send' );
846 if ( !$status->isOK() ) {
847 // Per bug 41113, nasty things can happen if bad cache entries get
848 // stuck in cache. It's also possible that this error can come up
849 // with simple race conditions. Clear out the stat cache to be safe.
850 $this->clearCache( array( $params['src'] ) );
851 $this->deleteFileCache( $params['src'] );
852 trigger_error( "Bad stat cache or race condition for file {$params['src']}." );
853 }
854 } else {
855 $status->fatal( 'backend-fail-stream', $params['src'] );
856 }
857
858 wfProfileOut( __METHOD__ . '-' . $this->name );
859 wfProfileOut( __METHOD__ );
860 return $status;
861 }
862
863 /**
864 * @see FileBackendStore::streamFile()
865 * @return Status
866 */
867 protected function doStreamFile( array $params ) {
868 $status = Status::newGood();
869
870 $fsFile = $this->getLocalReference( $params );
871 if ( !$fsFile ) {
872 $status->fatal( 'backend-fail-stream', $params['src'] );
873 } elseif ( !readfile( $fsFile->getPath() ) ) {
874 $status->fatal( 'backend-fail-stream', $params['src'] );
875 }
876
877 return $status;
878 }
879
880 /**
881 * @see FileBackend::directoryExists()
882 * @return bool|null
883 */
884 final public function directoryExists( array $params ) {
885 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
886 if ( $dir === null ) {
887 return false; // invalid storage path
888 }
889 if ( $shard !== null ) { // confined to a single container/shard
890 return $this->doDirectoryExists( $fullCont, $dir, $params );
891 } else { // directory is on several shards
892 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
893 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
894 $res = false; // response
895 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
896 $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
897 if ( $exists ) {
898 $res = true;
899 break; // found one!
900 } elseif ( $exists === null ) { // error?
901 $res = null; // if we don't find anything, it is indeterminate
902 }
903 }
904 return $res;
905 }
906 }
907
908 /**
909 * @see FileBackendStore::directoryExists()
910 *
911 * @param $container string Resolved container name
912 * @param $dir string Resolved path relative to container
913 * @param $params Array
914 * @return bool|null
915 */
916 abstract protected function doDirectoryExists( $container, $dir, array $params );
917
918 /**
919 * @see FileBackend::getDirectoryList()
920 * @return Traversable|Array|null Returns null on failure
921 */
922 final public function getDirectoryList( array $params ) {
923 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
924 if ( $dir === null ) { // invalid storage path
925 return null;
926 }
927 if ( $shard !== null ) {
928 // File listing is confined to a single container/shard
929 return $this->getDirectoryListInternal( $fullCont, $dir, $params );
930 } else {
931 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
932 // File listing spans multiple containers/shards
933 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
934 return new FileBackendStoreShardDirIterator( $this,
935 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
936 }
937 }
938
939 /**
940 * Do not call this function from places outside FileBackend
941 *
942 * @see FileBackendStore::getDirectoryList()
943 *
944 * @param $container string Resolved container name
945 * @param $dir string Resolved path relative to container
946 * @param $params Array
947 * @return Traversable|Array|null Returns null on failure
948 */
949 abstract public function getDirectoryListInternal( $container, $dir, array $params );
950
951 /**
952 * @see FileBackend::getFileList()
953 * @return Traversable|Array|null Returns null on failure
954 */
955 final public function getFileList( array $params ) {
956 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
957 if ( $dir === null ) { // invalid storage path
958 return null;
959 }
960 if ( $shard !== null ) {
961 // File listing is confined to a single container/shard
962 return $this->getFileListInternal( $fullCont, $dir, $params );
963 } else {
964 wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
965 // File listing spans multiple containers/shards
966 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
967 return new FileBackendStoreShardFileIterator( $this,
968 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
969 }
970 }
971
972 /**
973 * Do not call this function from places outside FileBackend
974 *
975 * @see FileBackendStore::getFileList()
976 *
977 * @param $container string Resolved container name
978 * @param $dir string Resolved path relative to container
979 * @param $params Array
980 * @return Traversable|Array|null Returns null on failure
981 */
982 abstract public function getFileListInternal( $container, $dir, array $params );
983
984 /**
985 * Return a list of FileOp objects from a list of operations.
986 * Do not call this function from places outside FileBackend.
987 *
988 * The result must have the same number of items as the input.
989 * An exception is thrown if an unsupported operation is requested.
990 *
991 * @param $ops Array Same format as doOperations()
992 * @return Array List of FileOp objects
993 * @throws MWException
994 */
995 final public function getOperationsInternal( array $ops ) {
996 $supportedOps = array(
997 'store' => 'StoreFileOp',
998 'copy' => 'CopyFileOp',
999 'move' => 'MoveFileOp',
1000 'delete' => 'DeleteFileOp',
1001 'create' => 'CreateFileOp',
1002 'null' => 'NullFileOp'
1003 );
1004
1005 $performOps = array(); // array of FileOp objects
1006 // Build up ordered array of FileOps...
1007 foreach ( $ops as $operation ) {
1008 $opName = $operation['op'];
1009 if ( isset( $supportedOps[$opName] ) ) {
1010 $class = $supportedOps[$opName];
1011 // Get params for this operation
1012 $params = $operation;
1013 // Append the FileOp class
1014 $performOps[] = new $class( $this, $params );
1015 } else {
1016 throw new MWException( "Operation '$opName' is not supported." );
1017 }
1018 }
1019
1020 return $performOps;
1021 }
1022
1023 /**
1024 * Get a list of storage paths to lock for a list of operations
1025 * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys,
1026 * each corresponding to a list of storage paths to be locked.
1027 * All returned paths are normalized.
1028 *
1029 * @param $performOps Array List of FileOp objects
1030 * @return Array ('sh' => list of paths, 'ex' => list of paths)
1031 */
1032 final public function getPathsToLockForOpsInternal( array $performOps ) {
1033 // Build up a list of files to lock...
1034 $paths = array( 'sh' => array(), 'ex' => array() );
1035 foreach ( $performOps as $fileOp ) {
1036 $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
1037 $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
1038 }
1039 // Optimization: if doing an EX lock anyway, don't also set an SH one
1040 $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
1041 // Get a shared lock on the parent directory of each path changed
1042 $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
1043
1044 return $paths;
1045 }
1046
1047 /**
1048 * @see FileBackend::getScopedLocksForOps()
1049 * @return Array
1050 */
1051 public function getScopedLocksForOps( array $ops, Status $status ) {
1052 $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
1053 return array(
1054 $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
1055 $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
1056 );
1057 }
1058
1059 /**
1060 * @see FileBackend::doOperationsInternal()
1061 * @return Status
1062 */
1063 final protected function doOperationsInternal( array $ops, array $opts ) {
1064 wfProfileIn( __METHOD__ );
1065 wfProfileIn( __METHOD__ . '-' . $this->name );
1066 $status = Status::newGood();
1067
1068 // Build up a list of FileOps...
1069 $performOps = $this->getOperationsInternal( $ops );
1070
1071 // Acquire any locks as needed...
1072 if ( empty( $opts['nonLocking'] ) ) {
1073 // Build up a list of files to lock...
1074 $paths = $this->getPathsToLockForOpsInternal( $performOps );
1075 // Try to lock those files for the scope of this function...
1076 $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
1077 $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
1078 if ( !$status->isOK() ) {
1079 wfProfileOut( __METHOD__ . '-' . $this->name );
1080 wfProfileOut( __METHOD__ );
1081 return $status; // abort
1082 }
1083 }
1084
1085 // Clear any file cache entries (after locks acquired)
1086 if ( empty( $opts['preserveCache'] ) ) {
1087 $this->clearCache();
1088 }
1089
1090 // Load from the persistent file and container caches
1091 $this->primeFileCache( $performOps );
1092 $this->primeContainerCache( $performOps );
1093
1094 // Actually attempt the operation batch...
1095 $opts = $this->setConcurrencyFlags( $opts );
1096 $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
1097
1098 // Merge errors into status fields
1099 $status->merge( $subStatus );
1100 $status->success = $subStatus->success; // not done in merge()
1101
1102 wfProfileOut( __METHOD__ . '-' . $this->name );
1103 wfProfileOut( __METHOD__ );
1104 return $status;
1105 }
1106
1107 /**
1108 * @see FileBackend::doQuickOperationsInternal()
1109 * @return Status
1110 * @throws MWException
1111 */
1112 final protected function doQuickOperationsInternal( array $ops ) {
1113 wfProfileIn( __METHOD__ );
1114 wfProfileIn( __METHOD__ . '-' . $this->name );
1115 $status = Status::newGood();
1116
1117 $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
1118 $async = ( $this->parallelize === 'implicit' );
1119 $maxConcurrency = $this->concurrency; // throttle
1120
1121 $statuses = array(); // array of (index => Status)
1122 $fileOpHandles = array(); // list of (index => handle) arrays
1123 $curFileOpHandles = array(); // current handle batch
1124 // Perform the sync-only ops and build up op handles for the async ops...
1125 foreach ( $ops as $index => $params ) {
1126 if ( !in_array( $params['op'], $supportedOps ) ) {
1127 wfProfileOut( __METHOD__ . '-' . $this->name );
1128 wfProfileOut( __METHOD__ );
1129 throw new MWException( "Operation '{$params['op']}' is not supported." );
1130 }
1131 $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
1132 $subStatus = $this->$method( array( 'async' => $async ) + $params );
1133 if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
1134 if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
1135 $fileOpHandles[] = $curFileOpHandles; // push this batch
1136 $curFileOpHandles = array();
1137 }
1138 $curFileOpHandles[$index] = $subStatus->value; // keep index
1139 } else { // error or completed
1140 $statuses[$index] = $subStatus; // keep index
1141 }
1142 }
1143 if ( count( $curFileOpHandles ) ) {
1144 $fileOpHandles[] = $curFileOpHandles; // last batch
1145 }
1146 // Do all the async ops that can be done concurrently...
1147 foreach ( $fileOpHandles as $fileHandleBatch ) {
1148 $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
1149 }
1150 // Marshall and merge all the responses...
1151 foreach ( $statuses as $index => $subStatus ) {
1152 $status->merge( $subStatus );
1153 if ( $subStatus->isOK() ) {
1154 $status->success[$index] = true;
1155 ++$status->successCount;
1156 } else {
1157 $status->success[$index] = false;
1158 ++$status->failCount;
1159 }
1160 }
1161
1162 wfProfileOut( __METHOD__ . '-' . $this->name );
1163 wfProfileOut( __METHOD__ );
1164 return $status;
1165 }
1166
1167 /**
1168 * Execute a list of FileBackendStoreOpHandle handles in parallel.
1169 * The resulting Status object fields will correspond
1170 * to the order in which the handles where given.
1171 *
1172 * @param $handles Array List of FileBackendStoreOpHandle objects
1173 * @return Array Map of Status objects
1174 * @throws MWException
1175 */
1176 final public function executeOpHandlesInternal( array $fileOpHandles ) {
1177 wfProfileIn( __METHOD__ );
1178 wfProfileIn( __METHOD__ . '-' . $this->name );
1179 foreach ( $fileOpHandles as $fileOpHandle ) {
1180 if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
1181 throw new MWException( "Given a non-FileBackendStoreOpHandle object." );
1182 } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
1183 throw new MWException( "Given a FileBackendStoreOpHandle for the wrong backend." );
1184 }
1185 }
1186 $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
1187 foreach ( $fileOpHandles as $fileOpHandle ) {
1188 $fileOpHandle->closeResources();
1189 }
1190 wfProfileOut( __METHOD__ . '-' . $this->name );
1191 wfProfileOut( __METHOD__ );
1192 return $res;
1193 }
1194
1195 /**
1196 * @see FileBackendStore::executeOpHandlesInternal()
1197 * @param array $fileOpHandles
1198 * @throws MWException
1199 * @return Array List of corresponding Status objects
1200 */
1201 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1202 foreach ( $fileOpHandles as $fileOpHandle ) { // OK if empty
1203 throw new MWException( "This backend supports no asynchronous operations." );
1204 }
1205 return array();
1206 }
1207
1208 /**
1209 * @see FileBackend::preloadCache()
1210 */
1211 final public function preloadCache( array $paths ) {
1212 $fullConts = array(); // full container names
1213 foreach ( $paths as $path ) {
1214 list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
1215 $fullConts[] = $fullCont;
1216 }
1217 // Load from the persistent file and container caches
1218 $this->primeContainerCache( $fullConts );
1219 $this->primeFileCache( $paths );
1220 }
1221
1222 /**
1223 * @see FileBackend::clearCache()
1224 */
1225 final public function clearCache( array $paths = null ) {
1226 if ( is_array( $paths ) ) {
1227 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1228 $paths = array_filter( $paths, 'strlen' ); // remove nulls
1229 }
1230 if ( $paths === null ) {
1231 $this->cheapCache->clear();
1232 $this->expensiveCache->clear();
1233 } else {
1234 foreach ( $paths as $path ) {
1235 $this->cheapCache->clear( $path );
1236 $this->expensiveCache->clear( $path );
1237 }
1238 }
1239 $this->doClearCache( $paths );
1240 }
1241
1242 /**
1243 * Clears any additional stat caches for storage paths
1244 *
1245 * @see FileBackend::clearCache()
1246 *
1247 * @param $paths Array Storage paths (optional)
1248 * @return void
1249 */
1250 protected function doClearCache( array $paths = null ) {}
1251
1252 /**
1253 * Is this a key/value store where directories are just virtual?
1254 * Virtual directories exists in so much as files exists that are
1255 * prefixed with the directory path followed by a forward slash.
1256 *
1257 * @return bool
1258 */
1259 abstract protected function directoriesAreVirtual();
1260
1261 /**
1262 * Check if a container name is valid.
1263 * This checks for for length and illegal characters.
1264 *
1265 * @param $container string
1266 * @return bool
1267 */
1268 final protected static function isValidContainerName( $container ) {
1269 // This accounts for Swift and S3 restrictions while leaving room
1270 // for things like '.xxx' (hex shard chars) or '.seg' (segments).
1271 // This disallows directory separators or traversal characters.
1272 // Note that matching strings URL encode to the same string;
1273 // in Swift, the length restriction is *after* URL encoding.
1274 return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
1275 }
1276
1277 /**
1278 * Splits a storage path into an internal container name,
1279 * an internal relative file name, and a container shard suffix.
1280 * Any shard suffix is already appended to the internal container name.
1281 * This also checks that the storage path is valid and within this backend.
1282 *
1283 * If the container is sharded but a suffix could not be determined,
1284 * this means that the path can only refer to a directory and can only
1285 * be scanned by looking in all the container shards.
1286 *
1287 * @param $storagePath string
1288 * @return Array (container, path, container suffix) or (null, null, null) if invalid
1289 */
1290 final protected function resolveStoragePath( $storagePath ) {
1291 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
1292 if ( $backend === $this->name ) { // must be for this backend
1293 $relPath = self::normalizeContainerPath( $relPath );
1294 if ( $relPath !== null ) {
1295 // Get shard for the normalized path if this container is sharded
1296 $cShard = $this->getContainerShard( $container, $relPath );
1297 // Validate and sanitize the relative path (backend-specific)
1298 $relPath = $this->resolveContainerPath( $container, $relPath );
1299 if ( $relPath !== null ) {
1300 // Prepend any wiki ID prefix to the container name
1301 $container = $this->fullContainerName( $container );
1302 if ( self::isValidContainerName( $container ) ) {
1303 // Validate and sanitize the container name (backend-specific)
1304 $container = $this->resolveContainerName( "{$container}{$cShard}" );
1305 if ( $container !== null ) {
1306 return array( $container, $relPath, $cShard );
1307 }
1308 }
1309 }
1310 }
1311 }
1312 return array( null, null, null );
1313 }
1314
1315 /**
1316 * Like resolveStoragePath() except null values are returned if
1317 * the container is sharded and the shard could not be determined.
1318 *
1319 * @see FileBackendStore::resolveStoragePath()
1320 *
1321 * @param $storagePath string
1322 * @return Array (container, path) or (null, null) if invalid
1323 */
1324 final protected function resolveStoragePathReal( $storagePath ) {
1325 list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
1326 if ( $cShard !== null ) {
1327 return array( $container, $relPath );
1328 }
1329 return array( null, null );
1330 }
1331
1332 /**
1333 * Get the container name shard suffix for a given path.
1334 * Any empty suffix means the container is not sharded.
1335 *
1336 * @param $container string Container name
1337 * @param $relPath string Storage path relative to the container
1338 * @return string|null Returns null if shard could not be determined
1339 */
1340 final protected function getContainerShard( $container, $relPath ) {
1341 list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
1342 if ( $levels == 1 || $levels == 2 ) {
1343 // Hash characters are either base 16 or 36
1344 $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1345 // Get a regex that represents the shard portion of paths.
1346 // The concatenation of the captures gives us the shard.
1347 if ( $levels === 1 ) { // 16 or 36 shards per container
1348 $hashDirRegex = '(' . $char . ')';
1349 } else { // 256 or 1296 shards per container
1350 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1351 $hashDirRegex = $char . '/(' . $char . '{2})';
1352 } else { // short hash dir format (e.g. "a/b/c")
1353 $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1354 }
1355 }
1356 // Allow certain directories to be above the hash dirs so as
1357 // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1358 // They must be 2+ chars to avoid any hash directory ambiguity.
1359 $m = array();
1360 if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1361 return '.' . implode( '', array_slice( $m, 1 ) );
1362 }
1363 return null; // failed to match
1364 }
1365 return ''; // no sharding
1366 }
1367
1368 /**
1369 * Check if a storage path maps to a single shard.
1370 * Container dirs like "a", where the container shards on "x/xy",
1371 * can reside on several shards. Such paths are tricky to handle.
1372 *
1373 * @param $storagePath string Storage path
1374 * @return bool
1375 */
1376 final public function isSingleShardPathInternal( $storagePath ) {
1377 list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
1378 return ( $shard !== null );
1379 }
1380
1381 /**
1382 * Get the sharding config for a container.
1383 * If greater than 0, then all file storage paths within
1384 * the container are required to be hashed accordingly.
1385 *
1386 * @param $container string
1387 * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
1388 */
1389 final protected function getContainerHashLevels( $container ) {
1390 if ( isset( $this->shardViaHashLevels[$container] ) ) {
1391 $config = $this->shardViaHashLevels[$container];
1392 $hashLevels = (int)$config['levels'];
1393 if ( $hashLevels == 1 || $hashLevels == 2 ) {
1394 $hashBase = (int)$config['base'];
1395 if ( $hashBase == 16 || $hashBase == 36 ) {
1396 return array( $hashLevels, $hashBase, $config['repeat'] );
1397 }
1398 }
1399 }
1400 return array( 0, 0, false ); // no sharding
1401 }
1402
1403 /**
1404 * Get a list of full container shard suffixes for a container
1405 *
1406 * @param $container string
1407 * @return Array
1408 */
1409 final protected function getContainerSuffixes( $container ) {
1410 $shards = array();
1411 list( $digits, $base ) = $this->getContainerHashLevels( $container );
1412 if ( $digits > 0 ) {
1413 $numShards = pow( $base, $digits );
1414 for ( $index = 0; $index < $numShards; $index++ ) {
1415 $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
1416 }
1417 }
1418 return $shards;
1419 }
1420
1421 /**
1422 * Get the full container name, including the wiki ID prefix
1423 *
1424 * @param $container string
1425 * @return string
1426 */
1427 final protected function fullContainerName( $container ) {
1428 if ( $this->wikiId != '' ) {
1429 return "{$this->wikiId}-$container";
1430 } else {
1431 return $container;
1432 }
1433 }
1434
1435 /**
1436 * Resolve a container name, checking if it's allowed by the backend.
1437 * This is intended for internal use, such as encoding illegal chars.
1438 * Subclasses can override this to be more restrictive.
1439 *
1440 * @param $container string
1441 * @return string|null
1442 */
1443 protected function resolveContainerName( $container ) {
1444 return $container;
1445 }
1446
1447 /**
1448 * Resolve a relative storage path, checking if it's allowed by the backend.
1449 * This is intended for internal use, such as encoding illegal chars or perhaps
1450 * getting absolute paths (e.g. FS based backends). Note that the relative path
1451 * may be the empty string (e.g. the path is simply to the container).
1452 *
1453 * @param $container string Container name
1454 * @param $relStoragePath string Storage path relative to the container
1455 * @return string|null Path or null if not valid
1456 */
1457 protected function resolveContainerPath( $container, $relStoragePath ) {
1458 return $relStoragePath;
1459 }
1460
1461 /**
1462 * Get the cache key for a container
1463 *
1464 * @param $container string Resolved container name
1465 * @return string
1466 */
1467 private function containerCacheKey( $container ) {
1468 return wfMemcKey( 'backend', $this->getName(), 'container', $container );
1469 }
1470
1471 /**
1472 * Set the cached info for a container
1473 *
1474 * @param $container string Resolved container name
1475 * @param $val mixed Information to cache
1476 */
1477 final protected function setContainerCache( $container, $val ) {
1478 $this->memCache->add( $this->containerCacheKey( $container ), $val, 14*86400 );
1479 }
1480
1481 /**
1482 * Delete the cached info for a container.
1483 * The cache key is salted for a while to prevent race conditions.
1484 *
1485 * @param $container string Resolved container name
1486 */
1487 final protected function deleteContainerCache( $container ) {
1488 if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) {
1489 trigger_error( "Unable to delete stat cache for container $container." );
1490 }
1491 }
1492
1493 /**
1494 * Do a batch lookup from cache for container stats for all containers
1495 * used in a list of container names, storage paths, or FileOp objects.
1496 * This loads the persistent cache values into the process cache.
1497 *
1498 * @param $items Array
1499 * @return void
1500 */
1501 final protected function primeContainerCache( array $items ) {
1502 wfProfileIn( __METHOD__ );
1503 wfProfileIn( __METHOD__ . '-' . $this->name );
1504
1505 $paths = array(); // list of storage paths
1506 $contNames = array(); // (cache key => resolved container name)
1507 // Get all the paths/containers from the items...
1508 foreach ( $items as $item ) {
1509 if ( $item instanceof FileOp ) {
1510 $paths = array_merge( $paths, $item->storagePathsRead() );
1511 $paths = array_merge( $paths, $item->storagePathsChanged() );
1512 } elseif ( self::isStoragePath( $item ) ) {
1513 $paths[] = $item;
1514 } elseif ( is_string( $item ) ) { // full container name
1515 $contNames[$this->containerCacheKey( $item )] = $item;
1516 }
1517 }
1518 // Get all the corresponding cache keys for paths...
1519 foreach ( $paths as $path ) {
1520 list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
1521 if ( $fullCont !== null ) { // valid path for this backend
1522 $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
1523 }
1524 }
1525
1526 $contInfo = array(); // (resolved container name => cache value)
1527 // Get all cache entries for these container cache keys...
1528 $values = $this->memCache->getMulti( array_keys( $contNames ) );
1529 foreach ( $values as $cacheKey => $val ) {
1530 $contInfo[$contNames[$cacheKey]] = $val;
1531 }
1532
1533 // Populate the container process cache for the backend...
1534 $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
1535
1536 wfProfileOut( __METHOD__ . '-' . $this->name );
1537 wfProfileOut( __METHOD__ );
1538 }
1539
1540 /**
1541 * Fill the backend-specific process cache given an array of
1542 * resolved container names and their corresponding cached info.
1543 * Only containers that actually exist should appear in the map.
1544 *
1545 * @param $containerInfo Array Map of resolved container names to cached info
1546 * @return void
1547 */
1548 protected function doPrimeContainerCache( array $containerInfo ) {}
1549
1550 /**
1551 * Get the cache key for a file path
1552 *
1553 * @param $path string Normalized storage path
1554 * @return string
1555 */
1556 private function fileCacheKey( $path ) {
1557 return wfMemcKey( 'backend', $this->getName(), 'file', sha1( $path ) );
1558 }
1559
1560 /**
1561 * Set the cached stat info for a file path.
1562 * Negatives (404s) are not cached. By not caching negatives, we can skip cache
1563 * salting for the case when a file is created at a path were there was none before.
1564 *
1565 * @param $path string Storage path
1566 * @param $val mixed Information to cache
1567 */
1568 final protected function setFileCache( $path, $val ) {
1569 $path = FileBackend::normalizeStoragePath( $path );
1570 if ( $path === null ) {
1571 return; // invalid storage path
1572 }
1573 $this->memCache->add( $this->fileCacheKey( $path ), $val, 7*86400 );
1574 }
1575
1576 /**
1577 * Delete the cached stat info for a file path.
1578 * The cache key is salted for a while to prevent race conditions.
1579 *
1580 * @param $path string Storage path
1581 */
1582 final protected function deleteFileCache( $path ) {
1583 $path = FileBackend::normalizeStoragePath( $path );
1584 if ( $path === null ) {
1585 return; // invalid storage path
1586 }
1587 if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) {
1588 trigger_error( "Unable to delete stat cache for file $path." );
1589 }
1590 }
1591
1592 /**
1593 * Do a batch lookup from cache for file stats for all paths
1594 * used in a list of storage paths or FileOp objects.
1595 * This loads the persistent cache values into the process cache.
1596 *
1597 * @param $items Array List of storage paths or FileOps
1598 * @return void
1599 */
1600 final protected function primeFileCache( array $items ) {
1601 wfProfileIn( __METHOD__ );
1602 wfProfileIn( __METHOD__ . '-' . $this->name );
1603
1604 $paths = array(); // list of storage paths
1605 $pathNames = array(); // (cache key => storage path)
1606 // Get all the paths/containers from the items...
1607 foreach ( $items as $item ) {
1608 if ( $item instanceof FileOp ) {
1609 $paths = array_merge( $paths, $item->storagePathsRead() );
1610 $paths = array_merge( $paths, $item->storagePathsChanged() );
1611 } elseif ( self::isStoragePath( $item ) ) {
1612 $paths[] = FileBackend::normalizeStoragePath( $item );
1613 }
1614 }
1615 // Get rid of any paths that failed normalization...
1616 $paths = array_filter( $paths, 'strlen' ); // remove nulls
1617 // Get all the corresponding cache keys for paths...
1618 foreach ( $paths as $path ) {
1619 list( $cont, $rel, $s ) = $this->resolveStoragePath( $path );
1620 if ( $rel !== null ) { // valid path for this backend
1621 $pathNames[$this->fileCacheKey( $path )] = $path;
1622 }
1623 }
1624 // Get all cache entries for these container cache keys...
1625 $values = $this->memCache->getMulti( array_keys( $pathNames ) );
1626 foreach ( $values as $cacheKey => $val ) {
1627 if ( is_array( $val ) ) {
1628 $path = $pathNames[$cacheKey];
1629 $this->cheapCache->set( $path, 'stat', $val );
1630 if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
1631 $this->cheapCache->set( $path, 'sha1',
1632 array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
1633 }
1634 }
1635 }
1636
1637 wfProfileOut( __METHOD__ . '-' . $this->name );
1638 wfProfileOut( __METHOD__ );
1639 }
1640
1641 /**
1642 * Set the 'concurrency' option from a list of operation options
1643 *
1644 * @param $opts array Map of operation options
1645 * @return Array
1646 */
1647 final protected function setConcurrencyFlags( array $opts ) {
1648 $opts['concurrency'] = 1; // off
1649 if ( $this->parallelize === 'implicit' ) {
1650 if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
1651 $opts['concurrency'] = $this->concurrency;
1652 }
1653 } elseif ( $this->parallelize === 'explicit' ) {
1654 if ( !empty( $opts['parallelize'] ) ) {
1655 $opts['concurrency'] = $this->concurrency;
1656 }
1657 }
1658 return $opts;
1659 }
1660 }
1661
1662 /**
1663 * FileBackendStore helper class for performing asynchronous file operations.
1664 *
1665 * For example, calling FileBackendStore::createInternal() with the "async"
1666 * param flag may result in a Status that contains this object as a value.
1667 * This class is largely backend-specific and is mostly just "magic" to be
1668 * passed to FileBackendStore::executeOpHandlesInternal().
1669 */
1670 abstract class FileBackendStoreOpHandle {
1671 /** @var Array */
1672 public $params = array(); // params to caller functions
1673 /** @var FileBackendStore */
1674 public $backend;
1675 /** @var Array */
1676 public $resourcesToClose = array();
1677
1678 public $call; // string; name that identifies the function called
1679
1680 /**
1681 * Close all open file handles
1682 *
1683 * @return void
1684 */
1685 public function closeResources() {
1686 array_map( 'fclose', $this->resourcesToClose );
1687 }
1688 }
1689
1690 /**
1691 * FileBackendStore helper function to handle listings that span container shards.
1692 * Do not use this class from places outside of FileBackendStore.
1693 *
1694 * @ingroup FileBackend
1695 */
1696 abstract class FileBackendStoreShardListIterator implements Iterator {
1697 /** @var FileBackendStore */
1698 protected $backend;
1699 /** @var Array */
1700 protected $params;
1701 /** @var Array */
1702 protected $shardSuffixes;
1703 protected $container; // string; full container name
1704 protected $directory; // string; resolved relative path
1705
1706 /** @var Traversable */
1707 protected $iter;
1708 protected $curShard = 0; // integer
1709 protected $pos = 0; // integer
1710
1711 /** @var Array */
1712 protected $multiShardPaths = array(); // (rel path => 1)
1713
1714 /**
1715 * @param $backend FileBackendStore
1716 * @param $container string Full storage container name
1717 * @param $dir string Storage directory relative to container
1718 * @param $suffixes Array List of container shard suffixes
1719 * @param $params Array
1720 */
1721 public function __construct(
1722 FileBackendStore $backend, $container, $dir, array $suffixes, array $params
1723 ) {
1724 $this->backend = $backend;
1725 $this->container = $container;
1726 $this->directory = $dir;
1727 $this->shardSuffixes = $suffixes;
1728 $this->params = $params;
1729 }
1730
1731 /**
1732 * @see Iterator::key()
1733 * @return integer
1734 */
1735 public function key() {
1736 return $this->pos;
1737 }
1738
1739 /**
1740 * @see Iterator::valid()
1741 * @return bool
1742 */
1743 public function valid() {
1744 if ( $this->iter instanceof Iterator ) {
1745 return $this->iter->valid();
1746 } elseif ( is_array( $this->iter ) ) {
1747 return ( current( $this->iter ) !== false ); // no paths can have this value
1748 }
1749 return false; // some failure?
1750 }
1751
1752 /**
1753 * @see Iterator::current()
1754 * @return string|bool String or false
1755 */
1756 public function current() {
1757 return ( $this->iter instanceof Iterator )
1758 ? $this->iter->current()
1759 : current( $this->iter );
1760 }
1761
1762 /**
1763 * @see Iterator::next()
1764 * @return void
1765 */
1766 public function next() {
1767 ++$this->pos;
1768 ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter );
1769 do {
1770 $continue = false; // keep scanning shards?
1771 $this->filterViaNext(); // filter out duplicates
1772 // Find the next non-empty shard if no elements are left
1773 if ( !$this->valid() ) {
1774 $this->nextShardIteratorIfNotValid();
1775 $continue = $this->valid(); // re-filter unless we ran out of shards
1776 }
1777 } while ( $continue );
1778 }
1779
1780 /**
1781 * @see Iterator::rewind()
1782 * @return void
1783 */
1784 public function rewind() {
1785 $this->pos = 0;
1786 $this->curShard = 0;
1787 $this->setIteratorFromCurrentShard();
1788 do {
1789 $continue = false; // keep scanning shards?
1790 $this->filterViaNext(); // filter out duplicates
1791 // Find the next non-empty shard if no elements are left
1792 if ( !$this->valid() ) {
1793 $this->nextShardIteratorIfNotValid();
1794 $continue = $this->valid(); // re-filter unless we ran out of shards
1795 }
1796 } while ( $continue );
1797 }
1798
1799 /**
1800 * Filter out duplicate items by advancing to the next ones
1801 */
1802 protected function filterViaNext() {
1803 while ( $this->valid() ) {
1804 $rel = $this->iter->current(); // path relative to given directory
1805 $path = $this->params['dir'] . "/{$rel}"; // full storage path
1806 if ( $this->backend->isSingleShardPathInternal( $path ) ) {
1807 break; // path is only on one shard; no issue with duplicates
1808 } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
1809 // Don't keep listing paths that are on multiple shards
1810 ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter );
1811 } else {
1812 $this->multiShardPaths[$rel] = 1;
1813 break;
1814 }
1815 }
1816 }
1817
1818 /**
1819 * If the list iterator for this container shard is out of items,
1820 * then move on to the next container that has items.
1821 * If there are none, then it advances to the last container.
1822 */
1823 protected function nextShardIteratorIfNotValid() {
1824 while ( !$this->valid() && ++$this->curShard < count( $this->shardSuffixes ) ) {
1825 $this->setIteratorFromCurrentShard();
1826 }
1827 }
1828
1829 /**
1830 * Set the list iterator to that of the current container shard
1831 */
1832 protected function setIteratorFromCurrentShard() {
1833 $this->iter = $this->listFromShard(
1834 $this->container . $this->shardSuffixes[$this->curShard],
1835 $this->directory, $this->params );
1836 // Start loading results so that current() works
1837 if ( $this->iter ) {
1838 ( $this->iter instanceof Iterator ) ? $this->iter->rewind() : reset( $this->iter );
1839 }
1840 }
1841
1842 /**
1843 * Get the list for a given container shard
1844 *
1845 * @param $container string Resolved container name
1846 * @param $dir string Resolved path relative to container
1847 * @param $params Array
1848 * @return Traversable|Array|null
1849 */
1850 abstract protected function listFromShard( $container, $dir, array $params );
1851 }
1852
1853 /**
1854 * Iterator for listing directories
1855 */
1856 class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
1857 /**
1858 * @see FileBackendStoreShardListIterator::listFromShard()
1859 * @return Array|null|Traversable
1860 */
1861 protected function listFromShard( $container, $dir, array $params ) {
1862 return $this->backend->getDirectoryListInternal( $container, $dir, $params );
1863 }
1864 }
1865
1866 /**
1867 * Iterator for listing regular files
1868 */
1869 class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
1870 /**
1871 * @see FileBackendStoreShardListIterator::listFromShard()
1872 * @return Array|null|Traversable
1873 */
1874 protected function listFromShard( $container, $dir, array $params ) {
1875 return $this->backend->getFileListInternal( $container, $dir, $params );
1876 }
1877 }