Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for AsyncStream #4

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading