summaryrefslogtreecommitdiff
path: root/lib/metropolis/hash.rb
blob: 75cc14e33fd4ae129c811dac63367ebe35d0ea50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# -*- encoding: binary -*-
require 'tempfile'

# use a Ruby hash as a plain data store
# It can unmarshal a hash from disk
module Metropolis::Hash
  include Metropolis::Common

  def setup(opts)
    super
    if @path
      begin
        @db = Marshal.load(File.open(@path, "rb") { |fp| fp.read })
        Hash === @db or raise ArgumentError, "#@path is not a marshaled Hash"
      rescue Errno::ENOENT
        @db = {}
      end
    else
      @db = {}
    end
    if @readonly
      extend Metropolis::Common::RO
    elsif @path
      args = [ @db, @path, !!opts[:fsync] ]
      @clean_proc = Metropolis::Hash.finalizer_callback(args)
      ObjectSpace.define_finalizer(self, @clean_proc)
    end
  end

  def close!
    if ! @readonly && ! @path.nil?
      @clean_proc.call
      ObjectSpace.undefine_finalizer(self)
    end
    @db = @path = nil
  end

  def get(key, env)
    value = @db[key] or return r(404)
    [ 200, { Content_Length => value.size.to_s }.merge!(@headers), [ value ] ]
  end

  def put(key, env)
    value = env[Rack_Input].read
    case env[HTTP_X_TT_PDMODE]
    when "1"
      @db.exists?(key) and r(409)
      @db[key] = value
    when "2"
      (tmp = @db[key] ||= "") << value
    else
      @db[key] = value
    end
    r(201)
  end

  def delete(key)
    r(@db.delete(key) ? 200 : 404)
  end

  def self.finalizer_callback(data)
    lambda {
      db, path, fsync = data
      dir = File.dirname(path)
      tmp = Tempfile.new('hash_save', dir)
      tmp.binmode
      tmp.sync = true
      tmp.write(Marshal.dump(db))
      tmp.fsync if fsync
      File.rename(tmp.path, path)
      File.open(dir) { |d| d.fsync } if fsync
      tmp.close!
    }
  end
end