45use Wikimedia\AtEase\AtEase;
46use Wikimedia\Timestamp\ConvertibleTimestamp;
87 private $warningTrapStack = [];
100 parent::__construct( $config );
102 if ( PHP_OS_FAMILY ===
'Windows' ) {
103 $this->os =
'Windows';
104 } elseif ( PHP_OS_FAMILY ===
'BSD' || PHP_OS_FAMILY ===
'Darwin' ) {
110 if ( isset( $config[
'basePath'] ) ) {
111 $this->basePath = rtrim( $config[
'basePath'],
'/' );
113 $this->basePath =
null;
116 $this->containerPaths = [];
117 foreach ( ( $config[
'containerPaths'] ?? [] ) as $container => $fsPath ) {
118 $this->containerPaths[$container] = rtrim( $fsPath,
'/' );
121 $this->fileMode = $config[
'fileMode'] ?? 0644;
122 $this->dirMode = $config[
'directoryMode'] ?? 0777;
123 if ( isset( $config[
'fileOwner'] ) && function_exists(
'posix_getuid' ) ) {
124 $this->fileOwner = $config[
'fileOwner'];
126 $this->currentUser = posix_getpwuid( posix_getuid() )[
'name'];
129 $this->usableDirCache =
new MapCacheLRU( self::CACHE_CHEAP_SIZE );
138 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
141 return $relStoragePath;
156 if ( preg_match(
'![^/]{256}!', $fsPath ) ) {
159 if ( $this->os ===
'Windows' ) {
160 return !preg_match(
'![:*?"<>|]!', $fsPath );
175 if ( isset( $this->containerPaths[$shortCont] ) ) {
176 return $this->containerPaths[$shortCont];
177 } elseif ( isset( $this->basePath ) ) {
178 return "{$this->basePath}/{$fullCont}";
192 if ( $relPath ===
null ) {
197 if ( $relPath !=
'' ) {
198 $fsPath .=
"/{$relPath}";
206 if ( $fsPath ===
null ) {
210 if ( $this->fileOwner !==
null && $this->currentUser !== $this->fileOwner ) {
211 trigger_error( __METHOD__ .
": PHP process owner is not '{$this->fileOwner}'." );
215 $fsDirectory = dirname( $fsPath );
216 $usable = $this->usableDirCache->get( $fsDirectory, MapCacheLRU::TTL_PROC_SHORT );
217 if ( $usable ===
null ) {
218 AtEase::suppressWarnings();
219 $usable = is_dir( $fsDirectory ) && is_writable( $fsDirectory );
220 AtEase::restoreWarnings();
221 $this->usableDirCache->set( $fsDirectory, $usable ? 1 : 0 );
231 if ( $fsDstPath ===
null ) {
232 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
237 if ( !empty( $params[
'async'] ) ) {
240 $status->fatal(
'backend-fail-create', $params[
'dst'] );
244 $cmd = $this->makeCopyCommand( $tempFile->getPath(), $fsDstPath,
false );
245 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
246 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
247 $status->fatal(
'backend-fail-create', $params[
'dst'] );
248 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
251 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
252 $tempFile->bind( $status->value );
258 $fsStagePath = $this->makeStagingPath( $fsDstPath );
260 $stageHandle = fopen( $fsStagePath,
'xb' );
261 if ( $stageHandle ) {
262 $bytes = fwrite( $stageHandle, $params[
'content'] );
263 $created = ( $bytes === strlen( $params[
'content'] ) );
264 fclose( $stageHandle );
265 $created = $created ? rename( $fsStagePath, $fsDstPath ) :
false;
268 if ( $hadError || !$created ) {
269 $status->fatal(
'backend-fail-create', $params[
'dst'] );
273 $this->
chmod( $fsDstPath );
282 $fsSrcPath = $params[
'src'];
284 if ( $fsDstPath ===
null ) {
285 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
290 if ( $fsSrcPath === $fsDstPath ) {
291 $status->fatal(
'backend-fail-internal', $this->name );
296 if ( !empty( $params[
'async'] ) ) {
297 $cmd = $this->makeCopyCommand( $fsSrcPath, $fsDstPath,
false );
298 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
299 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
300 $status->fatal(
'backend-fail-store', $params[
'src'], $params[
'dst'] );
301 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
304 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
310 $fsStagePath = $this->makeStagingPath( $fsDstPath );
312 $srcHandle = fopen( $fsSrcPath,
'rb' );
314 $stageHandle = fopen( $fsStagePath,
'xb' );
315 if ( $stageHandle ) {
316 $bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
317 $stored = ( $bytes !==
false && $bytes === fstat( $srcHandle )[
'size'] );
318 fclose( $stageHandle );
319 $stored = $stored ? rename( $fsStagePath, $fsDstPath ) :
false;
321 fclose( $srcHandle );
324 if ( $hadError || !$stored ) {
325 $status->fatal(
'backend-fail-store', $params[
'src'], $params[
'dst'] );
329 $this->
chmod( $fsDstPath );
339 if ( $fsSrcPath ===
null ) {
340 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
346 if ( $fsDstPath ===
null ) {
347 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
352 if ( $fsSrcPath === $fsDstPath ) {
356 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
358 if ( !empty( $params[
'async'] ) ) {
359 $cmd = $this->makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
360 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
361 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
362 $status->fatal(
'backend-fail-copy', $params[
'src'], $params[
'dst'] );
363 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
366 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
372 $fsStagePath = $this->makeStagingPath( $fsDstPath );
374 $srcHandle = fopen( $fsSrcPath,
'rb' );
376 $stageHandle = fopen( $fsStagePath,
'xb' );
377 if ( $stageHandle ) {
378 $bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
379 $copied = ( $bytes !==
false && $bytes === fstat( $srcHandle )[
'size'] );
380 fclose( $stageHandle );
381 $copied = $copied ? rename( $fsStagePath, $fsDstPath ) :
false;
383 fclose( $srcHandle );
386 if ( $hadError || ( !$copied && !$ignoreMissing ) ) {
387 $status->fatal(
'backend-fail-copy', $params[
'src'], $params[
'dst'] );
392 $this->
chmod( $fsDstPath );
403 if ( $fsSrcPath ===
null ) {
404 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
410 if ( $fsDstPath ===
null ) {
411 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
416 if ( $fsSrcPath === $fsDstPath ) {
420 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
422 if ( !empty( $params[
'async'] ) ) {
423 $cmd = $this->makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
424 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
425 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
426 $status->fatal(
'backend-fail-move', $params[
'src'], $params[
'dst'] );
427 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
430 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
436 $moved = rename( $fsSrcPath, $fsDstPath );
438 if ( $hadError || ( !$moved && !$ignoreMissing ) ) {
439 $status->fatal(
'backend-fail-move', $params[
'src'], $params[
'dst'] );
452 if ( $fsSrcPath ===
null ) {
453 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
458 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
460 if ( !empty( $params[
'async'] ) ) {
461 $cmd = $this->makeUnlinkCommand( $fsSrcPath, $ignoreMissing );
462 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
463 if ( $errors !==
'' && !( $this->os ===
'Windows' && $errors[0] ===
" " ) ) {
464 $status->fatal(
'backend-fail-delete', $params[
'src'] );
465 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
468 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
471 $deleted =
unlink( $fsSrcPath );
473 if ( $hadError || ( !$deleted && !$ignoreMissing ) ) {
474 $status->fatal(
'backend-fail-delete', $params[
'src'] );
490 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
493 AtEase::suppressWarnings();
494 $alreadyExisted = is_dir( $fsDirectory );
495 if ( !$alreadyExisted ) {
496 $created = mkdir( $fsDirectory, $this->dirMode,
true );
498 $alreadyExisted = is_dir( $fsDirectory );
501 $isWritable = $created ?: is_writable( $fsDirectory );
502 AtEase::restoreWarnings();
503 if ( !$alreadyExisted && !$created ) {
504 $this->logger->error( __METHOD__ .
": cannot create directory $fsDirectory" );
505 $status->fatal(
'directorycreateerror', $params[
'dir'] );
506 } elseif ( !$isWritable ) {
507 $this->logger->error( __METHOD__ .
": directory $fsDirectory is read-only" );
508 $status->fatal(
'directoryreadonlyerror', $params[
'dir'] );
515 if ( $status->isGood() ) {
516 $this->usableDirCache->set( $fsDirectory, 1 );
526 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
528 if ( !empty( $params[
'noListing'] ) && !is_file(
"{$fsDirectory}/index.html" ) ) {
530 $bytes = file_put_contents(
"{$fsDirectory}/index.html", $this->
indexHtmlPrivate() );
532 if ( $bytes ===
false ) {
533 $status->fatal(
'backend-fail-create', $params[
'dir'] .
'/index.html' );
537 if ( !empty( $params[
'noAccess'] ) && !is_file(
"{$contRoot}/.htaccess" ) ) {
538 AtEase::suppressWarnings();
539 $bytes = file_put_contents(
"{$contRoot}/.htaccess", $this->
htaccessPrivate() );
540 AtEase::restoreWarnings();
541 if ( $bytes ===
false ) {
542 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
543 $status->fatal(
'backend-fail-create',
"{$storeDir}/.htaccess" );
554 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
556 if ( !empty( $params[
'listing'] ) && is_file(
"{$fsDirectory}/index.html" ) ) {
557 $exists = ( file_get_contents(
"{$fsDirectory}/index.html" ) === $this->
indexHtmlPrivate() );
558 if ( $exists && !$this->
unlink(
"{$fsDirectory}/index.html" ) ) {
559 $status->fatal(
'backend-fail-delete', $params[
'dir'] .
'/index.html' );
563 if ( !empty( $params[
'access'] ) && is_file(
"{$contRoot}/.htaccess" ) ) {
564 $exists = ( file_get_contents(
"{$contRoot}/.htaccess" ) === $this->
htaccessPrivate() );
565 if ( $exists && !$this->
unlink(
"{$contRoot}/.htaccess" ) ) {
566 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
567 $status->fatal(
'backend-fail-delete',
"{$storeDir}/.htaccess" );
578 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
580 $this->
rmdir( $fsDirectory );
587 if ( $fsSrcPath ===
null ) {
592 $stat = is_file( $fsSrcPath ) ? stat( $fsSrcPath ) :
false;
595 if ( is_array( $stat ) ) {
596 $ct =
new ConvertibleTimestamp( $stat[
'mtime'] );
599 'mtime' => $ct->getTimestamp( TS_MW ),
600 'size' => $stat[
'size']
608 if ( is_array( $paths ) ) {
609 foreach ( $paths as
$path ) {
611 if ( $fsPath !==
null ) {
612 clearstatcache(
true, $fsPath );
613 $this->usableDirCache->clear( $fsPath );
617 clearstatcache(
true );
618 $this->usableDirCache->clear();
625 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
628 $exists = is_dir( $fsDirectory );
644 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
647 $error = $list->getLastError();
648 if ( $error !==
null ) {
650 $this->logger->info( __METHOD__ .
": non-existant directory: '$fsDirectory'" );
653 } elseif ( is_dir( $fsDirectory ) ) {
654 $this->logger->warning( __METHOD__ .
": unreadable directory: '$fsDirectory'" );
658 $this->logger->warning( __METHOD__ .
": unreachable directory: '$fsDirectory'" );
677 $fsDirectory = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
680 $error = $list->getLastError();
681 if ( $error !==
null ) {
683 $this->logger->info( __METHOD__ .
": non-existent directory: '$fsDirectory'" );
686 } elseif ( is_dir( $fsDirectory ) ) {
687 $this->logger->warning( __METHOD__ .
688 ": unreadable directory: '$fsDirectory': $error" );
692 $this->logger->warning( __METHOD__ .
693 ": unreachable directory: '$fsDirectory': $error" );
705 foreach ( $params[
'srcs'] as $src ) {
718 } elseif ( $hadError ) {
731 foreach ( $params[
'srcs'] as $src ) {
739 $tmpFile = $this->tmpFileFactory->newTempFSFile(
'localcopy_',
$ext );
745 $tmpPath = $tmpFile->getPath();
749 $copySuccess = $isFile ?
copy(
$source, $tmpPath ) :
false;
752 if ( $copySuccess ) {
753 $this->
chmod( $tmpPath );
754 $tmpFiles[$src] = $tmpFile;
755 } elseif ( $hadError ) {
778 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
779 $pipes[$index] = popen( $fileOpHandle->cmd,
'r' );
783 foreach ( $pipes as $index => $pipe ) {
786 $errs[$index] = stream_get_contents( $pipe );
790 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
792 $function = $fileOpHandle->callback;
793 $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
794 $statuses[$index] = $status;
804 private function makeStagingPath( $fsPath ) {
805 $time = dechex( time() );
806 $hash = \Wikimedia\base_convert( md5( basename( $fsPath ) ), 16, 36, 25 );
807 $unique = \Wikimedia\base_convert( bin2hex( random_bytes( 16 ) ), 16, 36, 25 );
809 return dirname( $fsPath ) .
"/.{$time}_{$hash}_{$unique}.tmpfsfile";
818 private function makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing ) {
822 $fsStagePath = $this->makeStagingPath( $fsDstPath );
826 if ( $this->os ===
'Windows' ) {
829 $cmdWrite =
"COPY /B /Y $encSrc $encStage 2>&1 && MOVE /Y $encStage $encDst 2>&1";
830 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $cmdWrite" : $cmdWrite;
834 $cmdWrite =
"cp $encSrc $encStage 2>&1 && mv $encStage $encDst 2>&1";
835 $cmd = $ignoreMissing ?
"test -f $encSrc && $cmdWrite" : $cmdWrite;
837 $octalPermissions =
'0' . decoct( $this->fileMode );
838 if ( strlen( $octalPermissions ) == 4 ) {
839 $cmd .=
" && chmod $octalPermissions $encDst 2>/dev/null";
852 private function makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing =
false ) {
857 if ( $this->os ===
'Windows' ) {
858 $writeCmd =
"MOVE /Y $encSrc $encDst 2>&1";
859 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $writeCmd" : $writeCmd;
861 $writeCmd =
"mv -f $encSrc $encDst 2>&1";
862 $cmd = $ignoreMissing ?
"test -f $encSrc && $writeCmd" : $writeCmd;
873 private function makeUnlinkCommand( $fsPath, $ignoreMissing =
false ) {
877 if ( $this->os ===
'Windows' ) {
878 $writeCmd =
"DEL /Q $encSrc 2>&1";
879 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $writeCmd" : $writeCmd;
881 $cmd = $ignoreMissing ?
"rm -f $encSrc 2>&1" :
"rm $encSrc 2>&1";
893 protected function chmod( $fsPath ) {
894 if ( $this->os ===
'Windows' ) {
898 AtEase::suppressWarnings();
899 $ok =
chmod( $fsPath, $this->fileMode );
900 AtEase::restoreWarnings();
912 AtEase::suppressWarnings();
914 AtEase::restoreWarnings();
915 clearstatcache(
true, $fsPath );
926 protected function rmdir( $fsDirectory ) {
927 AtEase::suppressWarnings();
928 $ok =
rmdir( $fsDirectory );
929 AtEase::restoreWarnings();
930 clearstatcache(
true, $fsDirectory );
940 $tempFile = $this->tmpFileFactory->newTempFSFile(
'create_',
'tmp' );
945 AtEase::suppressWarnings();
946 if ( file_put_contents( $tempFile->getPath(), $params[
'content'] ) ===
false ) {
949 AtEase::restoreWarnings();
969 return "Require all denied\n";
979 return ( $this->os ===
'Windows' ) ? strtr( $fsPath,
'/',
'\\' ) : $fsPath;
988 $this->warningTrapStack[] =
false;
989 set_error_handler(
function ( $errno, $errstr ) use ( $regexIgnore ) {
990 if ( $regexIgnore ===
null || !preg_match( $regexIgnore, $errstr ) ) {
991 $this->logger->error( $errstr );
992 $this->warningTrapStack[count( $this->warningTrapStack ) - 1] =
true;
1011 restore_error_handler();
1013 return array_pop( $this->warningTrapStack );
1023 if ( $regex ===
null ) {
1025 $alternatives = [
': No such file or directory' ];
1026 if ( $this->os ===
'Windows' ) {
1029 $alternatives[] =
' \(code: [23]\)';
1031 if ( function_exists(
'pcntl_strerror' ) ) {
1032 $alternatives[] = preg_quote(
': ' . pcntl_strerror( 2 ),
'/' );
1033 } elseif ( function_exists(
'socket_strerror' ) && defined(
'SOCKET_ENOENT' ) ) {
1034 $alternatives[] = preg_quote(
': ' . socket_strerror( SOCKET_ENOENT ),
'/' );
1036 $regex =
'/(' . implode(
'|', $alternatives ) .
')$/';
Class for a file system (FS) based file backend.
doCopyInternal(array $params)
doDirectoryExists( $fullCont, $dirRel, array $params)
newTempFileWithContent(array $params)
__construct(array $config)
getDirectoryListInternal( $fullCont, $dirRel, array $params)
doDeleteInternal(array $params)
string $os
Simpler version of PHP_OS_FAMILY.
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 directories.
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)
Check a relative file system path for validity.
array $containerPaths
Map of container names to root paths for custom container paths.
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)
doMoveInternal(array $params)
doClearCache(array $paths=null)
Clears any additional stat caches for storage paths.
trapWarnings( $regexIgnore=null)
Listen for E_WARNING errors and track whether any that happen.
doStoreInternal(array $params)
doPrepareInternal( $fullCont, $dirRel, array $params)
FileBackendStore::doPrepare() to override StatusValue Good status without value for success,...
getFeatures()
Get the a bitfield of extra features supported by the backend medium.
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.
string null $basePath
Directory holding the container directories.
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)
doGetLocalReferenceMulti(array $params)
string $currentUser
OS username running this script.
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