PocketMine-MP 5.15.1 git-be6754494fdbbb9dd57c058ba0e33a4a78c4581f
Filesystem.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\utils;
25
27use Symfony\Component\Filesystem\Path;
28use function copy;
29use function dirname;
30use function fclose;
31use function fflush;
32use function file_exists;
33use function file_get_contents;
34use function file_put_contents;
35use function flock;
36use function fopen;
37use function ftruncate;
38use function fwrite;
39use function getmypid;
40use function is_dir;
41use function is_file;
42use function ltrim;
43use function mkdir;
44use function preg_match;
45use function realpath;
46use function rename;
47use function rmdir;
48use function rtrim;
49use function scandir;
50use function str_replace;
51use function str_starts_with;
52use function stream_get_contents;
53use function strlen;
54use function uksort;
55use function unlink;
56use const DIRECTORY_SEPARATOR;
57use const LOCK_EX;
58use const LOCK_NB;
59use const LOCK_SH;
60use const LOCK_UN;
61use const SCANDIR_SORT_NONE;
62
63final class Filesystem{
65 private static array $lockFileHandles = [];
70 private static array $cleanedPaths = [
71 \pocketmine\PATH => self::CLEAN_PATH_SRC_PREFIX
72 ];
73
74 public const CLEAN_PATH_SRC_PREFIX = "pmsrc";
75 public const CLEAN_PATH_PLUGINS_PREFIX = "plugins";
76
77 private function __construct(){
78 //NOOP
79 }
80
81 public static function recursiveUnlink(string $dir) : void{
82 if(is_dir($dir)){
83 $objects = Utils::assumeNotFalse(scandir($dir, SCANDIR_SORT_NONE), "scandir() shouldn't return false when is_dir() returns true");
84 foreach($objects as $object){
85 if($object !== "." && $object !== ".."){
86 $fullObject = Path::join($dir, $object);
87 if(is_dir($fullObject)){
88 self::recursiveUnlink($fullObject);
89 }else{
90 unlink($fullObject);
91 }
92 }
93 }
94 rmdir($dir);
95 }elseif(is_file($dir)){
96 unlink($dir);
97 }
98 }
99
103 public static function recursiveCopy(string $origin, string $destination) : void{
104 if(!is_dir($origin)){
105 throw new \RuntimeException("$origin does not exist, or is not a directory");
106 }
107 if(!is_dir($destination)){
108 if(file_exists($destination)){
109 throw new \RuntimeException("$destination already exists, and is not a directory");
110 }
111 if(!is_dir(dirname($destination))){
112 //if the parent dir doesn't exist, the user most likely made a mistake
113 throw new \RuntimeException("The parent directory of $destination does not exist, or is not a directory");
114 }
115 try{
116 ErrorToExceptionHandler::trap(fn() => mkdir($destination));
117 }catch(\ErrorException $e){
118 if(!is_dir($destination)){
119 throw new \RuntimeException("Failed to create output directory $destination: " . $e->getMessage());
120 }
121 }
122 }
123 self::recursiveCopyInternal($origin, $destination);
124 }
125
126 private static function recursiveCopyInternal(string $origin, string $destination) : void{
127 if(is_dir($origin)){
128 if(!is_dir($destination)){
129 if(file_exists($destination)){
130 throw new \RuntimeException("Path $destination does not exist, or is not a directory");
131 }
132 mkdir($destination); //TODO: access permissions?
133 }
134 $objects = Utils::assumeNotFalse(scandir($origin, SCANDIR_SORT_NONE));
135 foreach($objects as $object){
136 if($object === "." || $object === ".."){
137 continue;
138 }
139 self::recursiveCopyInternal(Path::join($origin, $object), Path::join($destination, $object));
140 }
141 }else{
142 $dirName = dirname($destination);
143 if(!is_dir($dirName)){ //the destination folder should already exist
144 throw new AssumptionFailedError("The destination folder should have been created in the parent call");
145 }
146 copy($origin, $destination);
147 }
148 }
149
150 public static function addCleanedPath(string $path, string $replacement) : void{
151 self::$cleanedPaths[$path] = $replacement;
152 uksort(self::$cleanedPaths, function(string $str1, string $str2) : int{
153 return strlen($str2) <=> strlen($str1); //longest first
154 });
155 }
156
161 public static function getCleanedPaths() : array{ return self::$cleanedPaths; }
162
163 public static function cleanPath(string $path) : string{
164 $result = str_replace([DIRECTORY_SEPARATOR, ".php", "phar://"], ["/", "", ""], $path);
165
166 //remove relative paths
167 //this should probably never have integer keys, but it's safer than making PHPStan ignore it
168 foreach(Utils::stringifyKeys(self::$cleanedPaths) as $cleanPath => $replacement){
169 $cleanPath = rtrim(str_replace([DIRECTORY_SEPARATOR, "phar://"], ["/", ""], $cleanPath), "/");
170 if(str_starts_with($result, $cleanPath)){
171 $result = ltrim(str_replace($cleanPath, $replacement, $result), "/");
172 }
173 }
174 return $result;
175 }
176
185 public static function createLockFile(string $lockFilePath) : ?int{
186 try{
187 $resource = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => fopen($lockFilePath, "a+b"));
188 }catch(\ErrorException $e){
189 throw new \InvalidArgumentException("Failed to open lock file: " . $e->getMessage(), 0, $e);
190 }
191 if(!flock($resource, LOCK_EX | LOCK_NB)){
192 //wait for a shared lock to avoid race conditions if two servers started at the same time - this makes sure the
193 //other server wrote its PID and released exclusive lock before we get our lock
194 flock($resource, LOCK_SH);
195 $pid = Utils::assumeNotFalse(stream_get_contents($resource), "This is a known valid file resource, at worst we should receive an empty string");
196 if(preg_match('/^\d+$/', $pid) === 1){
197 return (int) $pid;
198 }
199 return -1;
200 }
201 ftruncate($resource, 0);
202 fwrite($resource, (string) getmypid());
203 fflush($resource);
204 flock($resource, LOCK_SH); //prevent acquiring an exclusive lock from another process, but allow reading
205 self::$lockFileHandles[realpath($lockFilePath)] = $resource; //keep the resource alive to preserve the lock
206 return null;
207 }
208
214 public static function releaseLockFile(string $lockFilePath) : void{
215 $lockFilePath = realpath($lockFilePath);
216 if($lockFilePath === false){
217 throw new \InvalidArgumentException("Invalid lock file path");
218 }
219 if(isset(self::$lockFileHandles[$lockFilePath])){
220 flock(self::$lockFileHandles[$lockFilePath], LOCK_UN);
221 fclose(self::$lockFileHandles[$lockFilePath]);
222 unset(self::$lockFileHandles[$lockFilePath]);
223 @unlink($lockFilePath);
224 }
225 }
226
238 public static function safeFilePutContents(string $fileName, string $contents, int $flags = 0, $context = null) : void{
239 $directory = dirname($fileName);
240 if(!is_dir($directory)){
241 throw new \RuntimeException("Target directory path does not exist or is not a directory");
242 }
243 if(is_dir($fileName)){
244 throw new \RuntimeException("Target file path already exists and is not a file");
245 }
246
247 $counter = 0;
248 do{
249 //we don't care about overwriting any preexisting tmpfile but we can't write if a directory is already here
250 $temporaryFileName = $fileName . ".$counter.tmp";
251 $counter++;
252 }while(is_dir($temporaryFileName));
253
254 try{
255 ErrorToExceptionHandler::trap(fn() => $context !== null ?
256 file_put_contents($temporaryFileName, $contents, $flags, $context) :
257 file_put_contents($temporaryFileName, $contents, $flags)
258 );
259 }catch(\ErrorException $filePutContentsException){
260 $context !== null ?
261 @unlink($temporaryFileName, $context) :
262 @unlink($temporaryFileName);
263 throw new \RuntimeException("Failed to write to temporary file $temporaryFileName: " . $filePutContentsException->getMessage(), 0, $filePutContentsException);
264 }
265
266 $renameTemporaryFileResult = $context !== null ?
267 @rename($temporaryFileName, $fileName, $context) :
268 @rename($temporaryFileName, $fileName);
269 if(!$renameTemporaryFileResult){
270 /*
271 * The following code works around a bug in Windows where rename() will periodically decide to give us a
272 * spurious "Access is denied (code: 5)" error. As far as I could determine, the fault comes from Windows
273 * itself, but since I couldn't reliably reproduce the issue it's very hard to debug.
274 *
275 * The following code can be used to test. Usually it will fail anywhere before 100,000 iterations.
276 *
277 * for($i = 0; $i < 10_000_000; ++$i){
278 * file_put_contents('ops.txt.0.tmp', 'some data ' . $i, 0);
279 * if(!rename('ops.txt.0.tmp', 'ops.txt')){
280 * throw new \Error("something weird happened");
281 * }
282 * }
283 */
284 try{
285 ErrorToExceptionHandler::trap(fn() => $context !== null ?
286 copy($temporaryFileName, $fileName, $context) :
287 copy($temporaryFileName, $fileName)
288 );
289 }catch(\ErrorException $copyException){
290 throw new \RuntimeException("Failed to move temporary file contents into target file: " . $copyException->getMessage(), 0, $copyException);
291 }
292 @unlink($temporaryFileName);
293 }
294 }
295
305 public static function fileGetContents(string $fileName, bool $useIncludePath = false, $context = null, int $offset = 0, ?int $length = null) : string{
306 try{
307 return ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($fileName, $useIncludePath, $context, $offset, $length));
308 }catch(\ErrorException $e){
309 throw new \RuntimeException("Failed to read file $fileName: " . $e->getMessage(), 0, $e);
310 }
311 }
312}
static trap(\Closure $closure, int $levels=E_WARNING|E_NOTICE)
static recursiveCopy(string $origin, string $destination)
Definition: Filesystem.php:103
static fileGetContents(string $fileName, bool $useIncludePath=false, $context=null, int $offset=0, ?int $length=null)
Definition: Filesystem.php:305
static createLockFile(string $lockFilePath)
Definition: Filesystem.php:185
static releaseLockFile(string $lockFilePath)
Definition: Filesystem.php:214
static safeFilePutContents(string $fileName, string $contents, int $flags=0, $context=null)
Definition: Filesystem.php:238
static assumeNotFalse(mixed $value, \Closure|string $context="This should never be false")
Definition: Utils.php:623