删除一个(显然)无限递归的文件夹

某种方式,我们的一台旧的Server 2008(非R2版)服务器出现了一个似乎无限递归的文件夹。这对我们的备份造成了严重影响,因为备份代理程序试图递归进入该文件夹并永远返回不了。
文件夹结构大致如下:
C:\Storage\Folder1
C:\Storage\Folder1\Folder1
C:\Storage\Folder1\Folder1\Folder1
C:\Storage\Folder1\Folder1\Folder1\Folder1

... 等等。就像我们在90年代都喜欢玩的那些曼德博集合之一。

我尝试过:

  • 从资源管理器中删除它。是的,我是个乐观主义者。
  • RMDIR C:\Storage\Folder1 /Q/S - 返回 该目录不为空
  • ROBOCOPY C:\temp\EmptyDirectory C:\Storage\Folder1 /PURGE - 这会花几分钟时间遍历文件夹,然后 robocopy.exe 就崩溃了。

有人能提供一种彻底删除这个文件夹的方法吗?


你试过没有静默开关的rmdir命令吗?尝试使用rmdir C:\storage\folder1 /s - Mathias R. Jessen
是的,恐怕结果一样。 - KenD
1我建议尝试使用/MIR参数,命令如下:ROBOCOPY /MIR C:\temp\EmptyDirectory C:\Storage\Folder1 另外也可以运行一次chkdsk来检查一下。 - jscott
1/MIR 似乎持续时间更长,但最终也失败了("robocopy 已停止工作")。我有点害怕运行 chkdsk;这是一台相当老的服务器,我担心这个问题可能预示着更大的文件系统问题... - KenD
有点相关,希望对你有些有用的信息:http://superuser.com/questions/620442/how-can-one-delete-recursive-directories-in-windows - chue x
还有这个:http://superuser.com/questions/416351/how-to-remove-an-infinitely-recurring-directory-tree - Calimo
7尝试从Linux(Ubuntu/Centos/Fedora/...)桌面试用光盘启动,并从那里删除该文件夹。 - Guntram Blohm
2@KenD 如果你怀疑文件系统出现了损坏问题,你应该首先尝试修复文件系统。尝试删除目录的技巧可能会使情况变得更糟。 - jscott
1根据您下面的回答,由于目录并不是无限深的,只是非常深,如果您安装了CygWin或UnxUtils,您可以使用find命令进行深度优先的目录删除:find Storage/Folder1 -depth -exec rmdir {} \; - Johnny
你试过执行 del /s /q C:\Storage\Folder1 命令了吗? - alexia
@Johnny 我想在MSYS2上也可以使用相同的方法。 - jpmc26
如果长路径名是问题的话,使用 find -exec 并不能解决。而是应该使用 -execdir 在切换到正确位置后使用相对路径。 - Peter Cordes
7个回答

感谢大家提供的有用建议。
深入研究StackOverflow领域后,我通过编写这段C#代码片段解决了问题。它使用了Delimon.Win32.IO库,专门解决访问长文件路径的问题。
以防对其他人有所帮助,这里是代码 - 它成功处理了我之前陷入的大约1600层递归,并花费了大约20分钟将其全部移除。
using System;
using Delimon.Win32.IO;

namespace ConsoleApplication1
{
    class Program
    {
        private static int level;
        static void Main(string[] args)
        {
            // Call the method to delete the directory structure
            RecursiveDelete(new DirectoryInfo(@"\\server\\c$\\storage\\folder1"));
        }

        // This deletes a particular folder, and recurses back to itself if it finds any subfolders
        public static void RecursiveDelete(DirectoryInfo Dir)
        {
            level++;
            Console.WriteLine("Now at level " +level);
            if (!Dir.Exists)
                return;

            // In any subdirectory ...
            foreach (var dir in Dir.GetDirectories())
            {
                // Call this method again, starting at the subdirectory
                RecursiveDelete(dir);
            }

            // Finally, delete the directory, and any files below it
            Dir.Delete(true);
            Console.WriteLine("Deleting directory at level " + level);
            level--;
        }
    }
}

