local_file_agent.rb 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. module Agents
  2. class LocalFileAgent < Agent
  3. include LongRunnable
  4. include FormConfigurable
  5. include FileHandling
  6. emits_file_pointer!
  7. default_schedule 'every_1h'
  8. def self.should_run?
  9. ENV['ENABLE_INSECURE_AGENTS'] == "true"
  10. end
  11. description do
  12. <<~MD
  13. The LocalFileAgent can watch a file/directory for changes or emit an event for every file in that directory. When receiving an event it writes the received data into a file.
  14. `mode` determines if the agent is emitting events for (changed) files or writing received event data to disk.
  15. ### Reading
  16. When `watch` is set to `true` the LocalFileAgent will watch the specified `path` for changes, the schedule is ignored and the file system is watched continuously. An event will be emitted for every detected change.
  17. When `watch` is set to `false` the agent will emit an event for every file in the directory on each scheduled run.
  18. #{emitting_file_handling_agent_description}
  19. ### Writing
  20. Every event will be writting into a file at `path`, Liquid interpolation is possible to change the path per event.
  21. When `append` is true the received data will be appended to the file.
  22. Use [Liquid](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) templating in `data` to specify which part of the received event should be written.
  23. *Warning*: This type of Agent can read and write any file the user that runs the Huginn server has access to, and is #{Agents::LocalFileAgent.should_run? ? "**currently enabled**" : "**currently disabled**"}.
  24. Only enable this Agent if you trust everyone using your Huginn installation.
  25. You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
  26. MD
  27. end
  28. event_description do
  29. "Events will looks like this:\n\n " +
  30. if boolify(interpolated['watch'])
  31. Utils.pretty_print(
  32. "file_pointer" => {
  33. "file" => "/tmp/test/filename",
  34. "agent_id" => id
  35. },
  36. "event_type" => "modified/added/removed"
  37. )
  38. else
  39. Utils.pretty_print(
  40. "file_pointer" => {
  41. "file" => "/tmp/test/filename",
  42. "agent_id" => id
  43. }
  44. )
  45. end
  46. end
  47. def default_options
  48. {
  49. 'mode' => 'read',
  50. 'watch' => 'true',
  51. 'append' => 'false',
  52. 'path' => "",
  53. 'data' => '{{ data }}'
  54. }
  55. end
  56. form_configurable :mode, type: :array, values: %w[read write]
  57. form_configurable :watch, type: :array, values: %w[true false]
  58. form_configurable :path, type: :string
  59. form_configurable :append, type: :boolean
  60. form_configurable :data, type: :string
  61. def validate_options
  62. if options['mode'].blank? || !['read', 'write'].include?(options['mode'])
  63. errors.add(:base, "The 'mode' option is required and must be set to 'read' or 'write'")
  64. end
  65. if options['watch'].blank? || ![true, false].include?(boolify(options['watch']))
  66. errors.add(:base, "The 'watch' option is required and must be set to 'true' or 'false'")
  67. end
  68. if options['append'].blank? || ![true, false].include?(boolify(options['append']))
  69. errors.add(:base, "The 'append' option is required and must be set to 'true' or 'false'")
  70. end
  71. if options['path'].blank?
  72. errors.add(:base, "The 'path' option is required.")
  73. end
  74. end
  75. def working?
  76. should_run?(false) && ((interpolated['mode'] == 'read' && check_path_existance && checked_without_error?) ||
  77. (interpolated['mode'] == 'write' && received_event_without_error?))
  78. end
  79. def check
  80. return if interpolated['mode'] != 'read' || boolify(interpolated['watch']) || !should_run?
  81. return unless check_path_existance(true)
  82. if File.directory?(expanded_path)
  83. Dir.glob(File.join(expanded_path, '*')).select { |f| File.file?(f) }
  84. else
  85. [expanded_path]
  86. end.each do |file|
  87. create_event payload: get_file_pointer(file)
  88. end
  89. end
  90. def receive(incoming_events)
  91. return if interpolated['mode'] != 'write' || !should_run?
  92. incoming_events.each do |event|
  93. mo = interpolated(event)
  94. expanded_path = File.expand_path(mo['path'])
  95. File.open(expanded_path, boolify(mo['append']) ? 'a' : 'w') do |file|
  96. file.write(mo['data'])
  97. end
  98. create_event payload: get_file_pointer(expanded_path)
  99. end
  100. end
  101. def start_worker?
  102. interpolated['mode'] == 'read' && boolify(interpolated['watch']) && should_run? && check_path_existance
  103. end
  104. def check_path_existance(log = true)
  105. if !File.exist?(expanded_path)
  106. error("File or directory '#{expanded_path}' does not exist") if log
  107. return false
  108. end
  109. true
  110. end
  111. def get_io(file)
  112. File.open(file, 'r')
  113. end
  114. def expanded_path
  115. @expanded_path ||= File.expand_path(interpolated['path'])
  116. end
  117. private
  118. def should_run?(log = true)
  119. if self.class.should_run?
  120. true
  121. else
  122. error("Unable to run because insecure agents are not enabled. Set ENABLE_INSECURE_AGENTS to true in the Huginn .env configuration.") if log
  123. false
  124. end
  125. end
  126. class Worker < LongRunnable::Worker
  127. def setup
  128. require 'listen'
  129. path, options = listen_options
  130. @listener = Listen.to(path, **options, &method(:callback))
  131. end
  132. def run
  133. sleep unless agent.check_path_existance(true)
  134. @listener.start
  135. sleep
  136. end
  137. def stop
  138. @listener.stop
  139. end
  140. private
  141. def callback(*changes)
  142. AgentRunner.with_connection do
  143. changes.zip([:modified, :added, :removed]).each do |files, event_type|
  144. files.each do |file|
  145. agent.create_event payload: agent.get_file_pointer(file).merge(event_type:)
  146. end
  147. end
  148. agent.touch(:last_check_at)
  149. end
  150. end
  151. def listen_options
  152. if File.directory?(agent.expanded_path)
  153. [
  154. agent.expanded_path,
  155. ignore!: []
  156. ]
  157. else
  158. [
  159. File.dirname(agent.expanded_path),
  160. ignore!: [], only: /\A#{Regexp.escape(File.basename(agent.expanded_path))}\z/
  161. ]
  162. end
  163. end
  164. end
  165. end
  166. end