MediaWiki REL1_29
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
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'] ) && isset( $config['srvCache'] ) ) {
138 $this->srvCache = $config['srvCache'];
139 } else {
140 $this->srvCache = new EmptyBagOStuff();
141 }
142 }
143
148
149 protected function resolveContainerPath( $container, $relStoragePath ) {
150 if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
151 return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
152 } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
153 return null; // too long for Swift
154 }
155
156 return $relStoragePath;
157 }
158
159 public function isPathUsableInternal( $storagePath ) {
160 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
161 if ( $rel === null ) {
162 return false; // invalid
163 }
164
165 return is_array( $this->getContainerStat( $container ) );
166 }
167
175 protected function sanitizeHdrs( array $params ) {
176 return isset( $params['headers'] )
177 ? $this->getCustomHeaders( $params['headers'] )
178 : [];
179 }
180
185 protected function getCustomHeaders( array $rawHeaders ) {
186 $headers = [];
187
188 // Normalize casing, and strip out illegal headers
189 foreach ( $rawHeaders as $name => $value ) {
190 $name = strtolower( $name );
191 if ( preg_match( '/^content-(type|length)$/', $name ) ) {
192 continue; // blacklisted
193 } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
194 $headers[$name] = $value; // allowed
195 } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
196 $headers[$name] = $value; // allowed
197 }
198 }
199 // By default, Swift has annoyingly low maximum header value limits
200 if ( isset( $headers['content-disposition'] ) ) {
201 $disposition = '';
202 // @note: assume FileBackend::makeContentDisposition() already used
203 foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
204 $part = trim( $part );
205 $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
206 if ( strlen( $new ) <= 255 ) {
207 $disposition = $new;
208 } else {
209 break; // too long; sigh
210 }
211 }
212 $headers['content-disposition'] = $disposition;
213 }
214
215 return $headers;
216 }
217
222 protected function getMetadataHeaders( array $rawHeaders ) {
223 $headers = [];
224 foreach ( $rawHeaders as $name => $value ) {
225 $name = strtolower( $name );
226 if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
227 $headers[$name] = $value;
228 }
229 }
230
231 return $headers;
232 }
233
238 protected function getMetadata( array $rawHeaders ) {
239 $metadata = [];
240 foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
241 $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
242 }
243
244 return $metadata;
245 }
246
247 protected function doCreateInternal( array $params ) {
248 $status = $this->newStatus();
249
250 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
251 if ( $dstRel === null ) {
252 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
253
254 return $status;
255 }
256
257 $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
258 $contentType = isset( $params['headers']['content-type'] )
259 ? $params['headers']['content-type']
260 : $this->getContentType( $params['dst'], $params['content'], null );
261
262 $reqs = [ [
263 'method' => 'PUT',
264 'url' => [ $dstCont, $dstRel ],
265 'headers' => [
266 'content-length' => strlen( $params['content'] ),
267 'etag' => md5( $params['content'] ),
268 'content-type' => $contentType,
269 'x-object-meta-sha1base36' => $sha1Hash
270 ] + $this->sanitizeHdrs( $params ),
271 'body' => $params['content']
272 ] ];
273
274 $method = __METHOD__;
275 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
276 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
277 if ( $rcode === 201 ) {
278 // good
279 } elseif ( $rcode === 412 ) {
280 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
281 } else {
282 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
283 }
284 };
285
286 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
287 if ( !empty( $params['async'] ) ) { // deferred
288 $status->value = $opHandle;
289 } else { // actually write the object in Swift
290 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
291 }
292
293 return $status;
294 }
295
296 protected function doStoreInternal( array $params ) {
297 $status = $this->newStatus();
298
299 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
300 if ( $dstRel === null ) {
301 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
302
303 return $status;
304 }
305
306 MediaWiki\suppressWarnings();
307 $sha1Hash = sha1_file( $params['src'] );
308 MediaWiki\restoreWarnings();
309 if ( $sha1Hash === false ) { // source doesn't exist?
310 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
311
312 return $status;
313 }
314 $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
315 $contentType = isset( $params['headers']['content-type'] )
316 ? $params['headers']['content-type']
317 : $this->getContentType( $params['dst'], null, $params['src'] );
318
319 $handle = fopen( $params['src'], 'rb' );
320 if ( $handle === false ) { // source doesn't exist?
321 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
322
323 return $status;
324 }
325
326 $reqs = [ [
327 'method' => 'PUT',
328 'url' => [ $dstCont, $dstRel ],
329 'headers' => [
330 'content-length' => filesize( $params['src'] ),
331 'etag' => md5_file( $params['src'] ),
332 'content-type' => $contentType,
333 'x-object-meta-sha1base36' => $sha1Hash
334 ] + $this->sanitizeHdrs( $params ),
335 'body' => $handle // resource
336 ] ];
337
338 $method = __METHOD__;
339 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
340 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
341 if ( $rcode === 201 ) {
342 // good
343 } elseif ( $rcode === 412 ) {
344 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
345 } else {
346 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
347 }
348 };
349
350 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
351 $opHandle->resourcesToClose[] = $handle;
352
353 if ( !empty( $params['async'] ) ) { // deferred
354 $status->value = $opHandle;
355 } else { // actually write the object in Swift
356 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
357 }
358
359 return $status;
360 }
361
362 protected function doCopyInternal( array $params ) {
363 $status = $this->newStatus();
364
365 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
366 if ( $srcRel === null ) {
367 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
368
369 return $status;
370 }
371
372 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
373 if ( $dstRel === null ) {
374 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
375
376 return $status;
377 }
378
379 $reqs = [ [
380 'method' => 'PUT',
381 'url' => [ $dstCont, $dstRel ],
382 'headers' => [
383 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
384 '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
385 ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
386 ] ];
387
388 $method = __METHOD__;
389 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
390 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
391 if ( $rcode === 201 ) {
392 // good
393 } elseif ( $rcode === 404 ) {
394 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
395 } else {
396 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
397 }
398 };
399
400 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
401 if ( !empty( $params['async'] ) ) { // deferred
402 $status->value = $opHandle;
403 } else { // actually write the object in Swift
404 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
405 }
406
407 return $status;
408 }
409
410 protected function doMoveInternal( array $params ) {
411 $status = $this->newStatus();
412
413 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
414 if ( $srcRel === null ) {
415 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
416
417 return $status;
418 }
419
420 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
421 if ( $dstRel === null ) {
422 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
423
424 return $status;
425 }
426
427 $reqs = [
428 [
429 'method' => 'PUT',
430 'url' => [ $dstCont, $dstRel ],
431 'headers' => [
432 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
433 '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
434 ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
435 ]
436 ];
437 if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
438 $reqs[] = [
439 'method' => 'DELETE',
440 'url' => [ $srcCont, $srcRel ],
441 'headers' => []
442 ];
443 }
444
445 $method = __METHOD__;
446 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
447 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
448 if ( $request['method'] === 'PUT' && $rcode === 201 ) {
449 // good
450 } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
451 // good
452 } elseif ( $rcode === 404 ) {
453 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
454 } else {
455 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
456 }
457 };
458
459 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
460 if ( !empty( $params['async'] ) ) { // deferred
461 $status->value = $opHandle;
462 } else { // actually move the object in Swift
463 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
464 }
465
466 return $status;
467 }
468
469 protected function doDeleteInternal( array $params ) {
470 $status = $this->newStatus();
471
472 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
473 if ( $srcRel === null ) {
474 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
475
476 return $status;
477 }
478
479 $reqs = [ [
480 'method' => 'DELETE',
481 'url' => [ $srcCont, $srcRel ],
482 'headers' => []
483 ] ];
484
485 $method = __METHOD__;
486 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
487 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
488 if ( $rcode === 204 ) {
489 // good
490 } elseif ( $rcode === 404 ) {
491 if ( empty( $params['ignoreMissingSource'] ) ) {
492 $status->fatal( 'backend-fail-delete', $params['src'] );
493 }
494 } else {
495 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
496 }
497 };
498
499 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
500 if ( !empty( $params['async'] ) ) { // deferred
501 $status->value = $opHandle;
502 } else { // actually delete the object in Swift
503 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
504 }
505
506 return $status;
507 }
508
509 protected function doDescribeInternal( array $params ) {
510 $status = $this->newStatus();
511
512 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
513 if ( $srcRel === null ) {
514 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
515
516 return $status;
517 }
518
519 // Fetch the old object headers/metadata...this should be in stat cache by now
520 $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
521 if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
522 $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
523 }
524 if ( !$stat ) {
525 $status->fatal( 'backend-fail-describe', $params['src'] );
526
527 return $status;
528 }
529
530 // POST clears prior headers, so we need to merge the changes in to the old ones
531 $metaHdrs = [];
532 foreach ( $stat['xattr']['metadata'] as $name => $value ) {
533 $metaHdrs["x-object-meta-$name"] = $value;
534 }
535 $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
536
537 $reqs = [ [
538 'method' => 'POST',
539 'url' => [ $srcCont, $srcRel ],
540 'headers' => $metaHdrs + $customHdrs
541 ] ];
542
543 $method = __METHOD__;
544 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
545 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
546 if ( $rcode === 202 ) {
547 // good
548 } elseif ( $rcode === 404 ) {
549 $status->fatal( 'backend-fail-describe', $params['src'] );
550 } else {
551 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
552 }
553 };
554
555 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
556 if ( !empty( $params['async'] ) ) { // deferred
557 $status->value = $opHandle;
558 } else { // actually change the object in Swift
559 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
560 }
561
562 return $status;
563 }
564
565 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
566 $status = $this->newStatus();
567
568 // (a) Check if container already exists
569 $stat = $this->getContainerStat( $fullCont );
570 if ( is_array( $stat ) ) {
571 return $status; // already there
572 } elseif ( $stat === null ) {
573 $status->fatal( 'backend-fail-internal', $this->name );
574 $this->logger->error( __METHOD__ . ': cannot get container stat' );
575
576 return $status;
577 }
578
579 // (b) Create container as needed with proper ACLs
580 if ( $stat === false ) {
581 $params['op'] = 'prepare';
582 $status->merge( $this->createContainer( $fullCont, $params ) );
583 }
584
585 return $status;
586 }
587
588 protected function doSecureInternal( $fullCont, $dir, array $params ) {
589 $status = $this->newStatus();
590 if ( empty( $params['noAccess'] ) ) {
591 return $status; // nothing to do
592 }
593
594 $stat = $this->getContainerStat( $fullCont );
595 if ( is_array( $stat ) ) {
596 // Make container private to end-users...
597 $status->merge( $this->setContainerAccess(
598 $fullCont,
599 [ $this->swiftUser ], // read
600 [ $this->swiftUser ] // write
601 ) );
602 } elseif ( $stat === false ) {
603 $status->fatal( 'backend-fail-usable', $params['dir'] );
604 } else {
605 $status->fatal( 'backend-fail-internal', $this->name );
606 $this->logger->error( __METHOD__ . ': cannot get container stat' );
607 }
608
609 return $status;
610 }
611
612 protected function doPublishInternal( $fullCont, $dir, array $params ) {
613 $status = $this->newStatus();
614
615 $stat = $this->getContainerStat( $fullCont );
616 if ( is_array( $stat ) ) {
617 // Make container public to end-users...
618 $status->merge( $this->setContainerAccess(
619 $fullCont,
620 [ $this->swiftUser, '.r:*' ], // read
621 [ $this->swiftUser ] // write
622 ) );
623 } elseif ( $stat === false ) {
624 $status->fatal( 'backend-fail-usable', $params['dir'] );
625 } else {
626 $status->fatal( 'backend-fail-internal', $this->name );
627 $this->logger->error( __METHOD__ . ': cannot get container stat' );
628 }
629
630 return $status;
631 }
632
633 protected function doCleanInternal( $fullCont, $dir, array $params ) {
634 $status = $this->newStatus();
635
636 // Only containers themselves can be removed, all else is virtual
637 if ( $dir != '' ) {
638 return $status; // nothing to do
639 }
640
641 // (a) Check the container
642 $stat = $this->getContainerStat( $fullCont, true );
643 if ( $stat === false ) {
644 return $status; // ok, nothing to do
645 } elseif ( !is_array( $stat ) ) {
646 $status->fatal( 'backend-fail-internal', $this->name );
647 $this->logger->error( __METHOD__ . ': cannot get container stat' );
648
649 return $status;
650 }
651
652 // (b) Delete the container if empty
653 if ( $stat['count'] == 0 ) {
654 $params['op'] = 'clean';
655 $status->merge( $this->deleteContainer( $fullCont, $params ) );
656 }
657
658 return $status;
659 }
660
661 protected function doGetFileStat( array $params ) {
662 $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
663 unset( $params['src'] );
664 $stats = $this->doGetFileStatMulti( $params );
665
666 return reset( $stats );
667 }
668
679 protected function convertSwiftDate( $ts, $format = TS_MW ) {
680 try {
681 $timestamp = new MWTimestamp( $ts );
682
683 return $timestamp->getTimestamp( $format );
684 } catch ( Exception $e ) {
685 throw new FileBackendError( $e->getMessage() );
686 }
687 }
688
696 protected function addMissingMetadata( array $objHdrs, $path ) {
697 if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
698 return $objHdrs; // nothing to do
699 }
700
702 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
703 $this->logger->error( __METHOD__ . ": $path was not stored with SHA-1 metadata." );
704
705 $objHdrs['x-object-meta-sha1base36'] = false;
706
707 $auth = $this->getAuthentication();
708 if ( !$auth ) {
709 return $objHdrs; // failed
710 }
711
712 // Find prior custom HTTP headers
713 $postHeaders = $this->getCustomHeaders( $objHdrs );
714 // Find prior metadata headers
715 $postHeaders += $this->getMetadataHeaders( $objHdrs );
716
717 $status = $this->newStatus();
719 $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
720 if ( $status->isOK() ) {
721 $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
722 if ( $tmpFile ) {
723 $hash = $tmpFile->getSha1Base36();
724 if ( $hash !== false ) {
725 $objHdrs['x-object-meta-sha1base36'] = $hash;
726 // Merge new SHA1 header into the old ones
727 $postHeaders['x-object-meta-sha1base36'] = $hash;
728 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
729 list( $rcode ) = $this->http->run( [
730 'method' => 'POST',
731 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
732 'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
733 ] );
734 if ( $rcode >= 200 && $rcode <= 299 ) {
735 $this->deleteFileCache( $path );
736
737 return $objHdrs; // success
738 }
739 }
740 }
741 }
742
743 $this->logger->error( __METHOD__ . ": unable to set SHA-1 metadata for $path" );
744
745 return $objHdrs; // failed
746 }
747
748 protected function doGetFileContentsMulti( array $params ) {
749 $contents = [];
750
751 $auth = $this->getAuthentication();
752
753 $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
754 // Blindly create tmp files and stream to them, catching any exception if the file does
755 // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
756 $reqs = []; // (path => op)
757
758 foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
759 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
760 if ( $srcRel === null || !$auth ) {
761 $contents[$path] = false;
762 continue;
763 }
764 // Create a new temporary memory file...
765 $handle = fopen( 'php://temp', 'wb' );
766 if ( $handle ) {
767 $reqs[$path] = [
768 'method' => 'GET',
769 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
770 'headers' => $this->authTokenHeaders( $auth )
771 + $this->headersFromParams( $params ),
772 'stream' => $handle,
773 ];
774 }
775 $contents[$path] = false;
776 }
777
778 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
779 $reqs = $this->http->runMulti( $reqs, $opts );
780 foreach ( $reqs as $path => $op ) {
781 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
782 if ( $rcode >= 200 && $rcode <= 299 ) {
783 rewind( $op['stream'] ); // start from the beginning
784 $contents[$path] = stream_get_contents( $op['stream'] );
785 } elseif ( $rcode === 404 ) {
786 $contents[$path] = false;
787 } else {
788 $this->onError( null, __METHOD__,
789 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
790 }
791 fclose( $op['stream'] ); // close open handle
792 }
793
794 return $contents;
795 }
796
797 protected function doDirectoryExists( $fullCont, $dir, array $params ) {
798 $prefix = ( $dir == '' ) ? null : "{$dir}/";
799 $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
800 if ( $status->isOK() ) {
801 return ( count( $status->value ) ) > 0;
802 }
803
804 return null; // error
805 }
806
814 public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
815 return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
816 }
817
825 public function getFileListInternal( $fullCont, $dir, array $params ) {
826 return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
827 }
828
840 public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
841 $dirs = [];
842 if ( $after === INF ) {
843 return $dirs; // nothing more
844 }
845
846 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
847
848 $prefix = ( $dir == '' ) ? null : "{$dir}/";
849 // Non-recursive: only list dirs right under $dir
850 if ( !empty( $params['topOnly'] ) ) {
851 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
852 if ( !$status->isOK() ) {
853 throw new FileBackendError( "Iterator page I/O error." );
854 }
855 $objects = $status->value;
856 foreach ( $objects as $object ) { // files and directories
857 if ( substr( $object, -1 ) === '/' ) {
858 $dirs[] = $object; // directories end in '/'
859 }
860 }
861 } else {
862 // Recursive: list all dirs under $dir and its subdirs
863 $getParentDir = function ( $path ) {
864 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
865 };
866
867 // Get directory from last item of prior page
868 $lastDir = $getParentDir( $after ); // must be first page
869 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
870
871 if ( !$status->isOK() ) {
872 throw new FileBackendError( "Iterator page I/O error." );
873 }
874
875 $objects = $status->value;
876
877 foreach ( $objects as $object ) { // files
878 $objectDir = $getParentDir( $object ); // directory of object
879
880 if ( $objectDir !== false && $objectDir !== $dir ) {
881 // Swift stores paths in UTF-8, using binary sorting.
882 // See function "create_container_table" in common/db.py.
883 // If a directory is not "greater" than the last one,
884 // then it was already listed by the calling iterator.
885 if ( strcmp( $objectDir, $lastDir ) > 0 ) {
886 $pDir = $objectDir;
887 do { // add dir and all its parent dirs
888 $dirs[] = "{$pDir}/";
889 $pDir = $getParentDir( $pDir );
890 } while ( $pDir !== false // sanity
891 && strcmp( $pDir, $lastDir ) > 0 // not done already
892 && strlen( $pDir ) > strlen( $dir ) // within $dir
893 );
894 }
895 $lastDir = $objectDir;
896 }
897 }
898 }
899 // Page on the unfiltered directory listing (what is returned may be filtered)
900 if ( count( $objects ) < $limit ) {
901 $after = INF; // avoid a second RTT
902 } else {
903 $after = end( $objects ); // update last item
904 }
905
906 return $dirs;
907 }
908
920 public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
921 $files = []; // list of (path, stat array or null) entries
922 if ( $after === INF ) {
923 return $files; // nothing more
924 }
925
926 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
927
928 $prefix = ( $dir == '' ) ? null : "{$dir}/";
929 // $objects will contain a list of unfiltered names or CF_Object items
930 // Non-recursive: only list files right under $dir
931 if ( !empty( $params['topOnly'] ) ) {
932 if ( !empty( $params['adviseStat'] ) ) {
933 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
934 } else {
935 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
936 }
937 } else {
938 // Recursive: list all files under $dir and its subdirs
939 if ( !empty( $params['adviseStat'] ) ) {
940 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
941 } else {
942 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
943 }
944 }
945
946 // Reformat this list into a list of (name, stat array or null) entries
947 if ( !$status->isOK() ) {
948 throw new FileBackendError( "Iterator page I/O error." );
949 }
950
951 $objects = $status->value;
952 $files = $this->buildFileObjectListing( $params, $dir, $objects );
953
954 // Page on the unfiltered object listing (what is returned may be filtered)
955 if ( count( $objects ) < $limit ) {
956 $after = INF; // avoid a second RTT
957 } else {
958 $after = end( $objects ); // update last item
959 $after = is_object( $after ) ? $after->name : $after;
960 }
961
962 return $files;
963 }
964
974 private function buildFileObjectListing( array $params, $dir, array $objects ) {
975 $names = [];
976 foreach ( $objects as $object ) {
977 if ( is_object( $object ) ) {
978 if ( isset( $object->subdir ) || !isset( $object->name ) ) {
979 continue; // virtual directory entry; ignore
980 }
981 $stat = [
982 // Convert various random Swift dates to TS_MW
983 'mtime' => $this->convertSwiftDate( $object->last_modified, TS_MW ),
984 'size' => (int)$object->bytes,
985 'sha1' => null,
986 // Note: manifiest ETags are not an MD5 of the file
987 'md5' => ctype_xdigit( $object->hash ) ? $object->hash : null,
988 'latest' => false // eventually consistent
989 ];
990 $names[] = [ $object->name, $stat ];
991 } elseif ( substr( $object, -1 ) !== '/' ) {
992 // Omit directories, which end in '/' in listings
993 $names[] = [ $object, null ];
994 }
995 }
996
997 return $names;
998 }
999
1006 public function loadListingStatInternal( $path, array $val ) {
1007 $this->cheapCache->set( $path, 'stat', $val );
1008 }
1009
1010 protected function doGetFileXAttributes( array $params ) {
1011 $stat = $this->getFileStat( $params );
1012 if ( $stat ) {
1013 if ( !isset( $stat['xattr'] ) ) {
1014 // Stat entries filled by file listings don't include metadata/headers
1015 $this->clearCache( [ $params['src'] ] );
1016 $stat = $this->getFileStat( $params );
1017 }
1018
1019 return $stat['xattr'];
1020 } else {
1021 return false;
1022 }
1023 }
1024
1025 protected function doGetFileSha1base36( array $params ) {
1026 $stat = $this->getFileStat( $params );
1027 if ( $stat ) {
1028 if ( !isset( $stat['sha1'] ) ) {
1029 // Stat entries filled by file listings don't include SHA1
1030 $this->clearCache( [ $params['src'] ] );
1031 $stat = $this->getFileStat( $params );
1032 }
1033
1034 return $stat['sha1'];
1035 } else {
1036 return false;
1037 }
1038 }
1039
1040 protected function doStreamFile( array $params ) {
1041 $status = $this->newStatus();
1042
1043 $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
1044
1045 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1046 if ( $srcRel === null ) {
1048 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
1049
1050 return $status;
1051 }
1052
1053 $auth = $this->getAuthentication();
1054 if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
1056 $status->fatal( 'backend-fail-stream', $params['src'] );
1057
1058 return $status;
1059 }
1060
1061 // If "headers" is set, we only want to send them if the file is there.
1062 // Do not bother checking if the file exists if headers are not set though.
1063 if ( $params['headers'] && !$this->fileExists( $params ) ) {
1065 $status->fatal( 'backend-fail-stream', $params['src'] );
1066
1067 return $status;
1068 }
1069
1070 // Send the requested additional headers
1071 foreach ( $params['headers'] as $header ) {
1072 header( $header ); // aways send
1073 }
1074
1075 if ( empty( $params['allowOB'] ) ) {
1076 // Cancel output buffering and gzipping if set
1077 call_user_func( $this->obResetFunc );
1078 }
1079
1080 $handle = fopen( 'php://output', 'wb' );
1081 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1082 'method' => 'GET',
1083 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1084 'headers' => $this->authTokenHeaders( $auth )
1085 + $this->headersFromParams( $params ) + $params['options'],
1086 'stream' => $handle,
1087 'flags' => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
1088 ] );
1089
1090 if ( $rcode >= 200 && $rcode <= 299 ) {
1091 // good
1092 } elseif ( $rcode === 404 ) {
1093 $status->fatal( 'backend-fail-stream', $params['src'] );
1094 // Per T43113, nasty things can happen if bad cache entries get
1095 // stuck in cache. It's also possible that this error can come up
1096 // with simple race conditions. Clear out the stat cache to be safe.
1097 $this->clearCache( [ $params['src'] ] );
1098 $this->deleteFileCache( $params['src'] );
1099 } else {
1100 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1101 }
1102
1103 return $status;
1104 }
1105
1106 protected function doGetLocalCopyMulti( array $params ) {
1108 $tmpFiles = [];
1109
1110 $auth = $this->getAuthentication();
1111
1112 $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
1113 // Blindly create tmp files and stream to them, catching any exception if the file does
1114 // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
1115 $reqs = []; // (path => op)
1116
1117 foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
1118 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1119 if ( $srcRel === null || !$auth ) {
1120 $tmpFiles[$path] = null;
1121 continue;
1122 }
1123 // Get source file extension
1125 // Create a new temporary file...
1126 $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
1127 if ( $tmpFile ) {
1128 $handle = fopen( $tmpFile->getPath(), 'wb' );
1129 if ( $handle ) {
1130 $reqs[$path] = [
1131 'method' => 'GET',
1132 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1133 'headers' => $this->authTokenHeaders( $auth )
1134 + $this->headersFromParams( $params ),
1135 'stream' => $handle,
1136 ];
1137 } else {
1138 $tmpFile = null;
1139 }
1140 }
1141 $tmpFiles[$path] = $tmpFile;
1142 }
1143
1144 $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
1145 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1146 $reqs = $this->http->runMulti( $reqs, $opts );
1147 foreach ( $reqs as $path => $op ) {
1148 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
1149 fclose( $op['stream'] ); // close open handle
1150 if ( $rcode >= 200 && $rcode <= 299 ) {
1151 $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
1152 // Double check that the disk is not full/broken
1153 if ( $size != $rhdrs['content-length'] ) {
1154 $tmpFiles[$path] = null;
1155 $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
1156 $this->onError( null, __METHOD__,
1157 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1158 }
1159 // Set the file stat process cache in passing
1160 $stat = $this->getStatFromHeaders( $rhdrs );
1161 $stat['latest'] = $isLatest;
1162 $this->cheapCache->set( $path, 'stat', $stat );
1163 } elseif ( $rcode === 404 ) {
1164 $tmpFiles[$path] = false;
1165 } else {
1166 $tmpFiles[$path] = null;
1167 $this->onError( null, __METHOD__,
1168 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1169 }
1170 }
1171
1172 return $tmpFiles;
1173 }
1174
1175 public function getFileHttpUrl( array $params ) {
1176 if ( $this->swiftTempUrlKey != '' ||
1177 ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
1178 ) {
1179 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1180 if ( $srcRel === null ) {
1181 return null; // invalid path
1182 }
1183
1184 $auth = $this->getAuthentication();
1185 if ( !$auth ) {
1186 return null;
1187 }
1188
1189 $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
1190 $expires = time() + $ttl;
1191
1192 if ( $this->swiftTempUrlKey != '' ) {
1193 $url = $this->storageUrl( $auth, $srcCont, $srcRel );
1194 // Swift wants the signature based on the unencoded object name
1195 $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
1196 $signature = hash_hmac( 'sha1',
1197 "GET\n{$expires}\n{$contPath}/{$srcRel}",
1198 $this->swiftTempUrlKey
1199 );
1200
1201 return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
1202 } else { // give S3 API URL for rgw
1203 // Path for signature starts with the bucket
1204 $spath = '/' . rawurlencode( $srcCont ) . '/' .
1205 str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1206 // Calculate the hash
1207 $signature = base64_encode( hash_hmac(
1208 'sha1',
1209 "GET\n\n\n{$expires}\n{$spath}",
1210 $this->rgwS3SecretKey,
1211 true // raw
1212 ) );
1213 // See https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
1214 // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
1215 // Note: S3 API is the rgw default; remove the /swift/ URL bit.
1216 return str_replace( '/swift/v1', '', $this->storageUrl( $auth ) . $spath ) .
1217 '?' .
1218 http_build_query( [
1219 'Signature' => $signature,
1220 'Expires' => $expires,
1221 'AWSAccessKeyId' => $this->rgwS3AccessKey
1222 ] );
1223 }
1224 }
1225
1226 return null;
1227 }
1228
1229 protected function directoriesAreVirtual() {
1230 return true;
1231 }
1232
1241 protected function headersFromParams( array $params ) {
1242 $hdrs = [];
1243 if ( !empty( $params['latest'] ) ) {
1244 $hdrs['x-newest'] = 'true';
1245 }
1246
1247 return $hdrs;
1248 }
1249
1255 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1257 $statuses = [];
1258
1259 $auth = $this->getAuthentication();
1260 if ( !$auth ) {
1261 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1262 $statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
1263 }
1264
1265 return $statuses;
1266 }
1267
1268 // Split the HTTP requests into stages that can be done concurrently
1269 $httpReqsByStage = []; // map of (stage => index => HTTP request)
1270 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1272 $reqs = $fileOpHandle->httpOp;
1273 // Convert the 'url' parameter to an actual URL using $auth
1274 foreach ( $reqs as $stage => &$req ) {
1275 list( $container, $relPath ) = $req['url'];
1276 $req['url'] = $this->storageUrl( $auth, $container, $relPath );
1277 $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
1278 $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
1279 $httpReqsByStage[$stage][$index] = $req;
1280 }
1281 $statuses[$index] = $this->newStatus();
1282 }
1283
1284 // Run all requests for the first stage, then the next, and so on
1285 $reqCount = count( $httpReqsByStage );
1286 for ( $stage = 0; $stage < $reqCount; ++$stage ) {
1287 $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
1288 foreach ( $httpReqs as $index => $httpReq ) {
1289 // Run the callback for each request of this operation
1290 $callback = $fileOpHandles[$index]->callback;
1291 call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
1292 // On failure, abort all remaining requests for this operation
1293 // (e.g. abort the DELETE request if the COPY request fails for a move)
1294 if ( !$statuses[$index]->isOK() ) {
1295 $stages = count( $fileOpHandles[$index]->httpOp );
1296 for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
1297 unset( $httpReqsByStage[$s][$index] );
1298 }
1299 }
1300 }
1301 }
1302
1303 return $statuses;
1304 }
1305
1328 protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
1329 $status = $this->newStatus();
1330 $auth = $this->getAuthentication();
1331
1332 if ( !$auth ) {
1333 $status->fatal( 'backend-fail-connect', $this->name );
1334
1335 return $status;
1336 }
1337
1338 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1339 'method' => 'POST',
1340 'url' => $this->storageUrl( $auth, $container ),
1341 'headers' => $this->authTokenHeaders( $auth ) + [
1342 'x-container-read' => implode( ',', $readGrps ),
1343 'x-container-write' => implode( ',', $writeGrps )
1344 ]
1345 ] );
1346
1347 if ( $rcode != 204 && $rcode !== 202 ) {
1348 $status->fatal( 'backend-fail-internal', $this->name );
1349 $this->logger->error( __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
1350 }
1351
1352 return $status;
1353 }
1354
1363 protected function getContainerStat( $container, $bypassCache = false ) {
1364 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1365
1366 if ( $bypassCache ) { // purge cache
1367 $this->containerStatCache->clear( $container );
1368 } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1369 $this->primeContainerCache( [ $container ] ); // check persistent cache
1370 }
1371 if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1372 $auth = $this->getAuthentication();
1373 if ( !$auth ) {
1374 return null;
1375 }
1376
1377 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1378 'method' => 'HEAD',
1379 'url' => $this->storageUrl( $auth, $container ),
1380 'headers' => $this->authTokenHeaders( $auth )
1381 ] );
1382
1383 if ( $rcode === 204 ) {
1384 $stat = [
1385 'count' => $rhdrs['x-container-object-count'],
1386 'bytes' => $rhdrs['x-container-bytes-used']
1387 ];
1388 if ( $bypassCache ) {
1389 return $stat;
1390 } else {
1391 $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
1392 $this->setContainerCache( $container, $stat ); // update persistent cache
1393 }
1394 } elseif ( $rcode === 404 ) {
1395 return false;
1396 } else {
1397 $this->onError( null, __METHOD__,
1398 [ 'cont' => $container ], $rerr, $rcode, $rdesc );
1399
1400 return null;
1401 }
1402 }
1403
1404 return $this->containerStatCache->get( $container, 'stat' );
1405 }
1406
1414 protected function createContainer( $container, array $params ) {
1415 $status = $this->newStatus();
1416
1417 $auth = $this->getAuthentication();
1418 if ( !$auth ) {
1419 $status->fatal( 'backend-fail-connect', $this->name );
1420
1421 return $status;
1422 }
1423
1424 // @see SwiftFileBackend::setContainerAccess()
1425 if ( empty( $params['noAccess'] ) ) {
1426 $readGrps = [ '.r:*', $this->swiftUser ]; // public
1427 } else {
1428 $readGrps = [ $this->swiftUser ]; // private
1429 }
1430 $writeGrps = [ $this->swiftUser ]; // sanity
1431
1432 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1433 'method' => 'PUT',
1434 'url' => $this->storageUrl( $auth, $container ),
1435 'headers' => $this->authTokenHeaders( $auth ) + [
1436 'x-container-read' => implode( ',', $readGrps ),
1437 'x-container-write' => implode( ',', $writeGrps )
1438 ]
1439 ] );
1440
1441 if ( $rcode === 201 ) { // new
1442 // good
1443 } elseif ( $rcode === 202 ) { // already there
1444 // this shouldn't really happen, but is OK
1445 } else {
1446 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1447 }
1448
1449 return $status;
1450 }
1451
1459 protected function deleteContainer( $container, array $params ) {
1460 $status = $this->newStatus();
1461
1462 $auth = $this->getAuthentication();
1463 if ( !$auth ) {
1464 $status->fatal( 'backend-fail-connect', $this->name );
1465
1466 return $status;
1467 }
1468
1469 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1470 'method' => 'DELETE',
1471 'url' => $this->storageUrl( $auth, $container ),
1472 'headers' => $this->authTokenHeaders( $auth )
1473 ] );
1474
1475 if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
1476 $this->containerStatCache->clear( $container ); // purge
1477 } elseif ( $rcode === 404 ) { // not there
1478 // this shouldn't really happen, but is OK
1479 } elseif ( $rcode === 409 ) { // not empty
1480 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
1481 } else {
1482 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1483 }
1484
1485 return $status;
1486 }
1487
1500 private function objectListing(
1501 $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
1502 ) {
1503 $status = $this->newStatus();
1504
1505 $auth = $this->getAuthentication();
1506 if ( !$auth ) {
1507 $status->fatal( 'backend-fail-connect', $this->name );
1508
1509 return $status;
1510 }
1511
1512 $query = [ 'limit' => $limit ];
1513 if ( $type === 'info' ) {
1514 $query['format'] = 'json';
1515 }
1516 if ( $after !== null ) {
1517 $query['marker'] = $after;
1518 }
1519 if ( $prefix !== null ) {
1520 $query['prefix'] = $prefix;
1521 }
1522 if ( $delim !== null ) {
1523 $query['delimiter'] = $delim;
1524 }
1525
1526 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1527 'method' => 'GET',
1528 'url' => $this->storageUrl( $auth, $fullCont ),
1529 'query' => $query,
1530 'headers' => $this->authTokenHeaders( $auth )
1531 ] );
1532
1533 $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
1534 if ( $rcode === 200 ) { // good
1535 if ( $type === 'info' ) {
1536 $status->value = FormatJson::decode( trim( $rbody ) );
1537 } else {
1538 $status->value = explode( "\n", trim( $rbody ) );
1539 }
1540 } elseif ( $rcode === 204 ) {
1541 $status->value = []; // empty container
1542 } elseif ( $rcode === 404 ) {
1543 $status->value = []; // no container
1544 } else {
1545 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1546 }
1547
1548 return $status;
1549 }
1550
1551 protected function doPrimeContainerCache( array $containerInfo ) {
1552 foreach ( $containerInfo as $container => $info ) {
1553 $this->containerStatCache->set( $container, 'stat', $info );
1554 }
1555 }
1556
1557 protected function doGetFileStatMulti( array $params ) {
1558 $stats = [];
1559
1560 $auth = $this->getAuthentication();
1561
1562 $reqs = [];
1563 foreach ( $params['srcs'] as $path ) {
1564 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1565 if ( $srcRel === null ) {
1566 $stats[$path] = false;
1567 continue; // invalid storage path
1568 } elseif ( !$auth ) {
1569 $stats[$path] = null;
1570 continue;
1571 }
1572
1573 // (a) Check the container
1574 $cstat = $this->getContainerStat( $srcCont );
1575 if ( $cstat === false ) {
1576 $stats[$path] = false;
1577 continue; // ok, nothing to do
1578 } elseif ( !is_array( $cstat ) ) {
1579 $stats[$path] = null;
1580 continue;
1581 }
1582
1583 $reqs[$path] = [
1584 'method' => 'HEAD',
1585 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1586 'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
1587 ];
1588 }
1589
1590 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1591 $reqs = $this->http->runMulti( $reqs, $opts );
1592
1593 foreach ( $params['srcs'] as $path ) {
1594 if ( array_key_exists( $path, $stats ) ) {
1595 continue; // some sort of failure above
1596 }
1597 // (b) Check the file
1598 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
1599 if ( $rcode === 200 || $rcode === 204 ) {
1600 // Update the object if it is missing some headers
1601 $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
1602 // Load the stat array from the headers
1603 $stat = $this->getStatFromHeaders( $rhdrs );
1604 if ( $this->isRGW ) {
1605 $stat['latest'] = true; // strong consistency
1606 }
1607 } elseif ( $rcode === 404 ) {
1608 $stat = false;
1609 } else {
1610 $stat = null;
1611 $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
1612 }
1613 $stats[$path] = $stat;
1614 }
1615
1616 return $stats;
1617 }
1618
1623 protected function getStatFromHeaders( array $rhdrs ) {
1624 // Fetch all of the custom metadata headers
1625 $metadata = $this->getMetadata( $rhdrs );
1626 // Fetch all of the custom raw HTTP headers
1627 $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
1628
1629 return [
1630 // Convert various random Swift dates to TS_MW
1631 'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
1632 // Empty objects actually return no content-length header in Ceph
1633 'size' => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
1634 'sha1' => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
1635 // Note: manifiest ETags are not an MD5 of the file
1636 'md5' => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
1637 'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
1638 ];
1639 }
1640
1644 protected function getAuthentication() {
1645 if ( $this->authErrorTimestamp !== null ) {
1646 if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
1647 return null; // failed last attempt; don't bother
1648 } else { // actually retry this time
1649 $this->authErrorTimestamp = null;
1650 }
1651 }
1652 // Session keys expire after a while, so we renew them periodically
1653 $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
1654 // Authenticate with proxy and get a session key...
1655 if ( !$this->authCreds || $reAuth ) {
1656 $this->authSessionTimestamp = 0;
1657 $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
1658 $creds = $this->srvCache->get( $cacheKey ); // credentials
1659 // Try to use the credential cache
1660 if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
1661 $this->authCreds = $creds;
1662 // Skew the timestamp for worst case to avoid using stale credentials
1663 $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
1664 } else { // cache miss
1665 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1666 'method' => 'GET',
1667 'url' => "{$this->swiftAuthUrl}/v1.0",
1668 'headers' => [
1669 'x-auth-user' => $this->swiftUser,
1670 'x-auth-key' => $this->swiftKey
1671 ]
1672 ] );
1673
1674 if ( $rcode >= 200 && $rcode <= 299 ) { // OK
1675 $this->authCreds = [
1676 'auth_token' => $rhdrs['x-auth-token'],
1677 'storage_url' => $rhdrs['x-storage-url']
1678 ];
1679 $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
1680 $this->authSessionTimestamp = time();
1681 } elseif ( $rcode === 401 ) {
1682 $this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
1683 $this->authErrorTimestamp = time();
1684
1685 return null;
1686 } else {
1687 $this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
1688 $this->authErrorTimestamp = time();
1689
1690 return null;
1691 }
1692 }
1693 // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
1694 if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
1695 $this->isRGW = true; // take advantage of strong consistency in Ceph
1696 }
1697 }
1698
1699 return $this->authCreds;
1700 }
1701
1708 protected function storageUrl( array $creds, $container = null, $object = null ) {
1709 $parts = [ $creds['storage_url'] ];
1710 if ( strlen( $container ) ) {
1711 $parts[] = rawurlencode( $container );
1712 }
1713 if ( strlen( $object ) ) {
1714 $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
1715 }
1716
1717 return implode( '/', $parts );
1718 }
1719
1724 protected function authTokenHeaders( array $creds ) {
1725 return [ 'x-auth-token' => $creds['auth_token'] ];
1726 }
1727
1734 private function getCredsCacheKey( $username ) {
1735 return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
1736 }
1737
1749 public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
1750 if ( $status instanceof StatusValue ) {
1751 $status->fatal( 'backend-fail-internal', $this->name );
1752 }
1753 if ( $code == 401 ) { // possibly a stale token
1754 $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
1755 }
1756 $this->logger->error(
1757 "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
1758 ( $err ? ": $err" : "" )
1759 );
1760 }
1761}
1762
1768 public $httpOp;
1771
1778 $this->backend = $backend;
1779 $this->callback = $callback;
1780 $this->httpOp = $httpOp;
1781 }
1782}
1783
1791abstract class SwiftFileBackendList implements Iterator {
1793 protected $bufferIter = [];
1794
1796 protected $bufferAfter = null;
1797
1799 protected $pos = 0;
1800
1802 protected $params = [];
1803
1805 protected $backend;
1806
1808 protected $container;
1809
1811 protected $dir;
1812
1814 protected $suffixStart;
1815
1816 const PAGE_SIZE = 9000; // file listing buffer size
1817
1824 public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
1825 $this->backend = $backend;
1826 $this->container = $fullCont;
1827 $this->dir = $dir;
1828 if ( substr( $this->dir, -1 ) === '/' ) {
1829 $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
1830 }
1831 if ( $this->dir == '' ) { // whole container
1832 $this->suffixStart = 0;
1833 } else { // dir within container
1834 $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
1835 }
1836 $this->params = $params;
1837 }
1838
1843 public function key() {
1844 return $this->pos;
1845 }
1846
1850 public function next() {
1851 // Advance to the next file in the page
1852 next( $this->bufferIter );
1853 ++$this->pos;
1854 // Check if there are no files left in this page and
1855 // advance to the next page if this page was not empty.
1856 if ( !$this->valid() && count( $this->bufferIter ) ) {
1857 $this->bufferIter = $this->pageFromList(
1858 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1859 ); // updates $this->bufferAfter
1860 }
1861 }
1862
1866 public function rewind() {
1867 $this->pos = 0;
1868 $this->bufferAfter = null;
1869 $this->bufferIter = $this->pageFromList(
1870 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1871 ); // updates $this->bufferAfter
1872 }
1873
1878 public function valid() {
1879 if ( $this->bufferIter === null ) {
1880 return false; // some failure?
1881 } else {
1882 return ( current( $this->bufferIter ) !== false ); // no paths can have this value
1883 }
1884 }
1885
1896 abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
1897}
1898
1907 public function current() {
1908 return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
1909 }
1910
1911 protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1912 return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
1913 }
1914}
1915
1924 public function current() {
1925 list( $path, $stat ) = current( $this->bufferIter );
1926 $relPath = substr( $path, $this->suffixStart );
1927 if ( is_array( $stat ) ) {
1928 $storageDir = rtrim( $this->params['dir'], '/' );
1929 $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
1930 }
1931
1932 return $relPath;
1933 }
1934
1935 protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1936 return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
1937 }
1938}
Apache License January http
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
interface is intended to be more or less compatible with the PHP memcached client.
Definition BagOStuff.php:47
A BagOStuff object with no objects in it.
File backend exception for checked exceptions (e.g.
FileBackendStore helper class for performing asynchronous file operations.
Base class for all backends using particular storage medium.
setContainerCache( $container, array $val)
Set the cached info for a container.
executeOpHandlesInternal(array $fileOpHandles)
Execute a list of FileBackendStoreOpHandle handles in parallel.
getFileStat(array $params)
Get quick information about a file at a storage path in the backend.
resolveStoragePathReal( $storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
clearCache(array $paths=null)
Invalidate any in-process file stat and property cache.
primeContainerCache(array $items)
Do a batch lookup from cache for container stats for all containers used in a list of container names...
deleteFileCache( $path)
Delete the cached stat info for a file path.
getContentType( $storagePath, $content, $fsPath)
Get the content type to use in HEAD/GET requests for a file.
fileExists(array $params)
Check if a file exists at a storage path in the backend.
getLocalCopy(array $params)
Get a local copy on disk of the file at a storage path in the backend.
string $name
Unique backend name.
const ATTR_UNICODE_PATHS
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.
newStatus()
Yields the result of the status wrapper callback on either:
scopedProfileSection( $section)
const ATTR_METADATA
const ATTR_HEADERS
Bitfield flags for supported features.
Library for creating and parsing MW-style timestamps.
Class to handle concurrent HTTP requests.
Handles per process caching of items.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
const STREAM_HEADLESS
static send404Message( $fname, $flags=0)
Send out a standard 404 message for a file.
Iterator for listing directories.
pageFromList( $container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
Iterator for listing regular files.
pageFromList( $container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
SwiftFileBackend helper class to page through listings.
string $dir
Storage directory.
string $container
Container name.
__construct(SwiftFileBackend $backend, $fullCont, $dir, array $params)
string $bufferAfter
List items after this path.
pageFromList( $container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
array $bufferIter
List of path or (path,stat array) entries.
SwiftFileBackend $backend
Class for an OpenStack Swift (or Ceph RGW) based file backend.
string $swiftUser
Swift user (account:user) to authenticate as.
sanitizeHdrs(array $params)
Sanitize and filter the custom headers from a $params array.
string $swiftAuthUrl
Authentication base URL (without version)
string $swiftTempUrlKey
Shared secret value for making temp URLs.
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path.
getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params)
Do not call this function outside of SwiftFileBackendFileList.
doPublishInternal( $fullCont, $dir, array $params)
doCreateInternal(array $params)
doGetFileStatMulti(array $params)
Get file stat information (concurrently if possible) for several files.
doGetFileSha1base36(array $params)
__construct(array $config)
MultiHttpClient $http
doGetFileXAttributes(array $params)
onError( $status, $func, array $params, $err='', $code=0, $desc='')
Log an unexpected exception for this backend.
buildFileObjectListing(array $params, $dir, array $objects)
Build a list of file objects, filtering out any directories and extracting any stat info if provided ...
authTokenHeaders(array $creds)
getStatFromHeaders(array $rhdrs)
createContainer( $container, array $params)
Create a Swift container.
doCopyInternal(array $params)
getCredsCacheKey( $username)
Get the cache key for a container.
getDirectoryListInternal( $fullCont, $dir, array $params)
string $rgwS3AccessKey
S3 access key (RADOS Gateway)
getFileHttpUrl(array $params)
getCustomHeaders(array $rawHeaders)
int $authTTL
TTL in seconds.
headersFromParams(array $params)
Get headers to send to Swift when reading a file based on a FileBackend params array,...
getMetadata(array $rawHeaders)
bool $isRGW
Whether the server is an Ceph RGW.
addMissingMetadata(array $objHdrs, $path)
Fill in any missing object metadata and save it to Swift.
doStoreInternal(array $params)
int $authErrorTimestamp
UNIX timestamp.
getMetadataHeaders(array $rawHeaders)
int $authSessionTimestamp
UNIX timestamp.
loadListingStatInternal( $path, array $val)
Do not call this function outside of SwiftFileBackendFileList.
doPrepareInternal( $fullCont, $dir, array $params)
doSecureInternal( $fullCont, $dir, array $params)
getFileListInternal( $fullCont, $dir, array $params)
doMoveInternal(array $params)
getFeatures()
Get the a bitfield of extra features supported by the backend medium.
deleteContainer( $container, array $params)
Delete a Swift container.
setContainerAccess( $container, array $readGrps, array $writeGrps)
Set read/write permissions for a Swift container.
doGetFileStat(array $params)
doGetLocalCopyMulti(array $params)
string $rgwS3SecretKey
S3 authentication key (RADOS Gateway)
doGetFileContentsMulti(array $params)
storageUrl(array $creds, $container=null, $object=null)
convertSwiftDate( $ts, $format=TS_MW)
Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
doStreamFile(array $params)
doPrimeContainerCache(array $containerInfo)
Fill the backend-specific process cache given an array of resolved container names and their correspo...
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
doCleanInternal( $fullCont, $dir, array $params)
ProcessCacheLRU $containerStatCache
Container stat cache.
getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params)
Do not call this function outside of SwiftFileBackendFileList.
string $swiftKey
Secret key for user.
doDirectoryExists( $fullCont, $dir, array $params)
objectListing( $fullCont, $type, $limit, $after=null, $prefix=null, $delim=null)
Get a list of objects under a container.
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
doExecuteOpHandlesInternal(array $fileOpHandles)
doDeleteInternal(array $params)
doDescribeInternal(array $params)
getContainerStat( $container, $bypassCache=false)
Get a Swift container stat array, possibly from process cache.
array $httpOp
List of Requests for MultiHttpClient.
__construct(SwiftFileBackend $backend, Closure $callback, array $httpOp)
static factory( $prefix, $extension='', $tmpDirectory=null)
Make a new temporary file on the file system.
Multi-datacenter aware caching interface.
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
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 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
this hook is for auditing only $req
Definition hooks.txt:990
the array() calling protocol came about after MediaWiki 1.4rc1.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup 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 please use GetContentModels hook to make them known to core 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:1143
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:865
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2753
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 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:2604
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2723
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:785
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup 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:1049
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:903
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:1601
processing should stop and the error should be shown to the user * false
Definition hooks.txt:189
returning false will NOT prevent logging $e
Definition hooks.txt:2127
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:37
$params
$header