如何快速监控文件夹中的新文件?

Posted

技术标签:

【中文标题】如何快速监控文件夹中的新文件?【英文标题】:How to monitor a folder for new files in swift? 【发布时间】:2014-06-10 20:13:50 【问题描述】:

如何在没有轮询的情况下快速监控文件夹中的新文件(效率非常低)?我听说过诸如 kqueue 和 FSEvents 之类的 API,但我不确定是否可以快速实现它们?

【问题讨论】:

FSEventStreamCreate 从 Swift 中丢失,不允许纯 Swift 实现。但是,您可以创建一个 Obj-C 包装类并使用 Swift 对其进行操作。我不得不对 CommonCrypto 进行类似的哈希处理。 注意:这是一个关于这个的旧讨论:***.com/questions/7720246/… 我看到了一个我正在努力使用的 FSEventStreamCreate 的 Swift 版本(由于完全缺乏 Swift 知识......)developer.apple.com/library/mac/documentation/Darwin/Reference/… 我发现blog.beecomedigital.com/2015/06/27/… 看起来很有希望,但不幸的是不能很好地处理内存。我只需要知道添加了哪个文件,但似乎没有一个简单的解决方案。 我找到了这个github.com/gurinderhans/SwiftFSWatcher。它甚至允许为不同的地方创建多个monitors 【参考方案1】:

GCD 似乎是要走的路。 NSFilePresenter 类不能正常工作。它们有问题,坏了,Apple 在过去的 4 年里一直不愿意修复它们。可能会被弃用。

这是一篇很好的帖子,描述了这项技术的要点。

"Handling Filesystem Events with GCD",大卫·哈姆里克

从网站引用的示例代码。我将他的 C 代码翻译成 Swift。

    let fildes = open("/path/to/config.plist", O_RDONLY)

    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    let source = dispatch_source_create(
        DISPATCH_SOURCE_TYPE_VNODE,
        UInt(fildes),
        DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE,
        queue)

    dispatch_source_set_event_handler(source,
        
            //Reload the config file
        )

    dispatch_source_set_cancel_handler(source,
        
            //Handle the cancel
        )

    dispatch_resume(source);

    ...

        // sometime later
        dispatch_source_cancel(source);

作为参考,这是作者发布的另一个QA:

Grand Central Dispatch (GCD) dispatch source flags Monitoring a directory in Cocoa/Cocoa Touch

如果您有兴趣观看目录,这里有另一篇描述它的帖子。

"Monitoring a Folder with GCD" 在 Cocoanetics 上。 (不幸的是,我找不到作者的名字。我很抱歉缺少署名)

唯一明显的区别是获取文件描述符。这使得目录的事件通知仅文件描述符。

_fileDescriptor = open(path.fileSystemRepresentation(), O_EVTONLY)

更新

之前我声称FSEvents API 不工作,但我错了。该 API 运行良好,如果您有兴趣观看深度文件树,那么它的简单性可能比 GCD 更好。

无论如何,FSEvents 不能在纯 Swift 程序中使用。因为它需要传递 C 回调函数,而 Swift 目前不支持它(Xcode 6.1.1)。然后我不得不退回到 Objective-C 并再次包装它。

此外,任何此类 API 都是完全异步的。这意味着在您收到通知时,实际的文件系统状态可能会有所不同。那么精确或准确的通知并没有真正的帮助,仅对标记脏标志有用。

更新 2

我最终为 Swift 编写了一个围绕 FSEvents 的包装器。 这是我的工作,希望对您有所帮助。

https://github.com/eonil/FileSystemEvents

【讨论】:

不错。我试图“听”一次被写入的文件夹,但因为无法让FSEvents 正常工作而放弃了。我一直在等待由相机驱动程序写入非常大的图像文件,并且在文件“准备好”打开、以黑色图像结束等的正确时刻得到通知。另外,我看到了一些广播的消息不一致,很难解释它们。也许我没有正确理解文档,也没有遇到好的示例代码。肯定会尝试 GCD。 谢谢。我刚刚通过直接从 GitHub 克隆来使用您的框架,并关注您的视频并构建!第一次工作。所以,现在我只需要学习一些 Swift 来使用它! :-) 嘿@Eonil 你知道我如何检测文件何时更新/修改吗?我看到了您的近似值,并且我尝试了更多我发现的方法,但是当文件更新时没有触发。 NSFilePresenter 不是一个类,它是一个协议。它本身没有任何功能。它通常与NSFileCoordinator 一起使用,并且此类不是用于监视目录,而是用于在单个进程内以及跨进程边界协调文件系统上的读/写 I/O 操作,并且使用 NSFileCoordinator 可以完美地工作。跨度> 【参考方案2】:

