Implement recursive watching of a directory
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "db0ba74c125e968c67646390cbba012a5572a5c9c54171588ecbb73e370a448d",
|
"originHash" : "fd1e824e418c767633bb79b055a4e84d9c86165746bc881d5d27457ad34b0c20",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "noora",
|
"identity" : "noora",
|
||||||
@@ -46,6 +46,15 @@
|
|||||||
"version" : "1.1.3"
|
"version" : "1.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-atomics",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-atomics.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
|
||||||
|
"version" : "1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-collections",
|
"identity" : "swift-collections",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -64,6 +73,15 @@
|
|||||||
"version" : "1.10.1"
|
"version" : "1.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "e932d3c4d8f77433c8f7093b5ebcbf91463948a0",
|
||||||
|
"version" : "2.95.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-subprocess",
|
"identity" : "swift-subprocess",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.0"),
|
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.0"),
|
||||||
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.1.3"),
|
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.1.3"),
|
||||||
.package(url: "https://github.com/apple/swift-log", from: "1.10.1"),
|
.package(url: "https://github.com/apple/swift-log", from: "1.10.1"),
|
||||||
|
.package(url: "https://github.com/apple/swift-nio", from: "2.95.0"),
|
||||||
.package(url: "https://github.com/apple/swift-system", from: "1.6.4"),
|
.package(url: "https://github.com/apple/swift-system", from: "1.6.4"),
|
||||||
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.3.0"),
|
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.3.0"),
|
||||||
.package(url: "https://github.com/tuist/Noora", from: "0.55.1")
|
.package(url: "https://github.com/tuist/Noora", from: "0.55.1")
|
||||||
@@ -29,6 +30,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
"CInotify",
|
"CInotify",
|
||||||
.product(name: "Logging", package: "swift-log"),
|
.product(name: "Logging", package: "swift-log"),
|
||||||
|
.product(name: "_NIOFileSystem", package: "swift-nio"),
|
||||||
.product(name: "SystemPackage", package: "swift-system")
|
.product(name: "SystemPackage", package: "swift-system")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
44
Sources/Inotify/DirectoryResolver.swift
Normal file
44
Sources/Inotify/DirectoryResolver.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import _NIOFileSystem
|
||||||
|
|
||||||
|
public struct DirectoryResolver {
|
||||||
|
static let fileManager = FileSystem.shared
|
||||||
|
|
||||||
|
public static func resolve(_ paths: String...) async throws -> [FilePath] {
|
||||||
|
try await Self.resolve(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func resolve(_ paths: [String]) async throws -> [FilePath] {
|
||||||
|
var resolved: [FilePath] = []
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let itemPath = FilePath(path)
|
||||||
|
try await Self.ensure(itemPath, is: .directory)
|
||||||
|
|
||||||
|
let allDirectoriesIncludingSelf = try await getAllSubdirectoriesAndSelf(at: itemPath)
|
||||||
|
resolved.append(contentsOf: allDirectoriesIncludingSelf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func ensure(_ path: FilePath, is fileType: FileType) 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)
|
||||||
|
for try await childContent in directoryHandle.listContents(recursive: true) {
|
||||||
|
guard childContent.type == .directory else { continue }
|
||||||
|
result.append(childContent.path)
|
||||||
|
}
|
||||||
|
try await directoryHandle.close()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Sources/Inotify/DirectoryResolverErrror.swift
Normal file
16
Sources/Inotify/DirectoryResolverErrror.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Foundation
|
||||||
|
import SystemPackage
|
||||||
|
|
||||||
|
public enum DirectoryResolverError: LocalizedError, Equatable {
|
||||||
|
case pathNotFound(FilePath)
|
||||||
|
case pathIsNoDirectory(FilePath)
|
||||||
|
|
||||||
|
var errorDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .pathNotFound(let path):
|
||||||
|
return "Path not found: \(path)"
|
||||||
|
case .pathIsNoDirectory(let path):
|
||||||
|
return "Path is not a directory: \(path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,17 @@ public actor Inotify {
|
|||||||
return wd
|
return wd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public func addRecursiveWatch(forDirectory path: String, mask: InotifyEventMask) async throws -> [CInt] {
|
||||||
|
let directoryPaths = try await DirectoryResolver.resolve(path)
|
||||||
|
var result: [CInt] = []
|
||||||
|
for path in directoryPaths {
|
||||||
|
let wd = try self.addWatch(path: path.string, mask: mask)
|
||||||
|
result.append(wd)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
public func removeWatch(_ wd: CInt) throws {
|
public func removeWatch(_ wd: CInt) throws {
|
||||||
guard inotify_rm_watch(self.fd, wd) == 0 else {
|
guard inotify_rm_watch(self.fd, wd) == 0 else {
|
||||||
throw InotifyError.removeWatchFailed(watchDescriptor: wd, errno: cinotify_get_errno())
|
throw InotifyError.removeWatchFailed(watchDescriptor: wd, errno: cinotify_get_errno())
|
||||||
|
|||||||
23
Tests/InotifyIntegrationTests/RecursiveEventTests.swift
Normal file
23
Tests/InotifyIntegrationTests/RecursiveEventTests.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
@testable import Inotify
|
||||||
|
|
||||||
|
@Suite("Recursive Event Detection")
|
||||||
|
struct RecursiveEventTests {
|
||||||
|
@Test func detectsFileCreationInSubfolder() 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: true
|
||||||
|
) { _ in try createFile(at: "\(filepath)", contents: "hello") }
|
||||||
|
|
||||||
|
let createEvent = events.first { $0.mask.contains(.create) && $0.path.string == filepath }
|
||||||
|
#expect(createEvent != nil, "Expected CREATE for '\(filepath)', got: \(events)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,15 @@ import Inotify
|
|||||||
func getEventsForTrigger(
|
func getEventsForTrigger(
|
||||||
in dir: String,
|
in dir: String,
|
||||||
mask: InotifyEventMask,
|
mask: InotifyEventMask,
|
||||||
trigger: @escaping (String) async throws -> Void
|
recursive: Bool = false,
|
||||||
|
trigger: @escaping (String) async throws -> Void,
|
||||||
) async throws -> [InotifyEvent] {
|
) async throws -> [InotifyEvent] {
|
||||||
let watcher = try Inotify()
|
let watcher = try Inotify()
|
||||||
try await watcher.addWatch(path: dir, mask: mask)
|
if recursive {
|
||||||
|
try await watcher.addRecursiveWatch(forDirectory: dir, mask: mask)
|
||||||
|
} else {
|
||||||
|
try await watcher.addWatch(path: dir, mask: mask)
|
||||||
|
}
|
||||||
|
|
||||||
let eventTask = Task {
|
let eventTask = Task {
|
||||||
var events: [InotifyEvent] = []
|
var events: [InotifyEvent] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user