Index: trunk/phase3/includes/filerepo/backend/SwiftFileBackend.php |
— | — | @@ -7,26 +7,29 @@ |
8 | 8 | */ |
9 | 9 | |
10 | 10 | /** |
11 | | - * Class for a Swift based file backend. |
12 | | - * Status messages should avoid mentioning the Swift account name |
13 | | - * Likewise, error suppression should be used to avoid path disclosure. |
| 11 | + * Class for an OpenStack Swift based file backend. |
14 | 12 | * |
15 | 13 | * This requires that the php-cloudfiles library is present, |
16 | 14 | * which is available at https://github.com/rackspace/php-cloudfiles. |
17 | 15 | * All of the library classes must be registed in $wgAutoloadClasses. |
18 | 16 | * |
| 17 | + * Status messages should avoid mentioning the Swift account name |
| 18 | + * Likewise, error suppression should be used to avoid path disclosure. |
| 19 | + * |
19 | 20 | * @ingroup FileBackend |
20 | 21 | * @since 1.19 |
21 | 22 | */ |
22 | 23 | class SwiftFileBackend extends FileBackend { |
23 | 24 | /** @var CF_Authentication */ |
24 | | - protected $auth; // swift authentication handler |
| 25 | + protected $auth; // Swift authentication handler |
| 26 | + |
25 | 27 | /** @var CF_Connection */ |
26 | | - protected $conn; // swift connection handle |
| 28 | + protected $conn; // Swift connection handle |
27 | 29 | protected $connStarted = 0; // integer UNIX timestamp |
| 30 | + protected $connContainers = array(); // container object cache |
| 31 | + protected $connTTL = 120; // integer seconds |
28 | 32 | |
29 | 33 | protected $swiftProxyUser; // string |
30 | | - protected $connTTL = 60; // integer seconds |
31 | 34 | |
32 | 35 | /** |
33 | 36 | * @see FileBackend::__construct() |
— | — | @@ -59,7 +62,7 @@ |
60 | 63 | */ |
61 | 64 | protected function resolveContainerPath( $container, $relStoragePath ) { |
62 | 65 | if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) { |
63 | | - return null; // too long for swift |
| 66 | + return null; // too long for Swift |
64 | 67 | } |
65 | 68 | return $relStoragePath; |
66 | 69 | } |
— | — | @@ -76,16 +79,9 @@ |
77 | 80 | return $status; |
78 | 81 | } |
79 | 82 | |
80 | | - // (a) Get a swift proxy connection |
81 | | - $conn = $this->getConnection(); |
82 | | - if ( !$conn ) { |
83 | | - $status->fatal( 'backend-fail-connect', $this->name ); |
84 | | - return $status; |
85 | | - } |
86 | | - |
87 | | - // (b) Check the destination container |
| 83 | + // (a) Check the destination container |
88 | 84 | try { |
89 | | - $dContObj = $conn->get_container( $dstCont ); |
| 85 | + $dContObj = $this->getContainer( $dstCont ); |
90 | 86 | } catch ( NoSuchContainerException $e ) { |
91 | 87 | $status->fatal( 'backend-fail-create', $params['dst'] ); |
92 | 88 | return $status; |
— | — | @@ -98,7 +94,7 @@ |
99 | 95 | return $status; |
100 | 96 | } |
101 | 97 | |
102 | | - // (c) Check if the destination object already exists |
| 98 | + // (b) Check if the destination object already exists |
103 | 99 | try { |
104 | 100 | $dContObj->get_object( $destRel ); // throws NoSuchObjectException |
105 | 101 | // NoSuchObjectException not thrown: file must exist |
— | — | @@ -117,10 +113,10 @@ |
118 | 114 | return $status; |
119 | 115 | } |
120 | 116 | |
121 | | - // (d) Get a SHA-1 hash of the object |
| 117 | + // (c) Get a SHA-1 hash of the object |
122 | 118 | $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 ); |
123 | 119 | |
124 | | - // (e) Actually create the object |
| 120 | + // (d) Actually create the object |
125 | 121 | try { |
126 | 122 | $obj = $dContObj->create_object( $destRel ); |
127 | 123 | // Note: metadata keys stored as [Upper case char][[Lower case char]...] |
— | — | @@ -150,16 +146,9 @@ |
151 | 147 | return $status; |
152 | 148 | } |
153 | 149 | |
154 | | - // (a) Get a swift proxy connection |
155 | | - $conn = $this->getConnection(); |
156 | | - if ( !$conn ) { |
157 | | - $status->fatal( 'backend-fail-connect', $this->name ); |
158 | | - return $status; |
159 | | - } |
160 | | - |
161 | | - // (b) Check the destination container |
| 150 | + // (a) Check the destination container |
162 | 151 | try { |
163 | | - $dContObj = $conn->get_container( $dstCont ); |
| 152 | + $dContObj = $this->getContainer( $dstCont ); |
164 | 153 | } catch ( NoSuchContainerException $e ) { |
165 | 154 | $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); |
166 | 155 | return $status; |
— | — | @@ -172,7 +161,7 @@ |
173 | 162 | return $status; |
174 | 163 | } |
175 | 164 | |
176 | | - // (c) Check if the destination object already exists |
| 165 | + // (b) Check if the destination object already exists |
177 | 166 | try { |
178 | 167 | $dContObj->get_object( $destRel ); // throws NoSuchObjectException |
179 | 168 | // NoSuchObjectException not thrown: file must exist |
— | — | @@ -191,7 +180,7 @@ |
192 | 181 | return $status; |
193 | 182 | } |
194 | 183 | |
195 | | - // (d) Get a SHA-1 hash of the object |
| 184 | + // (c) Get a SHA-1 hash of the object |
196 | 185 | $sha1Hash = sha1_file( $params['src'] ); |
197 | 186 | if ( $sha1Hash === false ) { // source doesn't exist? |
198 | 187 | $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); |
— | — | @@ -199,7 +188,7 @@ |
200 | 189 | } |
201 | 190 | $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 ); |
202 | 191 | |
203 | | - // (e) Actually store the object |
| 192 | + // (d) Actually store the object |
204 | 193 | try { |
205 | 194 | $obj = $dContObj->create_object( $destRel ); |
206 | 195 | // Note: metadata keys stored as [Upper case char][[Lower case char]...] |
— | — | @@ -237,17 +226,10 @@ |
238 | 227 | return $status; |
239 | 228 | } |
240 | 229 | |
241 | | - // (a) Get a swift proxy connection |
242 | | - $conn = $this->getConnection(); |
243 | | - if ( !$conn ) { |
244 | | - $status->fatal( 'backend-fail-connect', $this->name ); |
245 | | - return $status; |
246 | | - } |
247 | | - |
248 | | - // (b) Check the source and destination containers |
| 230 | + // (a) Check the source and destination containers |
249 | 231 | try { |
250 | | - $sContObj = $conn->get_container( $srcCont ); |
251 | | - $dContObj = $conn->get_container( $dstCont ); |
| 232 | + $sContObj = $this->getContainer( $srcCont ); |
| 233 | + $dContObj = $this->getContainer( $dstCont ); |
252 | 234 | } catch ( NoSuchContainerException $e ) { |
253 | 235 | $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); |
254 | 236 | return $status; |
— | — | @@ -260,7 +242,7 @@ |
261 | 243 | return $status; |
262 | 244 | } |
263 | 245 | |
264 | | - // (c) Check if the destination object already exists |
| 246 | + // (b) Check if the destination object already exists |
265 | 247 | try { |
266 | 248 | $dContObj->get_object( $destRel ); // throws NoSuchObjectException |
267 | 249 | // NoSuchObjectException not thrown: file must exist |
— | — | @@ -279,7 +261,7 @@ |
280 | 262 | return $status; |
281 | 263 | } |
282 | 264 | |
283 | | - // (d) Actually copy the file to the destination |
| 265 | + // (c) Actually copy the file to the destination |
284 | 266 | try { |
285 | 267 | $sContObj->copy_object_to( $srcRel, $dContObj, $destRel ); |
286 | 268 | } catch ( NoSuchObjectException $e ) { // source object does not exist |
— | — | @@ -306,16 +288,9 @@ |
307 | 289 | return $status; |
308 | 290 | } |
309 | 291 | |
310 | | - // (a) Get a swift proxy connection |
311 | | - $conn = $this->getConnection(); |
312 | | - if ( !$conn ) { |
313 | | - $status->fatal( 'backend-fail-connect', $this->name ); |
314 | | - return $status; |
315 | | - } |
316 | | - |
317 | | - // (b) Check the source container |
| 292 | + // (a) Check the source container |
318 | 293 | try { |
319 | | - $sContObj = $conn->get_container( $srcCont ); |
| 294 | + $sContObj = $this->getContainer( $srcCont ); |
320 | 295 | } catch ( NoSuchContainerException $e ) { |
321 | 296 | $status->fatal( 'backend-fail-delete', $params['src'] ); |
322 | 297 | return $status; |
— | — | @@ -328,7 +303,7 @@ |
329 | 304 | return $status; |
330 | 305 | } |
331 | 306 | |
332 | | - // (c) Actually delete the object |
| 307 | + // (b) Actually delete the object |
333 | 308 | try { |
334 | 309 | $sContObj->delete_object( $srcRel ); |
335 | 310 | } catch ( NoSuchObjectException $e ) { |
— | — | @@ -351,16 +326,8 @@ |
352 | 327 | protected function doPrepareInternal( $fullCont, $dir, array $params ) { |
353 | 328 | $status = Status::newGood(); |
354 | 329 | |
355 | | - // (a) Get a swift proxy connection |
356 | | - $conn = $this->getConnection(); |
357 | | - if ( !$conn ) { |
358 | | - $status->fatal( 'backend-fail-connect', $this->name ); |
359 | | - return $status; |
360 | | - } |
361 | | - |
362 | | - // (b) Create the destination container |
363 | 330 | try { |
364 | | - $conn->create_container( $fullCont ); |
| 331 | + $this->createContainer( $fullCont ); |
365 | 332 | } catch ( InvalidResponseException $e ) { |
366 | 333 | $status->fatal( 'backend-fail-connect', $this->name ); |
367 | 334 | } catch ( Exception $e ) { // some other exception? |
— | — | @@ -381,6 +348,45 @@ |
382 | 349 | } |
383 | 350 | |
384 | 351 | /** |
| 352 | + * @see FileBackend::doCleanInternal() |
| 353 | + */ |
| 354 | + protected function doCleanInternal( $fullCont, $dir, array $params ) { |
| 355 | + $status = Status::newGood(); |
| 356 | + |
| 357 | + // (a) Check the container |
| 358 | + try { |
| 359 | + $contObj = $this->getContainer( $fullCont, true ); |
| 360 | + } catch ( NoSuchContainerException $e ) { |
| 361 | + return $status; // ok, nothing to do |
| 362 | + } catch ( InvalidResponseException $e ) { |
| 363 | + $status->fatal( 'backend-fail-connect', $this->name ); |
| 364 | + return $status; |
| 365 | + } catch ( Exception $e ) { // some other exception? |
| 366 | + $status->fatal( 'backend-fail-internal', $this->name ); |
| 367 | + $this->logException( $e, __METHOD__, $params ); |
| 368 | + return $status; |
| 369 | + } |
| 370 | + |
| 371 | + // (c) Delete the container if empty |
| 372 | + if ( $contObj->count == 0 ) { |
| 373 | + try { |
| 374 | + $this->deleteContainer( $fullCont ); |
| 375 | + } catch ( NoSuchContainerException $e ) { |
| 376 | + return $status; // race? |
| 377 | + } catch ( InvalidResponseException $e ) { |
| 378 | + $status->fatal( 'backend-fail-connect', $this->name ); |
| 379 | + return $status; |
| 380 | + } catch ( Exception $e ) { // some other exception? |
| 381 | + $status->fatal( 'backend-fail-internal', $this->name ); |
| 382 | + $this->logException( $e, __METHOD__, $params ); |
| 383 | + return $status; |
| 384 | + } |
| 385 | + } |
| 386 | + |
| 387 | + return $status; |
| 388 | + } |
| 389 | + |
| 390 | + /** |
385 | 391 | * @see FileBackend::doFileExists() |
386 | 392 | */ |
387 | 393 | protected function doGetFileStat( array $params ) { |
— | — | @@ -389,17 +395,12 @@ |
390 | 396 | return false; // invalid storage path |
391 | 397 | } |
392 | 398 | |
393 | | - $conn = $this->getConnection(); |
394 | | - if ( !$conn ) { |
395 | | - return null; |
396 | | - } |
397 | | - |
398 | 399 | $stat = false; |
399 | 400 | try { |
400 | | - $container = $conn->get_container( $srcCont ); |
| 401 | + $container = $this->getContainer( $srcCont ); |
401 | 402 | // @TODO: handle 'latest' param as "X-Newest: true" |
402 | 403 | $obj = $container->get_object( $srcRel ); |
403 | | - // Convert "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW |
| 404 | + // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW |
404 | 405 | $date = DateTime::createFromFormat( 'D, d F Y G:i:s e', $obj->last_modified ); |
405 | 406 | if ( $date ) { |
406 | 407 | $stat = array( |
— | — | @@ -431,14 +432,9 @@ |
432 | 433 | return false; // invalid storage path |
433 | 434 | } |
434 | 435 | |
435 | | - $conn = $this->getConnection(); |
436 | | - if ( !$conn ) { |
437 | | - return false; |
438 | | - } |
439 | | - |
440 | 436 | $data = false; |
441 | 437 | try { |
442 | | - $container = $conn->get_container( $srcCont ); |
| 438 | + $container = $this->getContainer( $srcCont ); |
443 | 439 | $obj = $container->get_object( $srcRel ); |
444 | 440 | $data = $obj->read( $this->headersFromParams( $params ) ); |
445 | 441 | } catch ( NoSuchContainerException $e ) { |
— | — | @@ -468,14 +464,9 @@ |
469 | 465 | * @return Array |
470 | 466 | */ |
471 | 467 | public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) { |
472 | | - $conn = $this->getConnection(); |
473 | | - if ( !$conn ) { |
474 | | - return null; |
475 | | - } |
476 | | - |
477 | 468 | $files = array(); |
478 | 469 | try { |
479 | | - $container = $conn->get_container( $fullCont ); |
| 470 | + $container = $this->getContainer( $fullCont ); |
480 | 471 | $files = $container->list_objects( $limit, $after, "{$dir}/" ); |
481 | 472 | } catch ( NoSuchContainerException $e ) { |
482 | 473 | } catch ( NoSuchObjectException $e ) { |
— | — | @@ -510,13 +501,8 @@ |
511 | 502 | $status->fatal( 'backend-fail-invalidpath', $params['src'] ); |
512 | 503 | } |
513 | 504 | |
514 | | - $conn = $this->getConnection(); |
515 | | - if ( !$conn ) { |
516 | | - $status->fatal( 'backend-fail-connect', $this->name ); |
517 | | - } |
518 | | - |
519 | 505 | try { |
520 | | - $cont = $conn->get_container( $srcCont ); |
| 506 | + $cont = $this->getContainer( $srcCont ); |
521 | 507 | $obj = $cont->get_object( $srcRel ); |
522 | 508 | } catch ( NoSuchContainerException $e ) { |
523 | 509 | $status->fatal( 'backend-fail-stream', $params['src'] ); |
— | — | @@ -534,7 +520,7 @@ |
535 | 521 | } |
536 | 522 | |
537 | 523 | try { |
538 | | - $output = fopen( "php://output", "w" ); |
| 524 | + $output = fopen( 'php://output', 'w' ); |
539 | 525 | $obj->stream( $output, $this->headersFromParams( $params ) ); |
540 | 526 | } catch ( InvalidResponseException $e ) { |
541 | 527 | $status->fatal( 'backend-fail-connect', $this->name ); |
— | — | @@ -555,11 +541,6 @@ |
556 | 542 | return null; |
557 | 543 | } |
558 | 544 | |
559 | | - $conn = $this->getConnection(); |
560 | | - if ( !$conn ) { |
561 | | - return null; |
562 | | - } |
563 | | - |
564 | 545 | // Get source file extension |
565 | 546 | $ext = FileBackend::extensionFromPath( $srcRel ); |
566 | 547 | // Create a new temporary file... |
— | — | @@ -569,7 +550,7 @@ |
570 | 551 | } |
571 | 552 | |
572 | 553 | try { |
573 | | - $cont = $conn->get_container( $srcCont ); |
| 554 | + $cont = $this->getContainer( $srcCont ); |
574 | 555 | $obj = $cont->get_object( $srcRel ); |
575 | 556 | $handle = fopen( $tmpFile->getPath(), 'w' ); |
576 | 557 | if ( $handle ) { |
— | — | @@ -609,9 +590,10 @@ |
610 | 591 | } |
611 | 592 | |
612 | 593 | /** |
613 | | - * Get a connection to the swift proxy |
| 594 | + * Get a connection to the Swift proxy |
614 | 595 | * |
615 | 596 | * @return CF_Connection|false |
| 597 | + * @throws InvalidResponseException |
616 | 598 | */ |
617 | 599 | protected function getConnection() { |
618 | 600 | if ( $this->conn === false ) { |
— | — | @@ -620,6 +602,7 @@ |
621 | 603 | // Authenticate with proxy and get a session key. |
622 | 604 | // Session keys expire after a while, so we renew them periodically. |
623 | 605 | if ( $this->conn === null || ( time() - $this->connStarted ) > $this->connTTL ) { |
| 606 | + $this->connContainers = array(); |
624 | 607 | try { |
625 | 608 | $this->auth->authenticate(); |
626 | 609 | $this->conn = new CF_Connection( $this->auth ); |
— | — | @@ -630,10 +613,59 @@ |
631 | 614 | $this->conn = false; // don't keep re-trying |
632 | 615 | } |
633 | 616 | } |
| 617 | + if ( !$this->conn ) { |
| 618 | + throw new InvalidResponseException; // auth/connection problem |
| 619 | + } |
634 | 620 | return $this->conn; |
635 | 621 | } |
636 | 622 | |
637 | 623 | /** |
| 624 | + * Get a Swift container object, possibly from process cache. |
| 625 | + * Use $reCache if the file count or byte count is needed. |
| 626 | + * |
| 627 | + * @param $container string Container name |
| 628 | + * @param $reCache bool Refresh the process cache |
| 629 | + * @return CF_Container |
| 630 | + */ |
| 631 | + protected function getContainer( $container, $reCache = false ) { |
| 632 | + $conn = $this->getConnection(); // Swift proxy connection |
| 633 | + if ( $reCache ) { |
| 634 | + unset( $this->connContainers[$container] ); // purge cache |
| 635 | + } |
| 636 | + if ( !isset( $this->connContainers[$container] ) ) { |
| 637 | + $contObj = $conn->get_container( $container ); |
| 638 | + // Exception not thrown: container must exist |
| 639 | + $this->connContainers[$container] = $contObj; // cache it |
| 640 | + } |
| 641 | + return $this->connContainers[$container]; |
| 642 | + } |
| 643 | + |
| 644 | + /** |
| 645 | + * Create a Swift container |
| 646 | + * |
| 647 | + * @param $container string Container name |
| 648 | + * @return CF_Container |
| 649 | + */ |
| 650 | + protected function createContainer( $container ) { |
| 651 | + $conn = $this->getConnection(); // Swift proxy connection |
| 652 | + $contObj = $conn->create_container( $container ); |
| 653 | + $this->connContainers[$container] = $contObj; // cache it |
| 654 | + return $contObj; |
| 655 | + } |
| 656 | + |
| 657 | + /** |
| 658 | + * Delete a Swift container |
| 659 | + * |
| 660 | + * @param $container string Container name |
| 661 | + * @return void |
| 662 | + */ |
| 663 | + protected function deleteContainer( $container ) { |
| 664 | + $conn = $this->getConnection(); // Swift proxy connection |
| 665 | + $conn->delete_container( $container ); |
| 666 | + unset( $this->connContainers[$container] ); // purge cache |
| 667 | + } |
| 668 | + |
| 669 | + /** |
638 | 670 | * Log an unexpected exception for this backend |
639 | 671 | * |
640 | 672 | * @param $e Exception |