offroad 0.0.1
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 -0
- data/README.rdoc +29 -0
- data/Rakefile +75 -0
- data/TODO +42 -0
- data/lib/app/models/offroad/group_state.rb +85 -0
- data/lib/app/models/offroad/mirror_info.rb +53 -0
- data/lib/app/models/offroad/model_state.rb +36 -0
- data/lib/app/models/offroad/received_record_state.rb +109 -0
- data/lib/app/models/offroad/sendable_record_state.rb +91 -0
- data/lib/app/models/offroad/system_state.rb +33 -0
- data/lib/cargo_streamer.rb +222 -0
- data/lib/controller_extensions.rb +74 -0
- data/lib/exceptions.rb +16 -0
- data/lib/migrate/20100512164608_create_offroad_tables.rb +72 -0
- data/lib/mirror_data.rb +354 -0
- data/lib/model_extensions.rb +377 -0
- data/lib/module_funcs.rb +94 -0
- data/lib/offroad.rb +30 -0
- data/lib/version.rb +3 -0
- data/lib/view_helper.rb +7 -0
- data/templates/offline.rb +36 -0
- data/templates/offline_database.yml +7 -0
- data/templates/offroad.yml +6 -0
- data/test/app_root/app/controllers/application_controller.rb +2 -0
- data/test/app_root/app/controllers/group_controller.rb +28 -0
- data/test/app_root/app/models/global_record.rb +10 -0
- data/test/app_root/app/models/group.rb +12 -0
- data/test/app_root/app/models/group_owned_record.rb +68 -0
- data/test/app_root/app/models/guest.rb +7 -0
- data/test/app_root/app/models/subrecord.rb +12 -0
- data/test/app_root/app/models/unmirrored_record.rb +4 -0
- data/test/app_root/app/views/group/download_down_mirror.html.erb +4 -0
- data/test/app_root/app/views/group/download_initial_down_mirror.html.erb +4 -0
- data/test/app_root/app/views/group/download_up_mirror.html.erb +6 -0
- data/test/app_root/app/views/group/upload_down_mirror.html.erb +1 -0
- data/test/app_root/app/views/group/upload_up_mirror.html.erb +1 -0
- data/test/app_root/app/views/layouts/mirror.html.erb +9 -0
- data/test/app_root/config/boot.rb +115 -0
- data/test/app_root/config/database.yml +6 -0
- data/test/app_root/config/environment.rb +15 -0
- data/test/app_root/config/environments/test.rb +17 -0
- data/test/app_root/config/offroad.yml +6 -0
- data/test/app_root/config/routes.rb +4 -0
- data/test/app_root/db/migrate/20100529235049_create_tables.rb +64 -0
- data/test/app_root/lib/common_hobo.rb +15 -0
- data/test/app_root/vendor/plugins/offroad/init.rb +2 -0
- data/test/functional/mirror_operations_test.rb +148 -0
- data/test/test_helper.rb +405 -0
- data/test/unit/app_state_tracking_test.rb +275 -0
- data/test/unit/cargo_streamer_test.rb +332 -0
- data/test/unit/global_data_test.rb +102 -0
- data/test/unit/group_controller_test.rb +152 -0
- data/test/unit/group_data_test.rb +435 -0
- data/test/unit/group_single_test.rb +136 -0
- data/test/unit/hobo_permissions_test.rb +57 -0
- data/test/unit/mirror_data_test.rb +1271 -0
- data/test/unit/mirror_info_test.rb +31 -0
- data/test/unit/module_funcs_test.rb +37 -0
- data/test/unit/pathological_model_test.rb +62 -0
- data/test/unit/test_framework_test.rb +86 -0
- data/test/unit/unmirrored_data_test.rb +14 -0
- metadata +140 -0
data/README.rdoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
*NOTE*: This gem is a work in progress. It does work, but it's not especially convenient, yet.
|
2
|
+
Better documentation and scripts will follow in time.
|
3
|
+
|
4
|
+
*NOTE*: Currently the gem is only compatible with Rails 2.
|
5
|
+
|
6
|
+
== Overview
|
7
|
+
|
8
|
+
This gem allows users without Internet access to still use your app. It requires that your application can
|
9
|
+
be thought of in the following way:
|
10
|
+
|
11
|
+
Your site is broken down into a set of "groups" or "organizations". Each group has their own distinct set of
|
12
|
+
records, which are editable by only a distinct set of a users. Besides that, there are global records that can
|
13
|
+
only be edited by overall site administrators, though they can possibly be viewed by regular users.
|
14
|
+
|
15
|
+
In order to make offline use of your app possible, Offroad will generate an installer bundle, which is an
|
16
|
+
OS-specific executable that can install Ruby, Rails, and also a launcher which controls a local instance of
|
17
|
+
WEBrick and manages imports and exports. The installer and launcher will be visually labelled everywhere with
|
18
|
+
the name of the Rails app itself, making its function obvious to users. This launcher can generate up-mirror
|
19
|
+
files, which are transported on a USB thumbdrive or some other medium to an online system, where they can be
|
20
|
+
uploaded to the main app site. After having done an upload, the user is prompted to download a down-mirror
|
21
|
+
file which contains any changes to the global records, and bring this back to the offline system, which
|
22
|
+
completes the sneakernet round-trip.
|
23
|
+
|
24
|
+
Rather than sending all data, only globally accessible records and updates to the app itself are transferred
|
25
|
+
in the down-mirror (except for the first down-mirror, which also contains the initial set of records for the
|
26
|
+
group). Furthermore, the offline version of the app is considered be the absolutely canonical source of that
|
27
|
+
group's records; really, this is not a "sync", it's a "mirror". This means that no-one on the online site can
|
28
|
+
make changes to an offline group's records, not even the administrator; this is enforced by Offroad at a
|
29
|
+
low level.
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
def common_test_settings(t)
|
8
|
+
t.libs << 'lib'
|
9
|
+
t.libs << 'test'
|
10
|
+
t.pattern = 'test/**/*_test.rb'
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Default: run unit and functional tests.'
|
15
|
+
task :default => :test
|
16
|
+
|
17
|
+
desc 'Test Offroad'
|
18
|
+
Rake::TestTask.new(:test) do |t|
|
19
|
+
common_test_settings(t)
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'Generate documentation for Offroad.'
|
23
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
24
|
+
rdoc.rdoc_dir = 'rdoc'
|
25
|
+
rdoc.title = 'Offroad'
|
26
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
27
|
+
rdoc.rdoc_files.include('README')
|
28
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
require 'rcov/rcovtask'
|
33
|
+
|
34
|
+
Rcov::RcovTask.new(:rcov) do |t|
|
35
|
+
common_test_settings(t)
|
36
|
+
t.pattern = 'test/unit/*_test.rb' # Don't care about coverage added by functional tests
|
37
|
+
t.rcov_opts << '-o coverage -x "/ruby/,/gems/,/test/,/migrate/"'
|
38
|
+
end
|
39
|
+
rescue LoadError
|
40
|
+
# Rcov wasn't available
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
require 'ruby-prof/task'
|
45
|
+
|
46
|
+
RubyProf::ProfileTask.new(:profile) do |t|
|
47
|
+
common_test_settings(t)
|
48
|
+
t.output_dir = "#{File.dirname(__FILE__)}/profile"
|
49
|
+
t.printer = :call_tree
|
50
|
+
t.min_percent = 10
|
51
|
+
end
|
52
|
+
rescue LoadError
|
53
|
+
# Ruby-prof wasn't available
|
54
|
+
end
|
55
|
+
|
56
|
+
require 'lib/version'
|
57
|
+
gemspec = Gem::Specification.new do |s|
|
58
|
+
s.name = "offroad"
|
59
|
+
s.version = Offroad::VERSION
|
60
|
+
s.authors = ["David Mike Simon"]
|
61
|
+
s.email = "david.mike.simon@gmail.com"
|
62
|
+
s.homepage = "http://github.com/DavidMikeSimon/offroad"
|
63
|
+
s.summary = "Manages off-Internet instances of a Rails app"
|
64
|
+
s.description = "Offroad manages offline instances of a Rails app on computers without Internet access. The online and offline instances can communicate via mirror files, transported by the user via thumbdrive, burned CD, etc."
|
65
|
+
|
66
|
+
s.files = `git ls-files .`.split("\n") - [".gitignore"]
|
67
|
+
s.platform = Gem::Platform::RUBY
|
68
|
+
s.require_path = 'lib'
|
69
|
+
s.rubyforge_project = '[none]'
|
70
|
+
|
71
|
+
s.add_dependency('ar-extensions')
|
72
|
+
end
|
73
|
+
|
74
|
+
Rake::GemPackageTask.new(gemspec) do |pkg|
|
75
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
High priority:
|
2
|
+
- Creating/updating the installer (ideally, git pull on target platform, one rake task, then git push)
|
3
|
+
- Add a generate task to initialize config files in a Rails app
|
4
|
+
- Documentation
|
5
|
+
- Resolve issue of too-long index names
|
6
|
+
|
7
|
+
Low priority:
|
8
|
+
- Make unit tests against bugs fixed by 7f8415 and 31455d
|
9
|
+
- Try to come up with some more convenient way of building unit-test-specific model classes
|
10
|
+
- Make sure ModelStates don't continue to stick around and cause trouble even after a class no longer acts_as_offroadable
|
11
|
+
- Get README into the auto-generated gem rdocs
|
12
|
+
- Get the test suite to stop generating a log file, it's annoying
|
13
|
+
- Use finer-grained exceptions to allow for better error testing
|
14
|
+
- Don't allow group_single records to reference non-single group records, since group_single "ownership" can change
|
15
|
+
- Don't assume "id" as primary key name, always look it up with the appropriate ActiveRecord method
|
16
|
+
- Allow offline app to relinquish ownership by setting a belongs_to field to 0 or NULL
|
17
|
+
- Figure out why Hobo assumes all fields non-editable (something I'm doing in the patched-in updatable check...)
|
18
|
+
- Assign a random number to group state when it is created, only accept mirror files with matching random number
|
19
|
+
- This is to prevent confusion if a group is made offline, then online, then offline again
|
20
|
+
- Also assign a random number to the online app, and only accept mirror files which match this number as well
|
21
|
+
- Prevents multiple installations of an app from reading each others' mirror files
|
22
|
+
- Use read transactions for mirror write operations (is it already doing this?)
|
23
|
+
- The launcher should keep a log file
|
24
|
+
- Include recent log lines (for both Rails and the launcher) in generated up-mirror files, for debugging purposes
|
25
|
+
- Use rails logger to note activity
|
26
|
+
- When mirror data version is confirmed, delete all sendable record state entries for deletions older than min(version) over all group_states
|
27
|
+
- Try to gather some kind of machine identifier to put in a offline-owned group_state column
|
28
|
+
- Try streaming out with large data sets and make sure it is sufficiently speedy, and is actually streaming (i.e. no Content-Length header)
|
29
|
+
- Change mime-type of mirror files being downloaded to something that doesn't allow it to be accidentally viewed in-browser
|
30
|
+
- Use md5sum on the entire cargo file instead of just each individual part, to protect against corruption outside the actual data segments
|
31
|
+
- Allow app to optionally specify batch sizes on a model-by-model basis
|
32
|
+
- Mirror file imports in the down mirror app should happen through GUI, and GUI should delete files once succesfully imported
|
33
|
+
- If offline app attempts to import a down mirror file and it doesn't work, it should:
|
34
|
+
- Delete the down mirror file as usual, but...
|
35
|
+
- Explain that it wasn't imported and that another down mirror file should be downloaded
|
36
|
+
- Figure out which activerecord methods skip the save callbacks, and wrap them to also update the mirror version
|
37
|
+
- Maybe methods for which this isn't practical should raise an error when called...
|
38
|
+
- Load all the other informational values in GroupState
|
39
|
+
- App creates a record then deletes it then sends mirror; does a deletion srs still get sent?
|
40
|
+
- For any fix to this: make sure that if app updates record then deletes it then sends mirror that deletion gets sent
|
41
|
+
- Maybe that means we need both create_version and update_version on srs?
|
42
|
+
- Put the CargoStreamer into its own gem, it might be handy for other stuff
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Offroad
|
2
|
+
private
|
3
|
+
|
4
|
+
class GroupState < ActiveRecord::Base
|
5
|
+
set_table_name "offroad_group_states"
|
6
|
+
|
7
|
+
validates_presence_of :app_group_id
|
8
|
+
|
9
|
+
def validate
|
10
|
+
errors.add_to_base "Cannot find associated app group record" unless app_group
|
11
|
+
end
|
12
|
+
|
13
|
+
def app_group
|
14
|
+
Offroad::group_base_model.find_by_id(app_group_id)
|
15
|
+
end
|
16
|
+
|
17
|
+
has_many :received_record_states, :class_name => "::Offroad::ReceivedRecordState", :dependent => :delete_all
|
18
|
+
|
19
|
+
named_scope :for_group, lambda { |group| { :conditions => {
|
20
|
+
:app_group_id => valid_group_record?(group) ? group.id : 0
|
21
|
+
} } }
|
22
|
+
|
23
|
+
def before_create
|
24
|
+
if Offroad::app_offline?
|
25
|
+
# FIXME : Fill in last_installation_at, launcher_version, app_version, etc
|
26
|
+
self.operating_system ||= RUBY_PLATFORM
|
27
|
+
end
|
28
|
+
|
29
|
+
self.confirmed_group_data_version ||= 1
|
30
|
+
|
31
|
+
# When first setting a group offline at online app, assume it will start out with at least current global data.
|
32
|
+
# It should, since that's the earliest version that could be loaded into the initial down mirror file.
|
33
|
+
self.confirmed_global_data_version ||= Offroad::app_online? ? SystemState::current_mirror_version : 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_from_remote_group_state!(remote_gs)
|
37
|
+
versioning_columns = [
|
38
|
+
'confirmed_global_data_version',
|
39
|
+
'confirmed_group_data_version'
|
40
|
+
]
|
41
|
+
|
42
|
+
online_owned_columns = [
|
43
|
+
'last_installer_downloaded_at',
|
44
|
+
'last_down_mirror_created_at',
|
45
|
+
'last_up_mirror_loaded_at'
|
46
|
+
]
|
47
|
+
|
48
|
+
offline_owned_columns = [
|
49
|
+
'last_installation_at',
|
50
|
+
'last_down_mirror_loaded_at',
|
51
|
+
'last_up_mirror_created_at',
|
52
|
+
'launcher_version',
|
53
|
+
'app_version',
|
54
|
+
'operating_system'
|
55
|
+
]
|
56
|
+
|
57
|
+
# Copy in values from columns owned by the remote environment that created remote_gs
|
58
|
+
(Offroad::app_offline? ? online_owned_columns : offline_owned_columns).each do |col|
|
59
|
+
self.send("#{col}=", remote_gs.send(col))
|
60
|
+
end
|
61
|
+
|
62
|
+
# If the remote side says they have a newer version of something than we currently think they have, update
|
63
|
+
versioning_columns.each do |col|
|
64
|
+
self.send("#{col}=", [self.send(col), remote_gs.send(col)].max)
|
65
|
+
end
|
66
|
+
|
67
|
+
save!
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.safe_to_load_from_cargo_stream?
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.note_group_destroyed(group)
|
75
|
+
rec = find_by_app_group_id(group.id)
|
76
|
+
rec.destroy
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def self.valid_group_record?(rec)
|
82
|
+
rec.class.respond_to?(:offroad_group_base?) && rec.class.offroad_group_base?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Offroad
|
2
|
+
private
|
3
|
+
|
4
|
+
# Non-database model representing general information attached to any mirror file
|
5
|
+
# Based on the pattern found here: http://stackoverflow.com/questions/315850/rails-model-without-database
|
6
|
+
class MirrorInfo < ActiveRecord::Base
|
7
|
+
self.abstract_class = true
|
8
|
+
|
9
|
+
def self.columns
|
10
|
+
@columns ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
[
|
14
|
+
[:created_at, :datetime],
|
15
|
+
[:online_site, :string],
|
16
|
+
[:app, :string],
|
17
|
+
[:app_mode, :string],
|
18
|
+
[:app_version, :string],
|
19
|
+
[:operating_system, :string],
|
20
|
+
[:generator, :string],
|
21
|
+
[:schema_migrations, :string],
|
22
|
+
[:initial_file, :boolean]
|
23
|
+
].each do |attr_name, attr_type|
|
24
|
+
columns << ActiveRecord::ConnectionAdapters::Column.new(attr_name.to_s, nil, attr_type.to_s, true)
|
25
|
+
validates_presence_of attr_name unless attr_type == :boolean
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.safe_to_load_from_cargo_stream?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.new_from_group(group, initial_file = false)
|
33
|
+
mode = Offroad::app_online? ? "online" : "offline"
|
34
|
+
migration_query = "SELECT version FROM schema_migrations ORDER BY version"
|
35
|
+
migrations = Offroad::group_base_model.connection.select_all(migration_query).map{ |r| r["version"] }
|
36
|
+
return MirrorInfo.new(
|
37
|
+
:created_at => Time.now.to_s,
|
38
|
+
:online_site => Offroad::online_url,
|
39
|
+
:app => Offroad::app_name,
|
40
|
+
:app_mode => mode.titleize,
|
41
|
+
:app_version => Offroad::app_version,
|
42
|
+
:operating_system => RUBY_PLATFORM,
|
43
|
+
:generator => "Offroad " + Offroad::VERSION,
|
44
|
+
:schema_migrations => migrations.join(","),
|
45
|
+
:initial_file => initial_file
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def save
|
50
|
+
raise DataError.new("Cannot save MirrorInfo records")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Offroad
|
2
|
+
private
|
3
|
+
|
4
|
+
class ModelState < ActiveRecord::Base
|
5
|
+
set_table_name "offroad_model_states"
|
6
|
+
|
7
|
+
validates_presence_of :app_model_name
|
8
|
+
|
9
|
+
def validate
|
10
|
+
model = nil
|
11
|
+
begin
|
12
|
+
model = app_model
|
13
|
+
rescue NameError
|
14
|
+
errors.add_to_base "Given model name does not correspond to a constant"
|
15
|
+
end
|
16
|
+
|
17
|
+
if model
|
18
|
+
errors.add_to_base "Constant is not a mirrored model" unless self.class.valid_model?(model)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
named_scope :for_model, lambda { |model| { :conditions => {
|
23
|
+
:app_model_name => valid_model?(model) ? model.name : nil
|
24
|
+
} } }
|
25
|
+
|
26
|
+
def app_model
|
27
|
+
app_model_name.constantize
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def self.valid_model?(model)
|
33
|
+
model.respond_to?(:acts_as_offroadable?) && model.acts_as_offroadable?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Offroad
|
2
|
+
private
|
3
|
+
|
4
|
+
class ReceivedRecordState < ActiveRecord::Base
|
5
|
+
set_table_name "offroad_received_record_states"
|
6
|
+
|
7
|
+
belongs_to :model_state, :class_name => "::Offroad::ModelState"
|
8
|
+
|
9
|
+
belongs_to :group_state, :class_name => "::Offroad::GroupState"
|
10
|
+
|
11
|
+
def validate
|
12
|
+
unless model_state
|
13
|
+
errors.add_to_base "Cannot find associated model state"
|
14
|
+
return
|
15
|
+
end
|
16
|
+
model = model_state.app_model
|
17
|
+
|
18
|
+
if Offroad::app_offline?
|
19
|
+
if model.offroad_group_data?
|
20
|
+
errors.add_to_base "Cannot allow received record state for group data in offline app"
|
21
|
+
end
|
22
|
+
elsif Offroad::app_online?
|
23
|
+
if model.offroad_global_data?
|
24
|
+
errors.add_to_base "Cannot allow received record state for global records in online app"
|
25
|
+
elsif group_state.nil?
|
26
|
+
errors.add_to_base "Cannot allow received record state for online group records in online app"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if model.offroad_global_data? && group_state
|
31
|
+
errors.add_to_base "Cannot allow received record state for global records to also be assoc with a group"
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
app_record
|
36
|
+
rescue ActiveRecord::RecordNotFound
|
37
|
+
errors.add_to_base "Cannot find associated app record"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
named_scope :for_model, lambda { |model| { :conditions => {
|
42
|
+
:model_state_id => model.offroad_model_state.id
|
43
|
+
} } }
|
44
|
+
|
45
|
+
named_scope :for_model_and_group_if_apropos, lambda { |model, group| { :conditions => {
|
46
|
+
:model_state_id => model.offroad_model_state.id,
|
47
|
+
:group_state_id => (group && model.offroad_group_data? && group.group_state) ? group.group_state.id : 0
|
48
|
+
} } }
|
49
|
+
|
50
|
+
named_scope :for_record, lambda { |rec| { :conditions => {
|
51
|
+
:model_state_id => rec.class.offroad_model_state.id,
|
52
|
+
:group_state_id => (rec.class.offroad_group_data? && rec.group_state) ? rec.group_state.id : 0,
|
53
|
+
:local_record_id => rec.id
|
54
|
+
} } }
|
55
|
+
|
56
|
+
def app_record
|
57
|
+
model_state.app_model.find(local_record_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.redirect_to_local_ids(records, column, model, group)
|
61
|
+
column = column.to_sym
|
62
|
+
source = self.for_model_and_group_if_apropos(model, group)
|
63
|
+
already_allocated = source.all(:conditions => { :remote_record_id => records.map{|r| r[column]} }).index_by(&:remote_record_id)
|
64
|
+
|
65
|
+
remaining = {} # Maps newly discovered remote id to list of records in batch that reference that id
|
66
|
+
records.each do |r|
|
67
|
+
remote_id = r[column]
|
68
|
+
next unless remote_id && remote_id > 0
|
69
|
+
# TODO Check for illegal references here (i.e. group model referencing global model)
|
70
|
+
if already_allocated.has_key?(remote_id)
|
71
|
+
r[column] = already_allocated[remote_id].local_record_id
|
72
|
+
else
|
73
|
+
# Target doesn't exist yet, we'll figure out what its local id will be later
|
74
|
+
if remaining.has_key?(remote_id)
|
75
|
+
remaining[remote_id] << r
|
76
|
+
else
|
77
|
+
remaining[remote_id] = [r]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
return unless remaining.size > 0
|
83
|
+
|
84
|
+
# Reserve access to a block of local ids by creating temporary records to advance the autoincrement counter
|
85
|
+
# TODO I'm pretty sure this is safe because it'll always be used in a transaction, but I should check
|
86
|
+
model.import([model.primary_key.to_sym], [[nil]]*remaining.size, :validate => false, :timestamps => false)
|
87
|
+
last_id = model.last(:select => model.primary_key, :order => model.primary_key).id
|
88
|
+
local_ids = (last_id+1-remaining.size)..last_id
|
89
|
+
model.delete(local_ids)
|
90
|
+
|
91
|
+
# Create the corresponding RRSes
|
92
|
+
model_state_id = model.offroad_model_state.id
|
93
|
+
group_state = model.offroad_group_data? && group ? group.group_state : nil
|
94
|
+
group_state_id = group_state ? group_state.id : 0
|
95
|
+
self.import(
|
96
|
+
[:model_state_id, :group_state_id, :local_record_id, :remote_record_id],
|
97
|
+
local_ids.zip(remaining.keys).map{|here, there| [model_state_id, group_state_id, here, there]},
|
98
|
+
:validate => false, :timestamps => false
|
99
|
+
)
|
100
|
+
|
101
|
+
# Finally do the redirection to the new ids
|
102
|
+
remaining.each_key.each_with_index do |remote_id, i|
|
103
|
+
remaining[remote_id].each do |r|
|
104
|
+
r[column] = local_ids.first+i
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Offroad
|
2
|
+
private
|
3
|
+
|
4
|
+
class SendableRecordState < ActiveRecord::Base
|
5
|
+
set_table_name "offroad_sendable_record_states"
|
6
|
+
|
7
|
+
belongs_to :model_state, :class_name => "::Offroad::ModelState"
|
8
|
+
|
9
|
+
def validate
|
10
|
+
unless model_state
|
11
|
+
errors.add_to_base "Cannot find associated model state"
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
15
|
+
rec = nil
|
16
|
+
unless deleted
|
17
|
+
begin
|
18
|
+
rec = app_record
|
19
|
+
rescue ActiveRecord::RecordNotFound
|
20
|
+
errors.add_to_base "Cannot find associated app record"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if rec
|
25
|
+
if Offroad::app_offline? && app_record.class.offroad_global_data?
|
26
|
+
errors.add_to_base "Cannot create sendable record state for global data in offline app"
|
27
|
+
elsif Offroad::app_online? && app_record.class.offroad_group_data?
|
28
|
+
errors.add_to_base "Cannot create sendable record state for group data in online app"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
named_scope :for_model, lambda { |model| { :conditions => {
|
34
|
+
:model_state_id => model.offroad_model_state.id
|
35
|
+
} } }
|
36
|
+
|
37
|
+
named_scope :for_deleted_records, :conditions => { :deleted => true }
|
38
|
+
named_scope :for_non_deleted_records, :conditions => { :deleted => false }
|
39
|
+
|
40
|
+
named_scope :with_version_greater_than, lambda { |v| { :conditions => ["mirror_version > ?", v] } }
|
41
|
+
|
42
|
+
named_scope :for_record, lambda { |rec| { :conditions => {
|
43
|
+
:model_state_id => rec.class.offroad_model_state.id,
|
44
|
+
:local_record_id => rec.id
|
45
|
+
} } }
|
46
|
+
|
47
|
+
def app_record
|
48
|
+
model_state.app_model.find(local_record_id)
|
49
|
+
end
|
50
|
+
|
51
|
+
# We put SRS records in mirror files to represent deleted records
|
52
|
+
def self.safe_to_load_from_cargo_stream?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.note_record_destroyed(record)
|
57
|
+
mark_record_changes(record) do |rec|
|
58
|
+
rec.deleted = true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.note_record_created_or_updated(record)
|
63
|
+
mark_record_changes(record)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.setup_imported(model, batch)
|
67
|
+
model_state_id = model.offroad_model_state.id
|
68
|
+
mirror_version = SystemState::current_mirror_version
|
69
|
+
self.import(
|
70
|
+
[:model_state_id, :local_record_id, :mirror_version],
|
71
|
+
batch.map{|r| [model_state_id, r.id, mirror_version]},
|
72
|
+
:validate => false,
|
73
|
+
:timestamps => false
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def self.mark_record_changes(record)
|
80
|
+
transaction do
|
81
|
+
scope = for_record(record)
|
82
|
+
rec_state = scope.first || scope.create
|
83
|
+
rec_state.lock!
|
84
|
+
rec_state.mirror_version = SystemState::current_mirror_version
|
85
|
+
yield(rec_state) if block_given?
|
86
|
+
rec_state.save!
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +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
|