require Logger defmodule SecretsWatcherFileSystem.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 SecretsWatcherFileSystem.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