はじめに
ファイル削除は unlink
で行い、ディレクトリー削除は、 rmdir
で行う。
rmdir
は、ディレクトリーが空でない場合、失敗してしまうので、中身のファイル群を削除してから、
ディレクトリー削除を実行することになる。
とりあえず実装してみる(問題あるかも版)
[実装1] よく見るやつ版
<?php
/// Linux 環境でディレクトリーへのシンボリックリンクを含む場合、失敗する。
/// Windows 環境で存在しないディレクトリーへのリンクを含む場合、失敗する。
/// Windows 環境で <JUNCTION> を含む場合、その中身もリストされる。(バグ?)
/// 引数がディレクトリーへのリンクの場合、リンク先のファイルを削除する。(さらに Linux 環境では最後のrmdirで失敗)
function removeDirectory($directory)
{
$result = true;
foreach(new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
) as $file) {
if ($file->isDir()) {
$result &= @rmdir($file->getPathname());
} else {
$result &= @unlink($file->getPathname());
}
}
return $result && @rmdir($directory);
}
[実装2] 外部プログラム版
<?php
// 外部プログラムを利用する版(環境依存)。
// ディレクトリーを指すシンボリックリンクの場合、リンク先のファイル群が削除される場合がある
// (パスの末尾のセパレーターの有無とか、環境によって動作が異なる)
function removeDirectorySystem($directory)
{
//if (mb_substr_count(realpath($directory), DIRECTORY_SEPARATOR, $encoding) < 3) {
// throw new \UnexpectedValueException(__FUNCTION__."({$directory}) Forbidden shallow directory");
//}
if (defined('PHP_WINDOWS_VERSION_BUILD')) { //< PHP 5.3.0 or later
$command = 'rd /s /q '.escapeshellarg(realpath($directory)).' >nul 2>&1';
} else {
$command = 'rm -rf --no-preserve-root '.escapeshellarg($directory).' >/dev/null 2>&1';
}
$result = ((system($command, $returnVar) !== false) && ($returnVar === 0));
clearstatcache();
return $result || !file_exists($directory);
}
問題点
基本的に、シンボリックリンクで問題となる。
削除失敗ならまだマシ?だけど、リンク先のファイル群が削除されるのは避けたい。
(Linuxだけなら、is_link()
で先に判定すれば良さそうだけど、Windowsのリンクに対応するには少し工夫が必要そう。)
既存の実装を参考にする
種別 | プロジェクト | 関数 | ディレクトリー走査 |
---|---|---|---|
フレームワーク | Laravel | Filesystem::deleteDirectory() |
FilesystemIterator |
フレームワーク | Symfony/Filesystem | Filesystem::remove() |
FilesystemIterator |
フレームワーク | Yii2 | BaseFileHelper::removeDirectory() |
opendir |
フレームワーク | CodeIgniter | delete_files() |
opendir |
フレームワーク | CakePHP | Folder::delete() |
RecursiveDirectoryIterator |
フレームワーク | zend-cache | Filesystem::rmDir() |
glob |
フレームワーク | Nette/Utility | FileSystem::delete() |
FilesystemIterator |
CMS | WordPress | WP_Filesystem_Direct::delete() |
dir |
CMS | Piwik | Filesystem::unlinkRecursive() |
opendir |
CMS | Grav | Folder::doDelete() |
scandir |
CMS | Anchor CMS | delTree() |
scandir |
CMS | Drupal | file_unmanaged_delete_recursive() |
dir |
CMS | EC-CUBE | Symfony/Filesystem |
|
CMS | Zen Cart | zen_remove_file() |
dir |
ライブラリー | League\Flysystem | Local::deleteDir() |
RecursiveDirectoryIterator |
ライブラリー | Gaufrette | Local::delete() |
RecursiveDirectoryIterator |
ライブラリー | util.php | util::rmdir() |
scandir |
その他 | php-src(phar) | Extract_Phar::_removeTmpFiles() |
glob |
その他 | Composer | Filesystem::remove() |
RecursiveDirectoryIterator |
その他 | phpFastCache | Directory::rrmdir() |
RecursiveDirectoryIterator |
その他 | Doctrine/Cache | FileCache::doFlush() |
RecursiveDirectoryIterator |
その他 | Codeception | FileSystem::deleteDir() |
scandir |
その他 | Behat | FeatureContext::clearDirectory() |
scandir |
その他 | Phing | FileSystem::rmdir() |
opendir |
その他 | Rocketeer | League\Flysystem |
Issues(ピックアップ)
-
https://bugs.php.net/bug.php?id=36365 (PHP5.6以前のWindowsの
scandir
で、65535以上のファイルで問題) -
https://bugs.php.net/bug.php?id=52176 (Windowsでディレクトリーを指すシンボリックリンクが
unlink
で削除できない(仕様)) -
https://bugs.php.net/bug.php?id=54987 (PHP5.4.3以前のWindowsの
rmdir
はstatcacheをクリアしない) -
https://bugs.php.net/bug.php?id=55468 (
RecursiveDirectoryIterator
Windowsでのファイルハンドルの問題) -
https://bugs.php.net/bug.php?id=73884 (junction / symlinkd で
is_dir()
が false を返す) - https://github.com/composer/composer/issues/3074 (Windowsでのファイルロックの問題)
-
https://github.com/composer/composer/issues/4009 (
RecursiveDirectoryIterator
作成時のPHPのキャッシュ問題) -
https://github.com/composer/composer/issues/4955 (
<JUNCTION>
の内容が削除される) -
https://github.com/symfony/symfony/issues/4429 (Windowsでディレクトリーを指すシンボリックリンクが
unlink
で削除できない) - https://github.com/symfony/symfony/pull/18324 (mklink for symlinks/junctions)
- https://github.com/thephpleague/flysystem/issues/599 (symlinks サポート)
Windowsのリンク(HARDLINK, SYMLINK, SYMLINKD, JUNCTION)
PHP7.1.9 on Windows
対象 | file_exists | is_file | is_dir | is_link | getType (SplFileInfo) |
削除関数 |
---|---|---|---|---|---|---|
file_real.txt | ○ | ○ | × | × | file | unlink |
dir_real | ○ | × | ○ | × | dir | rmdir |
file_HARDLINK | ○ | ○ | × | × | file | unlink |
file_SYMLINK | ○ | ○ | × | ○ | link | unlink |
file_SYMLINK_broken | × | × | × | ○ | link | unlink |
dir_SYMLINKD | ○ | × | ○ | ○ | link | rmdir |
dir_SYMLINKD_broken | × | × | × | ○ | link | rmdir |
dir_JUNCTION | ○ | × | ○ | × | unknown | rmdir |
dir_JUNCTION_broken | × | × | × | × | unknown | rmdir |
- シンボリックリンク(symlinks)の場合、
file_exists
,is_file
,is_dir
は、リンク先の判定を行う。 - Windows 環境で、
RecursiveDirectoryIterator
を使用すると、<JUNCTION>
をディレクトリーと見做し、その中身もリストされる。(バグ?) -
is_link
とis_dir
だけでは、削除方法の異なる、壊れた<SYMLINK>
,<SYMLINKD>
の区分けが出来ない。 - 壊れた
<JUNCTION>
は、file_exists
,is_file
,is_dir
,is_link
全て失敗する。
再度、実装してみる
[実装3] シンボリックリンクを意識した版
<?php declare(strict_types=1);
/// シンボリックリンクはそのものを削除し、リンク先は削除しない
/// @param string $path ファイルかディレクトリーかリンクへのパス
function removeFileRecursive(string $path) : bool
{
// See https://github.com/composer/composer/issues/4955
$isJunction = function ($junction) {
if ('\\' !== DIRECTORY_SEPARATOR) {
return false;
}
// https://msdn.microsoft.com/en-us/library/14h5k7ff.aspx
// #define _S_IFDIR 0x4000
// #define _S_IFREG 0x8000
$stat = @lstat($junction);
return ($stat !== false) && !($stat['mode'] & 0xC000);
};
clearstatcache(true, $path);
if (is_file($path)) {
return @unlink($path);
} elseif (is_link($path)) {
// See https://bugs.php.net/52176
return (@unlink($path) || ('\\' === DIRECTORY_SEPARATOR && @rmdir($path)));
} elseif ($isJunction($path)) {
return @rmdir($path);
} elseif (is_dir($path)) {
$result = true;
foreach(new \FilesystemIterator($path) as $file) {
if (!removeFileRecursive($file->getPathname())) {
$result = false;
}
}
return $result && @rmdir($path);
}
throw new \UnexpectedValueException(__FUNCTION__."({$path}) No such file or directory");
}
おまけ
Windowsのリンク(HARDLINK, SYMLINK, SYMLINKD, JUNCTION) 表作成スクリプト
- 管理者権限のコマンドプロンプトでのみ有効
- 参考: https://technet.microsoft.com/ja-jp/library/cc753194(v=ws.10).aspx
<?php
$root = '.\\test\\';
function mklink($command)
{
if (!((system("mklink {$command} >nul 2>&1", $returnVar) !== false) && ($returnVar === 0))) {
throw new \ErrorException(__FUNCTION__."($command)");
};
}
function p($result, $pad_length)
{
return is_string($result)
? str_pad($result, $pad_length)
: (($result) ? str_pad('○', $pad_length + 1) : str_pad('×', $pad_length)); //< East Asian Width
}
if (is_dir($root)) {
echo "Failed: $root is exists.";
return;
}
mkdir($root, 0777, true);
$realpath = realpath($root).'\\';
mkdir($root.'d1', 0777, true);
mkdir($root.'d2', 0777, true);
touch($root.'d1\\file.txt');
touch($root.'d2\\file.txt');
mkdir($root.'d3\\dir_real', 0777, true);
touch($root.'d3\\file_real.txt');
chdir($root.'d3');
mklink("/d dir_SYMLINKD {$realpath}d1");
mklink(" file_SYMLINK {$realpath}d1\\file.txt");
mklink("/j dir_JUNCTION {$realpath}d1");
mklink("/h file_HARDLINK {$realpath}d1\\file.txt");
mklink("/d dir_SYMLINKD_broken {$realpath}d2");
mklink(" file_SYMLINK_broken {$realpath}d2\\file.txt");
mklink("/j dir_JUNCTION_broken {$realpath}d2");
chdir('..');
rename('d2', 'd2_');
echo '|対象 |file_exists|is_file|is_dir|is_link|getType<br>(SplFileInfo)|', "\n";
echo '|----------------------|:---------:|:-----:|:----:|:-----:|------------------------|', "\n";
foreach(new \FilesystemIterator('d3') as $file) {
$name = $file->getPathname();
echo '|', implode('|', [
p($file->getFilename(), 22),
p(file_exists($name), 11),
p(is_file($name), 7),
p(is_dir($name), 6),
p(is_link($name), 7),
p(@$file->getType(), 24),
//p(@unlink($name), 11),
//p(@rmdir($name), 11),
]), "|\n";
}
var_dump(is_file('d1\\file.txt'));