43 use Wikimedia\AtEase\AtEase;
44 use 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',
241 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
242 $status->fatal(
'backend-fail-create', $params[
'dst'] );
243 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
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',
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 );
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',
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 );
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;
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 );
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";
448 if ( $errors !==
'' && !( $this->isWindows && $errors[0] ===
" " ) ) {
449 $status->fatal(
'backend-fail-delete', $params[
'src'] );
450 trigger_error(
"$cmd\n$errors", E_USER_WARNING );
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 );
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 );