Skip to content
Draft
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/Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
# continue-on-error: true
strategy:
matrix:
julia-version: ["1.6"]
julia-version: ["1.6", "1.7"]
os: [ubuntu-latest, macOS-latest, windows-latest]

steps:
Expand Down
6 changes: 1 addition & 5 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ authors = ["Fons van der Plas <fons@plutojl.org>"]
version = "0.1.5"

[deps]
Deno_jll = "04572ae6-984a-583e-9378-9577a1c2574d"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
libwatcher_jll = "f2e3f4ed-6d30-53f6-bb33-6a6ab454fa9c"

[compat]
Deno_jll = "^1.10"
JSON = "^0.20, ^0.21"
julia = "1"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ watch_folder(f::Function, dir=".")

Watch a folder recursively for any changes. Includes changes to file contents. A [`FileEvent`](@ref) is passed to the callback function `f`.

## Examples

```julia
watch_file(f::Function, filename=".")
```

Watch a file for changes. A [`FileEvent`](@ref) is passed to the callback function `f`.

# Example

```julia
watch_folder(".") do event
@info "Something changed!" event
Expand All @@ -33,16 +33,29 @@ sleep(5)
schedule(watch_task, InterruptException(); error=true)
```

# Differences with the FileWatching stdlib
## Snapshots

The library also allow you take snapshots of a directory and read those snapshots later to see exactly which files have been updated/deleted/created.

```julia
options = Options(ignores = Set{String}(["./.git"]))
write_snapshot(dir, snapshot_path; options = options)

# Create some files, do some changes;, delete some files...

events = get_events_since(dir, snapshot_path; options = options)
```

## Differences with the FileWatching stdlib

