Implement recursive watching of a directory
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "db0ba74c125e968c67646390cbba012a5572a5c9c54171588ecbb73e370a448d",
|
||||
"originHash" : "fd1e824e418c767633bb79b055a4e84d9c86165746bc881d5d27457ad34b0c20",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "noora",
|
||||
@@ -46,6 +46,15 @@
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -64,6 +73,15 @@
|
||||
"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",
|
||||
"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-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-nio", from: "2.95.0"),
|
||||
.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/tuist/Noora", from: "0.55.1")
|
||||
@@ -29,6 +30,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
"CInotify",
|
||||
.product(name: "Logging", package: "swift-log"),
|
||||
.product(name: "_NIOFileSystem", package: "swift-nio"),
|
||||
.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
|
||||
}
|
||||
|
||||
@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 {
|
||||
guard inotify_rm_watch(self.fd, wd) == 0 else {
|
||||
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(
|
||||
in dir: String,
|
||||
mask: InotifyEventMask,
|
||||
trigger: @escaping (String) async throws -> Void
|
||||
recursive: Bool = false,
|
||||
trigger: @escaping (String) async throws -> Void,
|
||||
) async throws -> [InotifyEvent] {
|
||||
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 {
|
||||
var events: [InotifyEvent] = []
|
||||
|
||||
Reference in New Issue
Block a user