In FileBackend:
[lhc/web/wiklou.git] / includes / filerepo / backend / SwiftFileBackend.php
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Russ Nelson
6 * @author Aaron Schulz
7 */
8
9 /**
10 * Class for a Swift based file backend.
11 * Status messages should avoid mentioning the Swift account name
12 * Likewise, error suppression should be used to avoid path disclosure.
13 *
14 * This requires that the php-cloudfiles library is present,
15 * which is available at https://github.com/rackspace/php-cloudfiles.
16 * All of the library classes must be registed in $wgAutoloadClasses.
17 *
18 * @ingroup FileBackend
19 */
20 class SwiftFileBackend extends FileBackend {
21 /** @var CF_Authentication */
22 protected $auth; // swift authentication handler
23 /** @var CF_Connection */
24 protected $conn; // swift connection handle
25 protected $connStarted = 0; // integer UNIX timestamp
26
27 protected $swiftProxyUser; // string
28 protected $connTTL = 60; // integer seconds
29
30 /**
31 * @see FileBackend::__construct()
32 * Additional $config params include:
33 * swiftAuthUrl : Swift authentication server URL
34 * swiftUser : Swift user used by MediaWiki
35 * swiftKey : Swift authentication key for the above user
36 * swiftProxyUser : Swift user used for end-user hits to proxy server
37 * shardViaHashLevels : Map of container names to the number of hash levels
38 */
39 public function __construct( array $config ) {
40 parent::__construct( $config );
41 // Required settings
42 $this->auth = new CF_Authentication(
43 $config['swiftUser'], $config['swiftKey'], null, $config['swiftAuthUrl'] );
44 // Optional settings
45 $this->connTTL = isset( $config['connTTL'] )
46 ? $config['connTTL']
47 : 60; // some sane number
48 $this->swiftProxyUser = isset( $config['swiftProxyUser'] )
49 ? $config['swiftProxyUser']
50 : '';
51 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
52 ? $config['shardViaHashLevels']
53 : '';
54 }
55
56 /**
57 * @see FileBackend::resolveContainerPath()
58 */
59 protected function resolveContainerPath( $container, $relStoragePath ) {
60 if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
61 return null; // too long for swift
62 }
63 return $relStoragePath;
64 }
65
66 /**
67 * @see FileBackend::doCopyInternal()
68 */
69 protected function doCreateInternal( array $params ) {
70 $status = Status::newGood();
71
72 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
73 if ( $destRel === null ) {
74 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
75 return $status;
76 }
77
78 // (a) Get a swift proxy connection
79 $conn = $this->getConnection();
80 if ( !$conn ) {
81 $status->fatal( 'backend-fail-connect', $this->name );
82 return $status;
83 }
84
85 // (b) Check the destination container
86 try {
87 $dContObj = $conn->get_container( $dstCont );
88 } catch ( NoSuchContainerException $e ) {
89 $status->fatal( 'backend-fail-create', $params['dst'] );
90 return $status;
91 } catch ( InvalidResponseException $e ) {
92 $status->fatal( 'backend-fail-connect', $this->name );
93 return $status;
94 } catch ( Exception $e ) { // some other exception?
95 $status->fatal( 'backend-fail-internal', $this->name );
96 $this->logException( $e, __METHOD__, $params );
97 return $status;
98 }
99
100 // (c) Check if the destination object already exists
101 try {
102 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
103 // NoSuchObjectException not thrown: file must exist
104 if ( empty( $params['overwriteDest'] ) ) {
105 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
106 return $status;
107 }
108 } catch ( NoSuchObjectException $e ) {
109 // NoSuchObjectException thrown: file does not exist
110 } catch ( InvalidResponseException $e ) {
111 $status->fatal( 'backend-fail-connect', $this->name );
112 return $status;
113 } catch ( Exception $e ) { // some other exception?
114 $status->fatal( 'backend-fail-internal', $this->name );
115 $this->logException( $e, __METHOD__, $params );
116 return $status;
117 }
118
119 // (d) Get a SHA-1 hash of the object
120 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
121
122 // (e) Actually create the object
123 try {
124 $obj = $dContObj->create_object( $destRel );
125 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
126 $obj->metadata = array( 'Sha1base36' => $sha1Hash );
127 $obj->write( $params['content'] );
128 } catch ( BadContentTypeException $e ) {
129 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
130 } catch ( InvalidResponseException $e ) {
131 $status->fatal( 'backend-fail-connect', $this->name );
132 } catch ( Exception $e ) { // some other exception?
133 $status->fatal( 'backend-fail-internal', $this->name );
134 $this->logException( $e, __METHOD__, $params );
135 }
136
137 return $status;
138 }
139
140 /**
141 * @see FileBackend::doStoreInternal()
142 */
143 protected function doStoreInternal( array $params ) {
144 $status = Status::newGood();
145
146 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
147 if ( $destRel === null ) {
148 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
149 return $status;
150 }
151
152 // (a) Get a swift proxy connection
153 $conn = $this->getConnection();
154 if ( !$conn ) {
155 $status->fatal( 'backend-fail-connect', $this->name );
156 return $status;
157 }
158
159 // (b) Check the destination container
160 try {
161 $dContObj = $conn->get_container( $dstCont );
162 } catch ( NoSuchContainerException $e ) {
163 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
164 return $status;
165 } catch ( InvalidResponseException $e ) {
166 $status->fatal( 'backend-fail-connect', $this->name );
167 return $status;
168 } catch ( Exception $e ) { // some other exception?
169 $status->fatal( 'backend-fail-internal', $this->name );
170 $this->logException( $e, __METHOD__, $params );
171 return $status;
172 }
173
174 // (c) Check if the destination object already exists
175 try {
176 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
177 // NoSuchObjectException not thrown: file must exist
178 if ( empty( $params['overwriteDest'] ) ) {
179 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
180 return $status;
181 }
182 } catch ( NoSuchObjectException $e ) {
183 // NoSuchObjectException thrown: file does not exist
184 } catch ( InvalidResponseException $e ) {
185 $status->fatal( 'backend-fail-connect', $this->name );
186 return $status;
187 } catch ( Exception $e ) { // some other exception?
188 $status->fatal( 'backend-fail-internal', $this->name );
189 $this->logException( $e, __METHOD__, $params );
190 return $status;
191 }
192
193 // (d) Get a SHA-1 hash of the object
194 $sha1Hash = sha1_file( $params['src'] );
195 if ( $sha1Hash === false ) { // source doesn't exist?
196 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
197 return $status;
198 }
199 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
200
201 // (e) Actually store the object
202 try {
203 $obj = $dContObj->create_object( $destRel );
204 // Note: metadata keys stored as [Upper case char][[Lower case char]...]
205 $obj->metadata = array( 'Sha1base36' => $sha1Hash );
206 $obj->load_from_filename( $params['src'], True ); // calls $obj->write()
207 } catch ( BadContentTypeException $e ) {
208 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
209 } catch ( IOException $e ) {
210 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
211 } catch ( InvalidResponseException $e ) {
212 $status->fatal( 'backend-fail-connect', $this->name );
213 } catch ( Exception $e ) { // some other exception?
214 $status->fatal( 'backend-fail-internal', $this->name );
215 $this->logException( $e, __METHOD__, $params );
216 }
217
218 return $status;
219 }
220
221 /**
222 * @see FileBackend::doCopyInternal()
223 */
224 protected function doCopyInternal( array $params ) {
225 $status = Status::newGood();
226
227 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
228 if ( $srcRel === null ) {
229 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
230 return $status;
231 }
232
233 list( $dstCont, $destRel ) = $this->resolveStoragePathReal( $params['dst'] );
234 if ( $destRel === null ) {
235 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
236 return $status;
237 }
238
239 // (a) Get a swift proxy connection
240 $conn = $this->getConnection();
241 if ( !$conn ) {
242 $status->fatal( 'backend-fail-connect', $this->name );
243 return $status;
244 }
245
246 // (b) Check the source and destination containers
247 try {
248 $sContObj = $conn->get_container( $srcCont );
249 $dContObj = $conn->get_container( $dstCont );
250 } catch ( NoSuchContainerException $e ) {
251 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
252 return $status;
253 } catch ( InvalidResponseException $e ) {
254 $status->fatal( 'backend-fail-connect', $this->name );
255 return $status;
256 } catch ( Exception $e ) { // some other exception?
257 $status->fatal( 'backend-fail-internal', $this->name );
258 $this->logException( $e, __METHOD__, $params );
259 return $status;
260 }
261
262 // (c) Check if the destination object already exists
263 try {
264 $dContObj->get_object( $destRel ); // throws NoSuchObjectException
265 // NoSuchObjectException not thrown: file must exist
266 if ( empty( $params['overwriteDest'] ) ) {
267 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
268 return $status;
269 }
270 } catch ( NoSuchObjectException $e ) {
271 // NoSuchObjectException thrown: file does not exist
272 } catch ( InvalidResponseException $e ) {
273 $status->fatal( 'backend-fail-connect', $this->name );
274 return $status;
275 } catch ( Exception $e ) { // some other exception?
276 $status->fatal( 'backend-fail-internal', $this->name );
277 $this->logException( $e, __METHOD__, $params );
278 return $status;
279 }
280
281 // (d) Actually copy the file to the destination
282 try {
283 $sContObj->copy_object_to( $srcRel, $dContObj, $destRel );
284 } catch ( NoSuchObjectException $e ) { // source object does not exist
285 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
286 } catch ( InvalidResponseException $e ) {
287 $status->fatal( 'backend-fail-connect', $this->name );
288 } catch ( Exception $e ) { // some other exception?
289 $status->fatal( 'backend-fail-internal', $this->name );
290 $this->logException( $e, __METHOD__, $params );
291 }
292
293 return $status;
294 }
295
296 /**
297 * @see FileBackend::doDeleteInternal()
298 */
299 protected function doDeleteInternal( array $params ) {
300 $status = Status::newGood();
301
302 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
303 if ( $srcRel === null ) {
304 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
305 return $status;
306 }
307
308 // (a) Get a swift proxy connection
309 $conn = $this->getConnection();
310 if ( !$conn ) {
311 $status->fatal( 'backend-fail-connect', $this->name );
312 return $status;
313 }
314
315 // (b) Check the source container
316 try {
317 $sContObj = $conn->get_container( $srcCont );
318 } catch ( NoSuchContainerException $e ) {
319 $status->fatal( 'backend-fail-delete', $params['src'] );
320 return $status;
321 } catch ( InvalidResponseException $e ) {
322 $status->fatal( 'backend-fail-connect', $this->name );
323 return $status;
324 } catch ( Exception $e ) { // some other exception?
325 $status->fatal( 'backend-fail-internal', $this->name );
326 $this->logException( $e, __METHOD__, $params );
327 return $status;
328 }
329
330 // (c) Actually delete the object
331 try {
332 $sContObj->delete_object( $srcRel );
333 } catch ( NoSuchObjectException $e ) {
334 if ( empty( $params['ignoreMissingSource'] ) ) {
335 $status->fatal( 'backend-fail-delete', $params['src'] );
336 }
337 } catch ( InvalidResponseException $e ) {
338 $status->fatal( 'backend-fail-connect', $this->name );
339 } catch ( Exception $e ) { // some other exception?
340 $status->fatal( 'backend-fail-internal', $this->name );
341 $this->logException( $e, __METHOD__, $params );
342 }
343
344 return $status;
345 }
346
347 /**
348 * @see FileBackend::doPrepareInternal()
349 */
350 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
351 $status = Status::newGood();
352
353 // (a) Get a swift proxy connection
354 $conn = $this->getConnection();
355 if ( !$conn ) {
356 $status->fatal( 'backend-fail-connect', $this->name );
357 return $status;
358 }
359
360 // (b) Create the destination container
361 try {
362 $conn->create_container( $fullCont );
363 } catch ( InvalidResponseException $e ) {
364 $status->fatal( 'backend-fail-connect', $this->name );
365 } catch ( Exception $e ) { // some other exception?
366 $status->fatal( 'backend-fail-internal', $this->name );
367 $this->logException( $e, __METHOD__, $params );
368 }
369
370 return $status;
371 }
372
373 /**
374 * @see FileBackend::doSecureInternal()
375 */
376 protected function doSecureInternal( $fullCont, $dir, array $params ) {
377 $status = Status::newGood();
378 // @TODO: restrict container from $this->swiftProxyUser
379 return $status;
380 }
381
382 /**
383 * @see FileBackend::doFileExists()
384 */
385 protected function doGetFileStat( array $params ) {
386 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
387 if ( $srcRel === null ) {
388 return false; // invalid storage path
389 }
390
391 $conn = $this->getConnection();
392 if ( !$conn ) {
393 return null;
394 }
395
396 $stat = false;
397 try {
398 $container = $conn->get_container( $srcCont );
399 // @TODO: handle 'latest' param as "X-Newest: true"
400 $obj = $container->get_object( $srcRel );
401 // Convert "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
402 $date = DateTime::createFromFormat( 'D, d F Y G:i:s e', $obj->last_modified );
403 if ( $date ) {
404 $stat = array(
405 'mtime' => $date->format( 'YmdHis' ),
406 'size' => $obj->content_length,
407 'sha1' => $obj->metadata['Sha1base36']
408 );
409 } else { // exception will be caught below
410 throw new Exception( "Could not parse date for object {$srcRel}" );
411 }
412 } catch ( NoSuchContainerException $e ) {
413 } catch ( NoSuchObjectException $e ) {
414 } catch ( InvalidResponseException $e ) {
415 $stat = null;
416 } catch ( Exception $e ) { // some other exception?
417 $stat = null;
418 $this->logException( $e, __METHOD__, $params );
419 }
420
421 return $stat;
422 }
423
424 /**
425 * @see FileBackendBase::getFileContents()
426 */
427 public function getFileContents( array $params ) {
428 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
429 if ( $srcRel === null ) {
430 return false; // invalid storage path
431 }
432
433 $conn = $this->getConnection();
434 if ( !$conn ) {
435 return false;
436 }
437
438 $data = false;
439 try {
440 $container = $conn->get_container( $srcCont );
441 $obj = $container->get_object( $srcRel );
442 $data = $obj->read( $this->headersFromParams( $params ) );
443 } catch ( NoSuchContainerException $e ) {
444 } catch ( NoSuchObjectException $e ) {
445 } catch ( InvalidResponseException $e ) {
446 } catch ( Exception $e ) { // some other exception?
447 $this->logException( $e, __METHOD__, $params );
448 }
449
450 return $data;
451 }
452
453 /**
454 * @see FileBackend::getFileListInternal()
455 */
456 public function getFileListInternal( $fullCont, $dir, array $params ) {
457 return new SwiftFileIterator( $this, $fullCont, $dir );
458 }
459
460 /**
461 * Do not call this function outside of SwiftFileIterator
462 *
463 * @param $fullCont string Resolved container name
464 * @param $dir string Resolved storage directory with no trailing slash
465 * @param $after string Storage path of file to list items after
466 * @param $limit integer Max number of items to list
467 * @return Array
468 */
469 public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
470 $conn = $this->getConnection();
471 if ( !$conn ) {
472 return null;
473 }
474
475 $files = array();
476 try {
477 $container = $conn->get_container( $fullCont );
478 $files = $container->list_objects( $limit, $after, "{$dir}/" );
479 } catch ( NoSuchContainerException $e ) {
480 } catch ( NoSuchObjectException $e ) {
481 } catch ( InvalidResponseException $e ) {
482 } catch ( Exception $e ) { // some other exception?
483 $this->logException( $e, __METHOD__, $params );
484 }
485
486 return $files;
487 }
488
489 /**
490 * @see FileBackend::doGetFileSha1base36()
491 */
492 public function doGetFileSha1base36( array $params ) {
493 $stat = $this->getFileStat( $params );
494 if ( $stat ) {
495 return $stat['sha1'];
496 } else {
497 return false;
498 }
499 }
500
501 /**
502 * @see FileBackend::doStreamFile()
503 */
504 protected function doStreamFile( array $params ) {
505 $status = Status::newGood();
506
507 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
508 if ( $srcRel === null ) {
509 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
510 }
511
512 $conn = $this->getConnection();
513 if ( !$conn ) {
514 $status->fatal( 'backend-fail-connect', $this->name );
515 }
516
517 try {
518 $cont = $conn->get_container( $srcCont );
519 $obj = $cont->get_object( $srcRel );
520 } catch ( NoSuchContainerException $e ) {
521 $status->fatal( 'backend-fail-stream', $params['src'] );
522 return $status;
523 } catch ( NoSuchObjectException $e ) {
524 $status->fatal( 'backend-fail-stream', $params['src'] );
525 return $status;
526 } catch ( IOException $e ) {
527 $status->fatal( 'backend-fail-stream', $params['src'] );
528 return $status;
529 } catch ( Exception $e ) { // some other exception?
530 $status->fatal( 'backend-fail-stream', $params['src'] );
531 $this->logException( $e, __METHOD__, $params );
532 return $status;
533 }
534
535 try {
536 $output = fopen( "php://output", "w" );
537 $obj->stream( $output, $this->headersFromParams( $params ) );
538 } catch ( InvalidResponseException $e ) {
539 $status->fatal( 'backend-fail-connect', $this->name );
540 } catch ( Exception $e ) { // some other exception?
541 $status->fatal( 'backend-fail-stream', $params['src'] );
542 $this->logException( $e, __METHOD__, $params );
543 }
544
545 return $status;
546 }
547
548 /**
549 * @see FileBackend::getLocalCopy()
550 */
551 public function getLocalCopy( array $params ) {
552 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
553 if ( $srcRel === null ) {
554 return null;
555 }
556
557 $conn = $this->getConnection();
558 if ( !$conn ) {
559 return null;
560 }
561
562 // Get source file extension
563 $ext = FileBackend::extensionFromPath( $srcRel );
564 // Create a new temporary file...
565 $tmpFile = TempFSFile::factory( wfBaseName( $srcRel ) . '_', $ext );
566 if ( !$tmpFile ) {
567 return null;
568 }
569
570 try {
571 $cont = $conn->get_container( $srcCont );
572 $obj = $cont->get_object( $srcRel );
573 $handle = fopen( $tmpFile->getPath(), 'w' );
574 if ( $handle ) {
575 $obj->stream( $handle, $this->headersFromParams( $params ) );
576 fclose( $handle );
577 } else {
578 $tmpFile = null; // couldn't open temp file
579 }
580 } catch ( NoSuchContainerException $e ) {
581 $tmpFile = null;
582 } catch ( NoSuchObjectException $e ) {
583 $tmpFile = null;
584 } catch ( InvalidResponseException $e ) {
585 $tmpFile = null;
586 } catch ( Exception $e ) { // some other exception?
587 $tmpFile = null;
588 $this->logException( $e, __METHOD__, $params );
589 }
590
591 return $tmpFile;
592 }
593
594 /**
595 * Get headers to send to Swift when reading a file based
596 * on a FileBackend params array, e.g. that of getLocalCopy().
597 * $params is currently only checked for a 'latest' flag.
598 *
599 * @param $params Array
600 * @return Array
601 */
602 protected function headersFromParams( array $params ) {
603 $hdrs = array();
604 if ( !empty( $params['latest'] ) ) {
605 $hdrs[] = 'X-Newest: true';
606 }
607 return $hdrs;
608 }
609
610 /**
611 * Get a connection to the swift proxy
612 *
613 * @return CF_Connection|false
614 */
615 protected function getConnection() {
616 if ( $this->conn === false ) {
617 return false; // failed last attempt
618 }
619 // Authenticate with proxy and get a session key.
620 // Session keys expire after a while, so we renew them periodically.
621 if ( $this->conn === null || ( time() - $this->connStarted ) > $this->connTTL ) {
622 try {
623 $this->auth->authenticate();
624 $this->conn = new CF_Connection( $this->auth );
625 $this->connStarted = time();
626 } catch ( AuthenticationException $e ) {
627 $this->conn = false; // don't keep re-trying
628 } catch ( InvalidResponseException $e ) {
629 $this->conn = false; // don't keep re-trying
630 }
631 }
632 return $this->conn;
633 }
634
635 /**
636 * Log an unexpected exception for this backend
637 *
638 * @param $e Exception
639 * @param $func string
640 * @param $params Array
641 * @return void
642 */
643 protected function logException( Exception $e, $func, array $params ) {
644 wfDebugLog( 'SwiftBackend',
645 get_class( $e ) . " in '{$this->name}': '{$func}' with " . serialize( $params )
646 );
647 }
648 }
649
650 /**
651 * SwiftFileBackend helper class to page through object listings.
652 * Swift also has a listing limit of 10,000 objects for sanity.
653 *
654 * @ingroup FileBackend
655 */
656 class SwiftFileIterator implements Iterator {
657 /** @var Array */
658 protected $bufferIter = array();
659 protected $bufferAfter = null; // string; list items *after* this path
660 protected $pos = 0; // integer
661
662 /** @var SwiftFileBackend */
663 protected $backend;
664 protected $container; //
665 protected $dir; // string storage directory
666 protected $suffixStart; // integer
667
668 const PAGE_SIZE = 5000; // file listing buffer size
669
670 /**
671 * Get an FSFileIterator from a file system directory
672 *
673 * @param $backend SwiftFileBackend
674 * @param $fullCont string Resolved container name
675 * @param $dir string Resolved relative directory
676 */
677 public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) {
678 $this->backend = $backend;
679 $this->container = $fullCont;
680 $this->dir = $dir;
681 if ( substr( $this->dir, -1 ) === '/' ) {
682 $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
683 }
684 $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
685 }
686
687 public function current() {
688 return substr( current( $this->bufferIter ), $this->suffixStart );
689 }
690
691 public function key() {
692 return $this->pos;
693 }
694
695 public function next() {
696 // Advance to the next file in the page
697 next( $this->bufferIter );
698 ++$this->pos;
699 // Check if there are no files left in this page and
700 // advance to the next page if this page was not empty.
701 if ( !$this->valid() && count( $this->bufferIter ) ) {
702 $this->bufferAfter = end( $this->bufferIter );
703 $this->bufferIter = $this->backend->getFileListPageInternal(
704 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
705 );
706 }
707 }
708
709 public function rewind() {
710 $this->pos = 0;
711 $this->bufferAfter = null;
712 $this->bufferIter = $this->backend->getFileListPageInternal(
713 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
714 );
715 }
716
717 public function valid() {
718 return ( current( $this->bufferIter ) !== false ); // no paths can have this value
719 }
720 }