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