Index: branches/FileBackend/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php |
— | — | @@ -1,7 +1,8 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | 4 | class FileBackendTest extends MediaWikiTestCase { |
5 | | - private $backend, $filesToPrune, $pathsToPrune; |
| 5 | + private $backend, $multiBackend; |
| 6 | + private $filesToPrune, $pathsToPrune; |
6 | 7 | |
7 | 8 | function setUp() { |
8 | 9 | parent::setUp(); |
— | — | @@ -12,11 +13,34 @@ |
13 | 14 | 'cont1' => wfTempDir() . '/localtesting/cont1', |
14 | 15 | 'cont2' => wfTempDir() . '/localtesting/cont2' ) |
15 | 16 | ) ); |
16 | | - $this->filesToPrune = array(); |
17 | | - $this->pathsToPrune = array(); |
| 17 | + $this->multiBackend = new FileBackendMultiWrite( array( |
| 18 | + 'name' => 'localtestingmulti', |
| 19 | + 'lockManager' => 'fsLockManager', |
| 20 | + 'backends' => array( |
| 21 | + array( |
| 22 | + 'name' => 'localmutlitesting1', |
| 23 | + 'class' => 'FSFileBackend', |
| 24 | + 'lockManager' => 'nullLockManager', |
| 25 | + 'containerPaths' => array( |
| 26 | + 'cont1' => wfTempDir() . '/localtestingmulti1/cont1', |
| 27 | + 'cont2' => wfTempDir() . '/localtestingmulti1/cont2' ), |
| 28 | + 'isMultiMaster' => false |
| 29 | + ), |
| 30 | + array( |
| 31 | + 'name' => 'localmutlitesting2', |
| 32 | + 'class' => 'FSFileBackend', |
| 33 | + 'lockManager' => 'nullLockManager', |
| 34 | + 'containerPaths' => array( |
| 35 | + 'cont1' => wfTempDir() . '/localtestingmulti2/cont1', |
| 36 | + 'cont2' => wfTempDir() . '/localtestingmulti2/cont2' ), |
| 37 | + 'isMultiMaster' => true |
| 38 | + ) |
| 39 | + ) |
| 40 | + ) ); |
| 41 | + $this->filesToPrune = $this->pathsToPrune = array(); |
18 | 42 | } |
19 | 43 | |
20 | | - private function basePath() { |
| 44 | + private function singleBasePath() { |
21 | 45 | return 'mwstore://localtesting'; |
22 | 46 | } |
23 | 47 | |
— | — | @@ -49,7 +73,7 @@ |
50 | 74 | $cases = array(); |
51 | 75 | |
52 | 76 | $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath(); |
53 | | - $toPath = $this->basePath() . '/cont1/fun/obj1.txt'; |
| 77 | + $toPath = $this->singleBasePath() . '/cont1/fun/obj1.txt'; |
54 | 78 | $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ); |
55 | 79 | $cases[] = array( |
56 | 80 | $op, // operation |
— | — | @@ -70,36 +94,241 @@ |
71 | 95 | /** |
72 | 96 | * @dataProvider provider_testCopy |
73 | 97 | */ |
74 | | - public function testCopy() { |
75 | | - |
| 98 | + public function testCopy( $op, $source, $dest ) { |
| 99 | + $this->pathsToPrune[] = $source; |
| 100 | + $this->pathsToPrune[] = $dest; |
| 101 | + |
| 102 | + $status = $this->backend->doOperation( |
| 103 | + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); |
| 104 | + $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." ); |
| 105 | + |
| 106 | + $status = $this->backend->doOperation( $op ); |
| 107 | + $this->assertEquals( true, $status->isOK(), |
| 108 | + "Copy from $source to $dest succeeded." ); |
| 109 | + $this->assertEquals( true, $status->isGood(), |
| 110 | + "Copy from $source to $dest succeeded without warnings." ); |
| 111 | + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ), |
| 112 | + "Source file $source still exists." ); |
| 113 | + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ), |
| 114 | + "Destination file $dest exists after copy." ); |
| 115 | + |
| 116 | + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); |
| 117 | + $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); |
| 118 | + $this->assertEquals( $props1, $props2, |
| 119 | + "Source and destination have the same props." ); |
76 | 120 | } |
77 | 121 | |
| 122 | + public function provider_testCopy() { |
| 123 | + $cases = array(); |
| 124 | + |
| 125 | + $source = $this->singleBasePath() . '/cont1/file.txt'; |
| 126 | + $dest = $this->singleBasePath() . '/cont2/fileMoved.txt'; |
| 127 | + |
| 128 | + $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest ); |
| 129 | + $cases[] = array( |
| 130 | + $op, // operation |
| 131 | + $source, // source |
| 132 | + $dest, // dest |
| 133 | + ); |
| 134 | + |
| 135 | + $op['overwriteDest'] = true; |
| 136 | + $cases[] = array( |
| 137 | + $op, // operation |
| 138 | + $source, // source |
| 139 | + $dest, // dest |
| 140 | + ); |
| 141 | + |
| 142 | + return $cases; |
| 143 | + } |
| 144 | + |
78 | 145 | /** |
79 | 146 | * @dataProvider provider_testMove |
80 | 147 | */ |
81 | | - public function testMove() { |
82 | | - |
| 148 | + public function testMove( $op, $source, $dest ) { |
| 149 | + $this->pathsToPrune[] = $source; |
| 150 | + $this->pathsToPrune[] = $dest; |
| 151 | + |
| 152 | + $status = $this->backend->doOperation( |
| 153 | + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); |
| 154 | + $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." ); |
| 155 | + |
| 156 | + $status = $this->backend->doOperation( $op ); |
| 157 | + $this->assertEquals( true, $status->isOK(), |
| 158 | + "Move from $source to $dest succeeded." ); |
| 159 | + $this->assertEquals( true, $status->isGood(), |
| 160 | + "Move from $source to $dest succeeded without warnings." ); |
| 161 | + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ), |
| 162 | + "Source file $source does not still exists." ); |
| 163 | + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ), |
| 164 | + "Destination file $dest exists after move." ); |
| 165 | + |
| 166 | + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); |
| 167 | + $props2 = $this->backend->getFileProps( array( 'src' => $dest ) ); |
| 168 | + $this->assertEquals( false, $props1['fileExists'], |
| 169 | + "Source file does not exist accourding to props." ); |
| 170 | + $this->assertEquals( true, $props2['fileExists'], |
| 171 | + "Destination file exists accourding to props." ); |
83 | 172 | } |
84 | 173 | |
| 174 | + public function provider_testMove() { |
| 175 | + $cases = array(); |
| 176 | + |
| 177 | + $source = $this->singleBasePath() . '/cont1/file.txt'; |
| 178 | + $dest = $this->singleBasePath() . '/cont2/fileMoved.txt'; |
| 179 | + |
| 180 | + $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest ); |
| 181 | + $cases[] = array( |
| 182 | + $op, // operation |
| 183 | + $source, // source |
| 184 | + $dest, // dest |
| 185 | + ); |
| 186 | + |
| 187 | + $op['overwriteDest'] = true; |
| 188 | + $cases[] = array( |
| 189 | + $op, // operation |
| 190 | + $source, // source |
| 191 | + $dest, // dest |
| 192 | + ); |
| 193 | + |
| 194 | + return $cases; |
| 195 | + } |
| 196 | + |
85 | 197 | /** |
86 | 198 | * @dataProvider provider_testDelete |
87 | 199 | */ |
88 | | - public function testDelete() { |
89 | | - |
| 200 | + public function testDelete( $op, $source, $withSource, $okStatus ) { |
| 201 | + $this->pathsToPrune[] = $source; |
| 202 | + |
| 203 | + if ( $withSource ) { |
| 204 | + $status = $this->backend->doOperation( |
| 205 | + array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) ); |
| 206 | + $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." ); |
| 207 | + } |
| 208 | + |
| 209 | + $status = $this->backend->doOperation( $op ); |
| 210 | + if ( $okStatus ) { |
| 211 | + $this->assertEquals( true, $status->isOK(), "Deletion of file at $source succeeded." ); |
| 212 | + } else { |
| 213 | + $this->assertEquals( false, $status->isOK(), "Deletion of file at $source failed." ); |
| 214 | + } |
| 215 | + |
| 216 | + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ), |
| 217 | + "Source file $source does not exist after move." ); |
| 218 | + |
| 219 | + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); |
| 220 | + $this->assertEquals( false, $props1['fileExists'], |
| 221 | + "Source file $source does not exist according to props." ); |
90 | 222 | } |
91 | 223 | |
| 224 | + public function provider_testDelete() { |
| 225 | + $cases = array(); |
| 226 | + |
| 227 | + $source = $this->singleBasePath() . '/cont1/myfacefile.txt'; |
| 228 | + |
| 229 | + $op = array( 'op' => 'delete', 'src' => $source ); |
| 230 | + $cases[] = array( |
| 231 | + $op, // operation |
| 232 | + $source, // source |
| 233 | + true, // with source |
| 234 | + true // succeeds |
| 235 | + ); |
| 236 | + |
| 237 | + $cases[] = array( |
| 238 | + $op, // operation |
| 239 | + $source, // source |
| 240 | + false, // without source |
| 241 | + false // fails |
| 242 | + ); |
| 243 | + |
| 244 | + $op['ignoreMissingSource'] = true; |
| 245 | + $cases[] = array( |
| 246 | + $op, // operation |
| 247 | + $source, // source |
| 248 | + false, // without source |
| 249 | + true // succeeds |
| 250 | + ); |
| 251 | + |
| 252 | + return $cases; |
| 253 | + } |
| 254 | + |
92 | 255 | /** |
93 | 256 | * @dataProvider provider_testCreate |
94 | 257 | */ |
95 | | - public function testCreate() { |
96 | | - |
| 258 | + public function testCreate( $op, $source, $alreadyExists, $okStatus, $newSize ) { |
| 259 | + $this->pathsToPrune[] = $source; |
| 260 | + |
| 261 | + $oldText = 'blah...blah...waahwaah'; |
| 262 | + if ( $alreadyExists ) { |
| 263 | + $status = $this->backend->doOperation( |
| 264 | + array( 'op' => 'create', 'content' => $oldText, 'dst' => $source ) ); |
| 265 | + $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." ); |
| 266 | + } |
| 267 | + |
| 268 | + $status = $this->backend->doOperation( $op ); |
| 269 | + if ( $okStatus ) { |
| 270 | + $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." ); |
| 271 | + } else { |
| 272 | + $this->assertEquals( false, $status->isOK(), "Creation of file at $source failed." ); |
| 273 | + } |
| 274 | + |
| 275 | + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ), |
| 276 | + "Source file $source exists after creation." ); |
| 277 | + |
| 278 | + $props1 = $this->backend->getFileProps( array( 'src' => $source ) ); |
| 279 | + $this->assertEquals( true, $props1['fileExists'], |
| 280 | + "Source file $source exists according to props." ); |
| 281 | + if ( $okStatus ) { // file content is what we saved |
| 282 | + $this->assertEquals( $newSize, $props1['size'], |
| 283 | + "Source file $source has expected size according to props." ); |
| 284 | + } else { // file content is some other previous text |
| 285 | + $this->assertEquals( strlen( $oldText ), $props1['size'], |
| 286 | + "Source file $source has different size that given text according to props." ); |
| 287 | + } |
97 | 288 | } |
98 | 289 | |
99 | 290 | /** |
100 | 291 | * @dataProvider provider_testConcatenate |
101 | 292 | */ |
| 293 | + public function provider_testCreate() { |
| 294 | + $cases = array(); |
| 295 | + |
| 296 | + $source = $this->singleBasePath() . '/cont2/myspacefile.txt'; |
| 297 | + |
| 298 | + $dummyText = 'hey hey'; |
| 299 | + $op = array( 'op' => 'create', 'content' => $dummyText, 'dst' => $source ); |
| 300 | + $cases[] = array( |
| 301 | + $op, // operation |
| 302 | + $source, // source |
| 303 | + false, // no dest already exists |
| 304 | + true, // succeeds |
| 305 | + strlen( $dummyText ) |
| 306 | + ); |
| 307 | + |
| 308 | + $cases[] = array( |
| 309 | + $op, // operation |
| 310 | + $source, // source |
| 311 | + true, // dest already exists |
| 312 | + false, // fails |
| 313 | + strlen( $dummyText ) |
| 314 | + ); |
| 315 | + |
| 316 | + $op['overwriteDest'] = true; |
| 317 | + $cases[] = array( |
| 318 | + $op, // operation |
| 319 | + $source, // source |
| 320 | + true, // dest already exists |
| 321 | + true, // succeeds |
| 322 | + strlen( $dummyText ) |
| 323 | + ); |
| 324 | + |
| 325 | + return $cases; |
| 326 | + } |
| 327 | + |
| 328 | + /** |
| 329 | + * @dataProvider provider_testConcatenate |
| 330 | + */ |
102 | 331 | public function testConcatenate() { |
103 | | - |
| 332 | + |
104 | 333 | } |
105 | 334 | |
106 | 335 | /** |
— | — | @@ -130,16 +359,87 @@ |
131 | 360 | |
132 | 361 | } |
133 | 362 | |
| 363 | + /** |
| 364 | + * @dataProvider provider_testDoOperations |
| 365 | + */ |
| 366 | + public function testDoOperations() { |
| 367 | + |
| 368 | + } |
| 369 | + |
| 370 | + public function testGetFileList() { |
| 371 | + $base = $this->singleBasePath(); |
| 372 | + $files = array( |
| 373 | + "$base/cont1/test1.txt", |
| 374 | + "$base/cont1/test2.txt", |
| 375 | + "$base/cont1/test3.txt", |
| 376 | + "$base/cont1/subdir1/test1.txt", |
| 377 | + "$base/cont1/subdir1/test2.txt", |
| 378 | + "$base/cont1/subdir2/test3.txt", |
| 379 | + "$base/cont1/subdir2/test4.txt", |
| 380 | + "$base/cont1/subdir2/subdir/test1.txt", |
| 381 | + "$base/cont1/subdir2/subdir/test2.txt", |
| 382 | + "$base/cont1/subdir2/subdir/test3.txt", |
| 383 | + "$base/cont1/subdir2/subdir/test4.txt", |
| 384 | + "$base/cont1/subdir2/subdir/test5.txt", |
| 385 | + "$base/cont1/subdir2/subdir/sub/test0.txt", |
| 386 | + ); |
| 387 | + $this->pathsToPrune = array_merge( $this->pathsToPrune, $files ); |
| 388 | + |
| 389 | + // Add the files |
| 390 | + $ops = array(); |
| 391 | + foreach ( $files as $file ) { |
| 392 | + $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file ); |
| 393 | + } |
| 394 | + $status = $this->backend->doOperations( $ops ); |
| 395 | + $this->assertEquals( true, $status->isOK(), "Creation of files succeeded." ); |
| 396 | + |
| 397 | + // Expected listing |
| 398 | + $expected = array( |
| 399 | + "test1.txt", |
| 400 | + "test2.txt", |
| 401 | + "test3.txt", |
| 402 | + "subdir1/test1.txt", |
| 403 | + "subdir1/test2.txt", |
| 404 | + "subdir2/test3.txt", |
| 405 | + "subdir2/test1.txt", |
| 406 | + "subdir2/subdir/test1.txt", |
| 407 | + "subdir2/subdir/test2.txt", |
| 408 | + "subdir2/subdir/test3.txt", |
| 409 | + "subdir2/subdir/test4.txt", |
| 410 | + "subdir2/subdir/test5.txt", |
| 411 | + "subdir2/subdir/sub/test0.txt", |
| 412 | + ); |
| 413 | + $expected = sort( $expected ); |
| 414 | + |
| 415 | + // Actual listing (no trailing slash) |
| 416 | + $list = array(); |
| 417 | + $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1" ) ); |
| 418 | + foreach ( $iter as $file ) { |
| 419 | + $list[] = $file; |
| 420 | + } |
| 421 | + |
| 422 | + $this->assertEquals( $expected, sort( $list ), "Correct file listing." ); |
| 423 | + |
| 424 | + // Actual listing (with trailing slash) |
| 425 | + $list = array(); |
| 426 | + $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1/" ) ); |
| 427 | + foreach ( $iter as $file ) { |
| 428 | + $list[] = $file; |
| 429 | + } |
| 430 | + |
| 431 | + $this->assertEquals( $expected, sort( $list ), "Correct file listing." ); |
| 432 | + } |
| 433 | + |
134 | 434 | function tearDown() { |
135 | 435 | parent::tearDown(); |
136 | 436 | foreach ( $this->filesToPrune as $file ) { |
137 | 437 | @unlink( $file ); |
138 | 438 | } |
139 | 439 | foreach ( $this->pathsToPrune as $file ) { |
140 | | - $this->backend->delete( array( 'src' => $file ) ); |
| 440 | + $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); |
| 441 | + $this->multiBackend->doOperation( array( 'op' => 'delete', 'src' => $file ) ); |
141 | 442 | } |
142 | | - $this->backend = null; |
143 | | - $this->filesToPrune = array(); |
144 | | - $this->pathsToPrune = array(); |
| 443 | + $this->backend = $this->multiBackend = null; |
| 444 | + $this->filesToPrune = $this->pathsToPrune = array(); |
145 | 445 | } |
146 | 446 | } |
Index: branches/FileBackend/phase3/tests/phpunit/includes/parser/NewParserTest.php |
— | — | @@ -317,7 +317,7 @@ |
318 | 318 | $backend = array( |
319 | 319 | 'name' => 'local-backend', |
320 | 320 | 'class' => 'FSFileBackend', |
321 | | - 'lockManager' => 'fsLockManager', |
| 321 | + 'lockManager' => 'nullLockManager', |
322 | 322 | 'containerPaths' => array( |
323 | 323 | 'images-public' => $this->uploadDir, |
324 | 324 | 'images-thumb' => $this->uploadDir . '/thumb' ) |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php |
— | — | @@ -188,7 +188,7 @@ |
189 | 189 | } |
190 | 190 | |
191 | 191 | function fileExists( array $params ) { |
192 | | - # Hit all backends in case an operation failed to copy/move/delete a file |
| 192 | + # Hit all backends in case of failed operations (out of sync) |
193 | 193 | foreach ( $this->backends as $backend ) { |
194 | 194 | if ( $backend->fileExists( $params ) ) { |
195 | 195 | return true; |
— | — | @@ -213,6 +213,7 @@ |
214 | 214 | } |
215 | 215 | |
216 | 216 | function getFileProps( array $params ) { |
| 217 | + # Hit all backends in case of failed operations (out of sync) |
217 | 218 | foreach ( $this->backends as $backend ) { |
218 | 219 | $props = $backend->getFileProps( $params ); |
219 | 220 | if ( $props !== null ) { |
— | — | @@ -223,18 +224,23 @@ |
224 | 225 | } |
225 | 226 | |
226 | 227 | function streamFile( array $params ) { |
| 228 | + $status = Status::newGood(); |
227 | 229 | foreach ( $this->backends as $backend ) { |
228 | | - $status = $backend->streamFile( $params ); |
229 | | - if ( $status->isOK() ) { |
| 230 | + $subStatus = $backend->streamFile( $params ); |
| 231 | + $status->merge( $subStatus ); |
| 232 | + if ( $subStatus->isOK() ) { |
| 233 | + // Pass isOK() despite fatals from other backends |
| 234 | + $status->setResult( true ); |
230 | 235 | return $status; |
231 | 236 | } elseif ( headers_sent() ) { |
232 | 237 | return $status; // died mid-stream...so this is already fubar |
233 | 238 | } |
234 | 239 | } |
235 | | - return Status::newFatal( 'backend-fail-stream', $params['src'] ); |
| 240 | + return $status; |
236 | 241 | } |
237 | 242 | |
238 | 243 | function getLocalReference( array $params ) { |
| 244 | + # Hit all backends in case of failed operations (out of sync) |
239 | 245 | foreach ( $this->backends as $backend ) { |
240 | 246 | $fsFile = $backend->getLocalReference( $params ); |
241 | 247 | if ( $fsFile ) { |
— | — | @@ -245,6 +251,7 @@ |
246 | 252 | } |
247 | 253 | |
248 | 254 | function getLocalCopy( array $params ) { |
| 255 | + # Hit all backends in case of failed operations (out of sync) |
249 | 256 | foreach ( $this->backends as $backend ) { |
250 | 257 | $tmpFile = $backend->getLocalCopy( $params ); |
251 | 258 | if ( $tmpFile ) { |
— | — | @@ -256,6 +263,7 @@ |
257 | 264 | |
258 | 265 | function getFileList( array $params ) { |
259 | 266 | foreach ( $this->backends as $index => $backend ) { |
| 267 | + # Get results from the first backend |
260 | 268 | return $backend->getFileList( $params ); |
261 | 269 | } |
262 | 270 | return array(); // sanity |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php |
— | — | @@ -453,7 +453,7 @@ |
454 | 454 | if ( $dir === null ) { // invalid storage path |
455 | 455 | return array(); // empty result |
456 | 456 | } |
457 | | - return new FSFileIterator( $dir ); |
| 457 | + return new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) ); |
458 | 458 | } |
459 | 459 | |
460 | 460 | function getLocalReference( array $params ) { |
— | — | @@ -507,151 +507,3 @@ |
508 | 508 | return $ok; |
509 | 509 | } |
510 | 510 | } |
511 | | - |
512 | | -/** |
513 | | - * Semi-DFS based file browsing iterator. The highest number of file handles |
514 | | - * open at any given time is proportional to the height of the directory tree. |
515 | | - */ |
516 | | -class FSFileIterator implements Iterator { |
517 | | - private $directory; // starting directory |
518 | | - private $itemStart = 0; |
519 | | - |
520 | | - private $position = 0; |
521 | | - private $currentFile = false; |
522 | | - private $dirStack = array(); // array of (dir name, dir handle) tuples |
523 | | - |
524 | | - private $loaded = false; |
525 | | - |
526 | | - /** |
527 | | - * Get an iterator to list the files under $directory and its subdirectories |
528 | | - * @param $directory string |
529 | | - */ |
530 | | - public function __construct( $directory ) { |
531 | | - // Removing any trailing slash |
532 | | - if ( substr( $this->directory, -1 ) === '/' ) { |
533 | | - $this->directory = substr( $this->directory, 0, -1 ); |
534 | | - } |
535 | | - $this->directory = realpath( $directory ); |
536 | | - // Remove internal base directory and trailing slash from results |
537 | | - $this->itemStart = strlen( $this->directory ) + 1; |
538 | | - } |
539 | | - |
540 | | - private function load() { |
541 | | - if ( !$this->loaded ) { |
542 | | - $this->loaded = true; |
543 | | - // If we get an invalid directory then the result is an empty list |
544 | | - if ( is_dir( $this->directory ) ) { |
545 | | - $handle = opendir( $this->directory ); |
546 | | - if ( $handle ) { |
547 | | - $this->pushDirectory( $this->directory, $handle ); |
548 | | - $this->currentFile = $this->nextFile(); |
549 | | - } |
550 | | - } |
551 | | - } |
552 | | - } |
553 | | - |
554 | | - function rewind() { |
555 | | - $this->closeDirectories(); |
556 | | - $this->position = 0; |
557 | | - $this->currentFile = false; |
558 | | - $this->loaded = false; |
559 | | - } |
560 | | - |
561 | | - function current() { |
562 | | - $this->load(); |
563 | | - // Remove internal base directory and trailing slash from results |
564 | | - return substr( $this->currentFile, $this->itemStart ); |
565 | | - } |
566 | | - |
567 | | - function key() { |
568 | | - $this->load(); |
569 | | - return $this->position; |
570 | | - } |
571 | | - |
572 | | - function next() { |
573 | | - $this->load(); |
574 | | - ++$this->position; |
575 | | - $this->currentFile = $this->nextFile(); |
576 | | - } |
577 | | - |
578 | | - function valid() { |
579 | | - $this->load(); |
580 | | - return ( $this->currentFile !== false ); |
581 | | - } |
582 | | - |
583 | | - function nextFile() { |
584 | | - if ( !$this->currentDirectory() ) { |
585 | | - return false; // nothing else to scan |
586 | | - } |
587 | | - # Next file under the current directory (and subdirectories). |
588 | | - # This may advance the current directory down to a descendent. |
589 | | - # The current directory is set to the parent if nothing is found. |
590 | | - $nextFile = $this->nextFileBelowCurrent(); |
591 | | - if ( $nextFile !== false ) { |
592 | | - return $nextFile; |
593 | | - } else { |
594 | | - # Scan the higher directories |
595 | | - return $this->nextFile(); |
596 | | - } |
597 | | - } |
598 | | - |
599 | | - function nextFileBelowCurrent() { |
600 | | - list( $dir, $handle ) = $this->currentDirectory(); |
601 | | - while ( false !== ( $file = readdir( $handle ) ) ) { |
602 | | - // Exclude '.' and '..' as well .svn or .lock type files |
603 | | - if ( $file[0] !== '.' ) { |
604 | | - $path = "{$dir}/{$file}"; |
605 | | - // If the first thing we find is a file, then return it. |
606 | | - // If the first thing we find is a directory, then return |
607 | | - // the first file that it contains (via recursion). |
608 | | - // We exclude symlink dirs in order to avoid cycles. |
609 | | - if ( is_dir( $path ) && !is_link( $path ) ) { |
610 | | - $subHandle = opendir( $path ); |
611 | | - if ( $subHandle ) { |
612 | | - $this->pushDirectory( $path, $subHandle ); |
613 | | - $nextFile = $this->nextFileBelowCurrent(); |
614 | | - if ( $nextFile !== false ) { |
615 | | - return $nextFile; // found the next one! |
616 | | - } |
617 | | - } |
618 | | - } elseif ( is_file( $path ) ) { |
619 | | - return $path; // found the next one! |
620 | | - } |
621 | | - } |
622 | | - } |
623 | | - # If we didn't find anything else in this directory, |
624 | | - # then back out so we scan the other higher directories |
625 | | - $this->popDirectory(); |
626 | | - return false; |
627 | | - } |
628 | | - |
629 | | - private function currentDirectory() { |
630 | | - if ( !count( $this->dirStack ) ) { |
631 | | - return false; |
632 | | - } |
633 | | - return $this->dirStack[count( $this->dirStack ) - 1]; |
634 | | - } |
635 | | - |
636 | | - private function popDirectory() { |
637 | | - if ( count( $this->dirStack ) ) { |
638 | | - list( $dir, $handle ) = array_pop( $this->dirStack ); |
639 | | - closedir( $handle ); |
640 | | - } |
641 | | - } |
642 | | - |
643 | | - private function pushDirectory( $dir, $handle ) { |
644 | | - $this->dirStack[] = array( $dir, $handle ); |
645 | | - } |
646 | | - |
647 | | - private function closeDirectories() { |
648 | | - foreach ( $this->dirStack as $set ) { |
649 | | - list( $dir, $handle ) = $set; |
650 | | - closedir( $handle ); |
651 | | - } |
652 | | - $this->dirStack = array(); |
653 | | - } |
654 | | - |
655 | | - function __destruct() { |
656 | | - $this->closeDirectories(); |
657 | | - } |
658 | | -} |