有没有什么理由不直接使用 Directory.Delete(..., true) - Cole Tobin
2我尝试了使用Delimon版本的.Delete(而不是正常的System.IO版本),虽然它没有抛出异常,但似乎没有做任何事情。使用上述方法进行递归耗费了很长时间,而.Delete只花了5-10秒钟。也许它只删除了一些目录,然后放弃了? - KenD
4你最后弄清楚那是怎么发生的了吗?听起来像是某个写得很糟糕的用户程序的问题。 - Parthian Shot
我真的不知道 - 这是一台非常老旧且杂乱无章的服务器,多年来安装了数百种不同的工具,然后又被移除,各种奇怪的人通过RDP访问。我怀疑是某种NTFS的问题;这台服务器将在未来几个月内退役,所以在它被重新分配为支撑摇晃的桌子之前,我会运行CHKDSK命令并查看它找到了什么。 - KenD
8递归调用一个函数1600次?真是陷入了堆栈溢出的领域! - Aleksandr Dubinsky
2只是顺便问一下,这些文件夹里有什么内容吗?如果你能确定递归文件夹创建的时间间隔,并将其乘以递归次数,你就可以(希望能)大致确定这个问题开始的时间范围... - Get-HomeByFiveOClock
1@Get-HomeByFiveOClock:在文件夹中我没有看到任何东西,但我只查看了大约一百个层级 :) 我希望我能记下文件夹的时间戳 - 你说得对,看看它们是否都有相同的时间戳或者是在一段时间内创建的会很有趣... - KenD
8哇,很高兴你最终解决了这个问题。顺便说一下,微软官方支持的解决方法是“重新格式化卷”。是的,真的。:/ - HopelessN00b
1@AleksandrDubinsky:我的旧笔记本上仍然有一个近乎无限深的Eclipse工作空间。我相当确定如果我尝试运行这个程序,它实际上会溢出。 - Lilienthal
1事实上,它并不是无限递归,只是非常深入而已,这一点非常重要,并且是一个重大的误导假设。 - JamesRyan
1这个过程花了20分钟运行,因为每次列出目录时,你都会传递一个不断增长的路径给操作系统,所以它必须每次都检查路径的每个组成部分。而且在实际删除每个目录时也是如此。O(sum(1..n)) = O(n^2)。我发布了一个答案,其中包含两种没有这个问题的方法。 - Peter Cordes

可能是一个递归的连接点。可以使用Sysinternals的文件和磁盘工具junction来创建这样的东西。
mkdir c:\Hello
junction c:\Hello\Hello c:\Hello

现在你可以无限地进入 c:\Hello\Hello\Hello...(直到达到 MAX_PATH,对于大多数命令是260个字符,但对于某些Windows API函数是32,767个字符)。
目录列表显示它是一个连接点:
C:\>dir c:\hello
 Volume in drive C is DR1
 Volume Serial Number is 993E-B99C

 Directory of c:\hello

12/02/2015  08:18 AM    <DIR>          .
12/02/2015  08:18 AM    <DIR>          ..
12/02/2015  08:18 AM    <JUNCTION>     hello [\??\c:\hello]
               0 File(s)              0 bytes
               3 Dir(s)  461,591,506,944 bytes free

C:\>

使用连接工具来删除:
junction -d c:\Hello\Hello

