在C#中复制一个目录的全部内容

624

我想在C#中将一个目录的所有内容从一个位置复制到另一个位置。

使用System.IO类似乎没有一种不需要大量递归的方法来实现此目的。

如果我们添加对Microsoft.VisualBasic的引用,则可以使用VB中的一种方法:

new Microsoft.VisualBasic.Devices.Computer().
    FileSystem.CopyDirectory( sourceFolder, outputFolder );

这似乎是一个相当丑陋的黑科技。有没有更好的方法?


117
看下面发布的替代方案,我认为使用VB的方式并不那么丑陋。 - Kevin Kershaw
47
当它是.NET框架的一部分时,怎么可能算作黑客行为呢?停止编写代码,利用你所拥有的东西。 - AMissico
15
这是一个常见的误解。Microsft.VisualBasic包含所有常用的Visual Basic过程,使得使用VB进行编码变得更加容易。Microsot.VisualBasic.Compatibility是用于VB6遗留代码的程序集。 - AMissico
72
微软.VisualBasic.Devices.Computer.FileSystem有超过2000行的代码。CopyDirectory函数确保不会将父文件夹复制到子文件夹中,并进行其他检查。该功能经过高度优化等。所选答案最多只能算是脆弱的代码。 - AMissico
21
为什么这个被优化且完整的代码在Microsoft.VisualBasic而不是 System.IO中?之所以没有出现在Mono中是因为所有被认为是“核心”的库都是System.[something] - 所有其他的库都不是。我并不介意引用额外的DLL,但微软没有将这个功能包含在System.IO中,这其中有一个很好的原因。 - Keith
显示剩余19条评论
29个回答

656
更容易。
private static void CopyFilesRecursively(string sourcePath, string targetPath)
{
    //Now Create all of the directories
    foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
    {
        Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath));
    }

    //Copy all the files & Replaces any files with the same name
    foreach (string newPath in Directory.GetFiles(sourcePath, "*.*",SearchOption.AllDirectories))
    {
        File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
    }
}

36
确实是一段不错的代码,但这并不是可以在任何地方使用的代码。开发人员应该谨慎,因为dirPath.Replace可能会导致意想不到的后果。只是提醒那些喜欢从网上复制粘贴代码的人。由@jaysponsored发布的代码更安全,因为它没有使用string.Replace,但我相信它也有其边角案例。 - Alex
22
如果目标目录已经存在,这段代码会抛出异常,同时也不会覆盖已经存在的文件。在创建每个目录之前,只需要添加一个检查,并使用File.Copy的重载来覆盖目标文件即可。请小心使用此代码。 - joerage
42
如果路径中存在重复的模式,例如 "sourceDir/things/sourceDir/things", 使用 Replace 就会出现问题。应该将它改为 "destinationDir/things/sourceDir/things",但是如果使用 replace,结果会变成 "destinationDir/things/destinationDir/things"。请注意不要改变原文的意思,并尽可能地使翻译通俗易懂。 - Keith
45
为什么要用 *.* 而不是 *?难道你不想复制没有扩展名的文件吗? - Daryl
14
让我们一起构建某个东西并将其贡献给开源 .NET Core... :/ - Lzh
显示剩余16条评论

273

嗯,我觉得我可能误解了问题,但我冒一下险。以下直接的方法有什么问题吗?

public static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target) {
    foreach (DirectoryInfo dir in source.GetDirectories())
        CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name));
    foreach (FileInfo file in source.GetFiles())
        file.CopyTo(Path.Combine(target.FullName, file.Name));
}

编辑 鉴于这篇文章对于一个简单问题的简单回答获得了惊人的反对票数,让我补充一下解释。 在给出反对票之前阅读此内容

首先,这段代码并不是旨在替换问题中的代码的即插即用代码,它仅供示例目的。

Microsoft.VisualBasic.Devices.Computer.FileSystem.CopyDirectory 执行了一些额外的正确性测试(例如,源和目标是否是有效的目录,源是否是目标的父级等),而这些测试在本答案中缺失。那段代码可能也更加优化。

话虽如此,这段代码运行良好。它已经基本相同地用于成熟软件多年。除了所有IO处理都存在的固有的易变性之外(例如,如果用户在代码写入时手动拔掉USB驱动器会发生什么?),没有已知问题。

特别需要指出的是,在这里使用递归绝对不是问题。从理论上讲(概念上,它是最优雅的解决方案),也不会在实践中出现问题:这段代码不会溢出堆栈。堆栈足够大,可以处理即使是嵌套深层次的文件层次结构。在堆栈空间成为问题之前,文件夹路径长度限制就会触发。

请注意,一个恶意用户可能会使用每个字母的深度嵌套目录来破坏这个假设。我没有试过这个。但只是为了说明这一点:为了使这段代码在典型计算机上溢出,目录必须嵌套几千次。这根本不是一个现实的场景。


