MediaWiki  master
SwiftFileBackend.php
Go to the documentation of this file.
1 <?php
37  protected $http;
38 
40  protected $authTTL;
41 
43  protected $swiftAuthUrl;
44 
46  protected $swiftUser;
47 
49  protected $swiftKey;
50 
52  protected $swiftTempUrlKey;
53 
55  protected $rgwS3AccessKey;
56 
58  protected $rgwS3SecretKey;
59 
61  protected $srvCache;
62 
65 
67  protected $authCreds;
68 
70  protected $authSessionTimestamp = 0;
71 
73  protected $authErrorTimestamp = null;
74 
76  protected $isRGW = false;
77 
106  public function __construct( array $config ) {
107  parent::__construct( $config );
108  // Required settings
109  $this->swiftAuthUrl = $config['swiftAuthUrl'];
110  $this->swiftUser = $config['swiftUser'];
111  $this->swiftKey = $config['swiftKey'];
112  // Optional settings
113  $this->authTTL = isset( $config['swiftAuthTTL'] )
114  ? $config['swiftAuthTTL']
115  : 15 * 60; // some sane number
116  $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
117  ? $config['swiftTempUrlKey']
118  : '';
119  $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
120  ? $config['shardViaHashLevels']
121  : '';
122  $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
123  ? $config['rgwS3AccessKey']
124  : '';
125  $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
126  ? $config['rgwS3SecretKey']
127  : '';
128  // HTTP helper client
129  $this->http = new MultiHttpClient( [] );
130  // Cache container information to mask latency
131  if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
132  $this->memCache = $config['wanCache'];
133  }
134  // Process cache for container info
135  $this->containerStatCache = new ProcessCacheLRU( 300 );
136  // Cache auth token information to avoid RTTs
137  if ( !empty( $config['cacheAuthInfo'] ) ) {
138  if ( PHP_SAPI === 'cli' ) {
139  // Preferrably memcached
140  $this->srvCache = ObjectCache::getLocalClusterInstance();
141  } else {
142  // Look for APC, XCache, WinCache, ect...
143  $this->srvCache = ObjectCache::getLocalServerInstance( CACHE_NONE );
144  }
145  } else {
146  $this->srvCache = new EmptyBagOStuff();
147  }
148  }
149 
150  public function getFeatures() {
153  }
154 
155  protected function resolveContainerPath( $container, $relStoragePath ) {
156  if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
157  return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
158  } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
159  return null; // too long for Swift
160  }
161 
162  return $relStoragePath;
163  }
164 
165  public function isPathUsableInternal( $storagePath ) {
166  list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
167  if ( $rel === null ) {
168  return false; // invalid
169  }
170 
171  return is_array( $this->getContainerStat( $container ) );
172  }
173 
181  protected function sanitizeHdrs( array $params ) {
182  return isset( $params['headers'] )
183  ? $this->getCustomHeaders( $params['headers'] )
184  : [];
185 
186  }
187 
192  protected function getCustomHeaders( array $rawHeaders ) {
193  $headers = [];
194 
195  // Normalize casing, and strip out illegal headers
196  foreach ( $rawHeaders as $name => $value ) {
197  $name = strtolower( $name );
198  if ( preg_match( '/^content-(type|length)$/', $name ) ) {
199  continue; // blacklisted
200  } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
201  $headers[$name] = $value; // allowed
202  } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
203  $headers[$name] = $value; // allowed
204  }
205  }
206  // By default, Swift has annoyingly low maximum header value limits
207  if ( isset( $headers['content-disposition'] ) ) {
208  $disposition = '';
209  // @note: assume FileBackend::makeContentDisposition() already used
210  foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
211  $part = trim( $part );
212  $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
213  if ( strlen( $new ) <= 255 ) {
214  $disposition = $new;
215  } else {
216  break; // too long; sigh
217  }
218  }
219  $headers['content-disposition'] = $disposition;
220  }
221 
222  return $headers;
223  }
224 
229  protected function getMetadataHeaders( array $rawHeaders ) {
230  $headers = [];
231  foreach ( $rawHeaders as $name => $value ) {
232  $name = strtolower( $name );
233  if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
234  $headers[$name] = $value;
235  }
236  }
237 
238  return $headers;
239  }
240 
245  protected function getMetadata( array $rawHeaders ) {
246  $metadata = [];
247  foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
248  $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
249  }
250 
251  return $metadata;
252  }
253 
254  protected function doCreateInternal( array $params ) {
256 
257  list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
258  if ( $dstRel === null ) {
259  $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
260 
261  return $status;
262  }
263 
264  $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
265  $contentType = isset( $params['headers']['content-type'] )
266  ? $params['headers']['content-type']
267  : $this->getContentType( $params['dst'], $params['content'], null );
268 
269  $reqs = [ [
270  'method' => 'PUT',
271  'url' => [ $dstCont, $dstRel ],
272  'headers' => [
273  'content-length' => strlen( $params['content'] ),
274  'etag' => md5( $params['content'] ),
275  'content-type' => $contentType,
276  'x-object-meta-sha1base36' => $sha1Hash
277  ] + $this->sanitizeHdrs( $params ),
278  'body' => $params['content']
279  ] ];
280 
281  $method = __METHOD__;
282  $handler = function ( array $request, Status $status ) use ( $method, $params ) {
283  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
284  if ( $rcode === 201 ) {
285  // good
286  } elseif ( $rcode === 412 ) {
287  $status->fatal( 'backend-fail-contenttype', $params['dst'] );
288  } else {
289  $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
290  }
291  };
292 
293  $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
294  if ( !empty( $params['async'] ) ) { // deferred
295  $status->value = $opHandle;
296  } else { // actually write the object in Swift
297  $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
298  }
299 
300  return $status;
301  }
302 
303  protected function doStoreInternal( array $params ) {
305 
306  list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
307  if ( $dstRel === null ) {
308  $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
309 
310  return $status;
311  }
312 
313  MediaWiki\suppressWarnings();
314  $sha1Hash = sha1_file( $params['src'] );
315  MediaWiki\restoreWarnings();
316  if ( $sha1Hash === false ) { // source doesn't exist?
317  $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
318 
319  return $status;
320  }
321  $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
322  $contentType = isset( $params['headers']['content-type'] )
323  ? $params['headers']['content-type']
324  : $this->getContentType( $params['dst'], null, $params['src'] );
325 
326  $handle = fopen( $params['src'], 'rb' );
327  if ( $handle === false ) { // source doesn't exist?
328  $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
329 
330  return $status;
331  }
332 
333  $reqs = [ [
334  'method' => 'PUT',
335  'url' => [ $dstCont, $dstRel ],
336  'headers' => [
337  'content-length' => filesize( $params['src'] ),
338  'etag' => md5_file( $params['src'] ),
339  'content-type' => $contentType,
340  'x-object-meta-sha1base36' => $sha1Hash
341  ] + $this->sanitizeHdrs( $params ),
342  'body' => $handle // resource
343  ] ];
344 
345  $method = __METHOD__;
346  $handler = function ( array $request, Status $status ) use ( $method, $params ) {
347  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
348  if ( $rcode === 201 ) {
349  // good
350  } elseif ( $rcode === 412 ) {
351  $status->fatal( 'backend-fail-contenttype', $params['dst'] );
352  } else {
353  $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
354  }
355  };
356 
357  $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
358  if ( !empty( $params['async'] ) ) { // deferred
359  $status->value = $opHandle;
360  } else { // actually write the object in Swift
361  $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
362  }
363 
364  return $status;
365  }
366 
367  protected function doCopyInternal( array $params ) {
369 
370  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
371  if ( $srcRel === null ) {
372  $status->fatal( 'backend-fail-invalidpath', $params['src'] );
373 
374  return $status;
375  }
376 
377  list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
378  if ( $dstRel === null ) {
379  $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
380 
381  return $status;
382  }
383 
384  $reqs = [ [
385  'method' => 'PUT',
386  'url' => [ $dstCont, $dstRel ],
387  'headers' => [
388  'x-copy-from' => '/' . rawurlencode( $srcCont ) .
389  '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
390  ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
391  ] ];
392 
393  $method = __METHOD__;
394  $handler = function ( array $request, Status $status ) use ( $method, $params ) {
395  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
396  if ( $rcode === 201 ) {
397  // good
398  } elseif ( $rcode === 404 ) {
399  $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
400  } else {
401  $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
402  }
403  };
404 
405  $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
406  if ( !empty( $params['async'] ) ) { // deferred
407  $status->value = $opHandle;
408  } else { // actually write the object in Swift
409  $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
410  }
411 
412  return $status;
413  }
414 
415  protected function doMoveInternal( array $params ) {
417 
418  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
419  if ( $srcRel === null ) {
420  $status->fatal( 'backend-fail-invalidpath', $params['src'] );
421 
422  return $status;
423  }
424 
425  list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
426  if ( $dstRel === null ) {
427  $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
428 
429  return $status;
430  }
431 
432  $reqs = [
433  [
434  'method' => 'PUT',
435  'url' => [ $dstCont, $dstRel ],
436  'headers' => [
437  'x-copy-from' => '/' . rawurlencode( $srcCont ) .
438  '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
439  ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
440  ]
441  ];
442  if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
443  $reqs[] = [
444  'method' => 'DELETE',
445  'url' => [ $srcCont, $srcRel ],
446  'headers' => []
447  ];
448  }
449 
450  $method = __METHOD__;
451  $handler = function ( array $request, Status $status ) use ( $method, $params ) {
452  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
453  if ( $request['method'] === 'PUT' && $rcode === 201 ) {
454  // good
455  } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
456  // good
457  } elseif ( $rcode === 404 ) {
458  $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
459  } else {
460  $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
461  }
462  };
463 
464  $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
465  if ( !empty( $params['async'] ) ) { // deferred
466  $status->value = $opHandle;
467  } else { // actually move the object in Swift
468  $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
469  }
470 
471  return $status;
472  }
473 
474  protected function doDeleteInternal( array $params ) {
476 
477  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
478  if ( $srcRel === null ) {
479  $status->fatal( 'backend-fail-invalidpath', $params['src'] );
480 
481  return $status;
482  }
483 
484  $reqs = [ [
485  'method' => 'DELETE',
486  'url' => [ $srcCont, $srcRel ],
487  'headers' => []
488  ] ];
489 
490  $method = __METHOD__;
491  $handler = function ( array $request, Status $status ) use ( $method, $params ) {
492  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
493  if ( $rcode === 204 ) {
494  // good
495  } elseif ( $rcode === 404 ) {
496  if ( empty( $params['ignoreMissingSource'] ) ) {
497  $status->fatal( 'backend-fail-delete', $params['src'] );
498  }
499  } else {
500  $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
501  }
502  };
503 
504  $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
505  if ( !empty( $params['async'] ) ) { // deferred
506  $status->value = $opHandle;
507  } else { // actually delete the object in Swift
508  $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
509  }
510 
511  return $status;
512  }
513 
514  protected function doDescribeInternal( array $params ) {
516 
517  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
518  if ( $srcRel === null ) {
519  $status->fatal( 'backend-fail-invalidpath', $params['src'] );
520 
521  return $status;
522  }
523 
524  // Fetch the old object headers/metadata...this should be in stat cache by now
525  $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
526  if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
527  $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
528  }
529  if ( !$stat ) {
530  $status->fatal( 'backend-fail-describe', $params['src'] );
531 
532  return $status;
533  }
534 
535  // POST clears prior headers, so we need to merge the changes in to the old ones
536  $metaHdrs = [];
537  foreach ( $stat['xattr']['metadata'] as $name => $value ) {
538  $metaHdrs["x-object-meta-$name"] = $value;
539  }
540  $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
541 
542  $reqs = [ [
543  'method' => 'POST',
544  'url' => [ $srcCont, $srcRel ],
545  'headers' => $metaHdrs + $customHdrs
546  ] ];
547 
548  $method = __METHOD__;
549  $handler = function ( array $request, Status $status ) use ( $method, $params ) {
550  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
551  if ( $rcode === 202 ) {
552  // good
553  } elseif ( $rcode === 404 ) {
554  $status->fatal( 'backend-fail-describe', $params['src'] );
555  } else {
556  $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
557  }
558  };
559 
560  $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
561  if ( !empty( $params['async'] ) ) { // deferred
562  $status->value = $opHandle;
563  } else { // actually change the object in Swift
564  $status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
565  }
566 
567  return $status;
568  }
569 
570  protected function doPrepareInternal( $fullCont, $dir, array $params ) {
572 
573  // (a) Check if container already exists
574  $stat = $this->getContainerStat( $fullCont );
575  if ( is_array( $stat ) ) {
576  return $status; // already there
577  } elseif ( $stat === null ) {
578  $status->fatal( 'backend-fail-internal', $this->name );
579  wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
580 
581  return $status;
582  }
583 
584  // (b) Create container as needed with proper ACLs
585  if ( $stat === false ) {
586  $params['op'] = 'prepare';
587  $status->merge( $this->createContainer( $fullCont, $params ) );
588  }
589 
590  return $status;
591  }
592 
593  protected function doSecureInternal( $fullCont, $dir, array $params ) {
595  if ( empty( $params['noAccess'] ) ) {
596  return $status; // nothing to do
597  }
598 
599  $stat = $this->getContainerStat( $fullCont );
600  if ( is_array( $stat ) ) {
601  // Make container private to end-users...
602  $status->merge( $this->setContainerAccess(
603  $fullCont,
604  [ $this->swiftUser ], // read
605  [ $this->swiftUser ] // write
606  ) );
607  } elseif ( $stat === false ) {
608  $status->fatal( 'backend-fail-usable', $params['dir'] );
609  } else {
610  $status->fatal( 'backend-fail-internal', $this->name );
611  wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
612  }
613 
614  return $status;
615  }
616 
617  protected function doPublishInternal( $fullCont, $dir, array $params ) {
619 
620  $stat = $this->getContainerStat( $fullCont );
621  if ( is_array( $stat ) ) {
622  // Make container public to end-users...
623  $status->merge( $this->setContainerAccess(
624  $fullCont,
625  [ $this->swiftUser, '.r:*' ], // read
626  [ $this->swiftUser ] // write
627  ) );
628  } elseif ( $stat === false ) {
629  $status->fatal( 'backend-fail-usable', $params['dir'] );
630  } else {
631  $status->fatal( 'backend-fail-internal', $this->name );
632  wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
633  }
634 
635  return $status;
636  }
637 
638  protected function doCleanInternal( $fullCont, $dir, array $params ) {
640 
641  // Only containers themselves can be removed, all else is virtual
642  if ( $dir != '' ) {
643  return $status; // nothing to do
644  }
645 
646  // (a) Check the container
647  $stat = $this->getContainerStat( $fullCont, true );
648  if ( $stat === false ) {
649  return $status; // ok, nothing to do
650  } elseif ( !is_array( $stat ) ) {
651  $status->fatal( 'backend-fail-internal', $this->name );
652  wfDebugLog( 'SwiftBackend', __METHOD__ . ': cannot get container stat' );
653 
654  return $status;
655  }
656 
657  // (b) Delete the container if empty
658  if ( $stat['count'] == 0 ) {
659  $params['op'] = 'clean';
660  $status->merge( $this->deleteContainer( $fullCont, $params ) );
661  }
662 
663  return $status;
664  }
665 
666  protected function doGetFileStat( array $params ) {
667  $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
668  unset( $params['src'] );
669  $stats = $this->doGetFileStatMulti( $params );
670 
671  return reset( $stats );
672  }
673 
684  protected function convertSwiftDate( $ts, $format = TS_MW ) {
685  try {
686  $timestamp = new MWTimestamp( $ts );
687 
688  return $timestamp->getTimestamp( $format );
689  } catch ( Exception $e ) {
690  throw new FileBackendError( $e->getMessage() );
691  }
692  }
693 
701  protected function addMissingMetadata( array $objHdrs, $path ) {
702  if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
703  return $objHdrs; // nothing to do
704  }
705 
707  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
708  wfDebugLog( 'SwiftBackend', __METHOD__ . ": $path was not stored with SHA-1 metadata." );
709 
710  $objHdrs['x-object-meta-sha1base36'] = false;
711 
712  $auth = $this->getAuthentication();
713  if ( !$auth ) {
714  return $objHdrs; // failed
715  }
716 
717  // Find prior custom HTTP headers
718  $postHeaders = $this->getCustomHeaders( $objHdrs );
719  // Find prior metadata headers
720  $postHeaders += $this->getMetadataHeaders( $objHdrs );
721 
724  $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
725  if ( $status->isOK() ) {
726  $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
727  if ( $tmpFile ) {
728  $hash = $tmpFile->getSha1Base36();
729  if ( $hash !== false ) {
730  $objHdrs['x-object-meta-sha1base36'] = $hash;
731  // Merge new SHA1 header into the old ones
732  $postHeaders['x-object-meta-sha1base36'] = $hash;
733  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
734  list( $rcode ) = $this->http->run( [
735  'method' => 'POST',
736  'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
737  'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
738  ] );
739  if ( $rcode >= 200 && $rcode <= 299 ) {
740  $this->deleteFileCache( $path );
741 
742  return $objHdrs; // success
743  }
744  }
745  }
746  }
747 
748  wfDebugLog( 'SwiftBackend', __METHOD__ . ": unable to set SHA-1 metadata for $path" );
749 
750  return $objHdrs; // failed
751  }
752 
753  protected function doGetFileContentsMulti( array $params ) {
754  $contents = [];
755 
756  $auth = $this->getAuthentication();
757 
758  $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
759  // Blindly create tmp files and stream to them, catching any exception if the file does
760  // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
761  $reqs = []; // (path => op)
762 
763  foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
764  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
765  if ( $srcRel === null || !$auth ) {
766  $contents[$path] = false;
767  continue;
768  }
769  // Create a new temporary memory file...
770  $handle = fopen( 'php://temp', 'wb' );
771  if ( $handle ) {
772  $reqs[$path] = [
773  'method' => 'GET',
774  'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
775  'headers' => $this->authTokenHeaders( $auth )
776  + $this->headersFromParams( $params ),
777  'stream' => $handle,
778  ];
779  }
780  $contents[$path] = false;
781  }
782 
783  $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
784  $reqs = $this->http->runMulti( $reqs, $opts );
785  foreach ( $reqs as $path => $op ) {
786  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
787  if ( $rcode >= 200 && $rcode <= 299 ) {
788  rewind( $op['stream'] ); // start from the beginning
789  $contents[$path] = stream_get_contents( $op['stream'] );
790  } elseif ( $rcode === 404 ) {
791  $contents[$path] = false;
792  } else {
793  $this->onError( null, __METHOD__,
794  [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
795  }
796  fclose( $op['stream'] ); // close open handle
797  }
798 
799  return $contents;
800  }
801 
802  protected function doDirectoryExists( $fullCont, $dir, array $params ) {
803  $prefix = ( $dir == '' ) ? null : "{$dir}/";
804  $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
805  if ( $status->isOK() ) {
806  return ( count( $status->value ) ) > 0;
807  }
808 
809  return null; // error
810  }
811 
819  public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
820  return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
821  }
822 
830  public function getFileListInternal( $fullCont, $dir, array $params ) {
831  return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
832  }
833 
845  public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
846  $dirs = [];
847  if ( $after === INF ) {
848  return $dirs; // nothing more
849  }
850 
851  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
852 
853  $prefix = ( $dir == '' ) ? null : "{$dir}/";
854  // Non-recursive: only list dirs right under $dir
855  if ( !empty( $params['topOnly'] ) ) {
856  $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
857  if ( !$status->isOK() ) {
858  throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
859  }
860  $objects = $status->value;
861  foreach ( $objects as $object ) { // files and directories
862  if ( substr( $object, -1 ) === '/' ) {
863  $dirs[] = $object; // directories end in '/'
864  }
865  }
866  } else {
867  // Recursive: list all dirs under $dir and its subdirs
868  $getParentDir = function ( $path ) {
869  return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
870  };
871 
872  // Get directory from last item of prior page
873  $lastDir = $getParentDir( $after ); // must be first page
874  $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
875 
876  if ( !$status->isOK() ) {
877  throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
878  }
879 
880  $objects = $status->value;
881 
882  foreach ( $objects as $object ) { // files
883  $objectDir = $getParentDir( $object ); // directory of object
884 
885  if ( $objectDir !== false && $objectDir !== $dir ) {
886  // Swift stores paths in UTF-8, using binary sorting.
887  // See function "create_container_table" in common/db.py.
888  // If a directory is not "greater" than the last one,
889  // then it was already listed by the calling iterator.
890  if ( strcmp( $objectDir, $lastDir ) > 0 ) {
891  $pDir = $objectDir;
892  do { // add dir and all its parent dirs
893  $dirs[] = "{$pDir}/";
894  $pDir = $getParentDir( $pDir );
895  } while ( $pDir !== false // sanity
896  && strcmp( $pDir, $lastDir ) > 0 // not done already
897  && strlen( $pDir ) > strlen( $dir ) // within $dir
898  );
899  }
900  $lastDir = $objectDir;
901  }
902  }
903  }
904  // Page on the unfiltered directory listing (what is returned may be filtered)
905  if ( count( $objects ) < $limit ) {
906  $after = INF; // avoid a second RTT
907  } else {
908  $after = end( $objects ); // update last item
909  }
910 
911  return $dirs;
912  }
913 
925  public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
926  $files = []; // list of (path, stat array or null) entries
927  if ( $after === INF ) {
928  return $files; // nothing more
929  }
930 
931  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
932 
933  $prefix = ( $dir == '' ) ? null : "{$dir}/";
934  // $objects will contain a list of unfiltered names or CF_Object items
935  // Non-recursive: only list files right under $dir
936  if ( !empty( $params['topOnly'] ) ) {
937  if ( !empty( $params['adviseStat'] ) ) {
938  $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
939  } else {
940  $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
941  }
942  } else {
943  // Recursive: list all files under $dir and its subdirs
944  if ( !empty( $params['adviseStat'] ) ) {
945  $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
946  } else {
947  $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
948  }
949  }
950 
951  // Reformat this list into a list of (name, stat array or null) entries
952  if ( !$status->isOK() ) {
953  throw new FileBackendError( "Iterator page I/O error: {$status->getMessage()}" );
954  }
955 
956  $objects = $status->value;
957  $files = $this->buildFileObjectListing( $params, $dir, $objects );
958 
959  // Page on the unfiltered object listing (what is returned may be filtered)
960  if ( count( $objects ) < $limit ) {
961  $after = INF; // avoid a second RTT
962  } else {
963  $after = end( $objects ); // update last item
964  $after = is_object( $after ) ? $after->name : $after;
965  }
966 
967  return $files;
968  }
969 
979  private function buildFileObjectListing( array $params, $dir, array $objects ) {
980  $names = [];
981  foreach ( $objects as $object ) {
982  if ( is_object( $object ) ) {
983  if ( isset( $object->subdir ) || !isset( $object->name ) ) {
984  continue; // virtual directory entry; ignore
985  }
986  $stat = [
987  // Convert various random Swift dates to TS_MW
988  'mtime' => $this->convertSwiftDate( $object->last_modified, TS_MW ),
989  'size' => (int)$object->bytes,
990  'sha1' => null,
991  // Note: manifiest ETags are not an MD5 of the file
992  'md5' => ctype_xdigit( $object->hash ) ? $object->hash : null,
993  'latest' => false // eventually consistent
994  ];
995  $names[] = [ $object->name, $stat ];
996  } elseif ( substr( $object, -1 ) !== '/' ) {
997  // Omit directories, which end in '/' in listings
998  $names[] = [ $object, null ];
999  }
1000  }
1001 
1002  return $names;
1003  }
1004 
1011  public function loadListingStatInternal( $path, array $val ) {
1012  $this->cheapCache->set( $path, 'stat', $val );
1013  }
1014 
1015  protected function doGetFileXAttributes( array $params ) {
1016  $stat = $this->getFileStat( $params );
1017  if ( $stat ) {
1018  if ( !isset( $stat['xattr'] ) ) {
1019  // Stat entries filled by file listings don't include metadata/headers
1020  $this->clearCache( [ $params['src'] ] );
1021  $stat = $this->getFileStat( $params );
1022  }
1023 
1024  return $stat['xattr'];
1025  } else {
1026  return false;
1027  }
1028  }
1029 
1030  protected function doGetFileSha1base36( array $params ) {
1031  $stat = $this->getFileStat( $params );
1032  if ( $stat ) {
1033  if ( !isset( $stat['sha1'] ) ) {
1034  // Stat entries filled by file listings don't include SHA1
1035  $this->clearCache( [ $params['src'] ] );
1036  $stat = $this->getFileStat( $params );
1037  }
1038 
1039  return $stat['sha1'];
1040  } else {
1041  return false;
1042  }
1043  }
1044 
1045  protected function doStreamFile( array $params ) {
1047 
1048  $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
1049 
1050  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1051  if ( $srcRel === null ) {
1052  StreamFile::send404Message( $params['src'], $flags );
1053  $status->fatal( 'backend-fail-invalidpath', $params['src'] );
1054 
1055  return $status;
1056  }
1057 
1058  $auth = $this->getAuthentication();
1059  if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
1060  StreamFile::send404Message( $params['src'], $flags );
1061  $status->fatal( 'backend-fail-stream', $params['src'] );
1062 
1063  return $status;
1064  }
1065 
1066  // If "headers" is set, we only want to send them if the file is there.
1067  // Do not bother checking if the file exists if headers are not set though.
1068  if ( $params['headers'] && !$this->fileExists( $params ) ) {
1069  StreamFile::send404Message( $params['src'], $flags );
1070  $status->fatal( 'backend-fail-stream', $params['src'] );
1071 
1072  return $status;
1073  }
1074 
1075  // Send the requested additional headers
1076  foreach ( $params['headers'] as $header ) {
1077  header( $header ); // aways send
1078  }
1079 
1080  if ( empty( $params['allowOB'] ) ) {
1081  // Cancel output buffering and gzipping if set
1083  }
1084 
1085  $handle = fopen( 'php://output', 'wb' );
1086  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1087  'method' => 'GET',
1088  'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1089  'headers' => $this->authTokenHeaders( $auth )
1090  + $this->headersFromParams( $params ) + $params['options'],
1091  'stream' => $handle,
1092  'flags' => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
1093  ] );
1094 
1095  if ( $rcode >= 200 && $rcode <= 299 ) {
1096  // good
1097  } elseif ( $rcode === 404 ) {
1098  $status->fatal( 'backend-fail-stream', $params['src'] );
1099  // Per bug 41113, nasty things can happen if bad cache entries get
1100  // stuck in cache. It's also possible that this error can come up
1101  // with simple race conditions. Clear out the stat cache to be safe.
1102  $this->clearCache( [ $params['src'] ] );
1103  $this->deleteFileCache( $params['src'] );
1104  } else {
1105  $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1106  }
1107 
1108  return $status;
1109  }
1110 
1111  protected function doGetLocalCopyMulti( array $params ) {
1112  $tmpFiles = [];
1113 
1114  $auth = $this->getAuthentication();
1115 
1116  $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
1117  // Blindly create tmp files and stream to them, catching any exception if the file does
1118  // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
1119  $reqs = []; // (path => op)
1120 
1121  foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
1122  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1123  if ( $srcRel === null || !$auth ) {
1124  $tmpFiles[$path] = null;
1125  continue;
1126  }
1127  // Get source file extension
1129  // Create a new temporary file...
1130  $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
1131  if ( $tmpFile ) {
1132  $handle = fopen( $tmpFile->getPath(), 'wb' );
1133  if ( $handle ) {
1134  $reqs[$path] = [
1135  'method' => 'GET',
1136  'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1137  'headers' => $this->authTokenHeaders( $auth )
1138  + $this->headersFromParams( $params ),
1139  'stream' => $handle,
1140  ];
1141  } else {
1142  $tmpFile = null;
1143  }
1144  }
1145  $tmpFiles[$path] = $tmpFile;
1146  }
1147 
1148  $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
1149  $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1150  $reqs = $this->http->runMulti( $reqs, $opts );
1151  foreach ( $reqs as $path => $op ) {
1152  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
1153  fclose( $op['stream'] ); // close open handle
1154  if ( $rcode >= 200 && $rcode <= 299 ) {
1155  $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
1156  // Double check that the disk is not full/broken
1157  if ( $size != $rhdrs['content-length'] ) {
1158  $tmpFiles[$path] = null;
1159  $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
1160  $this->onError( null, __METHOD__,
1161  [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1162  }
1163  // Set the file stat process cache in passing
1164  $stat = $this->getStatFromHeaders( $rhdrs );
1165  $stat['latest'] = $isLatest;
1166  $this->cheapCache->set( $path, 'stat', $stat );
1167  } elseif ( $rcode === 404 ) {
1168  $tmpFiles[$path] = false;
1169  } else {
1170  $tmpFiles[$path] = null;
1171  $this->onError( null, __METHOD__,
1172  [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1173  }
1174  }
1175 
1176  return $tmpFiles;
1177  }
1178 
1179  public function getFileHttpUrl( array $params ) {
1180  if ( $this->swiftTempUrlKey != '' ||
1181  ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
1182  ) {
1183  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1184  if ( $srcRel === null ) {
1185  return null; // invalid path
1186  }
1187 
1188  $auth = $this->getAuthentication();
1189  if ( !$auth ) {
1190  return null;
1191  }
1192 
1193  $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
1194  $expires = time() + $ttl;
1195 
1196  if ( $this->swiftTempUrlKey != '' ) {
1197  $url = $this->storageUrl( $auth, $srcCont, $srcRel );
1198  // Swift wants the signature based on the unencoded object name
1199  $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
1200  $signature = hash_hmac( 'sha1',
1201  "GET\n{$expires}\n{$contPath}/{$srcRel}",
1202  $this->swiftTempUrlKey
1203  );
1204 
1205  return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
1206  } else { // give S3 API URL for rgw
1207  // Path for signature starts with the bucket
1208  $spath = '/' . rawurlencode( $srcCont ) . '/' .
1209  str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1210  // Calculate the hash
1211  $signature = base64_encode( hash_hmac(
1212  'sha1',
1213  "GET\n\n\n{$expires}\n{$spath}",
1214  $this->rgwS3SecretKey,
1215  true // raw
1216  ) );
1217  // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
1218  // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
1219  return wfAppendQuery(
1220  str_replace( '/swift/v1', '', // S3 API is the rgw default
1221  $this->storageUrl( $auth ) . $spath ),
1222  [
1223  'Signature' => $signature,
1224  'Expires' => $expires,
1225  'AWSAccessKeyId' => $this->rgwS3AccessKey ]
1226  );
1227  }
1228  }
1229 
1230  return null;
1231  }
1232 
1233  protected function directoriesAreVirtual() {
1234  return true;
1235  }
1236 
1245  protected function headersFromParams( array $params ) {
1246  $hdrs = [];
1247  if ( !empty( $params['latest'] ) ) {
1248  $hdrs['x-newest'] = 'true';
1249  }
1250 
1251  return $hdrs;
1252  }
1253 
1259  protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1260  $statuses = [];
1261 
1262  $auth = $this->getAuthentication();
1263  if ( !$auth ) {
1264  foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1265  $statuses[$index] = Status::newFatal( 'backend-fail-connect', $this->name );
1266  }
1267 
1268  return $statuses;
1269  }
1270 
1271  // Split the HTTP requests into stages that can be done concurrently
1272  $httpReqsByStage = []; // map of (stage => index => HTTP request)
1273  foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1274  $reqs = $fileOpHandle->httpOp;
1275  // Convert the 'url' parameter to an actual URL using $auth
1276  foreach ( $reqs as $stage => &$req ) {
1277  list( $container, $relPath ) = $req['url'];
1278  $req['url'] = $this->storageUrl( $auth, $container, $relPath );
1279  $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
1280  $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
1281  $httpReqsByStage[$stage][$index] = $req;
1282  }
1283  $statuses[$index] = Status::newGood();
1284  }
1285 
1286  // Run all requests for the first stage, then the next, and so on
1287  $reqCount = count( $httpReqsByStage );
1288  for ( $stage = 0; $stage < $reqCount; ++$stage ) {
1289  $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
1290  foreach ( $httpReqs as $index => $httpReq ) {
1291  // Run the callback for each request of this operation
1292  $callback = $fileOpHandles[$index]->callback;
1293  call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
1294  // On failure, abort all remaining requests for this operation
1295  // (e.g. abort the DELETE request if the COPY request fails for a move)
1296  if ( !$statuses[$index]->isOK() ) {
1297  $stages = count( $fileOpHandles[$index]->httpOp );
1298  for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
1299  unset( $httpReqsByStage[$s][$index] );
1300  }
1301  }
1302  }
1303  }
1304 
1305  return $statuses;
1306  }
1307 
1330  protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
1332  $auth = $this->getAuthentication();
1333 
1334  if ( !$auth ) {
1335  $status->fatal( 'backend-fail-connect', $this->name );
1336 
1337  return $status;
1338  }
1339 
1340  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1341  'method' => 'POST',
1342  'url' => $this->storageUrl( $auth, $container ),
1343  'headers' => $this->authTokenHeaders( $auth ) + [
1344  'x-container-read' => implode( ',', $readGrps ),
1345  'x-container-write' => implode( ',', $writeGrps )
1346  ]
1347  ] );
1348 
1349  if ( $rcode != 204 && $rcode !== 202 ) {
1350  $status->fatal( 'backend-fail-internal', $this->name );
1351  wfDebugLog( 'SwiftBackend', __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
1352  }
1353 
1354  return $status;
1355  }
1356 
1365  protected function getContainerStat( $container, $bypassCache = false ) {
1366  $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
1367 
1368  if ( $bypassCache ) { // purge cache
1369  $this->containerStatCache->clear( $container );
1370  } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1371  $this->primeContainerCache( [ $container ] ); // check persistent cache
1372  }
1373  if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1374  $auth = $this->getAuthentication();
1375  if ( !$auth ) {
1376  return null;
1377  }
1378 
1379  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1380  'method' => 'HEAD',
1381  'url' => $this->storageUrl( $auth, $container ),
1382  'headers' => $this->authTokenHeaders( $auth )
1383  ] );
1384 
1385  if ( $rcode === 204 ) {
1386  $stat = [
1387  'count' => $rhdrs['x-container-object-count'],
1388  'bytes' => $rhdrs['x-container-bytes-used']
1389  ];
1390  if ( $bypassCache ) {
1391  return $stat;
1392  } else {
1393  $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
1394  $this->setContainerCache( $container, $stat ); // update persistent cache
1395  }
1396  } elseif ( $rcode === 404 ) {
1397  return false;
1398  } else {
1399  $this->onError( null, __METHOD__,
1400  [ 'cont' => $container ], $rerr, $rcode, $rdesc );
1401 
1402  return null;
1403  }
1404  }
1405 
1406  return $this->containerStatCache->get( $container, 'stat' );
1407  }
1408 
1416  protected function createContainer( $container, array $params ) {
1418 
1419  $auth = $this->getAuthentication();
1420  if ( !$auth ) {
1421  $status->fatal( 'backend-fail-connect', $this->name );
1422 
1423  return $status;
1424  }
1425 
1426  // @see SwiftFileBackend::setContainerAccess()
1427  if ( empty( $params['noAccess'] ) ) {
1428  $readGrps = [ '.r:*', $this->swiftUser ]; // public
1429  } else {
1430  $readGrps = [ $this->swiftUser ]; // private
1431  }
1432  $writeGrps = [ $this->swiftUser ]; // sanity
1433 
1434  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1435  'method' => 'PUT',
1436  'url' => $this->storageUrl( $auth, $container ),
1437  'headers' => $this->authTokenHeaders( $auth ) + [
1438  'x-container-read' => implode( ',', $readGrps ),
1439  'x-container-write' => implode( ',', $writeGrps )
1440  ]
1441  ] );
1442 
1443  if ( $rcode === 201 ) { // new
1444  // good
1445  } elseif ( $rcode === 202 ) { // already there
1446  // this shouldn't really happen, but is OK
1447  } else {
1448  $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1449  }
1450 
1451  return $status;
1452  }
1453 
1461  protected function deleteContainer( $container, array $params ) {
1463 
1464  $auth = $this->getAuthentication();
1465  if ( !$auth ) {
1466  $status->fatal( 'backend-fail-connect', $this->name );
1467 
1468  return $status;
1469  }
1470 
1471  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1472  'method' => 'DELETE',
1473  'url' => $this->storageUrl( $auth, $container ),
1474  'headers' => $this->authTokenHeaders( $auth )
1475  ] );
1476 
1477  if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
1478  $this->containerStatCache->clear( $container ); // purge
1479  } elseif ( $rcode === 404 ) { // not there
1480  // this shouldn't really happen, but is OK
1481  } elseif ( $rcode === 409 ) { // not empty
1482  $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
1483  } else {
1484  $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1485  }
1486 
1487  return $status;
1488  }
1489 
1502  private function objectListing(
1503  $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
1504  ) {
1506 
1507  $auth = $this->getAuthentication();
1508  if ( !$auth ) {
1509  $status->fatal( 'backend-fail-connect', $this->name );
1510 
1511  return $status;
1512  }
1513 
1514  $query = [ 'limit' => $limit ];
1515  if ( $type === 'info' ) {
1516  $query['format'] = 'json';
1517  }
1518  if ( $after !== null ) {
1519  $query['marker'] = $after;
1520  }
1521  if ( $prefix !== null ) {
1522  $query['prefix'] = $prefix;
1523  }
1524  if ( $delim !== null ) {
1525  $query['delimiter'] = $delim;
1526  }
1527 
1528  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1529  'method' => 'GET',
1530  'url' => $this->storageUrl( $auth, $fullCont ),
1531  'query' => $query,
1532  'headers' => $this->authTokenHeaders( $auth )
1533  ] );
1534 
1535  $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
1536  if ( $rcode === 200 ) { // good
1537  if ( $type === 'info' ) {
1538  $status->value = FormatJson::decode( trim( $rbody ) );
1539  } else {
1540  $status->value = explode( "\n", trim( $rbody ) );
1541  }
1542  } elseif ( $rcode === 204 ) {
1543  $status->value = []; // empty container
1544  } elseif ( $rcode === 404 ) {
1545  $status->value = []; // no container
1546  } else {
1547  $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1548  }
1549 
1550  return $status;
1551  }
1552 
1553  protected function doPrimeContainerCache( array $containerInfo ) {
1554  foreach ( $containerInfo as $container => $info ) {
1555  $this->containerStatCache->set( $container, 'stat', $info );
1556  }
1557  }
1558 
1559  protected function doGetFileStatMulti( array $params ) {
1560  $stats = [];
1561 
1562  $auth = $this->getAuthentication();
1563 
1564  $reqs = [];
1565  foreach ( $params['srcs'] as $path ) {
1566  list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1567  if ( $srcRel === null ) {
1568  $stats[$path] = false;
1569  continue; // invalid storage path
1570  } elseif ( !$auth ) {
1571  $stats[$path] = null;
1572  continue;
1573  }
1574 
1575  // (a) Check the container
1576  $cstat = $this->getContainerStat( $srcCont );
1577  if ( $cstat === false ) {
1578  $stats[$path] = false;
1579  continue; // ok, nothing to do
1580  } elseif ( !is_array( $cstat ) ) {
1581  $stats[$path] = null;
1582  continue;
1583  }
1584 
1585  $reqs[$path] = [
1586  'method' => 'HEAD',
1587  'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1588  'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
1589  ];
1590  }
1591 
1592  $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1593  $reqs = $this->http->runMulti( $reqs, $opts );
1594 
1595  foreach ( $params['srcs'] as $path ) {
1596  if ( array_key_exists( $path, $stats ) ) {
1597  continue; // some sort of failure above
1598  }
1599  // (b) Check the file
1600  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
1601  if ( $rcode === 200 || $rcode === 204 ) {
1602  // Update the object if it is missing some headers
1603  $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
1604  // Load the stat array from the headers
1605  $stat = $this->getStatFromHeaders( $rhdrs );
1606  if ( $this->isRGW ) {
1607  $stat['latest'] = true; // strong consistency
1608  }
1609  } elseif ( $rcode === 404 ) {
1610  $stat = false;
1611  } else {
1612  $stat = null;
1613  $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
1614  }
1615  $stats[$path] = $stat;
1616  }
1617 
1618  return $stats;
1619  }
1620 
1625  protected function getStatFromHeaders( array $rhdrs ) {
1626  // Fetch all of the custom metadata headers
1627  $metadata = $this->getMetadata( $rhdrs );
1628  // Fetch all of the custom raw HTTP headers
1629  $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
1630 
1631  return [
1632  // Convert various random Swift dates to TS_MW
1633  'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
1634  // Empty objects actually return no content-length header in Ceph
1635  'size' => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
1636  'sha1' => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
1637  // Note: manifiest ETags are not an MD5 of the file
1638  'md5' => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
1639  'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
1640  ];
1641  }
1642 
1646  protected function getAuthentication() {
1647  if ( $this->authErrorTimestamp !== null ) {
1648  if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
1649  return null; // failed last attempt; don't bother
1650  } else { // actually retry this time
1651  $this->authErrorTimestamp = null;
1652  }
1653  }
1654  // Session keys expire after a while, so we renew them periodically
1655  $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
1656  // Authenticate with proxy and get a session key...
1657  if ( !$this->authCreds || $reAuth ) {
1658  $this->authSessionTimestamp = 0;
1659  $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
1660  $creds = $this->srvCache->get( $cacheKey ); // credentials
1661  // Try to use the credential cache
1662  if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
1663  $this->authCreds = $creds;
1664  // Skew the timestamp for worst case to avoid using stale credentials
1665  $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
1666  } else { // cache miss
1667  list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1668  'method' => 'GET',
1669  'url' => "{$this->swiftAuthUrl}/v1.0",
1670  'headers' => [
1671  'x-auth-user' => $this->swiftUser,
1672  'x-auth-key' => $this->swiftKey
1673  ]
1674  ] );
1675 
1676  if ( $rcode >= 200 && $rcode <= 299 ) { // OK
1677  $this->authCreds = [
1678  'auth_token' => $rhdrs['x-auth-token'],
1679  'storage_url' => $rhdrs['x-storage-url']
1680  ];
1681  $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
1682  $this->authSessionTimestamp = time();
1683  } elseif ( $rcode === 401 ) {
1684  $this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
1685  $this->authErrorTimestamp = time();
1686 
1687  return null;
1688  } else {
1689  $this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
1690  $this->authErrorTimestamp = time();
1691 
1692  return null;
1693  }
1694  }
1695  // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
1696  if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
1697  $this->isRGW = true; // take advantage of strong consistency in Ceph
1698  }
1699  }
1700 
1701  return $this->authCreds;
1702  }
1703 
1710  protected function storageUrl( array $creds, $container = null, $object = null ) {
1711  $parts = [ $creds['storage_url'] ];
1712  if ( strlen( $container ) ) {
1713  $parts[] = rawurlencode( $container );
1714  }
1715  if ( strlen( $object ) ) {
1716  $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
1717  }
1718 
1719  return implode( '/', $parts );
1720  }
1721 
1726  protected function authTokenHeaders( array $creds ) {
1727  return [ 'x-auth-token' => $creds['auth_token'] ];
1728  }
1729 
1736  private function getCredsCacheKey( $username ) {
1737  return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
1738  }
1739 
1751  public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
1752  if ( $status instanceof Status ) {
1753  $status->fatal( 'backend-fail-internal', $this->name );
1754  }
1755  if ( $code == 401 ) { // possibly a stale token
1756  $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
1757  }
1758  wfDebugLog( 'SwiftBackend',
1759  "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
1760  ( $err ? ": $err" : "" )
1761  );
1762  }
1763 }
1764 
1770  public $httpOp;
1772  public $callback;
1773 
1780  $this->backend = $backend;
1781  $this->callback = $callback;
1782  $this->httpOp = $httpOp;
1783  }
1784 }
1785 
1793 abstract class SwiftFileBackendList implements Iterator {
1795  protected $bufferIter = [];
1796 
1798  protected $bufferAfter = null;
1799 
1801  protected $pos = 0;
1802 
1804  protected $params = [];
1805 
1807  protected $backend;
1808 
1810  protected $container;
1811 
1813  protected $dir;
1814 
1816  protected $suffixStart;
1817 
1818  const PAGE_SIZE = 9000; // file listing buffer size
1819 
1826  public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
1827  $this->backend = $backend;
1828  $this->container = $fullCont;
1829  $this->dir = $dir;
1830  if ( substr( $this->dir, -1 ) === '/' ) {
1831  $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
1832  }
1833  if ( $this->dir == '' ) { // whole container
1834  $this->suffixStart = 0;
1835  } else { // dir within container
1836  $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
1837  }
1838  $this->params = $params;
1839  }
1840 
1845  public function key() {
1846  return $this->pos;
1847  }
1848 
1852  public function next() {
1853  // Advance to the next file in the page
1854  next( $this->bufferIter );
1855  ++$this->pos;
1856  // Check if there are no files left in this page and
1857  // advance to the next page if this page was not empty.
1858  if ( !$this->valid() && count( $this->bufferIter ) ) {
1859  $this->bufferIter = $this->pageFromList(
1860  $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1861  ); // updates $this->bufferAfter
1862  }
1863  }
1864 
1868  public function rewind() {
1869  $this->pos = 0;
1870  $this->bufferAfter = null;
1871  $this->bufferIter = $this->pageFromList(
1872  $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1873  ); // updates $this->bufferAfter
1874  }
1875 
1880  public function valid() {
1881  if ( $this->bufferIter === null ) {
1882  return false; // some failure?
1883  } else {
1884  return ( current( $this->bufferIter ) !== false ); // no paths can have this value
1885  }
1886  }
1887 
1898  abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
1899 }
1900 
1909  public function current() {
1910  return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
1911  }
1912 
1913  protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1914  return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
1915  }
1916 }
1917 
1926  public function current() {
1927  list( $path, $stat ) = current( $this->bufferIter );
1928  $relPath = substr( $path, $this->suffixStart );
1929  if ( is_array( $stat ) ) {
1930  $storageDir = rtrim( $this->params['dir'], '/' );
1931  $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
1932  }
1933 
1934  return $relPath;
1935  }
1936 
1937  protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1938  return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
1939  }
1940 }
static factory($prefix, $extension= '')
Make a new temporary file on the file system.
Definition: TempFSFile.php:54
doStoreInternal(array $params)
doPrepareInternal($fullCont, $dir, array $params)
doGetFileStatMulti(array $params)
getFileListPageInternal($fullCont, $dir, &$after, $limit, array $params)
Do not call this function outside of SwiftFileBackendFileList.
primeContainerCache(array $items)
Do a batch lookup from cache for container stats for all containers used in a list of container names...
buildFileObjectListing(array $params, $dir, array $objects)
Build a list of file objects, filtering out any directories and extracting any stat info if provided ...
doCleanInternal($fullCont, $dir, array $params)
getFileListInternal($fullCont, $dir, array $params)
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
the array() calling protocol came about after MediaWiki 1.4rc1.
resolveContainerPath($container, $relStoragePath)
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1435
getCustomHeaders(array $rawHeaders)
if(count($args)==0) $dir
Iterator for listing directories.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1980
string $swiftUser
Swift user (account:user) to authenticate as.
int $authErrorTimestamp
UNIX timestamp.
static instance()
Singleton.
Definition: Profiler.php:60
deleteContainer($container, array $params)
Delete a Swift container.
getFileStat(array $params)
const ATTR_HEADERS
Bitfield flags for supported features.
Generic operation result class Has warning/error list, boolean status and arbitrary value...
Definition: Status.php:40
doDirectoryExists($fullCont, $dir, array $params)
doPublishInternal($fullCont, $dir, array $params)
string $bufferAfter
List items after this path.
$value
$files
static getLocalClusterInstance()
Get the main cluster-local cache object.
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2588
SwiftFileBackend helper class to page through listings.
doDeleteInternal(array $params)
static send404Message($fname, $flags=0)
Send out a standard 404 message for a file.
Definition: StreamFile.php:166
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
doMoveInternal(array $params)
getStatFromHeaders(array $rhdrs)
Multi-datacenter aware caching interface.
static extensionFromPath($path, $case= 'lowercase')
Get the final extension from a storage or FS path.
setContainerAccess($container, array $readGrps, array $writeGrps)
Set read/write permissions for a Swift container.
doGetFileXAttributes(array $params)
loadListingStatInternal($path, array $val)
Do not call this function outside of SwiftFileBackendFileList.
Apache License January http
doGetFileContentsMulti(array $params)
addMissingMetadata(array $objHdrs, $path)
Fill in any missing object metadata and save it to Swift.
doGetLocalCopyMulti(array $params)
doGetFileSha1base36(array $params)
getContainerStat($container, $bypassCache=false)
Get a Swift container stat array, possibly from process cache.
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
doDescribeInternal(array $params)
getMetadata(array $rawHeaders)
getFileHttpUrl(array $params)
const LOCK_UW
Definition: LockManager.php:61
wfResetOutputBuffers($resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
File backend exception for checked exceptions (e.g.
wfAppendQuery($url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
fileExists(array $params)
if($limit) $timestamp
authTokenHeaders(array $creds)
headersFromParams(array $params)
Get headers to send to Swift when reading a file based on a FileBackend params array, e.g.
array $bufferIter
List of path or (path,stat array) entries.
storageUrl(array $creds, $container=null, $object=null)
isPathUsableInternal($storagePath)
__construct(SwiftFileBackend $backend, $fullCont, $dir, array $params)
static encode($value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:127
$params
const STREAM_HEADLESS
Definition: StreamFile.php:28
string $swiftAuthUrl
Authentication base URL (without version)
string $dir
Storage directory.
const ATTR_UNICODE_PATHS
getCredsCacheKey($username)
Get the cache key for a container.
const ATTR_METADATA
clearCache(array $paths=null)
A BagOStuff object with no objects in it.
resolveStoragePathReal($storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
objectListing($fullCont, $type, $limit, $after=null, $prefix=null, $delim=null)
Get a list of objects under a container.
string $container
Container name.
int $authTTL
TTL in seconds.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition: hooks.txt:776
getContentType($storagePath, $content, $fsPath)
Get the content type to use in HEAD/GET requests for a file.
onError($status, $func, array $params, $err= '', $code=0, $desc= '')
Log an unexpected exception for this backend.
string $name
Unique backend name.
Definition: FileBackend.php:87
Base class for all backends using particular storage medium.
getLocalCopy(array $params)
Get a local copy on disk of the file at a storage path in the backend.
doStreamFile(array $params)
pageFromList($container, $dir, &$after, $limit, array $params)
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
pageFromList($container, $dir, &$after, $limit, array $params)
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
this hook is for auditing only $req
Definition: hooks.txt:981
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:776
Iterator for listing regular files.
setContainerCache($container, array $val)
Set the cached info for a container.
getDirectoryListInternal($fullCont, $dir, array $params)
__construct(array $config)
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2458
SwiftFileBackend $backend
sanitizeHdrs(array $params)
Sanitize and filter the custom headers from a $params array.
doCopyInternal(array $params)
convertSwiftDate($ts, $format=TS_MW)
Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z". ...
string $rgwS3AccessKey
S3 access key (RADOS Gateway)
doCreateInternal(array $params)
static getLocalServerInstance($fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
array $httpOp
List of Requests for MultiHttpClient.
doPrimeContainerCache(array $containerInfo)
Class for an OpenStack Swift (or Ceph RGW) based file backend.
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at name
Definition: design.txt:12
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition: hooks.txt:1020
pageFromList($container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1020
getMetadataHeaders(array $rawHeaders)
int $authSessionTimestamp
UNIX timestamp.
MultiHttpClient $http
deleteFileCache($path)
Delete the cached stat info for a file path.
ProcessCacheLRU $containerStatCache
Container stat cache.
getDirListPageInternal($fullCont, $dir, &$after, $limit, array $params)
Do not call this function outside of SwiftFileBackendFileList.
doExecuteOpHandlesInternal(array $fileOpHandles)
doSecureInternal($fullCont, $dir, array $params)
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:776
Class to handle concurrent HTTP requests.
const CACHE_NONE
Definition: Defines.php:102
static decode($value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:187
doGetFileStat(array $params)
createContainer($container, array $params)
Create a Swift container.
FileBackendStore helper class for performing asynchronous file operations.
string $rgwS3SecretKey
S3 authentication key (RADOS Gateway)
Handles per process caching of items.
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:31
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2376
__construct(SwiftFileBackend $backend, Closure $callback, array $httpOp)
string $swiftKey
Secret key for user.
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
bool $isRGW
Whether the server is an Ceph RGW.
getScopedFileLocks(array $paths, $type, Status $status, $timeout=0)
Lock the files at the given storage paths in the backend.
string $swiftTempUrlKey
Shared secret value for making temp URLs.