Skip to content

Commit

Permalink
Adds support for AsyncStream
Browse files Browse the repository at this point in the history
  • Loading branch information
mcritzamazon committed May 28, 2024
1 parent 65fc98d commit 3210af0
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 16 deletions.
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
3 changes: 3 additions & 0 deletions Sources/FileMonitor/FileMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum FileMonitorErrors: Error {

/// FileMonitor: Watch for file changes in a directory with a unified API on Linux and macOS.
public struct FileMonitor: WatcherDelegate {
public let (stream, continuation) = AsyncStream.makeStream(of: FileChange.self)

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

// MARK: - WatcherDelegate
Expand All @@ -76,6 +78,7 @@ public struct FileMonitor: WatcherDelegate {
/// - Parameter event: A file change event
public func fileDidChanged(event: FileChangeEvent) {
delegate?.fileDidChanged(event: event)
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)")
}
}
14 changes: 12 additions & 2 deletions Tests/FileMonitorTests/FileMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ 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
let asyncExpectation = XCTestExpectation(description: "Async wait for file creation")
expectation.assertForOverFulfill = true

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))
Expand All @@ -77,10 +79,18 @@ final class FileMonitorTests: XCTestCase {
try monitor.start()
Watcher.fileChanges = 0

var events = [FileChange]()
for await event in monitor.stream {
events.append(event)
asyncExpectation.fulfill()
monitor.stop()
}

try "New Content".write(toFile: testFile.path, atomically: true, encoding: .utf8)
wait(for: [expectation], timeout: 10)
await fulfillment(of: [expectation, asyncExpectation], timeout: 10)

XCTAssertGreaterThan(Watcher.fileChanges, 0)
XCTAssertGreaterThan(events.count, 0)
}

func testLifecycleDelete() throws {
Expand Down

0 comments on commit 3210af0

Please sign in to comment.