MDL-38350 Lib: Rename directory before removing it, to reduce races
This commit is contained in:
@@ -9789,6 +9789,39 @@ function get_performance_info() {
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a file or directory to a unique name within the same directory.
|
||||
*
|
||||
* This function is designed to avoid any potential race conditions, and select an unused name.
|
||||
*
|
||||
* @param string $filepath Original filepath
|
||||
* @param string $prefix Prefix to use for the temporary name
|
||||
* @return string|bool New file path or false if failed
|
||||
* @since Moodle 3.10
|
||||
*/
|
||||
function rename_to_unused_name(string $filepath, string $prefix = '_temp_') {
|
||||
$dir = dirname($filepath);
|
||||
$basename = $dir . '/' . $prefix;
|
||||
$limit = 0;
|
||||
while ($limit < 100) {
|
||||
// Select a new name based on a random number.
|
||||
$newfilepath = $basename . md5(mt_rand());
|
||||
|
||||
// Attempt a rename to that new name.
|
||||
if (@rename($filepath, $newfilepath)) {
|
||||
return $newfilepath;
|
||||
}
|
||||
|
||||
// The first time, do some sanity checks, maybe it is failing for a good reason and there
|
||||
// is no point trying 100 times if so.
|
||||
if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) {
|
||||
return false;
|
||||
}
|
||||
$limit++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete directory or only its content
|
||||
*
|
||||
@@ -9801,6 +9834,19 @@ function remove_dir($dir, $contentonly=false) {
|
||||
// Nothing to do.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$contentonly) {
|
||||
// Start by renaming the directory; this will guarantee that other processes don't write to it
|
||||
// while it is in the process of being deleted.
|
||||
$tempdir = rename_to_unused_name($dir);
|
||||
if ($tempdir) {
|
||||
// If the rename was successful then delete the $tempdir instead.
|
||||
$dir = $tempdir;
|
||||
}
|
||||
// If the rename fails, we will continue through and attempt to delete the directory
|
||||
// without renaming it since that is likely to at least delete most of the files.
|
||||
}
|
||||
|
||||
if (!$handle = opendir($dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4713,4 +4713,67 @@ class core_moodlelib_testcase extends advanced_testcase {
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the rename_to_unused_name function with a file.
|
||||
*/
|
||||
public function test_rename_to_unused_name_file() {
|
||||
global $CFG;
|
||||
|
||||
// Create a new file in dataroot.
|
||||
$file = $CFG->dataroot . '/argh.txt';
|
||||
file_put_contents($file, 'Frogs');
|
||||
|
||||
// Rename it.
|
||||
$newname = rename_to_unused_name($file);
|
||||
|
||||
// Check new name has expected format.
|
||||
$this->assertRegExp('~/_temp_[a-f0-9]+$~', $newname);
|
||||
|
||||
// Check it's still in the same folder.
|
||||
$this->assertEquals($CFG->dataroot, dirname($newname));
|
||||
|
||||
// Check file can be loaded.
|
||||
$this->assertEquals('Frogs', file_get_contents($newname));
|
||||
|
||||
// OK, delete the file.
|
||||
unlink($newname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the rename_to_unused_name function with a directory.
|
||||
*/
|
||||
public function test_rename_to_unused_name_dir() {
|
||||
global $CFG;
|
||||
|
||||
// Create a new directory in dataroot.
|
||||
$file = $CFG->dataroot . '/arghdir';
|
||||
mkdir($file);
|
||||
|
||||
// Rename it.
|
||||
$newname = rename_to_unused_name($file);
|
||||
|
||||
// Check new name has expected format.
|
||||
$this->assertRegExp('~/_temp_[a-f0-9]+$~', $newname);
|
||||
|
||||
// Check it's still in the same folder.
|
||||
$this->assertEquals($CFG->dataroot, dirname($newname));
|
||||
|
||||
// Check it's still a directory
|
||||
$this->assertTrue(is_dir($newname));
|
||||
|
||||
// OK, delete the directory.
|
||||
rmdir($newname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the rename_to_unused_name function with error cases.
|
||||
*/
|
||||
public function test_rename_to_unused_name_failure() {
|
||||
global $CFG;
|
||||
|
||||
// Rename a file that doesn't exist.
|
||||
$file = $CFG->dataroot . '/argh.txt';
|
||||
$this->assertFalse(rename_to_unused_name($file));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ information provided here is intended especially for developers.
|
||||
a callback function instead of an array of options.
|
||||
* Admin setting admin_setting_configselect now supports validating the selection by supplying a
|
||||
callback function.
|
||||
* New library function rename_to_unused_name() to rename a file within its current location.
|
||||
|
||||
=== 3.9 ===
|
||||
* Following function has been deprecated, please use \core\task\manager::run_from_cli().
|
||||
|
||||
Reference in New Issue
Block a user