Skip to content

Commit

Permalink
Adds support for AsyncStream (#4)
Browse files Browse the repository at this point in the history
* Adds support for AsyncStream

* Swift 5.9 is a minimum requirement.

* Fixes Swift 6 error for setting tupel

* Split of asynchronous test and delegation test to avoid hang-ups in Linux

---------

Co-authored-by: Michael Critz <[email protected]>
Co-authored-by: Kris Simon <[email protected]>
  • Loading branch information
3 people authored Jun 17, 2024
1 parent 9d28ff7 commit ef22f14
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: [
Expand Down Expand Up @@ -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",
Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Sources/FileMonitor/FileMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileChange> {
fileChangeStream.stream
}

var watcher: WatcherProtocol
public var delegate: FileDidChangeDelegate? {
Expand Down Expand Up @@ -67,6 +71,7 @@ public struct FileMonitor: WatcherDelegate {
/// - Error
public func stop() {
watcher.stop()
fileChangeStream.continuation.finish()
}

// MARK: - WatcherDelegate
Expand All @@ -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)
}

}
Original file line number Diff line number Diff line change
@@ -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") <folder>")
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)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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
Expand All @@ -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)")
}
}
33 changes: 29 additions & 4 deletions Tests/FileMonitorTests/FileMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand Down

0 comments on commit ef22f14

Please sign in to comment.