4很遗憾,一个 DIR 命令只会显示普通的目录 - 恐怕看不到任何交叉点的迹象。 - KenD
2你能快速用 junction -s C:\Storage\Folder1 再做一次仔细检查吗? - Brian
3找不到重分析点。:( - KenD
3我很好奇是什么造成了这些实际子目录的混乱。 - Brian
2使用 dir /a 命令查看 '<JUNCTION>' 而无需指定具体名称。 - Chloe
@Brian:简单来说,这是一个批处理文件,它的功能有两个:1. 创建Folder1文件夹;2. 递归地将整个目录树复制到Folder1\中。现在让我们看看如果要复制的目录树中已经存在Folder1会发生什么。 - MSalters

不是一个答案,但我没有足够的声望来发表评论。
我曾经在一个当时庞大的500MB FAT16磁盘上解决了这个问题,那是在一个MS-DOS系统上。我使用DOS debug手动转储和解析目录表。然后,我翻转了一个位来标记递归目录为已删除。我的《Dettman and Wyatt 'DOS Programmers' Reference》给了我指引。
对此我仍然感到非常自豪。如果有任何通用工具能够对FAT32或NTFS卷具有如此强大的控制力,我会感到惊讶和恐惧。那时候生活更简单。

8我会说你对此感到理所当然的自豪。 - mfinni
3+1我给你加点声望。不错的解决方案。 - Chris Thornton
3现在你可以删除你回答的第一句话。 - A.L
这不是一个答案。这是关于另一个操作系统和另一个文件系统的故事。除了对于这个(NT,NTFS)问题没有任何帮助之外,它甚至不能帮助到有着相同问题的人(DOS,FAT16),因为它实际上并没有包含任何细节。 - nobody
@Andrew Medico:我同意你的观点,这也是我第一句话的意思。但是,我会告诉你在哪里找到解决这个略微相关问题的信息。 - Richard
@所有人:非常感谢你们的友善评论。根据手头的信息,最棘手的部分是在解析目录树时计算下一个扇区号进行转储。总共只花了大约一个半小时。生活当时确实更加轻松。 - Richard
你可能可以对XFS或EXT4这样做,但是你会想要运行fsck来更新空闲列表,否则文件系统将无法重新使用那个孤立/悬挂子树中的任何目录的元数据空间(索引节点)。实际上,fsck可能会将悬挂的子树链接回/lost+found - Peter Cordes

Java也可以处理很长的文件路径。而且它处理得更快。 这段代码(我从Java API文档中复制过来的)可以在大约1秒钟内删除一个1600层深的目录结构(在Windows 7,Java 8.0下),而且不会有堆栈溢出的风险,因为它实际上并没有使用递归。
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;

public class DeleteDir {

  static void deleteDirRecur(Path dir) throws IOException {
    Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
         @Override
         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
             throws IOException
         {
             Files.delete(file);
             return FileVisitResult.CONTINUE;
         }
         @Override
         public FileVisitResult postVisitDirectory(Path dir, IOException e)
             throws IOException
         {
             if (e == null) {
                 Files.delete(dir);
                 return FileVisitResult.CONTINUE;
             } else {
                 throw e;
             }
         }
     });
  }

  public static void main(String[] args) throws IOException {
    deleteDirRecur(Paths.get("C:/Storage/Folder1"));
  }
}

