123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class Sessions::Store::File
- def initialize
- # get application root directory
- @root = Dir.pwd.to_s
- if @root.blank? || @root == '/'
- @root = Rails.root
- end
- # get working directories
- @path = "#{@root}/tmp/websocket_#{Rails.env}"
- @nodes_path = "#{@root}/tmp/session_node_#{Rails.env}"
- end
- def create(client_id, content)
- path = "#{@path}/#{client_id}"
- path_tmp = "#{@path}/tmp/#{client_id}"
- session_file = "#{path_tmp}/session"
- # store session data in session file
- FileUtils.mkpath path_tmp
- File.binwrite(session_file, content)
- # destroy old session if needed
- if File.exist?(path)
- destroy(client_id)
- end
- # move to destination directory
- FileUtils.mv(path_tmp, path)
- end
- def sessions
- path = "#{@path}/"
- FileUtils.mkdir_p path
- data = []
- Dir.foreach(path) do |entry|
- next if entry == '.'
- next if entry == '..'
- next if entry == 'tmp'
- next if entry == 'spool'
- data.push entry.to_s
- end
- data
- end
- def session_exists?(client_id)
- session_dir = "#{@path}/#{client_id}"
- return false if !File.exist?(session_dir)
- session_file = "#{session_dir}/session"
- return false if !File.exist?(session_file)
- true
- end
- def destroy(client_id)
- path = "#{@path}/#{client_id}"
- FileUtils.rm_rf path
- end
- def set(client_id, data)
- path = "#{@path}/#{client_id}"
- write_with_lock("#{path}/session", data.to_json)
- end
- def get(client_id)
- session_dir = "#{@path}/#{client_id}"
- session_file = "#{session_dir}/session"
- data = nil
- return if !check_session_file_for_client(client_id, session_dir, session_file)
- begin
- data_json = JSON.parse(read_with_lock(session_file))
- if data_json
- data = Sessions.symbolize_keys(data_json)
- data[:user] = data_json['user'] # for compat. reasons
- end
- rescue => e
- Sessions.log('error', e.inspect)
- destroy(client_id)
- Sessions.log('error', "error in reading/parsing session file '#{session_file}', remove session.")
- return
- end
- data
- end
- def send_data(client_id, data)
- location = new_message_filename_for(client_id)
- return false if !location
- begin
- write_with_lock(location, data.to_json)
- rescue => e
- Sessions.log('error', e.inspect)
- Sessions.log('error', "error in writing message file '#{location}'")
- return false
- end
- true
- end
- def queue(client_id)
- path = "#{@path}/#{client_id}/"
- data = []
- files = []
- Dir.foreach(path) do |entry|
- next if entry == '.'
- next if entry == '..'
- files.push entry
- end
- files.sort.each do |entry|
- next if !entry.start_with?('send')
- message = queue_file_read(path, entry)
- next if !message
- data.push message
- end
- data
- end
- def cleanup
- return true if !File.exist?(@path)
- FileUtils.rm_rf @path
- true
- end
- def clear_spool
- path = "#{@path}/spool/"
- FileUtils.rm_rf path
- end
- ### Node-specific methods ###
- def clear_nodes
- FileUtils.rm_rf @nodes_path
- end
- def nodes
- path = "#{@nodes_path}/*.status"
- nodes = []
- files = Dir.glob(path)
- files.each do |filename|
- begin
- content = read_with_lock(filename)
- data = JSON.parse(content)
- nodes.push data
- rescue => e
- Rails.logger.error "can't parse status file #{filename}, #{e.inspect}"
- # to_delete.push "#{path}/#{entry}"
- # next
- end
- end
- nodes
- end
- def add_node(node_id, data)
- FileUtils.mkdir_p @nodes_path
- status_file = "#{@nodes_path}/#{node_id}.status"
- content = data.to_json
- # store session data in session file
- write_with_lock(status_file, content)
- end
- def each_node_session()
- # read node sessions
- path = "#{@nodes_path}/*.session"
- files = Dir.glob(path)
- files.each do |filename|
- begin
- content = read_with_lock(filename)
- next if content.blank?
- data = JSON.parse(content)
- next if data.blank?
- yield data
- rescue => e
- Rails.logger.error "can't parse session file #{filename}, #{e.inspect}"
- # to_delete.push "#{path}/#{entry}"
- # next
- end
- end
- end
- def create_node_session(node_id, client_id, data)
- FileUtils.mkdir_p @nodes_path
- status_file = "#{@nodes_path}/#{node_id}.#{client_id}.session"
- content = data.to_json
- # store session data in session file
- write_with_lock(status_file, content)
- end
- def each_session_by_node(node_id)
- # read node sessions
- path = "#{@nodes_path}/#{node_id}.*.session"
- files = Dir.glob(path)
- files.each do |filename|
- begin
- content = read_with_lock(filename)
- next if content.blank?
- data = JSON.parse(content)
- next if data.blank?
- yield data
- rescue => e
- Rails.logger.error "can't parse session file #{filename}, #{e.inspect}"
- # to_delete.push "#{path}/#{entry}"
- # next
- end
- end
- end
- private
- def write_with_lock(filename, data)
- File.open(filename, 'ab') do |file|
- file.flock(File::LOCK_EX)
- file.truncate 0 # Truncate only after locking to avoid empty state
- file.write data
- end
- rescue Errno::ENOENT => e
- Rails.logger.debug { "Can't write data to web socket session file #{filename}, maybe the session was removed in the meantime: #{e.inspect}" }
- Rails.logger.debug e
- end
- def read_with_lock(filename)
- File.open(filename, 'rb') do |file|
- file.flock(File::LOCK_SH)
- return file.read
- end
- end
- def queue_file_read(path, filename)
- location = "#{path}#{filename}"
- message = ''
- File.open(location, 'rb') do |file|
- file.flock(File::LOCK_EX)
- message = file.read
- file.flock(File::LOCK_UN)
- end
- File.delete(location)
- return if message.blank?
- begin
- JSON.parse(message)
- rescue => e
- Sessions.log('error', "can't parse queue message: #{message}, #{e.inspect}")
- nil
- end
- end
- def check_session_file_for_client(client_id, session_dir, session_file)
- # if no session dir exists, session got destoried
- if !File.exist?(session_dir)
- destroy(client_id)
- Sessions.log('debug', "missing session directory #{session_dir} for '#{client_id}', remove session.")
- return false
- end
- # if only session file is missing, then it's an error behavior
- if !File.exist?(session_file)
- destroy(client_id)
- Sessions.log('error', "missing session file for '#{client_id}', remove session.")
- return false
- end
- true
- end
- def new_message_filename_for(client_id)
- path = "#{@path}/#{client_id}/"
- filename = "send-#{Time.now.utc.to_f}"
- location = "#{path}#{filename}"
- check = true
- count = 0
- while check
- if File.exist?(location)
- count += 1
- location = "#{path}#{filename}-#{count}"
- else
- check = false
- end
- end
- return nil if !File.directory? path
- location
- end
- end
|