]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/libs/filebackend/SwiftFileBackend.php
MediaWiki 1.30.2-scripts2
[autoinstallsdev/mediawiki.git] / includes / libs / filebackend / SwiftFileBackend.php
1 <?php
2 /**
3  * OpenStack Swift based file backend.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup FileBackend
22  * @author Russ Nelson
23  */
24
25 /**
26  * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
27  *
28  * StatusValue messages should avoid mentioning the Swift account name.
29  * Likewise, error suppression should be used to avoid path disclosure.
30  *
31  * @ingroup FileBackend
32  * @since 1.19
33  */
34 class SwiftFileBackend extends FileBackendStore {
35         /** @var MultiHttpClient */
36         protected $http;
37         /** @var int TTL in seconds */
38         protected $authTTL;
39         /** @var string Authentication base URL (without version) */
40         protected $swiftAuthUrl;
41         /** @var string Override of storage base URL */
42         protected $swiftStorageUrl;
43         /** @var string Swift user (account:user) to authenticate as */
44         protected $swiftUser;
45         /** @var string Secret key for user */
46         protected $swiftKey;
47         /** @var string Shared secret value for making temp URLs */
48         protected $swiftTempUrlKey;
49         /** @var string S3 access key (RADOS Gateway) */
50         protected $rgwS3AccessKey;
51         /** @var string S3 authentication key (RADOS Gateway) */
52         protected $rgwS3SecretKey;
53
54         /** @var BagOStuff */
55         protected $srvCache;
56
57         /** @var ProcessCacheLRU Container stat cache */
58         protected $containerStatCache;
59
60         /** @var array */
61         protected $authCreds;
62         /** @var int UNIX timestamp */
63         protected $authSessionTimestamp = 0;
64         /** @var int UNIX timestamp */
65         protected $authErrorTimestamp = null;
66
67         /** @var bool Whether the server is an Ceph RGW */
68         protected $isRGW = false;
69
70         /**
71          * @see FileBackendStore::__construct()
72          * @param array $config Params include:
73          *   - swiftAuthUrl       : Swift authentication server URL
74          *   - swiftUser          : Swift user used by MediaWiki (account:username)
75          *   - swiftKey           : Swift authentication key for the above user
76          *   - swiftAuthTTL       : Swift authentication TTL (seconds)
77          *   - swiftTempUrlKey    : Swift "X-Account-Meta-Temp-URL-Key" value on the account.
78          *                          Do not set this until it has been set in the backend.
79          *   - swiftStorageUrl    : Swift storage URL (overrides that of the authentication response).
80          *                          This is useful to set if a TLS proxy is in use.
81          *   - shardViaHashLevels : Map of container names to sharding config with:
82          *                             - base   : base of hash characters, 16 or 36
83          *                             - levels : the number of hash levels (and digits)
84          *                             - repeat : hash subdirectories are prefixed with all the
85          *                                        parent hash directory names (e.g. "a/ab/abc")
86          *   - cacheAuthInfo      : Whether to cache authentication tokens in APC, XCache, ect.
87          *                          If those are not available, then the main cache will be used.
88          *                          This is probably insecure in shared hosting environments.
89          *   - rgwS3AccessKey     : Rados Gateway S3 "access key" value on the account.
90          *                          Do not set this until it has been set in the backend.
91          *                          This is used for generating expiring pre-authenticated URLs.
92          *                          Only use this when using rgw and to work around
93          *                          http://tracker.newdream.net/issues/3454.
94          *   - rgwS3SecretKey     : Rados Gateway S3 "secret key" value on the account.
95          *                          Do not set this until it has been set in the backend.
96          *                          This is used for generating expiring pre-authenticated URLs.
97          *                          Only use this when using rgw and to work around
98          *                          http://tracker.newdream.net/issues/3454.
99          */
100         public function __construct( array $config ) {
101                 parent::__construct( $config );
102                 // Required settings
103                 $this->swiftAuthUrl = $config['swiftAuthUrl'];
104                 $this->swiftUser = $config['swiftUser'];
105                 $this->swiftKey = $config['swiftKey'];
106                 // Optional settings
107                 $this->authTTL = isset( $config['swiftAuthTTL'] )
108                         ? $config['swiftAuthTTL']
109                         : 15 * 60; // some sane number
110                 $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
111                         ? $config['swiftTempUrlKey']
112                         : '';
113                 $this->swiftStorageUrl = isset( $config['swiftStorageUrl'] )
114                         ? $config['swiftStorageUrl']
115                         : null;
116                 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
117                         ? $config['shardViaHashLevels']
118                         : '';
119                 $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
120                         ? $config['rgwS3AccessKey']
121                         : '';
122                 $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
123                         ? $config['rgwS3SecretKey']
124                         : '';
125                 // HTTP helper client
126                 $this->http = new MultiHttpClient( [] );
127                 // Cache container information to mask latency
128                 if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
129                         $this->memCache = $config['wanCache'];
130                 }
131                 // Process cache for container info
132                 $this->containerStatCache = new ProcessCacheLRU( 300 );
133                 // Cache auth token information to avoid RTTs
134                 if ( !empty( $config['cacheAuthInfo'] ) && isset( $config['srvCache'] ) ) {
135                         $this->srvCache = $config['srvCache'];
136                 } else {
137                         $this->srvCache = new EmptyBagOStuff();
138                 }
139         }
140
141         public function getFeatures() {
142                 return ( FileBackend::ATTR_UNICODE_PATHS |
143                         FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
144         }
145
146         protected function resolveContainerPath( $container, $relStoragePath ) {
147                 if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
148                         return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
149                 } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
150                         return null; // too long for Swift
151                 }
152
153                 return $relStoragePath;
154         }
155
156         public function isPathUsableInternal( $storagePath ) {
157                 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
158                 if ( $rel === null ) {
159                         return false; // invalid
160                 }
161
162                 return is_array( $this->getContainerStat( $container ) );
163         }
164
165         /**
166          * Sanitize and filter the custom headers from a $params array.
167          * Only allows certain "standard" Content- and X-Content- headers.
168          *
169          * @param array $params
170          * @return array Sanitized value of 'headers' field in $params
171          */
172         protected function sanitizeHdrs( array $params ) {
173                 return isset( $params['headers'] )
174                         ? $this->getCustomHeaders( $params['headers'] )
175                         : [];
176         }
177
178         /**
179          * @param array $rawHeaders
180          * @return array Custom non-metadata HTTP headers
181          */
182         protected function getCustomHeaders( array $rawHeaders ) {
183                 $headers = [];
184
185                 // Normalize casing, and strip out illegal headers
186                 foreach ( $rawHeaders as $name => $value ) {
187                         $name = strtolower( $name );
188                         if ( preg_match( '/^content-(type|length)$/', $name ) ) {
189                                 continue; // blacklisted
190                         } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
191                                 $headers[$name] = $value; // allowed
192                         } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
193                                 $headers[$name] = $value; // allowed
194                         }
195                 }
196                 // By default, Swift has annoyingly low maximum header value limits
197                 if ( isset( $headers['content-disposition'] ) ) {
198                         $disposition = '';
199                         // @note: assume FileBackend::makeContentDisposition() already used
200                         foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
201                                 $part = trim( $part );
202                                 $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
203                                 if ( strlen( $new ) <= 255 ) {
204                                         $disposition = $new;
205                                 } else {
206                                         break; // too long; sigh
207                                 }
208                         }
209                         $headers['content-disposition'] = $disposition;
210                 }
211
212                 return $headers;
213         }
214
215         /**
216          * @param array $rawHeaders
217          * @return array Custom metadata headers
218          */
219         protected function getMetadataHeaders( array $rawHeaders ) {
220                 $headers = [];
221                 foreach ( $rawHeaders as $name => $value ) {
222                         $name = strtolower( $name );
223                         if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
224                                 $headers[$name] = $value;
225                         }
226                 }
227
228                 return $headers;
229         }
230
231         /**
232          * @param array $rawHeaders
233          * @return array Custom metadata headers with prefix removed
234          */
235         protected function getMetadata( array $rawHeaders ) {
236                 $metadata = [];
237                 foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
238                         $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
239                 }
240
241                 return $metadata;
242         }
243
244         protected function doCreateInternal( array $params ) {
245                 $status = $this->newStatus();
246
247                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
248                 if ( $dstRel === null ) {
249                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
250
251                         return $status;
252                 }
253
254                 $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
255                 $contentType = isset( $params['headers']['content-type'] )
256                         ? $params['headers']['content-type']
257                         : $this->getContentType( $params['dst'], $params['content'], null );
258
259                 $reqs = [ [
260                         'method' => 'PUT',
261                         'url' => [ $dstCont, $dstRel ],
262                         'headers' => [
263                                 'content-length' => strlen( $params['content'] ),
264                                 'etag' => md5( $params['content'] ),
265                                 'content-type' => $contentType,
266                                 'x-object-meta-sha1base36' => $sha1Hash
267                         ] + $this->sanitizeHdrs( $params ),
268                         'body' => $params['content']
269                 ] ];
270
271                 $method = __METHOD__;
272                 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
273                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
274                         if ( $rcode === 201 ) {
275                                 // good
276                         } elseif ( $rcode === 412 ) {
277                                 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
278                         } else {
279                                 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
280                         }
281                 };
282
283                 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
284                 if ( !empty( $params['async'] ) ) { // deferred
285                         $status->value = $opHandle;
286                 } else { // actually write the object in Swift
287                         $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
288                 }
289
290                 return $status;
291         }
292
293         protected function doStoreInternal( array $params ) {
294                 $status = $this->newStatus();
295
296                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
297                 if ( $dstRel === null ) {
298                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
299
300                         return $status;
301                 }
302
303                 MediaWiki\suppressWarnings();
304                 $sha1Hash = sha1_file( $params['src'] );
305                 MediaWiki\restoreWarnings();
306                 if ( $sha1Hash === false ) { // source doesn't exist?
307                         $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
308
309                         return $status;
310                 }
311                 $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
312                 $contentType = isset( $params['headers']['content-type'] )
313                         ? $params['headers']['content-type']
314                         : $this->getContentType( $params['dst'], null, $params['src'] );
315
316                 $handle = fopen( $params['src'], 'rb' );
317                 if ( $handle === false ) { // source doesn't exist?
318                         $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
319
320                         return $status;
321                 }
322
323                 $reqs = [ [
324                         'method' => 'PUT',
325                         'url' => [ $dstCont, $dstRel ],
326                         'headers' => [
327                                 'content-length' => filesize( $params['src'] ),
328                                 'etag' => md5_file( $params['src'] ),
329                                 'content-type' => $contentType,
330                                 'x-object-meta-sha1base36' => $sha1Hash
331                         ] + $this->sanitizeHdrs( $params ),
332                         'body' => $handle // resource
333                 ] ];
334
335                 $method = __METHOD__;
336                 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
337                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
338                         if ( $rcode === 201 ) {
339                                 // good
340                         } elseif ( $rcode === 412 ) {
341                                 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
342                         } else {
343                                 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
344                         }
345                 };
346
347                 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
348                 $opHandle->resourcesToClose[] = $handle;
349
350                 if ( !empty( $params['async'] ) ) { // deferred
351                         $status->value = $opHandle;
352                 } else { // actually write the object in Swift
353                         $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
354                 }
355
356                 return $status;
357         }
358
359         protected function doCopyInternal( array $params ) {
360                 $status = $this->newStatus();
361
362                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
363                 if ( $srcRel === null ) {
364                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
365
366                         return $status;
367                 }
368
369                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
370                 if ( $dstRel === null ) {
371                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
372
373                         return $status;
374                 }
375
376                 $reqs = [ [
377                         'method' => 'PUT',
378                         'url' => [ $dstCont, $dstRel ],
379                         'headers' => [
380                                 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
381                                         '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
382                         ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
383                 ] ];
384
385                 $method = __METHOD__;
386                 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
387                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
388                         if ( $rcode === 201 ) {
389                                 // good
390                         } elseif ( $rcode === 404 ) {
391                                 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
392                         } else {
393                                 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
394                         }
395                 };
396
397                 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
398                 if ( !empty( $params['async'] ) ) { // deferred
399                         $status->value = $opHandle;
400                 } else { // actually write the object in Swift
401                         $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
402                 }
403
404                 return $status;
405         }
406
407         protected function doMoveInternal( array $params ) {
408                 $status = $this->newStatus();
409
410                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
411                 if ( $srcRel === null ) {
412                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
413
414                         return $status;
415                 }
416
417                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
418                 if ( $dstRel === null ) {
419                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
420
421                         return $status;
422                 }
423
424                 $reqs = [
425                         [
426                                 'method' => 'PUT',
427                                 'url' => [ $dstCont, $dstRel ],
428                                 'headers' => [
429                                         'x-copy-from' => '/' . rawurlencode( $srcCont ) .
430                                                 '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
431                                 ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
432                         ]
433                 ];
434                 if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
435                         $reqs[] = [
436                                 'method' => 'DELETE',
437                                 'url' => [ $srcCont, $srcRel ],
438                                 'headers' => []
439                         ];
440                 }
441
442                 $method = __METHOD__;
443                 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
444                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
445                         if ( $request['method'] === 'PUT' && $rcode === 201 ) {
446                                 // good
447                         } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
448                                 // good
449                         } elseif ( $rcode === 404 ) {
450                                 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
451                         } else {
452                                 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
453                         }
454                 };
455
456                 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
457                 if ( !empty( $params['async'] ) ) { // deferred
458                         $status->value = $opHandle;
459                 } else { // actually move the object in Swift
460                         $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
461                 }
462
463                 return $status;
464         }
465
466         protected function doDeleteInternal( array $params ) {
467                 $status = $this->newStatus();
468
469                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
470                 if ( $srcRel === null ) {
471                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
472
473                         return $status;
474                 }
475
476                 $reqs = [ [
477                         'method' => 'DELETE',
478                         'url' => [ $srcCont, $srcRel ],
479                         'headers' => []
480                 ] ];
481
482                 $method = __METHOD__;
483                 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
484                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
485                         if ( $rcode === 204 ) {
486                                 // good
487                         } elseif ( $rcode === 404 ) {
488                                 if ( empty( $params['ignoreMissingSource'] ) ) {
489                                         $status->fatal( 'backend-fail-delete', $params['src'] );
490                                 }
491                         } else {
492                                 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
493                         }
494                 };
495
496                 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
497                 if ( !empty( $params['async'] ) ) { // deferred
498                         $status->value = $opHandle;
499                 } else { // actually delete the object in Swift
500                         $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
501                 }
502
503                 return $status;
504         }
505
506         protected function doDescribeInternal( array $params ) {
507                 $status = $this->newStatus();
508
509                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
510                 if ( $srcRel === null ) {
511                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
512
513                         return $status;
514                 }
515
516                 // Fetch the old object headers/metadata...this should be in stat cache by now
517                 $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
518                 if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
519                         $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
520                 }
521                 if ( !$stat ) {
522                         $status->fatal( 'backend-fail-describe', $params['src'] );
523
524                         return $status;
525                 }
526
527                 // POST clears prior headers, so we need to merge the changes in to the old ones
528                 $metaHdrs = [];
529                 foreach ( $stat['xattr']['metadata'] as $name => $value ) {
530                         $metaHdrs["x-object-meta-$name"] = $value;
531                 }
532                 $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
533
534                 $reqs = [ [
535                         'method' => 'POST',
536                         'url' => [ $srcCont, $srcRel ],
537                         'headers' => $metaHdrs + $customHdrs
538                 ] ];
539
540                 $method = __METHOD__;
541                 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
542                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
543                         if ( $rcode === 202 ) {
544                                 // good
545                         } elseif ( $rcode === 404 ) {
546                                 $status->fatal( 'backend-fail-describe', $params['src'] );
547                         } else {
548                                 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
549                         }
550                 };
551
552                 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
553                 if ( !empty( $params['async'] ) ) { // deferred
554                         $status->value = $opHandle;
555                 } else { // actually change the object in Swift
556                         $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
557                 }
558
559                 return $status;
560         }
561
562         protected function doPrepareInternal( $fullCont, $dir, array $params ) {
563                 $status = $this->newStatus();
564
565                 // (a) Check if container already exists
566                 $stat = $this->getContainerStat( $fullCont );
567                 if ( is_array( $stat ) ) {
568                         return $status; // already there
569                 } elseif ( $stat === null ) {
570                         $status->fatal( 'backend-fail-internal', $this->name );
571                         $this->logger->error( __METHOD__ . ': cannot get container stat' );
572
573                         return $status;
574                 }
575
576                 // (b) Create container as needed with proper ACLs
577                 if ( $stat === false ) {
578                         $params['op'] = 'prepare';
579                         $status->merge( $this->createContainer( $fullCont, $params ) );
580                 }
581
582                 return $status;
583         }
584
585         protected function doSecureInternal( $fullCont, $dir, array $params ) {
586                 $status = $this->newStatus();
587                 if ( empty( $params['noAccess'] ) ) {
588                         return $status; // nothing to do
589                 }
590
591                 $stat = $this->getContainerStat( $fullCont );
592                 if ( is_array( $stat ) ) {
593                         // Make container private to end-users...
594                         $status->merge( $this->setContainerAccess(
595                                 $fullCont,
596                                 [ $this->swiftUser ], // read
597                                 [ $this->swiftUser ] // write
598                         ) );
599                 } elseif ( $stat === false ) {
600                         $status->fatal( 'backend-fail-usable', $params['dir'] );
601                 } else {
602                         $status->fatal( 'backend-fail-internal', $this->name );
603                         $this->logger->error( __METHOD__ . ': cannot get container stat' );
604                 }
605
606                 return $status;
607         }
608
609         protected function doPublishInternal( $fullCont, $dir, array $params ) {
610                 $status = $this->newStatus();
611
612                 $stat = $this->getContainerStat( $fullCont );
613                 if ( is_array( $stat ) ) {
614                         // Make container public to end-users...
615                         $status->merge( $this->setContainerAccess(
616                                 $fullCont,
617                                 [ $this->swiftUser, '.r:*' ], // read
618                                 [ $this->swiftUser ] // write
619                         ) );
620                 } elseif ( $stat === false ) {
621                         $status->fatal( 'backend-fail-usable', $params['dir'] );
622                 } else {
623                         $status->fatal( 'backend-fail-internal', $this->name );
624                         $this->logger->error( __METHOD__ . ': cannot get container stat' );
625                 }
626
627                 return $status;
628         }
629
630         protected function doCleanInternal( $fullCont, $dir, array $params ) {
631                 $status = $this->newStatus();
632
633                 // Only containers themselves can be removed, all else is virtual
634                 if ( $dir != '' ) {
635                         return $status; // nothing to do
636                 }
637
638                 // (a) Check the container
639                 $stat = $this->getContainerStat( $fullCont, true );
640                 if ( $stat === false ) {
641                         return $status; // ok, nothing to do
642                 } elseif ( !is_array( $stat ) ) {
643                         $status->fatal( 'backend-fail-internal', $this->name );
644                         $this->logger->error( __METHOD__ . ': cannot get container stat' );
645
646                         return $status;
647                 }
648
649                 // (b) Delete the container if empty
650                 if ( $stat['count'] == 0 ) {
651                         $params['op'] = 'clean';
652                         $status->merge( $this->deleteContainer( $fullCont, $params ) );
653                 }
654
655                 return $status;
656         }
657
658         protected function doGetFileStat( array $params ) {
659                 $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
660                 unset( $params['src'] );
661                 $stats = $this->doGetFileStatMulti( $params );
662
663                 return reset( $stats );
664         }
665
666         /**
667          * Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
668          * Dates might also come in like "2013-05-11T07:37:27.678360" from Swift listings,
669          * missing the timezone suffix (though Ceph RGW does not appear to have this bug).
670          *
671          * @param string $ts
672          * @param int $format Output format (TS_* constant)
673          * @return string
674          * @throws FileBackendError
675          */
676         protected function convertSwiftDate( $ts, $format = TS_MW ) {
677                 try {
678                         $timestamp = new MWTimestamp( $ts );
679
680                         return $timestamp->getTimestamp( $format );
681                 } catch ( Exception $e ) {
682                         throw new FileBackendError( $e->getMessage() );
683                 }
684         }
685
686         /**
687          * Fill in any missing object metadata and save it to Swift
688          *
689          * @param array $objHdrs Object response headers
690          * @param string $path Storage path to object
691          * @return array New headers
692          */
693         protected function addMissingMetadata( array $objHdrs, $path ) {
694                 if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
695                         return $objHdrs; // nothing to do
696                 }
697
698                 /** @noinspection PhpUnusedLocalVariableInspection */
699                 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
700                 $this->logger->error( __METHOD__ . ": $path was not stored with SHA-1 metadata." );
701
702                 $objHdrs['x-object-meta-sha1base36'] = false;
703
704                 $auth = $this->getAuthentication();
705                 if ( !$auth ) {
706                         return $objHdrs; // failed
707                 }
708
709                 // Find prior custom HTTP headers
710                 $postHeaders = $this->getCustomHeaders( $objHdrs );
711                 // Find prior metadata headers
712                 $postHeaders += $this->getMetadataHeaders( $objHdrs );
713
714                 $status = $this->newStatus();
715                 /** @noinspection PhpUnusedLocalVariableInspection */
716                 $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
717                 if ( $status->isOK() ) {
718                         $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
719                         if ( $tmpFile ) {
720                                 $hash = $tmpFile->getSha1Base36();
721                                 if ( $hash !== false ) {
722                                         $objHdrs['x-object-meta-sha1base36'] = $hash;
723                                         // Merge new SHA1 header into the old ones
724                                         $postHeaders['x-object-meta-sha1base36'] = $hash;
725                                         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
726                                         list( $rcode ) = $this->http->run( [
727                                                 'method' => 'POST',
728                                                 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
729                                                 'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
730                                         ] );
731                                         if ( $rcode >= 200 && $rcode <= 299 ) {
732                                                 $this->deleteFileCache( $path );
733
734                                                 return $objHdrs; // success
735                                         }
736                                 }
737                         }
738                 }
739
740                 $this->logger->error( __METHOD__ . ": unable to set SHA-1 metadata for $path" );
741
742                 return $objHdrs; // failed
743         }
744
745         protected function doGetFileContentsMulti( array $params ) {
746                 $contents = [];
747
748                 $auth = $this->getAuthentication();
749
750                 $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
751                 // Blindly create tmp files and stream to them, catching any exception if the file does
752                 // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
753                 $reqs = []; // (path => op)
754
755                 foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
756                         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
757                         if ( $srcRel === null || !$auth ) {
758                                 $contents[$path] = false;
759                                 continue;
760                         }
761                         // Create a new temporary memory file...
762                         $handle = fopen( 'php://temp', 'wb' );
763                         if ( $handle ) {
764                                 $reqs[$path] = [
765                                         'method'  => 'GET',
766                                         'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
767                                         'headers' => $this->authTokenHeaders( $auth )
768                                                 + $this->headersFromParams( $params ),
769                                         'stream'  => $handle,
770                                 ];
771                         }
772                         $contents[$path] = false;
773                 }
774
775                 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
776                 $reqs = $this->http->runMulti( $reqs, $opts );
777                 foreach ( $reqs as $path => $op ) {
778                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
779                         if ( $rcode >= 200 && $rcode <= 299 ) {
780                                 rewind( $op['stream'] ); // start from the beginning
781                                 $contents[$path] = stream_get_contents( $op['stream'] );
782                         } elseif ( $rcode === 404 ) {
783                                 $contents[$path] = false;
784                         } else {
785                                 $this->onError( null, __METHOD__,
786                                         [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
787                         }
788                         fclose( $op['stream'] ); // close open handle
789                 }
790
791                 return $contents;
792         }
793
794         protected function doDirectoryExists( $fullCont, $dir, array $params ) {
795                 $prefix = ( $dir == '' ) ? null : "{$dir}/";
796                 $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
797                 if ( $status->isOK() ) {
798                         return ( count( $status->value ) ) > 0;
799                 }
800
801                 return null; // error
802         }
803
804         /**
805          * @see FileBackendStore::getDirectoryListInternal()
806          * @param string $fullCont
807          * @param string $dir
808          * @param array $params
809          * @return SwiftFileBackendDirList
810          */
811         public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
812                 return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
813         }
814
815         /**
816          * @see FileBackendStore::getFileListInternal()
817          * @param string $fullCont
818          * @param string $dir
819          * @param array $params
820          * @return SwiftFileBackendFileList
821          */
822         public function getFileListInternal( $fullCont, $dir, array $params ) {
823                 return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
824         }
825
826         /**
827          * Do not call this function outside of SwiftFileBackendFileList
828          *
829          * @param string $fullCont Resolved container name
830          * @param string $dir Resolved storage directory with no trailing slash
831          * @param string|null &$after Resolved container relative path to list items after
832          * @param int $limit Max number of items to list
833          * @param array $params Parameters for getDirectoryList()
834          * @return array List of container relative resolved paths of directories directly under $dir
835          * @throws FileBackendError
836          */
837         public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
838                 $dirs = [];
839                 if ( $after === INF ) {
840                         return $dirs; // nothing more
841                 }
842
843                 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
844
845                 $prefix = ( $dir == '' ) ? null : "{$dir}/";
846                 // Non-recursive: only list dirs right under $dir
847                 if ( !empty( $params['topOnly'] ) ) {
848                         $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
849                         if ( !$status->isOK() ) {
850                                 throw new FileBackendError( "Iterator page I/O error." );
851                         }
852                         $objects = $status->value;
853                         foreach ( $objects as $object ) { // files and directories
854                                 if ( substr( $object, -1 ) === '/' ) {
855                                         $dirs[] = $object; // directories end in '/'
856                                 }
857                         }
858                 } else {
859                         // Recursive: list all dirs under $dir and its subdirs
860                         $getParentDir = function ( $path ) {
861                                 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
862                         };
863
864                         // Get directory from last item of prior page
865                         $lastDir = $getParentDir( $after ); // must be first page
866                         $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
867
868                         if ( !$status->isOK() ) {
869                                 throw new FileBackendError( "Iterator page I/O error." );
870                         }
871
872                         $objects = $status->value;
873
874                         foreach ( $objects as $object ) { // files
875                                 $objectDir = $getParentDir( $object ); // directory of object
876
877                                 if ( $objectDir !== false && $objectDir !== $dir ) {
878                                         // Swift stores paths in UTF-8, using binary sorting.
879                                         // See function "create_container_table" in common/db.py.
880                                         // If a directory is not "greater" than the last one,
881                                         // then it was already listed by the calling iterator.
882                                         if ( strcmp( $objectDir, $lastDir ) > 0 ) {
883                                                 $pDir = $objectDir;
884                                                 do { // add dir and all its parent dirs
885                                                         $dirs[] = "{$pDir}/";
886                                                         $pDir = $getParentDir( $pDir );
887                                                 } while ( $pDir !== false // sanity
888                                                         && strcmp( $pDir, $lastDir ) > 0 // not done already
889                                                         && strlen( $pDir ) > strlen( $dir ) // within $dir
890                                                 );
891                                         }
892                                         $lastDir = $objectDir;
893                                 }
894                         }
895                 }
896                 // Page on the unfiltered directory listing (what is returned may be filtered)
897                 if ( count( $objects ) < $limit ) {
898                         $after = INF; // avoid a second RTT
899                 } else {
900                         $after = end( $objects ); // update last item
901                 }
902
903                 return $dirs;
904         }
905
906         /**
907          * Do not call this function outside of SwiftFileBackendFileList
908          *
909          * @param string $fullCont Resolved container name
910          * @param string $dir Resolved storage directory with no trailing slash
911          * @param string|null &$after Resolved container relative path of file to list items after
912          * @param int $limit Max number of items to list
913          * @param array $params Parameters for getDirectoryList()
914          * @return array List of resolved container relative paths of files under $dir
915          * @throws FileBackendError
916          */
917         public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
918                 $files = []; // list of (path, stat array or null) entries
919                 if ( $after === INF ) {
920                         return $files; // nothing more
921                 }
922
923                 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
924
925                 $prefix = ( $dir == '' ) ? null : "{$dir}/";
926                 // $objects will contain a list of unfiltered names or CF_Object items
927                 // Non-recursive: only list files right under $dir
928                 if ( !empty( $params['topOnly'] ) ) {
929                         if ( !empty( $params['adviseStat'] ) ) {
930                                 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
931                         } else {
932                                 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
933                         }
934                 } else {
935                         // Recursive: list all files under $dir and its subdirs
936                         if ( !empty( $params['adviseStat'] ) ) {
937                                 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
938                         } else {
939                                 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
940                         }
941                 }
942
943                 // Reformat this list into a list of (name, stat array or null) entries
944                 if ( !$status->isOK() ) {
945                         throw new FileBackendError( "Iterator page I/O error." );
946                 }
947
948                 $objects = $status->value;
949                 $files = $this->buildFileObjectListing( $params, $dir, $objects );
950
951                 // Page on the unfiltered object listing (what is returned may be filtered)
952                 if ( count( $objects ) < $limit ) {
953                         $after = INF; // avoid a second RTT
954                 } else {
955                         $after = end( $objects ); // update last item
956                         $after = is_object( $after ) ? $after->name : $after;
957                 }
958
959                 return $files;
960         }
961
962         /**
963          * Build a list of file objects, filtering out any directories
964          * and extracting any stat info if provided in $objects (for CF_Objects)
965          *
966          * @param array $params Parameters for getDirectoryList()
967          * @param string $dir Resolved container directory path
968          * @param array $objects List of CF_Object items or object names
969          * @return array List of (names,stat array or null) entries
970          */
971         private function buildFileObjectListing( array $params, $dir, array $objects ) {
972                 $names = [];
973                 foreach ( $objects as $object ) {
974                         if ( is_object( $object ) ) {
975                                 if ( isset( $object->subdir ) || !isset( $object->name ) ) {
976                                         continue; // virtual directory entry; ignore
977                                 }
978                                 $stat = [
979                                         // Convert various random Swift dates to TS_MW
980                                         'mtime'  => $this->convertSwiftDate( $object->last_modified, TS_MW ),
981                                         'size'   => (int)$object->bytes,
982                                         'sha1'   => null,
983                                         // Note: manifiest ETags are not an MD5 of the file
984                                         'md5'    => ctype_xdigit( $object->hash ) ? $object->hash : null,
985                                         'latest' => false // eventually consistent
986                                 ];
987                                 $names[] = [ $object->name, $stat ];
988                         } elseif ( substr( $object, -1 ) !== '/' ) {
989                                 // Omit directories, which end in '/' in listings
990                                 $names[] = [ $object, null ];
991                         }
992                 }
993
994                 return $names;
995         }
996
997         /**
998          * Do not call this function outside of SwiftFileBackendFileList
999          *
1000          * @param string $path Storage path
1001          * @param array $val Stat value
1002          */
1003         public function loadListingStatInternal( $path, array $val ) {
1004                 $this->cheapCache->set( $path, 'stat', $val );
1005         }
1006
1007         protected function doGetFileXAttributes( array $params ) {
1008                 $stat = $this->getFileStat( $params );
1009                 if ( $stat ) {
1010                         if ( !isset( $stat['xattr'] ) ) {
1011                                 // Stat entries filled by file listings don't include metadata/headers
1012                                 $this->clearCache( [ $params['src'] ] );
1013                                 $stat = $this->getFileStat( $params );
1014                         }
1015
1016                         return $stat['xattr'];
1017                 } else {
1018                         return false;
1019                 }
1020         }
1021
1022         protected function doGetFileSha1base36( array $params ) {
1023                 $stat = $this->getFileStat( $params );
1024                 if ( $stat ) {
1025                         if ( !isset( $stat['sha1'] ) ) {
1026                                 // Stat entries filled by file listings don't include SHA1
1027                                 $this->clearCache( [ $params['src'] ] );
1028                                 $stat = $this->getFileStat( $params );
1029                         }
1030
1031                         return $stat['sha1'];
1032                 } else {
1033                         return false;
1034                 }
1035         }
1036
1037         protected function doStreamFile( array $params ) {
1038                 $status = $this->newStatus();
1039
1040                 $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
1041
1042                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1043                 if ( $srcRel === null ) {
1044                         StreamFile::send404Message( $params['src'], $flags );
1045                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
1046
1047                         return $status;
1048                 }
1049
1050                 $auth = $this->getAuthentication();
1051                 if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
1052                         StreamFile::send404Message( $params['src'], $flags );
1053                         $status->fatal( 'backend-fail-stream', $params['src'] );
1054
1055                         return $status;
1056                 }
1057
1058                 // If "headers" is set, we only want to send them if the file is there.
1059                 // Do not bother checking if the file exists if headers are not set though.
1060                 if ( $params['headers'] && !$this->fileExists( $params ) ) {
1061                         StreamFile::send404Message( $params['src'], $flags );
1062                         $status->fatal( 'backend-fail-stream', $params['src'] );
1063
1064                         return $status;
1065                 }
1066
1067                 // Send the requested additional headers
1068                 foreach ( $params['headers'] as $header ) {
1069                         header( $header ); // aways send
1070                 }
1071
1072                 if ( empty( $params['allowOB'] ) ) {
1073                         // Cancel output buffering and gzipping if set
1074                         call_user_func( $this->obResetFunc );
1075                 }
1076
1077                 $handle = fopen( 'php://output', 'wb' );
1078                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1079                         'method' => 'GET',
1080                         'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1081                         'headers' => $this->authTokenHeaders( $auth )
1082                                 + $this->headersFromParams( $params ) + $params['options'],
1083                         'stream' => $handle,
1084                         'flags'  => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
1085                 ] );
1086
1087                 if ( $rcode >= 200 && $rcode <= 299 ) {
1088                         // good
1089                 } elseif ( $rcode === 404 ) {
1090                         $status->fatal( 'backend-fail-stream', $params['src'] );
1091                         // Per T43113, nasty things can happen if bad cache entries get
1092                         // stuck in cache. It's also possible that this error can come up
1093                         // with simple race conditions. Clear out the stat cache to be safe.
1094                         $this->clearCache( [ $params['src'] ] );
1095                         $this->deleteFileCache( $params['src'] );
1096                 } else {
1097                         $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1098                 }
1099
1100                 return $status;
1101         }
1102
1103         protected function doGetLocalCopyMulti( array $params ) {
1104                 /** @var TempFSFile[] $tmpFiles */
1105                 $tmpFiles = [];
1106
1107                 $auth = $this->getAuthentication();
1108
1109                 $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
1110                 // Blindly create tmp files and stream to them, catching any exception if the file does
1111                 // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
1112                 $reqs = []; // (path => op)
1113
1114                 foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
1115                         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1116                         if ( $srcRel === null || !$auth ) {
1117                                 $tmpFiles[$path] = null;
1118                                 continue;
1119                         }
1120                         // Get source file extension
1121                         $ext = FileBackend::extensionFromPath( $path );
1122                         // Create a new temporary file...
1123                         $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
1124                         if ( $tmpFile ) {
1125                                 $handle = fopen( $tmpFile->getPath(), 'wb' );
1126                                 if ( $handle ) {
1127                                         $reqs[$path] = [
1128                                                 'method'  => 'GET',
1129                                                 'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
1130                                                 'headers' => $this->authTokenHeaders( $auth )
1131                                                         + $this->headersFromParams( $params ),
1132                                                 'stream'  => $handle,
1133                                         ];
1134                                 } else {
1135                                         $tmpFile = null;
1136                                 }
1137                         }
1138                         $tmpFiles[$path] = $tmpFile;
1139                 }
1140
1141                 $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
1142                 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1143                 $reqs = $this->http->runMulti( $reqs, $opts );
1144                 foreach ( $reqs as $path => $op ) {
1145                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
1146                         fclose( $op['stream'] ); // close open handle
1147                         if ( $rcode >= 200 && $rcode <= 299 ) {
1148                                 $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
1149                                 // Double check that the disk is not full/broken
1150                                 if ( $size != $rhdrs['content-length'] ) {
1151                                         $tmpFiles[$path] = null;
1152                                         $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
1153                                         $this->onError( null, __METHOD__,
1154                                                 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1155                                 }
1156                                 // Set the file stat process cache in passing
1157                                 $stat = $this->getStatFromHeaders( $rhdrs );
1158                                 $stat['latest'] = $isLatest;
1159                                 $this->cheapCache->set( $path, 'stat', $stat );
1160                         } elseif ( $rcode === 404 ) {
1161                                 $tmpFiles[$path] = false;
1162                         } else {
1163                                 $tmpFiles[$path] = null;
1164                                 $this->onError( null, __METHOD__,
1165                                         [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1166                         }
1167                 }
1168
1169                 return $tmpFiles;
1170         }
1171
1172         public function getFileHttpUrl( array $params ) {
1173                 if ( $this->swiftTempUrlKey != '' ||
1174                         ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
1175                 ) {
1176                         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1177                         if ( $srcRel === null ) {
1178                                 return null; // invalid path
1179                         }
1180
1181                         $auth = $this->getAuthentication();
1182                         if ( !$auth ) {
1183                                 return null;
1184                         }
1185
1186                         $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
1187                         $expires = time() + $ttl;
1188
1189                         if ( $this->swiftTempUrlKey != '' ) {
1190                                 $url = $this->storageUrl( $auth, $srcCont, $srcRel );
1191                                 // Swift wants the signature based on the unencoded object name
1192                                 $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
1193                                 $signature = hash_hmac( 'sha1',
1194                                         "GET\n{$expires}\n{$contPath}/{$srcRel}",
1195                                         $this->swiftTempUrlKey
1196                                 );
1197
1198                                 return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
1199                         } else { // give S3 API URL for rgw
1200                                 // Path for signature starts with the bucket
1201                                 $spath = '/' . rawurlencode( $srcCont ) . '/' .
1202                                         str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1203                                 // Calculate the hash
1204                                 $signature = base64_encode( hash_hmac(
1205                                         'sha1',
1206                                         "GET\n\n\n{$expires}\n{$spath}",
1207                                         $this->rgwS3SecretKey,
1208                                         true // raw
1209                                 ) );
1210                                 // See https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
1211                                 // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
1212                                 // Note: S3 API is the rgw default; remove the /swift/ URL bit.
1213                                 return str_replace( '/swift/v1', '', $this->storageUrl( $auth ) . $spath ) .
1214                                         '?' .
1215                                         http_build_query( [
1216                                                 'Signature' => $signature,
1217                                                 'Expires' => $expires,
1218                                                 'AWSAccessKeyId' => $this->rgwS3AccessKey
1219                                         ] );
1220                         }
1221                 }
1222
1223                 return null;
1224         }
1225
1226         protected function directoriesAreVirtual() {
1227                 return true;
1228         }
1229
1230         /**
1231          * Get headers to send to Swift when reading a file based
1232          * on a FileBackend params array, e.g. that of getLocalCopy().
1233          * $params is currently only checked for a 'latest' flag.
1234          *
1235          * @param array $params
1236          * @return array
1237          */
1238         protected function headersFromParams( array $params ) {
1239                 $hdrs = [];
1240                 if ( !empty( $params['latest'] ) ) {
1241                         $hdrs['x-newest'] = 'true';
1242                 }
1243
1244                 return $hdrs;
1245         }
1246
1247         /**
1248          * @param FileBackendStoreOpHandle[] $fileOpHandles
1249          *
1250          * @return StatusValue[]
1251          */
1252         protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1253                 /** @var StatusValue[] $statuses */
1254                 $statuses = [];
1255
1256                 $auth = $this->getAuthentication();
1257                 if ( !$auth ) {
1258                         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1259                                 $statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
1260                         }
1261
1262                         return $statuses;
1263                 }
1264
1265                 // Split the HTTP requests into stages that can be done concurrently
1266                 $httpReqsByStage = []; // map of (stage => index => HTTP request)
1267                 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1268                         /** @var SwiftFileOpHandle $fileOpHandle */
1269                         $reqs = $fileOpHandle->httpOp;
1270                         // Convert the 'url' parameter to an actual URL using $auth
1271                         foreach ( $reqs as $stage => &$req ) {
1272                                 list( $container, $relPath ) = $req['url'];
1273                                 $req['url'] = $this->storageUrl( $auth, $container, $relPath );
1274                                 $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
1275                                 $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
1276                                 $httpReqsByStage[$stage][$index] = $req;
1277                         }
1278                         $statuses[$index] = $this->newStatus();
1279                 }
1280
1281                 // Run all requests for the first stage, then the next, and so on
1282                 $reqCount = count( $httpReqsByStage );
1283                 for ( $stage = 0; $stage < $reqCount; ++$stage ) {
1284                         $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
1285                         foreach ( $httpReqs as $index => $httpReq ) {
1286                                 // Run the callback for each request of this operation
1287                                 $callback = $fileOpHandles[$index]->callback;
1288                                 call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
1289                                 // On failure, abort all remaining requests for this operation
1290                                 // (e.g. abort the DELETE request if the COPY request fails for a move)
1291                                 if ( !$statuses[$index]->isOK() ) {
1292                                         $stages = count( $fileOpHandles[$index]->httpOp );
1293                                         for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
1294                                                 unset( $httpReqsByStage[$s][$index] );
1295                                         }
1296                                 }
1297                         }
1298                 }
1299
1300                 return $statuses;
1301         }
1302
1303         /**
1304          * Set read/write permissions for a Swift container.
1305          *
1306          * @see http://docs.openstack.org/developer/swift/misc.html#acls
1307          *
1308          * In general, we don't allow listings to end-users. It's not useful, isn't well-defined
1309          * (lists are truncated to 10000 item with no way to page), and is just a performance risk.
1310          *
1311          * @param string $container Resolved Swift container
1312          * @param array $readGrps List of the possible criteria for a request to have
1313          * access to read a container. Each item is one of the following formats:
1314          *   - account:user        : Grants access if the request is by the given user
1315          *   - ".r:<regex>"        : Grants access if the request is from a referrer host that
1316          *                           matches the expression and the request is not for a listing.
1317          *                           Setting this to '*' effectively makes a container public.
1318          *   -".rlistings:<regex>" : Grants access if the request is from a referrer host that
1319          *                           matches the expression and the request is for a listing.
1320          * @param array $writeGrps A list of the possible criteria for a request to have
1321          * access to write to a container. Each item is of the following format:
1322          *   - account:user       : Grants access if the request is by the given user
1323          * @return StatusValue
1324          */
1325         protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
1326                 $status = $this->newStatus();
1327                 $auth = $this->getAuthentication();
1328
1329                 if ( !$auth ) {
1330                         $status->fatal( 'backend-fail-connect', $this->name );
1331
1332                         return $status;
1333                 }
1334
1335                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1336                         'method' => 'POST',
1337                         'url' => $this->storageUrl( $auth, $container ),
1338                         'headers' => $this->authTokenHeaders( $auth ) + [
1339                                 'x-container-read' => implode( ',', $readGrps ),
1340                                 'x-container-write' => implode( ',', $writeGrps )
1341                         ]
1342                 ] );
1343
1344                 if ( $rcode != 204 && $rcode !== 202 ) {
1345                         $status->fatal( 'backend-fail-internal', $this->name );
1346                         $this->logger->error( __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
1347                 }
1348
1349                 return $status;
1350         }
1351
1352         /**
1353          * Get a Swift container stat array, possibly from process cache.
1354          * Use $reCache if the file count or byte count is needed.
1355          *
1356          * @param string $container Container name
1357          * @param bool $bypassCache Bypass all caches and load from Swift
1358          * @return array|bool|null False on 404, null on failure
1359          */
1360         protected function getContainerStat( $container, $bypassCache = false ) {
1361                 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1362
1363                 if ( $bypassCache ) { // purge cache
1364                         $this->containerStatCache->clear( $container );
1365                 } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1366                         $this->primeContainerCache( [ $container ] ); // check persistent cache
1367                 }
1368                 if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1369                         $auth = $this->getAuthentication();
1370                         if ( !$auth ) {
1371                                 return null;
1372                         }
1373
1374                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1375                                 'method' => 'HEAD',
1376                                 'url' => $this->storageUrl( $auth, $container ),
1377                                 'headers' => $this->authTokenHeaders( $auth )
1378                         ] );
1379
1380                         if ( $rcode === 204 ) {
1381                                 $stat = [
1382                                         'count' => $rhdrs['x-container-object-count'],
1383                                         'bytes' => $rhdrs['x-container-bytes-used']
1384                                 ];
1385                                 if ( $bypassCache ) {
1386                                         return $stat;
1387                                 } else {
1388                                         $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
1389                                         $this->setContainerCache( $container, $stat ); // update persistent cache
1390                                 }
1391                         } elseif ( $rcode === 404 ) {
1392                                 return false;
1393                         } else {
1394                                 $this->onError( null, __METHOD__,
1395                                         [ 'cont' => $container ], $rerr, $rcode, $rdesc );
1396
1397                                 return null;
1398                         }
1399                 }
1400
1401                 return $this->containerStatCache->get( $container, 'stat' );
1402         }
1403
1404         /**
1405          * Create a Swift container
1406          *
1407          * @param string $container Container name
1408          * @param array $params
1409          * @return StatusValue
1410          */
1411         protected function createContainer( $container, array $params ) {
1412                 $status = $this->newStatus();
1413
1414                 $auth = $this->getAuthentication();
1415                 if ( !$auth ) {
1416                         $status->fatal( 'backend-fail-connect', $this->name );
1417
1418                         return $status;
1419                 }
1420
1421                 // @see SwiftFileBackend::setContainerAccess()
1422                 if ( empty( $params['noAccess'] ) ) {
1423                         $readGrps = [ '.r:*', $this->swiftUser ]; // public
1424                 } else {
1425                         $readGrps = [ $this->swiftUser ]; // private
1426                 }
1427                 $writeGrps = [ $this->swiftUser ]; // sanity
1428
1429                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1430                         'method' => 'PUT',
1431                         'url' => $this->storageUrl( $auth, $container ),
1432                         'headers' => $this->authTokenHeaders( $auth ) + [
1433                                 'x-container-read' => implode( ',', $readGrps ),
1434                                 'x-container-write' => implode( ',', $writeGrps )
1435                         ]
1436                 ] );
1437
1438                 if ( $rcode === 201 ) { // new
1439                         // good
1440                 } elseif ( $rcode === 202 ) { // already there
1441                         // this shouldn't really happen, but is OK
1442                 } else {
1443                         $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1444                 }
1445
1446                 return $status;
1447         }
1448
1449         /**
1450          * Delete a Swift container
1451          *
1452          * @param string $container Container name
1453          * @param array $params
1454          * @return StatusValue
1455          */
1456         protected function deleteContainer( $container, array $params ) {
1457                 $status = $this->newStatus();
1458
1459                 $auth = $this->getAuthentication();
1460                 if ( !$auth ) {
1461                         $status->fatal( 'backend-fail-connect', $this->name );
1462
1463                         return $status;
1464                 }
1465
1466                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1467                         'method' => 'DELETE',
1468                         'url' => $this->storageUrl( $auth, $container ),
1469                         'headers' => $this->authTokenHeaders( $auth )
1470                 ] );
1471
1472                 if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
1473                         $this->containerStatCache->clear( $container ); // purge
1474                 } elseif ( $rcode === 404 ) { // not there
1475                         // this shouldn't really happen, but is OK
1476                 } elseif ( $rcode === 409 ) { // not empty
1477                         $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
1478                 } else {
1479                         $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1480                 }
1481
1482                 return $status;
1483         }
1484
1485         /**
1486          * Get a list of objects under a container.
1487          * Either just the names or a list of stdClass objects with details can be returned.
1488          *
1489          * @param string $fullCont
1490          * @param string $type ('info' for a list of object detail maps, 'names' for names only)
1491          * @param int $limit
1492          * @param string|null $after
1493          * @param string|null $prefix
1494          * @param string|null $delim
1495          * @return StatusValue With the list as value
1496          */
1497         private function objectListing(
1498                 $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
1499         ) {
1500                 $status = $this->newStatus();
1501
1502                 $auth = $this->getAuthentication();
1503                 if ( !$auth ) {
1504                         $status->fatal( 'backend-fail-connect', $this->name );
1505
1506                         return $status;
1507                 }
1508
1509                 $query = [ 'limit' => $limit ];
1510                 if ( $type === 'info' ) {
1511                         $query['format'] = 'json';
1512                 }
1513                 if ( $after !== null ) {
1514                         $query['marker'] = $after;
1515                 }
1516                 if ( $prefix !== null ) {
1517                         $query['prefix'] = $prefix;
1518                 }
1519                 if ( $delim !== null ) {
1520                         $query['delimiter'] = $delim;
1521                 }
1522
1523                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1524                         'method' => 'GET',
1525                         'url' => $this->storageUrl( $auth, $fullCont ),
1526                         'query' => $query,
1527                         'headers' => $this->authTokenHeaders( $auth )
1528                 ] );
1529
1530                 $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
1531                 if ( $rcode === 200 ) { // good
1532                         if ( $type === 'info' ) {
1533                                 $status->value = FormatJson::decode( trim( $rbody ) );
1534                         } else {
1535                                 $status->value = explode( "\n", trim( $rbody ) );
1536                         }
1537                 } elseif ( $rcode === 204 ) {
1538                         $status->value = []; // empty container
1539                 } elseif ( $rcode === 404 ) {
1540                         $status->value = []; // no container
1541                 } else {
1542                         $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1543                 }
1544
1545                 return $status;
1546         }
1547
1548         protected function doPrimeContainerCache( array $containerInfo ) {
1549                 foreach ( $containerInfo as $container => $info ) {
1550                         $this->containerStatCache->set( $container, 'stat', $info );
1551                 }
1552         }
1553
1554         protected function doGetFileStatMulti( array $params ) {
1555                 $stats = [];
1556
1557                 $auth = $this->getAuthentication();
1558
1559                 $reqs = [];
1560                 foreach ( $params['srcs'] as $path ) {
1561                         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1562                         if ( $srcRel === null ) {
1563                                 $stats[$path] = false;
1564                                 continue; // invalid storage path
1565                         } elseif ( !$auth ) {
1566                                 $stats[$path] = null;
1567                                 continue;
1568                         }
1569
1570                         // (a) Check the container
1571                         $cstat = $this->getContainerStat( $srcCont );
1572                         if ( $cstat === false ) {
1573                                 $stats[$path] = false;
1574                                 continue; // ok, nothing to do
1575                         } elseif ( !is_array( $cstat ) ) {
1576                                 $stats[$path] = null;
1577                                 continue;
1578                         }
1579
1580                         $reqs[$path] = [
1581                                 'method'  => 'HEAD',
1582                                 'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
1583                                 'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
1584                         ];
1585                 }
1586
1587                 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1588                 $reqs = $this->http->runMulti( $reqs, $opts );
1589
1590                 foreach ( $params['srcs'] as $path ) {
1591                         if ( array_key_exists( $path, $stats ) ) {
1592                                 continue; // some sort of failure above
1593                         }
1594                         // (b) Check the file
1595                         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
1596                         if ( $rcode === 200 || $rcode === 204 ) {
1597                                 // Update the object if it is missing some headers
1598                                 $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
1599                                 // Load the stat array from the headers
1600                                 $stat = $this->getStatFromHeaders( $rhdrs );
1601                                 if ( $this->isRGW ) {
1602                                         $stat['latest'] = true; // strong consistency
1603                                 }
1604                         } elseif ( $rcode === 404 ) {
1605                                 $stat = false;
1606                         } else {
1607                                 $stat = null;
1608                                 $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
1609                         }
1610                         $stats[$path] = $stat;
1611                 }
1612
1613                 return $stats;
1614         }
1615
1616         /**
1617          * @param array $rhdrs
1618          * @return array
1619          */
1620         protected function getStatFromHeaders( array $rhdrs ) {
1621                 // Fetch all of the custom metadata headers
1622                 $metadata = $this->getMetadata( $rhdrs );
1623                 // Fetch all of the custom raw HTTP headers
1624                 $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
1625
1626                 return [
1627                         // Convert various random Swift dates to TS_MW
1628                         'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
1629                         // Empty objects actually return no content-length header in Ceph
1630                         'size'  => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
1631                         'sha1'  => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
1632                         // Note: manifiest ETags are not an MD5 of the file
1633                         'md5'   => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
1634                         'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
1635                 ];
1636         }
1637
1638         /**
1639          * @return array|null Credential map
1640          */
1641         protected function getAuthentication() {
1642                 if ( $this->authErrorTimestamp !== null ) {
1643                         if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
1644                                 return null; // failed last attempt; don't bother
1645                         } else { // actually retry this time
1646                                 $this->authErrorTimestamp = null;
1647                         }
1648                 }
1649                 // Session keys expire after a while, so we renew them periodically
1650                 $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
1651                 // Authenticate with proxy and get a session key...
1652                 if ( !$this->authCreds || $reAuth ) {
1653                         $this->authSessionTimestamp = 0;
1654                         $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
1655                         $creds = $this->srvCache->get( $cacheKey ); // credentials
1656                         // Try to use the credential cache
1657                         if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
1658                                 $this->authCreds = $creds;
1659                                 // Skew the timestamp for worst case to avoid using stale credentials
1660                                 $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
1661                         } else { // cache miss
1662                                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1663                                         'method' => 'GET',
1664                                         'url' => "{$this->swiftAuthUrl}/v1.0",
1665                                         'headers' => [
1666                                                 'x-auth-user' => $this->swiftUser,
1667                                                 'x-auth-key' => $this->swiftKey
1668                                         ]
1669                                 ] );
1670
1671                                 if ( $rcode >= 200 && $rcode <= 299 ) { // OK
1672                                         $this->authCreds = [
1673                                                 'auth_token' => $rhdrs['x-auth-token'],
1674                                                 'storage_url' => ( $this->swiftStorageUrl !== null )
1675                                                         ? $this->swiftStorageUrl
1676                                                         : $rhdrs['x-storage-url']
1677                                         ];
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
1702         /**
1703          * @param array $creds From getAuthentication()
1704          * @param string $container
1705          * @param string $object
1706          * @return string
1707          */
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
1720         /**
1721          * @param array $creds From getAuthentication()
1722          * @return array
1723          */
1724         protected function authTokenHeaders( array $creds ) {
1725                 return [ 'x-auth-token' => $creds['auth_token'] ];
1726         }
1727
1728         /**
1729          * Get the cache key for a container
1730          *
1731          * @param string $username
1732          * @return string
1733          */
1734         private function getCredsCacheKey( $username ) {
1735                 return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
1736         }
1737
1738         /**
1739          * Log an unexpected exception for this backend.
1740          * This also sets the StatusValue object to have a fatal error.
1741          *
1742          * @param StatusValue|null $status
1743          * @param string $func
1744          * @param array $params
1745          * @param string $err Error string
1746          * @param int $code HTTP status
1747          * @param string $desc HTTP StatusValue description
1748          */
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
1763 /**
1764  * @see FileBackendStoreOpHandle
1765  */
1766 class SwiftFileOpHandle extends FileBackendStoreOpHandle {
1767         /** @var array List of Requests for MultiHttpClient */
1768         public $httpOp;
1769         /** @var Closure */
1770         public $callback;
1771
1772         /**
1773          * @param SwiftFileBackend $backend
1774          * @param Closure $callback Function that takes (HTTP request array, status)
1775          * @param array $httpOp MultiHttpClient op
1776          */
1777         public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
1778                 $this->backend = $backend;
1779                 $this->callback = $callback;
1780                 $this->httpOp = $httpOp;
1781         }
1782 }
1783
1784 /**
1785  * SwiftFileBackend helper class to page through listings.
1786  * Swift also has a listing limit of 10,000 objects for sanity.
1787  * Do not use this class from places outside SwiftFileBackend.
1788  *
1789  * @ingroup FileBackend
1790  */
1791 abstract class SwiftFileBackendList implements Iterator {
1792         /** @var array List of path or (path,stat array) entries */
1793         protected $bufferIter = [];
1794
1795         /** @var string List items *after* this path */
1796         protected $bufferAfter = null;
1797
1798         /** @var int */
1799         protected $pos = 0;
1800
1801         /** @var array */
1802         protected $params = [];
1803
1804         /** @var SwiftFileBackend */
1805         protected $backend;
1806
1807         /** @var string Container name */
1808         protected $container;
1809
1810         /** @var string Storage directory */
1811         protected $dir;
1812
1813         /** @var int */
1814         protected $suffixStart;
1815
1816         const PAGE_SIZE = 9000; // file listing buffer size
1817
1818         /**
1819          * @param SwiftFileBackend $backend
1820          * @param string $fullCont Resolved container name
1821          * @param string $dir Resolved directory relative to container
1822          * @param array $params
1823          */
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
1839         /**
1840          * @see Iterator::key()
1841          * @return int
1842          */
1843         public function key() {
1844                 return $this->pos;
1845         }
1846
1847         /**
1848          * @see Iterator::next()
1849          */
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
1863         /**
1864          * @see Iterator::rewind()
1865          */
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
1874         /**
1875          * @see Iterator::valid()
1876          * @return bool
1877          */
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
1886         /**
1887          * Get the given list portion (page)
1888          *
1889          * @param string $container Resolved container name
1890          * @param string $dir Resolved path relative to container
1891          * @param string &$after
1892          * @param int $limit
1893          * @param array $params
1894          * @return Traversable|array
1895          */
1896         abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
1897 }
1898
1899 /**
1900  * Iterator for listing directories
1901  */
1902 class SwiftFileBackendDirList extends SwiftFileBackendList {
1903         /**
1904          * @see Iterator::current()
1905          * @return string|bool String (relative path) or false
1906          */
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
1916 /**
1917  * Iterator for listing regular files
1918  */
1919 class SwiftFileBackendFileList extends SwiftFileBackendList {
1920         /**
1921          * @see Iterator::current()
1922          * @return string|bool String (relative path) or false
1923          */
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 }