24 use Wikimedia\Timestamp\ConvertibleTimestamp;
76 parent::__construct( $config );
78 $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) ===
'WIN' );
80 if ( isset( $config[
'basePath'] ) ) {
81 $this->basePath = rtrim( $config[
'basePath'],
'/' );
83 $this->basePath =
null;
86 if ( isset( $config[
'containerPaths'] ) ) {
87 $this->containerPaths = (
array)$config[
'containerPaths'];
88 foreach ( $this->containerPaths
as &
$path ) {
93 $this->fileMode = isset( $config[
'fileMode'] ) ? $config[
'fileMode'] : 0644;
94 $this->dirMode = isset( $config[
'directoryMode'] ) ? $config[
'directoryMode'] : 0777;
95 if ( isset( $config[
'fileOwner'] ) && function_exists(
'posix_getuid' ) ) {
96 $this->fileOwner = $config[
'fileOwner'];
98 $this->currentUser = posix_getpwuid( posix_getuid() )[
'name'];
108 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
111 return $relStoragePath;
126 if ( preg_match(
'![^/]{256}!',
$path ) ) {
129 if ( $this->isWindows ) {
130 return !preg_match(
'![:*?"<>|]!',
$path );
145 if ( isset( $this->containerPaths[$shortCont] ) ) {
146 return $this->containerPaths[$shortCont];
147 } elseif ( isset( $this->basePath ) ) {
148 return "{$this->basePath}/{$fullCont}";
162 if ( $relPath ===
null ) {
167 if ( $relPath !=
'' ) {
168 $fsPath .=
"/{$relPath}";
176 if ( $fsPath ===
null ) {
179 $parentDir = dirname( $fsPath );
181 if ( file_exists( $fsPath ) ) {
182 $ok = is_file( $fsPath ) && is_writable( $fsPath );
184 $ok = is_dir( $parentDir ) && is_writable( $parentDir );
187 if ( $this->fileOwner !==
null && $this->currentUser !== $this->fileOwner ) {
189 trigger_error( __METHOD__ .
": PHP process owner is not '{$this->fileOwner}'." );
199 if ( $dest ===
null ) {
205 if ( !empty(
$params[
'async'] ) ) {
213 $bytes = file_put_contents( $tempFile->getPath(),
$params[
'content'] );
215 if ( $bytes ===
false ) {
220 $cmd = implode(
' ', [
221 $this->isWindows ?
'COPY /B /Y' :
'cp',
226 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
228 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
232 $tempFile->bind(
$status->value );
235 $bytes = file_put_contents( $dest,
$params[
'content'] );
237 if ( $bytes ===
false ) {
242 $this->
chmod( $dest );
252 if ( $dest ===
null ) {
258 if ( !empty(
$params[
'async'] ) ) {
259 $cmd = implode(
' ', [
260 $this->isWindows ?
'COPY /B /Y' :
'cp',
265 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
267 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
276 if ( !$ok || ( filesize(
$params[
'src'] ) !== filesize( $dest ) ) ) {
279 trigger_error( __METHOD__ .
": copy() failed but returned true." );
285 $this->
chmod( $dest );
302 if ( $dest ===
null ) {
309 if ( empty(
$params[
'ignoreMissingSource'] ) ) {
316 if ( !empty(
$params[
'async'] ) ) {
317 $cmd = implode(
' ', [
318 $this->isWindows ?
'COPY /B /Y' :
'cp',
323 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
325 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
334 if ( !$ok || ( filesize(
$source ) !== filesize( $dest ) ) ) {
339 trigger_error( __METHOD__ .
": copy() failed but returned true." );
345 $this->
chmod( $dest );
362 if ( $dest ===
null ) {
369 if ( empty(
$params[
'ignoreMissingSource'] ) ) {
376 if ( !empty(
$params[
'async'] ) ) {
377 $cmd = implode(
' ', [
378 $this->isWindows ?
'MOVE /Y' :
'mv',
383 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
385 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
415 if ( empty(
$params[
'ignoreMissingSource'] ) ) {
422 if ( !empty(
$params[
'async'] ) ) {
423 $cmd = implode(
' ', [
424 $this->isWindows ?
'DEL' :
'unlink',
428 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
430 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
458 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
459 $existed = is_dir(
$dir );
462 if ( !$existed && !mkdir(
$dir, $this->dirMode,
true ) && !is_dir(
$dir ) ) {
463 $this->logger->error( __METHOD__ .
": cannot create directory $dir" );
465 } elseif ( !is_writable(
$dir ) ) {
466 $this->logger->error( __METHOD__ .
": directory $dir is read-only" );
468 } elseif ( !is_readable(
$dir ) ) {
469 $this->logger->error( __METHOD__ .
": directory $dir is not readable" );
474 if ( is_dir(
$dir ) && !$existed ) {
485 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
487 if ( !empty(
$params[
'noListing'] ) && !file_exists(
"{$dir}/index.html" ) ) {
489 $bytes = file_put_contents(
"{$dir}/index.html", $this->
indexHtmlPrivate() );
491 if ( $bytes ===
false ) {
492 $status->fatal(
'backend-fail-create',
$params[
'dir'] .
'/index.html' );
496 if ( !empty(
$params[
'noAccess'] ) && !file_exists(
"{$contRoot}/.htaccess" ) ) {
498 $bytes = file_put_contents(
"{$contRoot}/.htaccess", $this->
htaccessPrivate() );
500 if ( $bytes ===
false ) {
501 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
502 $status->fatal(
'backend-fail-create',
"{$storeDir}/.htaccess" );
513 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
515 if ( !empty(
$params[
'listing'] ) && is_file(
"{$dir}/index.html" ) ) {
516 $exists = ( file_get_contents(
"{$dir}/index.html" ) === $this->
indexHtmlPrivate() );
518 if ( $exists && !unlink(
"{$dir}/index.html" ) ) {
519 $status->fatal(
'backend-fail-delete',
$params[
'dir'] .
'/index.html' );
524 if ( !empty(
$params[
'access'] ) && is_file(
"{$contRoot}/.htaccess" ) ) {
525 $exists = ( file_get_contents(
"{$contRoot}/.htaccess" ) === $this->
htaccessPrivate() );
527 if ( $exists && !unlink(
"{$contRoot}/.htaccess" ) ) {
528 $storeDir =
"mwstore://{$this->name}/{$shortCont}";
529 $status->fatal(
'backend-fail-delete',
"{$storeDir}/.htaccess" );
541 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
543 if ( is_dir(
$dir ) ) {
562 $ct =
new ConvertibleTimestamp( $stat[
'mtime'] );
565 'mtime' => $ct->getTimestamp( TS_MW ),
566 'size' => $stat[
'size']
568 } elseif ( !$hadError ) {
582 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
585 $exists = is_dir(
$dir );
588 return $hadError ? null : $exists;
601 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
602 $exists = is_dir(
$dir );
604 $this->logger->warning( __METHOD__ .
"() given directory does not exist: '$dir'\n" );
607 } elseif ( !is_readable(
$dir ) ) {
608 $this->logger->warning( __METHOD__ .
"() given directory is unreadable: '$dir'\n" );
626 $dir = ( $dirRel !=
'' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
627 $exists = is_dir(
$dir );
629 $this->logger->warning( __METHOD__ .
"() given directory does not exist: '$dir'\n" );
632 } elseif ( !is_readable(
$dir ) ) {
633 $this->logger->warning( __METHOD__ .
"() given directory is unreadable: '$dir'\n" );
647 $fsFiles[$src] =
null;
662 $tmpFiles[$src] =
null;
668 $tmpFiles[$src] =
null;
670 $tmpPath = $tmpFile->getPath();
676 $tmpFiles[$src] =
null;
678 $this->
chmod( $tmpPath );
679 $tmpFiles[$src] = $tmpFile;
701 foreach ( $fileOpHandles
as $index => $fileOpHandle ) {
702 $pipes[$index] = popen(
"{$fileOpHandle->cmd} 2>&1",
'r' );
706 foreach ( $pipes
as $index => $pipe ) {
709 $errs[$index] = stream_get_contents( $pipe );
713 foreach ( $fileOpHandles
as $index => $fileOpHandle ) {
715 $function = $fileOpHandle->call;
716 $function( $errs[$index],
$status, $fileOpHandle->params, $fileOpHandle->cmd );
718 if (
$status->isOK() && $fileOpHandle->chmodPath ) {
719 $this->
chmod( $fileOpHandle->chmodPath );
756 return "Deny from all\n";
766 return $this->isWindows ? strtr(
$path,
'/',
'\\' ) :
$path;
773 $this->hadWarningErrors[] =
false;
774 set_error_handler( [ $this,
'handleWarning' ], E_WARNING );
783 restore_error_handler();
784 return array_pop( $this->hadWarningErrors );
794 $this->logger->error( $errstr );
795 $this->hadWarningErrors[
count( $this->hadWarningErrors ) - 1] =
true;
852 if (
$path ===
false ) {
855 $this->suffixStart = strlen(
$path ) + 1;
860 }
catch ( UnexpectedValueException
$e ) {
872 if ( !empty( $this->params[
'topOnly'] ) ) {
873 # Get an iterator that will get direct sub-nodes
874 return new DirectoryIterator(
$dir );
876 # Get an iterator that will return leaf nodes (non-directories)
877 # RecursiveDirectoryIterator extends FilesystemIterator.
878 # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
879 $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
881 return new RecursiveIteratorIterator(
882 new RecursiveDirectoryIterator(
$dir,
$flags ),
883 RecursiveIteratorIterator::CHILD_FIRST
901 return $this->
getRelPath( $this->iter->current()->getPathname() );
912 }
catch ( UnexpectedValueException
$e ) {
913 throw new FileBackendError(
"File iterator gave UnexpectedValueException." );
925 $this->iter->rewind();
927 }
catch ( UnexpectedValueException
$e ) {
928 throw new FileBackendError(
"File iterator gave UnexpectedValueException." );
937 return $this->iter && $this->iter->valid();
955 if (
$path ===
false ) {
959 return strtr( substr(
$path, $this->suffixStart ),
'\\',
'/' );
965 while ( $this->iter->valid() ) {
966 if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
977 while ( $this->iter->valid() ) {
978 if ( !$this->iter->current()->isFile() ) {