Initial Commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import Foundation
|
||||
import CoreServices
|
||||
|
||||
final class FileWatchService {
|
||||
typealias ChangeHandler = @Sendable (UUID, String) -> Void
|
||||
|
||||
private struct WatchTarget: Equatable {
|
||||
let targetPath: String
|
||||
let streamRoot: String
|
||||
let isDirectory: Bool
|
||||
}
|
||||
|
||||
private var stream: FSEventStreamRef?
|
||||
private var watchedTargets: [UUID: WatchTarget] = [:]
|
||||
private let queue = DispatchQueue(label: "net.absdev.RsyncManager.fsevents", qos: .utility)
|
||||
var onChange: ChangeHandler?
|
||||
|
||||
deinit { stop() }
|
||||
|
||||
func update(jobs: [SyncJob]) {
|
||||
var targets: [UUID: WatchTarget] = [:]
|
||||
for job in jobs where job.isEnabled && job.watchEnabled {
|
||||
let path = (job.effectiveWatchPath as NSString).expandingTildeInPath
|
||||
guard !path.isEmpty else { continue }
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
|
||||
let directory = exists ? isDirectory.boolValue : true
|
||||
let streamRoot = directory ? path : (path as NSString).deletingLastPathComponent
|
||||
targets[job.id] = WatchTarget(targetPath: path, streamRoot: streamRoot, isDirectory: directory)
|
||||
}
|
||||
guard targets != watchedTargets else { return }
|
||||
watchedTargets = targets
|
||||
rebuild()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
if let stream {
|
||||
FSEventStreamStop(stream)
|
||||
FSEventStreamInvalidate(stream)
|
||||
FSEventStreamRelease(stream)
|
||||
self.stream = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func rebuild() {
|
||||
stop()
|
||||
let paths = Array(Set(watchedTargets.values.map(\.streamRoot)))
|
||||
guard !paths.isEmpty else { return }
|
||||
|
||||
var context = FSEventStreamContext(
|
||||
version: 0,
|
||||
info: Unmanaged.passUnretained(self).toOpaque(),
|
||||
retain: nil,
|
||||
release: nil,
|
||||
copyDescription: nil
|
||||
)
|
||||
|
||||
let callback: FSEventStreamCallback = { _, info, count, eventPaths, _, _ in
|
||||
guard let info else { return }
|
||||
let service = Unmanaged<FileWatchService>.fromOpaque(info).takeUnretainedValue()
|
||||
let paths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] ?? []
|
||||
for index in 0..<min(count, paths.count) {
|
||||
service.handle(path: paths[index])
|
||||
}
|
||||
}
|
||||
|
||||
let flags = FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes)
|
||||
guard let stream = FSEventStreamCreate(
|
||||
nil,
|
||||
callback,
|
||||
&context,
|
||||
paths as CFArray,
|
||||
FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
|
||||
0.4,
|
||||
flags
|
||||
) else { return }
|
||||
|
||||
self.stream = stream
|
||||
FSEventStreamSetDispatchQueue(stream, queue)
|
||||
FSEventStreamStart(stream)
|
||||
}
|
||||
|
||||
private func handle(path: String) {
|
||||
let normalized = (path as NSString).standardizingPath
|
||||
for (jobID, target) in watchedTargets {
|
||||
let targetPath = (target.targetPath as NSString).standardizingPath
|
||||
let matches = target.isDirectory
|
||||
? (normalized == targetPath || normalized.hasPrefix(targetPath + "/"))
|
||||
: normalized == targetPath
|
||||
if matches { onChange?(jobID, normalized) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user