31
直到最近,操作系统限制了目录嵌套深度。我怀疑你不会找到嵌套超过几百次(即使有的话)的目录。上述代码可以处理更多的目录嵌套深度。 - Konrad Rudolph
6
我喜欢递归的方法,即使最坏情况下也风险极小。 - David Basarab
56
@DTashkinov:抱歉,但那似乎有些过度了。为什么显而易见的代码等于踩?相反应该是真实的。内置方法已经发布,但Keith专门要求另一种方法。此外,你最后一句话是什么意思?抱歉,但我完全不理解你踩的原因。 - Konrad Rudolph
6
比起什么更好?没有人声称它比框架中的VB代码更好。我们知道它不是更好的。 - Konrad Rudolph
6
如果您不想使用问题中提到的VB代码,这似乎是一个合理的方法。我不明白对递归的反对意见。除非遇到指向父目录的符号链接(又名重分析点)的情况,否则该代码永远不会崩溃堆栈,而且很容易避免跟随这些链接并计算递归深度以防止其他奇怪的循环。微软在他们的HowTo回答中推荐使用递归:http://msdn.microsoft.com/en-us/library/vstudio/bb513869%28v=vs.100%29.aspx - GrahamS
显示剩余7条评论

165

MSDN 复制:

using System;
using System.IO;

class CopyDir
{
    public static void Copy(string sourceDirectory, string targetDirectory)
    {
        DirectoryInfo diSource = new DirectoryInfo(sourceDirectory);
        DirectoryInfo diTarget = new DirectoryInfo(targetDirectory);

        CopyAll(diSource, diTarget);
    }

    public static void CopyAll(DirectoryInfo source, DirectoryInfo target)
    {
        Directory.CreateDirectory(target.FullName);

        // Copy each file into the new directory.
        foreach (FileInfo fi in source.GetFiles())
        {
            Console.WriteLine(@"Copying {0}\{1}", target.FullName, fi.Name);
            fi.CopyTo(Path.Combine(target.FullName, fi.Name), true);
        }

        // Copy each subdirectory using recursion.
        foreach (DirectoryInfo diSourceSubDir in source.GetDirectories())
        {
            DirectoryInfo nextTargetSubDir =
                target.CreateSubdirectory(diSourceSubDir.Name);
            CopyAll(diSourceSubDir, nextTargetSubDir);
        }
    }

    public static void Main()
    {
        string sourceDirectory = @"c:\sourceDirectory";
        string targetDirectory = @"c:\targetDirectory";

        Copy(sourceDirectory, targetDirectory);
    }

    // Output will vary based on the contents of the source directory.
}

13
无需检查文件夹是否存在,只需调用Directory.CreateDirectory即可。如果文件夹已经存在,该方法将不执行任何操作。 - Tal Jerome
1
对于那些需要处理超过256个字符的路径的人,你可以使用一个名为ZetaLongPaths的Nuget包。 - A.K
4
这个答案似乎是所有答案中最有用的。通过使用 DirectoryInfo 而不是字符串,可以避免许多潜在的问题。 - DaedalusAlpha

60

或者,如果你想走困难的路线,在你的项目中添加对Microsoft.VisualBasic的引用,然后使用以下内容:

Microsoft.VisualBasic.FileIO.FileSystem.CopyDirectory(fromDirectory, toDirectory);

然而,使用其中一种递归函数是更好的选择,因为它不需要加载VB DLL。

1
这与我之前的做法并没有太大区别 - 你仍然需要加载VB的向后兼容性组件才能完成它。 - Keith
13
加载VB程序集是否昂贵? VB选项比C#版本更优雅。 - jwmiller5
3
“VB的向后兼容性东西”指的是什么?CopyDirectory函数使用Shell或Framework其中之一。 - AMissico
3
我希望它在 System.IO.Directory 上,但这比重写它要好! - Josh M.
3
我认为这是正确的方法,比其他选项都要容易得多。 - reggaeguitar
1
这就是正确的方式!也许我们应该创建一个 UserVoice 或 Github 请求,将这段代码添加到 system.io.directory 中! - juFo

54

试试这个:

Process proc = new Process();
proc.StartInfo.UseShellExecute = true;
proc.StartInfo.FileName = Path.Combine(Environment.SystemDirectory, "xcopy.exe");
proc.StartInfo.Arguments = @"C:\source C:\destination /E /I";
proc.Start();

你的xcopy参数可能会有所不同,但是你已经了解了基本思路。


4
/E表示复制所有子目录(即使是空的)。/I表示如果目标目录不存在,则创建一个同名目录。 - d4nt
6
为了安全起见,请在文本中添加双引号。 - jaysonragasa
7
为了防止被提示覆盖现有文件,请添加/Y参数。https://dev59.com/sHVC5IYBdhLWcg3ww0Hr - Jon Crowell
32
抱歉,但这很糟糕。它假定目标系统是Windows。它假设未来的版本在该特定路径下包括xcopy.exe。它假定xcopy的参数不会改变。它要求将xcopy的参数组装为字符串,这会引入大量的错误潜在性。此外,示例没有提及已启动进程的结果的任何错误处理,我希望有,因为与其他方法不同,这将默默失败。 - cel sharp
4
我认为你很个人化了。回答直截了当,并详细解释了如何实现...由于问题没有要求跨平台兼容性或不使用xcopy或其他任何内容,所以作者只是回答了一个可以实现的方式...可能有1000种方法可以做同样的事情,答案也会有所不同...这就是为什么这个论坛存在的原因,程序员们来自全球分享他们的经验。我对你的评论进行了负评。 - KMX
显示剩余6条评论

45

这个网站总是帮助了我很多,现在轮到我用我所知道的来帮助其他人了。

我希望下面的代码对某些人有用。

string source_dir = @"E:\";
string destination_dir = @"C:\";

// substring is to remove destination_dir absolute path (E:\).

// Create subdirectory structure in destination    
    foreach (string dir in System.IO.Directory.GetDirectories(source_dir, "*", System.IO.SearchOption.AllDirectories))
    {
        System.IO.Directory.CreateDirectory(System.IO.Path.Combine(destination_dir, dir.Substring(source_dir.Length + 1)));
        // Example:
        //     > C:\sources (and not C:\E:\sources)
    }

    foreach (string file_name in System.IO.Directory.GetFiles(source_dir, "*", System.IO.SearchOption.AllDirectories))
    {
        System.IO.File.Copy(file_name, System.IO.Path.Combine(destination_dir, file_name.Substring(source_dir.Length + 1)));
    }

1
请记得添加末尾的反斜杠。 - Alexey F
33
朋友们,使用 Path.Combine() 。永远不要使用字符串拼接来组合文件路径。 - Andy
3
在上述代码片段中,你有一个 OBOB。你应该使用 source_dir.Length + 1,而不是 source_dir.Length - PellucidWombat
最佳答案,但是你应该使用source_dir.Length + 1,如上所述或者TrimStart('\\')) - juanora
不错。最初我的文件夹名称会缺少第一个字母,例如:ongs而不是Songs。然后我删除了source_dir = @"E:"中的斜杠,并将其更改为source_dir = @"E:";。然后它立刻就起作用了!!! - Venugopal M
显示剩余5条评论

16

递归复制文件夹会导致堆栈溢出,因此需要使用非递归方法进行文件夹的递归复制。

public static void CopyDirectory(string source, string target)
{
    var stack = new Stack<Folders>();
    stack.Push(new Folders(source, target));

    while (stack.Count > 0)
    {
        var folders = stack.Pop();
        Directory.CreateDirectory(folders.Target);
        foreach (var file in Directory.GetFiles(folders.Source, "*.*"))
        {
            File.Copy(file, Path.Combine(folders.Target, Path.GetFileName(file)));
        }

        foreach (var folder in Directory.GetDirectories(folders.Source))
        {
            stack.Push(new Folders(folder, Path.Combine(folders.Target, Path.GetFileName(folder))));
        }
    }
}

public class Folders
{
    public string Source { get; private set; }
    public string Target { get; private set; }

    public Folders(string source, string target)
    {
        Source = source;
        Target = target;
    }
}

有用的非递归模板 :) - Minh Nguyen
3
很难想象在超过路径极限之前就爆栈了。 - Ed S.

5

这是我用于像这样的IO任务的实用程序类。

using System;
using System.Runtime.InteropServices;

namespace MyNameSpace
{
    public class ShellFileOperation
    {
        private static String StringArrayToMultiString(String[] stringArray)
        {
            String multiString = "";

            if (stringArray == null)
                return "";

            for (int i=0 ; i<stringArray.Length ; i++)
                multiString += stringArray[i] + '\0';

            multiString += '\0';

            return multiString;
        }

        public static bool Copy(string source, string dest)
        {
            return Copy(new String[] { source }, new String[] { dest });
        }

        public static bool Copy(String[] source, String[] dest)
        {
            Win32.SHFILEOPSTRUCT FileOpStruct = new Win32.SHFILEOPSTRUCT();

            FileOpStruct.hwnd = IntPtr.Zero;
            FileOpStruct.wFunc = (uint)Win32.FO_COPY;

            String multiSource = StringArrayToMultiString(source);
            String multiDest = StringArrayToMultiString(dest);
            FileOpStruct.pFrom = Marshal.StringToHGlobalUni(multiSource);
            FileOpStruct.pTo = Marshal.StringToHGlobalUni(multiDest);

            FileOpStruct.fFlags = (ushort)Win32.ShellFileOperationFlags.FOF_NOCONFIRMATION;
            FileOpStruct.lpszProgressTitle = "";
            FileOpStruct.fAnyOperationsAborted = 0;
            FileOpStruct.hNameMappings = IntPtr.Zero;

            int retval = Win32.SHFileOperation(ref FileOpStruct);

            if(retval != 0) return false;
            return true;
        }

