43use Wikimedia\AtEase\AtEase;
44use Wikimedia\Timestamp\ConvertibleTimestamp;
95 parent::__construct( $config );
97 $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) ===
'WIN' );
99 if ( isset( $config[
'basePath'] ) ) {
100 $this->basePath = rtrim( $config[
'basePath'],
'/' );
102 $this->basePath =
null;
105 $this->containerPaths = [];
106 foreach ( ( $config[
'containerPaths'] ?? [] ) as $container =>
$path ) {
107 $this->containerPaths[$container] = rtrim(
$path,
'/' );
110 $this->fileMode = $config[
'fileMode'] ?? 0644;
111 $this->dirMode = $config[
'directoryMode'] ?? 0777;
112 if ( isset( $config[
'fileOwner'] ) && function_exists(
'posix_getuid' ) ) {
113 $this->fileOwner = $config[
'fileOwner'];
115 $this->currentUser = posix_getpwuid( posix_getuid() )[
'name'];
120 if ( $this->isWindows && version_compare( PHP_VERSION,
'7.1',
'lt' ) ) {
131 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
134 return $relStoragePath;
149 if ( preg_match(
'![^/]{256}!',
$path ) ) {
152 if ( $this->isWindows ) {
153 return !preg_match(
'![:*?"<>|]!',
$path );
168 if ( isset( $this->containerPaths[$shortCont] ) ) {
169 return $this->containerPaths[$shortCont];
170 } elseif ( isset( $this->basePath ) ) {
171 return "{$this->basePath}/{$fullCont}";
185 if ( $relPath ===
null ) {
190 if ( $relPath !=
'' ) {
191 $fsPath .=
"/{$relPath}";
199 if ( $fsPath ===
null ) {
202 $parentDir = dirname( $fsPath );
204 if ( file_exists( $fsPath ) ) {
205 $ok = is_file( $fsPath ) && is_writable( $fsPath );
207 $ok = is_dir( $parentDir ) && is_writable( $parentDir );
210 if ( $this->fileOwner !==
null && $this->currentUser !== $this->fileOwner ) {
212 trigger_error( __METHOD__ .
": PHP process owner is not '{$this->fileOwner}'." );
222 if ( $dest ===
null ) {
223 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
228 if ( !empty( $params[
'async'] ) ) {
231 $status->fatal(
'backend-fail-create', $params[
'dst'] );
235 $cmd = implode(
' ', [
236 $this->isWindows ?
'COPY /B /Y' :
'cp',
240 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
241 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
242 $status->fatal(
'backend-fail-create', $params[
'dst'] );
243 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
246 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
247 $tempFile->bind( $status->value );
250 $bytes = file_put_contents( $dest, $params[
'content'] );
252 if ( $bytes ===
false ) {
253 $status->fatal(
'backend-fail-create', $params[
'dst'] );
257 $this->
chmod( $dest );
267 if ( $dest ===
null ) {
268 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
273 if ( !empty( $params[
'async'] ) ) {
274 $cmd = implode(
' ', [
275 $this->isWindows ?
'COPY /B /Y' :
'cp',
279 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
280 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
281 $status->fatal(
'backend-fail-store', $params[
'src'], $params[
'dst'] );
282 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
285 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
288 $ok =
copy( $params[
'src'], $dest );
291 if ( !$ok || ( filesize( $params[
'src'] ) !== filesize( $dest ) ) ) {
294 trigger_error( __METHOD__ .
": copy() failed but returned true." );
296 $status->fatal(
'backend-fail-store', $params[
'src'], $params[
'dst'] );
300 $this->
chmod( $dest );
311 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
317 if ( $dest ===
null ) {
318 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
324 if ( empty( $params[
'ignoreMissingSource'] ) ) {
325 $status->fatal(
'backend-fail-copy', $params[
'src'] );
331 if ( !empty( $params[
'async'] ) ) {
332 $cmd = implode(
' ', [
333 $this->isWindows ?
'COPY /B /Y' :
'cp',
337 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
338 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
339 $status->fatal(
'backend-fail-copy', $params[
'src'], $params[
'dst'] );
340 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
343 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
349 if ( !$ok || ( filesize(
$source ) !== filesize( $dest ) ) ) {
354 trigger_error( __METHOD__ .
": copy() failed but returned true." );
356 $status->fatal(
'backend-fail-copy', $params[
'src'], $params[
'dst'] );
360 $this->
chmod( $dest );
370 if ( $fsSrcPath ===
null ) {
371 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
377 if ( $fsDstPath ===
null ) {
378 $status->fatal(
'backend-fail-invalidpath', $params[
'dst'] );
383 if ( $fsSrcPath === $fsDstPath ) {
387 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
389 if ( !empty( $params[
'async'] ) ) {
394 if ( $this->isWindows ) {
395 $writeCmd =
"MOVE /Y $encSrc $encDst";
396 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $writeCmd" : $writeCmd;
398 $writeCmd =
"mv -f $encSrc $encDst";
399 $cmd = $ignoreMissing ?
"test -f $encSrc && $writeCmd" : $writeCmd;
401 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
402 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
403 $status->fatal(
'backend-fail-move', $params[
'src'], $params[
'dst'] );
404 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
407 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
412 $this->
trapWarnings(
'/: No such file or directory$/' );
413 $moved = rename( $fsSrcPath, $fsDstPath );
415 if ( $hadError || ( !$moved && !$ignoreMissing ) ) {
416 $status->fatal(
'backend-fail-move', $params[
'src'], $params[
'dst'] );
429 if ( $fsSrcPath ===
null ) {
430 $status->fatal(
'backend-fail-invalidpath', $params[
'src'] );
435 $ignoreMissing = !empty( $params[
'ignoreMissingSource'] );
437 if ( !empty( $params[
'async'] ) ) {
441 if ( $this->isWindows ) {
442 $writeCmd =
"DEL /Q $encSrc";
443 $cmd = $ignoreMissing ?
"IF EXIST $encSrc $writeCmd" : $writeCmd;
445 $cmd = $ignoreMissing ?
"rm -f $encSrc" :
"rm $encSrc";
447 $handler =
function ( $errors,
StatusValue $status, array $params, $cmd ) {
448 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
449 $status->fatal(
'backend-fail-delete', $params[
'src'] );
450 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
453 $status->value =
new FSFileOpHandle( $this, $params, $handler, $cmd );
455 $this->
trapWarnings(
'/: No such file or directory$/' );
456 $deleted =
unlink( $fsSrcPath );
458 if ( $hadError || ( !$deleted && !$ignoreMissing ) ) {
459 $status->fatal(
'backend-fail-delete', $params[
'src'] );
478 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
479 $existed = is_dir( $dir );
481 AtEase::suppressWarnings();
482 if ( !$existed && !mkdir( $dir, $this->dirMode,
true ) && !is_dir( $dir ) ) {
483 $this->logger->error( __METHOD__ .
": cannot create directory $dir" );
484 $status->fatal(
'directorycreateerror', $params[
'dir'] );
485 } elseif ( !is_writable( $dir ) ) {
486 $this->logger->error( __METHOD__ .
": directory $dir is read-only" );
487 $status->fatal(
'directoryreadonlyerror', $params[
'dir'] );
488 } elseif ( !is_readable( $dir ) ) {
489 $this->logger->error( __METHOD__ .
": directory $dir is not readable" );
490 $status->fatal(
'directorynotreadableerror', $params[
'dir'] );
492 AtEase::restoreWarnings();
494 if ( is_dir( $dir ) && !$existed ) {
505 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
507 if ( !empty( $params[
'noListing'] ) && !file_exists(
"{$dir}/index.html" ) ) {
509 $bytes = file_put_contents(
"{$dir}/index.html", $this->
indexHtmlPrivate() );
511 if ( $bytes ===
false ) {
512 $status->fatal(
'backend-fail-create', $params[
'dir'] .
'/index.html' );
516 if ( !empty( $params[
'noAccess'] ) && !file_exists(
"{$contRoot}/.htaccess" ) ) {
517 AtEase::suppressWarnings();
518 $bytes = file_put_contents(
"{$contRoot}/.htaccess", $this->
htaccessPrivate() );
519 AtEase::restoreWarnings();
520 if ( $bytes ===
false ) {
521 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
522 $status->fatal(
'backend-fail-create',
"{$storeDir}/.htaccess" );
533 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
535 if ( !empty( $params[
'listing'] ) && is_file(
"{$dir}/index.html" ) ) {
536 $exists = ( file_get_contents(
"{$dir}/index.html" ) === $this->
indexHtmlPrivate() );
537 if ( $exists && !$this->
unlink(
"{$dir}/index.html" ) ) {
538 $status->fatal(
'backend-fail-delete', $params[
'dir'] .
'/index.html' );
542 if ( !empty( $params[
'access'] ) && is_file(
"{$contRoot}/.htaccess" ) ) {
543 $exists = ( file_get_contents(
"{$contRoot}/.htaccess" ) === $this->
htaccessPrivate() );
544 if ( $exists && !$this->
unlink(
"{$contRoot}/.htaccess" ) ) {
545 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
546 $status->fatal(
'backend-fail-delete',
"{$storeDir}/.htaccess" );
557 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
558 AtEase::suppressWarnings();
560 AtEase::restoreWarnings();
575 if ( is_array( $stat ) ) {
576 $ct =
new ConvertibleTimestamp( $stat[
'mtime'] );
579 'mtime' => $ct->getTimestamp( TS_MW ),
580 'size' => $stat[
'size']
594 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
597 $exists = is_dir( $dir );
613 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
616 $exists = is_dir( $dir );
617 $isReadable = $exists ? is_readable( $dir ) :
false;
622 } elseif ( $exists ) {
623 $this->logger->warning( __METHOD__ .
": given directory is unreadable: '$dir'" );
626 } elseif ( $hadError ) {
627 $this->logger->warning( __METHOD__ .
": given directory was unreachable: '$dir'" );
631 $this->logger->info( __METHOD__ .
": given directory does not exist: '$dir'" );
647 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
650 $exists = is_dir( $dir );
651 $isReadable = $exists ? is_readable( $dir ) :
false;
654 if ( $exists && $isReadable ) {
656 } elseif ( $exists ) {
657 $this->logger->warning( __METHOD__ .
": given directory is unreadable: '$dir'\n" );
660 } elseif ( $hadError ) {
661 $this->logger->warning( __METHOD__ .
": given directory was unreachable: '$dir'\n" );
665 $this->logger->info( __METHOD__ .
": given directory does not exist: '$dir'\n" );
674 foreach ( $params[
'srcs'] as $src ) {
687 } elseif ( $hadError ) {
700 foreach ( $params[
'srcs'] as $src ) {
708 $tmpFile = $this->tmpFileFactory->newTempFSFile(
'localcopy_',
$ext );
714 $tmpPath = $tmpFile->getPath();
718 $copySuccess = $isFile ?
copy(
$source, $tmpPath ) :
false;
721 if ( $copySuccess ) {
722 $this->
chmod( $tmpPath );
723 $tmpFiles[$src] = $tmpFile;
724 } elseif ( $hadError ) {
747 $octalPermissions =
'0' . decoct( $this->fileMode );
748 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
749 $cmd =
"{$fileOpHandle->cmd} 2>&1";
753 $fileOpHandle->chmodPath !==
null &&
754 strlen( $octalPermissions ) == 4
756 $encPath = escapeshellarg( $fileOpHandle->chmodPath );
757 $cmd .=
" && chmod $octalPermissions $encPath 2>/dev/null";
759 $pipes[$index] = popen( $cmd,
'r' );
763 foreach ( $pipes as $index => $pipe ) {
766 $errs[$index] = stream_get_contents( $pipe );
770 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
772 $function = $fileOpHandle->call;
773 $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
774 $statuses[$index] = $status;
789 if ( $this->isWindows ) {
793 AtEase::suppressWarnings();
795 AtEase::restoreWarnings();
807 AtEase::suppressWarnings();
809 AtEase::restoreWarnings();
820 $tempFile = $this->tmpFileFactory->newTempFSFile(
'create_',
'tmp' );
825 AtEase::suppressWarnings();
826 $tmpPath = $tempFile->getPath();
827 if ( file_put_contents( $tmpPath,
$content ) ===
false ) {
830 AtEase::restoreWarnings();
850 return "Deny from all\n";
860 return $this->isWindows ? strtr(
$path,
'/',
'\\' ) :
$path;
869 $this->warningTrapStack[] =
false;
870 set_error_handler(
function ( $errno, $errstr ) use ( $regexIgnore ) {
871 if ( $regexIgnore ===
null || !preg_match( $regexIgnore, $errstr ) ) {
872 $this->logger->error( $errstr );
873 $this->warningTrapStack[count( $this->warningTrapStack ) - 1] =
true;
885 restore_error_handler();
887 return array_pop( $this->warningTrapStack );
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)
__construct(array $config)
getDirectoryListInternal( $fullCont, $dirRel, array $params)
doDeleteInternal(array $params)
indexHtmlPrivate()
Return the text of an index.html file to hide directory listings.
chmod( $path)
Chmod a file, suppressing the warnings.
doExecuteOpHandlesInternal(array $fileOpHandles)
resolveToFSPath( $storagePath)
Get the absolute file system path for a storage path.
stageContentAsTempFile(array $params)
doGetFileStat(array $params)
htaccessPrivate()
Return the text of a .htaccess file to make a directory private.
doGetLocalCopyMulti(array $params)
isLegalRelPath( $path)
Sanity check a relative file system path for validity.
untrapWarnings()
Stop listening for E_WARNING errors and get whether any happened.
string $fileOwner
Required OS username to own files.
cleanPathSlashes( $path)
Clean up directory separators for the given OS.
doPublishInternal( $fullCont, $dirRel, array $params)
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
array $containerPaths
Map of container names to root paths for custom container paths.
bool $isWindows
Whether the OS is Windows (otherwise assumed Unix-like)
doCleanInternal( $fullCont, $dirRel, array $params)
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)
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 ...
int $fileMode
File permission mode.
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path in the backend.
unlink( $path)
Unlink a file, suppressing the warnings.
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
if(!is_readable( $file)) $ext