Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php |
— | — | @@ -234,6 +234,16 @@ |
235 | 235 | return Status::newFatal( 'backend-fail-stream', $params['src'] ); |
236 | 236 | } |
237 | 237 | |
| 238 | + function getLocalReference( array $params ) { |
| 239 | + foreach ( $this->backends as $backend ) { |
| 240 | + $fsFile = $backend->getLocalReference( $params ); |
| 241 | + if ( $fsFile ) { |
| 242 | + return $fsFile; |
| 243 | + } |
| 244 | + } |
| 245 | + return null; |
| 246 | + } |
| 247 | + |
238 | 248 | function getLocalCopy( array $params ) { |
239 | 249 | foreach ( $this->backends as $backend ) { |
240 | 250 | $tmpFile = $backend->getLocalCopy( $params ); |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php |
— | — | @@ -40,7 +40,7 @@ |
41 | 41 | $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); |
42 | 42 | return $status; |
43 | 43 | } |
44 | | - if ( is_file( $dest ) ) { |
| 44 | + if ( file_exists( $dest ) ) { |
45 | 45 | if ( !empty( $params['overwriteDest'] ) ) { |
46 | 46 | wfSuppressWarnings(); |
47 | 47 | $ok = unlink( $dest ); |
— | — | @@ -88,7 +88,7 @@ |
89 | 89 | return $status; |
90 | 90 | } |
91 | 91 | |
92 | | - if ( is_file( $dest ) ) { |
| 92 | + if ( file_exists( $dest ) ) { |
93 | 93 | if ( !empty( $params['overwriteDest'] ) ) { |
94 | 94 | wfSuppressWarnings(); |
95 | 95 | $ok = unlink( $dest ); |
— | — | @@ -237,20 +237,25 @@ |
238 | 238 | foreach ( $params['srcs'] as $virtualSource ) { |
239 | 239 | list( $c, $source ) = $this->resolveStoragePath( $virtualSource ); |
240 | 240 | if ( $source === null ) { |
| 241 | + fclose( $tmpHandle ); |
241 | 242 | $status->fatal( 'backend-fail-invalidpath', $virtualSource ); |
242 | 243 | return $status; |
243 | 244 | } |
244 | 245 | // Load chunk into memory (it should be a small file) |
245 | | - $chunk = file_get_contents( $source ); |
246 | | - if ( $chunk === false ) { |
| 246 | + $sourceHandle = fopen( $source, 'r' ); |
| 247 | + if ( $sourceHandle === false ) { |
| 248 | + fclose( $tmpHandle ); |
247 | 249 | $status->fatal( 'backend-fail-read', $virtualSource ); |
248 | 250 | return $status; |
249 | 251 | } |
250 | 252 | // Append chunk to file (pass chunk size to avoid magic quotes) |
251 | | - if ( !fwrite( $tmpHandle, $chunk, count( $chunk ) ) ) { |
| 253 | + if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) { |
| 254 | + fclose( $sourceHandle ); |
| 255 | + fclose( $tmpHandle ); |
252 | 256 | $status->fatal( 'backend-fail-writetemp', $tmpPath ); |
253 | 257 | return $status; |
254 | 258 | } |
| 259 | + fclose( $sourceHandle ); |
255 | 260 | } |
256 | 261 | wfSuppressWarnings(); |
257 | 262 | if ( !fclose( $tmpHandle ) ) { |
— | — | @@ -451,25 +456,12 @@ |
452 | 457 | return new FSFileIterator( $dir ); |
453 | 458 | } |
454 | 459 | |
455 | | - function streamFile( array $params ) { |
456 | | - $status = Status::newGood(); |
457 | | - |
| 460 | + function getLocalReference( array $params ) { |
458 | 461 | list( $c, $source ) = $this->resolveStoragePath( $params['src'] ); |
459 | 462 | if ( $source === null ) { |
460 | | - $status->fatal( 'backend-fail-invalidpath', $params['src'] ); |
461 | | - return $status; |
| 463 | + return null; |
462 | 464 | } |
463 | | - |
464 | | - $extraHeaders = isset( $params['headers'] ) |
465 | | - ? $params['headers'] |
466 | | - : array(); |
467 | | - $ok = StreamFile::stream( $source, $extraHeaders, false ); |
468 | | - if ( !$ok ) { |
469 | | - $status->fatal( 'backend-fail-stream', $params['src'] ); |
470 | | - return $status; |
471 | | - } |
472 | | - |
473 | | - return $status; |
| 465 | + return new FSFile( $source ); |
474 | 466 | } |
475 | 467 | |
476 | 468 | function getLocalCopy( array $params ) { |
Index: branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php |
— | — | @@ -134,12 +134,12 @@ |
135 | 135 | $this->locksHeld[$key][$type] = 1; |
136 | 136 | } else { |
137 | 137 | wfSuppressWarnings(); |
138 | | - $handle = fopen( $this->getLockPath( $key ), 'c' ); |
| 138 | + $handle = fopen( $this->getLockPath( $key ), 'a+' ); |
139 | 139 | wfRestoreWarnings(); |
140 | 140 | if ( !$handle ) { // lock dir missing? |
141 | 141 | wfMkdirParents( $this->lockDir ); |
142 | 142 | wfSuppressWarnings(); |
143 | | - $handle = fopen( $this->getLockPath( $key ), 'c' ); // try again |
| 143 | + $handle = fopen( $this->getLockPath( $key ), 'a+' ); // try again |
144 | 144 | wfRestoreWarnings(); |
145 | 145 | } |
146 | 146 | if ( $handle ) { |
— | — | @@ -176,6 +176,7 @@ |
177 | 177 | } elseif ( $type && !isset( $this->locksHeld[$key][$type] ) ) { |
178 | 178 | $status->warning( 'lockmanager-notlocked', $key ); |
179 | 179 | } else { |
| 180 | + $handlesToClose = array(); |
180 | 181 | foreach ( $this->locksHeld[$key] as $lockType => $count ) { |
181 | 182 | if ( $type && $lockType != $type ) { |
182 | 183 | continue; // only unlock locks of type $type |
— | — | @@ -186,33 +187,60 @@ |
187 | 188 | // If a LOCK_SH comes in while we have a LOCK_EX, we don't |
188 | 189 | // actually add a handler, so check for handler existence. |
189 | 190 | if ( isset( $this->handles[$key][$lockType] ) ) { |
190 | | - wfSuppressWarnings(); |
191 | | - if ( !flock( $this->handles[$key][$lockType], LOCK_UN ) ) { |
192 | | - $status->fatal( 'lockmanager-fail-releaselock', $key ); |
193 | | - } |
194 | | - if ( !fclose( $this->handles[$key][$lockType] ) ) { |
195 | | - $status->warning( 'lockmanager-fail-closelock', $key ); |
196 | | - } |
197 | | - wfRestoreWarnings(); |
| 191 | + // Mark this handle to be unlocked and closed |
| 192 | + $handlesToClose[] = $this->handles[$key][$lockType]; |
198 | 193 | unset( $this->handles[$key][$lockType] ); |
199 | 194 | } |
200 | 195 | } |
201 | 196 | } |
202 | | - if ( !count( $this->locksHeld[$key] ) ) { |
203 | | - wfSuppressWarnings(); |
204 | | - # No locks are held for the lock file anymore |
205 | | - if ( !unlink( $this->getLockPath( $key ) ) ) { |
206 | | - $status->warning( 'lockmanager-fail-deletelock', $key ); |
207 | | - } |
208 | | - wfRestoreWarnings(); |
209 | | - unset( $this->locksHeld[$key] ); |
210 | | - unset( $this->handles[$key] ); |
| 197 | + // Unlock handles to release locks and delete |
| 198 | + // any lock files that end up with no locks on them... |
| 199 | + if ( wfIsWindows() ) { |
| 200 | + // Windows: for any process, including this one, |
| 201 | + // calling unlink() on a locked file will fail |
| 202 | + $status->merge( $this->closeLockHandles( $key, $handlesToClose ) ); |
| 203 | + $status->merge( $this->pruneKeyLockFiles( $key ) ); |
| 204 | + } else { |
| 205 | + // Unix: unlink() can be used on files currently open by this |
| 206 | + // process and we must do so in order to avoid race conditions |
| 207 | + $status->merge( $this->pruneKeyLockFiles( $key ) ); |
| 208 | + $status->merge( $this->closeLockHandles( $key, $handlesToClose ) ); |
211 | 209 | } |
212 | 210 | } |
213 | 211 | |
214 | 212 | return $status; |
215 | 213 | } |
216 | 214 | |
| 215 | + private function closeLockHandles( $key, array $handlesToClose ) { |
| 216 | + $status = Status::newGood(); |
| 217 | + foreach ( $handlesToClose as $handle ) { |
| 218 | + wfSuppressWarnings(); |
| 219 | + if ( !flock( $handle, LOCK_UN ) ) { |
| 220 | + $status->fatal( 'lockmanager-fail-releaselock', $key ); |
| 221 | + } |
| 222 | + if ( !fclose( $handle ) ) { |
| 223 | + $status->warning( 'lockmanager-fail-closelock', $key ); |
| 224 | + } |
| 225 | + wfRestoreWarnings(); |
| 226 | + } |
| 227 | + return $status; |
| 228 | + } |
| 229 | + |
| 230 | + private function pruneKeyLockFiles( $key ) { |
| 231 | + $status = Status::newGood(); |
| 232 | + if ( !count( $this->locksHeld[$key] ) ) { |
| 233 | + wfSuppressWarnings(); |
| 234 | + # No locks are held for the lock file anymore |
| 235 | + if ( !unlink( $this->getLockPath( $key ) ) ) { |
| 236 | + $status->warning( 'lockmanager-fail-deletelock', $key ); |
| 237 | + } |
| 238 | + wfRestoreWarnings(); |
| 239 | + unset( $this->locksHeld[$key] ); |
| 240 | + unset( $this->handles[$key] ); |
| 241 | + } |
| 242 | + return $status; |
| 243 | + } |
| 244 | + |
217 | 245 | /** |
218 | 246 | * Get the path to the lock file for a key |
219 | 247 | * @param $key string |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php |
— | — | @@ -206,13 +206,34 @@ |
207 | 207 | abstract public function getFileList( array $params ); |
208 | 208 | |
209 | 209 | /** |
| 210 | + * Returns a file system file, identical to the file at a storage path. |
| 211 | + * The file return is either: |
| 212 | + * a) A local copy of the file at a storage path in the backend. |
| 213 | + * The temporary copy will have the same extension as the source. |
| 214 | + * b) An original of the file at a storage path in the backend. |
| 215 | + * |
| 216 | + * Write operations should *never* be done on this file as some backends |
| 217 | + * may do internal tracking or may be instances of FileBackendMultiWrite. |
| 218 | + * In that later case, there are copies of the file that must stay in sync. |
| 219 | + * |
| 220 | + * $params include: |
| 221 | + * src : source storage path |
| 222 | + * |
| 223 | + * @param Array $params |
| 224 | + * @return FSFile|null Returns null on failure |
| 225 | + */ |
| 226 | + public function getLocalReference( array $params ) { |
| 227 | + return $this->getLocalCopy( $params ); |
| 228 | + } |
| 229 | + |
| 230 | + /** |
210 | 231 | * Get a local copy on disk of the file at a storage path in the backend. |
211 | | - * The temporary copy should have the same file extension as the source. |
| 232 | + * The temporary copy will have the same file extension as the source. |
212 | 233 | * $params include: |
213 | 234 | * src : source storage path |
214 | 235 | * |
215 | 236 | * @param Array $params |
216 | | - * @return TempFSFile|null Temporary file or null on failure |
| 237 | + * @return TempFSFile|null Returns null on failure |
217 | 238 | */ |
218 | 239 | abstract public function getLocalCopy( array $params ); |
219 | 240 | |
— | — | @@ -355,14 +376,36 @@ |
356 | 377 | } |
357 | 378 | |
358 | 379 | public function getFileProps( array $params ) { |
359 | | - $tmpFile = $this->getLocalCopy( $params ); |
360 | | - if ( !$tmpFile ) { |
| 380 | + $fsFile = $this->getLocalReference( array( 'src' => $params['src'] ) ); |
| 381 | + if ( !$fsFile ) { |
361 | 382 | return FSFile::placeholderProps(); |
362 | 383 | } else { |
363 | | - return $tmpFile->getProps(); |
| 384 | + return $fsFile->getProps(); |
364 | 385 | } |
365 | 386 | } |
366 | 387 | |
| 388 | + function streamFile( array $params ) { |
| 389 | + $status = Status::newGood(); |
| 390 | + |
| 391 | + $fsFile = $this->getLocalReference( array( 'src' => $params['src'] ) ); |
| 392 | + if ( !$fsFile ) { |
| 393 | + $status->fatal( 'backend-fail-stream', $params['src'] ); |
| 394 | + return $status; |
| 395 | + } |
| 396 | + |
| 397 | + $extraHeaders = isset( $params['headers'] ) |
| 398 | + ? $params['headers'] |
| 399 | + : array(); |
| 400 | + |
| 401 | + $ok = StreamFile::stream( $fsFile->getPath(), $extraHeaders, false ); |
| 402 | + if ( !$ok ) { |
| 403 | + $status->fatal( 'backend-fail-stream', $params['src'] ); |
| 404 | + return $status; |
| 405 | + } |
| 406 | + |
| 407 | + return $status; |
| 408 | + } |
| 409 | + |
367 | 410 | /** |
368 | 411 | * Get the list of supported operations and their corresponding FileOp classes. |
369 | 412 | * |