perobs 2.3.1 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/perobs/Array.rb +17 -4
- data/lib/perobs/BTreeBlob.rb +22 -23
- data/lib/perobs/BTreeDB.rb +11 -12
- data/lib/perobs/Cache.rb +5 -4
- data/lib/perobs/DataBase.rb +19 -7
- data/lib/perobs/DynamoDB.rb +1 -1
- data/lib/perobs/FixedSizeBlobFile.rb +189 -0
- data/lib/perobs/FlatFile.rb +513 -0
- data/lib/perobs/FlatFileDB.rb +249 -0
- data/lib/perobs/FreeSpaceManager.rb +204 -0
- data/lib/perobs/Hash.rb +17 -4
- data/lib/perobs/IndexTree.rb +164 -0
- data/lib/perobs/IndexTreeNode.rb +296 -0
- data/lib/perobs/Log.rb +125 -0
- data/lib/perobs/Object.rb +10 -11
- data/lib/perobs/ObjectBase.rb +18 -5
- data/lib/perobs/StackFile.rb +137 -0
- data/lib/perobs/Store.rb +85 -19
- data/lib/perobs/TreeDB.rb +276 -0
- data/lib/perobs/version.rb +1 -1
- data/test/Array_spec.rb +11 -2
- data/test/FixedSizeBlobFile_spec.rb +91 -0
- data/test/FlatFileDB_spec.rb +56 -0
- data/test/FreeSpaceManager_spec.rb +91 -0
- data/test/Hash_spec.rb +11 -2
- data/test/IndexTree_spec.rb +118 -0
- data/test/Object_spec.rb +29 -17
- data/test/StackFile_spec.rb +113 -0
- data/test/Store_spec.rb +37 -3
- metadata +22 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2cb73944d6f6f8968d868d89e19e229dfecd5fc8
|
4
|
+
data.tar.gz: d9c7598cd06bd8b88678eaec6f71f494eb1a0c1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: befb35a3bc54b7f261889bd7b6495aaaa51c75f50cbb91fef4a657fda323bacb8080f91934062d2ae28e751ab23bc6130ec033fd802d54ae4c67a86b092442a0
|
7
|
+
data.tar.gz: 013e98cd4fe0fce2063ac8a204a33152f1e96c8e22786870046a66a6aa5d6d941444c3b7caaff319404d5d59a0ad2449fec5638266b883e833833a76131b90b2
|
data/README.md
CHANGED
data/lib/perobs/Array.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# = Array.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
5
|
+
# Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
|
6
6
|
#
|
7
7
|
# MIT License
|
8
8
|
#
|
@@ -25,6 +25,7 @@
|
|
25
25
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
26
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
27
|
|
28
|
+
require 'perobs/Log'
|
28
29
|
require 'perobs/ObjectBase'
|
29
30
|
|
30
31
|
module PEROBS
|
@@ -49,7 +50,7 @@ module PEROBS
|
|
49
50
|
:&, :*, :+, :-, :==, :[], :<=>, :at, :abbrev, :assoc, :bsearch, :collect,
|
50
51
|
:combination, :compact, :count, :cycle, :dclone, :drop, :drop_while,
|
51
52
|
:each, :each_index, :empty?, :eql?, :fetch, :find_index, :first,
|
52
|
-
:flatten, :frozen?, :hash, :include?, :index, :
|
53
|
+
:flatten, :frozen?, :hash, :include?, :index, :join, :last,
|
53
54
|
:length, :map, :pack, :permutation, :pretty_print, :pretty_print_cycle,
|
54
55
|
:product, :rassoc, :reject, :repeated_combination,
|
55
56
|
:repeated_permutation, :reverse, :reverse_each, :rindex, :rotate,
|
@@ -107,6 +108,7 @@ module PEROBS
|
|
107
108
|
@data.delete_if do |v|
|
108
109
|
v && v.respond_to?(:is_poxreference?) && v.id == id
|
109
110
|
end
|
111
|
+
@store.cache.cache_write(self)
|
110
112
|
end
|
111
113
|
|
112
114
|
# Restore the persistent data from a single data structure.
|
@@ -118,6 +120,17 @@ module PEROBS
|
|
118
120
|
POXReference.new(@store, v.id) : v }
|
119
121
|
end
|
120
122
|
|
123
|
+
# Textual dump for debugging purposes
|
124
|
+
# @return [String]
|
125
|
+
def inspect
|
126
|
+
"<#{self.class}:#{@_id}>\n[\n" +
|
127
|
+
@data.map do |v|
|
128
|
+
" " + (v.respond_to?(:is_poxreference?) ?
|
129
|
+
"<PEROBS::ObjectBase:#{v._id}>" : v.inspect)
|
130
|
+
end.join(",\n") +
|
131
|
+
"\n]\n"
|
132
|
+
end
|
133
|
+
|
121
134
|
private
|
122
135
|
|
123
136
|
def _serialize
|
@@ -129,9 +142,9 @@ module PEROBS
|
|
129
142
|
# objects should not be used directly. The library only exposes them
|
130
143
|
# via POXReference proxy objects.
|
131
144
|
if v.is_a?(ObjectBase)
|
132
|
-
|
145
|
+
PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
|
133
146
|
"It is stored in a PEROBS::Array at index #{@data.index(v)}. " +
|
134
|
-
'Have you used self() instead of myself() to' +
|
147
|
+
'Have you used self() instead of myself() to ' +
|
135
148
|
"get the reference of this PEROBS object?\n" +
|
136
149
|
v.inspect
|
137
150
|
end
|
data/lib/perobs/BTreeBlob.rb
CHANGED
@@ -27,6 +27,8 @@
|
|
27
27
|
|
28
28
|
require 'zlib'
|
29
29
|
|
30
|
+
require 'perobs/Log'
|
31
|
+
|
30
32
|
module PEROBS
|
31
33
|
|
32
34
|
# This class manages the usage of the data blobs in the corresponding
|
@@ -77,7 +79,7 @@ module PEROBS
|
|
77
79
|
crc32 = Zlib.crc32(raw, 0)
|
78
80
|
start_address = reserve_bytes(id, bytes, crc32)
|
79
81
|
if write_to_blobs_file(raw, start_address) != bytes
|
80
|
-
|
82
|
+
PEROBS.log.fatal 'Object length does not match written bytes'
|
81
83
|
end
|
82
84
|
write_index
|
83
85
|
end
|
@@ -118,8 +120,8 @@ module PEROBS
|
|
118
120
|
end
|
119
121
|
|
120
122
|
unless found
|
121
|
-
|
122
|
-
|
123
|
+
PEROBS.log.fatal "Cannot find an entry for ID #{'%016X' % id} " +
|
124
|
+
"#{id} to mark"
|
123
125
|
end
|
124
126
|
|
125
127
|
write_index
|
@@ -136,8 +138,7 @@ module PEROBS
|
|
136
138
|
end
|
137
139
|
|
138
140
|
return false if ignore_errors
|
139
|
-
|
140
|
-
"Cannot find an entry for ID #{'%016X' % id} to check"
|
141
|
+
PEROBS.log.fatal "Cannot find an entry for ID #{'%016X' % id} to check"
|
141
142
|
end
|
142
143
|
|
143
144
|
# Remove all entries from the index that have not been marked.
|
@@ -165,7 +166,7 @@ module PEROBS
|
|
165
166
|
# @return [TrueClass/FalseClass] Always true right now
|
166
167
|
def check(repair = false)
|
167
168
|
# Determine size of the data blobs file.
|
168
|
-
data_file_size = File.
|
169
|
+
data_file_size = File.exist?(@blobs_file_name) ?
|
169
170
|
File.size(@blobs_file_name) : 0
|
170
171
|
|
171
172
|
next_start = 0
|
@@ -173,7 +174,7 @@ module PEROBS
|
|
173
174
|
@entries.each do |entry|
|
174
175
|
# Entries should never overlap
|
175
176
|
if prev_entry && next_start > entry[START]
|
176
|
-
|
177
|
+
PEROBS.log.fatal
|
177
178
|
"#{@dir}: Index entries are overlapping\n" +
|
178
179
|
"ID: #{'%016X' % prev_entry[ID]} " +
|
179
180
|
"Start: #{prev_entry[START]} " +
|
@@ -185,7 +186,7 @@ module PEROBS
|
|
185
186
|
|
186
187
|
# Entries must fit within the data file
|
187
188
|
if next_start > data_file_size
|
188
|
-
|
189
|
+
PEROBS.log.fatal
|
189
190
|
"#{@dir}: Entry for ID #{'%016X' % entry[ID]} " +
|
190
191
|
"goes beyond 'data' file " +
|
191
192
|
"size (#{data_file_size})\n" +
|
@@ -209,8 +210,8 @@ module PEROBS
|
|
209
210
|
begin
|
210
211
|
File.write(@blobs_file_name, raw, address)
|
211
212
|
rescue => e
|
212
|
-
|
213
|
-
|
213
|
+
PEROBS.log.fatal "Cannot write blobs file #{@blobs_file_name}: " +
|
214
|
+
e.message
|
214
215
|
end
|
215
216
|
end
|
216
217
|
|
@@ -221,13 +222,12 @@ module PEROBS
|
|
221
222
|
begin
|
222
223
|
raw = File.read(@blobs_file_name, entry[BYTES], entry[START])
|
223
224
|
rescue => e
|
224
|
-
|
225
|
-
|
225
|
+
PEROBS.log.fatal "Cannot read blobs file #{@blobs_file_name}: " +
|
226
|
+
e.message
|
226
227
|
end
|
227
228
|
if Zlib.crc32(raw, 0) != entry[CRC]
|
228
|
-
|
229
|
-
|
230
|
-
"Checksum mismatch"
|
229
|
+
PEROBS.log.fatal "BTreeBlob for object #{entry[ID]} has been " +
|
230
|
+
"corrupted: Checksum mismatch"
|
231
231
|
end
|
232
232
|
|
233
233
|
raw
|
@@ -296,7 +296,7 @@ module PEROBS
|
|
296
296
|
entry_bytes = 29
|
297
297
|
entry_format = 'QQQCL'
|
298
298
|
restore_crc = false
|
299
|
-
if File.
|
299
|
+
if File.exist?(@index_file_name)
|
300
300
|
begin
|
301
301
|
File.open(@index_file_name, 'rb') do |f|
|
302
302
|
# Since version 2.3.0, all index files start with a header.
|
@@ -338,8 +338,8 @@ module PEROBS
|
|
338
338
|
begin
|
339
339
|
raw = File.read(@blobs_file_name, e[BYTES], e[START])
|
340
340
|
rescue => e
|
341
|
-
|
342
|
-
"
|
341
|
+
PEROBS.log.fatal "Cannot read blobs file " +
|
342
|
+
"#{@blobs_file_name}: #{e.message}"
|
343
343
|
end
|
344
344
|
e[CRC] = Zlib.crc32(raw)
|
345
345
|
end
|
@@ -348,8 +348,8 @@ module PEROBS
|
|
348
348
|
end
|
349
349
|
end
|
350
350
|
rescue => e
|
351
|
-
|
352
|
-
|
351
|
+
PEROBS.log.fatal "BTreeBlob file #{@index_file_name} corrupted: " +
|
352
|
+
e.message
|
353
353
|
end
|
354
354
|
end
|
355
355
|
end
|
@@ -364,9 +364,8 @@ module PEROBS
|
|
364
364
|
end
|
365
365
|
end
|
366
366
|
rescue => e
|
367
|
-
|
368
|
-
|
369
|
-
e.message
|
367
|
+
PEROBS.log.fatal "Cannot write BTreeBlob index file " +
|
368
|
+
"#{@index_file_name}: " + e.message
|
370
369
|
end
|
371
370
|
end
|
372
371
|
|
data/lib/perobs/BTreeDB.rb
CHANGED
@@ -27,6 +27,7 @@
|
|
27
27
|
|
28
28
|
require 'fileutils'
|
29
29
|
|
30
|
+
require 'perobs/Log'
|
30
31
|
require 'perobs/DataBase'
|
31
32
|
require 'perobs/BTreeBlob'
|
32
33
|
|
@@ -69,15 +70,15 @@ module PEROBS
|
|
69
70
|
# Check and set @dir_bits, the number of bits used for each tree level.
|
70
71
|
@dir_bits = options[:dir_bits] || 12
|
71
72
|
if @dir_bits < 4 || @dir_bits > 14
|
72
|
-
|
73
|
-
|
73
|
+
PEROBS.log.fatal "dir_bits option (#{@dir_bits}) must be between 4 " +
|
74
|
+
"and 12"
|
74
75
|
end
|
75
76
|
check_option('dir_bits')
|
76
77
|
|
77
78
|
@max_blob_size = options[:max_blob_size] || 32
|
78
79
|
if @max_blob_size < 4 || @max_blob_size > 128
|
79
|
-
|
80
|
-
"
|
80
|
+
PEROBS.log.fatal "max_blob_size option (#{@max_blob_size}) must be " +
|
81
|
+
"between 4 and 128"
|
81
82
|
end
|
82
83
|
check_option('max_blob_size')
|
83
84
|
|
@@ -115,8 +116,7 @@ module PEROBS
|
|
115
116
|
begin
|
116
117
|
File.write(file_name, hash.to_json)
|
117
118
|
rescue => e
|
118
|
-
|
119
|
-
"Cannot write hash file '#{file_name}': #{e.message}"
|
119
|
+
PEROBS.log.fatal "Cannot write hash file '#{file_name}': #{e.message}"
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
@@ -125,13 +125,12 @@ module PEROBS
|
|
125
125
|
# @return [Hash] A Hash that maps String objects to strings or numbers.
|
126
126
|
def get_hash(name)
|
127
127
|
file_name = File.join(@db_dir, name + '.json')
|
128
|
-
return ::Hash.new unless File.
|
128
|
+
return ::Hash.new unless File.exist?(file_name)
|
129
129
|
|
130
130
|
begin
|
131
131
|
json = File.read(file_name)
|
132
132
|
rescue => e
|
133
|
-
|
134
|
-
"Cannot read hash file '#{file_name}': #{e.message}"
|
133
|
+
PEROBS.log.fatal "Cannot read hash file '#{file_name}': #{e.message}"
|
135
134
|
end
|
136
135
|
JSON.parse(json, :create_additions => true)
|
137
136
|
end
|
@@ -219,8 +218,8 @@ module PEROBS
|
|
219
218
|
dir_bits = id & @dir_mask
|
220
219
|
dir_name = File.join(dir_name, @dir_format_string % dir_bits)
|
221
220
|
|
222
|
-
if Dir.
|
223
|
-
if File.
|
221
|
+
if Dir.exist?(dir_name)
|
222
|
+
if File.exist?(File.join(dir_name, 'index'))
|
224
223
|
# The directory is a blob directory and not a BTree node dir.
|
225
224
|
return BTreeBlob.new(dir_name, self)
|
226
225
|
end
|
@@ -260,7 +259,7 @@ module PEROBS
|
|
260
259
|
# contrast to BTree node directories that only contain other
|
261
260
|
# directories.
|
262
261
|
index_file = File.join(dir_name, 'index')
|
263
|
-
File.
|
262
|
+
File.exist?(index_file)
|
264
263
|
end
|
265
264
|
|
266
265
|
end
|
data/lib/perobs/Cache.rb
CHANGED
@@ -25,6 +25,7 @@
|
|
25
25
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
26
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
27
|
|
28
|
+
require 'perobs/Log'
|
28
29
|
require 'perobs/Store'
|
29
30
|
|
30
31
|
module PEROBS
|
@@ -55,7 +56,7 @@ module PEROBS
|
|
55
56
|
# to increase performance.
|
56
57
|
if obj.respond_to?(:is_poxreference?)
|
57
58
|
# If this condition triggers, we have a bug in the library.
|
58
|
-
|
59
|
+
PEROBS.log.fatal "POXReference objects should never be cached"
|
59
60
|
end
|
60
61
|
@reads[index(obj)] = obj
|
61
62
|
end
|
@@ -67,7 +68,7 @@ module PEROBS
|
|
67
68
|
# to increase performance.
|
68
69
|
if obj.respond_to?(:is_poxreference?)
|
69
70
|
# If this condition triggers, we have a bug in the library.
|
70
|
-
|
71
|
+
PEROBS.log.fatal "POXReference objects should never be cached"
|
71
72
|
end
|
72
73
|
|
73
74
|
if @transaction_stack.empty?
|
@@ -147,7 +148,7 @@ module PEROBS
|
|
147
148
|
def end_transaction
|
148
149
|
case @transaction_stack.length
|
149
150
|
when 0
|
150
|
-
|
151
|
+
PEROBS.log.fatal 'No ongoing transaction to end'
|
151
152
|
when 1
|
152
153
|
# All transactions completed successfully. Write all modified objects
|
153
154
|
# into the backend storage.
|
@@ -169,7 +170,7 @@ module PEROBS
|
|
169
170
|
# the transaction started.
|
170
171
|
def abort_transaction
|
171
172
|
if @transaction_stack.empty?
|
172
|
-
|
173
|
+
PEROBS.log.fatal 'No ongoing transaction to abort'
|
173
174
|
end
|
174
175
|
@transaction_stack.pop.each do |id|
|
175
176
|
@transaction_objects[id]._restore(@transaction_stack.length)
|
data/lib/perobs/DataBase.rb
CHANGED
@@ -32,6 +32,7 @@ require 'json/add/struct'
|
|
32
32
|
require 'yaml'
|
33
33
|
require 'fileutils'
|
34
34
|
|
35
|
+
require 'perobs/Log'
|
35
36
|
require 'perobs/ObjectBase'
|
36
37
|
|
37
38
|
module PEROBS
|
@@ -39,11 +40,23 @@ module PEROBS
|
|
39
40
|
# Base class for all storage back-ends.
|
40
41
|
class DataBase
|
41
42
|
|
43
|
+
# Create a new DataBase object. This method must be overwritten by the
|
44
|
+
# deriving classes and then called via their constructor.
|
42
45
|
def initialize(serializer = :json)
|
43
46
|
@serializer = serializer
|
44
47
|
@config = {}
|
45
48
|
end
|
46
49
|
|
50
|
+
# A dummy open method. Deriving classes must overload them to insert their
|
51
|
+
# open/close semantics.
|
52
|
+
def open
|
53
|
+
end
|
54
|
+
|
55
|
+
# A dummy close method. Deriving classes must overload them to insert their
|
56
|
+
# open/close semantics.
|
57
|
+
def close
|
58
|
+
end
|
59
|
+
|
47
60
|
# Serialize the given object using the object serializer.
|
48
61
|
# @param obj [ObjectBase] Object to serialize
|
49
62
|
# @return [String] Serialized version
|
@@ -58,8 +71,8 @@ module PEROBS
|
|
58
71
|
YAML.dump(obj)
|
59
72
|
end
|
60
73
|
rescue => e
|
61
|
-
|
62
|
-
|
74
|
+
PEROBS.log.fatal "Cannot serialize object as #{@serializer}: " +
|
75
|
+
e.message
|
63
76
|
end
|
64
77
|
end
|
65
78
|
|
@@ -77,9 +90,8 @@ module PEROBS
|
|
77
90
|
YAML.load(raw)
|
78
91
|
end
|
79
92
|
rescue => e
|
80
|
-
|
81
|
-
|
82
|
-
e.message
|
93
|
+
PEROBS.log.fatal "Cannot de-serialize object with #{@serializer} " +
|
94
|
+
"parser: " + e.message
|
83
95
|
end
|
84
96
|
end
|
85
97
|
|
@@ -106,11 +118,11 @@ module PEROBS
|
|
106
118
|
|
107
119
|
# Ensure that we have a directory to store the DB items.
|
108
120
|
def ensure_dir_exists(dir)
|
109
|
-
unless Dir.
|
121
|
+
unless Dir.exist?(dir)
|
110
122
|
begin
|
111
123
|
Dir.mkdir(dir)
|
112
124
|
rescue IOError => e
|
113
|
-
|
125
|
+
PEROBS.log.fatal "Cannote create DB directory '#{dir}': #{e.message}"
|
114
126
|
end
|
115
127
|
end
|
116
128
|
end
|
data/lib/perobs/DynamoDB.rb
CHANGED
@@ -0,0 +1,189 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = FixedSizeBlobFile.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'perobs/Log'
|
29
|
+
require 'perobs/StackFile'
|
30
|
+
|
31
|
+
module PEROBS
|
32
|
+
|
33
|
+
# This class implements persistent storage space for fixed size data blobs.
|
34
|
+
# The blobs can be stored and retrieved and can be deleted again. The
|
35
|
+
# FixedSizeBlobFile manages the storage of the blobs and free storage
|
36
|
+
# spaces. The files grows and shrinks as needed. A blob is referenced by its
|
37
|
+
# address.
|
38
|
+
class FixedSizeBlobFile
|
39
|
+
|
40
|
+
# Create a new stack file in the given directory with the given file name.
|
41
|
+
# @param dir [String] Directory
|
42
|
+
# @param name [String] File name
|
43
|
+
# @param entry_bytes [Fixnum] Number of bytes each entry must have
|
44
|
+
def initialize(dir, name, entry_bytes)
|
45
|
+
@file_name = File.join(dir, name + '.blobs')
|
46
|
+
@entry_bytes = entry_bytes
|
47
|
+
@free_list = StackFile.new(dir, name + '-freelist', 8)
|
48
|
+
@f = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Open the blob file.
|
52
|
+
def open
|
53
|
+
begin
|
54
|
+
if File.exist?(@file_name)
|
55
|
+
@f = File.open(@file_name, 'rb+')
|
56
|
+
else
|
57
|
+
@f = File.open(@file_name, 'wb+')
|
58
|
+
end
|
59
|
+
rescue IOError => e
|
60
|
+
PEROBS.log.fatal "Cannot open blob file #{@file_name}: #{e.message}"
|
61
|
+
end
|
62
|
+
@free_list.open
|
63
|
+
end
|
64
|
+
|
65
|
+
# Close the blob file. This method must be called before the program is
|
66
|
+
# terminated to avoid data loss.
|
67
|
+
def close
|
68
|
+
@free_list.close
|
69
|
+
begin
|
70
|
+
@f.flush
|
71
|
+
@f.close
|
72
|
+
rescue IOError => e
|
73
|
+
PEROBS.log.fatal "Cannot close blob file #{@file_name}: #{e.message}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Flush out all unwritten data.
|
78
|
+
def sync
|
79
|
+
@free_list.sync
|
80
|
+
begin
|
81
|
+
@f.sync
|
82
|
+
rescue IOError => e
|
83
|
+
PEROBS.log.fatal "Cannot sync blob file #{@file_name}: #{e.message}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Delete all data.
|
88
|
+
def clear
|
89
|
+
@f.truncate(0)
|
90
|
+
@f.flush
|
91
|
+
@free_list.clear
|
92
|
+
end
|
93
|
+
|
94
|
+
def empty?
|
95
|
+
sync
|
96
|
+
@f.size == 0
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return the address of a free blob storage space. Addresses start at 0
|
100
|
+
# and increase linearly.
|
101
|
+
# @return [Fixnum] address of a free blob space
|
102
|
+
def free_address
|
103
|
+
if (bytes = @free_list.pop)
|
104
|
+
# Return an entry from the free list.
|
105
|
+
return bytes.unpack('Q')[0]
|
106
|
+
else
|
107
|
+
# There is currently no free entry. Return the address at the end of
|
108
|
+
# the file.
|
109
|
+
offset_to_address(@f.size)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Store the given byte blob at the specified address. If the blob space is
|
114
|
+
# already in use the content will be overwritten.
|
115
|
+
# @param address [Fixnum] Address to store the blob
|
116
|
+
# @param bytes [String] bytes to store
|
117
|
+
def store_blob(address, bytes)
|
118
|
+
if bytes.length != @entry_bytes
|
119
|
+
PEROBS.log.fatal "All stack entries must be #{@entry_bytes} " +
|
120
|
+
"long. This entry is #{bytes.length} bytes long."
|
121
|
+
end
|
122
|
+
begin
|
123
|
+
@f.seek(address_to_offset(address))
|
124
|
+
# The first byte is tha flag byte. It's set to 1 for cells that hold a
|
125
|
+
# blob. 0 for empty cells.
|
126
|
+
@f.write([ 1 ].pack('C'))
|
127
|
+
@f.write(bytes)
|
128
|
+
@f.flush
|
129
|
+
rescue IOError => e
|
130
|
+
PEROBS.log.fatal "Cannot store blob at address #{address}: #{e.message}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Retrieve a blob from the given address.
|
135
|
+
# @param address [Fixnum] Address to store the blob
|
136
|
+
# @return [String] blob bytes
|
137
|
+
def retrieve_blob(address)
|
138
|
+
begin
|
139
|
+
if (offset = address_to_offset(address)) >= @f.size
|
140
|
+
return nil
|
141
|
+
end
|
142
|
+
|
143
|
+
@f.seek(address_to_offset(address))
|
144
|
+
if (@f.read(1).unpack('C')[0] != 1)
|
145
|
+
return nil
|
146
|
+
end
|
147
|
+
bytes = @f.read(@entry_bytes)
|
148
|
+
rescue IOError => e
|
149
|
+
PEROBS.log.fatal "Cannot retrieve blob at adress #{address}: " +
|
150
|
+
e.message
|
151
|
+
end
|
152
|
+
|
153
|
+
bytes
|
154
|
+
end
|
155
|
+
|
156
|
+
# Delete the blob at the given address.
|
157
|
+
# @param address [Fixnum] Address of blob to delete
|
158
|
+
def delete_blob(address)
|
159
|
+
begin
|
160
|
+
@f.seek(address_to_offset(address))
|
161
|
+
if (@f.read(1).unpack('C')[0] != 1)
|
162
|
+
PEROBS.log.fatal "There is no blob stored at address #{address}"
|
163
|
+
end
|
164
|
+
@f.seek(address_to_offset(address))
|
165
|
+
@f.write([ 0 ].pack('C'))
|
166
|
+
rescue IOError => e
|
167
|
+
PEROBS.log.fatal "Cannot delete blob at address #{address}: " +
|
168
|
+
e.message
|
169
|
+
end
|
170
|
+
# Add the address to the free list.
|
171
|
+
@free_list.push([ address ].pack('Q'))
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
# Translate a blob address to the actual offset in the file.
|
177
|
+
def address_to_offset(address)
|
178
|
+
address * (1 + @entry_bytes)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Translate the file offset to the address of a blob.
|
182
|
+
def offset_to_address(offset)
|
183
|
+
offset / (1 + @entry_bytes)
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|