Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Core file system class definition. 19 * 20 * @package core_files 21 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 /** 28 * File system class used for low level access to real files in filedir. 29 * 30 * @package core_files 31 * @category files 32 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class file_system_filedir extends file_system { 36 37 /** 38 * @var string The path to the local copy of the filedir. 39 */ 40 protected $filedir = null; 41 42 /** 43 * @var string The path to the trashdir. 44 */ 45 protected $trashdir = null; 46 47 /** 48 * @var string Default directory permissions for new dirs. 49 */ 50 protected $dirpermissions = null; 51 52 /** 53 * @var string Default file permissions for new files. 54 */ 55 protected $filepermissions = null; 56 57 58 /** 59 * Perform any custom setup for this type of file_system. 60 */ 61 public function __construct() { 62 global $CFG; 63 64 if (isset($CFG->filedir)) { 65 $this->filedir = $CFG->filedir; 66 } else { 67 $this->filedir = $CFG->dataroot.'/filedir'; 68 } 69 70 if (isset($CFG->trashdir)) { 71 $this->trashdir = $CFG->trashdir; 72 } else { 73 $this->trashdir = $CFG->dataroot.'/trashdir'; 74 } 75 76 $this->dirpermissions = $CFG->directorypermissions; 77 $this->filepermissions = $CFG->filepermissions; 78 79 // Make sure the file pool directory exists. 80 if (!is_dir($this->filedir)) { 81 if (!mkdir($this->filedir, $this->dirpermissions, true)) { 82 // Permission trouble. 83 throw new file_exception('storedfilecannotcreatefiledirs'); 84 } 85 86 // Place warning file in file pool root. 87 if (!file_exists($this->filedir.'/warning.txt')) { 88 file_put_contents($this->filedir.'/warning.txt', 89 'This directory contains the content of uploaded files and is controlled by Moodle code. ' . 90 'Do not manually move, change or rename any of the files and subdirectories here.'); 91 chmod($this->filedir . '/warning.txt', $this->filepermissions); 92 } 93 } 94 95 // Make sure the trashdir directory exists too. 96 if (!is_dir($this->trashdir)) { 97 if (!mkdir($this->trashdir, $this->dirpermissions, true)) { 98 // Permission trouble. 99 throw new file_exception('storedfilecannotcreatefiledirs'); 100 } 101 } 102 } 103 104 /** 105 * Get the full path for the specified hash, including the path to the filedir. 106 * 107 * @param string $contenthash The content hash 108 * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found. 109 * @return string The full path to the content file 110 */ 111 protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false) { 112 return $this->get_fulldir_from_hash($contenthash) . '/' .$contenthash; 113 } 114 115 /** 116 * Get a remote filepath for the specified stored file. 117 * 118 * @param stored_file $file The file to fetch the path for 119 * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found. 120 * @return string The full path to the content file 121 */ 122 public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) { 123 $filepath = $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound); 124 125 // Try content recovery. 126 if ($fetchifnotfound && !is_readable($filepath)) { 127 $this->recover_file($file); 128 } 129 130 return $filepath; 131 } 132 133 /** 134 * Get a remote filepath for the specified stored file. 135 * 136 * @param stored_file $file The file to serve. 137 * @return string full path to pool file with file content 138 */ 139 public function get_remote_path_from_storedfile(stored_file $file) { 140 return $this->get_local_path_from_storedfile($file, false); 141 } 142 143 /** 144 * Get the full path for the specified hash, including the path to the filedir. 145 * 146 * @param string $contenthash The content hash 147 * @return string The full path to the content file 148 */ 149 protected function get_remote_path_from_hash($contenthash) { 150 return $this->get_local_path_from_hash($contenthash, false); 151 } 152 153 /** 154 * Get the full directory to the stored file, including the path to the 155 * filedir, and the directory which the file is actually in. 156 * 157 * Note: This function does not ensure that the file is present on disk. 158 * 159 * @param stored_file $file The file to fetch details for. 160 * @return string The full path to the content directory 161 */ 162 protected function get_fulldir_from_storedfile(stored_file $file) { 163 return $this->get_fulldir_from_hash($file->get_contenthash()); 164 } 165 166 /** 167 * Get the full directory to the stored file, including the path to the 168 * filedir, and the directory which the file is actually in. 169 * 170 * @param string $contenthash The content hash 171 * @return string The full path to the content directory 172 */ 173 protected function get_fulldir_from_hash($contenthash) { 174 return $this->filedir . '/' . $this->get_contentdir_from_hash($contenthash); 175 } 176 177 /** 178 * Get the content directory for the specified content hash. 179 * This is the directory that the file will be in, but without the 180 * fulldir. 181 * 182 * @param string $contenthash The content hash 183 * @return string The directory within filedir 184 */ 185 protected function get_contentdir_from_hash($contenthash) { 186 $l1 = $contenthash[0] . $contenthash[1]; 187 $l2 = $contenthash[2] . $contenthash[3]; 188 return "$l1/$l2"; 189 } 190 191 /** 192 * Get the content path for the specified content hash within filedir. 193 * 194 * This does not include the filedir, and is often used by file systems 195 * as the object key for storage and retrieval. 196 * 197 * @param string $contenthash The content hash 198 * @return string The filepath within filedir 199 */ 200 protected function get_contentpath_from_hash($contenthash) { 201 return $this->get_contentdir_from_hash($contenthash) . '/' . $contenthash; 202 } 203 204 /** 205 * Get the full directory for the specified hash in the trash, including the path to the 206 * trashdir, and the directory which the file is actually in. 207 * 208 * @param string $contenthash The content hash 209 * @return string The full path to the trash directory 210 */ 211 protected function get_trash_fulldir_from_hash($contenthash) { 212 return $this->trashdir . '/' . $this->get_contentdir_from_hash($contenthash); 213 } 214 215 /** 216 * Get the full path for the specified hash in the trash, including the path to the trashdir. 217 * 218 * @param string $contenthash The content hash 219 * @return string The full path to the trash file 220 */ 221 protected function get_trash_fullpath_from_hash($contenthash) { 222 return $this->trashdir . '/' . $this->get_contentpath_from_hash($contenthash); 223 } 224 225 /** 226 * Copy content of file to given pathname. 227 * 228 * @param stored_file $file The file to be copied 229 * @param string $target real path to the new file 230 * @return bool success 231 */ 232 public function copy_content_from_storedfile(stored_file $file, $target) { 233 $source = $this->get_local_path_from_storedfile($file, true); 234 return copy($source, $target); 235 } 236 237 /** 238 * Tries to recover missing content of file from trash. 239 * 240 * @param stored_file $file stored_file instance 241 * @return bool success 242 */ 243 protected function recover_file(stored_file $file) { 244 $contentfile = $this->get_local_path_from_storedfile($file, false); 245 246 if (file_exists($contentfile)) { 247 // The file already exists on the file system. No need to recover. 248 return true; 249 } 250 251 $contenthash = $file->get_contenthash(); 252 $contentdir = $this->get_fulldir_from_storedfile($file); 253 $trashfile = $this->get_trash_fullpath_from_hash($contenthash); 254 $alttrashfile = "{$this->trashdir}/{$contenthash}"; 255 256 if (!is_readable($trashfile)) { 257 // The trash file was not found. Check the alternative trash file too just in case. 258 if (!is_readable($alttrashfile)) { 259 return false; 260 } 261 // The alternative trash file in trash root exists. 262 $trashfile = $alttrashfile; 263 } 264 265 if (filesize($trashfile) != $file->get_filesize() or file_storage::hash_from_path($trashfile) != $contenthash) { 266 // The files are different. Leave this one in trash - something seems to be wrong with it. 267 return false; 268 } 269 270 if (!is_dir($contentdir)) { 271 if (!mkdir($contentdir, $this->dirpermissions, true)) { 272 // Unable to create the target directory. 273 return false; 274 } 275 } 276 277 // Perform a rename - these are generally atomic which gives us big 278 // performance wins, especially for large files. 279 return rename($trashfile, $contentfile); 280 } 281 282 /** 283 * Marks pool file as candidate for deleting. 284 * 285 * @param string $contenthash 286 */ 287 public function remove_file($contenthash) { 288 if (!self::is_file_removable($contenthash)) { 289 // Don't remove the file - it's still in use. 290 return; 291 } 292 293 if (!$this->is_file_readable_remotely_by_hash($contenthash)) { 294 // The file wasn't found in the first place. Just ignore it. 295 return; 296 } 297 298 $trashpath = $this->get_trash_fulldir_from_hash($contenthash); 299 $trashfile = $this->get_trash_fullpath_from_hash($contenthash); 300 $contentfile = $this->get_local_path_from_hash($contenthash, true); 301 302 if (!is_dir($trashpath)) { 303 mkdir($trashpath, $this->dirpermissions, true); 304 } 305 306 if (file_exists($trashfile)) { 307 // A copy of this file is already in the trash. 308 // Remove the old version. 309 unlink($contentfile); 310 return; 311 } 312 313 // Move the contentfile to the trash, and fix permissions as required. 314 rename($contentfile, $trashfile); 315 316 // Fix permissions, only if needed. 317 $currentperms = octdec(substr(decoct(fileperms($trashfile)), -4)); 318 if ((int)$this->filepermissions !== $currentperms) { 319 chmod($trashfile, $this->filepermissions); 320 } 321 } 322 323 /** 324 * Cleanup the trash directory. 325 */ 326 public function cron() { 327 $this->empty_trash(); 328 } 329 330 protected function empty_trash() { 331 fulldelete($this->trashdir); 332 set_config('fileslastcleanup', time()); 333 } 334 335 /** 336 * Add the supplied file to the file system. 337 * 338 * Note: If overriding this function, it is advisable to store the file 339 * in the path returned by get_local_path_from_hash as there may be 340 * subsequent uses of the file in the same request. 341 * 342 * @param string $pathname Path to file currently on disk 343 * @param string $contenthash SHA1 hash of content if known (performance only) 344 * @return array (contenthash, filesize, newfile) 345 */ 346 public function add_file_from_path($pathname, $contenthash = null) { 347 348 list($contenthash, $filesize) = $this->validate_hash_and_file_size($contenthash, $pathname); 349 350 $hashpath = $this->get_fulldir_from_hash($contenthash); 351 $hashfile = $this->get_local_path_from_hash($contenthash, false); 352 353 $newfile = true; 354 355 if (file_exists($hashfile)) { 356 if (filesize($hashfile) === $filesize) { 357 return array($contenthash, $filesize, false); 358 } 359 if (file_storage::hash_from_path($hashfile) === $contenthash) { 360 // Jackpot! We have a hash collision. 361 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true); 362 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1"); 363 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2"); 364 throw new file_pool_content_exception($contenthash); 365 } 366 debugging("Replacing invalid content file $contenthash"); 367 unlink($hashfile); 368 $newfile = false; 369 } 370 371 if (!is_dir($hashpath)) { 372 if (!mkdir($hashpath, $this->dirpermissions, true)) { 373 // Permission trouble. 374 throw new file_exception('storedfilecannotcreatefiledirs'); 375 } 376 } 377 378 // Let's try to prevent some race conditions. 379 380 $prev = ignore_user_abort(true); 381 @unlink($hashfile.'.tmp'); 382 if (!copy($pathname, $hashfile.'.tmp')) { 383 // Borked permissions or out of disk space. 384 @unlink($hashfile.'.tmp'); 385 ignore_user_abort($prev); 386 throw new file_exception('storedfilecannotcreatefile'); 387 } 388 if (file_storage::hash_from_path($hashfile.'.tmp') !== $contenthash) { 389 // Highly unlikely edge case, but this can happen on an NFS volume with no space remaining. 390 @unlink($hashfile.'.tmp'); 391 ignore_user_abort($prev); 392 throw new file_exception('storedfilecannotcreatefile'); 393 } 394 if (!rename($hashfile.'.tmp', $hashfile)) { 395 // Something very strange went wrong. 396 @unlink($hashfile . '.tmp'); 397 // Note, we don't try to clean up $hashfile. Almost certainly, if it exists 398 // (e.g. written by another process?) it will be right, so don't wipe it. 399 ignore_user_abort($prev); 400 throw new file_exception('storedfilecannotcreatefile'); 401 } 402 chmod($hashfile, $this->filepermissions); // Fix permissions if needed. 403 @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way. 404 ignore_user_abort($prev); 405 406 return array($contenthash, $filesize, $newfile); 407 } 408 409 /** 410 * Add a file with the supplied content to the file system. 411 * 412 * Note: If overriding this function, it is advisable to store the file 413 * in the path returned by get_local_path_from_hash as there may be 414 * subsequent uses of the file in the same request. 415 * 416 * @param string $content file content - binary string 417 * @return array (contenthash, filesize, newfile) 418 */ 419 public function add_file_from_string($content) { 420 global $CFG; 421 422 $contenthash = file_storage::hash_from_string($content); 423 // Binary length. 424 $filesize = strlen($content); 425 426 $hashpath = $this->get_fulldir_from_hash($contenthash); 427 $hashfile = $this->get_local_path_from_hash($contenthash, false); 428 429 $newfile = true; 430 431 if (file_exists($hashfile)) { 432 if (filesize($hashfile) === $filesize) { 433 return array($contenthash, $filesize, false); 434 } 435 if (file_storage::hash_from_path($hashfile) === $contenthash) { 436 // Jackpot! We have a hash collision. 437 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true); 438 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1"); 439 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content); 440 throw new file_pool_content_exception($contenthash); 441 } 442 debugging("Replacing invalid content file $contenthash"); 443 unlink($hashfile); 444 $newfile = false; 445 } 446 447 if (!is_dir($hashpath)) { 448 if (!mkdir($hashpath, $this->dirpermissions, true)) { 449 // Permission trouble. 450 throw new file_exception('storedfilecannotcreatefiledirs'); 451 } 452 } 453 454 // Hopefully this works around most potential race conditions. 455 456 $prev = ignore_user_abort(true); 457 458 if (!empty($CFG->preventfilelocking)) { 459 $newsize = file_put_contents($hashfile.'.tmp', $content); 460 } else { 461 $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX); 462 } 463 464 if ($newsize === false) { 465 // Borked permissions most likely. 466 ignore_user_abort($prev); 467 throw new file_exception('storedfilecannotcreatefile'); 468 } 469 if (filesize($hashfile.'.tmp') !== $filesize) { 470 // Out of disk space? 471 unlink($hashfile.'.tmp'); 472 ignore_user_abort($prev); 473 throw new file_exception('storedfilecannotcreatefile'); 474 } 475 if (!rename($hashfile.'.tmp', $hashfile)) { 476 // Something very strange went wrong. 477 @unlink($hashfile . '.tmp'); 478 // Note, we don't try to clean up $hashfile. Almost certainly, if it exists 479 // (e.g. written by another process?) it will be right, so don't wipe it. 480 ignore_user_abort($prev); 481 throw new file_exception('storedfilecannotcreatefile'); 482 } 483 chmod($hashfile, $this->filepermissions); // Fix permissions if needed. 484 @unlink($hashfile.'.tmp'); // Just in case anything fails in a weird way. 485 ignore_user_abort($prev); 486 487 return array($contenthash, $filesize, $newfile); 488 } 489 490 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body