offroad 0.0.2 → 0.0.3
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.
- data/LICENSE +674 -674
- data/README.rdoc +29 -29
- data/Rakefile +75 -75
- data/TODO +42 -42
- data/lib/app/models/offroad/group_state.rb +85 -85
- data/lib/app/models/offroad/mirror_info.rb +53 -53
- data/lib/app/models/offroad/model_state.rb +36 -36
- data/lib/app/models/offroad/received_record_state.rb +115 -115
- data/lib/app/models/offroad/sendable_record_state.rb +91 -91
- data/lib/app/models/offroad/system_state.rb +33 -33
- data/lib/cargo_streamer.rb +222 -222
- data/lib/controller_extensions.rb +74 -74
- data/lib/exceptions.rb +16 -16
- data/lib/migrate/20100512164608_create_offroad_tables.rb +72 -72
- data/lib/mirror_data.rb +376 -376
- data/lib/model_extensions.rb +378 -377
- data/lib/module_funcs.rb +94 -94
- data/lib/offroad.rb +41 -41
- data/lib/version.rb +3 -3
- data/lib/view_helper.rb +7 -7
- data/templates/offline.rb +36 -36
- data/templates/offline_database.yml +7 -7
- data/templates/offroad.yml +6 -6
- data/test/app_root/app/controllers/application_controller.rb +2 -2
- data/test/app_root/app/controllers/group_controller.rb +28 -28
- data/test/app_root/app/models/global_record.rb +10 -10
- data/test/app_root/app/models/group.rb +12 -12
- data/test/app_root/app/models/group_owned_record.rb +68 -68
- data/test/app_root/app/models/guest.rb +7 -7
- data/test/app_root/app/models/subrecord.rb +12 -12
- data/test/app_root/app/models/unmirrored_record.rb +4 -4
- data/test/app_root/app/views/group/download_down_mirror.html.erb +3 -3
- data/test/app_root/app/views/group/download_initial_down_mirror.html.erb +3 -3
- data/test/app_root/app/views/group/download_up_mirror.html.erb +5 -5
- data/test/app_root/app/views/layouts/mirror.html.erb +8 -8
- data/test/app_root/config/boot.rb +115 -115
- data/test/app_root/config/database-pg.yml +8 -8
- data/test/app_root/config/database.yml +5 -5
- data/test/app_root/config/environment.rb +24 -24
- data/test/app_root/config/environments/test.rb +17 -17
- data/test/app_root/config/offroad.yml +6 -6
- data/test/app_root/config/routes.rb +4 -4
- data/test/app_root/db/migrate/20100529235049_create_tables.rb +64 -64
- data/test/app_root/lib/common_hobo.rb +15 -15
- data/test/app_root/vendor/plugins/offroad/init.rb +2 -2
- data/test/functional/mirror_operations_test.rb +148 -148
- data/test/test_helper.rb +453 -453
- data/test/unit/app_state_tracking_test.rb +275 -275
- data/test/unit/cargo_streamer_test.rb +332 -332
- data/test/unit/global_data_test.rb +102 -102
- data/test/unit/group_controller_test.rb +152 -152
- data/test/unit/group_data_test.rb +442 -435
- data/test/unit/group_single_test.rb +136 -136
- data/test/unit/hobo_permissions_test.rb +57 -57
- data/test/unit/mirror_data_test.rb +1283 -1283
- data/test/unit/mirror_info_test.rb +31 -31
- data/test/unit/module_funcs_test.rb +37 -37
- data/test/unit/pathological_model_test.rb +62 -62
- data/test/unit/test_framework_test.rb +86 -86
- data/test/unit/unmirrored_data_test.rb +14 -14
- metadata +6 -8
@@ -1,33 +1,33 @@
|
|
1
|
-
require 'forwardable'
|
2
|
-
|
3
|
-
module Offroad
|
4
|
-
private
|
5
|
-
|
6
|
-
# State of the Offroad-managed app as a whole; there should only be one record in this table
|
7
|
-
# Attributes of that record can be read via the class methods of this class
|
8
|
-
class SystemState < ActiveRecord::Base
|
9
|
-
set_table_name "offroad_system_state"
|
10
|
-
|
11
|
-
# Create validators and class-level attribute getters for the columns that contain system settings
|
12
|
-
extend SingleForwardable
|
13
|
-
for column in columns
|
14
|
-
sym = column.name.to_sym
|
15
|
-
next if sym == :id
|
16
|
-
def_delegator :instance_record, sym
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.increment_mirror_version
|
20
|
-
self.increment_counter(:current_mirror_version, instance_record.id)
|
21
|
-
end
|
22
|
-
|
23
|
-
# Returns the singleton record, first creating it if necessary
|
24
|
-
def self.instance_record
|
25
|
-
sys_state = first
|
26
|
-
if sys_state
|
27
|
-
return sys_state
|
28
|
-
else
|
29
|
-
return create(:current_mirror_version => 1)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Offroad
|
4
|
+
private
|
5
|
+
|
6
|
+
# State of the Offroad-managed app as a whole; there should only be one record in this table
|
7
|
+
# Attributes of that record can be read via the class methods of this class
|
8
|
+
class SystemState < ActiveRecord::Base
|
9
|
+
set_table_name "offroad_system_state"
|
10
|
+
|
11
|
+
# Create validators and class-level attribute getters for the columns that contain system settings
|
12
|
+
extend SingleForwardable
|
13
|
+
for column in columns
|
14
|
+
sym = column.name.to_sym
|
15
|
+
next if sym == :id
|
16
|
+
def_delegator :instance_record, sym
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.increment_mirror_version
|
20
|
+
self.increment_counter(:current_mirror_version, instance_record.id)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the singleton record, first creating it if necessary
|
24
|
+
def self.instance_record
|
25
|
+
sys_state = first
|
26
|
+
if sys_state
|
27
|
+
return sys_state
|
28
|
+
else
|
29
|
+
return create(:current_mirror_version => 1)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/cargo_streamer.rb
CHANGED
@@ -1,222 +1,222 @@
|
|
1
|
-
require 'zlib'
|
2
|
-
require 'digest/md5'
|
3
|
-
|
4
|
-
require 'exceptions'
|
5
|
-
|
6
|
-
module Offroad
|
7
|
-
class CargoStreamerError < DataError
|
8
|
-
end
|
9
|
-
|
10
|
-
private
|
11
|
-
|
12
|
-
# Class for encoding data to, and extracting data from, specially-formatted HTML comments which are called "cargo sections".
|
13
|
-
# Each such section has a name, an md5sum for verification, and some base64-encoded zlib-compressed json data.
|
14
|
-
# Multiple cargo sections can have the same name; when the cargo is later read, requests for that name will be yielded each section in turn.
|
15
|
-
# The data must always be in the form of arrays of ActiveRecord, or things that walk sufficiently like ActiveRecord
|
16
|
-
class CargoStreamer
|
17
|
-
# Models which are to be encoded need to have a method safe_to_load_from_cargo_stream? that returns true.
|
18
|
-
|
19
|
-
# Creates a new CargoStreamer on the given stream, which will be used in the given mode (must be "w" or "r").
|
20
|
-
# If the mode is "r", the file is immediately scanned to determine what cargo it contains.
|
21
|
-
def initialize(ioh, mode)
|
22
|
-
raise CargoStreamerError.new("Invalid mode: must be 'w' or 'r'") unless ["w", "r"].include?(mode)
|
23
|
-
@mode = mode
|
24
|
-
|
25
|
-
if ioh.is_a? String
|
26
|
-
raise CargoStreamerError.new("Cannot accept string as ioh in write mode") unless @mode == "r"
|
27
|
-
@ioh = StringIO.new(ioh, "r")
|
28
|
-
else
|
29
|
-
@ioh = ioh
|
30
|
-
end
|
31
|
-
|
32
|
-
scan_for_cargo if @mode == "r"
|
33
|
-
end
|
34
|
-
|
35
|
-
# Writes a cargo section with the given name and value to the IO stream.
|
36
|
-
# Options:
|
37
|
-
# * :human_readable => true - Before writing the cargo section, writes a comment with human-readable data.
|
38
|
-
# * :include => [:assoc, :other_assoc] - Includes these first-level associations in the encoded data
|
39
|
-
def write_cargo_section(name, value, options = {})
|
40
|
-
raise CargoStreamerError.new("Mode must be 'w' to write cargo data") unless @mode == "w"
|
41
|
-
raise CargoStreamerError.new("CargoStreamer section names must be strings") unless name.is_a? String
|
42
|
-
raise CargoStreamerError.new("Invalid cargo name '" + name + "'") unless name == clean_for_html_comment(name)
|
43
|
-
raise CargoStreamerError.new("Cargo name cannot include newlines") if name.include?("\n")
|
44
|
-
raise CargoStreamerError.new("Value must be an array") unless value.is_a? Array
|
45
|
-
[:to_xml, :attributes=, :valid?].each do |message|
|
46
|
-
unless value.all? { |e| e.respond_to? message }
|
47
|
-
raise CargoStreamerError.new("All elements must respond to #{message}")
|
48
|
-
end
|
49
|
-
end
|
50
|
-
unless value.all? { |e| e.class.respond_to?(:safe_to_load_from_cargo_stream?) && e.class.safe_to_load_from_cargo_stream? }
|
51
|
-
raise CargoStreamerError.new("All element classes must be models which are safe_to_load_from_cargo_stream")
|
52
|
-
end
|
53
|
-
|
54
|
-
unless options[:skip_validation]
|
55
|
-
unless value.all?(&:valid?)
|
56
|
-
raise CargoStreamerError.new("All elements must be valid")
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
if options[:human_readable]
|
61
|
-
human_data = value.map{ |rec|
|
62
|
-
rec.attributes.map{ |k, v| "#{k.to_s.titleize}: #{v.to_s}" }.join("\n")
|
63
|
-
}.join("\n\n")
|
64
|
-
@ioh.write "<!--\n"
|
65
|
-
@ioh.write name.titleize + "\n"
|
66
|
-
@ioh.write "\n"
|
67
|
-
@ioh.write clean_for_html_comment(human_data) + "\n"
|
68
|
-
@ioh.write "-->\n"
|
69
|
-
end
|
70
|
-
|
71
|
-
name = name.chomp
|
72
|
-
|
73
|
-
assoc_list = options[:include] || []
|
74
|
-
|
75
|
-
xml = Builder::XmlMarkup.new
|
76
|
-
xml_data = "<records>%s</records>" % value.map {
|
77
|
-
|r| r.to_xml(
|
78
|
-
:skip_instruct => true,
|
79
|
-
:skip_types => true,
|
80
|
-
:root => "record",
|
81
|
-
:indent => 0,
|
82
|
-
:include => assoc_list
|
83
|
-
) do |xml|
|
84
|
-
xml.cargo_streamer_type r.class.name
|
85
|
-
assoc_info = assoc_list.reject{|a| r.send(a) == nil}.map{|a| "#{a.to_s}=#{r.send(a).class.name}"}.join(",")
|
86
|
-
xml.cargo_streamer_includes assoc_info
|
87
|
-
end
|
88
|
-
}.join()
|
89
|
-
deflated_data = Zlib::Deflate::deflate(xml_data)
|
90
|
-
b64_data = Base64.encode64(deflated_data).chomp
|
91
|
-
digest = Digest::MD5::hexdigest(deflated_data).chomp
|
92
|
-
|
93
|
-
@ioh.write CARGO_BEGIN + "\n"
|
94
|
-
@ioh.write name + "\n"
|
95
|
-
@ioh.write digest + "\n"
|
96
|
-
@ioh.write b64_data + "\n"
|
97
|
-
@ioh.write CARGO_END + "\n"
|
98
|
-
end
|
99
|
-
|
100
|
-
# Returns a list of cargo section names available to be read
|
101
|
-
def cargo_section_names
|
102
|
-
return @cargo_locations.keys
|
103
|
-
end
|
104
|
-
|
105
|
-
# Returns true if cargo with a given name is available
|
106
|
-
def has_cargo_named?(name)
|
107
|
-
return @cargo_locations.has_key? name
|
108
|
-
end
|
109
|
-
|
110
|
-
# Reads, verifies, decodes, and returns the first cargo section with a given name
|
111
|
-
def first_cargo_section(name)
|
112
|
-
each_cargo_section(name) do |data|
|
113
|
-
return data
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# Returns the first element from the return value of first_cargo_section
|
118
|
-
def first_cargo_element(name)
|
119
|
-
arr = first_cargo_section(name)
|
120
|
-
return (arr && arr.size > 0) ? arr[0] : nil
|
121
|
-
end
|
122
|
-
|
123
|
-
# Reads, verifies, and decodes each cargo section with a given name, passing each section's decoded data to the block
|
124
|
-
def each_cargo_section(name)
|
125
|
-
raise CargoStreamerError.new("Mode must be 'r' to read cargo data") unless @mode == "r"
|
126
|
-
locations = @cargo_locations[name] or return
|
127
|
-
locations.each do |seek_location|
|
128
|
-
@ioh.seek(seek_location)
|
129
|
-
digest = ""
|
130
|
-
encoded_data = ""
|
131
|
-
@ioh.each_line do |line|
|
132
|
-
line.chomp!
|
133
|
-
if line == CARGO_END
|
134
|
-
break
|
135
|
-
elsif digest == ""
|
136
|
-
digest = line
|
137
|
-
else
|
138
|
-
encoded_data += line
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
yield verify_and_decode_cargo(digest, encoded_data)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
private
|
147
|
-
|
148
|
-
def scan_for_cargo
|
149
|
-
# Key is cargo section name as String, value is array of seek locations to digests for that section
|
150
|
-
@cargo_locations = {}
|
151
|
-
@ioh.rewind
|
152
|
-
|
153
|
-
in_cargo = false
|
154
|
-
found_name = false
|
155
|
-
@ioh.each_line do |line|
|
156
|
-
line.chomp!
|
157
|
-
if in_cargo
|
158
|
-
if line.include? CARGO_END
|
159
|
-
in_cargo = false
|
160
|
-
found_name = false
|
161
|
-
else
|
162
|
-
unless found_name
|
163
|
-
@cargo_locations[line] ||= []
|
164
|
-
@cargo_locations[line] << @ioh.tell
|
165
|
-
found_name = true
|
166
|
-
end
|
167
|
-
end
|
168
|
-
else
|
169
|
-
if line.include? CARGO_BEGIN
|
170
|
-
in_cargo = true
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
raise CargoStreamerError.new("Input contained un-terminated cargo section") unless in_cargo == false
|
175
|
-
|
176
|
-
@ioh.rewind
|
177
|
-
end
|
178
|
-
|
179
|
-
def clean_for_html_comment(s)
|
180
|
-
s.to_s.gsub("--", "__").gsub("<", "[").gsub(">", "]")
|
181
|
-
end
|
182
|
-
|
183
|
-
def compose_record_from_hash(model_class_name, attrs_hash)
|
184
|
-
model_class = model_class_name.constantize
|
185
|
-
raise "Class #{model_class_name} does not have cargo safety method" unless model_class.respond_to? :safe_to_load_from_cargo_stream?
|
186
|
-
raise "Class #{model_class_name} is not safe_to_load_from_cargo_stream" unless model_class.safe_to_load_from_cargo_stream?
|
187
|
-
|
188
|
-
rec = model_class.new
|
189
|
-
rec.send(:attributes=, attrs_hash, false) # No attr_accessible check like this, so all attributes can be set
|
190
|
-
rec.readonly! # rec is just source data for creation of a "real" record; it shouldn't be saveable itself
|
191
|
-
rec
|
192
|
-
end
|
193
|
-
|
194
|
-
def verify_and_decode_cargo(digest, b64_data)
|
195
|
-
deflated_data = Base64.decode64(b64_data)
|
196
|
-
raise "MD5 check failure" unless Digest::MD5::hexdigest(deflated_data) == digest
|
197
|
-
|
198
|
-
# Even though we encoded an Array with Array#to_xml, there is no Array#from_xml
|
199
|
-
# So, we have to use Hash#from_xml
|
200
|
-
records = Hash.from_xml(Zlib::Inflate::inflate(deflated_data))["records"]["record"]
|
201
|
-
raise "Decode failure, unable to find records key" unless records != nil
|
202
|
-
records = [records] unless records.is_a?(Array)
|
203
|
-
return records.map do |attrs_hash|
|
204
|
-
raise "Unable to find record type" unless attrs_hash.has_key?("cargo_streamer_type")
|
205
|
-
class_name = attrs_hash.delete("cargo_streamer_type")
|
206
|
-
|
207
|
-
raise "Unable to find includes list" unless attrs_hash.has_key?("cargo_streamer_includes")
|
208
|
-
(attrs_hash.delete("cargo_streamer_includes") || "").split(",").each do |assoc_info|
|
209
|
-
assoc_name, i_class_name = assoc_info.split("=")
|
210
|
-
attrs_hash[assoc_name] = compose_record_from_hash(i_class_name, attrs_hash[assoc_name])
|
211
|
-
end
|
212
|
-
|
213
|
-
compose_record_from_hash(class_name, attrs_hash)
|
214
|
-
end
|
215
|
-
rescue StandardError => e
|
216
|
-
raise CargoStreamerError.new("Corrupted data : #{e.class.to_s} : #{e.to_s}")
|
217
|
-
end
|
218
|
-
|
219
|
-
CARGO_BEGIN = "<!-- CARGO SEGMENT"
|
220
|
-
CARGO_END = "END CARGO SEGMENT -->"
|
221
|
-
end
|
222
|
-
end
|
1
|
+
require 'zlib'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
require 'exceptions'
|
5
|
+
|
6
|
+
module Offroad
|
7
|
+
class CargoStreamerError < DataError
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Class for encoding data to, and extracting data from, specially-formatted HTML comments which are called "cargo sections".
|
13
|
+
# Each such section has a name, an md5sum for verification, and some base64-encoded zlib-compressed json data.
|
14
|
+
# Multiple cargo sections can have the same name; when the cargo is later read, requests for that name will be yielded each section in turn.
|
15
|
+
# The data must always be in the form of arrays of ActiveRecord, or things that walk sufficiently like ActiveRecord
|
16
|
+
class CargoStreamer
|
17
|
+
# Models which are to be encoded need to have a method safe_to_load_from_cargo_stream? that returns true.
|
18
|
+
|
19
|
+
# Creates a new CargoStreamer on the given stream, which will be used in the given mode (must be "w" or "r").
|
20
|
+
# If the mode is "r", the file is immediately scanned to determine what cargo it contains.
|
21
|
+
def initialize(ioh, mode)
|
22
|
+
raise CargoStreamerError.new("Invalid mode: must be 'w' or 'r'") unless ["w", "r"].include?(mode)
|
23
|
+
@mode = mode
|
24
|
+
|
25
|
+
if ioh.is_a? String
|
26
|
+
raise CargoStreamerError.new("Cannot accept string as ioh in write mode") unless @mode == "r"
|
27
|
+
@ioh = StringIO.new(ioh, "r")
|
28
|
+
else
|
29
|
+
@ioh = ioh
|
30
|
+
end
|
31
|
+
|
32
|
+
scan_for_cargo if @mode == "r"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Writes a cargo section with the given name and value to the IO stream.
|
36
|
+
# Options:
|
37
|
+
# * :human_readable => true - Before writing the cargo section, writes a comment with human-readable data.
|
38
|
+
# * :include => [:assoc, :other_assoc] - Includes these first-level associations in the encoded data
|
39
|
+
def write_cargo_section(name, value, options = {})
|
40
|
+
raise CargoStreamerError.new("Mode must be 'w' to write cargo data") unless @mode == "w"
|
41
|
+
raise CargoStreamerError.new("CargoStreamer section names must be strings") unless name.is_a? String
|
42
|
+
raise CargoStreamerError.new("Invalid cargo name '" + name + "'") unless name == clean_for_html_comment(name)
|
43
|
+
raise CargoStreamerError.new("Cargo name cannot include newlines") if name.include?("\n")
|
44
|
+
raise CargoStreamerError.new("Value must be an array") unless value.is_a? Array
|
45
|
+
[:to_xml, :attributes=, :valid?].each do |message|
|
46
|
+
unless value.all? { |e| e.respond_to? message }
|
47
|
+
raise CargoStreamerError.new("All elements must respond to #{message}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
unless value.all? { |e| e.class.respond_to?(:safe_to_load_from_cargo_stream?) && e.class.safe_to_load_from_cargo_stream? }
|
51
|
+
raise CargoStreamerError.new("All element classes must be models which are safe_to_load_from_cargo_stream")
|
52
|
+
end
|
53
|
+
|
54
|
+
unless options[:skip_validation]
|
55
|
+
unless value.all?(&:valid?)
|
56
|
+
raise CargoStreamerError.new("All elements must be valid")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if options[:human_readable]
|
61
|
+
human_data = value.map{ |rec|
|
62
|
+
rec.attributes.map{ |k, v| "#{k.to_s.titleize}: #{v.to_s}" }.join("\n")
|
63
|
+
}.join("\n\n")
|
64
|
+
@ioh.write "<!--\n"
|
65
|
+
@ioh.write name.titleize + "\n"
|
66
|
+
@ioh.write "\n"
|
67
|
+
@ioh.write clean_for_html_comment(human_data) + "\n"
|
68
|
+
@ioh.write "-->\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
name = name.chomp
|
72
|
+
|
73
|
+
assoc_list = options[:include] || []
|
74
|
+
|
75
|
+
xml = Builder::XmlMarkup.new
|
76
|
+
xml_data = "<records>%s</records>" % value.map {
|
77
|
+
|r| r.to_xml(
|
78
|
+
:skip_instruct => true,
|
79
|
+
:skip_types => true,
|
80
|
+
:root => "record",
|
81
|
+
:indent => 0,
|
82
|
+
:include => assoc_list
|
83
|
+
) do |xml|
|
84
|
+
xml.cargo_streamer_type r.class.name
|
85
|
+
assoc_info = assoc_list.reject{|a| r.send(a) == nil}.map{|a| "#{a.to_s}=#{r.send(a).class.name}"}.join(",")
|
86
|
+
xml.cargo_streamer_includes assoc_info
|
87
|
+
end
|
88
|
+
}.join()
|
89
|
+
deflated_data = Zlib::Deflate::deflate(xml_data)
|
90
|
+
b64_data = Base64.encode64(deflated_data).chomp
|
91
|
+
digest = Digest::MD5::hexdigest(deflated_data).chomp
|
92
|
+
|
93
|
+
@ioh.write CARGO_BEGIN + "\n"
|
94
|
+
@ioh.write name + "\n"
|
95
|
+
@ioh.write digest + "\n"
|
96
|
+
@ioh.write b64_data + "\n"
|
97
|
+
@ioh.write CARGO_END + "\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a list of cargo section names available to be read
|
101
|
+
def cargo_section_names
|
102
|
+
return @cargo_locations.keys
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns true if cargo with a given name is available
|
106
|
+
def has_cargo_named?(name)
|
107
|
+
return @cargo_locations.has_key? name
|
108
|
+
end
|
109
|
+
|
110
|
+
# Reads, verifies, decodes, and returns the first cargo section with a given name
|
111
|
+
def first_cargo_section(name)
|
112
|
+
each_cargo_section(name) do |data|
|
113
|
+
return data
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the first element from the return value of first_cargo_section
|
118
|
+
def first_cargo_element(name)
|
119
|
+
arr = first_cargo_section(name)
|
120
|
+
return (arr && arr.size > 0) ? arr[0] : nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# Reads, verifies, and decodes each cargo section with a given name, passing each section's decoded data to the block
|
124
|
+
def each_cargo_section(name)
|
125
|
+
raise CargoStreamerError.new("Mode must be 'r' to read cargo data") unless @mode == "r"
|
126
|
+
locations = @cargo_locations[name] or return
|
127
|
+
locations.each do |seek_location|
|
128
|
+
@ioh.seek(seek_location)
|
129
|
+
digest = ""
|
130
|
+
encoded_data = ""
|
131
|
+
@ioh.each_line do |line|
|
132
|
+
line.chomp!
|
133
|
+
if line == CARGO_END
|
134
|
+
break
|
135
|
+
elsif digest == ""
|
136
|
+
digest = line
|
137
|
+
else
|
138
|
+
encoded_data += line
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
yield verify_and_decode_cargo(digest, encoded_data)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def scan_for_cargo
|
149
|
+
# Key is cargo section name as String, value is array of seek locations to digests for that section
|
150
|
+
@cargo_locations = {}
|
151
|
+
@ioh.rewind
|
152
|
+
|
153
|
+
in_cargo = false
|
154
|
+
found_name = false
|
155
|
+
@ioh.each_line do |line|
|
156
|
+
line.chomp!
|
157
|
+
if in_cargo
|
158
|
+
if line.include? CARGO_END
|
159
|
+
in_cargo = false
|
160
|
+
found_name = false
|
161
|
+
else
|
162
|
+
unless found_name
|
163
|
+
@cargo_locations[line] ||= []
|
164
|
+
@cargo_locations[line] << @ioh.tell
|
165
|
+
found_name = true
|
166
|
+
end
|
167
|
+
end
|
168
|
+
else
|
169
|
+
if line.include? CARGO_BEGIN
|
170
|
+
in_cargo = true
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
raise CargoStreamerError.new("Input contained un-terminated cargo section") unless in_cargo == false
|
175
|
+
|
176
|
+
@ioh.rewind
|
177
|
+
end
|
178
|
+
|
179
|
+
def clean_for_html_comment(s)
|
180
|
+
s.to_s.gsub("--", "__").gsub("<", "[").gsub(">", "]")
|
181
|
+
end
|
182
|
+
|
183
|
+
def compose_record_from_hash(model_class_name, attrs_hash)
|
184
|
+
model_class = model_class_name.constantize
|
185
|
+
raise "Class #{model_class_name} does not have cargo safety method" unless model_class.respond_to? :safe_to_load_from_cargo_stream?
|
186
|
+
raise "Class #{model_class_name} is not safe_to_load_from_cargo_stream" unless model_class.safe_to_load_from_cargo_stream?
|
187
|
+
|
188
|
+
rec = model_class.new
|
189
|
+
rec.send(:attributes=, attrs_hash, false) # No attr_accessible check like this, so all attributes can be set
|
190
|
+
rec.readonly! # rec is just source data for creation of a "real" record; it shouldn't be saveable itself
|
191
|
+
rec
|
192
|
+
end
|
193
|
+
|
194
|
+
def verify_and_decode_cargo(digest, b64_data)
|
195
|
+
deflated_data = Base64.decode64(b64_data)
|
196
|
+
raise "MD5 check failure" unless Digest::MD5::hexdigest(deflated_data) == digest
|
197
|
+
|
198
|
+
# Even though we encoded an Array with Array#to_xml, there is no Array#from_xml
|
199
|
+
# So, we have to use Hash#from_xml
|
200
|
+
records = Hash.from_xml(Zlib::Inflate::inflate(deflated_data))["records"]["record"]
|
201
|
+
raise "Decode failure, unable to find records key" unless records != nil
|
202
|
+
records = [records] unless records.is_a?(Array)
|
203
|
+
return records.map do |attrs_hash|
|
204
|
+
raise "Unable to find record type" unless attrs_hash.has_key?("cargo_streamer_type")
|
205
|
+
class_name = attrs_hash.delete("cargo_streamer_type")
|
206
|
+
|
207
|
+
raise "Unable to find includes list" unless attrs_hash.has_key?("cargo_streamer_includes")
|
208
|
+
(attrs_hash.delete("cargo_streamer_includes") || "").split(",").each do |assoc_info|
|
209
|
+
assoc_name, i_class_name = assoc_info.split("=")
|
210
|
+
attrs_hash[assoc_name] = compose_record_from_hash(i_class_name, attrs_hash[assoc_name])
|
211
|
+
end
|
212
|
+
|
213
|
+
compose_record_from_hash(class_name, attrs_hash)
|
214
|
+
end
|
215
|
+
rescue StandardError => e
|
216
|
+
raise CargoStreamerError.new("Corrupted data : #{e.class.to_s} : #{e.to_s}")
|
217
|
+
end
|
218
|
+
|
219
|
+
CARGO_BEGIN = "<!-- CARGO SEGMENT"
|
220
|
+
CARGO_END = "END CARGO SEGMENT -->"
|
221
|
+
end
|
222
|
+
end
|