mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-04-21 08:17:34 +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{
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
try{
|
||||
file_put_contents($this->getPlayerDataPath($name), zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP));
|
||||
}catch(\ErrorException $e){
|
||||
Filesystem::safeFilePutContents($this->getPlayerDataPath($name), zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP));
|
||||
}catch(\RuntimeException | \ErrorException $e){
|
||||
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
|
||||
$this->logger->logException($e);
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ use function date;
|
||||
use function explode;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
@ -228,7 +227,7 @@ class Config{
|
||||
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;
|
||||
}
|
||||
|
@ -26,9 +26,11 @@ namespace pocketmine\utils;
|
||||
use Webmozart\PathUtil\Path;
|
||||
use function copy;
|
||||
use function dirname;
|
||||
use function disk_free_space;
|
||||
use function fclose;
|
||||
use function fflush;
|
||||
use function file_exists;
|
||||
use function file_put_contents;
|
||||
use function flock;
|
||||
use function fopen;
|
||||
use function ftruncate;
|
||||
@ -40,6 +42,7 @@ use function ltrim;
|
||||
use function mkdir;
|
||||
use function preg_match;
|
||||
use function realpath;
|
||||
use function rename;
|
||||
use function rmdir;
|
||||
use function rtrim;
|
||||
use function scandir;
|
||||
@ -221,4 +224,53 @@ final class Filesystem{
|
||||
@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\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\Limits;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
@ -165,7 +166,7 @@ class BedrockWorldData extends BaseNbtWorldData{
|
||||
|
||||
$nbt = new LittleEndianNbtSerializer();
|
||||
$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{
|
||||
|
@ -29,6 +29,8 @@ use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\FloatTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\generator\GeneratorManager;
|
||||
use pocketmine\world\World;
|
||||
@ -111,7 +113,10 @@ class JavaWorldData extends BaseNbtWorldData{
|
||||
public function save() : void{
|
||||
$nbt = new BigEndianNbtSerializer();
|
||||
$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{
|
||||
|
Loading…
x
Reference in New Issue
Block a user