dump 1.0.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.
Files changed (89) hide show
  1. data/.autotest +13 -0
  2. data/.gitignore +12 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.markdown +250 -0
  5. data/dump.gemspec +22 -0
  6. data/lib/dump.rb +3 -0
  7. data/lib/dump/capistrano.rb +1 -0
  8. data/lib/dump/railtie.rb +8 -0
  9. data/lib/dump_rake.rb +85 -0
  10. data/lib/dump_rake/archive_tar_minitar_fix.rb +8 -0
  11. data/lib/dump_rake/assets.rb +22 -0
  12. data/lib/dump_rake/continious_timeout.rb +38 -0
  13. data/lib/dump_rake/dump.rb +175 -0
  14. data/lib/dump_rake/dump_reader.rb +289 -0
  15. data/lib/dump_rake/dump_writer.rb +119 -0
  16. data/lib/dump_rake/env.rb +139 -0
  17. data/lib/dump_rake/env/filter.rb +26 -0
  18. data/lib/dump_rake/rails_root.rb +12 -0
  19. data/lib/dump_rake/table_manipulation.rb +131 -0
  20. data/lib/generators/assets_config/assets_config_generator.rb +16 -0
  21. data/lib/generators/assets_config/templates/assets +8 -0
  22. data/lib/tasks/assets.rake +17 -0
  23. data/lib/tasks/dump.rake +27 -0
  24. data/recipes/dump.rb +343 -0
  25. data/script/update_readme +21 -0
  26. data/spec/.gitignore +1 -0
  27. data/spec/.tmignore +1 -0
  28. data/spec/cycle_spec.rb +229 -0
  29. data/spec/db/database.example.yml +19 -0
  30. data/spec/db/schema.rb +7 -0
  31. data/spec/dummy-3.1.3/.gitignore +15 -0
  32. data/spec/dummy-3.1.3/.rspec +1 -0
  33. data/spec/dummy-3.1.3/Gemfile +23 -0
  34. data/spec/dummy-3.1.3/Gemfile.lock +159 -0
  35. data/spec/dummy-3.1.3/README +261 -0
  36. data/spec/dummy-3.1.3/Rakefile +7 -0
  37. data/spec/dummy-3.1.3/app/assets/images/rails.png +0 -0
  38. data/spec/dummy-3.1.3/app/assets/javascripts/application.js +9 -0
  39. data/spec/dummy-3.1.3/app/assets/stylesheets/application.css +7 -0
  40. data/spec/dummy-3.1.3/app/controllers/application_controller.rb +3 -0
  41. data/spec/dummy-3.1.3/app/helpers/application_helper.rb +2 -0
  42. data/spec/dummy-3.1.3/app/mailers/.gitkeep +0 -0
  43. data/spec/dummy-3.1.3/app/models/.gitkeep +0 -0
  44. data/spec/dummy-3.1.3/app/views/layouts/application.html.erb +14 -0
  45. data/spec/dummy-3.1.3/config.ru +4 -0
  46. data/spec/dummy-3.1.3/config/application.rb +54 -0
  47. data/spec/dummy-3.1.3/config/boot.rb +6 -0
  48. data/spec/dummy-3.1.3/config/database.yml +25 -0
  49. data/spec/dummy-3.1.3/config/environment.rb +5 -0
  50. data/spec/dummy-3.1.3/config/environments/development.rb +30 -0
  51. data/spec/dummy-3.1.3/config/environments/production.rb +60 -0
  52. data/spec/dummy-3.1.3/config/environments/test.rb +39 -0
  53. data/spec/dummy-3.1.3/config/initializers/backtrace_silencers.rb +7 -0
  54. data/spec/dummy-3.1.3/config/initializers/inflections.rb +10 -0
  55. data/spec/dummy-3.1.3/config/initializers/mime_types.rb +5 -0
  56. data/spec/dummy-3.1.3/config/initializers/secret_token.rb +7 -0
  57. data/spec/dummy-3.1.3/config/initializers/session_store.rb +8 -0
  58. data/spec/dummy-3.1.3/config/initializers/wrap_parameters.rb +14 -0
  59. data/spec/dummy-3.1.3/config/locales/en.yml +5 -0
  60. data/spec/dummy-3.1.3/config/routes.rb +58 -0
  61. data/spec/dummy-3.1.3/db/seeds.rb +7 -0
  62. data/spec/dummy-3.1.3/doc/README_FOR_APP +2 -0
  63. data/spec/dummy-3.1.3/lib/assets/.gitkeep +0 -0
  64. data/spec/dummy-3.1.3/lib/tasks/.gitkeep +0 -0
  65. data/spec/dummy-3.1.3/log/.gitkeep +0 -0
  66. data/spec/dummy-3.1.3/public/404.html +26 -0
  67. data/spec/dummy-3.1.3/public/422.html +26 -0
  68. data/spec/dummy-3.1.3/public/500.html +26 -0
  69. data/spec/dummy-3.1.3/public/favicon.ico +0 -0
  70. data/spec/dummy-3.1.3/public/index.html +241 -0
  71. data/spec/dummy-3.1.3/public/robots.txt +5 -0
  72. data/spec/dummy-3.1.3/script/rails +6 -0
  73. data/spec/dummy-3.1.3/spec/spec_helper.rb +32 -0
  74. data/spec/dummy-3.1.3/vendor/assets/stylesheets/.gitkeep +0 -0
  75. data/spec/dummy-3.1.3/vendor/plugins/.gitkeep +0 -0
  76. data/spec/lib/dump_rake/dump_reader_spec.rb +638 -0
  77. data/spec/lib/dump_rake/dump_spec.rb +291 -0
  78. data/spec/lib/dump_rake/dump_writer_spec.rb +328 -0
  79. data/spec/lib/dump_rake/env/filter_spec.rb +56 -0
  80. data/spec/lib/dump_rake/env_spec.rb +139 -0
  81. data/spec/lib/dump_rake/rails_root_spec.rb +45 -0
  82. data/spec/lib/dump_rake/table_manipulation_spec.rb +256 -0
  83. data/spec/lib/dump_rake_spec.rb +326 -0
  84. data/spec/recipes/dump_spec.rb +553 -0
  85. data/spec/spec.opts +4 -0
  86. data/spec/spec_helper.rb +34 -0
  87. data/spec/tasks/assets_spec.rb +92 -0
  88. data/spec/tasks/dump_spec.rb +107 -0
  89. metadata +272 -0
