Implement watch exclusion lists
Allow exclusion of directories when watching recursively.
This commit is contained in:
@@ -3,42 +3,35 @@ import _NIOFileSystem
|
|||||||
public struct DirectoryResolver {
|
public struct DirectoryResolver {
|
||||||
static let fileManager = FileSystem.shared
|
static let fileManager = FileSystem.shared
|
||||||
|
|
||||||
public static func resolve(_ paths: String...) async throws -> [FilePath] {
|
public static func resolve(_ paths: String..., excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
||||||
try await Self.resolve(paths)
|
try await Self.resolve(paths, excluding: itemNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func resolve(_ paths: [String]) async throws -> [FilePath] {
|
static func resolve(_ paths: [String], excluding itemNames: Set<String> = []) async throws -> [FilePath] {
|
||||||
var resolved: [FilePath] = []
|
var resolved: [FilePath] = []
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let itemPath = FilePath(path)
|
let path = FilePath(path)
|
||||||
try await Self.ensure(itemPath, is: .directory)
|
resolved.append(path)
|
||||||
|
try await withSubdirectories(at: path, recursive: true) { subdirectoryPath in
|
||||||
let allDirectoriesIncludingSelf = try await getAllSubdirectoriesAndSelf(at: itemPath)
|
guard let basename = subdirectoryPath.lastComponent?.description else { return }
|
||||||
resolved.append(contentsOf: allDirectoriesIncludingSelf)
|
guard !itemNames.contains(basename) else { return }
|
||||||
|
resolved.append(subdirectoryPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func ensure(_ path: FilePath, is fileType: FileType) async throws {
|
private static func withSubdirectories(at path: FilePath, recursive: Bool = false, body: (FilePath) async throws -> Void) async throws {
|
||||||
guard let fileInfo = try await fileManager.info(forFileAt: path) else {
|
|
||||||
throw DirectoryResolverError.pathNotFound(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard fileInfo.type == fileType else {
|
|
||||||
throw DirectoryResolverError.pathIsNoDirectory(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func getAllSubdirectoriesAndSelf(at path: FilePath) async throws -> [FilePath] {
|
|
||||||
var result: [FilePath] = []
|
|
||||||
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
let directoryHandle = try await fileManager.openDirectory(atPath: path)
|
||||||
for try await childContent in directoryHandle.listContents(recursive: true) {
|
for try await childContent in directoryHandle.listContents() {
|
||||||
guard childContent.type == .directory else { continue }
|
guard childContent.type == .directory else { continue }
|
||||||
result.append(childContent.path)
|
try await body(childContent.path)
|
||||||
|
if recursive {
|
||||||
|
try await withSubdirectories(at: childContent.path, recursive: recursive, body: body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try await directoryHandle.close()
|
try await directoryHandle.close()
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import CInotify
|
|||||||
|
|
||||||
public actor Inotify {
|
public actor Inotify {
|
||||||
private let fd: CInt
|
private let fd: CInt
|
||||||
|
private var excludedItemNames: Set<String> = []
|
||||||
private var watches = InotifyWatchManager()
|
private var watches = InotifyWatchManager()
|
||||||
private var eventReader: any DispatchSourceRead
|
private var eventReader: any DispatchSourceRead
|
||||||
private var eventStream: AsyncStream<RawInotifyEvent>
|
private var eventStream: AsyncStream<RawInotifyEvent>
|
||||||
@@ -18,6 +19,24 @@ public actor Inotify {
|
|||||||
(self.eventReader, self.eventStream) = Self.createEventReader(forFileDescriptor: fd)
|
(self.eventReader, self.eventStream) = Self.createEventReader(forFileDescriptor: fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isExcluded(_ name: String) -> Bool {
|
||||||
|
self.excludedItemNames.contains(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func exclude(name: String) {
|
||||||
|
self.excludedItemNames.insert(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func exclude(names: String...) {
|
||||||
|
self.exclude(names: names)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func exclude(names: [String]) {
|
||||||
|
for name in names {
|
||||||
|
self.excludedItemNames.insert(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func addWatch(path: String, mask: InotifyEventMask) throws -> CInt {
|
public func addWatch(path: String, mask: InotifyEventMask) throws -> CInt {
|
||||||
let wd = inotify_add_watch(self.fd, path, mask.rawValue)
|
let wd = inotify_add_watch(self.fd, path, mask.rawValue)
|
||||||
@@ -30,7 +49,7 @@ public actor Inotify {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
||||||
let directoryPaths = try await DirectoryResolver.resolve(path)
|
let directoryPaths = try await DirectoryResolver.resolve(path, excluding: self.excludedItemNames)
|
||||||
var result: [CInt] = []
|
var result: [CInt] = []
|
||||||
for path in directoryPaths {
|
for path in directoryPaths {
|
||||||
let wd = try self.addWatch(path: path.string, mask: mask)
|
let wd = try self.addWatch(path: path.string, mask: mask)
|
||||||
@@ -59,6 +78,7 @@ public actor Inotify {
|
|||||||
|
|
||||||
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
private func transform(_ rawEvent: RawInotifyEvent) async -> InotifyEvent? {
|
||||||
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
guard let path = self.watches.path(forId: rawEvent.watchDescriptor) else { return nil }
|
||||||
|
guard !self.excludedItemNames.contains(rawEvent.name) else { return nil }
|
||||||
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
let event = InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
await self.addWatchInCaseOfAutomaticSubtreeWatching(event)
|
||||||
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
return InotifyEvent.init(from: rawEvent, inDirectory: path)
|
||||||
|
|||||||
17
Tests/InotifyIntegrationTests/DirectoryResolverTests.swift
Normal file
17
Tests/InotifyIntegrationTests/DirectoryResolverTests.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
@testable import Inotify
|
||||||
|
|
||||||
|
@Suite("Directory Resolver")
|
||||||
|
struct DirectoryResolverTests {
|
||||||
|
@Test func listsDirectoryTree() async throws {
|
||||||
|
try await withTempDir { dir in
|
||||||
|
let subDirectory = "\(dir)/Subfolder/Folder 01"
|
||||||
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
|
let directories = try await DirectoryResolver.resolve(dir)
|
||||||
|
|
||||||
|
#expect(directories.count == 3)
|
||||||
|
#expect(directories.map { $0.description } == [dir, "\(dir)/Subfolder", subDirectory])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,24 @@ struct RecursiveEventTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func ignoresFileCreationInIgnoredSubfolder() async throws {
|
||||||
|
try await withTempDir { dir in
|
||||||
|
let subDirectory = "\(dir)/Subfolder"
|
||||||
|
let filepath = "\(subDirectory)/modify-target.txt"
|
||||||
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let events = try await getEventsForTrigger(
|
||||||
|
in: dir,
|
||||||
|
mask: [.create],
|
||||||
|
recursive: .recursive,
|
||||||
|
exclude: ["Subfolder"]
|
||||||
|
) { _ in try createFile(at: "\(filepath)", contents: "hello") }
|
||||||
|
|
||||||
|
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
||||||
|
#expect(createEvent == nil, "Did not expect CREATE for '\(filepath)', got: \(events)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
@Test func newSubfoldersOfRecursiveWatchAreAutomaticallyWatchedToo() async throws {
|
||||||
try await withTempDir { dir in
|
try await withTempDir { dir in
|
||||||
let subDirectory = "\(dir)/Subfolder"
|
let subDirectory = "\(dir)/Subfolder"
|
||||||
@@ -32,7 +50,7 @@ struct RecursiveEventTests {
|
|||||||
recursive: .withAutomaticSubtreeWatching
|
recursive: .withAutomaticSubtreeWatching
|
||||||
) { _ in
|
) { _ in
|
||||||
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(atPath: subDirectory, withIntermediateDirectories: true)
|
||||||
try await Task.sleep(for: .milliseconds(200))
|
try await Task.sleep(for: .milliseconds(400))
|
||||||
try createFile(at: "\(filepath)", contents: "hello")
|
try createFile(at: "\(filepath)", contents: "hello")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ func getEventsForTrigger(
|
|||||||
in dir: String,
|
in dir: String,
|
||||||
mask: InotifyEventMask,
|
mask: InotifyEventMask,
|
||||||
recursive: RecursivKind = .nonrecursive,
|
recursive: RecursivKind = .nonrecursive,
|
||||||
|
exclude: [String] = [],
|
||||||
trigger: @escaping (String) async throws -> Void,
|
trigger: @escaping (String) async throws -> Void,
|
||||||
) async throws -> [InotifyEvent] {
|
) async throws -> [InotifyEvent] {
|
||||||
let watcher = try Inotify()
|
let watcher = try Inotify()
|
||||||
|
await watcher.exclude(names: exclude)
|
||||||
switch recursive {
|
switch recursive {
|
||||||
case .nonrecursive:
|
case .nonrecursive:
|
||||||
try await watcher.addWatch(path: dir, mask: mask)
|
try await watcher.addWatch(path: dir, mask: mask)
|
||||||
|
|||||||
Reference in New Issue
Block a user