Module: TinyBackup

Defined in:
lib/version.rb,
lib/configure.rb,
lib/tiny_backup.rb

Defined Under Namespace

Classes: Config

Constant Summary collapse

VERSION =
'0.1.0'

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.configure {|@config ||= Config.new| ... } ⇒ Object

Set global settings for TinyBackup like TinyBackup.configure {|config| config.max_versions = 100 }

Yields:



3
4
5
# File 'lib/configure.rb', line 3

def self.configure &block
  yield @config ||= Config.new
end

Instance Method Details

#backup_nowObject

Create a backup of the current database and choose automatically to create a .zip or .diff file.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/tiny_backup.rb', line 19

def backup_now
  # if the resource is locked, we skip to ensure block
  lock
  locked_by_this_method = true

  tmp_files = []
  nvf = new_version_filename

  stream = StringIO.new
  ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
  schema_rb = stream.string

  if nvf.split(".").last == "zip"
    # there is no .zip file so we must create one and add schema.rb and .csv file for each table
    # TODO: use a much better compression like Zlib::BEST_COMPRESSION to reduce the zip size, but this will consume processing power
    ZIPLIB.open("#{config.backup_folder}/#{nvf}", ZIPLIB::CREATE) do |f|
      t_benchmark = Benchmark.ms do
        f.get_output_stream("schema.rb") do |ff|
          ff.write schema_rb
          tmp_files << ff if ZIPOLD
        end
      end
      puts "-- backup_schema\n   -> #{'%.4f' % (t_benchmark/1000)}s\n" if !config.silent

      ActiveRecord::Base.connection.tables.each do |table|
        next if table == "schema_migrations"
        query_count = ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM #{table}").first.first

        t_benchmark = Benchmark.ms do
          if query_count > 0
            rows = []
            query_index = 0

            loop do
              break if query_index >= query_count
              query = ActiveRecord::Base.connection.execute("SELECT * FROM #{table} LIMIT #{config.per_page} OFFSET #{query_index}")
              rows << add_query(query.fields) if query_index == 0
              query.each { |row| rows << add_query(row) }
              query_index += config.per_page
            end

            f.get_output_stream("#{table}.csv") do |ff|
              ff.write rows.join
              tmp_files << ff if ZIPOLD
            end
          end
        end
        puts "-- backup_table(\"#{table}\")\n   -> #{'%.4f' % (t_benchmark/1000)}s\n" if !config.silent
      end
    end

  else
    # a new .diff file is created with the diff between origin_zip and tmp_origin_zip(made by merging all the versions into origin)
    tmp_origin_zip = "#{config.backup_folder}/#{compact_original(:all)}"
    tables = ActiveRecord::Base.connection.tables
    is_empty = true

    File.open("#{config.backup_folder}/#{nvf}", "wb") do |f|
      ZIPLIB.open(tmp_origin_zip) do |zf|
        zf.entries.each do |zf_entry|

          if zf_entry.name == "schema.rb"
            t_benchmark = Benchmark.ms do
              tables.delete "schema_migrations"
              this_diff = diff_files zf.read(zf_entry.name), schema_rb

              if this_diff.present?
                is_empty = false
                f.write "***************\n"
                f.write "*** schema.rb \n"
                f.write "\n"
                this_diff.each { |i| f.write i }
                f.write "\n\n"
              end
            end
            puts "-- backup_schema\n   -> #{'%.4f' % (t_benchmark/1000)}s\n" if !config.silent
          else

            table = zf_entry.name.split(".").first
            tables.delete table
            begin
              query_count = ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM #{table}").first.first
            rescue ActiveRecord::StatementInvalid
              next
            end

            rows = []
            query_index = 0

            t_benchmark = Benchmark.ms do
              loop do
                break if query_index >= query_count
                query = ActiveRecord::Base.connection.execute("SELECT * FROM #{table} LIMIT #{config.per_page} OFFSET #{query_index}")
                rows << add_query(query.fields) if query_index == 0
                query.each { |row| rows << add_query(row) }
                query_index += config.per_page
              end

              this_diff = diff_files zf.read(zf_entry.name), rows.join

              if this_diff.present?
                is_empty = false
                f.write "***************\n"
                f.write "*** #{zf_entry.name} \n"
                f.write "\n"
                this_diff.each { |i| f.write i }
                f.write "\n\n"
              end
            end
            puts "-- backup_table(\"#{table}\")\n   -> #{'%.4f' % (t_benchmark/1000)}s\n" if !config.silent
          end

        end
      end

      # tables that are created recently and doesn't have a .csv file in the tmp_origin_zip
      tables.each do |table|
        begin
          query_count = ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM #{table}").first.first
        rescue ActiveRecord::StatementInvalid
          next
        end

        rows = []
        query_index = 0

        t_benchmark = Benchmark.ms do
          loop do
            break if query_index >= query_count
            query = ActiveRecord::Base.connection.execute("SELECT * FROM #{table} LIMIT #{config.per_page} OFFSET #{query_index}")

            rows << add_query(query.fields) if query_index == 0
            query.each { |row| rows << add_query(row) }
            query_index += config.per_page
          end

          this_diff = diff_files "", rows.join

          if this_diff.present?
            is_empty = false
            f.write "***************\n"
            f.write "*** #{table}.csv \n"
            f.write "\n"
            this_diff.each { |i| f.write i }
            f.write "\n\n"
          end
        end
        puts "-- backup_table(\"#{table}\")\n   -> #{'%.4f' % (t_benchmark/1000)}s\n" if !config.silent
      end
    end

    File.delete tmp_origin_zip
    File.delete("#{config.backup_folder}/#{nvf}") if is_empty
  end

  # keep max versions
  version_files = Dir.glob("#{config.backup_folder}/#{config.version_prefix}*").sort
  if config.max_versions < version_files.length
    # throw files to garbage
    tmp_files << Dir.glob("#{config.backup_folder}/#{config.zip_prefix}*").first
    tmp_files << version_files.first

    File.rename "#{config.backup_folder}/#{compact_original(1)}", dup_file("#{config.backup_folder}/#{config.zip_prefix}_#{Time.now.strftime(config.date_format)}.zip")
  end

  # delete temporary files before method exit
  rescue => e
    @method_error = e
  ensure
    unlock if locked_by_this_method
    tmp_files.each { |i| File.delete(i) rescue nil } if tmp_files.present?
    raise @method_error if @method_error.present?
    return true
