Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php |
— | — | @@ -132,6 +132,11 @@ |
133 | 133 | return $this->doOperation( array( $op ) ); |
134 | 134 | } |
135 | 135 | |
| 136 | + function create( array $params ) { |
| 137 | + $op = array( 'operation' => 'create' ) + $params; |
| 138 | + return $this->doOperation( array( $op ) ); |
| 139 | + } |
| 140 | + |
136 | 141 | function fileExists( array $params ) { |
137 | 142 | foreach ( $this->backends as $backend ) { |
138 | 143 | if ( $backend->fileExists( $params ) ) { |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileOp.php |
— | — | @@ -0,0 +1,438 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * @file |
| 5 | + * @ingroup FileRepo |
| 6 | + */ |
| 7 | + |
| 8 | +/** |
| 9 | + * Helper class for representing operations with transaction support. |
| 10 | + * FileBackend::doOperations() will require these classes for supported operations. |
| 11 | + * |
| 12 | + * Access use of large fields should be avoided as we want to be able to support |
| 13 | + * potentially many FileOp classes in large arrays in memory. |
| 14 | + */ |
| 15 | +abstract class FileOp { |
| 16 | + /** $var Array */ |
| 17 | + protected $params; |
| 18 | + /** $var FileBackend */ |
| 19 | + protected $backend; |
| 20 | + |
| 21 | + protected $state; |
| 22 | + protected $failedAttempt; |
| 23 | + |
| 24 | + const STATE_NEW = 1; |
| 25 | + const STATE_ATTEMPTED = 2; |
| 26 | + const STATE_DONE = 3; |
| 27 | + |
| 28 | + /** |
| 29 | + * Build a new file operation transaction |
| 30 | + * |
| 31 | + * @params $backend FileBackend |
| 32 | + * @params $params Array |
| 33 | + */ |
| 34 | + final public function __construct( FileBackend $backend, array $params ) { |
| 35 | + $this->backend = $backend; |
| 36 | + $this->params = $params; |
| 37 | + $this->state = self::STATE_NEW; |
| 38 | + $this->failedAttempt = false; |
| 39 | + $this->initialize(); |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * Attempt the operation; this must be reversible |
| 44 | + * |
| 45 | + * @return Status |
| 46 | + */ |
| 47 | + final public function attempt() { |
| 48 | + if ( $this->state !== self::STATE_NEW ) { |
| 49 | + throw new MWException( "Cannot attempt operation called twice." ); |
| 50 | + } |
| 51 | + $this->state = self::STATE_ATTEMPTED; |
| 52 | + $status = $this->doAttempt(); |
| 53 | + if ( !$status->isOK() ) { |
| 54 | + $this->failedAttempt = true; |
| 55 | + } |
| 56 | + return $status; |
| 57 | + } |
| 58 | + |
| 59 | + /** |
| 60 | + * Revert the operation; affected files are restored |
| 61 | + * |
| 62 | + * @return Status |
| 63 | + */ |
| 64 | + final public function revert() { |
| 65 | + if ( $this->state !== self::STATE_ATTEMPTED ) { |
| 66 | + throw new MWException( "Cannot rollback an unstarted or finished operation." ); |
| 67 | + } |
| 68 | + $this->state = self::STATE_DONE; |
| 69 | + if ( $this->failedAttempt ) { |
| 70 | + $status = Status::newGood(); // nothing to revert |
| 71 | + } else { |
| 72 | + $status = $this->doRevert(); |
| 73 | + } |
| 74 | + return $status; |
| 75 | + } |
| 76 | + |
| 77 | + /** |
| 78 | + * Finish the operation; this may be irreversible |
| 79 | + * |
| 80 | + * @return Status |
| 81 | + */ |
| 82 | + final public function finish() { |
| 83 | + if ( $this->state !== self::STATE_ATTEMPTED ) { |
| 84 | + throw new MWException( "Cannot cleanup an unstarted or finished operation." ); |
| 85 | + } |
| 86 | + $this->state = self::STATE_DONE; |
| 87 | + if ( $this->failedAttempt ) { |
| 88 | + $status = Status::newGood(); // nothing to revert |
| 89 | + } else { |
| 90 | + $status = $this->doFinish(); |
| 91 | + } |
| 92 | + return $status; |
| 93 | + } |
| 94 | + |
| 95 | + /** |
| 96 | + * Get a list of storage paths to lock for this operation |
| 97 | + * |
| 98 | + * @return Array |
| 99 | + */ |
| 100 | + public function storagePathsToLock() { |
| 101 | + return array(); |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * @return void |
| 106 | + */ |
| 107 | + protected function initialize() {} |
| 108 | + |
| 109 | + /** |
| 110 | + * @return Status |
| 111 | + */ |
| 112 | + abstract protected function doAttempt(); |
| 113 | + |
| 114 | + /** |
| 115 | + * @return Status |
| 116 | + */ |
| 117 | + abstract protected function doRevert(); |
| 118 | + |
| 119 | + /** |
| 120 | + * @return Status |
| 121 | + */ |
| 122 | + abstract protected function doFinish(); |
| 123 | +} |
| 124 | + |
| 125 | +/** |
| 126 | + * Store a file into the backend from a file on disk. |
| 127 | + * Parameters must match FileBackend::store(), which include: |
| 128 | + * source : source path on disk |
| 129 | + * dest : destination storage path |
| 130 | + * overwriteDest : do nothing and pass if an identical file exists at destination |
| 131 | + * overwriteSame : override any existing file at destination |
| 132 | + */ |
| 133 | +class FileStoreOp extends FileOp { |
| 134 | + /** @var TempLocalFile|null */ |
| 135 | + protected $tmpDestFile; // temp copy of existing destination file |
| 136 | + |
| 137 | + function doAttempt() { |
| 138 | + // Create a backup copy of any file that exists at destination |
| 139 | + $status = $this->backupDest(); |
| 140 | + if ( !$status->isOK() ) { |
| 141 | + return $status; |
| 142 | + } |
| 143 | + // Store the file at the destination |
| 144 | + $status = $this->backend->store( $this->params ); |
| 145 | + return $status; |
| 146 | + } |
| 147 | + |
| 148 | + function doRevert() { |
| 149 | + // Remove the file saved to the destination |
| 150 | + $params = array( 'source' => $this->params['dest'] ); |
| 151 | + $status = $this->backend->delete( $params ); |
| 152 | + if ( !$status->isOK() ) { |
| 153 | + return $status; // also can't restore any dest file |
| 154 | + } |
| 155 | + // Restore any file that was at the destination |
| 156 | + $status = $this->restoreDest(); |
| 157 | + return $status; |
| 158 | + } |
| 159 | + |
| 160 | + function doFinish() { |
| 161 | + return Status::newGood(); |
| 162 | + } |
| 163 | + |
| 164 | + function storagePathsToLock() { |
| 165 | + return array( $this->params['dest'] ); |
| 166 | + } |
| 167 | + |
| 168 | + /** |
| 169 | + * Backup any file at destination to a temporary file. |
| 170 | + * Don't bother backing it up unless we might overwrite the file. |
| 171 | + * |
| 172 | + * @return Status |
| 173 | + */ |
| 174 | + protected function backupDest() { |
| 175 | + $status = Status::newGood(); |
| 176 | + // Check if a file already exists at the destination... |
| 177 | + if ( $this->backend->fileExists( $this->params['dest'] ) ) { |
| 178 | + if ( $this->params['overwriteDest'] ) { |
| 179 | + // Create a temporary backup copy... |
| 180 | + $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] ); |
| 181 | + if ( !$this->tmpDestFile ) { |
| 182 | + $status->fatal( 'backend-fail-restore', $this->params['dest'] ); |
| 183 | + return $status; |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + return $status; |
| 188 | + } |
| 189 | + |
| 190 | + /** |
| 191 | + * Restore any temporary destination backup file |
| 192 | + * |
| 193 | + * @return Status |
| 194 | + */ |
| 195 | + protected function restoreDest() { |
| 196 | + $status = Status::newGood(); |
| 197 | + // Restore any file that was at the destination |
| 198 | + if ( $this->tmpDestFile ) { |
| 199 | + $params = array( |
| 200 | + 'source' => $this->tmpDestFile->getPath(), |
| 201 | + 'dest' => $this->params['dest'] |
| 202 | + ); |
| 203 | + $status = $this->backend->store( $params ); |
| 204 | + if ( !$status->isOK() ) { |
| 205 | + return $status; |
| 206 | + } |
| 207 | + } |
| 208 | + return $status; |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +/** |
| 213 | + * Create a file in the backend with the given content. |
| 214 | + * Parameters must match FileBackend::create(), which include: |
| 215 | + * content : a string of raw file contents |
| 216 | + * dest : destination storage path |
| 217 | + * overwriteDest : do nothing and pass if an identical file exists at destination |
| 218 | + * overwriteSame : override any existing file at destination |
| 219 | + */ |
| 220 | +class FileCreateOp extends FileStoreOp { |
| 221 | + function doAttempt() { |
| 222 | + // Create a backup copy of any file that exists at destination |
| 223 | + $status = $this->backupDest(); |
| 224 | + if ( !$status->isOK() ) { |
| 225 | + return $status; |
| 226 | + } |
| 227 | + // Create the file at the destination |
| 228 | + $status = $this->backend->create( $this->params ); |
| 229 | + return $status; |
| 230 | + } |
| 231 | + |
| 232 | + function doRevert() { |
| 233 | + // Remove the file saved to the destination |
| 234 | + $params = array( 'source' => $this->params['dest'] ); |
| 235 | + $status = $this->backend->delete( $params ); |
| 236 | + if ( !$status->isOK() ) { |
| 237 | + return $status; // also can't restore any dest file |
| 238 | + } |
| 239 | + // Restore any file that was at the destination |
| 240 | + $status = $this->restoreDest(); |
| 241 | + return $status; |
| 242 | + } |
| 243 | + |
| 244 | + function doFinish() { |
| 245 | + return Status::newGood(); |
| 246 | + } |
| 247 | + |
| 248 | + function storagePathsToLock() { |
| 249 | + return array( $this->params['dest'] ); |
| 250 | + } |
| 251 | +} |
| 252 | + |
| 253 | +/** |
| 254 | + * Copy a file from one storage path to another in the backend. |
| 255 | + * Parameters must match FileBackend::copy(), which include: |
| 256 | + * source : source storage path |
| 257 | + * dest : destination storage path |
| 258 | + * overwriteDest : do nothing and pass if an identical file exists at destination |
| 259 | + * overwriteSame : override any existing file at destination |
| 260 | + */ |
| 261 | +class FileCopyOp extends FileStoreOp { |
| 262 | + function doAttempt() { |
| 263 | + // Create a backup copy of any file that exists at destination |
| 264 | + $status = $this->backupDest(); |
| 265 | + if ( !$status->isOK() ) { |
| 266 | + return $status; |
| 267 | + } |
| 268 | + // Copy the file into the destination |
| 269 | + $status = $this->backend->copy( $this->params ); |
| 270 | + return $status; |
| 271 | + } |
| 272 | + |
| 273 | + function doRevert() { |
| 274 | + // Remove the file saved to the destination |
| 275 | + $params = array( 'source' => $this->params['dest'] ); |
| 276 | + $status = $this->backend->delete( $params ); |
| 277 | + if ( !$status->isOK() ) { |
| 278 | + return $status; // also can't restore any dest file |
| 279 | + } |
| 280 | + // Restore any file that was at the destination |
| 281 | + $status = $this->restoreDest(); |
| 282 | + return $status; |
| 283 | + } |
| 284 | + |
| 285 | + function doFinish() { |
| 286 | + return Status::newGood(); |
| 287 | + } |
| 288 | + |
| 289 | + function storagePathsToLock() { |
| 290 | + return array( $this->params['source'], $this->params['dest'] ); |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +/** |
| 295 | + * Move a file from one storage path to another in the backend. |
| 296 | + * Parameters must match FileBackend::move(), which include: |
| 297 | + * source : source storage path |
| 298 | + * dest : destination storage path |
| 299 | + * overwriteDest : do nothing and pass if an identical file exists at destination |
| 300 | + * overwriteSame : override any existing file at destination |
| 301 | + */ |
| 302 | +class FileMoveOp extends FileStoreOp { |
| 303 | + protected $usingMove = false; // using backend move() function? |
| 304 | + |
| 305 | + function initialize() { |
| 306 | + // Use faster, native, move() if applicable |
| 307 | + $this->usingMove = $this->backend->canMove( $this->params ); |
| 308 | + } |
| 309 | + |
| 310 | + function doAttempt() { |
| 311 | + // Create a backup copy of any file that exists at destination |
| 312 | + $status = $this->backupDest(); |
| 313 | + if ( !$status->isOK() ) { |
| 314 | + return $status; |
| 315 | + } |
| 316 | + // Native moves: move the file into the destination |
| 317 | + if ( $this->usingMove ) { |
| 318 | + $status = $this->backend->move( $this->params ); |
| 319 | + // Non-native moves: copy the file into the destination |
| 320 | + } else { |
| 321 | + $status = $this->backend->copy( $this->params ); |
| 322 | + } |
| 323 | + return $status; |
| 324 | + } |
| 325 | + |
| 326 | + function doRevert() { |
| 327 | + // Native moves: move the file back to the source |
| 328 | + if ( $this->usingMove ) { |
| 329 | + $params = array( |
| 330 | + 'source' => $this->params['dest'], |
| 331 | + 'dest' => $this->params['source'] |
| 332 | + ); |
| 333 | + $status = $this->backend->move( $params ); |
| 334 | + if ( !$status->isOK() ) { |
| 335 | + return $status; // also can't restore any dest file |
| 336 | + } |
| 337 | + // Non-native moves: remove the file saved to the destination |
| 338 | + } else { |
| 339 | + $params = array( 'source' => $this->params['dest'] ); |
| 340 | + $status = $this->backend->delete( $params ); |
| 341 | + if ( !$status->isOK() ) { |
| 342 | + return $status; // also can't restore any dest file |
| 343 | + } |
| 344 | + } |
| 345 | + // Restore any file that was at the destination |
| 346 | + $status = $this->restoreDest(); |
| 347 | + return $status; |
| 348 | + } |
| 349 | + |
| 350 | + function doFinish() { |
| 351 | + // Native moves: nothing is at the source anymore |
| 352 | + if ( $this->usingMove ) { |
| 353 | + $status = Status::newGood(); |
| 354 | + // Non-native moves: delete the source file |
| 355 | + } else { |
| 356 | + $params = array( 'source' => $this->params['source'] ); |
| 357 | + $status = $this->backend->delete( $params ); |
| 358 | + } |
| 359 | + return $status; |
| 360 | + } |
| 361 | + |
| 362 | + function storagePathsToLock() { |
| 363 | + return array( $this->params['source'], $this->params['dest'] ); |
| 364 | + } |
| 365 | +} |
| 366 | + |
| 367 | +/** |
| 368 | + * Combines files from severals storage paths into a new file in the backend. |
| 369 | + * Parameters must match FileBackend::concatenate(), which include: |
| 370 | + * sources : ordered source storage paths (e.g. chunk1,chunk2,...) |
| 371 | + * dest : destination storage path |
| 372 | + * overwriteDest : do nothing and pass if an identical file exists at destination |
| 373 | + * overwriteSame : override any existing file at destination |
| 374 | + */ |
| 375 | +class FileConcatenateOp extends FileStoreOp { |
| 376 | + function doAttempt() { |
| 377 | + // Create a backup copy of any file that exists at destination |
| 378 | + $status = $this->backupDest(); |
| 379 | + if ( !$status->isOK() ) { |
| 380 | + return $status; |
| 381 | + } |
| 382 | + // Concatenate the file at the destination |
| 383 | + $status = $this->backend->concatenate( $this->params ); |
| 384 | + return $status; |
| 385 | + } |
| 386 | + |
| 387 | + function doRevert() { |
| 388 | + // Remove the file saved to the destination |
| 389 | + $params = array( 'source' => $this->params['dest'] ); |
| 390 | + $status = $this->backend->delete( $params ); |
| 391 | + if ( !$status->isOK() ) { |
| 392 | + return $status; // also can't restore any dest file |
| 393 | + } |
| 394 | + // Restore any file that was at the destination |
| 395 | + $status = $this->restoreDest(); |
| 396 | + return $status; |
| 397 | + } |
| 398 | + |
| 399 | + function doFinish() { |
| 400 | + return Status::newGood(); |
| 401 | + } |
| 402 | + |
| 403 | + function storagePathsToLock() { |
| 404 | + return array_merge( $this->params['sources'], $this->params['dest'] ); |
| 405 | + } |
| 406 | +} |
| 407 | + |
| 408 | +/** |
| 409 | + * Delete a file at the storage path. |
| 410 | + * Parameters must match FileBackend::delete(), which include: |
| 411 | + * source : source storage path |
| 412 | + * ignoreMissingSource : don't return an error if the file does not exist |
| 413 | + */ |
| 414 | +class FileDeleteOp extends FileOp { |
| 415 | + function doAttempt() { |
| 416 | + $status = Status::newGood(); |
| 417 | + if ( !$this->params['ignoreMissingSource'] ) { |
| 418 | + if ( !$this->backend->fileExists( $this->params['source'] ) ) { |
| 419 | + $status->fatal( 'backend-fail-notexists', $this->params['source'] ); |
| 420 | + return $status; |
| 421 | + } |
| 422 | + } |
| 423 | + return $status; |
| 424 | + } |
| 425 | + |
| 426 | + function doRevert() { |
| 427 | + return Status::newGood(); |
| 428 | + } |
| 429 | + |
| 430 | + function doFinish() { |
| 431 | + // Delete the source file |
| 432 | + $status = $this->fileBackend->delete( $this->params ); |
| 433 | + return $status; |
| 434 | + } |
| 435 | + |
| 436 | + function storagePathsToLock() { |
| 437 | + return array( $this->params['source'] ); |
| 438 | + } |
| 439 | +} |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/FileOp.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 440 | + native |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php |
— | — | @@ -55,7 +55,10 @@ |
56 | 56 | return $status; |
57 | 57 | } |
58 | 58 | } else { |
59 | | - wfMkdirParents( $dest ); |
| 59 | + if ( !wfMkdirParents( $dest ) ) { |
| 60 | + $status->fatal( 'directorycreateerror', $param['dest'] ); |
| 61 | + return $status; |
| 62 | + } |
60 | 63 | } |
61 | 64 | |
62 | 65 | wfSuppressWarnings(); |
— | — | @@ -113,7 +116,10 @@ |
114 | 117 | return $status; |
115 | 118 | } |
116 | 119 | } else { |
117 | | - wfMkdirParents( $dest ); |
| 120 | + if ( !wfMkdirParents( $dest ) ) { |
| 121 | + $status->fatal( 'directorycreateerror', $param['dest'] ); |
| 122 | + return $status; |
| 123 | + } |
118 | 124 | } |
119 | 125 | |
120 | 126 | wfSuppressWarnings(); |
— | — | @@ -234,7 +240,10 @@ |
235 | 241 | } |
236 | 242 | } else { |
237 | 243 | // Make sure destination directory exists |
238 | | - wfMkdirParents( $dest ); |
| 244 | + if ( !wfMkdirParents( $dest ) ) { |
| 245 | + $status->fatal( 'directorycreateerror', $param['dest'] ); |
| 246 | + return $status; |
| 247 | + } |
239 | 248 | } |
240 | 249 | |
241 | 250 | // Rename the temporary file to the destination path |
— | — | @@ -251,6 +260,51 @@ |
252 | 261 | return $status; |
253 | 262 | } |
254 | 263 | |
| 264 | + function create( array $params ) { |
| 265 | + $status = Status::newGood(); |
| 266 | + |
| 267 | + list( $c, $dest ) = $this->resolveVirtualPath( $params['dest'] ); |
| 268 | + if ( $dest === null ) { |
| 269 | + $status->fatal( 'backend-fail-invalidpath', $params['dest'] ); |
| 270 | + return $status; |
| 271 | + } |
| 272 | + |
| 273 | + if ( file_exists( $dest ) ) { |
| 274 | + if ( isset( $params['overwriteDest'] ) ) { |
| 275 | + $ok = unlink( $dest ); |
| 276 | + if ( !$ok ) { |
| 277 | + $status->fatal( 'backend-fail-delete', $param['dest'] ); |
| 278 | + return $status; |
| 279 | + } |
| 280 | + } elseif ( isset( $params['overwriteSame'] ) ) { |
| 281 | + if ( !$this->fileAndDataAreSame( $dest, $params['content'] ) ) { |
| 282 | + $status->fatal( 'backend-fail-notsame-raw', $params['dest'] ); |
| 283 | + } |
| 284 | + return $status; // do nothing; either OK or bad status |
| 285 | + } else { |
| 286 | + $status->fatal( 'backend-fail-alreadyexists', $params['dest'] ); |
| 287 | + return $status; |
| 288 | + } |
| 289 | + } else { |
| 290 | + if ( !wfMkdirParents( $dest ) ) { |
| 291 | + $status->fatal( 'directorycreateerror', $param['dest'] ); |
| 292 | + return $status; |
| 293 | + } |
| 294 | + } |
| 295 | + |
| 296 | + wfSuppressWarnings(); |
| 297 | + $ok = file_put_contents( $dest, $params['content'] ); |
| 298 | + wfRestoreWarnings(); |
| 299 | + if ( !$ok ) { |
| 300 | + $status->fatal( 'backend-fail-create', $params['dest'] ); |
| 301 | + return $status; |
| 302 | + } |
| 303 | + |
| 304 | + $this->chmod( $dest ); |
| 305 | + |
| 306 | + return $status; |
| 307 | + } |
| 308 | + |
255 | 309 | function fileExists( array $params ) { |
256 | 310 | list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); |
257 | 311 | if ( $source === null ) { |
— | — | @@ -335,6 +389,20 @@ |
336 | 390 | } |
337 | 391 | |
338 | 392 | /** |
| 393 | + * Check if a file has identical contents as a string |
| 394 | + * |
| 395 | + * @param $path string Absolute filesystem path |
| 396 | + * @param $content string Raw file data |
| 397 | + * @return bool |
| 398 | + */ |
| 399 | + protected function fileAndDataAreSame( $path, $content ) { |
| 400 | + return ( // check size first since it's faster |
| 401 | + filesize( $path ) === strlen( $content ) && |
| 402 | + sha1_file( $path ) === sha1( $content ) |
| 403 | + ); |
| 404 | + } |
| 405 | + |
| 406 | + /** |
339 | 407 | * Chmod a file, suppressing the warnings |
340 | 408 | * |
341 | 409 | * @param $path string Absolute file system path |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php |
— | — | @@ -235,10 +235,10 @@ |
236 | 236 | /** |
237 | 237 | * Construct a new instance from configuration. |
238 | 238 | * $config paramaters include: |
239 | | - * 'serverMap' : Array of no more than 16 consecutive integer keys, |
240 | | - * starting from 0, with a list of servers as values. |
241 | | - * The first server in each list is the main server and |
242 | | - * the others are fallback servers. |
| 239 | + * 'serverMap' : Array of no more than 16 consecutive integer keys, |
| 240 | + * starting from 0, with a list of servers as values. |
| 241 | + * The first server in each list is the main server and |
| 242 | + * the others are fallback servers. |
243 | 243 | * |
244 | 244 | * @param Array $config |
245 | 245 | */ |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php |
— | — | @@ -70,6 +70,18 @@ |
71 | 71 | abstract public function doOperations( array $ops ); |
72 | 72 | |
73 | 73 | /** |
| 74 | + * Prepare a storage path for usage. This will create containers |
| 75 | + * that don't yet exists or, on FS backends, create parent directories. |
| 76 | + * Do not call this function from places outside FileBackend and FileOp. |
| 77 | + * $params include: |
| 78 | + * directory : destination storage path |
| 79 | + * |
| 80 | + * @param Array $params |
| 81 | + * @return Status |
| 82 | + */ |
| 83 | + abstract public function prepare( array $params ); |
| 84 | + |
| 85 | + /** |
74 | 86 | * Store a file into the backend from a file on disk. |
75 | 87 | * Do not call this function from places outside FileBackend and FileOp. |
76 | 88 | * $params include: |
— | — | @@ -139,6 +151,20 @@ |
140 | 152 | abstract public function concatenate( array $params ); |
141 | 153 | |
142 | 154 | /** |
| 155 | + * Create a file in the backend with the given contents. |
| 156 | + * Do not call this function from places outside FileBackend and FileOp. |
| 157 | + * $params include: |
| 158 | + * contents : the raw file contents |
| 159 | + * dest : destination storage path |
| 160 | + * overwriteDest : do nothing and pass if an identical file exists at destination |
| 161 | + * overwriteSame : override any existing file at destination |
| 162 | + * |
| 163 | + * @param Array $params |
| 164 | + * @return Status |
| 165 | + */ |
| 166 | + abstract public function create( array $params ); |
| 167 | + |
| 168 | + /** |
143 | 169 | * Whether this backend implements move() and is applies to a potential |
144 | 170 | * move from one storage path to another. No backends hits are required. |
145 | 171 | * For example, moving objects accross containers may not be supported. |
— | — | @@ -256,7 +282,8 @@ |
257 | 283 | 'copy' => 'FileCopyOp', |
258 | 284 | 'move' => 'FileMoveOp', |
259 | 285 | 'delete' => 'FileDeleteOp', |
260 | | - 'concatenate' => 'FileConcatenateOp' |
| 286 | + 'concatenate' => 'FileConcatenateOp', |
| 287 | + 'create' => 'FileCreateOp' |
261 | 288 | ); |
262 | 289 | } |
263 | 290 | |
— | — | @@ -374,395 +401,3 @@ |
375 | 402 | return $relStoragePath; |
376 | 403 | } |
377 | 404 | } |
378 | | - |
379 | | -/** |
380 | | - * Helper class for representing operations with transaction support. |
381 | | - * FileBackend::doOperations() will require these classes for supported operations. |
382 | | - * |
383 | | - * Access use of large fields should be avoided as we want to be able to support |
384 | | - * potentially many FileOp classes in large arrays in memory. |
385 | | - */ |
386 | | -abstract class FileOp { |
387 | | - /** $var Array */ |
388 | | - protected $params; |
389 | | - /** $var FileBackend */ |
390 | | - protected $backend; |
391 | | - |
392 | | - protected $state; |
393 | | - protected $failedAttempt; |
394 | | - |
395 | | - const STATE_NEW = 1; |
396 | | - const STATE_ATTEMPTED = 2; |
397 | | - const STATE_DONE = 3; |
398 | | - |
399 | | - /** |
400 | | - * Build a new file operation transaction |
401 | | - * |
402 | | - * @params $backend FileBackend |
403 | | - * @params $params Array |
404 | | - */ |
405 | | - final public function __construct( FileBackend $backend, array $params ) { |
406 | | - $this->backend = $backend; |
407 | | - $this->params = $params; |
408 | | - $this->state = self::STATE_NEW; |
409 | | - $this->failedAttempt = false; |
410 | | - $this->initialize(); |
411 | | - } |
412 | | - |
413 | | - /** |
414 | | - * Attempt the operation; this must be reversible |
415 | | - * |
416 | | - * @return Status |
417 | | - */ |
418 | | - final public function attempt() { |
419 | | - if ( $this->state !== self::STATE_NEW ) { |
420 | | - throw new MWException( "Cannot attempt operation called twice." ); |
421 | | - } |
422 | | - $this->state = self::STATE_ATTEMPTED; |
423 | | - $status = $this->doAttempt(); |
424 | | - if ( !$status->isOK() ) { |
425 | | - $this->failedAttempt = true; |
426 | | - } |
427 | | - return $status; |
428 | | - } |
429 | | - |
430 | | - /** |
431 | | - * Revert the operation; affected files are restored |
432 | | - * |
433 | | - * @return Status |
434 | | - */ |
435 | | - final public function revert() { |
436 | | - if ( $this->state !== self::STATE_ATTEMPTED ) { |
437 | | - throw new MWException( "Cannot rollback an unstarted or finished operation." ); |
438 | | - } |
439 | | - $this->state = self::STATE_DONE; |
440 | | - if ( $this->failedAttempt ) { |
441 | | - $status = Status::newGood(); // nothing to revert |
442 | | - } else { |
443 | | - $status = $this->doRevert(); |
444 | | - } |
445 | | - return $status; |
446 | | - } |
447 | | - |
448 | | - /** |
449 | | - * Finish the operation; this may be irreversible |
450 | | - * |
451 | | - * @return Status |
452 | | - */ |
453 | | - final public function finish() { |
454 | | - if ( $this->state !== self::STATE_ATTEMPTED ) { |
455 | | - throw new MWException( "Cannot cleanup an unstarted or finished operation." ); |
456 | | - } |
457 | | - $this->state = self::STATE_DONE; |
458 | | - if ( $this->failedAttempt ) { |
459 | | - $status = Status::newGood(); // nothing to revert |
460 | | - } else { |
461 | | - $status = $this->doFinish(); |
462 | | - } |
463 | | - return $status; |
464 | | - } |
465 | | - |
466 | | - /** |
467 | | - * Get a list of storage paths to lock for this operation |
468 | | - * |
469 | | - * @return Array |
470 | | - */ |
471 | | - public function storagePathsToLock() { |
472 | | - return array(); |
473 | | - } |
474 | | - |
475 | | - /** |
476 | | - * @return void |
477 | | - */ |
478 | | - protected function initialize() {} |
479 | | - |
480 | | - /** |
481 | | - * @return Status |
482 | | - */ |
483 | | - abstract protected function doAttempt(); |
484 | | - |
485 | | - /** |
486 | | - * @return Status |
487 | | - */ |
488 | | - abstract protected function doRevert(); |
489 | | - |
490 | | - /** |
491 | | - * @return Status |
492 | | - */ |
493 | | - abstract protected function doFinish(); |
494 | | -} |
495 | | - |
496 | | -/** |
497 | | - * Store a file into the backend from a file on disk. |
498 | | - * Parameters must match FileBackend::store(), which include: |
499 | | - * source : source path on disk |
500 | | - * dest : destination storage path |
501 | | - * overwriteDest : do nothing and pass if an identical file exists at destination |
502 | | - * overwriteSame : override any existing file at destination |
503 | | - */ |
504 | | -class FileStoreOp extends FileOp { |
505 | | - /** @var TempLocalFile|null */ |
506 | | - protected $tmpDestFile; // temp copy of existing destination file |
507 | | - |
508 | | - function doAttempt() { |
509 | | - // Create a backup copy of any file that exists at destination |
510 | | - $status = $this->backupDest(); |
511 | | - if ( !$status->isOK() ) { |
512 | | - return $status; |
513 | | - } |
514 | | - // Store the file at the destination |
515 | | - $status = $this->backend->store( $this->params ); |
516 | | - return $status; |
517 | | - } |
518 | | - |
519 | | - function doRevert() { |
520 | | - // Remove the file saved to the destination |
521 | | - $params = array( 'source' => $this->params['dest'] ); |
522 | | - $status = $this->backend->delete( $params ); |
523 | | - if ( !$status->isOK() ) { |
524 | | - return $status; // also can't restore any dest file |
525 | | - } |
526 | | - // Restore any file that was at the destination |
527 | | - $status = $this->restoreDest(); |
528 | | - return $status; |
529 | | - } |
530 | | - |
531 | | - function doFinish() { |
532 | | - return Status::newGood(); |
533 | | - } |
534 | | - |
535 | | - function storagePathsToLock() { |
536 | | - return array( $this->params['dest'] ); |
537 | | - } |
538 | | - |
539 | | - /** |
540 | | - * Backup any file at destination to a temporary file. |
541 | | - * Don't bother backing it up unless we might overwrite the file. |
542 | | - * |
543 | | - * @return Status |
544 | | - */ |
545 | | - protected function backupDest() { |
546 | | - $status = Status::newGood(); |
547 | | - // Check if a file already exists at the destination... |
548 | | - if ( $this->backend->fileExists( $this->params['dest'] ) ) { |
549 | | - if ( $this->params['overwriteDest'] ) { |
550 | | - // Create a temporary backup copy... |
551 | | - $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] ); |
552 | | - if ( !$this->tmpDestFile ) { |
553 | | - $status->fatal( 'backend-fail-restore', $this->params['dest'] ); |
554 | | - return $status; |
555 | | - } |
556 | | - } |
557 | | - } |
558 | | - return $status; |
559 | | - } |
560 | | - |
561 | | - /** |
562 | | - * Restore any temporary destination backup file |
563 | | - * |
564 | | - * @return Status |
565 | | - */ |
566 | | - protected function restoreDest() { |
567 | | - $status = Status::newGood(); |
568 | | - // Restore any file that was at the destination |
569 | | - if ( $this->tmpDestFile ) { |
570 | | - $params = array( |
571 | | - 'source' => $this->tmpDestFile->getPath(), |
572 | | - 'dest' => $this->params['dest'] |
573 | | - ); |
574 | | - $status = $this->backend->store( $params ); |
575 | | - if ( !$status->isOK() ) { |
576 | | - return $status; |
577 | | - } |
578 | | - } |
579 | | - return $status; |
580 | | - } |
581 | | -} |
582 | | - |
583 | | -/** |
584 | | - * Copy a file from one storage path to another in the backend. |
585 | | - * Parameters must match FileBackend::copy(), which include: |
586 | | - * source : source storage path |
587 | | - * dest : destination storage path |
588 | | - * overwriteDest : do nothing and pass if an identical file exists at destination |
589 | | - * overwriteSame : override any existing file at destination |
590 | | - */ |
591 | | -class FileCopyOp extends FileStoreOp { |
592 | | - function doAttempt() { |
593 | | - // Create a backup copy of any file that exists at destination |
594 | | - $status = $this->backupDest(); |
595 | | - if ( !$status->isOK() ) { |
596 | | - return $status; |
597 | | - } |
598 | | - // Copy the file into the destination |
599 | | - $status = $this->backend->copy( $this->params ); |
600 | | - return $status; |
601 | | - } |
602 | | - |
603 | | - function doRevert() { |
604 | | - // Remove the file saved to the destination |
605 | | - $params = array( 'source' => $this->params['dest'] ); |
606 | | - $status = $this->backend->delete( $params ); |
607 | | - if ( !$status->isOK() ) { |
608 | | - return $status; // also can't restore any dest file |
609 | | - } |
610 | | - // Restore any file that was at the destination |
611 | | - $status = $this->restoreDest(); |
612 | | - return $status; |
613 | | - } |
614 | | - |
615 | | - function doFinish() { |
616 | | - return Status::newGood(); |
617 | | - } |
618 | | - |
619 | | - function storagePathsToLock() { |
620 | | - return array( $this->params['source'], $this->params['dest'] ); |
621 | | - } |
622 | | -} |
623 | | - |
624 | | -/** |
625 | | - * Move a file from one storage path to another in the backend. |
626 | | - * Parameters must match FileBackend::move(), which include: |
627 | | - * source : source storage path |
628 | | - * dest : destination storage path |
629 | | - * overwriteDest : do nothing and pass if an identical file exists at destination |
630 | | - * overwriteSame : override any existing file at destination |
631 | | - */ |
632 | | -class FileMoveOp extends FileStoreOp { |
633 | | - protected $usingMove = false; // using backend move() function? |
634 | | - |
635 | | - function initialize() { |
636 | | - // Use faster, native, move() if applicable |
637 | | - $this->usingMove = $this->backend->canMove( $this->params ); |
638 | | - } |
639 | | - |
640 | | - function doAttempt() { |
641 | | - // Create a backup copy of any file that exists at destination |
642 | | - $status = $this->backupDest(); |
643 | | - if ( !$status->isOK() ) { |
644 | | - return $status; |
645 | | - } |
646 | | - // Native moves: move the file into the destination |
647 | | - if ( $this->usingMove ) { |
648 | | - $status = $this->backend->move( $this->params ); |
649 | | - // Non-native moves: copy the file into the destination |
650 | | - } else { |
651 | | - $status = $this->backend->copy( $this->params ); |
652 | | - } |
653 | | - return $status; |
654 | | - } |
655 | | - |
656 | | - function doRevert() { |
657 | | - // Native moves: move the file back to the source |
658 | | - if ( $this->usingMove ) { |
659 | | - $params = array( |
660 | | - 'source' => $this->params['dest'], |
661 | | - 'dest' => $this->params['source'] |
662 | | - ); |
663 | | - $status = $this->backend->move( $params ); |
664 | | - if ( !$status->isOK() ) { |
665 | | - return $status; // also can't restore any dest file |
666 | | - } |
667 | | - // Non-native moves: remove the file saved to the destination |
668 | | - } else { |
669 | | - $params = array( 'source' => $this->params['dest'] ); |
670 | | - $status = $this->backend->delete( $params ); |
671 | | - if ( !$status->isOK() ) { |
672 | | - return $status; // also can't restore any dest file |
673 | | - } |
674 | | - } |
675 | | - // Restore any file that was at the destination |
676 | | - $status = $this->restoreDest(); |
677 | | - return $status; |
678 | | - } |
679 | | - |
680 | | - function doFinish() { |
681 | | - // Native moves: nothing is at the source anymore |
682 | | - if ( $this->usingMove ) { |
683 | | - $status = Status::newGood(); |
684 | | - // Non-native moves: delete the source file |
685 | | - } else { |
686 | | - $params = array( 'source' => $this->params['source'] ); |
687 | | - $status = $this->backend->delete( $params ); |
688 | | - } |
689 | | - return $status; |
690 | | - } |
691 | | - |
692 | | - function storagePathsToLock() { |
693 | | - return array( $this->params['source'], $this->params['dest'] ); |
694 | | - } |
695 | | -} |
696 | | - |
697 | | -/** |
698 | | - * Combines files from severals storage paths into a new file in the backend. |
699 | | - * Parameters must match FileBackend::concatenate(), which include: |
700 | | - * sources : ordered source storage paths (e.g. chunk1,chunk2,...) |
701 | | - * dest : destination storage path |
702 | | - * overwriteDest : do nothing and pass if an identical file exists at destination |
703 | | - * overwriteSame : override any existing file at destination |
704 | | - */ |
705 | | -class FileConcatenateOp extends FileStoreOp { |
706 | | - function doAttempt() { |
707 | | - // Create a backup copy of any file that exists at destination |
708 | | - $status = $this->backupDest(); |
709 | | - if ( !$status->isOK() ) { |
710 | | - return $status; |
711 | | - } |
712 | | - // Concatenate the file at the destination |
713 | | - $status = $this->backend->concatenate( $this->params ); |
714 | | - return $status; |
715 | | - } |
716 | | - |
717 | | - function doRevert() { |
718 | | - // Remove the file saved to the destination |
719 | | - $params = array( 'source' => $this->params['dest'] ); |
720 | | - $status = $this->backend->delete( $params ); |
721 | | - if ( !$status->isOK() ) { |
722 | | - return $status; // also can't restore any dest file |
723 | | - } |
724 | | - // Restore any file that was at the destination |
725 | | - $status = $this->restoreDest(); |
726 | | - return $status; |
727 | | - } |
728 | | - |
729 | | - function doFinish() { |
730 | | - return Status::newGood(); |
731 | | - } |
732 | | - |
733 | | - function storagePathsToLock() { |
734 | | - return array_merge( $this->params['sources'], $this->params['dest'] ); |
735 | | - } |
736 | | -} |
737 | | - |
738 | | -/** |
739 | | - * Delete a file at the storage path. |
740 | | - * Parameters must match FileBackend::delete(), which include: |
741 | | - * source : source storage path |
742 | | - * ignoreMissingSource : don't return an error if the file does not exist |
743 | | - */ |
744 | | -class FileDeleteOp extends FileOp { |
745 | | - function doAttempt() { |
746 | | - $status = Status::newGood(); |
747 | | - if ( !$this->params['ignoreMissingSource'] ) { |
748 | | - if ( !$this->backend->fileExists( $this->params['source'] ) ) { |
749 | | - $status->fatal( 'backend-fail-notexists', $this->params['source'] ); |
750 | | - return $status; |
751 | | - } |
752 | | - } |
753 | | - return $status; |
754 | | - } |
755 | | - |
756 | | - function doRevert() { |
757 | | - return Status::newGood(); |
758 | | - } |
759 | | - |
760 | | - function doFinish() { |
761 | | - // Delete the source file |
762 | | - $status = $this->fileBackend->delete( $this->params ); |
763 | | - return $status; |
764 | | - } |
765 | | - |
766 | | - function storagePathsToLock() { |
767 | | - return array( $this->params['source'] ); |
768 | | - } |
769 | | -} |
Index: branches/FileBackend/phase3/includes/filerepo/FileRepo.php |
— | — | @@ -12,13 +12,19 @@ |
13 | 13 | * |
14 | 14 | * @ingroup FileRepo |
15 | 15 | */ |
16 | | -abstract class FileRepo { |
| 16 | +class FileRepo { |
17 | 17 | const FILES_ONLY = 1; |
18 | 18 | const DELETE_SOURCE = 1; |
19 | 19 | const OVERWRITE = 2; |
20 | 20 | const OVERWRITE_SAME = 4; |
21 | 21 | const SKIP_VALIDATION = 8; |
22 | 22 | |
| 23 | + /** @var FileBackend */ |
| 24 | + protected $backend; |
| 25 | + /** @var Array Map of zones to config */ |
| 26 | + protected $zones; |
| 27 | + |
| 28 | + protected $wikiKey; // unique wiki identifier |
23 | 29 | var $thumbScriptUrl, $transformVia404; |
24 | 30 | var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl; |
25 | 31 | var $fetchDescription, $initialCapital; |
— | — | @@ -35,22 +41,136 @@ |
36 | 42 | function __construct( $info ) { |
37 | 43 | // Required settings |
38 | 44 | $this->name = $info['name']; |
| 45 | + $this->url = $info['url']; |
39 | 46 | |
40 | | - // Optional settings |
41 | | - $this->initialCapital = MWNamespace::isCapitalized( NS_FILE ); |
42 | | - foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', |
43 | | - 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', |
44 | | - 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl', 'scriptExtension' ) |
45 | | - as $var ) |
46 | | - { |
| 47 | + // Optional settings that can have no value |
| 48 | + $optionalSettings = array( |
| 49 | + 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', |
| 50 | + 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry', |
| 51 | + 'scriptExtension' |
| 52 | + ); |
| 53 | + foreach ( $optionalSettings as $var ) { |
47 | 54 | if ( isset( $info[$var] ) ) { |
48 | 55 | $this->$var = $info[$var]; |
49 | 56 | } |
50 | 57 | } |
| 58 | + |
| 59 | + // Optional settings that have a default |
| 60 | + $this->initialCapital = isset( $info['initialCapital'] ) |
| 61 | + ? $info['initialCapital'] |
| 62 | + : MWNamespace::isCapitalized( NS_FILE ); |
| 63 | + $this->wikiKey = isset( $info['wikiKey'] ) |
| 64 | + ? $info['wikiKey'] |
| 65 | + : wfWikiID(); |
| 66 | + $this->thumbUrl = isset( $info['thumbUrl'] ) |
| 67 | + ? $info['thumbUrl'] |
| 68 | + : "{$this->url}/thumb"; |
| 69 | + $this->hashLevels = isset( $info['hashLevels'] ) |
| 70 | + ? $info['hashLevels'] |
| 71 | + : 2; |
| 72 | + $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) |
| 73 | + ? $info['deletedHashLevels'] |
| 74 | + : $this->hashLevels; |
51 | 75 | $this->transformVia404 = !empty( $info['transformVia404'] ); |
| 76 | + |
| 77 | + // New backend config style |
| 78 | + if ( isset( $info['backend'] ) ) { |
| 79 | + $this->backend = $info['backend']; |
| 80 | + $this->zones = $info['zones']; |
| 81 | + $prefix = strlen( $this->wikiKey ) ? "{$this->wikiKey}-" : ""; |
| 82 | + // Give defaults for basic zones... |
| 83 | + foreach ( array( 'public', 'thumb', 'archive', 'temp' ) as $zone ) { |
| 84 | + if ( !isset( $this->zones[$zone] ) ) { |
| 85 | + $this->zones[$zone] = array( |
| 86 | + 'container' => "{$prefix}{$zone}", |
| 87 | + 'directory' => '' // container root |
| 88 | + ); |
| 89 | + } |
| 90 | + } |
| 91 | + // Old fashioned backend (FS) config |
| 92 | + } else { |
| 93 | + $this->initLegacyConfig( $info ); |
| 94 | + } |
52 | 95 | } |
53 | 96 | |
54 | 97 | /** |
| 98 | + * Handle backend and zone settings for old config style |
| 99 | + * |
| 100 | + * @param $info Array |
| 101 | + * @return void |
| 102 | + */ |
| 103 | + protected function initLegacyConfig( $info ) { |
| 104 | + // Local vars that used to be FSRepo members... |
| 105 | + $directory = $info['directory']; |
| 106 | + $deletedDir = isset( $info['deletedDir'] ) |
| 107 | + ? $info['deletedDir'] |
| 108 | + : false; |
| 109 | + if ( isset( $info['thumbDir'] ) ) { |
| 110 | + $thumbDir = $info['thumbDir']; |
| 111 | + } else { |
| 112 | + $thumbDir = "{$directory}/thumb"; |
| 113 | + } |
| 114 | + $fileMode = isset( $info['fileMode'] ) |
| 115 | + ? $info['fileMode'] |
| 116 | + : 0644; |
| 117 | + |
| 118 | + // Get the FS backend from configuration... |
| 119 | + $prefix = strlen( $this->wikiKey ) ? "{$this->wikiKey}-" : ""; |
| 120 | + $config = array( |
| 121 | + 'name' => "{$this->name}-backend", |
| 122 | + 'lockManager' => new FSFileLockManager( |
| 123 | + array( 'lockDir' => "{$directory}/locks" ) |
| 124 | + ), |
| 125 | + 'containerPaths' => array( |
| 126 | + "{$prefix}public" => "{$directory}", |
| 127 | + "{$prefix}temp" => "{$directory}/temp", |
| 128 | + "{$prefix}deleted" => $deletedDir, |
| 129 | + "{$prefix}thumb" => $thumbDir |
| 130 | + ), |
| 131 | + 'fileMode' => $fileMode, |
| 132 | + ); |
| 133 | + $this->backend = new FSFileBackend( $config ); |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * Prepare all the zones for usage |
| 138 | + * |
| 139 | + * @param $doZone string Only do a particular zone |
| 140 | + * @return Status |
| 141 | + */ |
| 142 | + protected function initZones( $doZone = '' ) { |
| 143 | + $status = Status::newGood(); |
| 144 | + foreach( $this->zones as $zone => $info ) { |
| 145 | + if ( $doZone && $zone !== $doZone ) { |
| 146 | + continue; |
| 147 | + } |
| 148 | + $params = array( 'directory' => $this->getZonePath( $zone ) ); |
| 149 | + $status->merge( $this->backend->prepare( $params ) ); |
| 150 | + } |
| 151 | + return $status; |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * Take all available measures to prevent web accessibility of new deleted |
| 156 | + * directories, in case the user has not configured offline storage |
| 157 | + * @return void |
| 158 | + */ |
| 159 | + protected function initDeletedDir( $dir ) { |
| 160 | + if ( !( $this->backend instanceof FSFileBackend ) ) { |
| 161 | + return; // this is not an apache web dir |
| 162 | + } |
| 163 | + // Add a .htaccess file to the root of the deleted zone |
| 164 | + $root = $this->getZonePath( 'deleted' ); |
| 165 | + if ( !$this->backend->fileExists( array( 'source' => "$root/.htaccess" ) ) ) { |
| 166 | + $params = array( 'content' => "Deny from all\n", 'dest' => "$root/.htaccess" ); |
| 167 | + $this->backend->create( $params ); |
| 168 | + } |
| 169 | + // Seed new directories with a blank index.html, to prevent crawling |
| 170 | + $params = array( 'content' => "$dir/index.html", 'dest' => '' ); |
| 171 | + $this->backend->create( $params ); |
| 172 | + } |
| 173 | + |
| 174 | + /** |
55 | 175 | * Determine if a string is an mwrepo:// URL |
56 | 176 | * |
57 | 177 | * @param $url string |
— | — | @@ -62,6 +182,97 @@ |
63 | 183 | } |
64 | 184 | |
65 | 185 | /** |
| 186 | + * Get a URL referring to this repository, with the private mwrepo protocol. |
| 187 | + * The suffix, if supplied, is considered to be unencoded, and will be |
| 188 | + * URL-encoded before being returned. |
| 189 | + * |
| 190 | + * @param $suffix string |
| 191 | + * |
| 192 | + * @return string |
| 193 | + */ |
| 194 | + function getVirtualUrl( $suffix = false ) { |
| 195 | + $path = 'mwrepo://' . $this->name; |
| 196 | + if ( $suffix !== false ) { |
| 197 | + $path .= '/' . rawurlencode( $suffix ); |
| 198 | + } |
| 199 | + return $path; |
| 200 | + } |
| 201 | + |
| 202 | + /** |
| 203 | + * Get the URL corresponding to one of the four basic zones |
| 204 | + * @param $zone String: one of: public, deleted, temp, thumb |
| 205 | + * @return String or false |
| 206 | + */ |
| 207 | + function getZoneUrl( $zone ) { |
| 208 | + switch ( $zone ) { |
| 209 | + case 'public': |
| 210 | + return $this->url; |
| 211 | + case 'temp': |
| 212 | + return "{$this->url}/temp"; |
| 213 | + case 'deleted': |
| 214 | + return false; // no public URL |
| 215 | + case 'thumb': |
| 216 | + return $this->thumbUrl; |
| 217 | + default: |
| 218 | + return false; |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + /** |
| 223 | + * Get the backend storage path corresponding to a virtual URL |
| 224 | + * |
| 225 | + * @param $url string |
| 226 | + * |
| 227 | + * @return string |
| 228 | + */ |
| 229 | + function resolveVirtualUrl( $url ) { |
| 230 | + if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { |
| 231 | + throw new MWException( __METHOD__.': unknown protocol' ); |
| 232 | + } |
| 233 | + |
| 234 | + $bits = explode( '/', substr( $url, 9 ), 3 ); |
| 235 | + if ( count( $bits ) != 3 ) { |
| 236 | + throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); |
| 237 | + } |
| 238 | + list( $repo, $zone, $rel ) = $bits; |
| 239 | + if ( $repo !== $this->name ) { |
| 240 | + throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" ); |
| 241 | + } |
| 242 | + $base = $this->getZonePath( $zone ); |
| 243 | + if ( !$base ) { |
| 244 | + throw new MWException( __METHOD__.": invalid zone: $zone" ); |
| 245 | + } |
| 246 | + return $base . '/' . rawurldecode( $rel ); |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * The the storage container and base path of a zone |
| 251 | + * |
| 252 | + * @param $zone string |
| 253 | + * @return Array (container, base path) or (null, null) |
| 254 | + */ |
| 255 | + function getZoneLocation( $zone ) { |
| 256 | + if ( !isset( $this->zones[$zone] ) ) { |
| 257 | + return array( null, null ); // bogus |
| 258 | + } |
| 259 | + return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ); |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Get the storage path corresponding to one of the zones |
| 264 | + * |
| 265 | + * @param $zone string |
| 266 | + * @return string|null |
| 267 | + */ |
| 268 | + function getZonePath( $zone ) { |
| 269 | + list( $container, $base ) = $this->getZoneLocation( $zone ); |
| 270 | + if ( $container === null || $base === null ) { |
| 271 | + return null; |
| 272 | + } |
| 273 | + return "mwstore://{$backend}/{$container}/{$base}"; |
| 274 | + } |
| 275 | + |
| 276 | + /** |
66 | 277 | * Create a new File object from the local repository |
67 | 278 | * |
68 | 279 | * @param $title Mixed: Title object or string |
— | — | @@ -217,22 +428,29 @@ |
218 | 429 | } |
219 | 430 | |
220 | 431 | /** |
221 | | - * Get the URL of thumb.php |
| 432 | + * Get the public root URL of the repository |
| 433 | + * @return string |
222 | 434 | */ |
223 | | - function getThumbScriptUrl() { |
224 | | - return $this->thumbScriptUrl; |
| 435 | + function getRootUrl() { |
| 436 | + return $this->url; |
225 | 437 | } |
226 | 438 | |
227 | 439 | /** |
228 | | - * Get the URL corresponding to one of the four basic zones |
229 | | - * @param $zone String: one of: public, deleted, temp, thumb |
230 | | - * @return String or false |
| 440 | + * Returns true if the repository uses a multi-level directory structure |
| 441 | + * @return string |
231 | 442 | */ |
232 | | - function getZoneUrl( $zone ) { |
233 | | - return false; |
| 443 | + function isHashed() { |
| 444 | + return (bool)$this->hashLevels; |
234 | 445 | } |
235 | 446 | |
236 | 447 | /** |
| 448 | + * Get the URL of thumb.php |
| 449 | + */ |
| 450 | + function getThumbScriptUrl() { |
| 451 | + return $this->thumbScriptUrl; |
| 452 | + } |
| 453 | + |
| 454 | + /** |
237 | 455 | * Returns true if the repository can transform files via a 404 handler |
238 | 456 | * |
239 | 457 | * @return bool |
— | — | @@ -404,11 +622,131 @@ |
405 | 623 | /** |
406 | 624 | * Store a batch of files |
407 | 625 | * |
408 | | - * @param $triplets Array: (src,zone,dest) triplets as per store() |
| 626 | + * @param $triplets Array: (src,zone,dest rel) triplets as per store() |
409 | 627 | * @param $flags Integer: flags as per store |
| 628 | + * @return Status |
410 | 629 | */ |
411 | | - abstract function storeBatch( $triplets, $flags = 0 ); |
| 630 | + function storeBatch( $triplets, $flags = 0 ) { |
| 631 | + $backend = $this->backend; // convenience |
412 | 632 | |
| 633 | + wfDebug( __METHOD__ . ': Storing ' . count( $triplets ) . |
| 634 | + " triplets; flags: {$flags}\n" ); |
| 635 | + |
| 636 | + $status = $this->newGood(); |
| 637 | + // Try creating directories |
| 638 | + $status->merge( $this->initZones() ); |
| 639 | + if ( !$status->isOK() ) { |
| 640 | + return $status; |
| 641 | + } |
| 642 | + |
| 643 | + // Validate each triplet |
| 644 | + $status = $this->newGood(); |
| 645 | + foreach ( $triplets as $i => $triplet ) { |
| 646 | + list( $srcPath, $dstZone, $dstRel ) = $triplet; |
| 647 | + |
| 648 | + // Resolve destination path |
| 649 | + $root = $this->getZonePath( $dstZone ); |
| 650 | + if ( !$root ) { |
| 651 | + throw new MWException( "Invalid zone: $dstZone" ); |
| 652 | + } |
| 653 | + if ( !$this->validateFilename( $dstRel ) ) { |
| 654 | + throw new MWException( 'Validation error in $dstRel' ); |
| 655 | + } |
| 656 | + $dstPath = "$root/$dstRel"; |
| 657 | + $dstDir = dirname( $dstPath ); |
| 658 | + |
| 659 | + // Create destination directories for this triplet |
| 660 | + $status->merge( $backend->prepare( array( 'directory' => $dstDir ) ) ); |
| 661 | + if ( !$status->isOK() ) { |
| 662 | + return $status; |
| 663 | + } |
| 664 | + |
| 665 | + if ( $dstZone == 'deleted' ) { |
| 666 | + $this->initDeletedDir( $dstDir ); |
| 667 | + } |
| 668 | + |
| 669 | + // Resolve source |
| 670 | + if ( self::isVirtualUrl( $srcPath ) ) { |
| 671 | + $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath ); |
| 672 | + } |
| 673 | + if ( !$backend->fileExists( array( 'source' => $srcPath ) ) ) { |
| 674 | + // Make a list of files that don't exist for return to the caller |
| 675 | + $status->fatal( 'filenotfound', $srcPath ); |
| 676 | + continue; |
| 677 | + } |
| 678 | + |
| 679 | + // Check overwriting |
| 680 | + if ( !( $flags & self::OVERWRITE ) && |
| 681 | + $backend->fileExists( array( 'source' => $dstPath ) ) ) |
| 682 | + { |
| 683 | + if ( $flags & self::OVERWRITE_SAME ) { |
| 684 | + $hashSource = sha1_file( $srcPath ); |
| 685 | + $hashDest = sha1_file( $dstPath ); |
| 686 | + if ( $hashSource != $hashDest ) { |
| 687 | + $status->fatal( 'fileexistserror', $dstPath ); |
| 688 | + $status->failCount++; |
| 689 | + } |
| 690 | + } else { |
| 691 | + $status->fatal( 'fileexistserror', $dstPath ); |
| 692 | + $status->failCount++; |
| 693 | + } |
| 694 | + } |
| 695 | + } |
| 696 | + |
| 697 | + // Windows does not support moving over existing files, so explicitly delete them |
| 698 | + $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE ); |
| 699 | + |
| 700 | + // Abort now on failure |
| 701 | + if ( !$status->ok ) { |
| 702 | + return $status; |
| 703 | + } |
| 704 | + |
| 705 | + // Execute the store operation for each triplet |
| 706 | + foreach ( $triplets as $i => $triplet ) { |
| 707 | + list( $srcPath, $dstZone, $dstRel ) = $triplet; |
| 708 | + $root = $this->getZonePath( $dstZone ); |
| 709 | + $dstPath = "$root/$dstRel"; |
| 710 | + $good = true; |
| 711 | + |
| 712 | + if ( $flags & self::DELETE_SOURCE ) { |
| 713 | + if ( $deleteDest ) { |
| 714 | + unlink( $dstPath ); |
| 715 | + } |
| 716 | + if ( !rename( $srcPath, $dstPath ) ) { |
| 717 | + $status->error( 'filerenameerror', $srcPath, $dstPath ); |
| 718 | + $good = false; |
| 719 | + } |
| 720 | + } else { |
| 721 | + if ( !copy( $srcPath, $dstPath ) ) { |
| 722 | + $status->error( 'filecopyerror', $srcPath, $dstPath ); |
| 723 | + $good = false; |
| 724 | + } |
| 725 | + if ( !( $flags & self::SKIP_VALIDATION ) ) { |
| 726 | + wfSuppressWarnings(); |
| 727 | + $hashSource = sha1_file( $srcPath ); |
| 728 | + $hashDest = sha1_file( $dstPath ); |
| 729 | + wfRestoreWarnings(); |
| 730 | + |
| 731 | + if ( $hashDest === false || $hashSource !== $hashDest ) { |
| 732 | + wfDebug( __METHOD__ . ': File copy validation failed: ' . |
| 733 | + "$srcPath ($hashSource) to $dstPath ($hashDest)\n" ); |
| 734 | + |
| 735 | + $status->error( 'filecopyerror', $srcPath, $dstPath ); |
| 736 | + $good = false; |
| 737 | + } |
| 738 | + } |
| 739 | + } |
| 740 | + if ( $good ) { |
| 741 | + $this->chmod( $dstPath ); |
| 742 | + $status->successCount++; |
| 743 | + } else { |
| 744 | + $status->failCount++; |
| 745 | + } |
| 746 | + $status->success[$i] = $good; |
| 747 | + } |
| 748 | + return $status; |
| 749 | + } |
| 750 | + |
413 | 751 | /** |
414 | 752 | * Pick a random name in the temp zone and store a file to it. |
415 | 753 | * Returns a FileRepoStatus object with the URL in the value. |