        public static bool Move(string source, string dest)
        {
            return Move(new String[] { source }, new String[] { dest });
        }

        public static bool Delete(string file)
        {
            Win32.SHFILEOPSTRUCT FileOpStruct = new Win32.SHFILEOPSTRUCT();

            FileOpStruct.hwnd = IntPtr.Zero;
            FileOpStruct.wFunc = (uint)Win32.FO_DELETE;

            String multiSource = StringArrayToMultiString(new string[] { file });
            FileOpStruct.pFrom = Marshal.StringToHGlobalUni(multiSource);
            FileOpStruct.pTo =  IntPtr.Zero;

            FileOpStruct.fFlags = (ushort)Win32.ShellFileOperationFlags.FOF_SILENT | (ushort)Win32.ShellFileOperationFlags.FOF_NOCONFIRMATION | (ushort)Win32.ShellFileOperationFlags.FOF_NOERRORUI | (ushort)Win32.ShellFileOperationFlags.FOF_NOCONFIRMMKDIR;
            FileOpStruct.lpszProgressTitle = "";
            FileOpStruct.fAnyOperationsAborted = 0;
            FileOpStruct.hNameMappings = IntPtr.Zero;

            int retval = Win32.SHFileOperation(ref FileOpStruct);

            if(retval != 0) return false;
            return true;
        }

        public static bool Move(String[] source, String[] dest)
        {
            Win32.SHFILEOPSTRUCT FileOpStruct = new Win32.SHFILEOPSTRUCT();

            FileOpStruct.hwnd = IntPtr.Zero;
            FileOpStruct.wFunc = (uint)Win32.FO_MOVE;

            String multiSource = StringArrayToMultiString(source);
            String multiDest = StringArrayToMultiString(dest);
            FileOpStruct.pFrom = Marshal.StringToHGlobalUni(multiSource);
            FileOpStruct.pTo = Marshal.StringToHGlobalUni(multiDest);

            FileOpStruct.fFlags = (ushort)Win32.ShellFileOperationFlags.FOF_NOCONFIRMATION;
            FileOpStruct.lpszProgressTitle = "";
            FileOpStruct.fAnyOperationsAborted = 0;
            FileOpStruct.hNameMappings = IntPtr.Zero;

            int retval = Win32.SHFileOperation(ref FileOpStruct);

            if(retval != 0) return false;
            return true;
        }
    }
}

请注意,Microsoft 在 Microsoft.VisualBasic 中内部使用 SHFileOperation。 - jrh

4

tboswell的替换证明版本(可以抵御文件路径中重复模式的影响)

public static void copyAll(string SourcePath , string DestinationPath )
{
   //Now Create all of the directories
   foreach (string dirPath in Directory.GetDirectories(SourcePath, "*", SearchOption.AllDirectories))
      Directory.CreateDirectory(Path.Combine(DestinationPath ,dirPath.Remove(0, SourcePath.Length ))  );

   //Copy all the files & Replaces any files with the same name
   foreach (string newPath in Directory.GetFiles(SourcePath, "*.*",  SearchOption.AllDirectories))
      File.Copy(newPath, Path.Combine(DestinationPath , newPath.Remove(0, SourcePath.Length)) , true);
    }

4
使用Path.Combine()来拼接文件路径,不要使用字符串连接的方式。 - Andy
在我的情况下,对于目录,我不得不使用Path.Join()而不是Path.Combine()。虽然我不完全明白为什么,但我猜测我可能正在做与文档中建议使用Path.Join()相关的事情。 - davrob01

4
这里有一个简洁高效的解决方案:
namespace System.IO {
  public static class ExtensionMethods {

    public static void CopyTo(this DirectoryInfo srcPath, string destPath) {
      Directory.CreateDirectory(destPath);
      Parallel.ForEach(srcPath.GetDirectories("*", SearchOption.AllDirectories), 
        srcInfo => Directory.CreateDirectory($"{destPath}{srcInfo.FullName[srcPath.FullName.Length..]}"));
      Parallel.ForEach(srcPath.GetFiles("*", SearchOption.AllDirectories), 
        srcInfo => File.Copy(srcInfo.FullName, $"{destPath}{srcInfo.FullName[srcPath.FullName.Length..]}", true));
      });
    }

  }
}

使用方法:

new DirectoryInfo(sourcePath).CopyTo(destinationPath);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接