3我讨厌你。你逼迫我点赞一个涉及使用Java的答案,现在我感觉很恶心。我需要洗个澡。 - HopelessN00b
非常抱歉。希望您最终能够从创伤中恢复过来。但是微软开发的编程语言(C#)真的更好吗? - Tesseract
5我现在主要是个Windows用户,所以是的,确实如此。由于我们公司使用的业务关键应用程序是一堆垃圾... 呃,恶意软件... 嗯,"企业级"软件套件,我可能对Java感到愤怒和偏见,因为我不得不在近1000个客户端中维护5个特定且不同版本的JRE,其中之一可以追溯到2009年。 - HopelessN00b

如果你进入目录并且只使用相对路径来删除目录,就不需要长路径名。
或者,如果你安装了POSIX shell,或将其移植到DOS等效版本:
# untested code, didn't bother actually testing since the OP already solved the problem.

while [ -d Folder1 ]; do
    mv Folder1/Folder1/Folder1/Folder1  tmp # repeat more times to work in larger batches
    rm -r Folder1     # remove the first several levels remaining after moving the main tree out
    # then repeat to end up with the remaining big tree under the original name
    mv tmp/Folder1/Folder1/.../Folder1 Folder1 
    rm -r tmp
done

(使用一个shell变量来跟踪你在循环条件中重命名的位置是另一种不展开循环的选择,就像我在那里做的那样。)
这样可以避免KenD解决方案的CPU开销,该解决方案强制操作系统每次添加新级别时都从顶部遍历树到第n个级别,检查权限等。因此,它的时间复杂度为sum(1, n) = n * (n-1) / 2 = O(n^2)。从链的开头削减一块的解决方案应该是O(n),除非Windows需要在重命名其父目录时遍历树。(Linux/Unix不需要)从树的底部一直chdir到底,并从那里使用相对路径,随着chdir返回,删除目录的解决方案也应该是O(n),假设操作系统在您执行CDed某处的操作时不需要检查所有父目录的每个系统调用。

find Folder1 -depth -execdir rmdir {} + 在最深的目录中运行rmdir命令。或者实际上,find的-delete选项也可以用于目录,并且隐含了-depth。所以find Folder1 -delete应该做相同的事情,但速度更快。是的,在Linux上的GNU find通过扫描目录、使用相对路径CD到子目录,然后使用相对路径rmdir,最后chdir("..")。它在向上遍历时不会重新扫描目录,因此会消耗O(n)的内存。

这只是一个近似值:strace显示它实际上使用unlinkat(AT_FDCWD, "tmp", AT_REMOVEDIR)open("..", O_DIRECTORY|...),和fchdir(打开目录的文件描述符),还有一些fstat调用混合在其中。但如果在find运行时目录树没有被修改,效果是相同的。

编辑:只是为了好玩,我在GNU/Linux(Ubuntu 14.10)上尝试了这个(在一台2.4GHz的第一代Core2Duo CPU上,在一个XFS文件系统上,使用一块WD 2.5TB Green Power硬盘(WD25EZRS))。
time mkdir -p $(perl -e 'print "annoyingfoldername/" x 2000, "\n"')

real    0m1.141s
user    0m0.005s
sys     0m0.052s

find annoyingfoldername/ | wc
   2000    2000 38019001  # 2k lines / 2k words / 38M characters of text


ll -R annoyingfoldername
... eventually
ls: cannot access ./annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername/annoyingfoldername: File name too long
total 0
?????????? ? ? ? ?            ? annoyingfoldername

time find annoyingfoldername -delete

real    0m0.054s
user    0m0.004s
sys     0m0.049s

# about the same for normal rm -r,
# which also didn't fail due to long path names

(mkdir -p 创建一个目录和任何缺失的路径组件)。
是的,真的只需要0.05秒来执行2k个rmdir操作。xfs在日志中很擅长将元数据操作批量处理在一起,因为他们在10年前就解决了元数据操作速度慢的问题。
在ext4上,创建操作花费了0m0.279秒,使用find删除操作仍然需要0m0.074秒。

好奇心驱使我尝试在Linux上运行。结果发现标准的GNU工具对于长路径来说都非常适用,因为它们会递归遍历整个目录树,而不是尝试使用巨长的路径进行系统调用。甚至当你在命令行中传入一个38k的路径时,mkdir命令也能正常运行! - Peter Cordes

我也遇到过这个问题,不过是在一个独立的Windows 10系统上。C:\User\Name\Repeat\Repeat\Repeat\Repeat\Repeat\Repeat\Repeat似乎无限循环。
我可以使用Windows或命令提示符导航到大约第50个,但无法继续。我无法删除它,也无法点击它等等。
因为C是我的语言,所以最终我编写了一个带有系统调用循环的程序,直到失败为止。你可以使用任何语言来做这个,甚至是DOS批处理。我创建了一个名为tmp的目录,并将Repeat\Repeat移动到其中,然后删除现在为空的Repeat文件夹,并将tmp\Repeat移回当前文件夹。一次又一次地重复这个过程!
 while (times<2000)
 {
  ChkSystem("move Repeat\\Repeat tmp");
  ChkSystem("rd Repeat");
  ChkSystem("move tmp\\Repeat Repeat");
  ++times;
  printf("Removed %d nested so far.\n", times);
 }

ChkSystem只是运行了一个system()调用并检查返回值,如果失败就停止。
重要的是,它失败了多次。我以为也许我的程序不工作了,或者说它终究是无限长的。然而,我之前在系统调用中遇到过这种情况,有时候事情不同步,所以我再次运行程序,它就从离开的地方继续执行了,所以不要立即认为你的程序不工作。总共运行了大约20次后,所有问题都解决了。最初总共有1280个文件夹深度。不知道是什么原因导致的。真是疯狂啊。

我遇到了一个类似的问题,涉及到一个深度为5000+的目录结构混乱的文件夹,是由某个Java应用程序引起的。我编写了一个程序,可以帮助你删除这个文件夹。整个源代码在这个链接中:

https://imanolbarba.net/gitlab/imanol/DiREKT

一段时间后它移除了整个东西,但它成功完成了任务,希望能帮到遇到同样烦人问题的人(包括我)。


请不要只发布链接回答。您应该将链接中最重要的信息放在帖子本身,并提供链接以作参考。 - Frederik
哦,抱歉,这是一个程序,我真的不想在这里发布全部的源代码...我认为很明显我写了一个程序,并且我正在通过这个链接进行托管,答案中也包含了动机和所有相关信息,所以对我来说看起来很明显这不是一个仅有链接的回答,尽管如此,我会更明确地说明这是一个旨在解决这个问题的软件。 - Imanol Barba Sabariego