2012-01-27 15:03:50 +00:00
|
|
|
<?php
|
2019-04-14 02:35:00 +00:00
|
|
|
/**
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
* @ingroup FileBackend
|
|
|
|
|
*/
|
|
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
/**
|
2012-05-07 07:11:33 +00:00
|
|
|
* File system based backend.
|
|
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
2011-12-20 03:52:06 +00:00
|
|
|
* @file
|
|
|
|
|
* @ingroup FileBackend
|
|
|
|
|
*/
|
2019-09-07 10:25:37 +00:00
|
|
|
|
2024-09-27 19:20:56 +00:00
|
|
|
namespace Wikimedia\FileBackend;
|
|
|
|
|
|
|
|
|
|
use MapCacheLRU;
|
2024-08-28 07:28:56 +00:00
|
|
|
use Shellbox\Command\BoxedCommand;
|
2021-09-21 05:40:55 +00:00
|
|
|
use Shellbox\Shellbox;
|
2024-09-27 19:20:56 +00:00
|
|
|
use StatusValue;
|
2019-09-07 05:58:29 +00:00
|
|
|
use Wikimedia\AtEase\AtEase;
|
2024-09-27 19:20:56 +00:00
|
|
|
use Wikimedia\FileBackend\FileIteration\FSFileBackendDirList;
|
|
|
|
|
use Wikimedia\FileBackend\FileIteration\FSFileBackendFileList;
|
|
|
|
|
use Wikimedia\FileBackend\FileOpHandle\FSFileOpHandle;
|
|
|
|
|
use Wikimedia\FileBackend\FSFile\FSFile;
|
|
|
|
|
use Wikimedia\FileBackend\FSFile\TempFSFile;
|
2016-10-02 04:51:51 +00:00
|
|
|
use Wikimedia\Timestamp\ConvertibleTimestamp;
|
2011-12-20 03:52:06 +00:00
|
|
|
|
|
|
|
|
/**
|
2012-03-03 19:14:50 +00:00
|
|
|
* @brief Class for a file system (FS) based file backend.
|
2012-04-05 05:56:08 +00:00
|
|
|
*
|
2012-01-12 18:44:00 +00:00
|
|
|
* All "containers" each map to a directory under the backend's base directory.
|
|
|
|
|
* For backwards-compatibility, some container paths can be set to custom paths.
|
2016-09-21 22:36:16 +00:00
|
|
|
* The domain ID will not be used in any custom paths, so this should be avoided.
|
2012-04-05 05:56:08 +00:00
|
|
|
*
|
2012-01-12 19:20:58 +00:00
|
|
|
* Having directories with thousands of files will diminish performance.
|
2012-01-12 18:44:00 +00:00
|
|
|
* Sharding can be accomplished by using FileRepo-style hash paths.
|
2012-01-04 01:08:33 +00:00
|
|
|
*
|
2016-09-16 22:55:40 +00:00
|
|
|
* StatusValue messages should avoid mentioning the internal FS paths.
|
2012-02-24 20:10:36 +00:00
|
|
|
* PHP warnings are assumed to be logged rather than output.
|
2011-12-20 03:52:06 +00:00
|
|
|
*
|
|
|
|
|
* @ingroup FileBackend
|
2012-01-13 23:30:46 +00:00
|
|
|
* @since 1.19
|
2011-12-20 03:52:06 +00:00
|
|
|
*/
|
2012-01-29 22:22:28 +00:00
|
|
|
class FSFileBackend extends FileBackendStore {
|
2021-12-30 12:44:13 +00:00
|
|
|
/** @var MapCacheLRU Cache for known prepared/usable directories */
|
2019-09-10 05:02:19 +00:00
|
|
|
protected $usableDirCache;
|
|
|
|
|
|
2022-02-26 07:54:16 +00:00
|
|
|
/** @var string|null Directory holding the container directories */
|
2013-11-23 18:23:32 +00:00
|
|
|
protected $basePath;
|
|
|
|
|
|
2023-10-16 10:27:38 +00:00
|
|
|
/** @var array<string,string> Map of container names to root paths for custom container paths */
|
2019-09-07 05:58:29 +00:00
|
|
|
protected $containerPaths;
|
2013-11-23 18:23:32 +00:00
|
|
|
|
2019-09-07 06:21:48 +00:00
|
|
|
/** @var int Directory permission mode */
|
|
|
|
|
protected $dirMode;
|
2013-11-23 18:23:32 +00:00
|
|
|
/** @var int File permission mode */
|
|
|
|
|
protected $fileMode;
|
|
|
|
|
/** @var string Required OS username to own files */
|
|
|
|
|
protected $fileOwner;
|
|
|
|
|
|
2022-03-08 22:57:00 +00:00
|
|
|
/** @var string Simpler version of PHP_OS_FAMILY */
|
2019-09-07 10:25:37 +00:00
|
|
|
protected $os;
|
2013-11-23 18:23:32 +00:00
|
|
|
/** @var string OS username running this script */
|
|
|
|
|
protected $currentUser;
|
|
|
|
|
|
2019-09-07 06:21:48 +00:00
|
|
|
/** @var bool[] Map of (stack index => whether a warning happened) */
|
|
|
|
|
private $warningTrapStack = [];
|
2012-01-28 17:05:20 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
/**
|
2012-01-29 22:22:28 +00:00
|
|
|
* @see FileBackendStore::__construct()
|
2011-12-20 03:52:06 +00:00
|
|
|
* Additional $config params include:
|
2012-07-18 19:08:30 +00:00
|
|
|
* - basePath : File system directory that holds containers.
|
|
|
|
|
* - containerPaths : Map of container names to custom file system directories.
|
|
|
|
|
* This should only be used for backwards-compatibility.
|
|
|
|
|
* - fileMode : Octal UNIX file permissions to use on files stored.
|
2016-09-21 22:34:46 +00:00
|
|
|
* - directoryMode : Octal UNIX file permissions to use on directories created.
|
2015-12-29 09:46:05 +00:00
|
|
|
* @param array $config
|
2011-12-20 03:52:06 +00:00
|
|
|
*/
|
2012-01-04 01:08:33 +00:00
|
|
|
public function __construct( array $config ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
parent::__construct( $config );
|
2012-02-08 09:16:19 +00:00
|
|
|
|
2020-06-13 18:54:36 +00:00
|
|
|
if ( PHP_OS_FAMILY === 'Windows' ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
$this->os = 'Windows';
|
2020-06-13 18:54:36 +00:00
|
|
|
} elseif ( PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin' ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
$this->os = 'BSD';
|
|
|
|
|
} else {
|
|
|
|
|
$this->os = 'Linux';
|
|
|
|
|
}
|
2012-02-08 09:16:19 +00:00
|
|
|
// Remove any possible trailing slash from directories
|
2012-01-12 18:44:00 +00:00
|
|
|
if ( isset( $config['basePath'] ) ) {
|
2012-02-08 09:54:44 +00:00
|
|
|
$this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
|
2012-01-12 18:44:00 +00:00
|
|
|
} else {
|
|
|
|
|
$this->basePath = null; // none; containers must have explicit paths
|
|
|
|
|
}
|
2012-02-08 09:16:19 +00:00
|
|
|
|
2019-09-07 05:58:29 +00:00
|
|
|
$this->containerPaths = [];
|
2019-09-07 10:25:37 +00:00
|
|
|
foreach ( ( $config['containerPaths'] ?? [] ) as $container => $fsPath ) {
|
|
|
|
|
$this->containerPaths[$container] = rtrim( $fsPath, '/' ); // remove trailing slash
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
2012-02-08 09:21:19 +00:00
|
|
|
|
2017-10-06 22:17:58 +00:00
|
|
|
$this->fileMode = $config['fileMode'] ?? 0644;
|
|
|
|
|
$this->dirMode = $config['directoryMode'] ?? 0777;
|
2012-08-20 20:03:50 +00:00
|
|
|
if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
|
|
|
|
|
$this->fileOwner = $config['fileOwner'];
|
2019-09-10 05:02:19 +00:00
|
|
|
// Cache this, assuming it doesn't change
|
2016-02-17 19:54:59 +00:00
|
|
|
$this->currentUser = posix_getpwuid( posix_getuid() )['name'];
|
2012-08-20 20:03:50 +00:00
|
|
|
}
|
2019-09-10 05:02:19 +00:00
|
|
|
|
|
|
|
|
$this->usableDirCache = new MapCacheLRU( self::CACHE_CHEAP_SIZE );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2014-04-12 06:50:58 +00:00
|
|
|
public function getFeatures() {
|
2019-10-12 21:42:58 +00:00
|
|
|
return self::ATTR_UNICODE_PATHS;
|
2014-04-12 06:50:58 +00:00
|
|
|
}
|
|
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
protected function resolveContainerPath( $container, $relStoragePath ) {
|
2012-02-08 09:00:31 +00:00
|
|
|
// Check that container has a root directory
|
2012-01-12 18:44:00 +00:00
|
|
|
if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
|
2021-11-22 13:35:17 +00:00
|
|
|
// Check for sensible relative paths (assume the base paths are OK)
|
2012-02-08 09:00:31 +00:00
|
|
|
if ( $this->isLegalRelPath( $relStoragePath ) ) {
|
|
|
|
|
return $relStoragePath;
|
|
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2019-08-30 07:01:29 +00:00
|
|
|
return null; // invalid
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2012-02-08 09:00:31 +00:00
|
|
|
/**
|
2021-11-19 23:19:42 +00:00
|
|
|
* Check a relative file system path for validity
|
2012-04-05 05:56:08 +00:00
|
|
|
*
|
2019-09-07 10:25:37 +00:00
|
|
|
* @param string $fsPath Normalized relative path
|
2012-02-09 00:24:11 +00:00
|
|
|
* @return bool
|
2012-02-08 09:00:31 +00:00
|
|
|
*/
|
2019-09-07 10:25:37 +00:00
|
|
|
protected function isLegalRelPath( $fsPath ) {
|
2012-02-08 09:00:31 +00:00
|
|
|
// Check for file names longer than 255 chars
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( preg_match( '![^/]{256}!', $fsPath ) ) { // ext3/NTFS
|
2012-02-08 09:00:31 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $this->os === 'Windows' ) { // NTFS
|
|
|
|
|
return !preg_match( '![:*?"<>|]!', $fsPath );
|
2012-02-08 09:00:31 +00:00
|
|
|
} else {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-12 18:44:00 +00:00
|
|
|
/**
|
|
|
|
|
* Given the short (unresolved) and full (resolved) name of
|
|
|
|
|
* a container, return the file system path of the container.
|
2012-04-05 05:56:08 +00:00
|
|
|
*
|
2013-06-13 18:18:52 +00:00
|
|
|
* @param string $shortCont
|
|
|
|
|
* @param string $fullCont
|
2012-04-05 05:56:08 +00:00
|
|
|
* @return string|null
|
2012-01-12 18:44:00 +00:00
|
|
|
*/
|
|
|
|
|
protected function containerFSRoot( $shortCont, $fullCont ) {
|
|
|
|
|
if ( isset( $this->containerPaths[$shortCont] ) ) {
|
2012-04-05 05:56:08 +00:00
|
|
|
return $this->containerPaths[$shortCont];
|
2012-01-12 18:44:00 +00:00
|
|
|
} elseif ( isset( $this->basePath ) ) {
|
|
|
|
|
return "{$this->basePath}/{$fullCont}";
|
|
|
|
|
}
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-01-12 18:44:00 +00:00
|
|
|
return null; // no container base path defined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the absolute file system path for a storage path
|
2012-04-05 05:56:08 +00:00
|
|
|
*
|
2021-11-26 15:21:17 +00:00
|
|
|
* @param string $storagePath
|
2012-01-12 18:44:00 +00:00
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
protected function resolveToFSPath( $storagePath ) {
|
|
|
|
|
[ $fullCont, $relPath ] = $this->resolveStoragePathReal( $storagePath );
|
|
|
|
|
if ( $relPath === null ) {
|
|
|
|
|
return null; // invalid
|
|
|
|
|
}
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $storagePath );
|
2012-01-12 18:44:00 +00:00
|
|
|
$fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
|
|
|
|
if ( $relPath != '' ) {
|
|
|
|
|
$fsPath .= "/{$relPath}";
|
|
|
|
|
}
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-01-12 18:44:00 +00:00
|
|
|
return $fsPath;
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-19 23:18:03 +00:00
|
|
|
public function isPathUsableInternal( $storagePath ) {
|
|
|
|
|
$fsPath = $this->resolveToFSPath( $storagePath );
|
|
|
|
|
if ( $fsPath === null ) {
|
|
|
|
|
return false; // invalid
|
|
|
|
|
}
|
|
|
|
|
|
2012-08-20 20:03:50 +00:00
|
|
|
if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
|
|
|
|
|
trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
|
2019-09-10 05:02:19 +00:00
|
|
|
return false;
|
2012-08-20 20:03:50 +00:00
|
|
|
}
|
|
|
|
|
|
2019-09-10 05:02:19 +00:00
|
|
|
$fsDirectory = dirname( $fsPath );
|
|
|
|
|
$usable = $this->usableDirCache->get( $fsDirectory, MapCacheLRU::TTL_PROC_SHORT );
|
|
|
|
|
if ( $usable === null ) {
|
|
|
|
|
AtEase::suppressWarnings();
|
|
|
|
|
$usable = is_dir( $fsDirectory ) && is_writable( $fsDirectory );
|
|
|
|
|
AtEase::restoreWarnings();
|
|
|
|
|
$this->usableDirCache->set( $fsDirectory, $usable ? 1 : 0 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $usable;
|
2012-01-19 23:18:03 +00:00
|
|
|
}
|
|
|
|
|
|
2012-11-08 17:50:00 +00:00
|
|
|
protected function doCreateInternal( array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2012-11-08 17:50:00 +00:00
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
$fsDstPath = $this->resolveToFSPath( $params['dst'] );
|
|
|
|
|
if ( $fsDstPath === null ) {
|
2012-11-08 17:50:00 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-11-08 17:50:00 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !empty( $params['async'] ) ) { // deferred
|
2019-09-07 10:25:37 +00:00
|
|
|
$tempFile = $this->newTempFileWithContent( $params );
|
2012-11-08 17:50:00 +00:00
|
|
|
if ( !$tempFile ) {
|
|
|
|
|
$status->fatal( 'backend-fail-create', $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-11-08 17:50:00 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
$cmd = $this->makeCopyCommand( $tempFile->getPath(), $fsDstPath, false );
|
2016-09-16 22:55:40 +00:00
|
|
|
$handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
|
2014-06-12 20:40:03 +00:00
|
|
|
$status->fatal( 'backend-fail-create', $params['dst'] );
|
|
|
|
|
trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
|
|
|
|
|
}
|
|
|
|
|
};
|
2019-09-07 10:25:37 +00:00
|
|
|
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
|
2012-11-08 17:50:00 +00:00
|
|
|
$tempFile->bind( $status->value );
|
|
|
|
|
} else { // immediate write
|
2019-09-07 10:25:37 +00:00
|
|
|
$created = false;
|
|
|
|
|
// Use fwrite+rename since (a) this clears xattrs, (b) threads still reading the old
|
|
|
|
|
// inode are unaffected since it writes to a new inode, and (c) new threads reading
|
|
|
|
|
// the file will either totally see the old version or totally see the new version
|
|
|
|
|
$fsStagePath = $this->makeStagingPath( $fsDstPath );
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->trapWarningsIgnoringNotFound();
|
2019-09-07 10:25:37 +00:00
|
|
|
$stageHandle = fopen( $fsStagePath, 'xb' );
|
|
|
|
|
if ( $stageHandle ) {
|
|
|
|
|
$bytes = fwrite( $stageHandle, $params['content'] );
|
|
|
|
|
$created = ( $bytes === strlen( $params['content'] ) );
|
|
|
|
|
fclose( $stageHandle );
|
|
|
|
|
$created = $created ? rename( $fsStagePath, $fsDstPath ) : false;
|
|
|
|
|
}
|
|
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
if ( $hadError || !$created ) {
|
2012-11-08 17:50:00 +00:00
|
|
|
$status->fatal( 'backend-fail-create', $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-11-08 17:50:00 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
$this->chmod( $fsDstPath );
|
2012-11-08 17:50:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-21 09:16:28 +00:00
|
|
|
protected function doStoreInternal( array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
$fsSrcPath = $params['src']; // file system path
|
|
|
|
|
$fsDstPath = $this->resolveToFSPath( $params['dst'] );
|
|
|
|
|
if ( $fsDstPath === null ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
2012-01-12 18:44:00 +00:00
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $fsSrcPath === $fsDstPath ) {
|
|
|
|
|
$status->fatal( 'backend-fail-internal', $this->name );
|
|
|
|
|
|
2021-11-19 23:19:42 +00:00
|
|
|
return $status;
|
2019-09-07 10:25:37 +00:00
|
|
|
}
|
|
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
if ( !empty( $params['async'] ) ) { // deferred
|
2019-09-07 10:25:37 +00:00
|
|
|
$cmd = $this->makeCopyCommand( $fsSrcPath, $fsDstPath, false );
|
2016-09-16 22:55:40 +00:00
|
|
|
$handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
|
2014-06-12 20:40:03 +00:00
|
|
|
$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
|
|
|
|
|
trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
|
|
|
|
|
}
|
|
|
|
|
};
|
2019-09-07 10:25:37 +00:00
|
|
|
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
|
2012-04-11 17:51:02 +00:00
|
|
|
} else { // immediate write
|
2019-09-07 10:25:37 +00:00
|
|
|
$stored = false;
|
|
|
|
|
// Use fwrite+rename since (a) this clears xattrs, (b) threads still reading the old
|
|
|
|
|
// inode are unaffected since it writes to a new inode, and (c) new threads reading
|
|
|
|
|
// the file will either totally see the old version or totally see the new version
|
|
|
|
|
$fsStagePath = $this->makeStagingPath( $fsDstPath );
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->trapWarningsIgnoringNotFound();
|
2019-09-07 10:25:37 +00:00
|
|
|
$srcHandle = fopen( $fsSrcPath, 'rb' );
|
|
|
|
|
if ( $srcHandle ) {
|
|
|
|
|
$stageHandle = fopen( $fsStagePath, 'xb' );
|
|
|
|
|
if ( $stageHandle ) {
|
|
|
|
|
$bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
|
|
|
|
|
$stored = ( $bytes !== false && $bytes === fstat( $srcHandle )['size'] );
|
|
|
|
|
fclose( $stageHandle );
|
|
|
|
|
$stored = $stored ? rename( $fsStagePath, $fsDstPath ) : false;
|
2012-04-11 17:51:02 +00:00
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
fclose( $srcHandle );
|
|
|
|
|
}
|
|
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
if ( $hadError || !$stored ) {
|
2012-04-11 17:51:02 +00:00
|
|
|
$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
return $status;
|
2012-05-04 22:23:57 +00:00
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
$this->chmod( $fsDstPath );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-21 09:16:28 +00:00
|
|
|
protected function doCopyInternal( array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
$fsSrcPath = $this->resolveToFSPath( $params['src'] );
|
|
|
|
|
if ( $fsSrcPath === null ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
$fsDstPath = $this->resolveToFSPath( $params['dst'] );
|
|
|
|
|
if ( $fsDstPath === null ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $fsSrcPath === $fsDstPath ) {
|
|
|
|
|
return $status; // no-op
|
2012-10-29 19:57:04 +00:00
|
|
|
}
|
|
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
$ignoreMissing = !empty( $params['ignoreMissingSource'] );
|
|
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
if ( !empty( $params['async'] ) ) { // deferred
|
2019-09-07 10:25:37 +00:00
|
|
|
$cmd = $this->makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
|
2016-09-16 22:55:40 +00:00
|
|
|
$handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
|
2014-06-12 20:40:03 +00:00
|
|
|
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
|
|
|
|
|
trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
|
|
|
|
|
}
|
|
|
|
|
};
|
2019-09-07 10:25:37 +00:00
|
|
|
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
|
2012-04-11 17:51:02 +00:00
|
|
|
} else { // immediate write
|
2019-09-07 10:25:37 +00:00
|
|
|
$copied = false;
|
|
|
|
|
// Use fwrite+rename since (a) this clears xattrs, (b) threads still reading the old
|
|
|
|
|
// inode are unaffected since it writes to a new inode, and (c) new threads reading
|
|
|
|
|
// the file will either totally see the old version or totally see the new version
|
|
|
|
|
$fsStagePath = $this->makeStagingPath( $fsDstPath );
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->trapWarningsIgnoringNotFound();
|
2019-09-07 10:25:37 +00:00
|
|
|
$srcHandle = fopen( $fsSrcPath, 'rb' );
|
|
|
|
|
if ( $srcHandle ) {
|
|
|
|
|
$stageHandle = fopen( $fsStagePath, 'xb' );
|
|
|
|
|
if ( $stageHandle ) {
|
|
|
|
|
$bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
|
|
|
|
|
$copied = ( $bytes !== false && $bytes === fstat( $srcHandle )['size'] );
|
|
|
|
|
fclose( $stageHandle );
|
|
|
|
|
$copied = $copied ? rename( $fsStagePath, $fsDstPath ) : false;
|
2012-04-11 17:51:02 +00:00
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
fclose( $srcHandle );
|
|
|
|
|
}
|
|
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
if ( $hadError || ( !$copied && !$ignoreMissing ) ) {
|
2012-04-11 17:51:02 +00:00
|
|
|
$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
return $status;
|
2012-05-04 22:23:57 +00:00
|
|
|
}
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $copied ) {
|
|
|
|
|
$this->chmod( $fsDstPath );
|
|
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-21 09:16:28 +00:00
|
|
|
protected function doMoveInternal( array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2019-09-07 08:25:19 +00:00
|
|
|
$fsSrcPath = $this->resolveToFSPath( $params['src'] );
|
|
|
|
|
if ( $fsSrcPath === null ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
2012-01-12 18:44:00 +00:00
|
|
|
|
2019-09-07 08:25:19 +00:00
|
|
|
$fsDstPath = $this->resolveToFSPath( $params['dst'] );
|
|
|
|
|
if ( $fsDstPath === null ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-07 08:25:19 +00:00
|
|
|
if ( $fsSrcPath === $fsDstPath ) {
|
|
|
|
|
return $status; // no-op
|
2012-10-29 19:57:04 +00:00
|
|
|
}
|
|
|
|
|
|
2019-09-07 08:25:19 +00:00
|
|
|
$ignoreMissing = !empty( $params['ignoreMissingSource'] );
|
|
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
if ( !empty( $params['async'] ) ) { // deferred
|
2019-09-07 10:25:37 +00:00
|
|
|
$cmd = $this->makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
|
2016-09-16 22:55:40 +00:00
|
|
|
$handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
|
2014-06-12 20:40:03 +00:00
|
|
|
$status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
|
|
|
|
|
trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
|
2012-04-11 17:51:02 +00:00
|
|
|
} else { // immediate write
|
2019-09-07 08:25:19 +00:00
|
|
|
// Use rename() here since (a) this clears xattrs, (b) any threads still reading the
|
|
|
|
|
// old inode are unaffected since it writes to a new inode, and (c) this is fast and
|
|
|
|
|
// atomic within a file system volume (as is normally the case)
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->trapWarningsIgnoringNotFound();
|
2019-09-07 08:25:19 +00:00
|
|
|
$moved = rename( $fsSrcPath, $fsDstPath );
|
|
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
if ( $hadError || ( !$moved && !$ignoreMissing ) ) {
|
2012-04-11 17:51:02 +00:00
|
|
|
$status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-21 09:16:28 +00:00
|
|
|
protected function doDeleteInternal( array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2019-09-07 06:21:48 +00:00
|
|
|
$fsSrcPath = $this->resolveToFSPath( $params['src'] );
|
|
|
|
|
if ( $fsSrcPath === null ) {
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-07 06:21:48 +00:00
|
|
|
$ignoreMissing = !empty( $params['ignoreMissingSource'] );
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
if ( !empty( $params['async'] ) ) { // deferred
|
2019-09-07 10:25:37 +00:00
|
|
|
$cmd = $this->makeUnlinkCommand( $fsSrcPath, $ignoreMissing );
|
2016-09-16 22:55:40 +00:00
|
|
|
$handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
|
2014-06-12 20:40:03 +00:00
|
|
|
$status->fatal( 'backend-fail-delete', $params['src'] );
|
|
|
|
|
trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
$status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
|
2012-04-11 17:51:02 +00:00
|
|
|
} else { // immediate write
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->trapWarningsIgnoringNotFound();
|
2019-09-07 06:21:48 +00:00
|
|
|
$deleted = unlink( $fsSrcPath );
|
|
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
if ( $hadError || ( !$deleted && !$ignoreMissing ) ) {
|
2012-04-11 17:51:02 +00:00
|
|
|
$status->fatal( 'backend-fail-delete', $params['src'] );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-23 18:23:32 +00:00
|
|
|
/**
|
Use DeletePage in FileDeleteForm and fix output of ApiDelete
- Use DeletePage in FileDeleteForm instead of
WikiPage::doDeleteArticleReal
- Properly handle scheduled deletions in FileDeleteForm: previously, a
null status value could indicate a missing page OR a scheduled
deletion, but the code always assumed the first, and it would generate
a duplicated log entry. The API response would also not contain the
"delete-scheduled" message. This has been broken since the introduction
of scheduled deletion.
- In ApiDelete, for file deletions, check whether the status is OK not
good. The two might be equivalent, but this way it's more consistent.
- Add some documentation for the Status objects returned by file-related
methods. This is still incomplete, as there are many methods using
Status and none of them says what the status could be. In particular,
this means that for now we keep checking whether the status is OK
instead of good, even though it's unclear what could produce a
non-fatal error.
- In LocalFileDeleteBatch, avoid using a class property for the returned
status, as that's hard to follow. Instead, use a local variable and
pass it around when needed.
Bug: T288758
Change-Id: I22d60c05bdd4a3ea531e63dbb9e49efc36935137
2021-10-12 12:42:16 +00:00
|
|
|
* @inheritDoc
|
2013-11-23 18:23:32 +00:00
|
|
|
*/
|
2012-01-12 18:44:00 +00:00
|
|
|
protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-01-12 18:44:00 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
2012-12-31 19:16:05 +00:00
|
|
|
// Create the directory and its parents as needed...
|
2019-09-10 05:02:19 +00:00
|
|
|
$created = false;
|
2019-09-07 05:58:29 +00:00
|
|
|
AtEase::suppressWarnings();
|
2019-09-10 19:41:00 +00:00
|
|
|
$alreadyExisted = is_dir( $fsDirectory ); // already there?
|
2019-09-10 05:02:19 +00:00
|
|
|
if ( !$alreadyExisted ) {
|
2019-09-10 19:41:00 +00:00
|
|
|
$created = mkdir( $fsDirectory, $this->dirMode, true );
|
2019-09-10 05:02:19 +00:00
|
|
|
if ( !$created ) {
|
2019-09-10 19:41:00 +00:00
|
|
|
$alreadyExisted = is_dir( $fsDirectory ); // another thread made it?
|
2019-09-10 05:02:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
2019-09-10 19:41:00 +00:00
|
|
|
$isWritable = $created ?: is_writable( $fsDirectory ); // assume writable if created here
|
2019-09-10 05:02:19 +00:00
|
|
|
AtEase::restoreWarnings();
|
|
|
|
|
if ( !$alreadyExisted && !$created ) {
|
2019-09-10 19:41:00 +00:00
|
|
|
$this->logger->error( __METHOD__ . ": cannot create directory $fsDirectory" );
|
2012-05-21 22:19:06 +00:00
|
|
|
$status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
|
2019-09-10 05:02:19 +00:00
|
|
|
} elseif ( !$isWritable ) {
|
2019-09-10 19:41:00 +00:00
|
|
|
$this->logger->error( __METHOD__ . ": directory $fsDirectory is read-only" );
|
2011-12-20 03:52:06 +00:00
|
|
|
$status->fatal( 'directoryreadonlyerror', $params['dir'] );
|
|
|
|
|
}
|
2012-12-31 19:16:05 +00:00
|
|
|
// Respect any 'noAccess' or 'noListing' flags...
|
2019-09-10 05:02:19 +00:00
|
|
|
if ( $created ) {
|
2012-05-21 22:19:06 +00:00
|
|
|
$status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
|
|
|
|
|
}
|
2013-11-22 21:17:15 +00:00
|
|
|
|
Use DeletePage in FileDeleteForm and fix output of ApiDelete
- Use DeletePage in FileDeleteForm instead of
WikiPage::doDeleteArticleReal
- Properly handle scheduled deletions in FileDeleteForm: previously, a
null status value could indicate a missing page OR a scheduled
deletion, but the code always assumed the first, and it would generate
a duplicated log entry. The API response would also not contain the
"delete-scheduled" message. This has been broken since the introduction
of scheduled deletion.
- In ApiDelete, for file deletions, check whether the status is OK not
good. The two might be equivalent, but this way it's more consistent.
- Add some documentation for the Status objects returned by file-related
methods. This is still incomplete, as there are many methods using
Status and none of them says what the status could be. In particular,
this means that for now we keep checking whether the status is OK
instead of good, even though it's unclear what could produce a
non-fatal error.
- In LocalFileDeleteBatch, avoid using a class property for the returned
status, as that's hard to follow. Instead, use a local variable and
pass it around when needed.
Bug: T288758
Change-Id: I22d60c05bdd4a3ea531e63dbb9e49efc36935137
2021-10-12 12:42:16 +00:00
|
|
|
if ( $status->isGood() ) {
|
2019-09-10 19:41:00 +00:00
|
|
|
$this->usableDirCache->set( $fsDirectory, 1 );
|
2019-09-10 05:02:19 +00:00
|
|
|
}
|
|
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-12 18:44:00 +00:00
|
|
|
protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-01-12 18:44:00 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
2012-01-04 01:08:33 +00:00
|
|
|
// Seed new directories with a blank index.html, to prevent crawling...
|
2019-09-10 19:41:00 +00:00
|
|
|
if ( !empty( $params['noListing'] ) && !is_file( "{$fsDirectory}/index.html" ) ) {
|
2012-12-31 19:16:05 +00:00
|
|
|
$this->trapWarnings();
|
2019-09-10 19:41:00 +00:00
|
|
|
$bytes = file_put_contents( "{$fsDirectory}/index.html", $this->indexHtmlPrivate() );
|
2012-12-31 19:16:05 +00:00
|
|
|
$this->untrapWarnings();
|
2012-05-21 22:19:06 +00:00
|
|
|
if ( $bytes === false ) {
|
2012-01-04 01:08:33 +00:00
|
|
|
$status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
2012-01-04 01:08:33 +00:00
|
|
|
// Add a .htaccess file to the root of the container...
|
2019-09-10 05:02:19 +00:00
|
|
|
if ( !empty( $params['noAccess'] ) && !is_file( "{$contRoot}/.htaccess" ) ) {
|
2019-09-07 05:58:29 +00:00
|
|
|
AtEase::suppressWarnings();
|
2012-05-21 22:19:06 +00:00
|
|
|
$bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
|
2019-09-07 05:58:29 +00:00
|
|
|
AtEase::restoreWarnings();
|
2012-05-21 22:19:06 +00:00
|
|
|
if ( $bytes === false ) {
|
|
|
|
|
$storeDir = "mwstore://{$this->name}/{$shortCont}";
|
|
|
|
|
$status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2012-05-21 22:19:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-05-21 22:19:06 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
2012-05-21 22:19:06 +00:00
|
|
|
// Unseed new directories with a blank index.html, to allow crawling...
|
2019-09-10 19:41:00 +00:00
|
|
|
if ( !empty( $params['listing'] ) && is_file( "{$fsDirectory}/index.html" ) ) {
|
|
|
|
|
$exists = ( file_get_contents( "{$fsDirectory}/index.html" ) === $this->indexHtmlPrivate() );
|
|
|
|
|
if ( $exists && !$this->unlink( "{$fsDirectory}/index.html" ) ) { // reverse secure()
|
2012-05-21 22:19:06 +00:00
|
|
|
$status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 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() );
|
2019-09-07 05:58:29 +00:00
|
|
|
if ( $exists && !$this->unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
|
2012-05-21 22:19:06 +00:00
|
|
|
$storeDir = "mwstore://{$this->name}/{$shortCont}";
|
|
|
|
|
$status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-12 18:44:00 +00:00
|
|
|
protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-01-12 18:44:00 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
|
|
|
|
|
|
|
|
|
$this->rmdir( $fsDirectory );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-08 08:40:00 +00:00
|
|
|
protected function doGetFileStat( array $params ) {
|
2019-09-10 05:02:19 +00:00
|
|
|
$fsSrcPath = $this->resolveToFSPath( $params['src'] );
|
|
|
|
|
if ( $fsSrcPath === null ) {
|
2023-10-16 08:00:44 +00:00
|
|
|
return self::RES_ERROR; // invalid storage path
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
2012-01-08 08:40:00 +00:00
|
|
|
|
2012-02-24 20:10:36 +00:00
|
|
|
$this->trapWarnings(); // don't trust 'false' if there were errors
|
2019-09-10 05:02:19 +00:00
|
|
|
$stat = is_file( $fsSrcPath ) ? stat( $fsSrcPath ) : false; // regular files only
|
2012-01-13 04:32:28 +00:00
|
|
|
$hadError = $this->untrapWarnings();
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2019-08-30 07:01:29 +00:00
|
|
|
if ( is_array( $stat ) ) {
|
2016-09-21 22:34:46 +00:00
|
|
|
$ct = new ConvertibleTimestamp( $stat['mtime'] );
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
return [
|
2016-09-21 22:34:46 +00:00
|
|
|
'mtime' => $ct->getTimestamp( TS_MW ),
|
2013-04-20 17:18:13 +00:00
|
|
|
'size' => $stat['size']
|
2016-02-17 09:09:32 +00:00
|
|
|
];
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
2019-08-30 07:01:29 +00:00
|
|
|
|
2023-10-16 08:00:44 +00:00
|
|
|
return $hadError ? self::RES_ERROR : self::RES_ABSENT;
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-16 18:58:33 +00:00
|
|
|
protected function doClearCache( ?array $paths = null ) {
|
2019-09-10 05:02:19 +00:00
|
|
|
if ( is_array( $paths ) ) {
|
|
|
|
|
foreach ( $paths as $path ) {
|
|
|
|
|
$fsPath = $this->resolveToFSPath( $path );
|
|
|
|
|
if ( $fsPath !== null ) {
|
|
|
|
|
clearstatcache( true, $fsPath );
|
|
|
|
|
$this->usableDirCache->clear( $fsPath );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
clearstatcache( true ); // clear the PHP file stat cache
|
|
|
|
|
$this->usableDirCache->clear();
|
|
|
|
|
}
|
2012-01-20 21:55:15 +00:00
|
|
|
}
|
|
|
|
|
|
2012-04-05 05:56:08 +00:00
|
|
|
protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-04-05 05:56:08 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
2012-04-05 05:56:08 +00:00
|
|
|
|
|
|
|
|
$this->trapWarnings(); // don't trust 'false' if there were errors
|
2019-09-10 19:41:00 +00:00
|
|
|
$exists = is_dir( $fsDirectory );
|
2012-04-05 05:56:08 +00:00
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
|
2023-10-16 08:00:44 +00:00
|
|
|
return $hadError ? self::RES_ERROR : $exists;
|
2012-04-05 05:56:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @see FileBackendStore::getDirectoryListInternal()
|
2013-11-23 18:23:32 +00:00
|
|
|
* @param string $fullCont
|
|
|
|
|
* @param string $dirRel
|
|
|
|
|
* @param array $params
|
2016-09-24 10:34:14 +00:00
|
|
|
* @return array|FSFileBackendDirList|null
|
2012-04-05 05:56:08 +00:00
|
|
|
*/
|
|
|
|
|
public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-04-05 05:56:08 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
2019-08-30 07:01:29 +00:00
|
|
|
|
2019-09-10 19:41:00 +00:00
|
|
|
$list = new FSFileBackendDirList( $fsDirectory, $params );
|
2019-09-10 05:02:19 +00:00
|
|
|
$error = $list->getLastError();
|
|
|
|
|
if ( $error !== null ) {
|
2020-11-26 02:49:24 +00:00
|
|
|
if ( $this->isFileNotFoundError( $error ) ) {
|
2019-09-10 19:41:00 +00:00
|
|
|
$this->logger->info( __METHOD__ . ": non-existant directory: '$fsDirectory'" );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2019-09-10 05:02:19 +00:00
|
|
|
return []; // nothing under this dir
|
2019-09-10 19:41:00 +00:00
|
|
|
} elseif ( is_dir( $fsDirectory ) ) {
|
|
|
|
|
$this->logger->warning( __METHOD__ . ": unreadable directory: '$fsDirectory'" );
|
2019-08-30 07:01:29 +00:00
|
|
|
|
2023-10-16 08:00:44 +00:00
|
|
|
return self::RES_ERROR; // bad permissions?
|
2019-09-10 05:02:19 +00:00
|
|
|
} else {
|
2019-09-10 19:41:00 +00:00
|
|
|
$this->logger->warning( __METHOD__ . ": unreachable directory: '$fsDirectory'" );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2023-10-16 08:00:44 +00:00
|
|
|
return self::RES_ERROR;
|
2019-09-10 05:02:19 +00:00
|
|
|
}
|
2019-08-30 07:01:29 +00:00
|
|
|
}
|
2019-09-10 05:02:19 +00:00
|
|
|
|
|
|
|
|
return $list;
|
2012-04-05 05:56:08 +00:00
|
|
|
}
|
|
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
/**
|
2012-01-29 22:22:28 +00:00
|
|
|
* @see FileBackendStore::getFileListInternal()
|
2013-11-23 18:23:32 +00:00
|
|
|
* @param string $fullCont
|
|
|
|
|
* @param string $dirRel
|
|
|
|
|
* @param array $params
|
|
|
|
|
* @return array|FSFileBackendFileList|null
|
2011-12-20 03:52:06 +00:00
|
|
|
*/
|
2012-01-12 18:44:00 +00:00
|
|
|
public function getFileListInternal( $fullCont, $dirRel, array $params ) {
|
2012-12-09 03:27:02 +00:00
|
|
|
[ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
|
2012-01-12 18:44:00 +00:00
|
|
|
$contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
|
2019-09-10 19:41:00 +00:00
|
|
|
$fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
|
2019-08-30 07:01:29 +00:00
|
|
|
|
2019-09-10 19:41:00 +00:00
|
|
|
$list = new FSFileBackendFileList( $fsDirectory, $params );
|
2019-09-10 05:02:19 +00:00
|
|
|
$error = $list->getLastError();
|
|
|
|
|
if ( $error !== null ) {
|
2020-11-26 02:49:24 +00:00
|
|
|
if ( $this->isFileNotFoundError( $error ) ) {
|
|
|
|
|
$this->logger->info( __METHOD__ . ": non-existent directory: '$fsDirectory'" );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2019-09-10 05:02:19 +00:00
|
|
|
return []; // nothing under this dir
|
2019-09-10 19:41:00 +00:00
|
|
|
} elseif ( is_dir( $fsDirectory ) ) {
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->logger->warning( __METHOD__ .
|
|
|
|
|
": unreadable directory: '$fsDirectory': $error" );
|
2013-11-22 21:17:15 +00:00
|
|
|
|
2023-10-16 08:00:44 +00:00
|
|
|
return self::RES_ERROR; // bad permissions?
|
2019-09-10 05:02:19 +00:00
|
|
|
} else {
|
2020-11-26 02:49:24 +00:00
|
|
|
$this->logger->warning( __METHOD__ .
|
|
|
|
|
": unreachable directory: '$fsDirectory': $error" );
|
2019-08-30 07:01:29 +00:00
|
|
|
|
2023-10-16 08:00:44 +00:00
|
|
|
return self::RES_ERROR;
|
2019-09-10 05:02:19 +00:00
|
|
|
}
|
2019-08-30 07:01:29 +00:00
|
|
|
}
|
2019-09-10 05:02:19 +00:00
|
|
|
|
|
|
|
|
return $list;
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2012-09-18 18:21:30 +00:00
|
|
|
protected function doGetLocalReferenceMulti( array $params ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$fsFiles = []; // (path => FSFile)
|
2012-09-18 18:21:30 +00:00
|
|
|
|
|
|
|
|
foreach ( $params['srcs'] as $src ) {
|
|
|
|
|
$source = $this->resolveToFSPath( $src );
|
2019-08-30 07:01:29 +00:00
|
|
|
if ( $source === null ) {
|
2023-10-16 08:00:44 +00:00
|
|
|
$fsFiles[$src] = self::RES_ERROR; // invalid path
|
2019-08-30 07:01:29 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->trapWarnings(); // don't trust 'false' if there were errors
|
|
|
|
|
$isFile = is_file( $source ); // regular files only
|
|
|
|
|
$hadError = $this->untrapWarnings();
|
|
|
|
|
|
|
|
|
|
if ( $isFile ) {
|
2012-09-18 18:21:30 +00:00
|
|
|
$fsFiles[$src] = new FSFile( $source );
|
2019-08-30 07:01:29 +00:00
|
|
|
} elseif ( $hadError ) {
|
2023-10-16 08:00:44 +00:00
|
|
|
$fsFiles[$src] = self::RES_ERROR;
|
2019-08-30 07:01:29 +00:00
|
|
|
} else {
|
2023-10-16 08:00:44 +00:00
|
|
|
$fsFiles[$src] = self::RES_ABSENT;
|
2012-09-18 18:21:30 +00:00
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
2012-09-18 18:21:30 +00:00
|
|
|
|
|
|
|
|
return $fsFiles;
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2012-09-18 18:21:30 +00:00
|
|
|
protected function doGetLocalCopyMulti( array $params ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$tmpFiles = []; // (path => TempFSFile)
|
2011-12-20 03:52:06 +00:00
|
|
|
|
2012-09-18 18:21:30 +00:00
|
|
|
foreach ( $params['srcs'] as $src ) {
|
|
|
|
|
$source = $this->resolveToFSPath( $src );
|
|
|
|
|
if ( $source === null ) {
|
2023-10-16 08:00:44 +00:00
|
|
|
$tmpFiles[$src] = self::RES_ERROR; // invalid path
|
2019-08-30 07:01:29 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Create a new temporary file with the same extension...
|
|
|
|
|
$ext = FileBackend::extensionFromPath( $src );
|
|
|
|
|
$tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
|
|
|
|
|
if ( !$tmpFile ) {
|
2023-10-16 08:00:44 +00:00
|
|
|
$tmpFiles[$src] = self::RES_ERROR;
|
2019-08-30 07:01:29 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tmpPath = $tmpFile->getPath();
|
|
|
|
|
// Copy the source file over the temp file
|
2019-09-07 06:21:48 +00:00
|
|
|
$this->trapWarnings(); // don't trust 'false' if there were errors
|
2019-08-30 07:01:29 +00:00
|
|
|
$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 ) {
|
2023-10-16 08:00:44 +00:00
|
|
|
$tmpFiles[$src] = self::RES_ERROR; // copy failed
|
2012-09-18 18:21:30 +00:00
|
|
|
} else {
|
2023-10-16 08:00:44 +00:00
|
|
|
$tmpFiles[$src] = self::RES_ABSENT;
|
2012-09-18 18:21:30 +00:00
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2012-09-18 18:21:30 +00:00
|
|
|
return $tmpFiles;
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
|
|
|
|
|
2024-08-28 07:28:56 +00:00
|
|
|
public function addShellboxInputFile( BoxedCommand $command, string $boxedName,
|
|
|
|
|
array $params
|
|
|
|
|
) {
|
|
|
|
|
$path = $this->resolveToFSPath( $params['src'] );
|
|
|
|
|
if ( $path === null ) {
|
|
|
|
|
return $this->newStatus( 'backend-fail-invalidpath', $params['src'] );
|
|
|
|
|
}
|
|
|
|
|
$command->inputFileFromFile( $boxedName, $path );
|
|
|
|
|
return $this->newStatus();
|
|
|
|
|
}
|
|
|
|
|
|
2012-04-26 18:40:47 +00:00
|
|
|
protected function directoriesAreVirtual() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-18 16:22:52 +00:00
|
|
|
/**
|
2015-12-29 09:46:05 +00:00
|
|
|
* @param FSFileOpHandle[] $fileOpHandles
|
2015-03-18 16:22:52 +00:00
|
|
|
*
|
2016-09-16 22:55:40 +00:00
|
|
|
* @return StatusValue[]
|
2015-03-18 16:22:52 +00:00
|
|
|
*/
|
2012-04-11 17:51:02 +00:00
|
|
|
protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$statuses = [];
|
2012-04-11 17:51:02 +00:00
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$pipes = [];
|
2012-04-11 17:51:02 +00:00
|
|
|
foreach ( $fileOpHandles as $index => $fileOpHandle ) {
|
2019-09-07 10:25:37 +00:00
|
|
|
$pipes[$index] = popen( $fileOpHandle->cmd, 'r' );
|
2012-04-11 17:51:02 +00:00
|
|
|
}
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$errs = [];
|
2012-04-11 17:51:02 +00:00
|
|
|
foreach ( $pipes as $index => $pipe ) {
|
|
|
|
|
// Result will be empty on success in *NIX. On Windows,
|
|
|
|
|
// it may be something like " 1 file(s) [copied|moved].".
|
|
|
|
|
$errs[$index] = stream_get_contents( $pipe );
|
|
|
|
|
fclose( $pipe );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $fileOpHandles as $index => $fileOpHandle ) {
|
2016-09-16 22:55:40 +00:00
|
|
|
$status = $this->newStatus();
|
2019-09-07 10:25:37 +00:00
|
|
|
$function = $fileOpHandle->callback;
|
2014-06-12 20:40:03 +00:00
|
|
|
$function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
|
2012-04-11 17:51:02 +00:00
|
|
|
$statuses[$index] = $status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $statuses;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-07 10:25:37 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $fsPath Absolute file system path
|
|
|
|
|
* @return string Absolute file system path on the same device
|
|
|
|
|
*/
|
|
|
|
|
private function makeStagingPath( $fsPath ) {
|
|
|
|
|
$time = dechex( time() ); // make it easy to find old orphans
|
|
|
|
|
$hash = \Wikimedia\base_convert( md5( basename( $fsPath ) ), 16, 36, 25 );
|
|
|
|
|
$unique = \Wikimedia\base_convert( bin2hex( random_bytes( 16 ) ), 16, 36, 25 );
|
|
|
|
|
|
|
|
|
|
return dirname( $fsPath ) . "/.{$time}_{$hash}_{$unique}.tmpfsfile";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $fsSrcPath Absolute file system path
|
|
|
|
|
* @param string $fsDstPath Absolute file system path
|
2021-12-30 12:44:13 +00:00
|
|
|
* @param bool $ignoreMissing Whether to no-op if the source file is non-existent
|
2019-09-07 10:25:37 +00:00
|
|
|
* @return string Command
|
|
|
|
|
*/
|
|
|
|
|
private function makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing ) {
|
|
|
|
|
// Use copy+rename since (a) this clears xattrs, (b) threads still reading the old
|
|
|
|
|
// inode are unaffected since it writes to a new inode, and (c) new threads reading
|
|
|
|
|
// the file will either totally see the old version or totally see the new version
|
|
|
|
|
$fsStagePath = $this->makeStagingPath( $fsDstPath );
|
2021-09-21 05:40:55 +00:00
|
|
|
$encSrc = Shellbox::escape( $this->cleanPathSlashes( $fsSrcPath ) );
|
|
|
|
|
$encStage = Shellbox::escape( $this->cleanPathSlashes( $fsStagePath ) );
|
|
|
|
|
$encDst = Shellbox::escape( $this->cleanPathSlashes( $fsDstPath ) );
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $this->os === 'Windows' ) {
|
|
|
|
|
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/copy
|
|
|
|
|
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/move
|
|
|
|
|
$cmdWrite = "COPY /B /Y $encSrc $encStage 2>&1 && MOVE /Y $encStage $encDst 2>&1";
|
|
|
|
|
$cmd = $ignoreMissing ? "IF EXIST $encSrc $cmdWrite" : $cmdWrite;
|
|
|
|
|
} else {
|
|
|
|
|
// https://manpages.debian.org/buster/coreutils/cp.1.en.html
|
|
|
|
|
// https://manpages.debian.org/buster/coreutils/mv.1.en.html
|
|
|
|
|
$cmdWrite = "cp $encSrc $encStage 2>&1 && mv $encStage $encDst 2>&1";
|
|
|
|
|
$cmd = $ignoreMissing ? "test -f $encSrc && $cmdWrite" : $cmdWrite;
|
|
|
|
|
// Clean up permissions on any newly created destination file
|
|
|
|
|
$octalPermissions = '0' . decoct( $this->fileMode );
|
|
|
|
|
if ( strlen( $octalPermissions ) == 4 ) {
|
|
|
|
|
$cmd .= " && chmod $octalPermissions $encDst 2>/dev/null";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $cmd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $fsSrcPath Absolute file system path
|
|
|
|
|
* @param string $fsDstPath Absolute file system path
|
2022-05-09 09:09:00 +00:00
|
|
|
* @param bool $ignoreMissing Whether to no-op if the source file is non-existent
|
2019-09-07 10:25:37 +00:00
|
|
|
* @return string Command
|
|
|
|
|
*/
|
|
|
|
|
private function makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing = false ) {
|
|
|
|
|
// https://manpages.debian.org/buster/coreutils/mv.1.en.html
|
|
|
|
|
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/move
|
2021-09-21 05:40:55 +00:00
|
|
|
$encSrc = Shellbox::escape( $this->cleanPathSlashes( $fsSrcPath ) );
|
|
|
|
|
$encDst = Shellbox::escape( $this->cleanPathSlashes( $fsDstPath ) );
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $this->os === 'Windows' ) {
|
|
|
|
|
$writeCmd = "MOVE /Y $encSrc $encDst 2>&1";
|
|
|
|
|
$cmd = $ignoreMissing ? "IF EXIST $encSrc $writeCmd" : $writeCmd;
|
|
|
|
|
} else {
|
|
|
|
|
$writeCmd = "mv -f $encSrc $encDst 2>&1";
|
|
|
|
|
$cmd = $ignoreMissing ? "test -f $encSrc && $writeCmd" : $writeCmd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $cmd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $fsPath Absolute file system path
|
2022-05-09 09:09:00 +00:00
|
|
|
* @param bool $ignoreMissing Whether to no-op if the file is non-existent
|
2019-09-07 10:25:37 +00:00
|
|
|
* @return string Command
|
|
|
|
|
*/
|
|
|
|
|
private function makeUnlinkCommand( $fsPath, $ignoreMissing = false ) {
|
|
|
|
|
// https://manpages.debian.org/buster/coreutils/rm.1.en.html
|
|
|
|
|
// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/del
|
2021-09-21 05:40:55 +00:00
|
|
|
$encSrc = Shellbox::escape( $this->cleanPathSlashes( $fsPath ) );
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( $this->os === 'Windows' ) {
|
|
|
|
|
$writeCmd = "DEL /Q $encSrc 2>&1";
|
|
|
|
|
$cmd = $ignoreMissing ? "IF EXIST $encSrc $writeCmd" : $writeCmd;
|
|
|
|
|
} else {
|
|
|
|
|
$cmd = $ignoreMissing ? "rm -f $encSrc 2>&1" : "rm $encSrc 2>&1";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $cmd;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-20 03:52:06 +00:00
|
|
|
/**
|
|
|
|
|
* Chmod a file, suppressing the warnings
|
|
|
|
|
*
|
2019-09-07 10:25:37 +00:00
|
|
|
* @param string $fsPath Absolute file system path
|
2011-12-20 03:52:06 +00:00
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
2019-09-07 10:25:37 +00:00
|
|
|
protected function chmod( $fsPath ) {
|
|
|
|
|
if ( $this->os === 'Windows' ) {
|
2019-09-07 05:58:29 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AtEase::suppressWarnings();
|
2019-09-07 10:25:37 +00:00
|
|
|
$ok = chmod( $fsPath, $this->fileMode );
|
2019-09-07 05:58:29 +00:00
|
|
|
AtEase::restoreWarnings();
|
2011-12-20 03:52:06 +00:00
|
|
|
|
|
|
|
|
return $ok;
|
|
|
|
|
}
|
2012-01-13 04:32:28 +00:00
|
|
|
|
2019-09-07 05:58:29 +00:00
|
|
|
/**
|
|
|
|
|
* Unlink a file, suppressing the warnings
|
|
|
|
|
*
|
2019-09-07 10:25:37 +00:00
|
|
|
* @param string $fsPath Absolute file system path
|
2019-09-07 05:58:29 +00:00
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
2019-09-07 10:25:37 +00:00
|
|
|
protected function unlink( $fsPath ) {
|
2019-09-07 05:58:29 +00:00
|
|
|
AtEase::suppressWarnings();
|
2019-09-07 10:25:37 +00:00
|
|
|
$ok = unlink( $fsPath );
|
2019-09-07 05:58:29 +00:00
|
|
|
AtEase::restoreWarnings();
|
2019-09-10 05:02:19 +00:00
|
|
|
clearstatcache( true, $fsPath );
|
|
|
|
|
|
|
|
|
|
return $ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove an empty directory, suppressing the warnings
|
|
|
|
|
*
|
|
|
|
|
* @param string $fsDirectory Absolute file system path
|
|
|
|
|
* @return bool Success
|
|
|
|
|
*/
|
|
|
|
|
protected function rmdir( $fsDirectory ) {
|
|
|
|
|
AtEase::suppressWarnings();
|
|
|
|
|
$ok = rmdir( $fsDirectory ); // remove directory if empty
|
|
|
|
|
AtEase::restoreWarnings();
|
|
|
|
|
clearstatcache( true, $fsDirectory );
|
2019-09-07 05:58:29 +00:00
|
|
|
|
|
|
|
|
return $ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-09-07 10:25:37 +00:00
|
|
|
* @param array $params Parameters for FileBackend 'create' operation
|
2019-09-07 05:58:29 +00:00
|
|
|
* @return TempFSFile|null
|
|
|
|
|
*/
|
2019-09-07 10:25:37 +00:00
|
|
|
protected function newTempFileWithContent( array $params ) {
|
2019-09-07 05:58:29 +00:00
|
|
|
$tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' );
|
|
|
|
|
if ( !$tempFile ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AtEase::suppressWarnings();
|
2019-09-07 10:25:37 +00:00
|
|
|
if ( file_put_contents( $tempFile->getPath(), $params['content'] ) === false ) {
|
2019-09-07 05:58:29 +00:00
|
|
|
$tempFile = null;
|
|
|
|
|
}
|
|
|
|
|
AtEase::restoreWarnings();
|
|
|
|
|
|
|
|
|
|
return $tempFile;
|
|
|
|
|
}
|
|
|
|
|
|
2012-05-21 22:19:06 +00:00
|
|
|
/**
|
|
|
|
|
* Return the text of an index.html file to hide directory listings
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function indexHtmlPrivate() {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the text of a .htaccess file to make a directory private
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function htaccessPrivate() {
|
2024-03-24 03:34:53 +00:00
|
|
|
return "Require all denied\n";
|
2012-05-21 22:19:06 +00:00
|
|
|
}
|
|
|
|
|
|
2012-04-11 17:51:02 +00:00
|
|
|
/**
|
|
|
|
|
* Clean up directory separators for the given OS
|
|
|
|
|
*
|
2021-11-26 15:21:17 +00:00
|
|
|
* @param string $fsPath
|
2012-04-11 17:51:02 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
2019-09-07 10:25:37 +00:00
|
|
|
protected function cleanPathSlashes( $fsPath ) {
|
|
|
|
|
return ( $this->os === 'Windows' ) ? strtr( $fsPath, '/', '\\' ) : $fsPath;
|
2012-04-11 17:51:02 +00:00
|
|
|
}
|
|
|
|
|
|
2012-01-13 04:32:28 +00:00
|
|
|
/**
|
2019-09-07 06:21:48 +00:00
|
|
|
* Listen for E_WARNING errors and track whether any that happen
|
|
|
|
|
*
|
|
|
|
|
* @param string|null $regexIgnore Optional regex of errors to ignore
|
2012-01-13 04:32:28 +00:00
|
|
|
*/
|
2019-09-07 06:21:48 +00:00
|
|
|
protected function trapWarnings( $regexIgnore = null ) {
|
|
|
|
|
$this->warningTrapStack[] = false;
|
|
|
|
|
set_error_handler( function ( $errno, $errstr ) use ( $regexIgnore ) {
|
|
|
|
|
if ( $regexIgnore === null || !preg_match( $regexIgnore, $errstr ) ) {
|
|
|
|
|
$this->logger->error( $errstr );
|
|
|
|
|
$this->warningTrapStack[count( $this->warningTrapStack ) - 1] = true;
|
|
|
|
|
}
|
|
|
|
|
return true; // suppress from PHP handler
|
2019-08-23 13:15:33 +00:00
|
|
|
}, E_WARNING );
|
2012-01-13 04:32:28 +00:00
|
|
|
}
|
|
|
|
|
|
2020-11-26 02:49:24 +00:00
|
|
|
/**
|
|
|
|
|
* Track E_WARNING errors but ignore any that correspond to ENOENT "No such file or directory"
|
|
|
|
|
*/
|
|
|
|
|
protected function trapWarningsIgnoringNotFound() {
|
|
|
|
|
$this->trapWarnings( $this->getFileNotFoundRegex() );
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-13 04:32:28 +00:00
|
|
|
/**
|
2019-09-07 06:21:48 +00:00
|
|
|
* Stop listening for E_WARNING errors and get whether any happened
|
2012-01-13 04:32:28 +00:00
|
|
|
*
|
2019-09-07 06:21:48 +00:00
|
|
|
* @return bool Whether any warnings happened
|
2012-01-13 04:32:28 +00:00
|
|
|
*/
|
|
|
|
|
protected function untrapWarnings() {
|
2019-08-23 13:15:33 +00:00
|
|
|
restore_error_handler();
|
2019-09-07 06:21:48 +00:00
|
|
|
|
|
|
|
|
return array_pop( $this->warningTrapStack );
|
2012-01-13 04:32:28 +00:00
|
|
|
}
|
2020-11-26 02:49:24 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a regex matching file not found errors
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function getFileNotFoundRegex() {
|
|
|
|
|
static $regex;
|
|
|
|
|
if ( $regex === null ) {
|
|
|
|
|
// "No such file or directory": string literal in spl_directory.c etc.
|
|
|
|
|
$alternatives = [ ': No such file or directory' ];
|
|
|
|
|
if ( $this->os === 'Windows' ) {
|
|
|
|
|
// 2 = The system cannot find the file specified.
|
|
|
|
|
// 3 = The system cannot find the path specified.
|
|
|
|
|
$alternatives[] = ' \(code: [23]\)';
|
|
|
|
|
}
|
|
|
|
|
if ( function_exists( 'pcntl_strerror' ) ) {
|
|
|
|
|
$alternatives[] = preg_quote( ': ' . pcntl_strerror( 2 ), '/' );
|
2021-01-05 01:15:05 +00:00
|
|
|
} elseif ( function_exists( 'socket_strerror' ) && defined( 'SOCKET_ENOENT' ) ) {
|
2020-11-26 02:49:24 +00:00
|
|
|
$alternatives[] = preg_quote( ': ' . socket_strerror( SOCKET_ENOENT ), '/' );
|
|
|
|
|
}
|
|
|
|
|
$regex = '/(' . implode( '|', $alternatives ) . ')$/';
|
|
|
|
|
}
|
|
|
|
|
return $regex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine whether a given error message is a file not found error.
|
|
|
|
|
*
|
|
|
|
|
* @param string $error
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
protected function isFileNotFoundError( $error ) {
|
|
|
|
|
return (bool)preg_match( $this->getFileNotFoundRegex(), $error );
|
|
|
|
|
}
|
2011-12-20 03:52:06 +00:00
|
|
|
}
|
2024-09-27 19:20:56 +00:00
|
|
|
|
|
|
|
|
/** @deprecated class alias since 1.43 */
|
|
|
|
|
class_alias( FSFileBackend::class, 'FSFileBackend' );
|