mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-07-09 11:31:49 +00:00
Avoid file_put_contents() when overwriting files
this fixes many cases of corruption during disk-full situations - file_put_contents() would write an empty file, destroying the original data. fixes #3152
This commit is contained in:
parent
8e8cee45b8
commit
8e37f86480
@ -538,8 +538,8 @@ class Server{
|
|||||||
Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
|
Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
|
||||||
$nbt = new BigEndianNbtSerializer();
|
$nbt = new BigEndianNbtSerializer();
|
||||||
try{
|
try{
|
||||||
file_put_contents($this->getPlayerDataPath($name), zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP));
|
Filesystem::safeFilePutContents($this->getPlayerDataPath($name), zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP));
|
||||||
}catch(\ErrorException $e){
|
}catch(\RuntimeException | \ErrorException $e){
|
||||||
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
|
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
|
||||||
$this->logger->logException($e);
|
$this->logger->logException($e);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,6 @@ use function date;
|
|||||||
use function explode;
|
use function explode;
|
||||||
use function file_exists;
|
use function file_exists;
|
||||||
use function file_get_contents;
|
use function file_get_contents;
|
||||||
use function file_put_contents;
|
|
||||||
use function implode;
|
use function implode;
|
||||||
use function is_array;
|
use function is_array;
|
||||||
use function is_bool;
|
use function is_bool;
|
||||||
@ -228,7 +227,7 @@ class Config{
|
|||||||
throw new AssumptionFailedError("Config type is unknown, has not been set or not detected");
|
throw new AssumptionFailedError("Config type is unknown, has not been set or not detected");
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents($this->file, $content);
|
Filesystem::safeFilePutContents($this->file, $content);
|
||||||
|
|
||||||
$this->changed = false;
|
$this->changed = false;
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,11 @@ namespace pocketmine\utils;
|
|||||||
use Webmozart\PathUtil\Path;
|
use Webmozart\PathUtil\Path;
|
||||||
use function copy;
|
use function copy;
|
||||||
use function dirname;
|
use function dirname;
|
||||||
|
use function disk_free_space;
|
||||||
use function fclose;
|
use function fclose;
|
||||||
use function fflush;
|
use function fflush;
|
||||||
use function file_exists;
|
use function file_exists;
|
||||||
|
use function file_put_contents;
|
||||||
use function flock;
|
use function flock;
|
||||||
use function fopen;
|
use function fopen;
|
||||||
use function ftruncate;
|
use function ftruncate;
|
||||||
@ -40,6 +42,7 @@ use function ltrim;
|
|||||||
use function mkdir;
|
use function mkdir;
|
||||||
use function preg_match;
|
use function preg_match;
|
||||||
use function realpath;
|
use function realpath;
|
||||||
|
use function rename;
|
||||||
use function rmdir;
|
use function rmdir;
|
||||||
use function rtrim;
|
use function rtrim;
|
||||||
use function scandir;
|
use function scandir;
|
||||||
@ -221,4 +224,53 @@ final class Filesystem{
|
|||||||
@unlink($lockFilePath);
|
@unlink($lockFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around file_put_contents() which writes to a temporary file before overwriting the original. If the disk
|
||||||
|
* is full, writing to the temporary file will fail before the original file is modified, leaving it untouched.
|
||||||
|
*
|
||||||
|
* This is necessary because file_put_contents() destroys the data currently in the file if it fails to write the
|
||||||
|
* new contents.
|
||||||
|
*
|
||||||
|
* @param resource|null $context Context to pass to file_put_contents
|
||||||
|
*/
|
||||||
|
public static function safeFilePutContents(string $fileName, string $contents, int $flags = 0, $context = null) : void{
|
||||||
|
$directory = dirname($fileName);
|
||||||
|
if(!is_dir($directory)){
|
||||||
|
throw new \RuntimeException("Target directory path does not exist or is not a directory");
|
||||||
|
}
|
||||||
|
if(is_dir($fileName)){
|
||||||
|
throw new \RuntimeException("Target file path already exists and is not a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
$counter = 0;
|
||||||
|
do{
|
||||||
|
//we don't care about overwriting any preexisting tmpfile but we can't write if a directory is already here
|
||||||
|
$temporaryFileName = $fileName . ".$counter.tmp";
|
||||||
|
$counter++;
|
||||||
|
}while(is_dir($temporaryFileName));
|
||||||
|
|
||||||
|
$writeTemporaryFileResult = $context !== null ?
|
||||||
|
file_put_contents($temporaryFileName, $contents, $flags, $context) :
|
||||||
|
file_put_contents($temporaryFileName, $contents, $flags);
|
||||||
|
|
||||||
|
if($writeTemporaryFileResult !== strlen($contents)){
|
||||||
|
$context !== null ?
|
||||||
|
@unlink($temporaryFileName, $context) :
|
||||||
|
@unlink($temporaryFileName);
|
||||||
|
$diskSpace = disk_free_space($directory);
|
||||||
|
if($diskSpace !== false && $diskSpace < strlen($contents)){
|
||||||
|
throw new \RuntimeException("Failed to write to temporary file $temporaryFileName (out of free disk space)");
|
||||||
|
}
|
||||||
|
throw new \RuntimeException("Failed to write to temporary file $temporaryFileName (possibly out of free disk space)");
|
||||||
|
}
|
||||||
|
|
||||||
|
$renameTemporaryFileResult = $context !== null ?
|
||||||
|
rename($temporaryFileName, $fileName, $context) :
|
||||||
|
rename($temporaryFileName, $fileName);
|
||||||
|
|
||||||
|
if(!$renameTemporaryFileResult){
|
||||||
|
throw new \RuntimeException("Failed to move temporary file contents into target file");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ use pocketmine\nbt\tag\StringTag;
|
|||||||
use pocketmine\nbt\TreeRoot;
|
use pocketmine\nbt\TreeRoot;
|
||||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||||
use pocketmine\utils\Binary;
|
use pocketmine\utils\Binary;
|
||||||
|
use pocketmine\utils\Filesystem;
|
||||||
use pocketmine\utils\Limits;
|
use pocketmine\utils\Limits;
|
||||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||||
@ -165,7 +166,7 @@ class BedrockWorldData extends BaseNbtWorldData{
|
|||||||
|
|
||||||
$nbt = new LittleEndianNbtSerializer();
|
$nbt = new LittleEndianNbtSerializer();
|
||||||
$buffer = $nbt->write(new TreeRoot($this->compoundTag));
|
$buffer = $nbt->write(new TreeRoot($this->compoundTag));
|
||||||
file_put_contents($this->dataPath, Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
|
Filesystem::safeFilePutContents($this->dataPath, Binary::writeLInt(self::CURRENT_STORAGE_VERSION) . Binary::writeLInt(strlen($buffer)) . $buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDifficulty() : int{
|
public function getDifficulty() : int{
|
||||||
|
@ -29,6 +29,8 @@ use pocketmine\nbt\tag\CompoundTag;
|
|||||||
use pocketmine\nbt\tag\FloatTag;
|
use pocketmine\nbt\tag\FloatTag;
|
||||||
use pocketmine\nbt\tag\StringTag;
|
use pocketmine\nbt\tag\StringTag;
|
||||||
use pocketmine\nbt\TreeRoot;
|
use pocketmine\nbt\TreeRoot;
|
||||||
|
use pocketmine\utils\AssumptionFailedError;
|
||||||
|
use pocketmine\utils\Filesystem;
|
||||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||||
use pocketmine\world\generator\GeneratorManager;
|
use pocketmine\world\generator\GeneratorManager;
|
||||||
use pocketmine\world\World;
|
use pocketmine\world\World;
|
||||||
@ -111,7 +113,10 @@ class JavaWorldData extends BaseNbtWorldData{
|
|||||||
public function save() : void{
|
public function save() : void{
|
||||||
$nbt = new BigEndianNbtSerializer();
|
$nbt = new BigEndianNbtSerializer();
|
||||||
$buffer = zlib_encode($nbt->write(new TreeRoot(CompoundTag::create()->setTag("Data", $this->compoundTag))), ZLIB_ENCODING_GZIP);
|
$buffer = zlib_encode($nbt->write(new TreeRoot(CompoundTag::create()->setTag("Data", $this->compoundTag))), ZLIB_ENCODING_GZIP);
|
||||||
file_put_contents($this->dataPath, $buffer);
|
if($buffer === false){
|
||||||
|
throw new AssumptionFailedError("zlib_encode() failed unexpectedly");
|
||||||
|
}
|
||||||
|
Filesystem::safeFilePutContents($this->dataPath, $buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDifficulty() : int{
|
public function getDifficulty() : int{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user