travis-backup 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da5ede447a9a2a1b215d03d8582f62e8c4573e69e3337da91afaaffef07323f3
4
- data.tar.gz: 01bc3c92f48f093ae3dd4ff23c07a0f2e9c5516b79c05279700d82570b0a2d40
3
+ metadata.gz: 85a9fa54e0b9f6637f1371e29d72682ff808bb2a1c7ee5e8d9432634f2961f14
4
+ data.tar.gz: 96f242b4475d5b50f5a46bfb0e2aaac618aad78a7107a89977d4e5ffde103eba
5
5
  SHA512:
6
- metadata.gz: 775e8b95e35be972da5194d703d33645bfe21198d91f28efe2a417214d74e4fc71fee5a9c6d7a715ae2819b8d12a0b0bad691bca44b507782acd1dbf48c43d16
7
- data.tar.gz: 289107dfc9d91b3cc43479c1a4973fe0fec4db91a519dd9cd8be321c9ce56f65e2170464ae3c88492385af5609d0ea52bfbe9d7510f1a44bcffa8f1dfea81b63
6
+ metadata.gz: 343866ae70df1f9f7c5fccdbc49f350c4d58ff195c9c527dd13995f3a66710b09ac53e269ab03eacaa36e6c0a17004d58635aec7c5ce0f7fce5d44c1029282de
7
+ data.tar.gz: f6f2a7d430da5bdfb6ece3de27c4c7201061071f6636e4d16f7dc3b70f04b922d1d0e80b87c78bdec33bdf42fa267c7d46ed733dc91eaccb8d094ec1e7005c22
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # README
2
2
 
3
- *travis-backup* is an application that helps with housekeeping and backup for Travis CI database v2.2 and with migration to v3.0 database.
3
+ *travis-backup* is an application that helps with housekeeping and backup for Travis CI database v2.2 and with migration to v3.0 database. By default it removes requests and builds with their corresponding jobs and logs, as long as they are older than given threshold says (and backups them in files, if this option is active). Although it can be also run with special modes: `move_logs`, for moving logs from one database to another, and `remove_orphans`, for deleting all orphaned data.
4
4
 
5
5
  ### Installation and run
6
6
 
@@ -75,15 +75,19 @@ backup:
75
75
  limit: 1000 # builds limit for one backup file
76
76
  threshold: 6 # number of months from now - data younger than this time won't be backuped
77
77
  files_location: './dump' # path of the folder in which backup files will be placed
