local_file_agent.rb 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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 %s" % if boolify(interpolated['watch'])
  30. Utils.pretty_print(
  31. "file_pointer" => {
  32. "file" => "/tmp/test/filename",
  33. "agent_id" => id
  34. },
  35. "event_type" => "modified/added/removed"
  36. )
  37. else
  38. Utils.pretty_print(
  39. "file_pointer" => {
  40. "file" => "/tmp/test/filename",
  41. "agent_id" => id
  42. }
  43. )
  44. end
  45. end
  46. def default_options
  47. {
  48. 'mode' => 'read',
  49. 'watch' => 'true',
  50. 'append' => 'false',
  51. 'path' => "",
  52. 'data' => '{{ data }}'
  53. }
  54. end
  55. form_configurable :mode, type: :array, values: %w(read write)
  56. form_configurable :watch, type: :array, values: %w(true false)
  57. form_configurable :path, type: :string
  58. form_configurable :append, type: :boolean
  59. form_configurable :data, type: :string
  60. def validate_options
  61. if options['mode'].blank? || !['read', 'write'].include?(options['mode'])
  62. errors.add(:base, "The 'mode' option is required and must be set to 'read' or 'write'")
  63. end
  64. if options['watch'].blank? || ![true, false].include?(boolify(options['watch']))
  65. errors.add(:base, "The 'watch' option is required and must be set to 'true' or 'false'")
  66. end
  67. if options['append'].blank? || ![true, false].include?(boolify(options['append']))
  68. errors.add(:base, "The 'append' option is required and must be set to 'true' or 'false'")
  69. end
  70. if options['path'].blank?
  71. errors.add(:base, "The 'path' option is required.")
  72. end
  73. end
  74. def working?
  75. should_run?(false) && ((interpolated['mode'] == 'read' && check_path_existance && checked_without_error?) ||
  76. (interpolated['mode'] == 'write' && received_event_without_error?))
  77. end
  78. def check
  79. return if interpolated['mode'] != 'read' || boolify(interpolated['watch']) || !should_run?
  80. return unless check_path_existance(true)
  81. if File.directory?(expanded_path)
  82. Dir.glob(File.join(expanded_path, '*')).select { |f| File.file?(f) }
  83. else
  84. [expanded_path]
  85. end.each do |file|
  86. create_event payload: get_file_pointer(file)
  87. end
  88. end
  89. def receive(incoming_events)
  90. return if interpolated['mode'] != 'write' || !should_run?
  91. incoming_events.each do |event|
  92. mo = interpolated(event)
  93. File.open(File.expand_path(mo['path']), boolify(mo['append']) ? 'a' : 'w') do |file|
  94. file.write(mo['data'])
  95. end
  96. end
  97. end
  98. def start_worker?
  99. interpolated['mode'] == 'read' && boolify(interpolated['watch']) && should_run? && check_path_existance
  100. end
  101. def check_path_existance(log = true)
  102. if !File.exist?(expanded_path)
  103. error("File or directory '#{expanded_path}' does not exist") if log
  104. return false
  105. end
  106. true
  107. end
  108. def get_io(file)
  109. File.open(file, 'r')
  110. end
  111. def expanded_path
  112. @expanded_path ||= File.expand_path(interpolated['path'])
  113. end
  114. private
  115. def should_run?(log = true)
  116. if self.class.should_run?
  117. true
  118. else
  119. error("Unable to run because insecure agents are not enabled. Set ENABLE_INSECURE_AGENTS to true in the Huginn .env configuration.") if log
  120. false
  121. end
  122. end
  123. class Worker < LongRunnable::Worker
  124. def setup
  125. require 'listen'
  126. @listener = Listen.to(*listen_options, &method(:callback))
  127. end
  128. def run
  129. sleep unless agent.check_path_existance(true)
  130. @listener.start
  131. sleep
  132. end
  133. def stop
  134. @listener.stop
  135. end
  136. private
  137. def callback(*changes)
  138. AgentRunner.with_connection do
  139. changes.zip([:modified, :added, :removed]).each do |files, event_type|
  140. files.each do |file|
  141. agent.create_event payload: agent.get_file_pointer(file).merge(event_type: event_type)
  142. end
  143. end
  144. agent.touch(:last_check_at)
  145. end
  146. end
  147. def listen_options
  148. if File.directory?(agent.expanded_path)
  149. [agent.expanded_path, ignore!: [] ]
  150. else
  151. [File.dirname(agent.expanded_path), { ignore!: [], only: /\A#{Regexp.escape(File.basename(agent.expanded_path))}\z/ } ]
  152. end
  153. end
  154. end
  155. end
  156. end