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