44use Wikimedia\AtEase\AtEase;
45use Wikimedia\Timestamp\ConvertibleTimestamp;
99 parent::__construct( $config );
101 if ( PHP_OS_FAMILY ===
'Windows' ) {
102 $this->os =
'Windows';
103 } elseif ( PHP_OS_FAMILY ===
'BSD' || PHP_OS_FAMILY ===
'Darwin' ) {
109 if ( isset( $config[
'basePath'] ) ) {
110 $this->basePath = rtrim( $config[
'basePath'],
'/' );
112 $this->basePath =
null;
115 $this->containerPaths = [];
116 foreach ( ( $config[
'containerPaths'] ?? [] ) as $container => $fsPath ) {
117 $this->containerPaths[$container] = rtrim( $fsPath,
'/' );
120 $this->fileMode = $config[
'fileMode'] ?? 0644;
121 $this->dirMode = $config[
'directoryMode'] ?? 0777;
122 if ( isset( $config[
'fileOwner'] ) && function_exists(
'posix_getuid' ) ) {
123 $this->fileOwner = $config[
'fileOwner'];
125 $this->currentUser = posix_getpwuid( posix_getuid() )[
'name'];
128 $this->usableDirCache =
new MapCacheLRU( self::CACHE_CHEAP_SIZE );
137 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
140 return $relStoragePath;
155 if ( preg_match(
'![^/]{256}!', $fsPath ) ) {
158 if ( $this->os ===
'Windows' ) {
159 return !preg_match(
'![:*?"<>|]!', $fsPath );
174 if ( isset( $this->containerPaths[$shortCont] ) ) {
175 return $this->containerPaths[$shortCont];
176 } elseif ( isset( $this->basePath ) ) {
177 return "{$this->basePath}/{$fullCont}";
191 if ( $relPath ===
null ) {
196 if ( $relPath !=
'' ) {
197 $fsPath .=
"/{$relPath}";
205 if ( $fsPath ===
null ) {
209 if ( $this->fileOwner !==
null && $this->currentUser !== $this->fileOwner ) {
210 trigger_error( __METHOD__ .
": PHP process owner is not '{$this->fileOwner}'." );
214 $fsDirectory = dirname( $fsPath );
215 $usable = $this->usableDirCache->get( $fsDirectory, MapCacheLRU::TTL_PROC_SHORT );
216 if ( $usable ===
null ) {
217 AtEase::suppressWarnings();
218 $usable = is_dir( $fsDirectory ) && is_writable( $fsDirectory );
219 AtEase::restoreWarnings();
220 $this->usableDirCache->set( $fsDirectory, $usable ? 1 : 0 );
230 if ( $fsDstPath ===
null ) {
231 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
236 if ( !empty( $params[
'async'] ) ) {
239 $status->fatal(
'backend-fail-create', $params[
'dst'] );
243 $cmd = $this->
makeCopyCommand( $tempFile->getPath(), $fsDstPath,
false );
244 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
245 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
246 $status->fatal(
'backend-fail-create', $params[
'dst'] );
247 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
250 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
251 $tempFile->bind( $status->value );
259 $stageHandle = fopen( $fsStagePath,
'xb' );
260 if ( $stageHandle ) {
261 $bytes = fwrite( $stageHandle, $params[
'content'] );
262 $created = ( $bytes === strlen( $params[
'content'] ) );
263 fclose( $stageHandle );
264 $created = $created ? rename( $fsStagePath, $fsDstPath ) :
false;
267 if ( $hadError || !$created ) {
268 $status->fatal(
'backend-fail-create', $params[
'dst'] );
272 $this->
chmod( $fsDstPath );
281 $fsSrcPath = $params[
'src'];
283 if ( $fsDstPath ===
null ) {
284 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
289 if ( $fsSrcPath === $fsDstPath ) {
290 $status->fatal(
'backend-fail-internal', $this->name );
295 if ( !empty( $params[
'async'] ) ) {
297 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
298 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
299 $status->fatal(
'backend-fail-store', $params[
'src'], $params[
'dst'] );
300 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
303 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
311 $srcHandle = fopen( $fsSrcPath,
'rb' );
313 $stageHandle = fopen( $fsStagePath,
'xb' );
314 if ( $stageHandle ) {
315 $bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
316 $stored = ( $bytes !==
false && $bytes === fstat( $srcHandle )[
'size'] );
317 fclose( $stageHandle );
318 $stored = $stored ? rename( $fsStagePath, $fsDstPath ) :
false;
320 fclose( $srcHandle );
323 if ( $hadError || !$stored ) {
324 $status->fatal(
'backend-fail-store', $params[
'src'], $params[
'dst'] );
328 $this->
chmod( $fsDstPath );
338 if ( $fsSrcPath ===
null ) {
339 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
345 if ( $fsDstPath ===
null ) {
346 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
351 if ( $fsSrcPath === $fsDstPath ) {
355 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
357 if ( !empty( $params[
'async'] ) ) {
358 $cmd = $this->
makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
359 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
360 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
361 $status->fatal(
'backend-fail-copy', $params[
'src'], $params[
'dst'] );
362 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
365 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
373 $srcHandle = fopen( $fsSrcPath,
'rb' );
375 $stageHandle = fopen( $fsStagePath,
'xb' );
376 if ( $stageHandle ) {
377 $bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
378 $copied = ( $bytes !==
false && $bytes === fstat( $srcHandle )[
'size'] );
379 fclose( $stageHandle );
380 $copied = $copied ? rename( $fsStagePath, $fsDstPath ) :
false;
382 fclose( $srcHandle );
385 if ( $hadError || ( !$copied && !$ignoreMissing ) ) {
386 $status->fatal(
'backend-fail-copy', $params[
'src'], $params[
'dst'] );
391 $this->
chmod( $fsDstPath );
402 if ( $fsSrcPath ===
null ) {
403 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
409 if ( $fsDstPath ===
null ) {
410 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
415 if ( $fsSrcPath === $fsDstPath ) {
419 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
421 if ( !empty( $params[
'async'] ) ) {
422 $cmd = $this->
makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
423 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
424 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
425 $status->fatal(
'backend-fail-move', $params[
'src'], $params[
'dst'] );
426 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
429 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
435 $moved = rename( $fsSrcPath, $fsDstPath );
437 if ( $hadError || ( !$moved && !$ignoreMissing ) ) {
438 $status->fatal(
'backend-fail-move', $params[
'src'], $params[
'dst'] );
451 if ( $fsSrcPath ===
null ) {
452 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
457 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
459 if ( !empty( $params[
'async'] ) ) {
461 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
462 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
463 $status->fatal(
'backend-fail-delete', $params[
'src'] );
464 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
467 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
470 $deleted =
unlink( $fsSrcPath );
472 if ( $hadError || ( !$deleted && !$ignoreMissing ) ) {
473 $status->fatal(
'backend-fail-delete', $params[
'src'] );
492 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
495 AtEase::suppressWarnings();
496 $alreadyExisted = is_dir( $fsDirectory );
497 if ( !$alreadyExisted ) {
498 $created = mkdir( $fsDirectory, $this->dirMode,
true );
500 $alreadyExisted = is_dir( $fsDirectory );
503 $isWritable = $created ?: is_writable( $fsDirectory );
504 AtEase::restoreWarnings();
505 if ( !$alreadyExisted && !$created ) {
506 $this->logger->error( __METHOD__ .
": cannot create directory $fsDirectory" );
507 $status->fatal(
'directorycreateerror', $params[
'dir'] );
508 } elseif ( !$isWritable ) {
509 $this->logger->error( __METHOD__ .
": directory $fsDirectory is read-only" );
510 $status->fatal(
'directoryreadonlyerror', $params[
'dir'] );
517 if ( $status->isOK() ) {
518 $this->usableDirCache->set( $fsDirectory, 1 );
528 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
530 if ( !empty( $params[
'noListing'] ) && !is_file(
"{$fsDirectory}/index.html" ) ) {
532 $bytes = file_put_contents(
"{$fsDirectory}/index.html", $this->
indexHtmlPrivate() );
534 if ( $bytes ===
false ) {
535 $status->fatal(
'backend-fail-create', $params[
'dir'] .
'/index.html' );
539 if ( !empty( $params[
'noAccess'] ) && !is_file(
"{$contRoot}/.htaccess" ) ) {
540 AtEase::suppressWarnings();
541 $bytes = file_put_contents(
"{$contRoot}/.htaccess", $this->
htaccessPrivate() );
542 AtEase::restoreWarnings();
543 if ( $bytes ===
false ) {
544 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
545 $status->fatal(
'backend-fail-create',
"{$storeDir}/.htaccess" );
556 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
558 if ( !empty( $params[
'listing'] ) && is_file(
"{$fsDirectory}/index.html" ) ) {
559 $exists = ( file_get_contents(
"{$fsDirectory}/index.html" ) === $this->
indexHtmlPrivate() );
560 if ( $exists && !$this->
unlink(
"{$fsDirectory}/index.html" ) ) {
561 $status->fatal(
'backend-fail-delete', $params[
'dir'] .
'/index.html' );
565 if ( !empty( $params[
'access'] ) && is_file(
"{$contRoot}/.htaccess" ) ) {
566 $exists = ( file_get_contents(
"{$contRoot}/.htaccess" ) === $this->
htaccessPrivate() );
567 if ( $exists && !$this->
unlink(
"{$contRoot}/.htaccess" ) ) {
568 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
569 $status->fatal(
'backend-fail-delete',
"{$storeDir}/.htaccess" );
580 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
582 $this->
rmdir( $fsDirectory );
589 if ( $fsSrcPath ===
null ) {
594 $stat = is_file( $fsSrcPath ) ? stat( $fsSrcPath ) :
false;
597 if ( is_array( $stat ) ) {
598 $ct =
new ConvertibleTimestamp( $stat[
'mtime'] );
601 'mtime' => $ct->getTimestamp( TS_MW ),
602 'size' => $stat[
'size']
610 if ( is_array( $paths ) ) {
611 foreach ( $paths as
$path ) {
613 if ( $fsPath !==
null ) {
614 clearstatcache(
true, $fsPath );
615 $this->usableDirCache->clear( $fsPath );
619 clearstatcache(
true );
620 $this->usableDirCache->clear();
627 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
630 $exists = is_dir( $fsDirectory );
646 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
649 $error = $list->getLastError();
650 if ( $error !==
null ) {
652 $this->logger->info( __METHOD__ .
": non-existant directory: '$fsDirectory'" );
655 } elseif ( is_dir( $fsDirectory ) ) {
656 $this->logger->warning( __METHOD__ .
": unreadable directory: '$fsDirectory'" );
660 $this->logger->warning( __METHOD__ .
": unreachable directory: '$fsDirectory'" );
679 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
682 $error = $list->getLastError();
683 if ( $error !==
null ) {
685 $this->logger->info( __METHOD__ .
": non-existent directory: '$fsDirectory'" );
688 } elseif ( is_dir( $fsDirectory ) ) {
689 $this->logger->warning( __METHOD__ .
690 ": unreadable directory: '$fsDirectory': $error" );
694 $this->logger->warning( __METHOD__ .
695 ": unreachable directory: '$fsDirectory': $error" );
707 foreach ( $params[
'srcs'] as $src ) {
720 } elseif ( $hadError ) {
733 foreach ( $params[
'srcs'] as $src ) {
741 $tmpFile = $this->tmpFileFactory->newTempFSFile(
'localcopy_',
$ext );
747 $tmpPath = $tmpFile->getPath();
751 $copySuccess = $isFile ?
copy(
$source, $tmpPath ) :
false;
754 if ( $copySuccess ) {
755 $this->
chmod( $tmpPath );
756 $tmpFiles[$src] = $tmpFile;
757 } elseif ( $hadError ) {
780 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
781 $pipes[$index] = popen( $fileOpHandle->cmd,
'r' );
785 foreach ( $pipes as $index => $pipe ) {
788 $errs[$index] = stream_get_contents( $pipe );
792 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
794 $function = $fileOpHandle->callback;
795 $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
796 $statuses[$index] = $status;
807 $time = dechex( time() );
808 $hash = \Wikimedia\base_convert( md5( basename( $fsPath ) ), 16, 36, 25 );
809 $unique = \Wikimedia\base_convert( bin2hex( random_bytes( 16 ) ), 16, 36, 25 );
811 return dirname( $fsPath ) .
"/.{$time}_{$hash}_{$unique}.tmpfsfile";
828 if ( $this->os ===
'Windows' ) {
831 $cmdWrite =
"COPY /B /Y $encSrc $encStage 2>&1 && MOVE /Y $encStage $encDst 2>&1";
832 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $cmdWrite" : $cmdWrite;
836 $cmdWrite =
"cp $encSrc $encStage 2>&1 && mv $encStage $encDst 2>&1";
837 $cmd = $ignoreMissing ?
"test -f $encSrc && $cmdWrite" : $cmdWrite;
839 $octalPermissions =
'0' . decoct( $this->fileMode );
840 if ( strlen( $octalPermissions ) == 4 ) {
841 $cmd .=
" && chmod $octalPermissions $encDst 2>/dev/null";
859 if ( $this->os ===
'Windows' ) {
860 $writeCmd =
"MOVE /Y $encSrc $encDst 2>&1";
861 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $writeCmd" : $writeCmd;
863 $writeCmd =
"mv -f $encSrc $encDst 2>&1";
864 $cmd = $ignoreMissing ?
"test -f $encSrc && $writeCmd" : $writeCmd;
879 if ( $this->os ===
'Windows' ) {
880 $writeCmd =
"DEL /Q $encSrc 2>&1";
881 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $writeCmd" : $writeCmd;
883 $cmd = $ignoreMissing ?
"rm -f $encSrc 2>&1" :
"rm $encSrc 2>&1";
895 protected function chmod( $fsPath ) {
896 if ( $this->os ===
'Windows' ) {
900 AtEase::suppressWarnings();
901 $ok =
chmod( $fsPath, $this->fileMode );
902 AtEase::restoreWarnings();
914 AtEase::suppressWarnings();
916 AtEase::restoreWarnings();
917 clearstatcache(
true, $fsPath );
928 protected function rmdir( $fsDirectory ) {
929 AtEase::suppressWarnings();
930 $ok =
rmdir( $fsDirectory );
931 AtEase::restoreWarnings();
932 clearstatcache(
true, $fsDirectory );
942 $tempFile = $this->tmpFileFactory->newTempFSFile(
'create_',
'tmp' );
947 AtEase::suppressWarnings();
948 if ( file_put_contents( $tempFile->getPath(), $params[
'content'] ) ===
false ) {
951 AtEase::restoreWarnings();
971 return "Deny from all\n";
981 return ( $this->os ===
'Windows' ) ? strtr( $fsPath,
'/',
'\\' ) : $fsPath;
990 $this->warningTrapStack[] =
false;
991 set_error_handler(
function ( $errno, $errstr ) use ( $regexIgnore ) {
992 if ( $regexIgnore ===
null || !preg_match( $regexIgnore, $errstr ) ) {
993 $this->logger->error( $errstr );
994 $this->warningTrapStack[count( $this->warningTrapStack ) - 1] =
true;
1013 restore_error_handler();
1015 return array_pop( $this->warningTrapStack );
1025 if ( $regex ===
null ) {
1027 $alternatives = [
': No such file or directory' ];
1028 if ( $this->os ===
'Windows' ) {
1031 $alternatives[] =
' \(code: [23]\)';
1033 if ( function_exists(
'pcntl_strerror' ) ) {
1034 $alternatives[] = preg_quote(
': ' . pcntl_strerror( 2 ),
'/' );
1035 } elseif ( function_exists(
'socket_strerror' ) && defined(
'SOCKET_ENOENT' ) ) {
1036 $alternatives[] = preg_quote(
': ' . socket_strerror( SOCKET_ENOENT ),
'/' );
1038 $regex =
'/(' . implode(
'|', $alternatives ) .
')$/';
Class for a file system (FS) based file backend.
doCopyInternal(array $params)
string $basePath
Directory holding the container directories.
bool[] $warningTrapStack
Map of (stack index => whether a warning happened)
doDirectoryExists( $fullCont, $dirRel, array $params)
newTempFileWithContent(array $params)
__construct(array $config)
getDirectoryListInternal( $fullCont, $dirRel, array $params)
doDeleteInternal(array $params)
indexHtmlPrivate()
Return the text of an index.html file to hide directory listings.
doExecuteOpHandlesInternal(array $fileOpHandles)
getFileNotFoundRegex()
Get a regex matching file not found errors.
resolveToFSPath( $storagePath)
Get the absolute file system path for a storage path.
isFileNotFoundError( $error)
Determine whether a given error message is a file not found error.
doGetFileStat(array $params)
rmdir( $fsDirectory)
Remove an empty directory, suppressing the warnings.
htaccessPrivate()
Return the text of a .htaccess file to make a directory private.
doGetLocalCopyMulti(array $params)
MapCacheLRU $usableDirCache
Cache for known prepared/usable directorries.
untrapWarnings()
Stop listening for E_WARNING errors and get whether any happened.
string $fileOwner
Required OS username to own files.
doPublishInternal( $fullCont, $dirRel, array $params)
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
isLegalRelPath( $fsPath)
Sanity check a relative file system path for validity.
array $containerPaths
Map of container names to root paths for custom container paths.
makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing=false)
doCleanInternal( $fullCont, $dirRel, array $params)
chmod( $fsPath)
Chmod a file, suppressing the warnings.
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
doSecureInternal( $fullCont, $dirRel, array $params)
makeUnlinkCommand( $fsPath, $ignoreMissing=false)
doMoveInternal(array $params)
doClearCache(array $paths=null)
Clears any additional stat caches for storage paths Stable to override.
trapWarnings( $regexIgnore=null)
Listen for E_WARNING errors and track whether any that happen.
doStoreInternal(array $params)
doPrepareInternal( $fullCont, $dirRel, array $params)
getFeatures()
Get the a bitfield of extra features supported by the backend medium Stable to override.
containerFSRoot( $shortCont, $fullCont)
Given the short (unresolved) and full (resolved) name of a container, return the file system path of ...
unlink( $fsPath)
Unlink a file, suppressing the warnings.
int $fileMode
File permission mode.
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path in the backend.
trapWarningsIgnoringNotFound()
Track E_WARNING errors but ignore any that correspond to ENOENT "No such file or directory".
cleanPathSlashes( $fsPath)
Clean up directory separators for the given OS.
doCreateInternal(array $params)
bool $os
Simpler version of PHP_OS_FAMILY.
makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing)
doGetLocalReferenceMulti(array $params)
string $currentUser
OS username running this script.
makeStagingPath( $fsPath)
int $dirMode
Directory permission mode.
getFileListInternal( $fullCont, $dirRel, array $params)
Class representing a non-directory file on the file system.
Base class for all backends using particular storage medium.
resolveStoragePathReal( $storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
static false $RES_ABSENT
Idiom for "no result due to missing file" (since 1.34)
static null $RES_ERROR
Idiom for "no result due to I/O errors" (since 1.34)
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
newStatus(... $args)
Yields the result of the status wrapper callback on either:
copy(array $params, array $opts=[])
Performs a single copy operation.
Handles a simple LRU key/value map with a maximum number of entries.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
if(!is_readable( $file)) $ext