等待文件夹及其内容被完全复制

Wait for a folder and its content to be completely copied

我正在使用 FileSystemWatcher 监视文件夹并检查是否出现新文件夹。然后我必须从其他地方复制一些文件。但是我必须先等待文件夹被复制。这是代码。

bool waiting = true;
var watcher = new FileSystemWatcher(path);
watcher.Created += (obj, args) =>
{
    //do something
    waiting = false;        
};
watcher.NotifyFilter = NotifyFilters.DirectoryName;
watcher.EnableRaisingEvents = true;

while(waiting)
{

}

问题是文件夹一创建我就收到通知并且 "do something" 部分发生,即使文件夹尚未完全复制,显然我遇到了问题。我必须以某种方式等待文件夹在 "do something" 部分之前完全复制。我该怎么做?

这是所有文件同步应用程序(如 Dropbox、OneDrive 等)面临的一个常见问题。复制大文件涉及一个创建事件和 多个 Changed 事件,因为文件不能在单个操作中创建。没有 Closed 事件,因此应用程序只能等到 changed 事件停止后再开始散列和同步。

事实上,您会注意到,当您将大量文件复制到 Dropbox 等人监控的文件夹中时,他们会停止正在做的事情,并在复制停止后稍等片刻。

Reactive Extensions in .NET, Java, Javascript and other languages allow the use of LINQ-like queries over streams of events. One of the available operators is Debounce 在发出最后一个事件之前等待事件流平静下来。此运算符(在 .NET 中称为 Throttle)可用于检测文件创建何时停止。

此示例在最后一个文件创建后等待 5 秒,然后再调用订阅者方法:

using (var fsw = new FileSystemWatcher(@"K:\Backups"))
{
    fsw.InternalBufferSize = 65536;
    var creations = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => fsw.Created += h,
        h => fsw.Created -= h);

    creations.Timestamp()
        .Throttle(TimeSpan.FromSeconds(5))
        .Select(x => $"{ x.Timestamp} : {DateTime.Now - x.Timestamp} -  {x.Value.EventArgs.FullPath}")
        .Subscribe(Console.WriteLine);
}

Timestamp用于为每个事件添加一个Timestamp属性,用来演示订阅者创建文件和执行文件的时间差。

通过仅 return 最后一个事件,这个 Throttle() 可用于整个文件夹的信号处理。要处理单个文件,我们需要分别限制每个文件生成的事件流。换句话说,按文件对事件进行分组:

var obs = from creation in creations
            group creation by creation.EventArgs.FullPath into g
            from last in g.Throttle(TimeSpan.FromSeconds(5))
            select last.EventArgs.FullPath;
obs.Subscribe(Console.WriteLine);

在这种情况下,LINQ 查询语法要容易得多。 group by 按文件名对事件进行分组,然后 Throttle() 在安静 5 秒后发出每个文件的最后一个事件。

要使其适用于大文件,我们需要结合 Created 和 Changed 事件。这是 Merge 运算符的工作:

var changes = Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
        h => fsw.Changed += h,
        h => fsw.Changed -= h)

var obs = from evt in creations.Merge(changes)
        group evt by evt.EventArgs.FullPath into g
        from last in g.Throttle(TimeSpan.FromSeconds(5))
        select $"{last.EventArgs.ChangeType} - {last.EventArgs.FullPath}";

这就是事情的发展方向 BOOM!

在Windows 10 上复制仅引发两个 更改事件,最后一个仅在复制完成时引发。如果文件太大(GB 或数百 MB,取决于磁盘速度),第二个事件可能需要很长时间才能到达。

一个选项是设置一个大的时间跨度,大到足以涵盖大多数 IO 操作,例如 TimeSpan.FromMinutes(1)

另一种选择是使用另一个运算符 Buffer(),它可以批量捕获指定数量的项目并将它们 return 作为数组 :

var obs = from evt in creations.Merge(changes)
        group evt by evt.EventArgs.FullPath into g
        from last in g.Buffer(3)
        select $"{last[2].EventArgs.ChangeType} - {last[2].EventArgs.FullPath}";

这仅在复制时有效。保存来自 Excel 或 Word 的文件可能会导致多个 Changed 事件,因为应用程序对文件进行了多次更改。

Buffer 还可以采用 Timespan 参数,该参数可用于收集每个文件的所有 Changed 事件并检查它们是否符合两种模式之一。多重变化?只是 start/end 更改事件?最后一个事件是什么时候发生的(由 Timestamp 提供)?