* @file
* @ingroup FileBackend
*/
+use Wikimedia\AtEase\AtEase;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
protected $basePath;
/** @var array Map of container names to root paths for custom container paths */
- protected $containerPaths = [];
+ protected $containerPaths;
/** @var int File permission mode */
protected $fileMode;
/** @var string Required OS username to own files */
protected $fileOwner;
- /** @var bool */
+ /** @var bool Whether the OS is Windows (otherwise assumed Unix-like)*/
protected $isWindows;
/** @var string OS username running this script */
protected $currentUser;
$this->basePath = null; // none; containers must have explicit paths
}
- if ( isset( $config['containerPaths'] ) ) {
- $this->containerPaths = (array)$config['containerPaths'];
- foreach ( $this->containerPaths as &$path ) {
- $path = rtrim( $path, '/' ); // remove trailing slash
- }
+ $this->containerPaths = [];
+ foreach ( ( $config['containerPaths'] ?? [] ) as $container => $path ) {
+ $this->containerPaths[$container] = rtrim( $path, '/' ); // remove trailing slash
}
$this->fileMode = $config['fileMode'] ?? 0644;
}
}
- return null;
+ return null; // invalid
}
/**
}
if ( !empty( $params['async'] ) ) { // deferred
- $tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' );
+ $tempFile = $this->stageContentAsTempFile( $params );
if ( !$tempFile ) {
$status->fatal( 'backend-fail-create', $params['dst'] );
return $status;
}
- $this->trapWarnings();
- $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
- $this->untrapWarnings();
- if ( $bytes === false ) {
- $status->fatal( 'backend-fail-create', $params['dst'] );
-
- return $status;
- }
$cmd = implode( ' ', [
$this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
escapeshellarg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
};
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
} else { // immediate write
- $this->trapWarnings();
- $ok = unlink( $source );
- $this->untrapWarnings();
+ $ok = $this->unlink( $source );
if ( !$ok ) {
$status->fatal( 'backend-fail-delete', $params['src'] );
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
$existed = is_dir( $dir ); // already there?
// Create the directory and its parents as needed...
- $this->trapWarnings();
+ AtEase::suppressWarnings();
if ( !$existed && !mkdir( $dir, $this->dirMode, true ) && !is_dir( $dir ) ) {
$this->logger->error( __METHOD__ . ": cannot create directory $dir" );
$status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
$this->logger->error( __METHOD__ . ": directory $dir is not readable" );
$status->fatal( 'directorynotreadableerror', $params['dir'] );
}
- $this->untrapWarnings();
+ AtEase::restoreWarnings();
// Respect any 'noAccess' or 'noListing' flags...
if ( is_dir( $dir ) && !$existed ) {
$status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
}
// Add a .htaccess file to the root of the container...
if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
- $this->trapWarnings();
+ AtEase::suppressWarnings();
$bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
- $this->untrapWarnings();
+ AtEase::restoreWarnings();
if ( $bytes === false ) {
$storeDir = "mwstore://{$this->name}/{$shortCont}";
$status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
// Unseed new directories with a blank index.html, to allow crawling...
if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
$exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
- $this->trapWarnings();
- if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
+ if ( $exists && !$this->unlink( "{$dir}/index.html" ) ) { // reverse secure()
$status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
}
- $this->untrapWarnings();
}
// Remove the .htaccess file from the root of the container...
if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
$exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
- $this->trapWarnings();
- if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
+ if ( $exists && !$this->unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
$storeDir = "mwstore://{$this->name}/{$shortCont}";
$status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
}
- $this->untrapWarnings();
}
return $status;
list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
- $this->trapWarnings();
+ AtEase::suppressWarnings();
if ( is_dir( $dir ) ) {
rmdir( $dir ); // remove directory if empty
}
- $this->untrapWarnings();
+ AtEase::restoreWarnings();
return $status;
}
protected function doGetFileStat( array $params ) {
$source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
- return false; // invalid storage path
+ return self::$RES_ERROR; // invalid storage path
}
$this->trapWarnings(); // don't trust 'false' if there were errors
$stat = is_file( $source ) ? stat( $source ) : false; // regular files only
$hadError = $this->untrapWarnings();
- if ( $stat ) {
+ if ( is_array( $stat ) ) {
$ct = new ConvertibleTimestamp( $stat['mtime'] );
return [
'mtime' => $ct->getTimestamp( TS_MW ),
'size' => $stat['size']
];
- } elseif ( !$hadError ) {
- return false; // file does not exist
- } else {
- return null; // failure
}
+
+ return $hadError ? self::$RES_ERROR : self::$RES_ABSENT;
}
protected function doClearCache( array $paths = null ) {
$exists = is_dir( $dir );
$hadError = $this->untrapWarnings();
- return $hadError ? null : $exists;
+ return $hadError ? self::$RES_ERROR : $exists;
}
/**
list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+ $this->trapWarnings(); // don't trust 'false' if there were errors
$exists = is_dir( $dir );
- if ( !$exists ) {
- $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+ $isReadable = $exists ? is_readable( $dir ) : false;
+ $hadError = $this->untrapWarnings();
- return []; // nothing under this dir
- } elseif ( !is_readable( $dir ) ) {
- $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+ if ( $isReadable ) {
+ return new FSFileBackendDirList( $dir, $params );
+ } elseif ( $exists ) {
+ $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'" );
- return null; // bad permissions?
- }
+ return self::$RES_ERROR; // bad permissions?
+ } elseif ( $hadError ) {
+ $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'" );
- return new FSFileBackendDirList( $dir, $params );
+ return self::$RES_ERROR;
+ } else {
+ $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'" );
+
+ return []; // nothing under this dir
+ }
}
/**
list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
$dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+
+ $this->trapWarnings(); // don't trust 'false' if there were errors
$exists = is_dir( $dir );
- if ( !$exists ) {
- $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
+ $isReadable = $exists ? is_readable( $dir ) : false;
+ $hadError = $this->untrapWarnings();
- return []; // nothing under this dir
- } elseif ( !is_readable( $dir ) ) {
- $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
+ if ( $exists && $isReadable ) {
+ return new FSFileBackendFileList( $dir, $params );
+ } elseif ( $exists ) {
+ $this->logger->warning( __METHOD__ . ": given directory is unreadable: '$dir'\n" );
- return null; // bad permissions?
- }
+ return self::$RES_ERROR; // bad permissions?
+ } elseif ( $hadError ) {
+ $this->logger->warning( __METHOD__ . ": given directory was unreachable: '$dir'\n" );
+
+ return self::$RES_ERROR;
+ } else {
+ $this->logger->info( __METHOD__ . ": given directory does not exist: '$dir'\n" );
- return new FSFileBackendFileList( $dir, $params );
+ return []; // nothing under this dir
+ }
}
protected function doGetLocalReferenceMulti( array $params ) {
foreach ( $params['srcs'] as $src ) {
$source = $this->resolveToFSPath( $src );
- if ( $source === null || !is_file( $source ) ) {
- $fsFiles[$src] = null; // invalid path or file does not exist
- } else {
+ if ( $source === null ) {
+ $fsFiles[$src] = self::$RES_ERROR; // invalid path
+ continue;
+ }
+
+ $this->trapWarnings(); // don't trust 'false' if there were errors
+ $isFile = is_file( $source ); // regular files only
+ $hadError = $this->untrapWarnings();
+
+ if ( $isFile ) {
$fsFiles[$src] = new FSFile( $source );
+ } elseif ( $hadError ) {
+ $fsFiles[$src] = self::$RES_ERROR;
+ } else {
+ $fsFiles[$src] = self::$RES_ABSENT;
}
}
foreach ( $params['srcs'] as $src ) {
$source = $this->resolveToFSPath( $src );
if ( $source === null ) {
- $tmpFiles[$src] = null; // invalid path
+ $tmpFiles[$src] = self::$RES_ERROR; // invalid path
+ continue;
+ }
+ // Create a new temporary file with the same extension...
+ $ext = FileBackend::extensionFromPath( $src );
+ $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
+ if ( !$tmpFile ) {
+ $tmpFiles[$src] = self::$RES_ERROR;
+ continue;
+ }
+
+ $tmpPath = $tmpFile->getPath();
+ // Copy the source file over the temp file
+ $this->trapWarnings();
+ $isFile = is_file( $source ); // regular files only
+ $copySuccess = $isFile ? copy( $source, $tmpPath ) : false;
+ $hadError = $this->untrapWarnings();
+
+ if ( $copySuccess ) {
+ $this->chmod( $tmpPath );
+ $tmpFiles[$src] = $tmpFile;
+ } elseif ( $hadError ) {
+ $tmpFiles[$src] = self::$RES_ERROR; // copy failed
} else {
- // Create a new temporary file with the same extension...
- $ext = FileBackend::extensionFromPath( $src );
- $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
- if ( !$tmpFile ) {
- $tmpFiles[$src] = null;
- } else {
- $tmpPath = $tmpFile->getPath();
- // Copy the source file over the temp file
- $this->trapWarnings();
- $ok = copy( $source, $tmpPath );
- $this->untrapWarnings();
- if ( !$ok ) {
- $tmpFiles[$src] = null;
- } else {
- $this->chmod( $tmpPath );
- $tmpFiles[$src] = $tmpFile;
- }
- }
+ $tmpFiles[$src] = self::$RES_ABSENT;
}
}
$statuses = [];
$pipes = [];
+ $octalPermissions = '0' . decoct( $this->fileMode );
foreach ( $fileOpHandles as $index => $fileOpHandle ) {
- $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
+ $cmd = "{$fileOpHandle->cmd} 2>&1";
+ // Add a post-operation chmod command for permissions cleanup if applicable
+ if (
+ !$this->isWindows &&
+ $fileOpHandle->chmodPath !== null &&
+ strlen( $octalPermissions ) == 4
+ ) {
+ $encPath = escapeshellarg( $fileOpHandle->chmodPath );
+ $cmd .= " && chmod $octalPermissions $encPath 2>/dev/null";
+ }
+ $pipes[$index] = popen( $cmd, 'r' );
}
$errs = [];
$function = $fileOpHandle->call;
$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
$statuses[$index] = $status;
- if ( $status->isOK() && $fileOpHandle->chmodPath ) {
- $this->chmod( $fileOpHandle->chmodPath );
- }
}
clearstatcache(); // files changed
+
return $statuses;
}
* @return bool Success
*/
protected function chmod( $path ) {
- $this->trapWarnings();
+ if ( $this->isWindows ) {
+ return true;
+ }
+
+ AtEase::suppressWarnings();
$ok = chmod( $path, $this->fileMode );
- $this->untrapWarnings();
+ AtEase::restoreWarnings();
+
+ return $ok;
+ }
+
+ /**
+ * Unlink a file, suppressing the warnings
+ *
+ * @param string $path Absolute file system path
+ * @return bool Success
+ */
+ protected function unlink( $path ) {
+ AtEase::suppressWarnings();
+ $ok = unlink( $path );
+ AtEase::restoreWarnings();
return $ok;
}
+ /**
+ * @param array $params Operation parameters with 'content' and 'headers' fields
+ * @return TempFSFile|null
+ */
+ protected function stageContentAsTempFile( array $params ) {
+ $content = $params['content'];
+ $tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' );
+ if ( !$tempFile ) {
+ return null;
+ }
+
+ AtEase::suppressWarnings();
+ $tmpPath = $tempFile->getPath();
+ if ( file_put_contents( $tmpPath, $content ) === false ) {
+ $tempFile = null;
+ }
+ AtEase::restoreWarnings();
+
+ return $tempFile;
+ }
+
/**
* Return the text of an index.html file to hide directory listings
*