Class | PStore |
In: |
lib/pstore.rb
|
Parent: | Object |
PStore implements a file based persistence mechanism based on a Hash. User code can store hierarchies of Ruby objects (values) into the data store file by name (keys). An object hierarchy may be just a single object. User code may later read values back from the data store or even update data, as needed.
The transactional behavior ensures that any changes succeed or fail together. This can be used to ensure that the data store is not left in a transitory state, where some values were updated but others were not.
Behind the scenes, Ruby objects are stored to the data store file with Marshal. That carries the usual limitations. Proc objects cannot be marshalled, for example.
require "pstore" # a mock wiki object... class WikiPage def initialize( page_name, author, contents ) @page_name = page_name @revisions = Array.new add_revision(author, contents) end attr_reader :page_name def add_revision( author, contents ) @revisions << { :created => Time.now, :author => author, :contents => contents } end def wiki_page_references [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/) end # ... end # create a new page... home_page = WikiPage.new( "HomePage", "James Edward Gray II", "A page about the JoysOfDocumentation..." ) # then we want to update page data and the index together, or not at all... wiki = PStore.new("wiki_pages.pstore") wiki.transaction do # begin transaction; do all of this or none of it # store page... wiki[home_page.page_name] = home_page # ensure that an index has been created... wiki[:wiki_index] ||= Array.new # update wiki index... wiki[:wiki_index].push(*home_page.wiki_page_references) end # commit changes to wiki data store file ### Some time later... ### # read wiki data... wiki.transaction(true) do # begin read-only transaction, no changes allowed wiki.roots.each do |data_root_name| p data_root_name p wiki[data_root_name] end end
RDWR_ACCESS | = | File::RDWR | File::CREAT | binmode |
RD_ACCESS | = | File::RDONLY | binmode |
WR_ACCESS | = | File::WRONLY | File::CREAT | File::TRUNC | binmode |
To construct a PStore object, pass in the file path where you would like the data to be stored.
# File lib/pstore.rb, line 94 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
Retrieves a value from the PStore file data, by name. The hierarchy of Ruby objects stored under that root name will be returned.
WARNING: This method is only valid in a PStore#transaction. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 128 128: def [](name) 129: in_transaction 130: @table[name] 131: end
Stores an individual Ruby object or a hierarchy of Ruby objects in the data store file under the root name. Assigning to a name already in the data store clobbers the old data.
require "pstore" store = PStore.new("data_file.pstore") store.transaction do # begin transaction # load some data into the store... store[:single_object] = "My data..." store[:obj_heirarchy] = { "Kev Jackson" => ["rational.rb", "pstore.rb"], "James Gray" => ["erb.rb", "pstore.rb"] } end # commit changes to data store file
WARNING: This method is only valid in a PStore#transaction and it cannot be read-only. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 173 173: def []=(name, value) 174: in_transaction_wr() 175: @table[name] = value 176: end
Ends the current PStore#transaction, discarding any changes to the data store.
require "pstore" store = PStore.new("data_file.pstore") store.transaction do # begin transaction store[:one] = 1 # this change is not applied, see below... store[:two] = 2 # this change is not applied, see below... store.abort # end transaction here, discard all changes store[:three] = 3 # this change is never reached end
WARNING: This method is only valid in a PStore#transaction. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 261 261: def abort 262: in_transaction 263: @abort = true 264: throw :pstore_abort_transaction 265: end
Ends the current PStore#transaction, committing any changes to the data store immediately.
require "pstore" store = PStore.new("data_file.pstore") store.transaction do # begin transaction # load some data into the store... store[:one] = 1 store[:two] = 2 store.commit # end transaction here, committing changes store[:three] = 3 # this change is never reached end
WARNING: This method is only valid in a PStore#transaction. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 235 235: def commit 236: in_transaction 237: @abort = false 238: throw :pstore_abort_transaction 239: end
Removes an object hierarchy from the data store, by name.
WARNING: This method is only valid in a PStore#transaction and it cannot be read-only. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 183 183: def delete(name) 184: in_transaction_wr() 185: @table.delete name 186: end
This method is just like PStore#[], save that you may also provide a default value for the object. In the event the specified name is not found in the data store, your default will be returned instead. If you do not specify a default, PStore::Error will be raised if the object is not found.
WARNING: This method is only valid in a PStore#transaction. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 142 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
Returns true if the supplied name is currently in the data store.
WARNING: This method is only valid in a PStore#transaction. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 204 204: def root?(name) 205: in_transaction 206: @table.key? name 207: end
Returns the names of all object hierarchies currently in the store.
WARNING: This method is only valid in a PStore#transaction. It will raise PStore::Error if called at any other time.
# File lib/pstore.rb, line 194 194: def roots 195: in_transaction 196: @table.keys 197: end
Opens a new transaction for the data store. Code executed inside a block passed to this method may read and write data to and from the data store file.
At the end of the block, changes are committed to the data store automatically. You may exit the transaction early with a call to either PStore#commit or PStore#abort. See those methods for details about how changes are handled. Raising an uncaught Exception in the block is equivalent to calling PStore#abort.
If read_only is set to true, you will only be allowed to read from the data store during the transaction and any attempts to change the data will raise a PStore::Error.
Note that PStore does not support nested transactions.
# File lib/pstore.rb, line 284 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
Commits changes to the data store file.
# File lib/pstore.rb, line 364 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
Raises PStore::Error if the calling code is not in a PStore#transaction.
# File lib/pstore.rb, line 108 108: def in_transaction 109: raise PStore::Error, "not in transaction" unless @transaction 110: end
Raises PStore::Error if the calling code is not in a PStore#transaction or if the code is in a read-only PStore#transaction.
# File lib/pstore.rb, line 115 115: def in_transaction_wr() 116: in_transaction() 117: raise PStore::Error, "in read-only transaction" if @rdonly 118: end