`BetterFileWatching.watch_file` is an alternative to `FileWatching.watch_file`. The differences are:
- We offer an additional callback API (`watch_file(::Function, ::String)`, like the examples above), which means that *handling* events does not block *receiving new events*: we keep listening to changes asynchronously while your callback runs.
- BetterFileWatching.jl is just a small wrapper around [`Deno.watchFs`](https://doc.deno.land/builtin/stable#Deno.watchFs), made available through the [Deno_jll](https://github.com/JuliaBinaryWrappers/Deno_jll.jl) package. `Deno.watchFs` is well-tested and widely used.
- BetterFileWatching.jl is just a small wrapper around [parcel-bundler/watcher](https://github.com/parcel-bundler/watcher), made available through the [libwatcher_jll](https://github.com/JuliaBinaryWrappers/libwatcher_jll.jl) package. `watcher` is well-tested and widely used.

`BetterFileWatching.watch_folder` is an alternative to `FileWatching.watch_folder`. The differences are, in addition to those mentioned above for `watch_file`:
- `BetterFileWatching.watch_folder` works _recursively_, i.e. subfolders are also watched.
- `BetterFileWatching.watch_folder` also watches for changes to the _contents_ of files contained in the folder.

---

In fact, `BetterFileWatching.watch_file` and `BetterFileWatching.watch_folder` are actually just the same function! It handles both files and folders.
In fact, `BetterFileWatching.watch_file` and `BetterFileWatching.watch_folder` are actually just the same function! It handles both files and folders.
156 changes: 87 additions & 69 deletions src/BetterFileWatching.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
module BetterFileWatching

using Deno_jll

import JSON

include("./libwatcher.jl")

abstract type FileEvent end

Expand All @@ -18,16 +15,28 @@ struct Created <: FileEvent
end
struct Accessed <: FileEvent
paths::Vector{String}

function Accessed(p)
@warn "Accessed is deprecated and will be removed in the following versions."
end
end

const mapFileEvent = Dict(
"modify" => Modified,
"create" => Created,
"remove" => Removed,
"access" => Accessed,
)
function convert_to_deno_events(events::Vector{Event})
reduce(events; init=(; modified=Modified(String[]), removed=Removed(String[]), created=Created(String[]))) do acc, event
if event.is_created
push!(acc.created.paths, event.path)
elseif event.is_deleted
push!(acc.removed.paths, event.path)
else
push!(acc.modified.paths, event.path)
end

acc
end
end

export watch_folder, watch_file
export Options, write_snapshot, get_events_since

function _doc_examples(folder)
f = folder ? "folder" : "file"
Expand Down Expand Up @@ -71,72 +80,45 @@ $(_doc_examples(true))

- `BetterFileWatching.watch_folder` works _recursively_, i.e. subfolders are also watched.
- `BetterFileWatching.watch_folder` also watching file _contents_ for changes.
- BetterFileWatching.jl is based on [Deno.watchFs](https://doc.deno.land/builtin/stable#Deno.watchFs), made available through the [Deno_jll](https://github.com/JuliaBinaryWrappers/Deno_jll.jl) package.
- BetterFileWatching.jl is based on a port of [parcel-bundler/watcher](https://github.com/parcel-bundler/watcher) to Julia (available on [JuliaPluto/watcher](https://github.com/JuliaPluto/watcher))
"""
function watch_folder(on_event::Function, dir::AbstractString="."; ignore_accessed::Bool=true, ignore_dotgit::Bool=true)
script = """
const watcher = Deno.watchFs($(JSON.json(dir)));
for await (const event of watcher) {
try {
await Deno.stdout.write(new TextEncoder().encode("\\n" + JSON.stringify(event) + "\\n"));
} catch(e) {
Deno.exit();
}
}
"""

outpipe = Pipe()

function on_stdout(str)
for s in split(str, "\n"; keepempty=false)
local event_raw = nothing
event = try
event_raw = JSON.parse(s)
T = mapFileEvent[event_raw["kind"]]
T(String.(event_raw["paths"]))
catch e
@warn "Unrecognized file watching event. Please report this to https://github.com/JuliaPluto/BetterFileWatching.jl" event_raw ex=(e,catch_backtrace())
end
if !(ignore_accessed && event isa Accessed)
if !(ignore_dotgit && event isa FileEvent && all(".git" ∈ splitpath(relpath(path, dir)) for path in event.paths))
on_event(event)
end
end
end
function watch_folder(on_event::Function, dir::AbstractString="."; ignore_accessed::Union{Bool,Nothing}=nothing, ignore_dotgit::Bool=true)
# blocking version with a callback
if ignore_accessed === true
@warn "ignore_accessed is deprecated and will be removed in the coming versions."
end

deno_task = @async run(pipeline(`$(deno()) eval $(script)`; stdout=outpipe))
watch_task = @async try
sleep(.1)
while true
on_stdout(String(readavailable(outpipe)))
end
catch e
if !istaskdone(deno_task)
schedule(deno_task, e; error=true)
end
if !(e isa InterruptException)
showerror(stderr, e, catch_backtrace())
end
watch_folder_sync(dir; options = Options(ignores=Set{String}(ignore_dotgit ? [] : [".git/"]))) do events
events = convert_to_deno_events(events)
length(events.modified.paths) > 0 && on_event(events.modified)
length(events.created.paths) > 0 && on_event(events.created)
length(events.removed.paths) > 0 && on_event(events.removed)
end

try wait(watch_task) catch; end
end


function watch_folder(dir::AbstractString="."; kwargs...)::Union{Nothing,FileEvent}
event = Ref{Union{Nothing,FileEvent}}(nothing)
task = Ref{Task}()
task[] = @async watch_folder(dir; kwargs...) do e
event[] = e
try
schedule(task[], InterruptException(); error=true)
catch; end
# legacy API -----
# blocking without callback
chan = Channel{FileEvent}(1)

watcher = subscribe(dir) do events
events = convert_to_deno_events(events)

if length(events.modified.paths) > 0
put!(chan, events.modified)
elseif length(events.created.paths) > 0
put!(chan, events.created)
elseif length(events.removed.paths) > 0
put!(chan, events.removed)
end
end
wait(task[])
event[]
end

event = take!(chan)
unsubscribe(watcher)

event
end

"""
```julia
Expand All @@ -151,9 +133,45 @@ $(_doc_examples(false))

# Differences with the FileWatching stdlib

- BetterFileWatching.jl is based on [Deno.watchFs](https://doc.deno.land/builtin/stable#Deno.watchFs), made available through the [Deno_jll](https://github.com/JuliaBinaryWrappers/Deno_jll.jl) package.
- BetterFileWatching.jl is based on a port of [parcel-bundler/watcher](https://github.com/parcel-bundler/watcher) to Julia (available on [JuliaPluto/watcher](https://github.com/JuliaPluto/watcher))
"""
watch_file(filename::AbstractString; kwargs...) = watch_folder(filename; kwargs...)
watch_file(f::Function, filename::AbstractString; kwargs...) = watch_folder(f, filename; kwargs...)
function watch_file(filename::AbstractString; kwargs...)
file_path = abspath(filename)
dir_path = dirname(file_path)

chan = Channel{FileEvent}(1)

watcher = subscribe(dir_path) do events
file_event = findfirst(event -> abspath(event.path) == file_path, events)
file_event === nothing && return
events = convert_to_deno_events([events[file_event]])

if length(events.modified.paths) > 0
put!(chan, events.modified)
elseif length(events.created.paths) > 0
put!(chan, events.created)
elseif length(events.removed.paths) > 0
put!(chan, events.removed)
end
end

event = take!(chan)
unsubscribe(watcher)

event
end

function watch_file(f::Function, filename::AbstractString; kwargs...)
file_path = abspath(filename)
dir_path = dirname(file_path)
watch_folder(dir_path; kwargs...) do event
filter!(event.paths) do path
normpath(path) == file_path
end
if length(event.paths) > 0
f(event)
end
end
end

end
Loading