1 | # = PStore -- Transactional File Storage for Ruby Objects
|
---|
2 | #
|
---|
3 | # pstore.rb -
|
---|
4 | # originally by matz
|
---|
5 | # documentation by Kev Jackson and James Edward Gray II
|
---|
6 | #
|
---|
7 | # See PStore for documentation.
|
---|
8 |
|
---|
9 |
|
---|
10 | require "fileutils"
|
---|
11 | require "digest/md5"
|
---|
12 |
|
---|
13 | #
|
---|
14 | # PStore implements a file based persistance mechanism based on a Hash. User
|
---|
15 | # code can store hierarchies of Ruby objects (values) into the data store file
|
---|
16 | # by name (keys). An object hierarchy may be just a single object. User code
|
---|
17 | # may later read values back from the data store or even update data, as needed.
|
---|
18 | #
|
---|
19 | # The transactional behavior ensures that any changes succeed or fail together.
|
---|
20 | # This can be used to ensure that the data store is not left in a transitory
|
---|
21 | # state, where some values were upated but others were not.
|
---|
22 | #
|
---|
23 | # Behind the scenes, Ruby objects are stored to the data store file with
|
---|
24 | # Marshal. That carries the usual limitations. Proc objects cannot be
|
---|
25 | # marshalled, for example.
|
---|
26 | #
|
---|
27 | # == Usage example:
|
---|
28 | #
|
---|
29 | # require "pstore"
|
---|
30 | #
|
---|
31 | # # a mock wiki object...
|
---|
32 | # class WikiPage
|
---|
33 | # def initialize( page_name, author, contents )
|
---|
34 | # @page_name = page_name
|
---|
35 | # @revisions = Array.new
|
---|
36 | #
|
---|
37 | # add_revision(author, contents)
|
---|
38 | # end
|
---|
39 | #
|
---|
40 | # attr_reader :page_name
|
---|
41 | #
|
---|
42 | # def add_revision( author, contents )
|
---|
43 | # @revisions << { :created => Time.now,
|
---|
44 | # :author => author,
|
---|
45 | # :contents => contents }
|
---|
46 | # end
|
---|
47 | #
|
---|
48 | # def wiki_page_references
|
---|
49 | # [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
|
---|
50 | # end
|
---|
51 | #
|
---|
52 | # # ...
|
---|
53 | # end
|
---|
54 | #
|
---|
55 | # # create a new page...
|
---|
56 | # home_page = WikiPage.new( "HomePage", "James Edward Gray II",
|
---|
57 | # "A page about the JoysOfDocumentation..." )
|
---|
58 | #
|
---|
59 | # # then we want to update page data and the index together, or not at all...
|
---|
60 | # wiki = PStore.new("wiki_pages.pstore")
|
---|
61 | # wiki.transaction do # begin transaction; do all of this or none of it
|
---|
62 | # # store page...
|
---|
63 | # wiki[home_page.page_name] = home_page
|
---|
64 | # # ensure that an index has been created...
|
---|
65 | # wiki[:wiki_index] ||= Array.new
|
---|
66 | # # update wiki index...
|
---|
67 | # wiki[:wiki_index].push(*home_page.wiki_page_references)
|
---|
68 | # end # commit changes to wiki data store file
|
---|
69 | #
|
---|
70 | # ### Some time later... ###
|
---|
71 | #
|
---|
72 | # # read wiki data...
|
---|
73 | # wiki.transaction(true) do # begin read-only transaction, no changes allowed
|
---|
74 | # wiki.roots.each do |data_root_name|
|
---|
75 | # p data_root_name
|
---|
76 | # p wiki[data_root_name]
|
---|
77 | # end
|
---|
78 | # end
|
---|
79 | #
|
---|
80 | class PStore
|
---|
81 | binmode = defined?(File::BINARY) ? File::BINARY : 0
|
---|
82 | RDWR_ACCESS = File::RDWR | File::CREAT | binmode
|
---|
83 | RD_ACCESS = File::RDONLY | binmode
|
---|
84 | WR_ACCESS = File::WRONLY | File::CREAT | File::TRUNC | binmode
|
---|
85 |
|
---|
86 | # The error type thrown by all PStore methods.
|
---|
87 | class Error < StandardError
|
---|
88 | end
|
---|
89 |
|
---|
90 | #
|
---|
91 | # To construct a PStore object, pass in the _file_ path where you would like
|
---|
92 | # the data to be stored.
|
---|
93 | #
|
---|
94 | def initialize(file)
|
---|
95 | dir = File::dirname(file)
|
---|
96 | unless File::directory? dir
|
---|
97 | raise PStore::Error, format("directory %s does not exist", dir)
|
---|
98 | end
|
---|
99 | if File::exist? file and not File::readable? file
|
---|
100 | raise PStore::Error, format("file %s not readable", file)
|
---|
101 | end
|
---|
102 | @transaction = false
|
---|
103 | @filename = file
|
---|
104 | @abort = false
|
---|
105 | end
|
---|
106 |
|
---|
107 | # Raises PStore::Error if the calling code is not in a PStore#transaction.
|
---|
108 | def in_transaction
|
---|
109 | raise PStore::Error, "not in transaction" unless @transaction
|
---|
110 | end
|
---|
111 | #
|
---|
112 | # Raises PStore::Error if the calling code is not in a PStore#transaction or
|
---|
113 | # if the code is in a read-only PStore#transaction.
|
---|
114 | #
|
---|
115 | def in_transaction_wr()
|
---|
116 | in_transaction()
|
---|
117 | raise PStore::Error, "in read-only transaction" if @rdonly
|
---|
118 | end
|
---|
119 | private :in_transaction, :in_transaction_wr
|
---|
120 |
|
---|
121 | #
|
---|
122 | # Retrieves a value from the PStore file data, by _name_. The hierarchy of
|
---|
123 | # Ruby objects stored under that root _name_ will be returned.
|
---|
124 | #
|
---|
125 | # *WARNING*: This method is only valid in a PStore#transaction. It will
|
---|
126 | # raise PStore::Error if called at any other time.
|
---|
127 | #
|
---|
128 | def [](name)
|
---|
129 | in_transaction
|
---|
130 | @table[name]
|
---|
131 | end
|
---|
132 | #
|
---|
133 | # This method is just like PStore#[], save that you may also provide a
|
---|
134 | # _default_ value for the object. In the event the specified _name_ is not
|
---|
135 | # found in the data store, your _default_ will be returned instead. If you do
|
---|
136 | # not specify a default, PStore::Error will be raised if the object is not
|
---|
137 | # found.
|
---|
138 | #
|
---|
139 | # *WARNING*: This method is only valid in a PStore#transaction. It will
|
---|
140 | # raise PStore::Error if called at any other time.
|
---|
141 | #
|
---|
142 | def fetch(name, default=PStore::Error)
|
---|
143 | in_transaction
|
---|
144 | unless @table.key? name
|
---|
145 | if default==PStore::Error
|
---|
146 | raise PStore::Error, format("undefined root name `%s'", name)
|
---|
147 | else
|
---|
148 | return default
|
---|
149 | end
|
---|
150 | end
|
---|
151 | @table[name]
|
---|
152 | end
|
---|
153 | #
|
---|
154 | # Stores an individual Ruby object or a hierarchy of Ruby objects in the data
|
---|
155 | # store file under the root _name_. Assigning to a _name_ already in the data
|
---|
156 | # store clobbers the old data.
|
---|
157 | #
|
---|
158 | # == Example:
|
---|
159 | #
|
---|
160 | # require "pstore"
|
---|
161 | #
|
---|
162 | # store = PStore.new("data_file.pstore")
|
---|
163 | # store.transaction do # begin transaction
|
---|
164 | # # load some data into the store...
|
---|
165 | # store[:single_object] = "My data..."
|
---|
166 | # store[:obj_heirarchy] = { "Kev Jackson" => ["rational.rb", "pstore.rb"],
|
---|
167 | # "James Gray" => ["erb.rb", "pstore.rb"] }
|
---|
168 | # end # commit changes to data store file
|
---|
169 | #
|
---|
170 | # *WARNING*: This method is only valid in a PStore#transaction and it cannot
|
---|
171 | # be read-only. It will raise PStore::Error if called at any other time.
|
---|
172 | #
|
---|
173 | def []=(name, value)
|
---|
174 | in_transaction_wr()
|
---|
175 | @table[name] = value
|
---|
176 | end
|
---|
177 | #
|
---|
178 | # Removes an object hierarchy from the data store, by _name_.
|
---|
179 | #
|
---|
180 | # *WARNING*: This method is only valid in a PStore#transaction and it cannot
|
---|
181 | # be read-only. It will raise PStore::Error if called at any other time.
|
---|
182 | #
|
---|
183 | def delete(name)
|
---|
184 | in_transaction_wr()
|
---|
185 | @table.delete name
|
---|
186 | end
|
---|
187 |
|
---|
188 | #
|
---|
189 | # Returns the names of all object hierarchies currently in the store.
|
---|
190 | #
|
---|
191 | # *WARNING*: This method is only valid in a PStore#transaction. It will
|
---|
192 | # raise PStore::Error if called at any other time.
|
---|
193 | #
|
---|
194 | def roots
|
---|
195 | in_transaction
|
---|
196 | @table.keys
|
---|
197 | end
|
---|
198 | #
|
---|
199 | # Returns true if the supplied _name_ is currently in the data store.
|
---|
200 | #
|
---|
201 | # *WARNING*: This method is only valid in a PStore#transaction. It will
|
---|
202 | # raise PStore::Error if called at any other time.
|
---|
203 | #
|
---|
204 | def root?(name)
|
---|
205 | in_transaction
|
---|
206 | @table.key? name
|
---|
207 | end
|
---|
208 | # Returns the path to the data store file.
|
---|
209 | def path
|
---|
210 | @filename
|
---|
211 | end
|
---|
212 |
|
---|
213 | #
|
---|
214 | # Ends the current PStore#transaction, committing any changes to the data
|
---|
215 | # store immediately.
|
---|
216 | #
|
---|
217 | # == Example:
|
---|
218 | #
|
---|
219 | # require "pstore"
|
---|
220 | #
|
---|
221 | # store = PStore.new("data_file.pstore")
|
---|
222 | # store.transaction do # begin transaction
|
---|
223 | # # load some data into the store...
|
---|
224 | # store[:one] = 1
|
---|
225 | # store[:two] = 2
|
---|
226 | #
|
---|
227 | # store.commit # end transaction here, committing changes
|
---|
228 | #
|
---|
229 | # store[:three] = 3 # this change is never reached
|
---|
230 | # end
|
---|
231 | #
|
---|
232 | # *WARNING*: This method is only valid in a PStore#transaction. It will
|
---|
233 | # raise PStore::Error if called at any other time.
|
---|
234 | #
|
---|
235 | def commit
|
---|
236 | in_transaction
|
---|
237 | @abort = false
|
---|
238 | throw :pstore_abort_transaction
|
---|
239 | end
|
---|
240 | #
|
---|
241 | # Ends the current PStore#transaction, discarding any changes to the data
|
---|
242 | # store.
|
---|
243 | #
|
---|
244 | # == Example:
|
---|
245 | #
|
---|
246 | # require "pstore"
|
---|
247 | #
|
---|
248 | # store = PStore.new("data_file.pstore")
|
---|
249 | # store.transaction do # begin transaction
|
---|
250 | # store[:one] = 1 # this change is not applied, see below...
|
---|
251 | # store[:two] = 2 # this change is not applied, see below...
|
---|
252 | #
|
---|
253 | # store.abort # end transaction here, discard all changes
|
---|
254 | #
|
---|
255 | # store[:three] = 3 # this change is never reached
|
---|
256 | # end
|
---|
257 | #
|
---|
258 | # *WARNING*: This method is only valid in a PStore#transaction. It will
|
---|
259 | # raise PStore::Error if called at any other time.
|
---|
260 | #
|
---|
261 | def abort
|
---|
262 | in_transaction
|
---|
263 | @abort = true
|
---|
264 | throw :pstore_abort_transaction
|
---|
265 | end
|
---|
266 |
|
---|
267 | #
|
---|
268 | # Opens a new transaction for the data store. Code executed inside a block
|
---|
269 | # passed to this method may read and write data to and from the data store
|
---|
270 | # file.
|
---|
271 | #
|
---|
272 | # At the end of the block, changes are committed to the data store
|
---|
273 | # automatically. You may exit the transaction early with a call to either
|
---|
274 | # PStore#commit or PStore#abort. See those methods for details about how
|
---|
275 | # changes are handled. Raising an uncaught Exception in the block is
|
---|
276 | # equivalent to calling PStore#abort.
|
---|
277 | #
|
---|
278 | # If _read_only_ is set to +true+, you will only be allowed to read from the
|
---|
279 | # data store during the transaction and any attempts to change the data will
|
---|
280 | # raise a PStore::Error.
|
---|
281 | #
|
---|
282 | # Note that PStore does not support nested transactions.
|
---|
283 | #
|
---|
284 | def transaction(read_only=false) # :yields: pstore
|
---|
285 | raise PStore::Error, "nested transaction" if @transaction
|
---|
286 | begin
|
---|
287 | @rdonly = read_only
|
---|
288 | @abort = false
|
---|
289 | @transaction = true
|
---|
290 | value = nil
|
---|
291 | new_file = @filename + ".new"
|
---|
292 |
|
---|
293 | content = nil
|
---|
294 | unless read_only
|
---|
295 | file = File.open(@filename, RDWR_ACCESS)
|
---|
296 | file.flock(File::LOCK_EX)
|
---|
297 | commit_new(file) if FileTest.exist?(new_file)
|
---|
298 | content = file.read()
|
---|
299 | else
|
---|
300 | begin
|
---|
301 | file = File.open(@filename, RD_ACCESS)
|
---|
302 | file.flock(File::LOCK_SH)
|
---|
303 | content = (File.open(new_file, RD_ACCESS) {|n| n.read} rescue file.read())
|
---|
304 | rescue Errno::ENOENT
|
---|
305 | content = ""
|
---|
306 | end
|
---|
307 | end
|
---|
308 |
|
---|
309 | if content != ""
|
---|
310 | @table = load(content)
|
---|
311 | if !read_only
|
---|
312 | size = content.size
|
---|
313 | md5 = Digest::MD5.digest(content)
|
---|
314 | end
|
---|
315 | else
|
---|
316 | @table = {}
|
---|
317 | end
|
---|
318 | content = nil # unreference huge data
|
---|
319 |
|
---|
320 | begin
|
---|
321 | catch(:pstore_abort_transaction) do
|
---|
322 | value = yield(self)
|
---|
323 | end
|
---|
324 | rescue Exception
|
---|
325 | @abort = true
|
---|
326 | raise
|
---|
327 | ensure
|
---|
328 | if !read_only and !@abort
|
---|
329 | tmp_file = @filename + ".tmp"
|
---|
330 | content = dump(@table)
|
---|
331 | if !md5 || size != content.size || md5 != Digest::MD5.digest(content)
|
---|
332 | File.open(tmp_file, WR_ACCESS) {|t| t.write(content)}
|
---|
333 | File.rename(tmp_file, new_file)
|
---|
334 | commit_new(file)
|
---|
335 | end
|
---|
336 | content = nil # unreference huge data
|
---|
337 | end
|
---|
338 | end
|
---|
339 | ensure
|
---|
340 | @table = nil
|
---|
341 | @transaction = false
|
---|
342 | file.close if file
|
---|
343 | end
|
---|
344 | value
|
---|
345 | end
|
---|
346 |
|
---|
347 | # This method is just a wrapped around Marshal.dump.
|
---|
348 | def dump(table) # :nodoc:
|
---|
349 | Marshal::dump(table)
|
---|
350 | end
|
---|
351 |
|
---|
352 | # This method is just a wrapped around Marshal.load.
|
---|
353 | def load(content) # :nodoc:
|
---|
354 | Marshal::load(content)
|
---|
355 | end
|
---|
356 |
|
---|
357 | # This method is just a wrapped around Marshal.load.
|
---|
358 | def load_file(file) # :nodoc:
|
---|
359 | Marshal::load(file)
|
---|
360 | end
|
---|
361 |
|
---|
362 | private
|
---|
363 | # Commits changes to the data store file.
|
---|
364 | def commit_new(f)
|
---|
365 | f.truncate(0)
|
---|
366 | f.rewind
|
---|
367 | new_file = @filename + ".new"
|
---|
368 | File.open(new_file, RD_ACCESS) do |nf|
|
---|
369 | FileUtils.copy_stream(nf, f)
|
---|
370 | end
|
---|
371 | File.unlink(new_file)
|
---|
372 | end
|
---|
373 | end
|
---|
374 |
|
---|
375 | # :enddoc:
|
---|
376 |
|
---|
377 | if __FILE__ == $0
|
---|
378 | db = PStore.new("/tmp/foo")
|
---|
379 | db.transaction do
|
---|
380 | p db.roots
|
---|
381 | ary = db["root"] = [1,2,3,4]
|
---|
382 | ary[1] = [1,1.5]
|
---|
383 | end
|
---|
384 |
|
---|
385 | 1000.times do
|
---|
386 | db.transaction do
|
---|
387 | db["root"][0] += 1
|
---|
388 | p db["root"][0]
|
---|
389 | end
|
---|
390 | end
|
---|
391 |
|
---|
392 | db.transaction(true) do
|
---|
393 | p db["root"]
|
---|
394 | end
|
---|
395 | end
|
---|