Index: trunk/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php |
— | — | @@ -760,8 +760,91 @@ |
761 | 761 | |
762 | 762 | // @TODO: testSecure |
763 | 763 | |
764 | | - // @TODO: testDoOperations |
| 764 | + public function testDoOperations() { |
| 765 | + $this->backend = $this->singleBackend; |
| 766 | + $this->doTestDoOperations(); |
765 | 767 | |
| 768 | + $this->backend = $this->multiBackend; |
| 769 | + $this->doTestDoOperations(); |
| 770 | + } |
| 771 | + |
| 772 | + function doTestDoOperations() { |
| 773 | + $base = $this->baseStorePath(); |
| 774 | + |
| 775 | + $fileA = "$base/cont1/a/b/fileA.txt"; |
| 776 | + $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq'; |
| 777 | + $fileB = "$base/cont1/a/b/fileB.txt"; |
| 778 | + $fileBContents = 'g-jmq3gpqgt3qtg q3GT '; |
| 779 | + $fileC = "$base/cont1/a/b/fileC.txt"; |
| 780 | + $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag'; |
| 781 | + $fileD = "$base/cont1/a/b/fileD.txt"; |
| 782 | + |
| 783 | + $this->pathsToPrune[] = $fileA; |
| 784 | + $this->pathsToPrune[] = $fileB; |
| 785 | + $this->pathsToPrune[] = $fileC; |
| 786 | + $this->pathsToPrune[] = $fileD; |
| 787 | + |
| 788 | + $this->backend->prepare( array( 'dir' => dirname( $fileA ) ) ); |
| 789 | + $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) ); |
| 790 | + $this->backend->prepare( array( 'dir' => dirname( $fileB ) ) ); |
| 791 | + $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) ); |
| 792 | + $this->backend->prepare( array( 'dir' => dirname( $fileC ) ) ); |
| 793 | + $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) ); |
| 794 | + |
| 795 | + $status = $this->backend->doOperations( array( |
| 796 | + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwriteDest' => 1 ), |
| 797 | + // Now: A:<A>, B:<B>, C:<A>, D:<D> (file:<orginal contents>) |
| 798 | + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ), |
| 799 | + // Now: A:<A>, B:<B>, C:<A>, D:<D> |
| 800 | + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwriteDest' => 1 ), |
| 801 | + // Now: A:<A>, B:<B>, C:<empty>, D:<A> |
| 802 | + array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ), |
| 803 | + // Now: A:<A>, B:<empty>, C:<B>, D:<A> |
| 804 | + array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ), |
| 805 | + // Now: A:<A>, B:<empty>, C:<B>, D:<empty> |
| 806 | + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwriteDest' => 1 ), |
| 807 | + // Now: A:<B>, B:<empty>, C:<empty>, D:<empty> |
| 808 | + array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ), |
| 809 | + // Now: A:<B>, B:<empty>, C:<B>, D:<empty> |
| 810 | + array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ), |
| 811 | + // Now: A:<empty>, B:<empty>, C:<B>, D:<empty> |
| 812 | + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteDest' => 1 ), |
| 813 | + // Does nothing |
| 814 | + array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), |
| 815 | + // Does nothing |
| 816 | + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteDest' => 1 ), |
| 817 | + // Does nothing |
| 818 | + array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ), |
| 819 | + // Does nothing |
| 820 | + ) ); |
| 821 | + |
| 822 | + $this->assertEquals( array(), $status->errors, "Operation batch succeeded" ); |
| 823 | + $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" ); |
| 824 | + $this->assertEquals( 12, count( $status->success ), |
| 825 | + "Operation batch has correct success array" ); |
| 826 | + |
| 827 | + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ), |
| 828 | + "File does not exist at $fileA" ); |
| 829 | + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ), |
| 830 | + "File does not exist at $fileB" ); |
| 831 | + $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ), |
| 832 | + "File does not exist at $fileD" ); |
| 833 | + |
| 834 | + $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ), |
| 835 | + "File exists at $fileC" ); |
| 836 | + $this->assertEquals( $fileBContents, |
| 837 | + $this->backend->getFileContents( array( 'src' => $fileC ) ), |
| 838 | + "Correct file contents of $fileC" ); |
| 839 | + $this->assertEquals( strlen( $fileBContents ), |
| 840 | + $this->backend->getFileSize( array( 'src' => $fileC ) ), |
| 841 | + "Correct file size of $fileC" ); |
| 842 | + $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ), |
| 843 | + $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ), |
| 844 | + "Correct file SHA-1 of $fileC" ); |
| 845 | + |
| 846 | + // @TODO: test some cases where the ops should fail |
| 847 | + } |
| 848 | + |
766 | 849 | public function testGetFileList() { |
767 | 850 | $this->backend = $this->singleBackend; |
768 | 851 | $this->doTestGetFileList(); |
Index: trunk/phase3/includes/filerepo/backend/FileOp.php |
— | — | @@ -17,25 +17,22 @@ |
18 | 18 | * @since 1.19 |
19 | 19 | */ |
20 | 20 | abstract class FileOp { |
21 | | - /** $var Array */ |
| 21 | + /** @var Array */ |
22 | 22 | protected $params = array(); |
23 | | - /** $var FileBackendBase */ |
| 23 | + /** @var FileBackendBase */ |
24 | 24 | protected $backend; |
25 | | - /** @var TempFSFile|null */ |
26 | | - protected $tmpSourceFile, $tmpDestFile; |
27 | 25 | |
28 | 26 | protected $state = self::STATE_NEW; // integer |
29 | 27 | protected $failed = false; // boolean |
30 | | - protected $useBackups = true; // boolean |
31 | 28 | protected $useLatest = true; // boolean |
32 | | - protected $destSameAsSource = false; // boolean |
33 | | - protected $destAlreadyExists = false; // boolean |
34 | 29 | |
| 30 | + protected $sourceSha1; // string |
| 31 | + protected $destSameAsSource; // boolean |
| 32 | + |
35 | 33 | /* Object life-cycle */ |
36 | 34 | const STATE_NEW = 1; |
37 | 35 | const STATE_CHECKED = 2; |
38 | 36 | const STATE_ATTEMPTED = 3; |
39 | | - const STATE_DONE = 4; |
40 | 37 | |
41 | 38 | /** |
42 | 39 | * Build a new file operation transaction |
— | — | @@ -54,18 +51,7 @@ |
55 | 52 | } |
56 | 53 | |
57 | 54 | /** |
58 | | - * Disable file backups for this operation |
59 | | - * |
60 | | - * @return void |
61 | | - */ |
62 | | - final protected function disableBackups() { |
63 | | - $this->useBackups = false; |
64 | | - } |
65 | | - |
66 | | - /** |
67 | 55 | * Allow stale data for file reads and existence checks. |
68 | | - * If this is called, then disableBackups() should also be called |
69 | | - * unless the affected files are known to have not changed recently. |
70 | 56 | * |
71 | 57 | * @return void |
72 | 58 | */ |
— | — | @@ -91,81 +77,57 @@ |
92 | 78 | final public static function attemptBatch( array $performOps, array $opts ) { |
93 | 79 | $status = Status::newGood(); |
94 | 80 | |
95 | | - $allowStale = isset( $opts['allowStale'] ) && $opts['allowStale']; |
96 | | - $ignoreErrors = isset( $opts['force'] ) && $opts['force']; |
| 81 | + $allowStale = !empty( $opts['allowStale'] ); |
| 82 | + $ignoreErrors = !empty( $opts['force'] ); |
| 83 | + |
97 | 84 | $predicates = FileOp::newPredicates(); // account for previous op in prechecks |
98 | 85 | // Do pre-checks for each operation; abort on failure... |
99 | 86 | foreach ( $performOps as $index => $fileOp ) { |
100 | 87 | if ( $allowStale ) { |
101 | 88 | $fileOp->allowStaleReads(); // allow potentially stale reads |
102 | 89 | } |
103 | | - $status->merge( $fileOp->precheck( $predicates ) ); |
104 | | - if ( !$status->isOK() ) { // operation failed? |
105 | | - if ( $ignoreErrors ) { |
106 | | - ++$status->failCount; |
107 | | - $status->success[$index] = false; |
108 | | - } else { |
109 | | - return $status; |
| 90 | + $subStatus = $fileOp->precheck( $predicates ); |
| 91 | + $status->merge( $subStatus ); |
| 92 | + if ( !$subStatus->isOK() ) { // operation failed? |
| 93 | + $status->success[$index] = false; |
| 94 | + ++$status->failCount; |
| 95 | + if ( !$ignoreErrors ) { |
| 96 | + return $status; // abort |
110 | 97 | } |
111 | 98 | } |
112 | 99 | } |
113 | 100 | |
114 | | - // Attempt each operation; abort on failure... |
| 101 | + // Attempt each operation... |
115 | 102 | foreach ( $performOps as $index => $fileOp ) { |
116 | 103 | if ( $fileOp->failed() ) { |
117 | 104 | continue; // nothing to do |
118 | | - } elseif ( $ignoreErrors ) { |
119 | | - $fileOp->disableBackups(); // no chance of revert() calls |
120 | 105 | } |
121 | | - $status->merge( $fileOp->attempt() ); |
122 | | - if ( !$status->isOK() ) { // operation failed? |
123 | | - if ( $ignoreErrors ) { |
124 | | - ++$status->failCount; |
125 | | - $status->success[$index] = false; |
126 | | - } else { |
127 | | - // Revert everything done so far and abort. |
128 | | - // Do this by reverting each previous operation in reverse order. |
129 | | - $pos = $index - 1; // last one failed; no need to revert() |
130 | | - while ( $pos >= 0 ) { |
131 | | - if ( !$performOps[$pos]->failed() ) { |
132 | | - $status->merge( $performOps[$pos]->revert() ); |
133 | | - } |
134 | | - $pos--; |
135 | | - } |
136 | | - return $status; |
137 | | - } |
138 | | - } |
139 | | - } |
140 | | - |
141 | | - $wasOk = $status->isOK(); |
142 | | - // Finish each operation... |
143 | | - foreach ( $performOps as $index => $fileOp ) { |
144 | | - if ( $fileOp->failed() ) { |
145 | | - continue; // nothing to do |
146 | | - } |
147 | | - $subStatus = $fileOp->finish(); |
| 106 | + $subStatus = $fileOp->attempt(); |
| 107 | + $status->merge( $subStatus ); |
148 | 108 | if ( $subStatus->isOK() ) { |
149 | | - ++$status->successCount; |
150 | 109 | $status->success[$index] = true; |
| 110 | + ++$status->successCount; |
151 | 111 | } else { |
152 | | - ++$status->failCount; |
153 | 112 | $status->success[$index] = false; |
| 113 | + ++$status->failCount; |
| 114 | + if ( !$ignoreErrors ) { |
| 115 | + // Log remaining ops as failed for recovery... |
| 116 | + for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) { |
| 117 | + $performOps[$i]->logFailure( 'attempt_aborted' ); |
| 118 | + } |
| 119 | + return $status; // bail out |
| 120 | + } |
154 | 121 | } |
155 | | - $status->merge( $subStatus ); |
156 | 122 | } |
157 | 123 | |
158 | | - // Make sure status is OK, despite any finish() fatals |
159 | | - $status->setResult( $wasOk, $status->value ); |
160 | | - |
161 | 124 | return $status; |
162 | 125 | } |
163 | 126 | |
164 | 127 | /** |
165 | | - * Get the value of the parameter with the given name. |
166 | | - * Returns null if the parameter is not set. |
| 128 | + * Get the value of the parameter with the given name |
167 | 129 | * |
168 | 130 | * @param $name string |
169 | | - * @return mixed |
| 131 | + * @return mixed Returns null if the parameter is not set |
170 | 132 | */ |
171 | 133 | final public function getParam( $name ) { |
172 | 134 | return isset( $this->params[$name] ) ? $this->params[$name] : null; |
— | — | @@ -173,6 +135,7 @@ |
174 | 136 | |
175 | 137 | /** |
176 | 138 | * Check if this operation failed precheck() or attempt() |
| 139 | + * |
177 | 140 | * @return type |
178 | 141 | */ |
179 | 142 | final public function failed() { |
— | — | @@ -185,7 +148,7 @@ |
186 | 149 | * @return Array |
187 | 150 | */ |
188 | 151 | final public static function newPredicates() { |
189 | | - return array( 'exists' => array() ); |
| 152 | + return array( 'exists' => array(), 'sha1' => array() ); |
190 | 153 | } |
191 | 154 | |
192 | 155 | /** |
— | — | @@ -227,45 +190,6 @@ |
228 | 191 | } |
229 | 192 | |
230 | 193 | /** |
231 | | - * Revert the operation; affected files are restored |
232 | | - * |
233 | | - * @return Status |
234 | | - */ |
235 | | - final public function revert() { |
236 | | - if ( $this->state !== self::STATE_ATTEMPTED ) { |
237 | | - return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state ); |
238 | | - } |
239 | | - $this->state = self::STATE_DONE; |
240 | | - if ( $this->failed ) { |
241 | | - $status = Status::newGood(); // nothing to revert |
242 | | - } else { |
243 | | - $status = $this->doRevert(); |
244 | | - if ( !$status->isOK() ) { |
245 | | - $this->logFailure( 'revert' ); |
246 | | - } |
247 | | - } |
248 | | - return $status; |
249 | | - } |
250 | | - |
251 | | - /** |
252 | | - * Finish the operation; this may be irreversible |
253 | | - * |
254 | | - * @return Status |
255 | | - */ |
256 | | - final public function finish() { |
257 | | - if ( $this->state !== self::STATE_ATTEMPTED ) { |
258 | | - return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state ); |
259 | | - } |
260 | | - $this->state = self::STATE_DONE; |
261 | | - if ( $this->failed ) { |
262 | | - $status = Status::newGood(); // nothing to finish |
263 | | - } else { |
264 | | - $status = $this->doFinish(); |
265 | | - } |
266 | | - return $status; |
267 | | - } |
268 | | - |
269 | | - /** |
270 | 194 | * Get a list of storage paths read from for this operation |
271 | 195 | * |
272 | 196 | * @return Array |
— | — | @@ -303,189 +227,55 @@ |
304 | 228 | abstract protected function doAttempt(); |
305 | 229 | |
306 | 230 | /** |
307 | | - * @return Status |
308 | | - */ |
309 | | - abstract protected function doRevert(); |
310 | | - |
311 | | - /** |
312 | | - * @return Status |
313 | | - */ |
314 | | - protected function doFinish() { |
315 | | - return Status::newGood(); |
316 | | - } |
317 | | - |
318 | | - /** |
319 | | - * Check if the destination file exists and update the |
320 | | - * destAlreadyExists member variable. A bad status will |
321 | | - * be returned if there is no chance it can be overwritten. |
| 231 | + * Check for errors with regards to the destination file already existing. |
| 232 | + * This also updates the destSameAsSource and sourceSha1 member variables. |
| 233 | + * A bad status will be returned if there is no chance it can be overwritten. |
322 | 234 | * |
323 | 235 | * @param $predicates Array |
324 | 236 | * @return Status |
325 | 237 | */ |
326 | 238 | protected function precheckDestExistence( array $predicates ) { |
327 | 239 | $status = Status::newGood(); |
328 | | - if ( $this->fileExists( $this->params['dst'], $predicates ) ) { |
329 | | - $this->destAlreadyExists = true; |
330 | | - if ( !$this->getParam( 'overwriteDest' ) && !$this->getParam( 'overwriteSame' ) ) { |
331 | | - $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); |
332 | | - return $status; |
333 | | - } |
334 | | - } else { |
335 | | - $this->destAlreadyExists = false; |
| 240 | + // Get hash of source file/string and the destination file |
| 241 | + $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string |
| 242 | + if ( $this->sourceSha1 === null ) { // file in storage? |
| 243 | + $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates ); |
336 | 244 | } |
337 | | - return $status; |
338 | | - } |
339 | | - |
340 | | - /** |
341 | | - * Backup any file at the source to a temporary file |
342 | | - * |
343 | | - * @return Status |
344 | | - */ |
345 | | - protected function backupSource() { |
346 | | - $status = Status::newGood(); |
347 | | - if ( $this->useBackups ) { |
348 | | - // Check if a file already exists at the source... |
349 | | - $params = array( 'src' => $this->params['src'], 'latest' => $this->useLatest ); |
350 | | - if ( $this->backend->fileExists( $params ) ) { |
351 | | - // Create a temporary backup copy... |
352 | | - $this->tmpSourcePath = $this->backend->getLocalCopy( $params ); |
353 | | - if ( $this->tmpSourcePath === null ) { |
354 | | - $status->fatal( 'backend-fail-backup', $this->params['src'] ); |
355 | | - return $status; |
356 | | - } |
357 | | - } |
358 | | - } |
359 | | - return $status; |
360 | | - } |
361 | | - |
362 | | - /** |
363 | | - * Backup the file at the destination to a temporary file. |
364 | | - * Don't bother backing it up unless we might overwrite the file. |
365 | | - * This assumes that the destination is in the backend and that |
366 | | - * the source is either in the backend or on the file system. |
367 | | - * This also handles the 'overwriteSame' check logic and updates |
368 | | - * the destSameAsSource member variable. |
369 | | - * |
370 | | - * @return Status |
371 | | - */ |
372 | | - protected function checkAndBackupDest() { |
373 | | - $status = Status::newGood(); |
374 | 245 | $this->destSameAsSource = false; |
375 | | - |
376 | | - if ( $this->getParam( 'overwriteDest' ) ) { |
377 | | - if ( $this->useBackups ) { |
378 | | - // Create a temporary backup copy... |
379 | | - $params = array( 'src' => $this->params['dst'], 'latest' => $this->useLatest ); |
380 | | - $this->tmpDestFile = $this->backend->getLocalCopy( $params ); |
381 | | - if ( !$this->tmpDestFile ) { |
382 | | - $status->fatal( 'backend-fail-backup', $this->params['dst'] ); |
383 | | - return $status; |
384 | | - } |
385 | | - } |
386 | | - } elseif ( $this->getParam( 'overwriteSame' ) ) { |
387 | | - $shash = $this->getSourceSha1Base36(); |
388 | | - // If there is a single source, then we can do some checks already. |
389 | | - // For things like concatenate(), we would need to build a temp file |
390 | | - // first and thus don't support 'overwriteSame' ($shash is null). |
391 | | - if ( $shash !== null ) { |
392 | | - $dhash = $this->getFileSha1Base36( $this->params['dst'] ); |
393 | | - if ( !strlen( $shash ) || !strlen( $dhash ) ) { |
| 246 | + if ( $this->fileExists( $this->params['dst'], $predicates ) ) { |
| 247 | + if ( $this->getParam( 'overwriteDest' ) ) { |
| 248 | + return $status; // OK |
| 249 | + } elseif ( $this->getParam( 'overwriteSame' ) ) { |
| 250 | + $dhash = $this->fileSha1( $this->params['dst'], $predicates ); |
| 251 | + // Check if hashes are valid and match each other... |
| 252 | + if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) { |
394 | 253 | $status->fatal( 'backend-fail-hashes' ); |
395 | | - } elseif ( $shash !== $dhash ) { |
| 254 | + } elseif ( $this->sourceSha1 !== $dhash ) { |
396 | 255 | // Give an error if the files are not identical |
397 | 256 | $status->fatal( 'backend-fail-notsame', $this->params['dst'] ); |
398 | 257 | } else { |
399 | | - $this->destSameAsSource = true; |
| 258 | + $this->destSameAsSource = true; // OK |
400 | 259 | } |
401 | 260 | return $status; // do nothing; either OK or bad status |
| 261 | + } else { |
| 262 | + $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); |
| 263 | + return $status; |
402 | 264 | } |
403 | | - } else { |
404 | | - $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] ); |
405 | | - return $status; |
406 | 265 | } |
407 | | - |
408 | 266 | return $status; |
409 | 267 | } |
410 | 268 | |
411 | 269 | /** |
412 | | - * checkAndBackupDest() helper function to get the source file Sha1. |
413 | | - * Returns false on failure and null if there is no single source. |
| 270 | + * precheckDestExistence() helper function to get the source file SHA-1. |
| 271 | + * Subclasses should overwride this iff the source is not in storage. |
414 | 272 | * |
415 | | - * @return string|false|null |
| 273 | + * @return string|false Returns false on failure |
416 | 274 | */ |
417 | 275 | protected function getSourceSha1Base36() { |
418 | 276 | return null; // N/A |
419 | 277 | } |
420 | 278 | |
421 | 279 | /** |
422 | | - * checkAndBackupDest() helper function to get the Sha1 of a file. |
423 | | - * |
424 | | - * @return string|false False on failure |
425 | | - */ |
426 | | - protected function getFileSha1Base36( $path ) { |
427 | | - // Source file is in backend |
428 | | - if ( FileBackend::isStoragePath( $path ) ) { |
429 | | - // For some backends (e.g. Swift, Azure) we can get |
430 | | - // standard hashes to use for this types of comparisons. |
431 | | - $params = array( 'src' => $path, 'latest' => $this->useLatest ); |
432 | | - $hash = $this->backend->getFileSha1Base36( $params ); |
433 | | - // Source file is on file system |
434 | | - } else { |
435 | | - wfSuppressWarnings(); |
436 | | - $hash = sha1_file( $path ); |
437 | | - wfRestoreWarnings(); |
438 | | - if ( $hash !== false ) { |
439 | | - $hash = wfBaseConvert( $hash, 16, 36, 31 ); |
440 | | - } |
441 | | - } |
442 | | - return $hash; |
443 | | - } |
444 | | - |
445 | | - /** |
446 | | - * Restore any temporary source backup file |
447 | | - * |
448 | | - * @return Status |
449 | | - */ |
450 | | - protected function restoreSource() { |
451 | | - $status = Status::newGood(); |
452 | | - // Restore any file that was at the destination |
453 | | - if ( $this->tmpSourcePath !== null ) { |
454 | | - $params = array( |
455 | | - 'src' => $this->tmpSourcePath, |
456 | | - 'dst' => $this->params['src'], |
457 | | - 'overwriteDest' => true |
458 | | - ); |
459 | | - $status = $this->backend->storeInternal( $params ); |
460 | | - if ( !$status->isOK() ) { |
461 | | - return $status; |
462 | | - } |
463 | | - } |
464 | | - return $status; |
465 | | - } |
466 | | - |
467 | | - /** |
468 | | - * Restore any temporary destination backup file |
469 | | - * |
470 | | - * @return Status |
471 | | - */ |
472 | | - protected function restoreDest() { |
473 | | - $status = Status::newGood(); |
474 | | - // Restore any file that was at the destination |
475 | | - if ( $this->tmpDestFile ) { |
476 | | - $params = array( |
477 | | - 'src' => $this->tmpDestFile->getPath(), |
478 | | - 'dst' => $this->params['dst'], |
479 | | - 'overwriteDest' => true |
480 | | - ); |
481 | | - $status = $this->backend->storeInternal( $params ); |
482 | | - if ( !$status->isOK() ) { |
483 | | - return $status; |
484 | | - } |
485 | | - } |
486 | | - return $status; |
487 | | - } |
488 | | - |
489 | | - /** |
490 | 280 | * Check if a file will exist in storage when this operation is attempted |
491 | 281 | * |
492 | 282 | * @param $source string Storage path |
— | — | @@ -502,23 +292,30 @@ |
503 | 293 | } |
504 | 294 | |
505 | 295 | /** |
| 296 | + * Get the SHA-1 of a file in storage when this operation is attempted |
| 297 | + * |
| 298 | + * @param $source string Storage path |
| 299 | + * @param $predicates Array |
| 300 | + * @return string|false |
| 301 | + */ |
| 302 | + final protected function fileSha1( $source, array $predicates ) { |
| 303 | + if ( isset( $predicates['sha1'][$source] ) ) { |
| 304 | + return $predicates['sha1'][$source]; // previous op assures this |
| 305 | + } else { |
| 306 | + $params = array( 'src' => $source, 'latest' => $this->useLatest ); |
| 307 | + return $this->backend->getFileSha1Base36( $params ); |
| 308 | + } |
| 309 | + } |
| 310 | + |
| 311 | + /** |
506 | 312 | * Log a file operation failure and preserve any temp files |
507 | 313 | * |
508 | | - * @param $fileOp FileOp |
| 314 | + * @param $action string |
509 | 315 | * @return void |
510 | 316 | */ |
511 | 317 | final protected function logFailure( $action ) { |
512 | 318 | $params = $this->params; |
513 | 319 | $params['failedAction'] = $action; |
514 | | - // Preserve backup files just in case (for recovery) |
515 | | - if ( $this->tmpSourceFile ) { |
516 | | - $this->tmpSourceFile->preserve(); // don't purge |
517 | | - $params['srcBackupPath'] = $this->tmpSourceFile->getPath(); |
518 | | - } |
519 | | - if ( $this->tmpDestFile ) { |
520 | | - $this->tmpDestFile->preserve(); // don't purge |
521 | | - $params['dstBackupPath'] = $this->tmpDestFile->getPath(); |
522 | | - } |
523 | 320 | try { |
524 | 321 | wfDebugLog( 'FileOperation', |
525 | 322 | get_class( $this ) . ' failed:' . serialize( $params ) ); |
— | — | @@ -543,30 +340,24 @@ |
544 | 341 | |
545 | 342 | protected function doPrecheck( array &$predicates ) { |
546 | 343 | $status = Status::newGood(); |
547 | | - // Check if destination file exists |
548 | | - $status->merge( $this->precheckDestExistence( $predicates ) ); |
549 | | - if ( !$status->isOK() ) { |
550 | | - return $status; |
551 | | - } |
552 | 344 | // Check if the source file exists on the file system |
553 | 345 | if ( !is_file( $this->params['src'] ) ) { |
554 | 346 | $status->fatal( 'backend-fail-notexists', $this->params['src'] ); |
555 | 347 | return $status; |
556 | 348 | } |
| 349 | + // Check if destination file exists |
| 350 | + $status->merge( $this->precheckDestExistence( $predicates ) ); |
| 351 | + if ( !$status->isOK() ) { |
| 352 | + return $status; |
| 353 | + } |
557 | 354 | // Update file existence predicates |
558 | 355 | $predicates['exists'][$this->params['dst']] = true; |
559 | | - return $status; |
| 356 | + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; |
| 357 | + return $status; // safe to call attempt() |
560 | 358 | } |
561 | 359 | |
562 | 360 | protected function doAttempt() { |
563 | 361 | $status = Status::newGood(); |
564 | | - // Create a destination backup copy as needed |
565 | | - if ( $this->destAlreadyExists ) { |
566 | | - $status->merge( $this->checkAndBackupDest() ); |
567 | | - if ( !$status->isOK() ) { |
568 | | - return $status; |
569 | | - } |
570 | | - } |
571 | 362 | // Store the file at the destination |
572 | 363 | if ( !$this->destSameAsSource ) { |
573 | 364 | $status->merge( $this->backend->storeInternal( $this->params ) ); |
— | — | @@ -574,20 +365,16 @@ |
575 | 366 | return $status; |
576 | 367 | } |
577 | 368 | |
578 | | - protected function doRevert() { |
579 | | - $status = Status::newGood(); |
580 | | - if ( !$this->destSameAsSource ) { |
581 | | - // Restore any file that was at the destination, |
582 | | - // overwritting what was put there in attempt() |
583 | | - $status->merge( $this->restoreDest() ); |
| 369 | + protected function getSourceSha1Base36() { |
| 370 | + wfSuppressWarnings(); |
| 371 | + $hash = sha1_file( $this->params['src'] ); |
| 372 | + wfRestoreWarnings(); |
| 373 | + if ( $hash !== false ) { |
| 374 | + $hash = wfBaseConvert( $hash, 16, 36, 31 ); |
584 | 375 | } |
585 | | - return $status; |
| 376 | + return $hash; |
586 | 377 | } |
587 | 378 | |
588 | | - protected function getSourceSha1Base36() { |
589 | | - return $this->getFileSha1Base36( $this->params['src'] ); |
590 | | - } |
591 | | - |
592 | 379 | public function storagePathsChanged() { |
593 | 380 | return array( $this->params['dst'] ); |
594 | 381 | } |
— | — | @@ -615,18 +402,12 @@ |
616 | 403 | } |
617 | 404 | // Update file existence predicates |
618 | 405 | $predicates['exists'][$this->params['dst']] = true; |
619 | | - return $status; |
| 406 | + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; |
| 407 | + return $status; // safe to call attempt() |
620 | 408 | } |
621 | 409 | |
622 | 410 | protected function doAttempt() { |
623 | 411 | $status = Status::newGood(); |
624 | | - // Create a destination backup copy as needed |
625 | | - if ( $this->destAlreadyExists ) { |
626 | | - $status->merge( $this->checkAndBackupDest() ); |
627 | | - if ( !$status->isOK() ) { |
628 | | - return $status; |
629 | | - } |
630 | | - } |
631 | 412 | // Create the file at the destination |
632 | 413 | if ( !$this->destSameAsSource ) { |
633 | 414 | $status->merge( $this->backend->createInternal( $this->params ) ); |
— | — | @@ -634,16 +415,6 @@ |
635 | 416 | return $status; |
636 | 417 | } |
637 | 418 | |
638 | | - protected function doRevert() { |
639 | | - $status = Status::newGood(); |
640 | | - if ( !$this->destSameAsSource ) { |
641 | | - // Restore any file that was at the destination, |
642 | | - // overwritting what was put there in attempt() |
643 | | - $status->merge( $this->restoreDest() ); |
644 | | - } |
645 | | - return $status; |
646 | | - } |
647 | | - |
648 | 419 | protected function getSourceSha1Base36() { |
649 | 420 | return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 ); |
650 | 421 | } |
— | — | @@ -668,51 +439,34 @@ |
669 | 440 | |
670 | 441 | protected function doPrecheck( array &$predicates ) { |
671 | 442 | $status = Status::newGood(); |
672 | | - // Check if destination file exists |
673 | | - $status->merge( $this->precheckDestExistence( $predicates ) ); |
674 | | - if ( !$status->isOK() ) { |
675 | | - return $status; |
676 | | - } |
677 | 443 | // Check if the source file exists |
678 | 444 | if ( !$this->fileExists( $this->params['src'], $predicates ) ) { |
679 | 445 | $status->fatal( 'backend-fail-notexists', $this->params['src'] ); |
680 | 446 | return $status; |
681 | 447 | } |
| 448 | + // Check if destination file exists |
| 449 | + $status->merge( $this->precheckDestExistence( $predicates ) ); |
| 450 | + if ( !$status->isOK() ) { |
| 451 | + return $status; |
| 452 | + } |
682 | 453 | // Update file existence predicates |
683 | 454 | $predicates['exists'][$this->params['dst']] = true; |
684 | | - return $status; |
| 455 | + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; |
| 456 | + return $status; // safe to call attempt() |
685 | 457 | } |
686 | 458 | |
687 | 459 | protected function doAttempt() { |
688 | 460 | $status = Status::newGood(); |
689 | | - // Create a destination backup copy as needed |
690 | | - if ( $this->destAlreadyExists ) { |
691 | | - $status->merge( $this->checkAndBackupDest() ); |
692 | | - if ( !$status->isOK() ) { |
693 | | - return $status; |
| 461 | + // Do nothing if the src/dst paths are the same |
| 462 | + if ( $this->params['src'] !== $this->params['dst'] ) { |
| 463 | + // Copy the file into the destination |
| 464 | + if ( !$this->destSameAsSource ) { |
| 465 | + $status->merge( $this->backend->copyInternal( $this->params ) ); |
694 | 466 | } |
695 | 467 | } |
696 | | - // Copy the file into the destination |
697 | | - if ( !$this->destSameAsSource ) { |
698 | | - $status->merge( $this->backend->copyInternal( $this->params ) ); |
699 | | - } |
700 | 468 | return $status; |
701 | 469 | } |
702 | 470 | |
703 | | - protected function doRevert() { |
704 | | - $status = Status::newGood(); |
705 | | - if ( !$this->destSameAsSource ) { |
706 | | - // Restore any file that was at the destination, |
707 | | - // overwritting what was put there in attempt() |
708 | | - $status->merge( $this->restoreDest() ); |
709 | | - } |
710 | | - return $status; |
711 | | - } |
712 | | - |
713 | | - protected function getSourceSha1Base36() { |
714 | | - return $this->getFileSha1Base36( $this->params['src'] ); |
715 | | - } |
716 | | - |
717 | 471 | public function storagePathsRead() { |
718 | 472 | return array( $this->params['src'] ); |
719 | 473 | } |
— | — | @@ -737,76 +491,43 @@ |
738 | 492 | |
739 | 493 | protected function doPrecheck( array &$predicates ) { |
740 | 494 | $status = Status::newGood(); |
741 | | - // Check if destination file exists |
742 | | - $status->merge( $this->precheckDestExistence( $predicates ) ); |
743 | | - if ( !$status->isOK() ) { |
744 | | - return $status; |
745 | | - } |
746 | 495 | // Check if the source file exists |
747 | 496 | if ( !$this->fileExists( $this->params['src'], $predicates ) ) { |
748 | 497 | $status->fatal( 'backend-fail-notexists', $this->params['src'] ); |
749 | 498 | return $status; |
750 | 499 | } |
| 500 | + // Check if destination file exists |
| 501 | + $status->merge( $this->precheckDestExistence( $predicates ) ); |
| 502 | + if ( !$status->isOK() ) { |
| 503 | + return $status; |
| 504 | + } |
751 | 505 | // Update file existence predicates |
752 | 506 | $predicates['exists'][$this->params['src']] = false; |
| 507 | + $predicates['sha1'][$this->params['src']] = false; |
753 | 508 | $predicates['exists'][$this->params['dst']] = true; |
754 | | - return $status; |
| 509 | + $predicates['sha1'][$this->params['dst']] = $this->sourceSha1; |
| 510 | + return $status; // safe to call attempt() |
755 | 511 | } |
756 | 512 | |
757 | 513 | protected function doAttempt() { |
758 | 514 | $status = Status::newGood(); |
759 | | - // Create a destination backup copy as needed |
760 | | - if ( $this->destAlreadyExists ) { |
761 | | - $status->merge( $this->checkAndBackupDest() ); |
762 | | - if ( !$status->isOK() ) { |
763 | | - return $status; |
| 515 | + // Do nothing if the src/dst paths are the same |
| 516 | + if ( $this->params['src'] !== $this->params['dst'] ) { |
| 517 | + if ( !$this->destSameAsSource ) { |
| 518 | + // Move the file into the destination |
| 519 | + $status->merge( $this->backend->moveInternal( $this->params ) ); |
| 520 | + } else { |
| 521 | + // Just delete source as the destination needs no changes |
| 522 | + $params = array( 'src' => $this->params['src'] ); |
| 523 | + $status->merge( $this->backend->deleteInternal( $params ) ); |
| 524 | + if ( !$status->isOK() ) { |
| 525 | + return $status; |
| 526 | + } |
764 | 527 | } |
765 | 528 | } |
766 | | - if ( !$this->destSameAsSource ) { |
767 | | - // Move the file into the destination |
768 | | - $status->merge( $this->backend->moveInternal( $this->params ) ); |
769 | | - } else { |
770 | | - // Create a source backup copy as needed |
771 | | - $status->merge( $this->backupSource() ); |
772 | | - if ( !$status->isOK() ) { |
773 | | - return $status; |
774 | | - } |
775 | | - // Just delete source as the destination needs no changes |
776 | | - $params = array( 'src' => $this->params['src'] ); |
777 | | - $status->merge( $this->backend->deleteInternal( $params ) ); |
778 | | - if ( !$status->isOK() ) { |
779 | | - return $status; |
780 | | - } |
781 | | - } |
782 | 529 | return $status; |
783 | 530 | } |
784 | 531 | |
785 | | - protected function doRevert() { |
786 | | - $status = Status::newGood(); |
787 | | - if ( !$this->destSameAsSource ) { |
788 | | - // Move the file back to the source |
789 | | - $params = array( |
790 | | - 'src' => $this->params['dst'], |
791 | | - 'dst' => $this->params['src'] |
792 | | - ); |
793 | | - $status->merge( $this->backend->moveInternal( $params ) ); |
794 | | - if ( !$status->isOK() ) { |
795 | | - return $status; // also can't restore any dest file |
796 | | - } |
797 | | - // Restore any file that was at the destination |
798 | | - $status->merge( $this->restoreDest() ); |
799 | | - } else { |
800 | | - // Restore any source file |
801 | | - return $this->restoreSource(); |
802 | | - } |
803 | | - |
804 | | - return $status; |
805 | | - } |
806 | | - |
807 | | - protected function getSourceSha1Base36() { |
808 | | - return $this->getFileSha1Base36( $this->params['src'] ); |
809 | | - } |
810 | | - |
811 | 532 | public function storagePathsRead() { |
812 | 533 | return array( $this->params['src'] ); |
813 | 534 | } |
— | — | @@ -841,17 +562,13 @@ |
842 | 563 | } |
843 | 564 | // Update file existence predicates |
844 | 565 | $predicates['exists'][$this->params['src']] = false; |
845 | | - return $status; |
| 566 | + $predicates['sha1'][$this->params['src']] = false; |
| 567 | + return $status; // safe to call attempt() |
846 | 568 | } |
847 | 569 | |
848 | 570 | protected function doAttempt() { |
849 | 571 | $status = Status::newGood(); |
850 | 572 | if ( $this->needsDelete ) { |
851 | | - // Create a source backup copy as needed |
852 | | - $status->merge( $this->backupSource() ); |
853 | | - if ( !$status->isOK() ) { |
854 | | - return $status; |
855 | | - } |
856 | 573 | // Delete the source file |
857 | 574 | $status->merge( $this->backend->deleteInternal( $this->params ) ); |
858 | 575 | if ( !$status->isOK() ) { |
— | — | @@ -861,11 +578,6 @@ |
862 | 579 | return $status; |
863 | 580 | } |
864 | 581 | |
865 | | - protected function doRevert() { |
866 | | - // Restore any source file that we deleted |
867 | | - return $this->restoreSource(); |
868 | | - } |
869 | | - |
870 | 582 | public function storagePathsChanged() { |
871 | 583 | return array( $this->params['src'] ); |
872 | 584 | } |
— | — | @@ -878,8 +590,4 @@ |
879 | 591 | protected function doAttempt() { |
880 | 592 | return Status::newGood(); |
881 | 593 | } |
882 | | - |
883 | | - protected function doRevert() { |
884 | | - return Status::newGood(); |
885 | | - } |
886 | 594 | } |
Index: trunk/phase3/includes/filerepo/backend/FileBackend.php |
— | — | @@ -155,6 +155,11 @@ |
156 | 156 | * This can increase performance for non-critical writes. |
157 | 157 | * This has no effect unless the 'force' flag is set. |
158 | 158 | * |
| 159 | + * Remarks: |
| 160 | + * File system paths given to operations should refer to files that are |
| 161 | + * either locked or otherwise safe from modification from other processes. |
| 162 | + * Normally these files will be new temp files, which should be adequate. |
| 163 | + * |
159 | 164 | * Return value: |
160 | 165 | * This returns a Status, which contains all warnings and fatals that occured |
161 | 166 | * during the operation. The 'failCount', 'successCount', and 'success' members |