r108944: fixed bogus field...php-cloudfiles documentation misspelled this, it's ...
[mediawiki.git] / includes / filerepo / backend / SwiftFileBackend.php
blob8b7602ba30937a1a8d92f64132605d9e36515dfb
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Russ Nelson
6 * @author Aaron Schulz
7 */
9 /**
10 * Class for an OpenStack Swift based file backend.
12 * This requires that the php-cloudfiles library is present,
13 * which is available at https://github.com/rackspace/php-cloudfiles.
14 * All of the library classes must be registed in $wgAutoloadClasses.
16 * Status messages should avoid mentioning the Swift account name
17 * Likewise, error suppression should be used to avoid path disclosure.
19 * @ingroup FileBackend
20 * @since 1.19
22 class SwiftFileBackend extends FileBackend {
23 /** @var CF_Authentication */
24 protected $auth; // Swift authentication handler
26 /** @var CF_Connection */
27 protected $conn; // Swift connection handle
28 protected $connStarted = 0; // integer UNIX timestamp
29 protected $connContainers = array(); // container object cache
30 protected $connTTL = 120; // integer seconds
32 protected $swiftProxyUser; // string
34 /**
35 * @see FileBackend::__construct()
36 * Additional $config params include:
37 * swiftAuthUrl : Swift authentication server URL
38 * swiftUser : Swift user used by MediaWiki
39 * swiftKey : Swift authentication key for the above user
40 * swiftProxyUser : Swift user used for end-user hits to proxy server
41 * shardViaHashLevels : Map of container names to the number of hash levels
43 public function __construct( array $config ) {
44 parent::__construct( $config );
45 // Required settings
46 $this->auth = new CF_Authentication(
47 $config['swiftUser'], $config['swiftKey'], null, $config['swiftAuthUrl'] );
48 // Optional settings
49 $this->connTTL = isset( $config['connTTL'] )
50 ? $config['connTTL']
51 : 60; // some sane number
52 $this->swiftProxyUser = isset( $config['swiftProxyUser'] )
53 ? $config['swiftProxyUser']
54 : '';
55 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
56 ? $config['shardViaHashLevels']
57 : '';
60 /**
61 * @see FileBackend::resolveContainerPath()
63 protected function resolveContainerPath( $container, $relStoragePath ) {
64 if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
65 return null; // too long for Swift
67 return $relStoragePath;
70 /**
71 * @see FileBackend::doCopyInternal()
73 protected function doCreateInternal( array $params ) {
74 $status = Status::newGood();
76 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
77 if ( $destRel === null ) {
78 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
79 return $status;
82 // (a) Check the destination container
83 try {
84 $dContObj = $this->getContainer( $dstCont );
85 } catch ( NoSuchContainerException $e ) {
86 $status->fatal( 'backend-fail-create', $params['dst'] );
87 return $status;
88 } catch ( InvalidResponseException $e ) {
89 $status->fatal( 'backend-fail-connect', $this->name );
90 return $status;
91 } catch ( Exception $e ) { // some other exception?
92 $status->fatal( 'backend-fail-internal', $this->name );
93 $this->logException( $e, __METHOD__, $params );
94 return $status;
97 // (b) Check if the destination object already exists
98 try {
99 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
100 // NoSuchObjectException not thrown: file must exist
101 if ( empty( $params['overwriteDest'] ) ) {
102 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
103 return $status;
105 } catch ( NoSuchObjectException $e ) {
106 // NoSuchObjectException thrown: file does not exist
107 } catch ( InvalidResponseException $e ) {
108 $status->fatal( 'backend-fail-connect', $this->name );
109 return $status;
110 } catch ( Exception $e ) { // some other exception?
111 $status->fatal( 'backend-fail-internal', $this->name );
112 $this->logException( $e, __METHOD__, $params );
113 return $status;
116 // (c) Get a SHA-1 hash of the object
117 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
119 // (d) Actually create the object
120 try {
121 $obj = $dContObj->create_object( $destRel );
122 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
123 $obj->metadata = array( 'Sha1base36' => $sha1Hash );
124 $obj->write( $params['content'] );
125 } catch ( BadContentTypeException $e ) {
126 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
127 } catch ( InvalidResponseException $e ) {
128 $status->fatal( 'backend-fail-connect', $this->name );
129 } catch ( Exception $e ) { // some other exception?
130 $status->fatal( 'backend-fail-internal', $this->name );
131 $this->logException( $e, __METHOD__, $params );
134 return $status;
138 * @see FileBackend::doStoreInternal()
140 protected function doStoreInternal( array $params ) {
141 $status = Status::newGood();
143 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
144 if ( $destRel === null ) {
145 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
146 return $status;
149 // (a) Check the destination container
150 try {
151 $dContObj = $this->getContainer( $dstCont );
152 } catch ( NoSuchContainerException $e ) {
153 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
154 return $status;
155 } catch ( InvalidResponseException $e ) {
156 $status->fatal( 'backend-fail-connect', $this->name );
157 return $status;
158 } catch ( Exception $e ) { // some other exception?
159 $status->fatal( 'backend-fail-internal', $this->name );
160 $this->logException( $e, __METHOD__, $params );
161 return $status;
164 // (b) Check if the destination object already exists
165 try {
166 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
167 // NoSuchObjectException not thrown: file must exist
168 if ( empty( $params['overwriteDest'] ) ) {
169 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
170 return $status;
172 } catch ( NoSuchObjectException $e ) {
173 // NoSuchObjectException thrown: file does not exist
174 } catch ( InvalidResponseException $e ) {
175 $status->fatal( 'backend-fail-connect', $this->name );
176 return $status;
177 } catch ( Exception $e ) { // some other exception?
178 $status->fatal( 'backend-fail-internal', $this->name );
179 $this->logException( $e, __METHOD__, $params );
180 return $status;
183 // (c) Get a SHA-1 hash of the object
184 $sha1Hash = sha1_file( $params['src'] );
185 if ( $sha1Hash === false ) { // source doesn't exist?
186 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
187 return $status;
189 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
191 // (d) Actually store the object
192 try {
193 $obj = $dContObj->create_object( $destRel );
194 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
195 $obj->metadata = array( 'Sha1base36' => $sha1Hash );
196 $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
197 } catch ( BadContentTypeException $e ) {
198 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
199 } catch ( IOException $e ) {
200 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
201 } catch ( InvalidResponseException $e ) {
202 $status->fatal( 'backend-fail-connect', $this->name );
203 } catch ( Exception $e ) { // some other exception?
204 $status->fatal( 'backend-fail-internal', $this->name );
205 $this->logException( $e, __METHOD__, $params );
208 return $status;
212 * @see FileBackend::doCopyInternal()
214 protected function doCopyInternal( array $params ) {
215 $status = Status::newGood();
217 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
218 if ( $srcRel === null ) {
219 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
220 return $status;
223 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
224 if ( $destRel === null ) {
225 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
226 return $status;
229 // (a) Check the source and destination containers
230 try {
231 $sContObj = $this->getContainer( $srcCont );
232 $dContObj = $this->getContainer( $dstCont );
233 } catch ( NoSuchContainerException $e ) {
234 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
235 return $status;
236 } catch ( InvalidResponseException $e ) {
237 $status->fatal( 'backend-fail-connect', $this->name );
238 return $status;
239 } catch ( Exception $e ) { // some other exception?
240 $status->fatal( 'backend-fail-internal', $this->name );
241 $this->logException( $e, __METHOD__, $params );
242 return $status;
245 // (b) Check if the destination object already exists
246 try {
247 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
248 // NoSuchObjectException not thrown: file must exist
249 if ( empty( $params['overwriteDest'] ) ) {
250 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
251 return $status;
253 } catch ( NoSuchObjectException $e ) {
254 // NoSuchObjectException thrown: file does not exist
255 } catch ( InvalidResponseException $e ) {
256 $status->fatal( 'backend-fail-connect', $this->name );
257 return $status;
258 } catch ( Exception $e ) { // some other exception?
259 $status->fatal( 'backend-fail-internal', $this->name );
260 $this->logException( $e, __METHOD__, $params );
261 return $status;
264 // (c) Actually copy the file to the destination
265 try {
266 $sContObj->copy_object_to( $srcRel, $dContObj, $destRel );
267 } catch ( NoSuchObjectException $e ) { // source object does not exist
268 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
269 } catch ( InvalidResponseException $e ) {
270 $status->fatal( 'backend-fail-connect', $this->name );
271 } catch ( Exception $e ) { // some other exception?
272 $status->fatal( 'backend-fail-internal', $this->name );
273 $this->logException( $e, __METHOD__, $params );
276 return $status;
280 * @see FileBackend::doDeleteInternal()
282 protected function doDeleteInternal( array $params ) {
283 $status = Status::newGood();
285 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
286 if ( $srcRel === null ) {
287 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
288 return $status;
291 // (a) Check the source container
292 try {
293 $sContObj = $this->getContainer( $srcCont );
294 } catch ( NoSuchContainerException $e ) {
295 $status->fatal( 'backend-fail-delete', $params['src'] );
296 return $status;
297 } catch ( InvalidResponseException $e ) {
298 $status->fatal( 'backend-fail-connect', $this->name );
299 return $status;
300 } catch ( Exception $e ) { // some other exception?
301 $status->fatal( 'backend-fail-internal', $this->name );
302 $this->logException( $e, __METHOD__, $params );
303 return $status;
306 // (b) Actually delete the object
307 try {
308 $sContObj->delete_object( $srcRel );
309 } catch ( NoSuchObjectException $e ) {
310 if ( empty( $params['ignoreMissingSource'] ) ) {
311 $status->fatal( 'backend-fail-delete', $params['src'] );
313 } catch ( InvalidResponseException $e ) {
314 $status->fatal( 'backend-fail-connect', $this->name );
315 } catch ( Exception $e ) { // some other exception?
316 $status->fatal( 'backend-fail-internal', $this->name );
317 $this->logException( $e, __METHOD__, $params );
320 return $status;
324 * @see FileBackend::doPrepareInternal()
326 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
327 $status = Status::newGood();
329 try {
330 $this->createContainer( $fullCont );
331 } catch ( InvalidResponseException $e ) {
332 $status->fatal( 'backend-fail-connect', $this->name );
333 } catch ( Exception $e ) { // some other exception?
334 $status->fatal( 'backend-fail-internal', $this->name );
335 $this->logException( $e, __METHOD__, $params );
338 return $status;
342 * @see FileBackend::doSecureInternal()
344 protected function doSecureInternal( $fullCont, $dir, array $params ) {
345 $status = Status::newGood();
346 // @TODO: restrict container from $this->swiftProxyUser
347 return $status;
351 * @see FileBackend::doCleanInternal()
353 protected function doCleanInternal( $fullCont, $dir, array $params ) {
354 $status = Status::newGood();
356 // (a) Check the container
357 try {
358 $contObj = $this->getContainer( $fullCont, true );
359 } catch ( NoSuchContainerException $e ) {
360 return $status; // ok, nothing to do
361 } catch ( InvalidResponseException $e ) {
362 $status->fatal( 'backend-fail-connect', $this->name );
363 return $status;
364 } catch ( Exception $e ) { // some other exception?
365 $status->fatal( 'backend-fail-internal', $this->name );
366 $this->logException( $e, __METHOD__, $params );
367 return $status;
370 // (c) Delete the container if empty
371 if ( $contObj->object_count == 0 ) {
372 try {
373 $this->deleteContainer( $fullCont );
374 } catch ( NoSuchContainerException $e ) {
375 return $status; // race?
376 } catch ( InvalidResponseException $e ) {
377 $status->fatal( 'backend-fail-connect', $this->name );
378 return $status;
379 } catch ( Exception $e ) { // some other exception?
380 $status->fatal( 'backend-fail-internal', $this->name );
381 $this->logException( $e, __METHOD__, $params );
382 return $status;
386 return $status;
390 * @see FileBackend::doFileExists()
392 protected function doGetFileStat( array $params ) {
393 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
394 if ( $srcRel === null ) {
395 return false; // invalid storage path
398 $stat = false;
399 try {
400 $container = $this->getContainer( $srcCont );
401 // @TODO: handle 'latest' param as "X-Newest: true"
402 $obj = $container->get_object( $srcRel );
403 // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
404 $date = DateTime::createFromFormat( 'D, d F Y G:i:s e', $obj->last_modified );
405 if ( $date ) {
406 $stat = array(
407 'mtime' => $date->format( 'YmdHis' ),
408 'size' => $obj->content_length,
409 'sha1' => $obj->metadata['Sha1base36']
411 } else { // exception will be caught below
412 throw new Exception( "Could not parse date for object {$srcRel}" );
414 } catch ( NoSuchContainerException $e ) {
415 } catch ( NoSuchObjectException $e ) {
416 } catch ( InvalidResponseException $e ) {
417 $stat = null;
418 } catch ( Exception $e ) { // some other exception?
419 $stat = null;
420 $this->logException( $e, __METHOD__, $params );
423 return $stat;
427 * @see FileBackendBase::getFileContents()
429 public function getFileContents( array $params ) {
430 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
431 if ( $srcRel === null ) {
432 return false; // invalid storage path
435 $data = false;
436 try {
437 $container = $this->getContainer( $srcCont );
438 $obj = $container->get_object( $srcRel );
439 $data = $obj->read( $this->headersFromParams( $params ) );
440 } catch ( NoSuchContainerException $e ) {
441 } catch ( NoSuchObjectException $e ) {
442 } catch ( InvalidResponseException $e ) {
443 } catch ( Exception $e ) { // some other exception?
444 $this->logException( $e, __METHOD__, $params );
447 return $data;
451 * @see FileBackend::getFileListInternal()
453 public function getFileListInternal( $fullCont, $dir, array $params ) {
454 return new SwiftFileBackendFileList( $this, $fullCont, $dir );
458 * Do not call this function outside of SwiftFileBackendFileList
460 * @param $fullCont string Resolved container name
461 * @param $dir string Resolved storage directory with no trailing slash
462 * @param $after string Storage path of file to list items after
463 * @param $limit integer Max number of items to list
464 * @return Array
466 public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
467 $files = array();
468 try {
469 $container = $this->getContainer( $fullCont );
470 $files = $container->list_objects( $limit, $after, "{$dir}/" );
471 } catch ( NoSuchContainerException $e ) {
472 } catch ( NoSuchObjectException $e ) {
473 } catch ( InvalidResponseException $e ) {
474 } catch ( Exception $e ) { // some other exception?
475 $this->logException( $e, __METHOD__, $params );
478 return $files;
482 * @see FileBackend::doGetFileSha1base36()
484 public function doGetFileSha1base36( array $params ) {
485 $stat = $this->getFileStat( $params );
486 if ( $stat ) {
487 return $stat['sha1'];
488 } else {
489 return false;
494 * @see FileBackend::doStreamFile()
496 protected function doStreamFile( array $params ) {
497 $status = Status::newGood();
499 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
500 if ( $srcRel === null ) {
501 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
504 try {
505 $cont = $this->getContainer( $srcCont );
506 $obj = $cont->get_object( $srcRel );
507 } catch ( NoSuchContainerException $e ) {
508 $status->fatal( 'backend-fail-stream', $params['src'] );
509 return $status;
510 } catch ( NoSuchObjectException $e ) {
511 $status->fatal( 'backend-fail-stream', $params['src'] );
512 return $status;
513 } catch ( IOException $e ) {
514 $status->fatal( 'backend-fail-stream', $params['src'] );
515 return $status;
516 } catch ( Exception $e ) { // some other exception?
517 $status->fatal( 'backend-fail-stream', $params['src'] );
518 $this->logException( $e, __METHOD__, $params );
519 return $status;
522 try {
523 $output = fopen( 'php://output', 'w' );
524 $obj->stream( $output, $this->headersFromParams( $params ) );
525 } catch ( InvalidResponseException $e ) {
526 $status->fatal( 'backend-fail-connect', $this->name );
527 } catch ( Exception $e ) { // some other exception?
528 $status->fatal( 'backend-fail-stream', $params['src'] );
529 $this->logException( $e, __METHOD__, $params );
532 return $status;
536 * @see FileBackend::getLocalCopy()
538 public function getLocalCopy( array $params ) {
539 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
540 if ( $srcRel === null ) {
541 return null;
544 // Get source file extension
545 $ext = FileBackend::extensionFromPath( $srcRel );
546 // Create a new temporary file...
547 $tmpFile = TempFSFile::factory( wfBaseName( $srcRel ) . '_', $ext );
548 if ( !$tmpFile ) {
549 return null;
552 try {
553 $cont = $this->getContainer( $srcCont );
554 $obj = $cont->get_object( $srcRel );
555 $handle = fopen( $tmpFile->getPath(), 'w' );
556 if ( $handle ) {
557 $obj->stream( $handle, $this->headersFromParams( $params ) );
558 fclose( $handle );
559 } else {
560 $tmpFile = null; // couldn't open temp file
562 } catch ( NoSuchContainerException $e ) {
563 $tmpFile = null;
564 } catch ( NoSuchObjectException $e ) {
565 $tmpFile = null;
566 } catch ( InvalidResponseException $e ) {
567 $tmpFile = null;
568 } catch ( Exception $e ) { // some other exception?
569 $tmpFile = null;
570 $this->logException( $e, __METHOD__, $params );
573 return $tmpFile;
577 * Get headers to send to Swift when reading a file based
578 * on a FileBackend params array, e.g. that of getLocalCopy().
579 * $params is currently only checked for a 'latest' flag.
581 * @param $params Array
582 * @return Array
584 protected function headersFromParams( array $params ) {
585 $hdrs = array();
586 if ( !empty( $params['latest'] ) ) {
587 $hdrs[] = 'X-Newest: true';
589 return $hdrs;
593 * Get a connection to the Swift proxy
595 * @return CF_Connection|false
596 * @throws InvalidResponseException
598 protected function getConnection() {
599 if ( $this->conn === false ) {
600 return false; // failed last attempt
602 // Authenticate with proxy and get a session key.
603 // Session keys expire after a while, so we renew them periodically.
604 if ( $this->conn === null || ( time() - $this->connStarted ) > $this->connTTL ) {
605 $this->connContainers = array();
606 try {
607 $this->auth->authenticate();
608 $this->conn = new CF_Connection( $this->auth );
609 $this->connStarted = time();
610 } catch ( AuthenticationException $e ) {
611 $this->conn = false; // don't keep re-trying
612 } catch ( InvalidResponseException $e ) {
613 $this->conn = false; // don't keep re-trying
616 if ( !$this->conn ) {
617 throw new InvalidResponseException; // auth/connection problem
619 return $this->conn;
623 * Get a Swift container object, possibly from process cache.
624 * Use $reCache if the file count or byte count is needed.
626 * @param $container string Container name
627 * @param $reCache bool Refresh the process cache
628 * @return CF_Container
630 protected function getContainer( $container, $reCache = false ) {
631 $conn = $this->getConnection(); // Swift proxy connection
632 if ( $reCache ) {
633 unset( $this->connContainers[$container] ); // purge cache
635 if ( !isset( $this->connContainers[$container] ) ) {
636 $contObj = $conn->get_container( $container );
637 // Exception not thrown: container must exist
638 $this->connContainers[$container] = $contObj; // cache it
640 return $this->connContainers[$container];
644 * Create a Swift container
646 * @param $container string Container name
647 * @return CF_Container
649 protected function createContainer( $container ) {
650 $conn = $this->getConnection(); // Swift proxy connection
651 $contObj = $conn->create_container( $container );
652 $this->connContainers[$container] = $contObj; // cache it
653 return $contObj;
657 * Delete a Swift container
659 * @param $container string Container name
660 * @return void
662 protected function deleteContainer( $container ) {
663 $conn = $this->getConnection(); // Swift proxy connection
664 $conn->delete_container( $container );
665 unset( $this->connContainers[$container] ); // purge cache
669 * Log an unexpected exception for this backend
671 * @param $e Exception
672 * @param $func string
673 * @param $params Array
674 * @return void
676 protected function logException( Exception $e, $func, array $params ) {
677 wfDebugLog( 'SwiftBackend',
678 get_class( $e ) . " in '{$this->name}': '{$func}' with " . serialize( $params )
684 * SwiftFileBackend helper class to page through object listings.
685 * Swift also has a listing limit of 10,000 objects for sanity.
686 * Do not use this class from places outside SwiftFileBackend.
688 * @ingroup FileBackend
690 class SwiftFileBackendFileList implements Iterator {
691 /** @var Array */
692 protected $bufferIter = array();
693 protected $bufferAfter = null; // string; list items *after* this path
694 protected $pos = 0; // integer
696 /** @var SwiftFileBackend */
697 protected $backend;
698 protected $container; //
699 protected $dir; // string storage directory
700 protected $suffixStart; // integer
702 const PAGE_SIZE = 5000; // file listing buffer size
705 * @param $backend SwiftFileBackend
706 * @param $fullCont string Resolved container name
707 * @param $dir string Resolved directory relative to container
709 public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) {
710 $this->backend = $backend;
711 $this->container = $fullCont;
712 $this->dir = $dir;
713 if ( substr( $this->dir, -1 ) === '/' ) {
714 $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
716 $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
719 public function current() {
720 return substr( current( $this->bufferIter ), $this->suffixStart );
723 public function key() {
724 return $this->pos;
727 public function next() {
728 // Advance to the next file in the page
729 next( $this->bufferIter );
730 ++$this->pos;
731 // Check if there are no files left in this page and
732 // advance to the next page if this page was not empty.
733 if ( !$this->valid() && count( $this->bufferIter ) ) {
734 $this->bufferAfter = end( $this->bufferIter );
735 $this->bufferIter = $this->backend->getFileListPageInternal(
736 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
741 public function rewind() {
742 $this->pos = 0;
743 $this->bufferAfter = null;
744 $this->bufferIter = $this->backend->getFileListPageInternal(
745 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
749 public function valid() {
750 return ( current( $this->bufferIter ) !== false ); // no paths can have this value