require Logger
defmodule FileSystem.Backends.FSPoll do
@moduledoc """
File system backend for any OS.
## Backend Options
* `:interval` (integer, default: 1000), polling interval
## Using FSPoll Backend
Unlike other backends, polling backend is never automatically chosen in any
OS environment, despite being usable on all platforms.
To use polling backend, one has to explicitly specify in the backend option.
"""
use GenServer
@behaviour FileSystem.Backend
def bootstrap, do: :ok
def supported_systems do
[{:unix, :linux}, {:unix, :freebsd}, {:unix, :openbsd}, {:unix, :darwin}, {:win32, :nt}]
end
def known_events do
[:created, :deleted, :modified]
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end
def init(args) do
worker_pid = Keyword.fetch!(args, :worker_pid)
dirs = Keyword.fetch!(args, :dirs)
interval = Keyword.get(args, :interval, 1000)
Logger.info("Polling file changes every #{interval}ms...")
send(self(), :first_check)
{:ok, {worker_pid, dirs, interval, %{}}}
end
def handle_info(:first_check, {worker_pid, dirs, interval, _empty_map}) do
schedule_check(interval)
{:noreply, {worker_pid, dirs, interval, files_mtimes(dirs)}}
end
def handle_info(:check, {worker_pid, dirs, interval, stale_mtimes}) do
fresh_mtimes = files_mtimes(dirs)
diff(stale_mtimes, fresh_mtimes)
|> Tuple.to_list()
|> Enum.zip([:created, :deleted, :modified])
|> Enum.each(&report_change(&1, worker_pid))
schedule_check(interval)
{:noreply, {worker_pid, dirs, interval, fresh_mtimes}}
end
defp schedule_check(interval) do
Process.send_after(self(), :check, interval)
end
defp files_mtimes(dirs, files_mtimes_map \\ %{}) do
Enum.reduce(dirs, files_mtimes_map, fn dir, map ->
case File.stat!(dir) do
%{type: :regular, mtime: mtime} ->
Map.put(map, dir, mtime)
%{type: :directory} ->
dir
|> Path.join("*")
|> Path.wildcard()
|> files_mtimes(map)
%{type: _other} ->
map
end
end)
end
@doc false
def diff(stale_mtimes, fresh_mtimes) do
fresh_file_paths = fresh_mtimes |> Map.keys() |> MapSet.new()
stale_file_paths = stale_mtimes |> Map.keys() |> MapSet.new()
created_file_paths =
MapSet.difference(fresh_file_paths, stale_file_paths) |> MapSet.to_list()
deleted_file_paths =
MapSet.difference(stale_file_paths, fresh_file_paths) |> MapSet.to_list()
modified_file_paths =
for file_path <- MapSet.intersection(stale_file_paths, fresh_file_paths),
stale_mtimes[file_path] != fresh_mtimes[file_path],
do: file_path
{created_file_paths, deleted_file_paths, modified_file_paths}
end
defp report_change({file_paths, event}, worker_pid) do
for file_path <- file_paths do
send(worker_pid, {:backend_file_event, self(), {file_path, [event]}})
end
end
end