class Vines::Storage::Local

A storage implementation that persists data to YAML files on the local file system.

Public Class Methods

new(&block) click to toggle source
# File lib/vines/storage/local.rb, line 11
def initialize(&block)
  @dir = nil
  instance_eval(&block)
  unless @dir && File.directory?(@dir) && File.writable?(@dir)
    raise 'Must provide a writable storage directory'
  end

  %w[user vcard fragment room message].each do |sub|
    sub = File.expand_path(sub, @dir)
    Dir.mkdir(sub, 0700) unless File.exists?(sub)
  end
end

Public Instance Methods

delete_offline_messages(jid) click to toggle source
# File lib/vines/storage/local.rb, line 57
def delete_offline_messages(jid)
  if offline_messages_present?(jid)
    File.delete(absolute_path("message/#{jid.bare.to_s}"))
  end
end
dir(dir=nil) click to toggle source
# File lib/vines/storage/local.rb, line 24
def dir(dir=nil)
  dir ? @dir = File.expand_path(dir) : @dir
end
fetch_offline_messages(jid) click to toggle source
# File lib/vines/storage/local.rb, line 63
def fetch_offline_messages(jid)
  jid = JID.new(jid).bare.to_s        
  file = absolute_path("message/#{jid}") unless jid.empty?        
  offline_msgs = YAML.load_file(file) rescue {}
end
find_fragment(jid, node) click to toggle source
# File lib/vines/storage/local.rb, line 92
def find_fragment(jid, node)
  jid = JID.new(jid).bare.to_s
  return if jid.empty?
  file = 'fragment/%s' % fragment_id(jid, node)
  Nokogiri::XML(read(file)).root rescue nil
end
find_user(jid) click to toggle source
# File lib/vines/storage/local.rb, line 28
def find_user(jid)
  jid = JID.new(jid).bare.to_s
  file = "user/#{jid}" unless jid.empty?
  record = YAML.load(read(file)) rescue nil
  return User.new(jid: jid).tap do |user|
    user.name, user.password = record.values_at('name', 'password')
    (record['roster'] || {}).each_pair do |jid, props|
      user.roster << Contact.new(
        jid: jid,
        name: props['name'],
        subscription: props['subscription'],
        ask: props['ask'],
        groups: props['groups'] || [])
    end
  end if record
end
find_vcard(jid) click to toggle source
# File lib/vines/storage/local.rb, line 79
def find_vcard(jid)
  jid = JID.new(jid).bare.to_s
  return if jid.empty?
  file = "vcard/#{jid}"
  Nokogiri::XML(read(file)).root rescue nil
end
offline_messages_present?(jid) click to toggle source
# File lib/vines/storage/local.rb, line 53
def offline_messages_present?(jid)
  File.exist?(absolute_path("message/#{jid.bare.to_s}"))
end
save_fragment(jid, node) click to toggle source
# File lib/vines/storage/local.rb, line 99
def save_fragment(jid, node)
  jid = JID.new(jid).bare.to_s
  return if jid.empty?
  file = 'fragment/%s' % fragment_id(jid, node)
  save(file, node.to_xml)
end
save_offline_message(msg) click to toggle source
# File lib/vines/storage/local.rb, line 69
def save_offline_message(msg)
  file = "message/#{msg[:to]}"
  offline_msgs = YAML.load_file(absolute_path(file)) rescue []
  msg.delete('to')
  offline_msgs << msg
  save(file) do |f|          
    YAML.dump(offline_msgs,f)
  end
end
save_user(user) click to toggle source
# File lib/vines/storage/local.rb, line 45
def save_user(user)
  record = {'name' => user.name, 'password' => user.password, 'roster' => {}}
  user.roster.each do |contact|
    record['roster'][contact.jid.bare.to_s] = contact.to_h
  end
  save("user/#{user.jid.bare}", YAML.dump(record))
end
save_vcard(jid, card) click to toggle source
# File lib/vines/storage/local.rb, line 86
def save_vcard(jid, card)
  jid = JID.new(jid).bare.to_s
  return if jid.empty?
  save("vcard/#{jid}", card.to_xml)
end

Private Instance Methods

absolute_path(file) click to toggle source

Resolves a relative file name into an absolute path inside the storage directory.

file - A fully-qualified or relative file name String.

Returns the fully-qualified file path String.

Raises RuntimeError if the resolved path is outside of the storage directory. This prevents directory path traversals with maliciously crafted JIDs.

# File lib/vines/storage/local.rb, line 118
def absolute_path(file)
  File.expand_path(file, @dir).tap do |absolute|
    parent = File.dirname(File.dirname(absolute))
    raise "path traversal failed for #{file}" unless parent == @dir
  end
end
fragment_id(jid, node) click to toggle source

Generates a unique file id for the user's private XML fragment.

Private XML fragment storage needs to uniquely identify fragment files on disk. We combine the user's JID with a SHA-1 hash of the element's name and namespace to avoid special characters in the file name.

jid - A bare JID identifying the user who owns this fragment. node - A Nokogiri::XML::Node for the XML to be stored.

Returns an id String suitable for use in a file name.

# File lib/vines/storage/local.rb, line 159
def fragment_id(jid, node)
  id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
  "#{jid}-#{id}"
end
read(file) click to toggle source

Read the file from the filesystem and return its contents as a String. All files are assumed to be encoded as UTF-8.

file - A fully-qualified or relative file name String.

Returns the file content as a UTF-8 encoded String.

# File lib/vines/storage/local.rb, line 131
def read(file)
  file = absolute_path(file)
  File.read(file, encoding: 'utf-8')
end
save(file, content) click to toggle source

Write the content to the file. Make sure to consistently encode files we read and write as UTF-8.

file - A fully-qualified or relative file name String. content - The String to write.

Returns nothing.

# File lib/vines/storage/local.rb, line 143
def save(file, content)
  file = absolute_path(file)
  File.open(file, 'w:utf-8') {|f| f.write(content) }
  File.chmod(0600, file)
end