end

#compact_allObject

Merge into the .zip file and delete all the .diff files to clear the space.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/tiny_backup.rb', line 195

def compact_all
  lock # if the resource is locked, we skip to ensure block
  locked_by_this_method = true

  tmp_files = []
  tmp_files += Dir.glob("#{config.backup_folder}/#{config.zip_prefix}*")
  tmp_files += Dir.glob("#{config.backup_folder}/#{config.version_prefix}*")

  # make the temporary zip be the original and apply the updated_at time-stamp
  File.rename "#{config.backup_folder}/#{compact_original(:all)}", dup_file("#{config.backup_folder}/#{config.zip_prefix}_#{Time.now.strftime(config.date_format)}.zip")

  # delete temporary files before method exit
  rescue => e
    @method_error = e
  ensure
    unlock if locked_by_this_method
    tmp_files.each { |i| File.delete(i) rescue nil } if tmp_files.present?
    raise @method_error if @method_error.present?
    return true
end

#configObject

Global settings for TinyBackup



8
9
10
# File 'lib/configure.rb', line 8

def config
  @config ||= Config.new
end

#restore_db(version_number, just_temporary = true, with_backup = true) ⇒ Object

Change the database to match the selected integer version_number.

Parameters:

  • version_number (Integer or Symbol)

    can be :all to restore all backup data or 0 to restore only the data collected in the .zip file

  • just_temporary (Boolean) (defaults to: true)

    if is false, the unused version files will be deleted and the latest backup version will be synchronized with the database data.

  • with_backup (Boolean) (defaults to: true)

    if is false, the attempt to backup not saved data is canceled



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/tiny_backup.rb', line 220

def restore_db version_number, just_temporary=true, with_backup=true
  # do this before deleting the database and lose data
  backup_now if just_temporary && with_backup

  lock # if the resource is locked, we skip to ensure block
  locked_by_this_method = true

  tmp_files = []

  if just_temporary && version_number != :all
    puts "you want to restore just temporary: DO NOT start a backup BEFORE calling TinyBackup.restore_db(:all)\n" if !config.silent
  end

  version_files = Dir.glob("#{config.backup_folder}/#{config.version_prefix}*")
  if version_number == :all
    version_count = version_files.length
  else
    good_versions = version_files.find_all { |i| i.gsub("#{config.backup_folder}/#{config.version_prefix}", "").split("_").first.to_i <= version_number.to_i }
    version_count = good_versions.length
    tmp_files += version_files - good_versions if !just_temporary
  end

  tmp_origin_zip = compact_original version_count
  tmp_files << "#{config.backup_folder}/#{tmp_origin_zip}"
  tmp_files << "#{config.backup_folder}/schema_tmp.rb"

  db_name = Rails.configuration.database_configuration[Rails.env]["database"]
  db_collation = ActiveRecord::Base.connection.collation
  ActiveRecord::Base.connection.drop_database   db_name
  ActiveRecord::Base.connection.create_database db_name, collation: db_collation
  ActiveRecord::Base.connection.reconnect!

  # prepare the structure
  ZIPLIB.open("#{config.backup_folder}/#{tmp_origin_zip}") do |zf|
    zf.entries.each do |zf_entry|
      if zf_entry.name == "schema.rb"
        File.open("#{config.backup_folder}/schema_tmp.rb", "wb") { |f| f.write zf.read(zf_entry.name) }
        break
      end
    end
  end
  schema_verbose = ActiveRecord::Schema.verbose
  ActiveRecord::Schema.verbose = !config.silent
  ActiveRecord::Schema.load("#{config.backup_folder}/schema_tmp.rb")
  ActiveRecord::Schema.verbose = schema_verbose

  # add the data
  ZIPLIB.open("#{config.backup_folder}/#{tmp_origin_zip}") do |zf|
    zf.entries.each do |zf_entry|
      next if zf_entry.name == "schema.rb"
      table_rows = zf.read(zf_entry.name).split("\n")
      table_header = table_rows.shift
      table_name = zf_entry.name.split(".").first

      t_benchmark = Benchmark.ms do
        table_rows.in_groups_of(config.per_page, false) do |tr_group|
          ActiveRecord::Base.connection.execute insert_row(table_name, table_header, tr_group)
        end
      end
      puts "-- insert_data(\"#{table_name}\")\n   -> #{'%.4f' % (t_benchmark/1000)}s\n" if !config.silent
    end
  end

  # delete temporary files before method exit
  rescue => e
    @method_error = e
  ensure
    unlock if locked_by_this_method
    tmp_files.each { |i| File.delete(i) rescue nil } if tmp_files.present?
    raise @method_error if @method_error.present?
    return true
end