@@ -0,0 +1,38 @@
1
+ # based on Timeout
2
+
3
+ module ContiniousTimeout
4
+ class TimeoutException < ::Exception
5
+ end
6
+
7
+ class RestartException < ::Exception
8
+ end
9
+
10
+ class Deferer
11
+ def initialize(thread)
12
+ @thread = thread
13
+ end
14
+
15
+ def defer
16
+ @thread.raise RestartException.new
17
+ end
18
+ end
19
+
20
+ def self.timeout(sec)
21
+ begin
22
+ x = Thread.current
23
+ y = Thread.start do
24
+ 1.times do
25
+ begin
26
+ sleep sec
27
+ rescue RestartException => e
28
+ retry
29
+ end
30
+ end
31
+ x.raise TimeoutException, "execution expired" if x.alive?
32
+ end
33
+ yield Deferer.new(y)
34
+ ensure
35
+ y.kill if y and y.alive?
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,175 @@
1
+ # encoding: UTF-8
2
+
3
+ class DumpRake
4
+ class Dump
5
+ include TableManipulation
6
+ def self.list(options = {})
7
+ dumps = Dir[File.join(DumpRake::RailsRoot, 'dump', options[:all] ? '*.*' : '*.tgz')].sort.select{ |path| File.file?(path) }.map{ |path| new(path) }
8
+ dumps = dumps.select{ |dump| dump.name[options[:like]] } if options[:like]
9
+ if options[:tags]
10
+ tags = get_filter_tags(options[:tags])
11
+ dumps = dumps.select{ |dump| (dump.tags & tags[:simple]).present? } if tags[:simple].present?
12
+ dumps = dumps.select{ |dump| (dump.tags & tags[:mandatory]) == tags[:mandatory] } if tags[:mandatory].present?
13
+ dumps = dumps.reject{ |dump| (dump.tags & tags[:forbidden]).present? } if tags[:forbidden].present?
14
+ end
15
+ dumps
16
+ end
17
+
18
+ def initialize(path_or_options = {})
19
+ if path_or_options.is_a?(Hash)
20
+ options = path_or_options
21
+
22
+ name = Time.now.utc.strftime("%Y%m%d%H%M%S")
23
+
24
+ description = clean_description(options[:desc])
25
+ name += "-#{description}" unless description.blank?
26
+
27
+ tags = clean_tags(options[:tags])
28
+ name += "@#{tags * ','}" unless tags.empty?
29
+
30
+ tgz_name = "#{name}.tgz"
31
+
32
+ @path = options[:dir] ? Pathname(options[:dir]) + tgz_name : Pathname(tgz_name)
33
+
34
+ else
35
+ @path = Pathname(path_or_options)
36
+ end
37
+ end
38
+
39
+ attr_reader :path
40
+
41
+ def tgz_path
42
+ path_with_ext('tgz')
43
+ end
44
+
45
+ def tmp_path
46
+ path_with_ext('tmp')
47
+ end
48
+
49
+ def ==(other)
50
+ path == other.path
51
+ end
52
+
53
+ def parts
54
+ @parts ||=
55
+ if m = name.match(/^(\d{#{4+2+2 + 2+2+2}})(-[^@]+)?((?:@[^@]+)+)?\.(tmp|tgz)$/)
56
+ {
57
+ :time => m[1],
58
+ :desc => m[2] && m[2][1, m[2].length],
59
+ :tags => m[3] && m[3][1, m[3].length],
60
+ :ext => m[4]
61
+ }
62
+ else
63
+ {}
64
+ end
65
+ end
66
+
67
+ def time
68
+ parts[:time] && Time.utc(*parts[:time].match(/(\d{4})#{'(\d{2})' * 5}/)[1..6])
69
+ end
70
+
71
+ def description
72
+ clean_description(parts[:desc])
73
+ end
74
+
75
+ def tags
76
+ clean_tags(parts[:tags])
77
+ end
78
+
79
+ def ext
80
+ parts[:ext]
81
+ end
82
+
83
+ def name
84
+ @name ||= File.basename(path)
85
+ end
86
+ alias to_s name
87
+
88
+ def size
89
+ File.size(path) rescue nil
90
+ end
91
+
92
+ def human_size
93
+ number = size
94
+ return nil if number.nil?
95
+ degree = 0
96
+ symbols = %W[B K M G T]
97
+ while number >= 1000 && degree < symbols.length - 1
98
+ degree += 1
99
+ number /= 1024.0
100
+ end
101
+ "#{'%.2f' % number}#{symbols[degree]}"
102
+ end
103
+
104
+ def inspect
105
+ "#<%s:0x%x %s>" % [self.class, object_id, path.to_s.sub(/^.+(?=..\/[^\/]*$)/, '…')]
106
+ end
107
+
108
+ def lock
109
+ if lock = File.open(path, 'r')
110
+ begin
111
+ if lock.flock(File::LOCK_EX | File::LOCK_NB)
112
+ yield
113
+ end
114
+ ensure
115
+ lock.flock(File::LOCK_UN)
116
+ lock.close
117
+ end
118
+ end
119
+ end
120
+
121
+ protected
122
+
123
+ def assets_root_link
124
+ prefix = 'assets'
125
+ Dir.mktmpdir do |dir|
126
+ Dir.chdir(dir) do
127
+ File.symlink(DumpRake::RailsRoot, prefix)
128
+ begin
129
+ yield dir, prefix
130
+ ensure
131
+ File.unlink(prefix)
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def path_with_ext(ext)
138
+ Pathname(path.to_s.sub(/#{parts[:ext]}$/, ext))
139
+ end
140
+
141
+ module CleanNParse
142
+ def clean_str(str, additional = nil)
143
+ str.to_s.strip.gsub(/\s+/, ' ').gsub(/[^A-Za-z0-9 \-_#{Regexp.escape(additional.to_s) if additional}]+/, '_')
144
+ end
145
+ def clean_description(description)
146
+ clean_str(description, '()#')[0, 50].strip
147
+ end
148
+ def clean_tag(tag)
149
+ clean_str(tag).downcase.sub(/^\-+/, '')[0, 20].strip
150
+ end
151
+ def clean_tags(tags)
152
+ tags.to_s.split(',').map{ |tag| clean_tag(tag) }.uniq.reject(&:blank?).sort
153
+ end
154
+ def get_filter_tags(tags)
155
+ groups = Hash.new{ |hash, key| hash[key] = SortedSet.new }
156
+ tags.to_s.split(',').each do |tag|
157
+ if m = tag.strip.match(/^(\-|\+)?(.*)$/)
158
+ type = {'+' => :mandatory, '-' => :forbidden}[m[1]] || :simple
159
+ unless (claned_tag = clean_tag(m[2])).blank?
160
+ groups[type] << claned_tag
161
+ end
162
+ end
163
+ end
164
+ [:simple, :mandatory].each do |type|
165
+ if (clashing = (groups[type] & groups[:forbidden])).present?
166
+ raise "#{type} tags clashes with forbidden ones: #{clashing}"
167
+ end
168
+ end
169
+ groups.each_with_object({}){ |(key, value), hsh| hsh[key] = value.to_a }
170
+ end
171
+ end
172
+ include CleanNParse
173
+ extend CleanNParse
174
+ end
175
+ end
@@ -0,0 +1,289 @@
1
+ class DumpRake
2
+ class DumpReader < Dump
3
+ attr_reader :stream, :config
4
+
5
+ def self.restore(path)
6
+ new(path).open do |dump|
7
+ ActiveRecord::Base.logger.silence do
8
+ dump.read_config
9
+ dump.migrate_down
10
+ dump.read_schema
11
+
12
+ dump.read_tables
13
+ dump.read_assets
14
+ end
15
+ end
16
+ end
17
+
18
+ class Summary
19
+ attr_reader :text
20
+ alias_method :to_s, :text
21
+ def initialize
22
+ @text = ''
23
+ end
24
+
25
+ def header(header)
26
+ @text << " #{header}:\n"
27
+ end
28
+
29
+ def data(entries)
30
+ entries.each do |entry|
31
+ @text << " #{entry}\n"
32
+ end
33
+ end
34
+
35
+ # from ActionView::Helpers::TextHelper
36
+ def self.pluralize(count, singular)
37
+ "#{count} #{count == 1 ? singular : singular.pluralize}"
38
+ end
39
+ end
40
+
41
+ def self.summary(path, options = {})
42
+ new(path).open do |dump|
43
+ dump.read_config
44
+
45
+ sum = Summary.new
46
+
47
+ tables = dump.config[:tables]
48
+ sum.header 'Tables'
49
+ sum.data tables.sort.map{ |(table, rows)|
50
+ "#{table}: #{Summary.pluralize(rows, 'row')}"
51
+ }
52
+
53
+ assets = dump.config[:assets]
54
+ if assets.present?
55
+ sum.header 'Assets'
56
+ sum.data assets.sort.map{ |entry|
57
+ if String === entry
58
+ entry
59
+ else
60
+ asset, paths = entry
61
+ if Hash === paths
62
+ "#{asset}: #{Summary.pluralize paths[:files], 'file'} (#{Summary.pluralize paths[:total], 'entry'} total)"
63
+ else
64
+ "#{asset}: #{Summary.pluralize paths, 'entry'}"
65
+ end
66
+ end
67
+ }
68
+ end
69
+
70
+ if options[:schema]
71
+ sum.header 'Schema'
72
+ sum.data dump.schema.split("\n")
73
+ end
74
+
75
+ sum
76
+ end
77
+ end
78
+
79
+ def open
80
+ Zlib::GzipReader.open(path) do |gzip|
81
+ Archive::Tar::Minitar.open(gzip, 'r') do |stream|
82
+ @stream = stream
83
+ yield(self)
84
+ end
85
+ end
86
+ end
87
+
88
+ def find_entry(matcher)
89
+ stream.each do |entry|
90
+ if matcher === entry.full_name
91
+ # we can not return entry - after exiting stream.each the entry will be invalid and will read from tar start
92
+ return yield(entry)
93
+ end
94
+ end
95
+ end
96
+
97
+ def read_entry(matcher)
98
+ find_entry(matcher) do |entry|
99
+ return entry.read
100
+ end
101
+ end
102
+
103
+ def read_entry_to_file(matcher)
104
+ find_entry(matcher) do |entry|
105
+ Tempfile.open('dumper') do |temp|
106
+ temp.write(entry.read(4096)) until entry.eof?
107
+ temp.rewind
108
+ yield(temp)
109
+ end
110
+ end
111
+ end
112
+
113
+ def read_config
114
+ @config = Marshal.load(read_entry('config'))
115
+ end
116
+
117
+ def migrate_down
118
+ case
119
+ when DumpRake::Env.downcase(:migrate_down) == 'reset'
120
+ Rake::Task['db:drop'].invoke
121
+ Rake::Task['db:create'].invoke
122
+ when !DumpRake::Env.no?(:migrate_down)
123
+ if avaliable_tables.include?('schema_migrations')
124
+ find_entry("schema_migrations.dump") do |entry|
125
+ migrated = table_rows('schema_migrations').map{ |row| row['version'] }
126
+
127
+ dump_migrations = []
128
+ Marshal.load(entry) # skip header
129
+ dump_migrations << Marshal.load(entry).first until entry.eof?
130
+
131
+ migrate_down = (migrated - dump_migrations)
132
+
133
+ unless migrate_down.empty?
134
+ migrate_down.reverse.with_progress('Migrating down') do |version|
135
+ DumpRake::Env.with_env('VERSION' => version) do
136
+ Rake::Task['db:migrate:down'].tap do |task|
137
+ begin
138
+ task.invoke
139
+ rescue ActiveRecord::IrreversibleMigration
140
+ $stderr.puts "Irreversible migration: #{version}"
141
+ end
142
+ task.reenable
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def restore_schema?
153
+ !DumpRake::Env.no?(:restore_schema)
154
+ end
155
+
156
+ def read_schema
157
+ if restore_schema?
158
+ read_entry_to_file('schema.rb') do |f|
159
+ DumpRake::Env.with_env('SCHEMA' => f.path) do
160
+ Rake::Task['db:schema:load'].invoke
161
+ end
162
+ Rake::Task['db:schema:dump'].invoke
163
+ end
164
+ end
165
+ end
166
+
167
+ def schema
168
+ read_entry('schema.rb')
169
+ end
170
+
171
+ def read_tables
172
+ verify_connection
173
+ config[:tables].with_progress('Tables') do |table, rows|
174
+ if (restore_schema? && schema_tables.include?(table)) || DumpRake::Env.filter(:restore_tables).pass?(table)
175
+ read_table(table, rows)
176
+ end
177
+ end
178
+ end
179
+
180
+ def read_table(table, rows_count)
181
+ find_entry("#{table}.dump") do |entry|
182
+ table_sql = quote_table_name(table)
183
+ clear_table(table_sql)
184
+
185
+ columns = Marshal.load(entry)
186
+ columns_sql = columns_insert_sql(columns)
187
+ Progress.start(table, rows_count) do
188
+ until entry.eof?
189
+ rows_sql = []
190
+ 1000.times do
191
+ rows_sql << values_insert_sql(Marshal.load(entry)) unless entry.eof?
192
+ end
193
+
194
+ begin
195
+ insert_into_table(table_sql, columns_sql, rows_sql)
196
+ Progress.step(rows_sql.length)
197
+ rescue
198
+ rows_sql.each do |row_sql|
199
+ insert_into_table(table_sql, columns_sql, row_sql)
200
+ Progress.step
201
+ end
202
+ end
203
+ end
204
+ end
205
+ fix_sequence!(table)
206
+ end
207
+ end
208
+
209
+ def read_assets
210
+ unless config[:assets].blank?
211
+ assets = config[:assets]
212
+ if Hash === assets
213
+ assets_count = assets.values.sum{ |value| Hash === value ? value[:total] : value }
214
+ assets_paths = assets.keys
215
+ else
216
+ assets_count, assets_paths = nil, assets
217
+ end
218
+
219
+ if DumpRake::Env[:restore_assets]
220
+ assets_paths.each do |asset|
221
+ DumpRake::Assets.glob_asset_children(asset, '**/*').reverse.each do |child|
222
+ if read_asset?(child, DumpRake::RailsRoot)
223
+ case
224
+ when File.file?(child)
225
+ File.unlink(child)
226
+ when File.directory?(child)
227
+ begin
228
+ Dir.unlink(child)
229
+ rescue Errno::ENOTEMPTY
230
+ nil
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ else
237
+ DumpRake::Env.with_env(:assets => assets_paths.join(':')) do
238
+ Rake::Task['assets:delete'].invoke
239
+ end
240
+ end
241
+
242
+ read_assets_entries(assets_paths, assets_count) do |stream, root, entry, prefix|
243
+ if !DumpRake::Env[:restore_assets] || read_asset?(entry.full_name, prefix)
244
+ stream.extract_entry(root, entry)
245
+ end
246
+ end
247
+ end
248
+ end
249
+
250
+ def read_asset?(path, prefix)
251
+ DumpRake::Env.filter(:restore_assets, DumpRake::Assets::SPLITTER).custom_pass? do |value|
252
+ File.fnmatch(File.join(prefix, value), path) ||
253
+ File.fnmatch(File.join(prefix, value, '**'), path)
254
+ end
255
+ end
256
+
257
+ def read_assets_entries(assets_paths, assets_count)
258
+ Progress.start('Assets', assets_count || 1) do
259
+ found_assets = false
260
+ # old style — in separate tar
261
+ find_entry('assets.tar') do |assets_tar|
262
+ def assets_tar.rewind
263
+ # rewind will fail - it must go to center of gzip
264
+ # also we don't need it - this is last step in dump restore
265
+ end
266
+ Archive::Tar::Minitar.open(assets_tar) do |inp|
267
+ inp.each do |entry|
268
+ yield inp, DumpRake::RailsRoot, entry, nil
269
+ Progress.step if assets_count
270
+ end
271
+ end
272
+ found_assets = true
273
+ end
274
+
275
+ unless found_assets
276
+ # new style — in same tar
277
+ assets_root_link do |tmpdir, prefix|
278
+ stream.each do |entry|
279
+ if entry.full_name.starts_with?("#{prefix}/")
280
+ yield stream, tmpdir, entry, prefix
281
+ Progress.step if assets_count
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end