file.rb 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Sessions::Store::File
  3. def initialize
  4. # get application root directory
  5. @root = Dir.pwd.to_s
  6. if @root.blank? || @root == '/'
  7. @root = Rails.root
  8. end
  9. # get working directories
  10. @path = "#{@root}/tmp/websocket_#{Rails.env}"
  11. @nodes_path = "#{@root}/tmp/session_node_#{Rails.env}"
  12. end
  13. def create(client_id, content)
  14. path = "#{@path}/#{client_id}"
  15. path_tmp = "#{@path}/tmp/#{client_id}"
  16. session_file = "#{path_tmp}/session"
  17. # store session data in session file
  18. FileUtils.mkpath path_tmp
  19. File.binwrite(session_file, content)
  20. # destroy old session if needed
  21. if File.exist?(path)
  22. destroy(client_id)
  23. end
  24. # move to destination directory
  25. FileUtils.mv(path_tmp, path)
  26. end
  27. def sessions
  28. path = "#{@path}/"
  29. FileUtils.mkdir_p path
  30. data = []
  31. Dir.foreach(path) do |entry|
  32. next if entry == '.'
  33. next if entry == '..'
  34. next if entry == 'tmp'
  35. next if entry == 'spool'
  36. data.push entry.to_s
  37. end
  38. data
  39. end
  40. def session_exists?(client_id)
  41. session_dir = "#{@path}/#{client_id}"
  42. return false if !File.exist?(session_dir)
  43. session_file = "#{session_dir}/session"
  44. return false if !File.exist?(session_file)
  45. true
  46. end
  47. def destroy(client_id)
  48. path = "#{@path}/#{client_id}"
  49. FileUtils.rm_rf path
  50. end
  51. def set(client_id, data)
  52. path = "#{@path}/#{client_id}"
  53. write_with_lock("#{path}/session", data.to_json)
  54. end
  55. def get(client_id)
  56. session_dir = "#{@path}/#{client_id}"
  57. session_file = "#{session_dir}/session"
  58. data = nil
  59. return if !check_session_file_for_client(client_id, session_dir, session_file)
  60. begin
  61. data_json = JSON.parse(read_with_lock(session_file))
  62. if data_json
  63. data = Sessions.symbolize_keys(data_json)
  64. data[:user] = data_json['user'] # for compat. reasons
  65. end
  66. rescue => e
  67. Sessions.log('error', e.inspect)
  68. destroy(client_id)
  69. Sessions.log('error', "error in reading/parsing session file '#{session_file}', remove session.")
  70. return
  71. end
  72. data
  73. end
  74. def send_data(client_id, data)
  75. location = new_message_filename_for(client_id)
  76. return false if !location
  77. begin
  78. write_with_lock(location, data.to_json)
  79. rescue => e
  80. Sessions.log('error', e.inspect)
  81. Sessions.log('error', "error in writing message file '#{location}'")
  82. return false
  83. end
  84. true
  85. end
  86. def queue(client_id)
  87. path = "#{@path}/#{client_id}/"
  88. data = []
  89. files = []
  90. Dir.foreach(path) do |entry|
  91. next if entry == '.'
  92. next if entry == '..'
  93. files.push entry
  94. end
  95. files.sort.each do |entry|
  96. next if !entry.start_with?('send')
  97. message = queue_file_read(path, entry)
  98. next if !message
  99. data.push message
  100. end
  101. data
  102. end
  103. def cleanup
  104. return true if !File.exist?(@path)
  105. FileUtils.rm_rf @path
  106. true
  107. end
  108. def clear_spool
  109. path = "#{@path}/spool/"
  110. FileUtils.rm_rf path
  111. end
  112. ### Node-specific methods ###
  113. def clear_nodes
  114. FileUtils.rm_rf @nodes_path
  115. end
  116. def nodes
  117. path = "#{@nodes_path}/*.status"
  118. nodes = []
  119. files = Dir.glob(path)
  120. files.each do |filename|
  121. begin
  122. content = read_with_lock(filename)
  123. data = JSON.parse(content)
  124. nodes.push data
  125. rescue => e
  126. Rails.logger.error "can't parse status file #{filename}, #{e.inspect}"
  127. # to_delete.push "#{path}/#{entry}"
  128. # next
  129. end
  130. end
  131. nodes
  132. end
  133. def add_node(node_id, data)
  134. FileUtils.mkdir_p @nodes_path
  135. status_file = "#{@nodes_path}/#{node_id}.status"
  136. content = data.to_json
  137. # store session data in session file
  138. write_with_lock(status_file, content)
  139. end
  140. def each_node_session()
  141. # read node sessions
  142. path = "#{@nodes_path}/*.session"
  143. files = Dir.glob(path)
  144. files.each do |filename|
  145. begin
  146. content = read_with_lock(filename)
  147. next if content.blank?
  148. data = JSON.parse(content)
  149. next if data.blank?
  150. yield data
  151. rescue => e
  152. Rails.logger.error "can't parse session file #{filename}, #{e.inspect}"
  153. # to_delete.push "#{path}/#{entry}"
  154. # next
  155. end
  156. end
  157. end
  158. def create_node_session(node_id, client_id, data)
  159. FileUtils.mkdir_p @nodes_path
  160. status_file = "#{@nodes_path}/#{node_id}.#{client_id}.session"
  161. content = data.to_json
  162. # store session data in session file
  163. write_with_lock(status_file, content)
  164. end
  165. def each_session_by_node(node_id)
  166. # read node sessions
  167. path = "#{@nodes_path}/#{node_id}.*.session"
  168. files = Dir.glob(path)
  169. files.each do |filename|
  170. begin
  171. content = read_with_lock(filename)
  172. next if content.blank?
  173. data = JSON.parse(content)
  174. next if data.blank?
  175. yield data
  176. rescue => e
  177. Rails.logger.error "can't parse session file #{filename}, #{e.inspect}"
  178. # to_delete.push "#{path}/#{entry}"
  179. # next
  180. end
  181. end
  182. end
  183. private
  184. def write_with_lock(filename, data)
  185. File.open(filename, 'ab') do |file|
  186. file.flock(File::LOCK_EX)
  187. file.truncate 0 # Truncate only after locking to avoid empty state
  188. file.write data
  189. end
  190. rescue Errno::ENOENT => e
  191. Rails.logger.debug { "Can't write data to web socket session file #{filename}, maybe the session was removed in the meantime: #{e.inspect}" }
  192. Rails.logger.debug e
  193. end
  194. def read_with_lock(filename)
  195. File.open(filename, 'rb') do |file|
  196. file.flock(File::LOCK_SH)
  197. return file.read
  198. end
  199. end
  200. def queue_file_read(path, filename)
  201. location = "#{path}#{filename}"
  202. message = ''
  203. File.open(location, 'rb') do |file|
  204. file.flock(File::LOCK_EX)
  205. message = file.read
  206. file.flock(File::LOCK_UN)
  207. end
  208. File.delete(location)
  209. return if message.blank?
  210. begin
  211. JSON.parse(message)
  212. rescue => e
  213. Sessions.log('error', "can't parse queue message: #{message}, #{e.inspect}")
  214. nil
  215. end
  216. end
  217. def check_session_file_for_client(client_id, session_dir, session_file)
  218. # if no session dir exists, session got destoried
  219. if !File.exist?(session_dir)
  220. destroy(client_id)
  221. Sessions.log('debug', "missing session directory #{session_dir} for '#{client_id}', remove session.")
  222. return false
  223. end
  224. # if only session file is missing, then it's an error behavior
  225. if !File.exist?(session_file)
  226. destroy(client_id)
  227. Sessions.log('error', "missing session file for '#{client_id}', remove session.")
  228. return false
  229. end
  230. true
  231. end
  232. def new_message_filename_for(client_id)
  233. path = "#{@path}/#{client_id}/"
  234. filename = "send-#{Time.now.utc.to_f}"
  235. location = "#{path}#{filename}"
  236. check = true
  237. count = 0
  238. while check
  239. if File.exist?(location)
  240. count += 1
  241. location = "#{path}#{filename}-#{count}"
  242. else
  243. check = false
  244. end
  245. end
  246. return nil if !File.directory? path
  247. location
  248. end
  249. end