diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index daf22cf..790a8fa 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -17,7 +17,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v1 with: - swift-version: 5.7 + swift-version: 5.9 - name: Build run: swift build -v - name: Run tests diff --git a/Package.swift b/Package.swift index 6cf94c9..18641b4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -14,8 +14,11 @@ let package = Package( name: "FileMonitor", targets: ["FileMonitor"]), .executable( - name: "FileMonitorExample", - targets: ["FileMonitorExample"] + name: "FileMonitorDelegateExample", + targets: ["FileMonitorDelegateExample"] + ), + .executable(name: "FileMonitorAsyncStreamExample", + targets: ["FileMonitorAsyncStreamExample"] ) ], dependencies: [ @@ -50,7 +53,10 @@ let package = Package( path: "Sources/FileMonitorMacOS" ), .executableTarget( - name: "FileMonitorExample", + name: "FileMonitorDelegateExample", + dependencies: ["FileMonitor"]), + .executableTarget( + name: "FileMonitorAsyncStreamExample", dependencies: ["FileMonitor"]), .testTarget( name: "FileMonitorTests", diff --git a/README.md b/README.md index 469278e..6633560 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ FileMonitor focuses on monitoring file changes within a given directory. It offe - Detection of file creations - Detection of file modifications - Detection of file deletions +- AsyncStream delivery of detections All events are propagated through a delegate function using a switchable enum type. @@ -40,7 +41,32 @@ Don't forget to add the product "FileMonitor" as a dependency for your target: ``` ## Usage -To use FileMonitor, follow this example: +### Use with AsyncStream +Example usage: +```swift +import FileMonitor +import Foundation + +struct FileMonitorExample: FileDidChangeDelegate { + init() throws { + let dir = FileManager.default.homeDirectoryForCurrentUser.appending(path: "Downloads") + let monitor = try FileMonitor(directory: dir, delegate: self ) + try monitor.start() + for await event in monitor.stream { + switch event { + case .added(let file): + print("New file \(file.path)") + default: + print("\(event)") + } + } + } +} +``` + + +### Use as a Delegate +Example usage: ```swift import FileMonitor diff --git a/Sources/FileMonitor/FileMonitor.swift b/Sources/FileMonitor/FileMonitor.swift index d5e0217..65f9c11 100644 --- a/Sources/FileMonitor/FileMonitor.swift +++ b/Sources/FileMonitor/FileMonitor.swift @@ -21,6 +21,10 @@ public enum FileMonitorErrors: Error { /// FileMonitor: Watch for file changes in a directory with a unified API on Linux and macOS. public struct FileMonitor: WatcherDelegate { + private let fileChangeStream = AsyncStream.makeStream(of: FileChange.self) + public var stream: AsyncStream { + fileChangeStream.stream + } var watcher: WatcherProtocol public var delegate: FileDidChangeDelegate? { @@ -67,6 +71,7 @@ public struct FileMonitor: WatcherDelegate { /// - Error public func stop() { watcher.stop() + fileChangeStream.continuation.finish() } // MARK: - WatcherDelegate @@ -76,6 +81,7 @@ public struct FileMonitor: WatcherDelegate { /// - Parameter event: A file change event public func fileDidChanged(event: FileChangeEvent) { delegate?.fileDidChanged(event: event) + fileChangeStream.continuation.yield(event) } } diff --git a/Sources/FileMonitorAsyncStreamExample/FileMonitorAsyncStreamExample.swift b/Sources/FileMonitorAsyncStreamExample/FileMonitorAsyncStreamExample.swift new file mode 100644 index 0000000..304dd7b --- /dev/null +++ b/Sources/FileMonitorAsyncStreamExample/FileMonitorAsyncStreamExample.swift @@ -0,0 +1,46 @@ +// +// aus der Technik, on 17.05.23. +// https://www.ausdertechnik.de +// + +import Foundation +import FileMonitor + +/// This example shows how to use `FileMonitor`’s AsyncStream with Swift Structured Concurrency +@main +public struct FileMonitorAsyncStreamExample { + + /// Main entrypoint + /// Start FileMonitorExample with an argument to the monitored directory + /// - Throws: an error when the FileMonitor can't be initialized + public static func main() async throws { + let arguments = CommandLine.arguments + if arguments.count < 2 { + print("One folder should be provided at least.") + print("Run \(arguments.first ?? "program") ") + exit(1) + } + guard let folderToWatch = URL(string: arguments[1]) else { + print("Folder '\(arguments[1])' is not an valid location.") + exit(1) + } + + let fileMonitor = FileMonitorAsyncStreamExample() + try await fileMonitor.run(on: folderToWatch) + } + + /// Run a file monitor on a given folder + /// + /// - Parameter folder: A URL of a directory + /// - Throws: an error when the FileMonitor can't be initialized + func run(on folder: URL) async throws { + print("Monitoring files in \(folder.standardized.path)") + + let monitor = try FileMonitor(directory: folder.standardized) + try monitor.start() + // MARK: - AsyncStream + for await event in monitor.stream { + print("Stream: \(event.description)") + } + } +} diff --git a/Sources/FileMonitorExample/FileMonitorExample.swift b/Sources/FileMonitorDelegateExample/FileMonitorDelegateExample.swift similarity index 73% rename from Sources/FileMonitorExample/FileMonitorExample.swift rename to Sources/FileMonitorDelegateExample/FileMonitorDelegateExample.swift index 63de316..6fd76d9 100644 --- a/Sources/FileMonitorExample/FileMonitorExample.swift +++ b/Sources/FileMonitorDelegateExample/FileMonitorDelegateExample.swift @@ -6,13 +6,14 @@ import Foundation import FileMonitor +/// This example shows how to use `FileMonitor` as a Delegate-callback system (without Structured Concurrency) @main -public struct FileMonitorExample: FileDidChangeDelegate { +public struct FileMonitorDelegateExample: FileDidChangeDelegate { /// Main entrypoint /// Start FileMonitorExample with an argument to the monitored directory /// - Throws: an error when the FileMonitor can't be initialized - public static func main() throws { + public static func main() async throws { let arguments = CommandLine.arguments if arguments.count < 2 { print("One folder should be provided at least.") @@ -24,21 +25,19 @@ public struct FileMonitorExample: FileDidChangeDelegate { exit(1) } - let fileMonitor = FileMonitorExample() - try fileMonitor.run(on: folderToWatch); + let fileMonitor = FileMonitorDelegateExample() + try await fileMonitor.run(on: folderToWatch) } /// Run a file monitor on a given folder /// /// - Parameter folder: A URL of a directory /// - Throws: an error when the FileMonitor can't be initialized - func run(on folder: URL) throws { + func run(on folder: URL) async throws { print("Monitoring files in \(folder.standardized.path)") let monitor = try FileMonitor(directory: folder.standardized, delegate: self ) - try monitor.start(); - - RunLoop.main.run() + try monitor.start() } // MARK: - Delegate FileDidChanged @@ -47,6 +46,6 @@ public struct FileMonitorExample: FileDidChangeDelegate { /// /// - Parameter event: A FileChange event public func fileDidChanged(event: FileChange) { - print("\(event.description)") + print("Callback: \(event.description)") } } diff --git a/Tests/FileMonitorTests/FileMonitorTests.swift b/Tests/FileMonitorTests/FileMonitorTests.swift index c3ae312..6957cb5 100644 --- a/Tests/FileMonitorTests/FileMonitorTests.swift +++ b/Tests/FileMonitorTests/FileMonitorTests.swift @@ -64,7 +64,7 @@ final class FileMonitorTests: XCTestCase { XCTAssertGreaterThan(Watcher.fileChanges, 0) } - func testLifecycleChange() throws { + func testLifecycleChange() async throws { let expectation = expectation(description: "Wait for file creation") expectation.assertForOverFulfill = false @@ -76,11 +76,36 @@ final class FileMonitorTests: XCTestCase { let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir), delegate: watcher) try monitor.start() Watcher.fileChanges = 0 + + try "Next New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8) + await fulfillment(of: [expectation], timeout: 10) + monitor.stop() + XCTAssertGreaterThan(Watcher.fileChanges, 0) + } - try "New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8) - wait(for: [expectation], timeout: 10) + func testLifecycleChangeAsync() async throws { + let asyncExpectation = XCTestExpectation(description: "Async wait for file creation") - XCTAssertGreaterThan(Watcher.fileChanges, 0) + let testFile = tmp.appendingPathComponent(dir).appendingPathComponent("\(String.random(length: 8)).\(String.random(length: 3))"); + FileManager.default.createFile(atPath: testFile.path, contents: "hello".data(using: .utf8)) + + let monitor = try FileMonitor(directory: tmp.appendingPathComponent(dir)) + try monitor.start() + Watcher.fileChanges = 0 + + var events = [FileChange]() + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + try? "New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8) + } + + for await event in monitor.stream { + events.append(event) + asyncExpectation.fulfill() + monitor.stop() + } + + await fulfillment(of: [asyncExpectation], timeout: 10) + XCTAssertGreaterThan(events.count, 0) } func testLifecycleDelete() throws {