我改编了 Stanislav Smida 的代码,使其与 Xcode 8 和 Swift 3 一起使用

class DirectoryObserver 

    private let fileDescriptor: CInt
    private let source: DispatchSourceProtocol

    deinit 

      self.source.cancel()
      close(fileDescriptor)
    

    init(URL: URL, block: @escaping ()->Void) 

      self.fileDescriptor = open(URL.path, O_EVTONLY)
      self.source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: self.fileDescriptor, eventMask: .all, queue: DispatchQueue.global())
      self.source.setEventHandler  
          block()
      
      self.source.resume()
  


【讨论】:

用 Swift 4.2 测试过,仍然很好用。谢谢! 2021 年。代码有效。很棒的文件监视器!看起来只有一个没有子目录的目录。只是我搜索的想法!非常感谢!【参考方案3】:

最简单的解决方案是使用 Apple 的 DirectoryMonitor.swift https://developer.apple.com/library/mac/samplecode/Lister/Listings/ListerKit_DirectoryMonitor_swift.html

var dm = DirectoryMonitor(URL: AppDelegate.applicationDocumentsDirectory)
dm.delegate = self
dm.startMonitoring()

【讨论】:

链接已损坏。 是的,苹果已经删除了这个链接( Swift 3 版本镜像于:github.com/robovm/apple-ios-samples/blob/master/…【参考方案4】:

SKQueue 是一个围绕 kqueue 的 Swift 包装器。这是监视目录并通知写入事件的示例代码。

class SomeClass: SKQueueDelegate 
  func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) 
    print("\(notification.toStrings().map  $0.rawValue ) @ \(path)")
  


if let queue = SKQueue() 
  let delegate = SomeClass()

  queue.delegate = delegate
  queue.addPath("/some/file/or/directory")
  queue.addPath("/some/other/file/or/directory")

【讨论】:

【参考方案5】:

用于目录监视器的 Swift 5 版本,带有 GCD,来自Apple

import Foundation

/// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory.
protocol DirectoryMonitorDelegate: class 
    func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor)


class DirectoryMonitor 
    // MARK: Properties

    /// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates.
    weak var delegate: DirectoryMonitorDelegate?

    /// A file descriptor for the monitored directory.
    var monitoredDirectoryFileDescriptor: CInt = -1

    /// A dispatch queue used for sending file changes in the directory.
    let directoryMonitorQueue =  DispatchQueue(label: "directorymonitor", attributes: .concurrent)

    /// A dispatch source to monitor a file descriptor created from the directory.
    var directoryMonitorSource: DispatchSource?

    /// URL for the directory being monitored.
    var url: URL

    // MARK: Initializers
    init(url: URL) 
        self.url = url
    

    // MARK: Monitoring

    func startMonitoring() 
        // Listen for changes to the directory (if we are not already).
        if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 
            // Open the directory referenced by URL for monitoring only.
            monitoredDirectoryFileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)

            // Define a dispatch source monitoring the directory for additions, deletions, and renamings.
            directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: directoryMonitorQueue) as? DispatchSource

            // Define the block to call when a file change is detected.
            directoryMonitorSource?.setEventHandler
                // Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change.
                self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self)
            

            // Define a cancel handler to ensure the directory is closed when the source is cancelled.
            directoryMonitorSource?.setCancelHandler
                close(self.monitoredDirectoryFileDescriptor)

                self.monitoredDirectoryFileDescriptor = -1

                self.directoryMonitorSource = nil
            

            // Start monitoring the directory via the source.
            directoryMonitorSource?.resume()
        
    

    func stopMonitoring() 
        // Stop listening for changes to the directory, if the source has been created.
        if directoryMonitorSource != nil 
            // Stop monitoring the directory via the source.
            directoryMonitorSource?.cancel()
        
    


【讨论】:

【参考方案6】:

我尝试过使用这几行代码。到目前为止似乎有效。

