lib/pstore.rb
DEFINITIONS
This source file includes following functions.
1 #
2 # How to use:
3 #
4 # db = PStore.new("/tmp/foo")
5 # db.transaction do
6 # p db.roots
7 # ary = db["root"] = [1,2,3,4]
8 # ary[0] = [1,1.5]
9 # end
10
11 # db.transaction do
12 # p db["root"]
13 # end
14
15 require "ftools"
16 require "digest/md5"
17
18 class PStore
19 class Error < StandardError
20 end
21
22 def initialize(file)
23 dir = File::dirname(file)
24 unless File::directory? dir
25 raise PStore::Error, format("directory %s does not exist", dir)
26 end
27 unless File::writable? dir
28 raise PStore::Error, format("directory %s not writable", dir)
29 end
30 if File::exist? file and not File::readable? file
31 raise PStore::Error, format("file %s not readable", file)
32 end
33 @transaction = false
34 @filename = file
35 @abort = false
36 end
37
38 def in_transaction
39 raise PStore::Error, "not in transaction" unless @transaction
40 end
41 private :in_transaction
42
43 def [](name)
44 in_transaction
45 @table[name]
46 end
47 def fetch(name, default=PStore::Error)
48 unless @table.key? name
49 if default==PStore::Error
50 raise PStore::Error, format("undefined root name `%s'", name)
51 else
52 default
53 end
54 end
55 self[name]
56 end
57 def []=(name, value)
58 in_transaction
59 @table[name] = value
60 end
61 def delete(name)
62 in_transaction
63 @table.delete name
64 end
65
66 def roots
67 in_transaction
68 @table.keys
69 end
70 def root?(name)
71 in_transaction
72 @table.key? name
73 end
74 def path
75 @filename
76 end
77
78 def commit
79 in_transaction
80 @abort = false
81 throw :pstore_abort_transaction
82 end
83 def abort
84 in_transaction
85 @abort = true
86 throw :pstore_abort_transaction
87 end
88
89 def transaction(read_only=false)
90 raise PStore::Error, "nested transaction" if @transaction
91 begin
92 @transaction = true
93 value = nil
94 backup = @filename+"~"
95 begin
96 file = File::open(@filename, "rb+")
97 orig = true
98 rescue Errno::ENOENT
99 raise if read_only
100 file = File::open(@filename, "wb+")
101 end
102 file.flock(read_only ? File::LOCK_SH : File::LOCK_EX)
103 if read_only
104 @table = Marshal::load(file)
105 elsif orig and (content = file.read) != nil
106 @table = Marshal::load(content)
107 size = content.size
108 md5 = Digest::MD5.digest(content)
109 content = nil # unreference huge data
110 else
111 @table = {}
112 end
113 begin
114 catch(:pstore_abort_transaction) do
115 value = yield(self)
116 end
117 rescue Exception
118 @abort = true
119 raise
120 ensure
121 if !read_only && !@abort
122 file.rewind
123 content = Marshal::dump(@table)
124 if !md5 || size != content.size || md5 != Digest::MD5.digest(content)
125 File::copy @filename, backup
126 begin
127 file.write(content)
128 file.truncate(file.pos)
129 content = nil # unreference huge data
130 rescue
131 File::rename backup, @filename if File::exist?(backup)
132 raise
133 end
134 end
135 end
136 @abort = false
137 end
138 ensure
139 @table = nil
140 @transaction = false
141 file.close if file
142 end
143 value
144 end
145 end
146
147 if __FILE__ == $0
148 db = PStore.new("/tmp/foo")
149 db.transaction do
150 p db.roots
151 ary = db["root"] = [1,2,3,4]
152 ary[1] = [1,1.5]
153 end
154
155 1000.times do
156 db.transaction do
157 db["root"][0] += 1
158 p db["root"][0]
159 end
160 end
161
162 db.transaction(true) do
163 p db["root"]
164 end
165 end