LoginSignup
10
9

More than 5 years have passed since last update.

PHPで再帰的にrmdir(ディレクトリー削除)の考察

Last updated at Posted at 2017-09-29

はじめに

ファイル削除は 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(ピックアップ)

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_linkis_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) 表作成スクリプト

<?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'));
10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9