fs_windows.ex (5072B)
1 require Logger 2 3 defmodule FileSystem.Backends.FSWindows do 4 @moduledoc """ 5 This file is a fork from https://github.com/synrc/fs. 6 FileSysetm backend for windows, a GenServer receive data from Port, parse event 7 and send it to the worker process. 8 Need binary executable file packaged in to use this backend. 9 10 ## Backend Options 11 12 * `:recursive` (bool, default: true), monitor directories and their contents recursively 13 14 ## Executable File Path 15 16 The default executable file is `inotifywait.exe` in `priv` dir of `:file_system` application, there're two ways to custom it, useful when run `:file_system` with escript. 17 18 * config with `config.exs` 19 `config :file_system, :fs_windows, executable_file: "YOUR_EXECUTABLE_FILE_PATH"` 20 21 * config with `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` os environment 22 FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH 23 """ 24 25 use GenServer 26 @behaviour FileSystem.Backend 27 @sep_char <<1>> 28 29 @default_exec_file "inotifywait.exe" 30 31 def bootstrap do 32 exec_file = executable_path() 33 if not is_nil(exec_file) and File.exists?(exec_file) do 34 :ok 35 else 36 Logger.error "Can't find executable `inotifywait.exe`" 37 {:error, :fs_windows_bootstrap_error} 38 end 39 end 40 41 def supported_systems do 42 [{:win32, :nt}] 43 end 44 45 def known_events do 46 [:created, :modified, :removed, :renamed, :undefined] 47 end 48 49 defp executable_path do 50 executable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv) 51 end 52 53 defp executable_path(:config) do 54 Application.get_env(:file_system, :fs_windows)[:executable_file] 55 end 56 57 defp executable_path(:system_env) do 58 System.get_env("FILESYSTEM_FSMWINDOWS_EXECUTABLE_FILE") 59 end 60 61 defp executable_path(:system_path) do 62 System.find_executable(@default_exec_file) 63 end 64 65 defp executable_path(:priv) do 66 case :code.priv_dir(:file_system) do 67 {:error, _} -> 68 Logger.error "`priv` dir for `:file_system` application is not avalible in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` env." 69 nil 70 dir when is_list(dir) -> 71 Path.join(dir, @default_exec_file) 72 end 73 end 74 75 def parse_options(options) do 76 case Keyword.pop(options, :dirs) do 77 {nil, _} -> 78 Logger.error "required argument `dirs` is missing" 79 {:error, :missing_dirs_argument} 80 {dirs, rest} -> 81 format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlist 82 args = [ 83 '--format', format, '--quiet', '-m', '-r' 84 | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1) 85 ] 86 parse_options(rest, args) 87 end 88 end 89 90 defp parse_options([], result), do: {:ok, result} 91 defp parse_options([{:recursive, true} | t], result) do 92 parse_options(t, result) 93 end 94 defp parse_options([{:recursive, false} | t], result) do 95 parse_options(t, result -- ['-r']) 96 end 97 defp parse_options([{:recursive, value} | t], result) do 98 Logger.error "unknown value `#{inspect value}` for recursive, ignore" 99 parse_options(t, result) 100 end 101 defp parse_options([h | t], result) do 102 Logger.error "unknown option `#{inspect h}`, ignore" 103 parse_options(t, result) 104 end 105 106 def start_link(args) do 107 GenServer.start_link(__MODULE__, args, []) 108 end 109 110 def init(args) do 111 {worker_pid, rest} = Keyword.pop(args, :worker_pid) 112 case parse_options(rest) do 113 {:ok, port_args} -> 114 port = Port.open( 115 {:spawn_executable, to_charlist(executable_path())}, 116 [:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}] 117 ) 118 Process.link(port) 119 Process.flag(:trap_exit, true) 120 {:ok, %{port: port, worker_pid: worker_pid}} 121 {:error, _} -> 122 :ignore 123 end 124 end 125 126 def handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do 127 {file_path, events} = line |> parse_line 128 send(state.worker_pid, {:backend_file_event, self(), {file_path, events}}) 129 {:noreply, state} 130 end 131 132 def handle_info({port, {:exit_status, _}}, %{port: port}=state) do 133 send(state.worker_pid, {:backend_file_event, self(), :stop}) 134 {:stop, :normal, state} 135 end 136 137 def handle_info({:EXIT, port, _reason}, %{port: port}=state) do 138 send(state.worker_pid, {:backend_file_event, self(), :stop}) 139 {:stop, :normal, state} 140 end 141 142 def handle_info(_, state) do 143 {:noreply, state} 144 end 145 146 def parse_line(line) do 147 {path, flags} = 148 case line |> to_string |> String.split(@sep_char, trim: true) do 149 [dir, flags, file] -> {Enum.join([dir, file], "\\"), flags} 150 [path, flags] -> {path, flags} 151 end 152 {path |> Path.split() |> Path.join(), flags |> String.split(",") |> Enum.map(&convert_flag/1)} 153 end 154 155 defp convert_flag("CREATE"), do: :created 156 defp convert_flag("MODIFY"), do: :modified 157 defp convert_flag("DELETE"), do: :removed 158 defp convert_flag("MOVED_TO"), do: :renamed 159 defp convert_flag(_), do: :undefined 160 end