perobs 4.0.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +27 -16
- data/lib/perobs/Array.rb +66 -19
- data/lib/perobs/BTree.rb +106 -15
- data/lib/perobs/BTreeBlob.rb +4 -3
- data/lib/perobs/BTreeDB.rb +5 -4
- data/lib/perobs/BTreeNode.rb +482 -156
- data/lib/perobs/BTreeNodeLink.rb +10 -0
- data/lib/perobs/BigArray.rb +285 -0
- data/lib/perobs/BigArrayNode.rb +1002 -0
- data/lib/perobs/BigHash.rb +246 -0
- data/lib/perobs/BigTree.rb +197 -0
- data/lib/perobs/BigTreeNode.rb +873 -0
- data/lib/perobs/Cache.rb +48 -10
- data/lib/perobs/ConsoleProgressMeter.rb +61 -0
- data/lib/perobs/DataBase.rb +4 -3
- data/lib/perobs/DynamoDB.rb +57 -15
- data/lib/perobs/EquiBlobsFile.rb +155 -50
- data/lib/perobs/FNV_Hash_1a_64.rb +54 -0
- data/lib/perobs/FlatFile.rb +519 -227
- data/lib/perobs/FlatFileBlobHeader.rb +113 -54
- data/lib/perobs/FlatFileDB.rb +49 -23
- data/lib/perobs/FuzzyStringMatcher.rb +175 -0
- data/lib/perobs/Hash.rb +127 -33
- data/lib/perobs/IDList.rb +144 -0
- data/lib/perobs/IDListPage.rb +107 -0
- data/lib/perobs/IDListPageFile.rb +180 -0
- data/lib/perobs/IDListPageRecord.rb +142 -0
- data/lib/perobs/Object.rb +18 -15
- data/lib/perobs/ObjectBase.rb +46 -5
- data/lib/perobs/PersistentObjectCache.rb +57 -68
- data/lib/perobs/PersistentObjectCacheLine.rb +24 -12
- data/lib/perobs/ProgressMeter.rb +97 -0
- data/lib/perobs/SpaceManager.rb +273 -0
- data/lib/perobs/SpaceTree.rb +21 -12
- data/lib/perobs/SpaceTreeNode.rb +53 -61
- data/lib/perobs/Store.rb +264 -145
- data/lib/perobs/version.rb +1 -1
- data/lib/perobs.rb +2 -0
- data/perobs.gemspec +4 -4
- data/test/Array_spec.rb +15 -6
- data/test/BTree_spec.rb +6 -2
- data/test/BigArray_spec.rb +261 -0
- data/test/BigHash_spec.rb +152 -0
- data/test/BigTreeNode_spec.rb +153 -0
- data/test/BigTree_spec.rb +259 -0
- data/test/EquiBlobsFile_spec.rb +105 -1
- data/test/FNV_Hash_1a_64_spec.rb +59 -0
- data/test/FlatFileDB_spec.rb +198 -14
- data/test/FuzzyStringMatcher_spec.rb +261 -0
- data/test/Hash_spec.rb +13 -3
- data/test/IDList_spec.rb +77 -0
- data/test/LegacyDBs/LegacyDB.rb +155 -0
- data/test/LegacyDBs/version_3/class_map.json +1 -0
- data/test/LegacyDBs/version_3/config.json +1 -0
- data/test/LegacyDBs/version_3/database.blobs +0 -0
- data/test/LegacyDBs/version_3/database_spaces.blobs +0 -0
- data/test/LegacyDBs/version_3/index.blobs +0 -0
- data/test/LegacyDBs/version_3/version +1 -0
- data/test/LockFile_spec.rb +9 -6
- data/test/SpaceManager_spec.rb +176 -0
- data/test/SpaceTree_spec.rb +4 -1
- data/test/Store_spec.rb +305 -203
- data/test/spec_helper.rb +9 -4
- metadata +57 -16
- data/lib/perobs/BTreeNodeCache.rb +0 -109
- data/lib/perobs/TreeDB.rb +0 -277
data/lib/perobs/Cache.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# = Cache.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
|
5
|
+
# Copyright (c) 2015, 2016, 2019 by Chris Schlaeger <chris@taskjuggler.org>
|
6
6
|
#
|
7
7
|
# MIT License
|
8
8
|
#
|
@@ -66,10 +66,10 @@ module PEROBS
|
|
66
66
|
def cache_write(obj)
|
67
67
|
# This is just a safety check. It can probably be disabled in the future
|
68
68
|
# to increase performance.
|
69
|
-
if obj.respond_to?(:is_poxreference?)
|
70
|
-
|
71
|
-
|
72
|
-
end
|
69
|
+
#if obj.respond_to?(:is_poxreference?)
|
70
|
+
# # If this condition triggers, we have a bug in the library.
|
71
|
+
# PEROBS.log.fatal "POXReference objects should never be cached"
|
72
|
+
#end
|
73
73
|
|
74
74
|
if @transaction_stack.empty?
|
75
75
|
# We are not in transaction mode.
|
@@ -93,16 +93,54 @@ module PEROBS
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
-
#
|
96
|
+
# Evict the object with the given ID from the cache.
|
97
97
|
# @param id [Integer] ID of the cached PEROBS::ObjectBase
|
98
|
-
|
98
|
+
# @return [True/False] True if object was stored in the cache. False
|
99
|
+
# otherwise.
|
100
|
+
def evict(id)
|
101
|
+
unless @transaction_stack.empty?
|
102
|
+
PEROBS.log.fatal "You cannot evict entries during a transaction."
|
103
|
+
end
|
104
|
+
|
99
105
|
idx = id & @mask
|
100
106
|
# The index is just a hash. We still need to check if the object IDs are
|
101
107
|
# actually the same before we can return the object.
|
102
108
|
if (obj = @writes[idx]) && obj._id == id
|
103
|
-
# The object
|
104
|
-
|
109
|
+
# The object is in the write cache.
|
110
|
+
@writes[idx] = nil
|
111
|
+
return true
|
105
112
|
elsif (obj = @reads[idx]) && obj._id == id
|
113
|
+
# The object is in the read cache.
|
114
|
+
@reads[idx] = nil
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
|
118
|
+
false
|
119
|
+
end
|
120
|
+
|
121
|
+
# Return the PEROBS::Object with the specified ID or nil if not found.
|
122
|
+
# @param id [Integer] ID of the cached PEROBS::ObjectBase
|
123
|
+
def object_by_id(id)
|
124
|
+
idx = id & @mask
|
125
|
+
|
126
|
+
if @transaction_stack.empty?
|
127
|
+
# The index is just a hash. We still need to check if the object IDs are
|
128
|
+
# actually the same before we can return the object.
|
129
|
+
if (obj = @writes[idx]) && obj._id == id
|
130
|
+
# The object was in the write cache.
|
131
|
+
return obj
|
132
|
+
end
|
133
|
+
else
|
134
|
+
# During transactions, the read cache is used to provide fast access
|
135
|
+
# to modified objects. But it does not store all modified objects
|
136
|
+
# since there can be hash collisions. So we also have to check all
|
137
|
+
# transaction objects first.
|
138
|
+
if (obj = @transaction_objects[id])
|
139
|
+
return obj
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
if (obj = @reads[idx]) && obj._id == id
|
106
144
|
# The object was in the read cache.
|
107
145
|
return obj
|
108
146
|
end
|
@@ -160,7 +198,7 @@ module PEROBS
|
|
160
198
|
transactions = @transaction_stack.pop
|
161
199
|
# Merge the two lists
|
162
200
|
@transaction_stack.push(@transaction_stack.pop + transactions)
|
163
|
-
# Ensure that each object is only included once in the list.
|
201
|
+
# Ensure that each object ID is only included once in the list.
|
164
202
|
@transaction_stack.last.uniq!
|
165
203
|
end
|
166
204
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = ConsoleProgressMeter.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2018 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/ProgressMeter'
|
29
|
+
|
30
|
+
module PEROBS
|
31
|
+
|
32
|
+
class ConsoleProgressMeter < ProgressMeter
|
33
|
+
|
34
|
+
LINE_LENGTH = 79
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def print_bar
|
39
|
+
percent = @max_value == 0 ? 100.0 :
|
40
|
+
(@current_value.to_f / @max_value) * 100.0
|
41
|
+
percent = 0.0 if percent < 0
|
42
|
+
percent = 100.0 if percent > 100.0
|
43
|
+
|
44
|
+
meter = "<#{percent.to_i}%>"
|
45
|
+
|
46
|
+
bar_length = LINE_LENGTH - @name.chars.length - 3 - meter.chars.length
|
47
|
+
left_bar = '*' * (bar_length * percent / 100.0)
|
48
|
+
right_bar = ' ' * (bar_length - left_bar.chars.length)
|
49
|
+
|
50
|
+
print "\r#{@name} [#{left_bar}#{meter}#{right_bar}]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def print_time
|
54
|
+
s = "\r#{@name} [#{secsToHMS(@end_time - @start_time)}]"
|
55
|
+
puts s + (' ' * (LINE_LENGTH - s.chars.length + 1))
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
data/lib/perobs/DataBase.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# = DataBase.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
5
|
+
# Copyright (c) 2015, 2018, 2019 by Chris Schlaeger <chris@taskjuggler.org>
|
6
6
|
#
|
7
7
|
# MIT License
|
8
8
|
#
|
@@ -42,8 +42,9 @@ module PEROBS
|
|
42
42
|
|
43
43
|
# Create a new DataBase object. This method must be overwritten by the
|
44
44
|
# deriving classes and then called via their constructor.
|
45
|
-
def initialize(
|
46
|
-
@serializer = serializer
|
45
|
+
def initialize(options)
|
46
|
+
@serializer = options[:serializer] || :json
|
47
|
+
@progressmeter = options[:progressmeter] || ProgressMeter.new
|
47
48
|
@config = {}
|
48
49
|
end
|
49
50
|
|
data/lib/perobs/DynamoDB.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
#
|
3
3
|
# = DynamoDB.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2015, 2016
|
5
|
+
# Copyright (c) 2015, 2016, 2017, 2018
|
6
|
+
# by Chris Schlaeger <chris@taskjuggler.org>
|
6
7
|
#
|
7
8
|
# MIT License
|
8
9
|
#
|
@@ -25,7 +26,7 @@
|
|
25
26
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
27
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
28
|
|
28
|
-
require 'aws-sdk'
|
29
|
+
require 'aws-sdk-dynamodb'
|
29
30
|
|
30
31
|
require 'perobs/DataBase'
|
31
32
|
require 'perobs/BTreeBlob'
|
@@ -35,6 +36,10 @@ module PEROBS
|
|
35
36
|
# This class implements an Amazon DynamoDB storage engine for PEROBS.
|
36
37
|
class DynamoDB < DataBase
|
37
38
|
|
39
|
+
INTERNAL_ITEMS = %w( config item_counter )
|
40
|
+
|
41
|
+
attr_reader :item_counter
|
42
|
+
|
38
43
|
# Create a new DynamoDB object.
|
39
44
|
# @param db_name [String] name of the DB directory
|
40
45
|
# @param options [Hash] options to customize the behavior. Currently only
|
@@ -50,7 +55,7 @@ module PEROBS
|
|
50
55
|
options[:serializer] = :yaml
|
51
56
|
end
|
52
57
|
|
53
|
-
super(options
|
58
|
+
super(options)
|
54
59
|
|
55
60
|
if options.include?(:aws_id) && options.include?(:aws_key)
|
56
61
|
Aws.config[:credentials] = Aws::Credentials.new(options[:aws_id],
|
@@ -62,12 +67,25 @@ module PEROBS
|
|
62
67
|
|
63
68
|
@dynamodb = Aws::DynamoDB::Client.new
|
64
69
|
@table_name = db_name
|
65
|
-
|
70
|
+
@config = nil
|
71
|
+
# The number of items currently stored in the DB.
|
72
|
+
@item_counter = nil
|
73
|
+
if create_table(@table_name)
|
74
|
+
@config = { 'serializer' => @serializer }
|
75
|
+
put_hash('config', @config)
|
76
|
+
@item_counter = 0
|
77
|
+
dynamo_put_item('item_counter', @item_counter.to_s)
|
78
|
+
else
|
79
|
+
@config = get_hash('config')
|
80
|
+
if @config['serializer'] != @serializer
|
81
|
+
raise ArgumentError, "DynamoDB #{@table_name} was created with " +
|
82
|
+
"serializer #{@config['serializer']} but was now opened with " +
|
83
|
+
"serializer #{@serializer}."
|
84
|
+
end
|
85
|
+
@item_counter = dynamo_get_item('item_counter').to_i
|
86
|
+
end
|
66
87
|
|
67
88
|
# Read the existing DB config.
|
68
|
-
@config = get_hash('config')
|
69
|
-
check_option('serializer')
|
70
|
-
put_hash('config', @config)
|
71
89
|
end
|
72
90
|
|
73
91
|
# Delete the entire database. The database is no longer usable after this
|
@@ -112,6 +130,13 @@ module PEROBS
|
|
112
130
|
# Store the given object into the cluster files.
|
113
131
|
# @param obj [Hash] Object as defined by PEROBS::ObjectBase
|
114
132
|
def put_object(obj, id)
|
133
|
+
id_str = id.to_s
|
134
|
+
unless dynamo_get_item(id_str)
|
135
|
+
# The is no object with this ID yet. Increase the item counter.
|
136
|
+
@item_counter += 1
|
137
|
+
dynamo_put_item('item_counter', @item_counter.to_s)
|
138
|
+
end
|
139
|
+
|
115
140
|
dynamo_put_item(id.to_s, serialize(obj))
|
116
141
|
end
|
117
142
|
|
@@ -128,23 +153,23 @@ module PEROBS
|
|
128
153
|
each_item do |id|
|
129
154
|
dynamo_mark_item(id, false)
|
130
155
|
end
|
131
|
-
# Mark the 'config' item so it will not get deleted.
|
132
|
-
dynamo_mark_item('config')
|
133
156
|
end
|
134
157
|
|
135
158
|
# Permanently delete all objects that have not been marked. Those are
|
136
159
|
# orphaned and are no longer referenced by any actively used object.
|
137
|
-
# @return [
|
160
|
+
# @return [Integer] Count of the deleted objects.
|
138
161
|
def delete_unmarked_objects
|
139
|
-
|
162
|
+
deleted_objects_count = 0
|
140
163
|
each_item do |id|
|
141
164
|
unless dynamo_is_marked?(id)
|
142
165
|
dynamo_delete_item(id)
|
143
|
-
|
166
|
+
deleted_objects_count += 1
|
167
|
+
@item_counter -= 1
|
144
168
|
end
|
145
169
|
end
|
170
|
+
dynamo_put_item('item_counter', @item_counter.to_s)
|
146
171
|
|
147
|
-
|
172
|
+
deleted_objects_count
|
148
173
|
end
|
149
174
|
|
150
175
|
# Mark an object.
|
@@ -163,7 +188,17 @@ module PEROBS
|
|
163
188
|
# @param repair [TrueClass/FalseClass] True if found errors should be
|
164
189
|
# repaired.
|
165
190
|
def check_db(repair = false)
|
166
|
-
|
191
|
+
unless (item_counter = dynamo_get_item('item_counter')) &&
|
192
|
+
item_counter == @item_counter
|
193
|
+
PEROBS.log.error "@item_counter variable (#{@item_counter}) and " +
|
194
|
+
"item_counter table entry (#{item_counter}) don't match"
|
195
|
+
end
|
196
|
+
item_counter = 0
|
197
|
+
each_item { item_counter += 1 }
|
198
|
+
unless item_counter == @item_counter
|
199
|
+
PEROBS.log.error "Table contains #{item_counter} items but " +
|
200
|
+
"@item_counter is #{@item_counter}"
|
201
|
+
end
|
167
202
|
end
|
168
203
|
|
169
204
|
# Check if the stored object is syntactically correct.
|
@@ -185,9 +220,11 @@ module PEROBS
|
|
185
220
|
|
186
221
|
private
|
187
222
|
|
188
|
-
def
|
223
|
+
def create_table(table_name)
|
189
224
|
begin
|
190
225
|
@dynamodb.describe_table(:table_name => table_name)
|
226
|
+
# The table exists already. No need to create it.
|
227
|
+
return false
|
191
228
|
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
192
229
|
@dynamodb.create_table(
|
193
230
|
:table_name => table_name,
|
@@ -210,6 +247,8 @@ module PEROBS
|
|
210
247
|
)
|
211
248
|
|
212
249
|
@dynamodb.wait_until(:table_exists, table_name: table_name)
|
250
|
+
# The table was successfully created.
|
251
|
+
return true
|
213
252
|
end
|
214
253
|
end
|
215
254
|
|
@@ -251,6 +290,9 @@ module PEROBS
|
|
251
290
|
break if resp.count <= 0
|
252
291
|
|
253
292
|
resp.items.each do |item|
|
293
|
+
# Skip all internal items
|
294
|
+
next if INTERNAL_ITEMS.include?(item['Id'])
|
295
|
+
|
254
296
|
yield(item['Id'])
|
255
297
|
end
|
256
298
|
|
data/lib/perobs/EquiBlobsFile.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
#
|
3
3
|
# = EquiBlobsFile.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2016, 2017
|
5
|
+
# Copyright (c) 2016, 2017, 2018, 2019
|
6
|
+
# by Chris Schlaeger <chris@taskjuggler.org>
|
6
7
|
#
|
7
8
|
# MIT License
|
8
9
|
#
|
@@ -26,6 +27,7 @@
|
|
26
27
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
28
|
|
28
29
|
require 'perobs/Log'
|
30
|
+
require 'perobs/ProgressMeter'
|
29
31
|
|
30
32
|
module PEROBS
|
31
33
|
|
@@ -37,6 +39,11 @@ module PEROBS
|
|
37
39
|
# is used to represent an undefined address or nil. The file has a 4 * 8
|
38
40
|
# bytes long header that stores the total entry count, the total space
|
39
41
|
# count, the offset of the first entry and the offset of the first space.
|
42
|
+
# The header is followed by a custom entry section. Each entry is also 8
|
43
|
+
# bytes long. After the custom entry section the data blobs start. Each data
|
44
|
+
# blob starts with a mark byte that indicates if the blob is valid data (2),
|
45
|
+
# a free space (0) or reseved space (1). Then it is followed by @entry_bytes
|
46
|
+
# number of bytes for the data blob.
|
40
47
|
class EquiBlobsFile
|
41
48
|
|
42
49
|
TOTAL_ENTRIES_OFFSET = 0
|
@@ -50,15 +57,21 @@ module PEROBS
|
|
50
57
|
# Create a new stack file in the given directory with the given file name.
|
51
58
|
# @param dir [String] Directory
|
52
59
|
# @param name [String] File name
|
60
|
+
# @param progressmeter [ProgressMeter] Reference to a progress meter
|
61
|
+
# object
|
53
62
|
# @param entry_bytes [Integer] Number of bytes each entry must have
|
54
63
|
# @param first_entry_default [Integer] Default address of the first blob
|
55
|
-
def initialize(dir, name, entry_bytes,
|
64
|
+
def initialize(dir, name, progressmeter, entry_bytes,
|
65
|
+
first_entry_default = 0)
|
66
|
+
@name = name
|
56
67
|
@file_name = File.join(dir, name + '.blobs')
|
68
|
+
@progressmeter = progressmeter
|
57
69
|
if entry_bytes < 8
|
58
70
|
PEROBS.log.fatal "EquiBlobsFile entry size must be at least 8"
|
59
71
|
end
|
60
72
|
@entry_bytes = entry_bytes
|
61
73
|
@first_entry_default = first_entry_default
|
74
|
+
clear_custom_data
|
62
75
|
reset_counters
|
63
76
|
|
64
77
|
# The File handle.
|
@@ -102,6 +115,57 @@ module PEROBS
|
|
102
115
|
end
|
103
116
|
end
|
104
117
|
|
118
|
+
# In addition to the standard offsets for the first entry and the first
|
119
|
+
# space any number of additional data fields can be registered. This must be
|
120
|
+
# done right after the object is instanciated and before the open() method
|
121
|
+
# is called. Each field represents a 64 bit unsigned integer.
|
122
|
+
# @param name [String] The label for this offset
|
123
|
+
# @param default_value [Integer] The default value for the offset
|
124
|
+
def register_custom_data(name, default_value = 0)
|
125
|
+
if @custom_data_labels.include?(name)
|
126
|
+
PEROBS.log.fatal "Custom data field #{name} has already been registered"
|
127
|
+
end
|
128
|
+
|
129
|
+
@custom_data_labels << name
|
130
|
+
@custom_data_values << default_value
|
131
|
+
@custom_data_defaults << default_value
|
132
|
+
end
|
133
|
+
|
134
|
+
# Reset (delete) all custom data labels that have been registered.
|
135
|
+
def clear_custom_data
|
136
|
+
unless @f.nil?
|
137
|
+
PEROBS.log.fatal "clear_custom_data should only be called when " +
|
138
|
+
"the file is not opened"
|
139
|
+
end
|
140
|
+
|
141
|
+
@custom_data_labels = []
|
142
|
+
@custom_data_values = []
|
143
|
+
@custom_data_defaults = []
|
144
|
+
end
|
145
|
+
|
146
|
+
# Set the registered custom data field to the given value.
|
147
|
+
# @param name [String] Label of the offset
|
148
|
+
# @param value [Integer] Value
|
149
|
+
def set_custom_data(name, value)
|
150
|
+
unless @custom_data_labels.include?(name)
|
151
|
+
PEROBS.log.fatal "Unknown custom data field #{name}"
|
152
|
+
end
|
153
|
+
|
154
|
+
@custom_data_values[@custom_data_labels.index(name)] = value
|
155
|
+
write_header if @f
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get the registered custom data field value.
|
159
|
+
# @param name [String] Label of the offset
|
160
|
+
# @return [Integer] Value of the custom data field
|
161
|
+
def get_custom_data(name)
|
162
|
+
unless @custom_data_labels.include?(name)
|
163
|
+
PEROBS.log.fatal "Unknown custom data field #{name}"
|
164
|
+
end
|
165
|
+
|
166
|
+
@custom_data_values[@custom_data_labels.index(name)]
|
167
|
+
end
|
168
|
+
|
105
169
|
# Erase the backing store. This method should only be called when the file
|
106
170
|
# is not currently open.
|
107
171
|
def erase
|
@@ -285,7 +349,7 @@ module PEROBS
|
|
285
349
|
|
286
350
|
@first_space = offset
|
287
351
|
@total_spaces += 1
|
288
|
-
@total_entries -= 1
|
352
|
+
@total_entries -= 1 unless marker == 1
|
289
353
|
write_header
|
290
354
|
|
291
355
|
if offset == @f.size - 1 - @entry_bytes
|
@@ -298,12 +362,16 @@ module PEROBS
|
|
298
362
|
# Check the file for logical errors.
|
299
363
|
# @return [Boolean] true of file has no errors, false otherwise.
|
300
364
|
def check
|
365
|
+
sync
|
366
|
+
|
301
367
|
return false unless check_spaces
|
302
368
|
return false unless check_entries
|
303
369
|
|
304
|
-
|
305
|
-
|
306
|
-
|
370
|
+
expected_size = address_to_offset(@total_entries + @total_spaces + 1)
|
371
|
+
actual_size = @f.size
|
372
|
+
if actual_size != expected_size
|
373
|
+
PEROBS.log.error "Size mismatch in EquiBlobsFile #{@file_name}. " +
|
374
|
+
"Expected #{expected_size} bytes but found #{actual_size} bytes."
|
307
375
|
return false
|
308
376
|
end
|
309
377
|
|
@@ -326,6 +394,9 @@ module PEROBS
|
|
326
394
|
@first_entry = @first_entry_default
|
327
395
|
# The file offset of the first empty entry.
|
328
396
|
@first_space = 0
|
397
|
+
|
398
|
+
# Copy default custom values
|
399
|
+
@custom_data_values = @custom_data_defaults.dup
|
329
400
|
end
|
330
401
|
|
331
402
|
def read_header
|
@@ -333,6 +404,12 @@ module PEROBS
|
|
333
404
|
@f.seek(0)
|
334
405
|
@total_entries, @total_spaces, @first_entry, @first_space =
|
335
406
|
@f.read(HEADER_SIZE).unpack('QQQQ')
|
407
|
+
custom_labels_count = @custom_data_labels.length
|
408
|
+
if custom_labels_count > 0
|
409
|
+
@custom_data_values =
|
410
|
+
@f.read(custom_labels_count * 8).unpack("Q#{custom_labels_count}")
|
411
|
+
end
|
412
|
+
|
336
413
|
rescue IOError => e
|
337
414
|
PEROBS.log.fatal "Cannot read EquiBlobsFile header: #{e.message}"
|
338
415
|
end
|
@@ -343,7 +420,13 @@ module PEROBS
|
|
343
420
|
begin
|
344
421
|
@f.seek(0)
|
345
422
|
@f.write(header_ary.pack('QQQQ'))
|
423
|
+
unless @custom_data_values.empty?
|
424
|
+
@f.write(@custom_data_values.
|
425
|
+
pack("Q#{@custom_data_values.length}"))
|
426
|
+
end
|
346
427
|
@f.flush
|
428
|
+
rescue IOError => e
|
429
|
+
PEROBS.log.fatal "Cannot write EquiBlobsFile header: " + e.message
|
347
430
|
end
|
348
431
|
end
|
349
432
|
|
@@ -367,25 +450,31 @@ module PEROBS
|
|
367
450
|
return false
|
368
451
|
end
|
369
452
|
|
453
|
+
return true if next_offset == 0
|
454
|
+
|
370
455
|
total_spaces = 0
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
456
|
+
@progressmeter.start("Checking #{@name} spaces list",
|
457
|
+
@total_spaces) do |pm|
|
458
|
+
begin
|
459
|
+
while next_offset != 0
|
460
|
+
# Check that the marker byte is 0
|
461
|
+
@f.seek(next_offset)
|
462
|
+
if (marker = read_char) != 0
|
463
|
+
PEROBS.log.error "Marker byte at address " +
|
464
|
+
"#{offset_to_address(next_offset)} is #{marker} instead of 0."
|
465
|
+
return false
|
466
|
+
end
|
467
|
+
# Read offset of next empty space
|
468
|
+
next_offset = read_unsigned_int
|
382
469
|
|
383
|
-
|
470
|
+
total_spaces += 1
|
471
|
+
pm.update(total_spaces)
|
472
|
+
end
|
473
|
+
rescue IOError => e
|
474
|
+
PEROBS.log.error "Cannot check space list of EquiBlobsFile " +
|
475
|
+
"#{@file_name}: #{e.message}"
|
476
|
+
return false
|
384
477
|
end
|
385
|
-
rescue IOError => e
|
386
|
-
PEROBS.log.error "Cannot check space list of EquiBlobsFile " +
|
387
|
-
"#{@file_name}: #{e.message}"
|
388
|
-
return false
|
389
478
|
end
|
390
479
|
|
391
480
|
unless total_spaces == @total_spaces
|
@@ -414,35 +503,48 @@ module PEROBS
|
|
414
503
|
return false
|
415
504
|
end
|
416
505
|
|
417
|
-
next_offset =
|
506
|
+
next_offset = address_to_offset(1)
|
418
507
|
total_entries = 0
|
419
508
|
total_spaces = 0
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
509
|
+
last_entry_is_space = false
|
510
|
+
@progressmeter.start("Checking #{@name} entries",
|
511
|
+
@total_spaces + @total_entries) do |pm|
|
512
|
+
begin
|
513
|
+
@f.seek(next_offset)
|
514
|
+
while !@f.eof
|
515
|
+
marker, bytes = @f.read(1 + @entry_bytes).
|
516
|
+
unpack("C#{1 + @entry_bytes}")
|
517
|
+
case marker
|
518
|
+
when 0
|
519
|
+
total_spaces += 1
|
520
|
+
last_entry_is_space = true
|
521
|
+
when 1
|
522
|
+
PEROBS.log.error "Entry at address " +
|
523
|
+
"#{offset_to_address(next_offset)} in EquiBlobsFile " +
|
524
|
+
"#{@file_name} has reserved marker"
|
525
|
+
return false
|
526
|
+
when 2
|
527
|
+
total_entries += 1
|
528
|
+
last_entry_is_space = false
|
529
|
+
else
|
530
|
+
PEROBS.log.error "Entry at address " +
|
531
|
+
"#{offset_to_address(next_offset)} in EquiBlobsFile " +
|
532
|
+
"#{@file_name} has illegal marker #{marker}"
|
533
|
+
return false
|
534
|
+
end
|
535
|
+
next_offset += 1 + @entry_bytes
|
440
536
|
end
|
441
|
-
|
537
|
+
|
538
|
+
pm.update(total_spaces + total_entries)
|
539
|
+
rescue
|
540
|
+
PEROBS.log.error "Cannot check entries of EquiBlobsFile " +
|
541
|
+
"#{@file_name}: #{e.message}"
|
542
|
+
return false
|
442
543
|
end
|
443
|
-
|
444
|
-
|
445
|
-
|
544
|
+
end
|
545
|
+
|
546
|
+
if last_entry_is_space
|
547
|
+
PEROBS.log.error "EquiBlobsFile #{@file_name} is not properly trimmed"
|
446
548
|
return false
|
447
549
|
end
|
448
550
|
|
@@ -464,7 +566,7 @@ module PEROBS
|
|
464
566
|
|
465
567
|
def trim_file
|
466
568
|
offset = @f.size - 1 - @entry_bytes
|
467
|
-
while offset >=
|
569
|
+
while offset >= address_to_offset(1)
|
468
570
|
@f.seek(offset)
|
469
571
|
begin
|
470
572
|
if (marker = read_char) == 0
|
@@ -548,12 +650,15 @@ module PEROBS
|
|
548
650
|
|
549
651
|
# Translate a blob address to the actual offset in the file.
|
550
652
|
def address_to_offset(address)
|
551
|
-
|
653
|
+
# Since address 0 is illegal, we can use address - 1 as index here.
|
654
|
+
HEADER_SIZE + @custom_data_labels.length * 8 +
|
655
|
+
(address - 1) * (1 + @entry_bytes)
|
552
656
|
end
|
553
657
|
|
554
658
|
# Translate the file offset to the address of a blob.
|
555
659
|
def offset_to_address(offset)
|
556
|
-
(offset - HEADER_SIZE
|
660
|
+
(offset - HEADER_SIZE - @custom_data_labels.length * 8) /
|
661
|
+
(1 + @entry_bytes) + 1
|
557
662
|
end
|
558
663
|
|
559
664
|
def write_char(c)
|