class DirectoryObserver 

    deinit 

        dispatch_source_cancel(source)
        close(fileDescriptor)
    

    init(URL: NSURL, block: dispatch_block_t) 

        fileDescriptor = open(URL.path!, O_EVTONLY)
        source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, UInt(fileDescriptor), DISPATCH_VNODE_WRITE, dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT))
        dispatch_source_set_event_handler(source,  dispatch_async(dispatch_get_main_queue(), block) )
        dispatch_resume(source)
    

    //

    private let fileDescriptor: CInt
    private let source: dispatch_source_t

确保不要进入保留周期。如果您要在块中使用此实例的所有者,请安全地进行。例如:

self.directoryObserver = DirectoryObserver(URL: URL, block:  [weak self] in

    self?.doSomething()
)

【讨论】:

很棒的代码。下面是一个监控桌​​面的例子: let theURL = NSURL("~/Desktop/".stringByExpandingTildeInPath) 您的代码仅在添加或删除文件时有效。不是文件更改。无论如何都很棒。我需要监控文件更改,所以我一直在寻找。 FSEvent 似乎是我正在寻找的。​​span> 【参考方案7】:

我发现我目前正在使用的最简单的方法是这个很棒的库:https://github.com/eonist/FileWatcher

From README

安装:

CocoaPods pod "FileWatcher" 迦太基github "eonist/FileWatcher" "master" 手动开启FileWatcherExample.xcodeproj
let filewatcher = FileWatcher([NSString(string: "~/Desktop").expandingTildeInPath])

filewatcher.callback =  event in
  print("Something happened here: " + event.path)


filewatcher.start() // start monitoring

【讨论】:

但是效率高吗? 非常适合我。没有性能问题。你的用例是什么?【参考方案8】:

我遇到了任何答案中都没有提到的问题。由于我的应用程序使用 UIDocumentBrowserViewController(即 Apple 自己的 Files 应用程序)来管理其文档,因此我无法控制用户的习惯。我使用SKQueue 监控所有文件以保持元数据同步,但在某个时候应用程序开始崩溃。

事实证明,一个应用程序可以同时打开的文件描述符上限为 256 个,即使只是为了监控也是如此。我最终将 SKQueue 和 Apple 的 Directory Monitor(您可以在当前线程的 this answer 中找到的参考)结合起来创建了一个名为 SFSMonitor 的类,它使用 Dispatch Sources 监视整个文件或目录队列。

我在this SO thread 中详细介绍了我的发现和我现在使用的做法。

【讨论】:

它是否监控子目录? 每个目录和子目录都作为一个单独的实体进行监控。假设您将一个子目录复制到一个受监控的目录中:您将收到该更改的通知,但您不会知道该子目录中发生的任何事情。您必须检查添加了什么,当您意识到它是一个子目录时,将其添加到您的监视列表中。换句话说 - 观察不是递归的,就原始目录而言,子目录只是另一个条目。【参考方案9】:

您可以将 UKKQueue 添加到您的项目中。请参阅http://zathras.de/angelweb/sourcecode.htm,它很容易使用。 UKKQueue 是用 Objective C 编写的,但你可以从 swift 中使用它

【讨论】:

【参考方案10】:

根据您的应用需求,您或许可以使用简单的解决方案。

我实际上在生产产品中使用了 kqueue;我对性能并不疯狂,但它确实有效,所以我并没有想太多,直到我找到了一个可以更好地满足我的需求的小技巧,此外,它使用的资源更少,这对于性能密集型来说很重要程式。

再次,如果您的项目允许,您可以做的是,每次切换到应用程序时,您只需检查文件夹作为逻辑的一部分,而不必使用 kqueue 定期检查文件夹。这很有效,而且使用的资源要少得多。

【讨论】:

我认为 Atom.io 在断言文件是否已通过 git 推送时会这样做。必须在应用程序之间来回切换才能看到变化,这非常烦人。这对我来说是一种 UI/UX 反模式。

以上是关于如何快速监控文件夹中的新文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何快速找到添加/删除的文件?

Flutter 如何将图像文件保存到图库中的新文件夹?

Google Cloud Storage:如何使用 gsutil 获取存储桶/文件夹中的新文件列表

如何在没有监控线程或进程的情况下使用 C++ 快速获取目录是不是已更改?

WGCLOUD如何设置指定主机的告警值

如何在 Mac 上快速执行 python 文件