78
- user_id # run only for given user
79
- org_id # run only for given organization
80
- repo_id # run only for given repository
78
+ user_id: 1 # run only for given user
79
+ org_id: 1 # run only for given organization
80
+ repo_id: 1 # run only for given repository
81
+ move_logs: false # run in move logs mode - move all logs to database at destination_db_url URL
82
+ remove_orphans: false # run in remove orphans mode
81
83
  ```
82
84
 
83
- You can also set these properties using env vars corresponding to them: `IF_BACKUP`, `BACKUP_DRY_RUN`, `BACKUP_LIMIT`, `BACKUP_THRESHOLD`, `BACKUP_FILES_LOCATION`, `USER_ID`, `ORG_ID`, `REPO_ID`.
85
+ You can also set these properties using env vars corresponding to them: `IF_BACKUP`, `BACKUP_DRY_RUN`, `BACKUP_LIMIT`, `BACKUP_THRESHOLD`, `BACKUP_FILES_LOCATION`, `BACKUP_USER_ID`, `BACKUP_ORG_ID`, `BACKUP_REPO_ID`, `BACKUP_MOVE_LOGS`, `BACKUP_REMOVE_ORPHANS`.
84
86
 
85
87
  You should also specify your database url. You can do this the standard way in `config/database.yml` file, setting the `database_url` hash argument while creating `Backup` instance or using the `DATABASE_URL` env var. Your database should be consistent with the Travis 2.2 database schema.
86
88
 
89
+ For `move_logs` mode you need also to specify a destination database. You can set it also in `config/database.yml` file, in `destination` subsection, setting the `destination_db_url` hash argument while creating `Backup` instance or using the `BACKUP_DESTINATION_DB_URL` env var. Your destination database should be consistent with the Travis 3.0 database schema.
90
+
87
91
  ### How to run the test suite
88
92
 
89
93
  You can run the test after cloning this repository. Next you should call
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Backup
4
+ class MoveLogs
5
+ attr_reader :config
6
+
7
+ def initialize(config, db_helper, dry_run_reporter=nil)
8
+ @config = config
9
+ @dry_run_reporter = dry_run_reporter
10
+ @db_helper = db_helper
11
+ end
12
+
13
+ def run
14
+ return run_dry if @config.dry_run
15
+
16
+ @db_helper.connect_db(@config.database_url)
17
+ Log.order(:id).in_batches(of: @config.limit.to_i).map do |logs_batch|
18
+ log_hashes = logs_batch.as_json
19
+ @db_helper.connect_db(@config.destination_db_url)
20
+
21
+ log_hashes.each do |log_hash|
22
+ new_log = Log.new(log_hash)
23
+ new_log.save!
24
+ end
25
+
26
+ @db_helper.connect_db(@config.database_url)
27
+
28
+ logs_batch.each(&:destroy)
29
+ end
30
+ end
31
+
32
+ def run_dry
33
+ ids = Log.order(:id).map(&:id)
34
+ @dry_run_reporter.add_to_report(:logs, *ids)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Backup
4
+ class RemoveOld
5
+ attr_reader :config
6
+
7
+ def initialize(config, dry_run_reporter=nil)
8
+ @config = config
9
+ @dry_run_reporter = dry_run_reporter
10
+ end
11
+
12
+ def dry_run_report
13
+ @dry_run_reporter.report
14
+ end
15
+
16
+ def run(args={})
17
+ user_id = args[:user_id] || @config.user_id
18
+ repo_id = args[:repo_id] || @config.repo_id
19
+ org_id = args[:org_id] || @config.org_id
20
+
21
+ if user_id
22
+ process_repos_for_owner(user_id, 'User')
23
+ elsif org_id
24
+ process_repos_for_owner(org_id, 'Organization')
25
+ elsif repo_id
26
+ process_repo_with_id(repo_id)
27
+ else
28
+ process_all_repos
29
+ end
30
+ end
31
+
32
+ def process_repos_for_owner(owner_id, owner_type)
33
+ Repository.where('owner_id = ? and owner_type = ?', owner_id, owner_type).order(:id).each do |repository|
34
+ process_repo(repository)
35
+ end
36
+ end
37
+
38
+ def process_repo_with_id(repo_id)
39
+ process_repo(Repository.find(repo_id))
40
+ end
41
+
42
+ def process_all_repos
43
+ Repository.order(:id).each do |repository|
44
+ process_repo(repository)
45
+ end
46
+ end
47
+
48
+ def process_repo(repository)
49
+ process_repo_builds(repository)
50
+ process_repo_requests(repository)
51
+ end
52
+
53
+ def process_repo_builds(repository) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
54
+ threshold = @config.threshold.to_i.months.ago.to_datetime
55
+ current_build_id = repository.current_build_id || -1
56
+ repository.builds.where('created_at < ? and id != ?', threshold, current_build_id)
57
+ .in_batches(of: @config.limit.to_i).map do |builds_batch|
58
+ if @config.if_backup
59
+ file_prefix = "repository_#{repository.id}"
60
+ save_and_destroy_builds_batch(builds_batch, file_prefix)
61
+ else
62
+ destroy_builds_batch(builds_batch)
63
+ end
64
+ end.compact.reduce(&:&)
65
+ end
66
+
67
+ def process_repo_requests(repository)
68
+ threshold = @config.threshold.to_i.months.ago.to_datetime
69
+ repository.requests.where('created_at < ?', threshold)
70
+ .in_batches(of: @config.limit.to_i).map do |requests_batch|
71
+ @config.if_backup ? save_and_destroy_requests_batch(requests_batch, repository) : destroy_requests_batch(requests_batch)
72
+ end.compact
73
+ end
74
+
75
+ private
76
+
77
+ def save_and_destroy_builds_batch(builds_batch, file_prefix)
78
+ builds_export = builds_batch.map(&:attributes)
79
+
80
+ dependencies_saved = builds_batch.map do |build|
81
+ save_build_jobs_and_logs(build, file_prefix)
82
+ end.reduce(&:&)
83
+
84
+ if dependencies_saved
85
+ file_name = "#{file_prefix}_builds_#{builds_batch.first.id}-#{builds_batch.last.id}.json"
86
+ pretty_json = JSON.pretty_generate(builds_export)
87
+ save_file(file_name, pretty_json) ? destroy_builds_batch(builds_batch) : false
88
+ else
89
+ false
90
+ end
91
+ end
92
+
93
+ def save_build_jobs_and_logs(build, file_prefix)
94
+ build.jobs.in_batches(of: @config.limit.to_i).map do |jobs_batch|
95
+ file_prefix = "#{file_prefix}_build_#{build.id}"
96
+ save_jobs_batch(jobs_batch, file_prefix)
97
+ end.compact.reduce(&:&)
98
+ end
99
+
100
+ def save_jobs_batch(jobs_batch, file_prefix)
101
+ jobs_export = jobs_batch.map(&:attributes)
102
+
103
+ logs_saved = jobs_batch.map do |job|
104
+ save_job_logs(job, file_prefix)
105
+ end.reduce(&:&)
106
+
107
+ if logs_saved
108
+ file_name = "#{file_prefix}_jobs_#{jobs_batch.first.id}-#{jobs_batch.last.id}.json"
109
+ pretty_json = JSON.pretty_generate(jobs_export)
110
+ save_file(file_name, pretty_json)
111
+ else
112
+ false
113
+ end
114
+ end
115
+
116
+ def save_job_logs(job, file_prefix)
117
+ job.logs.in_batches(of: @config.limit.to_i).map do |logs_batch|
118
+ file_prefix = "#{file_prefix}_job_#{job.id}"
119
+ save_logs_batch(logs_batch, file_prefix)
120
+ end.compact.reduce(&:&)
121
+ end
122
+
123
+ def save_logs_batch(logs_batch, file_prefix)
124
+ logs_export = logs_batch.map(&:attributes)
125
+ file_name = "#{file_prefix}_logs_#{logs_batch.first.id}-#{logs_batch.last.id}.json"
126
+ pretty_json = JSON.pretty_generate(logs_export)
127
+ save_file(file_name, pretty_json)
128
+ end
129
+
130
+ def destroy_builds_batch(builds_batch)
131
+ return destroy_builds_batch_dry(builds_batch) if @config.dry_run
132
+
133
+ builds_batch.each(&:destroy)
134
+ end
135
+
136
+ def destroy_builds_batch_dry(builds_batch)
137
+ @dry_run_reporter.add_to_report(:builds, *builds_batch.map(&:id))
138
+
139
+ jobs_ids = builds_batch.map do |build|
140
+ build.jobs.map(&:id) || []
141
+ end.flatten
142
+
143
+ @dry_run_reporter.add_to_report(:jobs, *jobs_ids)
144
+
145
+ logs_ids = builds_batch.map do |build|
146
+ build.jobs.map do |job|
147
+ job.logs.map(&:id) || []
148
+ end.flatten || []
149
+ end.flatten
150
+
151
+ @dry_run_reporter.add_to_report(:logs, *logs_ids)
152
+ end
153
+
154
+ def save_and_destroy_requests_batch(requests_batch, repository)
155
+ requests_export = export_requests(requests_batch)
156
+ file_name = "repository_#{repository.id}_requests_#{requests_batch.first.id}-#{requests_batch.last.id}.json"
157
+ pretty_json = JSON.pretty_generate(requests_export)
158
+ if save_file(file_name, pretty_json)
159
+ destroy_requests_batch(requests_batch)
160
+ end
161
+ requests_export
162
+ end
163
+
164
+ def destroy_requests_batch(requests_batch)
165
+ return destroy_requests_batch_dry(requests_batch) if @config.dry_run
166
+
167
+ requests_batch.each(&:destroy)
168
+ end
169
+
170
+ def destroy_requests_batch_dry(requests_batch)
171
+ @dry_run_reporter.add_to_report(:requests, *requests_batch.map(&:id))
172
+ end
173
+
174
+ def save_file(file_name, content) # rubocop:disable Metrics/MethodLength
175
+ return true if @config.dry_run
176
+
177
+ saved = false
178
+ begin
179
+ unless File.directory?(@config.files_location)
180
+ FileUtils.mkdir_p(@config.files_location)
181
+ end
182
+
183
+ File.open(file_path(file_name), 'w') do |file|
184
+ file.write(content)
185
+ file.close
186
+ saved = true
187
+ end
188
+ rescue => e
189
+ print "Failed to save #{file_name}, error: #{e.inspect}\n"
190
+ end
191
+ saved
192
+ end
193
+
194
+ def file_path(file_name)
195
+ "#{@config.files_location}/#{file_name}"
196
+ end
197
+
198
+ def export_requests(requests)
199
+ requests.map do |request|
200
+ request.attributes
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Backup
4
+ class RemoveOrphans
5
+ attr_reader :config
6
+
7
+ def initialize(config, dry_run_reporter=nil)
8
+ @config = config
9
+ @dry_run_reporter = dry_run_reporter
10
+ end
11
+
12
+ def dry_run_report
13
+ @dry_run_reporter.report
14
+ end
15
+
16
+ def run
17
+ cases.each do |model_block|
18
+ model_block[:relations].each do |relation|
19
+ process_table(
20
+ main_model: model_block[:main_model],
21
+ related_model: relation[:related_model],
22
+ fk_name: relation[:fk_name],
23
+ method: model_block[:method],
24
+ dry_run_complement: model_block[:dry_run_complement]
25
+ )
26
+ end
27
+ end
28
+ end
29
+
30
+ def cases
31
+ [
32
+ {
33
+ main_model: Repository,
34
+ relations: [
35
+ {related_model: Build, fk_name: 'current_build_id'},
36
+ {related_model: Build, fk_name: 'last_build_id'}
37
+ ]
38
+ }, {
39
+ main_model: Build,
40
+ relations: [
41
+ {related_model: Repository, fk_name: 'repository_id'},
42
+ {related_model: Commit, fk_name: 'commit_id'},
43
+ {related_model: Request, fk_name: 'request_id'},
44
+ {related_model: PullRequest, fk_name: 'pull_request_id'},
45
+ {related_model: Branch, fk_name: 'branch_id'},
46
+ {related_model: Tag, fk_name: 'tag_id'}
47
+ ],
48
+ method: :destroy_all,
49
+ dry_run_complement: -> (ids) { add_builds_dependencies_to_dry_run_report(ids) }
50
+ }, {
51
+ main_model: Job,
52
+ relations: [
53
+ {related_model: Repository, fk_name: 'repository_id'},
54
+ {related_model: Commit, fk_name: 'commit_id'},
55
+ {related_model: Stage, fk_name: 'stage_id'},
56
+ ]
57
+ }, {
58
+ main_model: Branch,
59
+ relations: [
60
+ {related_model: Repository, fk_name: 'repository_id'},
61
+ {related_model: Build, fk_name: 'last_build_id'}
62
+ ]
63
+ }, {
64
+ main_model: Tag,
65
+ relations: [
66
+ {related_model: Repository, fk_name: 'repository_id'},
67
+ {related_model: Build, fk_name: 'last_build_id'}
68
+ ]
69
+ }, {
70
+ main_model: Commit,
71
+ relations: [
72
+ {related_model: Repository, fk_name: 'repository_id'},
73
+ {related_model: Branch, fk_name: 'branch_id'},
74
+ {related_model: Tag, fk_name: 'tag_id'}
75
+ ]
76
+ }, {
77
+ main_model: Cron,
78
+ relations: [
79
+ {related_model: Branch, fk_name: 'branch_id'}
80
+ ]
81
+ }, {
82
+ main_model: PullRequest,
83
+ relations: [
84
+ {related_model: Repository, fk_name: 'repository_id'}
85
+ ]
86
+ }, {
87
+ main_model: SslKey,
88
+ relations: [
89
+ {related_model: Repository, fk_name: 'repository_id'}
90
+ ]
91
+ }, {
92
+ main_model: Request,
93
+ relations: [
94
+ {related_model: Commit, fk_name: 'commit_id'},
95
+ {related_model: PullRequest, fk_name: 'pull_request_id'},
96
+ {related_model: Branch, fk_name: 'branch_id'},
97
+ {related_model: Tag, fk_name: 'tag_id'}
98
+ ]
99
+ }, {
100
+ main_model: Stage,
101
+ relations: [
102
+ {related_model: Build, fk_name: 'build_id'}
103
+ ]
104
+ }
105
+ ]
106
+ end
107
+
108
+ def add_builds_dependencies_to_dry_run_report(ids_for_delete)
109
+ repos_for_delete = Repository.where(current_build_id: ids_for_delete)
110
+ jobs_for_delete = Job.where(source_id: ids_for_delete)
111
+ @dry_run_reporter.add_to_report(:repositories, *repos_for_delete.map(&:id))
112
+ @dry_run_reporter.add_to_report(:jobs, *jobs_for_delete.map(&:id))
113
+ end
114
+
115
+ def process_table(args)
116
+ main_model = args[:main_model]
117
+ related_model = args[:related_model]
118
+ fk_name = args[:fk_name]
119
+ method = args[:method] || :delete_all
120
+ dry_run_complement = args[:dry_run_complement]
121
+
122
+ main_table = main_model.table_name
123
+ related_table = related_model.table_name
124
+
125
+ for_delete = main_model.find_by_sql(%{
126
+ select a.*
127
+ from #{main_table} a
128
+ left join #{related_table} b
129
+ on a.#{fk_name} = b.id
130
+ where
131
+ a.#{fk_name} is not null
132
+ and b.id is null;
133
+ })
134
+
135
+ ids_for_delete = for_delete.map(&:id)
136
+
137
+ if config.dry_run
138
+ key = main_table.to_sym
139
+ @dry_run_reporter.add_to_report(key, *ids_for_delete)
140
+ dry_run_complement.call(ids_for_delete) if dry_run_complement
141
+ else
142
+ main_model.where(id: ids_for_delete).send(method)
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/config.rb CHANGED
@@ -67,19 +67,19 @@ class Config
67
67
  @user_id = first_not_nil(
68
68
  args[:user_id],
69
69
  argv_opts[:user_id],
70
- ENV['USER_ID'],
70
+ ENV['BACKUP_USER_ID'],
71
71
  config.dig('backup', 'user_id')
72
72
  )
73
73
  @repo_id = first_not_nil(
74
74
  args[:repo_id],
75
75
  argv_opts[:repo_id],
76
- ENV['REPO_ID'],
76
+ ENV['BACKUP_REPO_ID'],
77
77
  config.dig('backup', 'repo_id')
78
78
  )
79
79
  @org_id = first_not_nil(
80
80
  args[:org_id],
81
81
  argv_opts[:org_id],
82
- ENV['ORG_ID'],
82
+ ENV['BACKUP_ORG_ID'],
83
83
  config.dig('backup', 'org_id')
84
84
  )
85
85
  @move_logs = first_not_nil(
data/lib/db_helper.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DbHelper
4
+ def initialize(config)
5
+ @config = config
6
+ connect_db
7
+ end
8
+
9
+ def connect_db(config_or_url=@config.database_url)
10
+ ActiveRecord::Base.establish_connection(config_or_url)
11
+ end
12
+
13
+ def do_in_other_db(config_or_url)
14
+ saved_config = ActiveRecord::Base.connection_db_config
15
+ connect_db(config_or_url)
16
+ result = yield
17
+ connect_db(saved_config)
18
+ result
19
+ end
20
+
21
+ def do_without_triggers
22
+ ActiveRecord::Base.connection.execute('set session_replication_role = replica;')
23
+ result = yield
24
+ ActiveRecord::Base.connection.execute('set session_replication_role = default;')
25
+ result
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DryRunReporter
4
+ attr_reader :report
5
+
6
+ def initialize
7
+ @report = {}
8
+ end
9
+
10
+ def add_to_report(key, *values)
11
+ report[key] = [] if report[key].nil?
12
+ report[key].concat(values)
13
+ report[key].uniq!
14
+ end
15
+
16
+ def print_report
17
+ if @report.to_a.map(&:second).flatten.empty?
18
+ puts 'Dry run active. No data would be removed in normal run.'
19
+ else
20
+ puts 'Dry run active. The following data would be removed in normal run:'
21
+
22
+ @report.to_a.map(&:first).each do |symbol|
23
+ print_report_line(symbol)
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def print_report_line(symbol)
31
+ puts " - #{symbol}: #{@report[symbol].to_json}" if @report[symbol].any?
32
+ end
33
+ end
data/lib/models/build.rb CHANGED
@@ -8,6 +8,7 @@ require 'models/repository'
8
8
  class Build < Model
9
9
  belongs_to :repository
10
10
  has_many :jobs, -> { order('id') }, foreign_key: :source_id, dependent: :destroy, class_name: 'Job'
11
+ has_one :repo_for_that_this_build_is_current, foreign_key: :current_build_id, dependent: :destroy, class_name: 'Repository'
11
12
 
12
13
  self.table_name = 'builds'
13
14
  end
@@ -6,7 +6,7 @@ require 'models/request'
6
6
 
7
7
  # Repository model
8
8
  class Repository < Model
9
- has_many :builds, -> { order('id') }, foreign_key: :repository_id, dependent: :destroy, class_name: 'Build'
9
+ has_many :builds, -> { order('id') }, foreign_key: :repository_id, class_name: 'Build'
10
10
  has_many :requests, -> { order('id') }, foreign_key: :repository_id, dependent: :destroy, class_name: 'Request'
11
11
 
12
12
  self.table_name = 'repositories'
data/lib/travis-backup.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'active_support/core_ext/array'
4
4
  require 'active_support/time'
5
5
  require 'config'
6
+ require 'db_helper'
7
+ require 'dry_run_reporter'
6
8
  require 'models/repository'
7
9
  require 'models/log'
8
10
  require 'models/branch'
@@ -13,288 +15,36 @@ require 'models/pull_request'
13
15
  require 'models/ssl_key'
14
16
  require 'models/request'
15
17
  require 'models/stage'
18
+ require 'backup/move_logs'
19
+ require 'backup/remove_orphans'
20
+ require 'backup/remove_old'
16
21
 
17
22
  # main travis-backup class
18
23
  class Backup
19
24
  attr_accessor :config
20
- attr_reader :dry_run_report
21
25
 
22
26
  def initialize(config_args={})
23
27
  @config = Config.new(config_args)
28
+ @db_helper = DbHelper.new(@config)
24
29
 
25
30
  if @config.dry_run
26
- @dry_run_report = {builds: [], jobs: [], logs: [], requests: []}
31
+ @dry_run_reporter = DryRunReporter.new
27
32
  end
28
-
29
- connect_db
30
33
  end
31
34
 
32
- def connect_db(url=@config.database_url)
33
- ActiveRecord::Base.establish_connection(url)
35
+ def dry_run_report
36
+ @dry_run_reporter.report
34
37
  end
35
38
 
36
39
  def run(args={})
37
- user_id = args[:user_id] || @config.user_id
38
- repo_id = args[:repo_id] || @config.repo_id
39
- org_id = args[:org_id] || @config.org_id
40
-
41
- if user_id
42
- owner_id = user_id
43
- owner_type = 'User'
44
- elsif org_id
45
- owner_id = org_id
46
- owner_type = 'Organization'
47
- end
48
-
49
40
  if @config.move_logs
50
- move_logs
41
+ Backup::MoveLogs.new(@config, @db_helper, @dry_run_reporter).run
51
42
  elsif @config.remove_orphans
52
- remove_orphans
53
- elsif owner_id
54
- process_repos_for_owner(owner_id, owner_type)
55
- elsif repo_id
56
- process_repo_with_id(repo_id)
43
+ Backup::RemoveOrphans.new(@config, @dry_run_reporter).run
57
44
  else
58
- process_all_repos
59
- end
60
-
61
- print_dry_run_report if @config.dry_run
62
- end
63
-
64
- def process_repos_for_owner(owner_id, owner_type)
65
- Repository.where('owner_id = ? and owner_type = ?', owner_id, owner_type).order(:id).each do |repository|
66
- process_repo(repository)
67
- end
68
- end
69
-
70
- def process_repo_with_id(repo_id)
71
- process_repo(Repository.find(repo_id))
72
- end
73
-
74
- def process_all_repos
75
- Repository.order(:id).each do |repository|
76
- process_repo(repository)
77
- end
78
- end
79
-
80
- def print_dry_run_report
81
- if @dry_run_report.to_a.map(&:second).flatten.empty?
82
- puts 'Dry run active. No data would be removed in normal run.'
83
- else
84
- puts 'Dry run active. The following data would be removed in normal run:'
85
-
86
- @dry_run_report.to_a.map(&:first).each do |symbol|
87
- print_dry_run_report_line(symbol)
88
- end
89
- end
90
- end
91
-
92
- def print_dry_run_report_line(symbol)
93
- puts " - #{symbol}: #{@dry_run_report[symbol].to_json}" if @dry_run_report[symbol].any?
94
- end
95
-
96
- def process_repo(repository)
97
- process_repo_builds(repository)
98
- process_repo_requests(repository)
99
- end
100
-
101
- def process_repo_builds(repository) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
102
- threshold = @config.threshold.to_i.months.ago.to_datetime
103
- current_build_id = repository.current_build_id || -1
104
- repository.builds.where('created_at < ? and id != ?', threshold, current_build_id)
105
- .in_groups_of(@config.limit.to_i, false).map do |builds_batch|
106
- @config.if_backup ? save_and_destroy_builds_batch(builds_batch, repository) : destroy_builds_batch(builds_batch)
107
- end.compact
108
- end
109
-
110
- def process_repo_requests(repository)
111
- threshold = @config.threshold.to_i.months.ago.to_datetime
112
- repository.requests.where('created_at < ?', threshold)
113
- .in_groups_of(@config.limit.to_i, false).map do |requests_batch|
114
- @config.if_backup ? save_and_destroy_requests_batch(requests_batch, repository) : destroy_requests_batch(requests_batch)
115
- end.compact
116
- end
117
-
118
- def move_logs
119
- return move_logs_dry if config.dry_run
120
-
121
- connect_db(@config.database_url)
122
- Log.order(:id).in_groups_of(@config.limit.to_i, false).map do |logs_batch|
123
- log_hashes = logs_batch.as_json
124
- connect_db(@config.destination_db_url)
125
-
126
- log_hashes.each do |log_hash|
127
- new_log = Log.new(log_hash)
128
- new_log.save!
129
- end
130
-
131
- connect_db(@config.database_url)
132
-
133
- logs_batch.each(&:destroy)
45
+ Backup::RemoveOld.new(@config, @dry_run_reporter).run(args)
134
46
  end
135
- end
136
-
137
- def move_logs_dry
138
- dry_run_report[:logs].concat(Log.order(:id).map(&:id))
139
- end
140
-
141
- def remove_orphans
142
- remove_orphans_for_table(Repository, 'repositories', 'builds', 'current_build_id')
143
- remove_orphans_for_table(Repository, 'repositories', 'builds', 'last_build_id')
144
- remove_orphans_for_table(Build, 'builds', 'repositories', 'repository_id')
145
- remove_orphans_for_table(Build, 'builds', 'commits', 'commit_id')
146
- remove_orphans_for_table(Build, 'builds', 'requests', 'request_id')
147
- remove_orphans_for_table(Build, 'builds', 'pull_requests', 'pull_request_id')
148
- remove_orphans_for_table(Build, 'builds', 'branches', 'branch_id')
149
- remove_orphans_for_table(Build, 'builds', 'tags', 'tag_id')
150
- remove_orphans_for_table(Job, 'jobs', 'repositories', 'repository_id')
151
- remove_orphans_for_table(Job, 'jobs', 'commits', 'commit_id')
152
- remove_orphans_for_table(Job, 'jobs', 'stages', 'stage_id')
153
- remove_orphans_for_table(Branch, 'branches', 'repositories', 'repository_id')
154
- remove_orphans_for_table(Branch, 'branches', 'builds', 'last_build_id')
155
- remove_orphans_for_table(Tag, 'tags', 'repositories', 'repository_id')
156
- remove_orphans_for_table(Tag, 'tags', 'builds', 'last_build_id')
157
- remove_orphans_for_table(Commit, 'commits', 'repositories', 'repository_id')
158
- remove_orphans_for_table(Commit, 'commits', 'branches', 'branch_id')
159
- remove_orphans_for_table(Commit, 'commits', 'tags', 'tag_id')
160
- remove_orphans_for_table(Cron, 'crons', 'branches', 'branch_id')
161
- remove_orphans_for_table(PullRequest, 'pull_requests', 'repositories', 'repository_id')
162
- remove_orphans_for_table(SslKey, 'ssl_keys', 'repositories', 'repository_id')
163
- remove_orphans_for_table(Request, 'requests', 'commits', 'commit_id')
164
- remove_orphans_for_table(Request, 'requests', 'pull_requests', 'pull_request_id')
165
- remove_orphans_for_table(Request, 'requests', 'branches', 'branch_id')
166
- remove_orphans_for_table(Request, 'requests', 'tags', 'tag_id')
167
- remove_orphans_for_table(Stage, 'stages', 'builds', 'build_id')
168
- end
169
-
170
- def remove_orphans_for_table(model_class, table_a_name, table_b_name, fk_name)
171
- for_delete = model_class.find_by_sql(%{
172
- select a.*
173
- from #{table_a_name} a
174
- left join #{table_b_name} b
175
- on a.#{fk_name} = b.id
176
- where
177
- a.#{fk_name} is not null
178
- and b.id is null;
179
- })
180
47
 
181
- if config.dry_run
182
- key = table_a_name.to_sym
183
- dry_run_report[key] = [] if dry_run_report[key].nil?
184
- dry_run_report[key].concat(for_delete.map(&:id))
185
- dry_run_report[key].uniq!
186
- else
187
- model_class.where(id: for_delete.map(&:id)).delete_all
188
- end
189
- end
190
-
191
- private
192
-
193
- def save_and_destroy_builds_batch(builds_batch, repository)
194
- builds_export = export_builds(builds_batch)
195
- file_name = "repository_#{repository.id}_builds_#{builds_batch.first.id}-#{builds_batch.last.id}.json"
196
- pretty_json = JSON.pretty_generate(builds_export)
197
- if save_file(file_name, pretty_json)
198
- destroy_builds_batch(builds_batch)
199
- end
200
- builds_export
201
- end
202
-
203
- def destroy_builds_batch(builds_batch)
204
- return destroy_builds_batch_dry(builds_batch) if @config.dry_run
205
-
206
- builds_batch.each(&:destroy)
207
- end
208
-
209
- def destroy_builds_batch_dry(builds_batch)
210
- @dry_run_report[:builds].concat(builds_batch.map(&:id))
211
-
212
- jobs = builds_batch.map do |build|
213
- build.jobs.map(&:id) || []
214
- end.flatten
215
-
216
- @dry_run_report[:jobs].concat(jobs)
217
-
218
- logs = builds_batch.map do |build|
219
- build.jobs.map do |job|
220
- job.logs.map(&:id) || []
221
- end.flatten || []
222
- end.flatten
223
-
224
- @dry_run_report[:logs].concat(logs)
225
- end
226
-
227
- def save_and_destroy_requests_batch(requests_batch, repository)
228
- requests_export = export_requests(requests_batch)
229
- file_name = "repository_#{repository.id}_requests_#{requests_batch.first.id}-#{requests_batch.last.id}.json"
230
- pretty_json = JSON.pretty_generate(requests_export)
231
- if save_file(file_name, pretty_json)
232
- destroy_requests_batch(requests_batch)
233
- end
234
- requests_export
235
- end
236
-
237
- def destroy_requests_batch(requests_batch)
238
- return destroy_requests_batch_dry(requests_batch) if @config.dry_run
239
-
240
- requests_batch.each(&:destroy)
241
- end
242
-
243
- def destroy_requests_batch_dry(requests_batch)
244
- @dry_run_report[:requests].concat(requests_batch.map(&:id))
245
- end
246
-
247
- def save_file(file_name, content) # rubocop:disable Metrics/MethodLength
248
- return true if @config.dry_run
249
-
250
- saved = false
251
- begin
252
- unless File.directory?(@config.files_location)
253
- FileUtils.mkdir_p(@config.files_location)
254
- end
255
-
256
- File.open(file_path(file_name), 'w') do |file|
257
- file.write(content)
258
- file.close
259
- saved = true
260
- end
261
- rescue => e
262
- print "Failed to save #{file_name}, error: #{e.inspect}\n"
263
- end
264
- saved
265
- end
266
-
267
- def file_path(file_name)
268
- "#{@config.files_location}/#{file_name}"
269
- end
270
-
271
- def export_builds(builds)
272
- builds.map do |build|
273
- build_export = build.attributes
274
- build_export[:jobs] = export_jobs(build.jobs)
275
-
276
- build_export
277
- end
278
- end
279
-
280
- def export_jobs(jobs)
281
- jobs.map do |job|
282
- job_export = job.attributes
283
- job_export[:logs] = export_logs(job.logs)
284
-
285
- job_export
286
- end
287
- end
288
-
289
- def export_logs(logs)
290
- logs.map do |log|
291
- log.attributes
292
- end
293
- end
294
-
295
- def export_requests(requests)
296
- requests.map do |request|
297
- request.attributes
298
- end
48
+ @dry_run_reporter.print_report if @config.dry_run
299
49
  end
300
50
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'travis-backup'
3
- s.version = '0.1.0'
3
+ s.version = '0.2.0'
4
4
  s.summary = 'Travis CI backup tool'
5
5
  s.authors = ['Karol Selak']
6
6
  s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'listen'
27
27
  s.add_development_dependency 'rubocop', '~> 0.75.1'
28
28
  s.add_development_dependency 'rubocop-rspec'
29
+ s.add_development_dependency 'database_cleaner-active_record'
29
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: travis-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Selak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-07 00:00:00.000000000 Z
11
+ date: 2021-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: database_cleaner-active_record
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
195
209
  description:
196
210
  email:
197
211
  executables:
@@ -209,7 +223,6 @@ files:
209
223
  - ".gitignore"
210
224
  - ".travis.yml"
211
225
  - Gemfile
212
- - Gemfile.lock
213
226
  - README.md
214
227
  - Rakefile
215
228
  - bin/bundle
@@ -247,7 +260,12 @@ files:
247
260
  - config/spring.rb
248
261
  - db/schema.sql
249
262
  - dump/.keep
263
+ - lib/backup/move_logs.rb
264
+ - lib/backup/remove_old.rb
265
+ - lib/backup/remove_orphans.rb
250
266
  - lib/config.rb
267
+ - lib/db_helper.rb
268
+ - lib/dry_run_reporter.rb
251
269
  - lib/models/branch.rb
252
270
  - lib/models/build.rb
253
271
  - lib/models/commit.rb
data/Gemfile.lock DELETED
@@ -1,212 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- travis-backup (0.1.0)
5
- activerecord
6
- bootsnap
7
- pg
8
- pry
9
- rails
10
- tzinfo-data
11
-
12
- GEM
13
- remote: https://rubygems.org/
14
- specs:
15
- actioncable (6.1.4.1)
16
- actionpack (= 6.1.4.1)
17
- activesupport (= 6.1.4.1)
18
- nio4r (~> 2.0)
19
- websocket-driver (>= 0.6.1)
20
- actionmailbox (6.1.4.1)
21
- actionpack (= 6.1.4.1)
22
- activejob (= 6.1.4.1)
23
- activerecord (= 6.1.4.1)
24
- activestorage (= 6.1.4.1)
25
- activesupport (= 6.1.4.1)
26
- mail (>= 2.7.1)
27
- actionmailer (6.1.4.1)
28
- actionpack (= 6.1.4.1)
29
- actionview (= 6.1.4.1)
30
- activejob (= 6.1.4.1)
31
- activesupport (= 6.1.4.1)
32
- mail (~> 2.5, >= 2.5.4)
33
- rails-dom-testing (~> 2.0)
34
- actionpack (6.1.4.1)
35
- actionview (= 6.1.4.1)
36
- activesupport (= 6.1.4.1)
37
- rack (~> 2.0, >= 2.0.9)
38
- rack-test (>= 0.6.3)
39
- rails-dom-testing (~> 2.0)
40
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
41
- actiontext (6.1.4.1)
42
- actionpack (= 6.1.4.1)
43
- activerecord (= 6.1.4.1)
44
- activestorage (= 6.1.4.1)
45
- activesupport (= 6.1.4.1)
46
- nokogiri (>= 1.8.5)
47
- actionview (6.1.4.1)
48
- activesupport (= 6.1.4.1)
49
- builder (~> 3.1)
50
- erubi (~> 1.4)
51
- rails-dom-testing (~> 2.0)
52
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
53
- activejob (6.1.4.1)
54
- activesupport (= 6.1.4.1)
55
- globalid (>= 0.3.6)
56
- activemodel (6.1.4.1)
57
- activesupport (= 6.1.4.1)
58
- activerecord (6.1.4.1)
59
- activemodel (= 6.1.4.1)
60
- activesupport (= 6.1.4.1)
61
- activestorage (6.1.4.1)
62
- actionpack (= 6.1.4.1)
63
- activejob (= 6.1.4.1)
64
- activerecord (= 6.1.4.1)
65
- activesupport (= 6.1.4.1)
66
- marcel (~> 1.0.0)
67
- mini_mime (>= 1.1.0)
68
- activesupport (6.1.4.1)
69
- concurrent-ruby (~> 1.0, >= 1.0.2)
70
- i18n (>= 1.6, < 2)
71
- minitest (>= 5.1)
72
- tzinfo (~> 2.0)
73
- zeitwerk (~> 2.3)
74
- ast (2.4.2)
75
- bootsnap (1.7.7)
76
- msgpack (~> 1.0)
77
- brakeman (5.1.1)
78
- builder (3.2.4)
79
- byebug (11.1.3)
80
- coderay (1.1.3)
81
- concurrent-ruby (1.1.9)
82
- crass (1.0.6)
83
- diff-lcs (1.4.4)
84
- erubi (1.10.0)
85
- factory_bot (6.2.0)
86
- activesupport (>= 5.0.0)
87
- ffi (1.15.3)
88
- globalid (0.5.2)
89
- activesupport (>= 5.0)
90
- i18n (1.8.10)
91
- concurrent-ruby (~> 1.0)
92
- jaro_winkler (1.5.4)
93
- listen (3.7.0)
94
- rb-fsevent (~> 0.10, >= 0.10.3)
95
- rb-inotify (~> 0.9, >= 0.9.10)
96
- loofah (2.12.0)
97
- crass (~> 1.0.2)
98
- nokogiri (>= 1.5.9)
99
- mail (2.7.1)
100
- mini_mime (>= 0.1.1)
101
- marcel (1.0.1)
102
- method_source (1.0.0)
103
- mini_mime (1.1.1)
104
- mini_portile2 (2.6.1)
105
- minitest (5.14.4)
106
- msgpack (1.4.2)
107
- nio4r (2.5.8)
108
- nokogiri (1.12.3)
109
- mini_portile2 (~> 2.6.1)
110
- racc (~> 1.4)
111
- parallel (1.20.1)
112
- parser (3.0.2.0)
113
- ast (~> 2.4.1)
114
- pg (1.2.3)
115
- pry (0.14.1)
116
- coderay (~> 1.1)
117
- method_source (~> 1.0)
118
- racc (1.5.2)
119
- rack (2.2.3)
120
- rack-test (1.1.0)
121
- rack (>= 1.0, < 3)
122
- rails (6.1.4.1)
123
- actioncable (= 6.1.4.1)
124
- actionmailbox (= 6.1.4.1)
125
- actionmailer (= 6.1.4.1)
126
- actionpack (= 6.1.4.1)
127
- actiontext (= 6.1.4.1)
128
- actionview (= 6.1.4.1)
129
- activejob (= 6.1.4.1)
130
- activemodel (= 6.1.4.1)
131
- activerecord (= 6.1.4.1)
132
- activestorage (= 6.1.4.1)
133
- activesupport (= 6.1.4.1)
134
- bundler (>= 1.15.0)
135
- railties (= 6.1.4.1)
136
- sprockets-rails (>= 2.0.0)
137
- rails-dom-testing (2.0.3)
138
- activesupport (>= 4.2.0)
139
- nokogiri (>= 1.6)
140
- rails-html-sanitizer (1.4.1)
141
- loofah (~> 2.3)
142
- railties (6.1.4.1)
143
- actionpack (= 6.1.4.1)
144
- activesupport (= 6.1.4.1)
145
- method_source
146
- rake (>= 0.13)
147
- thor (~> 1.0)
148
- rainbow (3.0.0)
149
- rake (13.0.6)
150
- rb-fsevent (0.11.0)
151
- rb-inotify (0.10.1)
152
- ffi (~> 1.0)
153
- rspec-core (3.10.1)
154
- rspec-support (~> 3.10.0)
155
- rspec-expectations (3.10.1)
156
- diff-lcs (>= 1.2.0, < 2.0)
157
- rspec-support (~> 3.10.0)
158
- rspec-mocks (3.10.2)
159
- diff-lcs (>= 1.2.0, < 2.0)
160
- rspec-support (~> 3.10.0)
161
- rspec-rails (5.0.2)
162
- actionpack (>= 5.2)
163
- activesupport (>= 5.2)
164
- railties (>= 5.2)
165
- rspec-core (~> 3.10)
166
- rspec-expectations (~> 3.10)
167
- rspec-mocks (~> 3.10)
168
- rspec-support (~> 3.10)
169
- rspec-support (3.10.2)
170
- rubocop (0.75.1)
171
- jaro_winkler (~> 1.5.1)
172
- parallel (~> 1.10)
173
- parser (>= 2.6)
174
- rainbow (>= 2.2.2, < 4.0)
175
- ruby-progressbar (~> 1.7)
176
- unicode-display_width (>= 1.4.0, < 1.7)
177
- rubocop-rspec (1.41.0)
178
- rubocop (>= 0.68.1)
179
- ruby-progressbar (1.11.0)
180
- sprockets (4.0.2)
181
- concurrent-ruby (~> 1.0)
182
- rack (> 1, < 3)
183
- sprockets-rails (3.2.2)
184
- actionpack (>= 4.0)
185
- activesupport (>= 4.0)
186
- sprockets (>= 3.0.0)
187
- thor (1.1.0)
188
- tzinfo (2.0.4)
189
- concurrent-ruby (~> 1.0)
190
- tzinfo-data (1.2021.1)
191
- tzinfo (>= 1.0.0)
192
- unicode-display_width (1.6.1)
193
- websocket-driver (0.7.5)
194
- websocket-extensions (>= 0.1.0)
195
- websocket-extensions (0.1.5)
196
- zeitwerk (2.4.2)
197
-
198
- PLATFORMS
199
- ruby
200
-
201
- DEPENDENCIES
202
- brakeman
203
- byebug
204
- factory_bot
205
- listen
206
- rspec-rails
207
- rubocop (~> 0.75.1)
208
- rubocop-rspec
209
- travis-backup!
210
-
211
- BUNDLED WITH
212
- 2.1.4