wordmove-ng 6.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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/stale.yml +18 -0
- data/.github/workflows/release.yml +100 -0
- data/.github/workflows/ruby.yml +41 -0
- data/.gitignore +21 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +50 -0
- data/.rubocop_todo.yml +89 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.vscode/launch.json +73 -0
- data/CHANGELOG.md +78 -0
- data/CLAUDE.md +114 -0
- data/CONTRIBUTING.md +114 -0
- data/Gemfile +14 -0
- data/LICENSE +22 -0
- data/README.md +340 -0
- data/Rakefile +7 -0
- data/assets/images/wordmove-ng.png +0 -0
- data/bin/bundle +105 -0
- data/bin/bundler +17 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +16 -0
- data/bin/htmldiff +29 -0
- data/bin/kwalify +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/rumoji +29 -0
- data/bin/setup +7 -0
- data/bin/thor +29 -0
- data/bin/wordmove-ng +10 -0
- data/deploy/deploy.sh +3 -0
- data/exe/wordmove-ng +6 -0
- data/lib/wordmove/assets/wordmove_schema_global.yml +16 -0
- data/lib/wordmove/assets/wordmove_schema_local.yml +31 -0
- data/lib/wordmove/assets/wordmove_schema_remote.yml +167 -0
- data/lib/wordmove/cli.rb +134 -0
- data/lib/wordmove/deployer/base.rb +356 -0
- data/lib/wordmove/deployer/ssh.rb +339 -0
- data/lib/wordmove/doctor/movefile.rb +112 -0
- data/lib/wordmove/doctor/mysql.rb +136 -0
- data/lib/wordmove/doctor/rsync.rb +27 -0
- data/lib/wordmove/doctor/ssh.rb +90 -0
- data/lib/wordmove/doctor/wpcli.rb +43 -0
- data/lib/wordmove/doctor.rb +67 -0
- data/lib/wordmove/environments_list.rb +68 -0
- data/lib/wordmove/exceptions.rb +10 -0
- data/lib/wordmove/generators/movefile.rb +50 -0
- data/lib/wordmove/generators/movefile.yml +104 -0
- data/lib/wordmove/generators/movefile_adapter.rb +188 -0
- data/lib/wordmove/guardian.rb +37 -0
- data/lib/wordmove/hook.rb +128 -0
- data/lib/wordmove/logger.rb +165 -0
- data/lib/wordmove/movefile.rb +129 -0
- data/lib/wordmove/prerequisites.rb +57 -0
- data/lib/wordmove/sql_adapter/wpcli.rb +72 -0
- data/lib/wordmove/ssh_runner.rb +110 -0
- data/lib/wordmove/version.rb +3 -0
- data/lib/wordmove/wordpress_directory/path.rb +11 -0
- data/lib/wordmove/wordpress_directory.rb +41 -0
- data/lib/wordmove-ng.rb +4 -0
- data/lib/wordmove.rb +51 -0
- data/release-please-config.json +25 -0
- data/wordmove-ng.gemspec +62 -0
- metadata +318 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
|
|
3
|
+
module Wordmove
|
|
4
|
+
module Deployer
|
|
5
|
+
# Syncs files with rsync and the database with mysqldump/mysql over SSH.
|
|
6
|
+
#
|
|
7
|
+
# Database sync follows the same shape in both directions: dump the
|
|
8
|
+
# source, import the dump on the target, then run `wp search-replace` on
|
|
9
|
+
# the target for vhost and wordpress_path. The source database is only
|
|
10
|
+
# ever read, and wp-cli is required on whichever side is the target
|
|
11
|
+
# (remote for push, local for pull).
|
|
12
|
+
class SSH < Base
|
|
13
|
+
attr_reader :local_dump_path,
|
|
14
|
+
:local_backup_path,
|
|
15
|
+
:local_gzipped_dump_path,
|
|
16
|
+
:local_gzipped_backup_path
|
|
17
|
+
|
|
18
|
+
def initialize(environment, options)
|
|
19
|
+
super
|
|
20
|
+
|
|
21
|
+
@runner = SshRunner.new(remote_options[:ssh])
|
|
22
|
+
@copier = Photocopier::SSH.new(photocopier_options).tap { |c| c.logger = logger }
|
|
23
|
+
|
|
24
|
+
@local_dump_path = local_wp_content_dir.path("dump.sql")
|
|
25
|
+
@local_backup_path = local_wp_content_dir.path("local-backup-#{Time.now.to_i}.sql")
|
|
26
|
+
@local_gzipped_dump_path = local_dump_path + '.gz'
|
|
27
|
+
@local_gzipped_backup_path = local_wp_content_dir
|
|
28
|
+
.path("#{environment}-backup-#{Time.now.to_i}.sql.gz")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Photocopier deletes :gateway and :rsync_options from the hash it is given
|
|
34
|
+
# and we append --dry-run when simulating, so hand it a copy rather than the
|
|
35
|
+
# hash shared with the movefile options.
|
|
36
|
+
def photocopier_options
|
|
37
|
+
ssh_options = remote_options[:ssh].dup
|
|
38
|
+
return ssh_options unless simulate?
|
|
39
|
+
|
|
40
|
+
ssh_options[:rsync_options] = [ssh_options[:rsync_options], '--dry-run'].compact.join(' ')
|
|
41
|
+
ssh_options
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def push_db
|
|
45
|
+
super
|
|
46
|
+
|
|
47
|
+
return true if simulate?
|
|
48
|
+
|
|
49
|
+
check_push_db_prerequisites!
|
|
50
|
+
backup_remote_db!
|
|
51
|
+
adapt_local_db!
|
|
52
|
+
after_push_cleanup!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def pull_db
|
|
56
|
+
super
|
|
57
|
+
|
|
58
|
+
return true if simulate?
|
|
59
|
+
|
|
60
|
+
check_pull_db_prerequisites!
|
|
61
|
+
backup_local_db!
|
|
62
|
+
adapt_remote_db!
|
|
63
|
+
after_pull_cleanup!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Both checks run before any backup or dump so a missing dependency has
|
|
67
|
+
# no side effects.
|
|
68
|
+
def check_push_db_prerequisites!
|
|
69
|
+
check_prerequisites!(local: Prerequisites::DB_SOURCE, remote: Prerequisites::DB_TARGET)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check_pull_db_prerequisites!
|
|
73
|
+
check_prerequisites!(local: Prerequisites::DB_TARGET, remote: Prerequisites::DB_SOURCE)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_prerequisites!(local:, remote:)
|
|
77
|
+
logger.task_step true, "checking prerequisites"
|
|
78
|
+
warn_about_prefix_collisions
|
|
79
|
+
missing_local = Prerequisites.missing_locally(local)
|
|
80
|
+
missing_remote = Prerequisites.missing_remotely(@runner, remote)
|
|
81
|
+
return true if missing_local.empty? && missing_remote.empty?
|
|
82
|
+
|
|
83
|
+
problems = []
|
|
84
|
+
problems << "locally: #{Prerequisites.describe(missing_local)}" if missing_local.any?
|
|
85
|
+
if missing_remote.any?
|
|
86
|
+
problems << "on \"#{environment}\": #{Prerequisites.describe(missing_remote)}"
|
|
87
|
+
end
|
|
88
|
+
raise UnmetPeerDependencyError,
|
|
89
|
+
"Missing programs required for the database sync (#{problems.join('; ')}). " \
|
|
90
|
+
"Install them, or make sure they are in the login shell $PATH."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def warn_about_prefix_collisions
|
|
94
|
+
Wordmove::Movefile.new(options[:config]).prefix_collisions(environment).each do |short, long|
|
|
95
|
+
logger.warn "\"#{short}\" is a prefix of \"#{long}\": the search-replace of the " \
|
|
96
|
+
"shorter value will also rewrite the longer one. Check the result."
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def backup_remote_db!
|
|
101
|
+
download_remote_db(local_gzipped_backup_path)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def adapt_local_db!
|
|
105
|
+
save_local_db(local_dump_path)
|
|
106
|
+
normalize_collations!(local_dump_path)
|
|
107
|
+
run compress_command(local_dump_path)
|
|
108
|
+
with_maintenance_mode(remote: true) do
|
|
109
|
+
import_remote_dump(local_gzipped_dump_path)
|
|
110
|
+
adapt_remote_db_after_import!
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def after_push_cleanup!
|
|
115
|
+
local_delete(local_gzipped_dump_path)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def backup_local_db!
|
|
119
|
+
save_local_db(local_backup_path)
|
|
120
|
+
run compress_command(local_backup_path)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def adapt_remote_db!
|
|
124
|
+
download_remote_db(local_gzipped_dump_path)
|
|
125
|
+
run uncompress_command(local_gzipped_dump_path)
|
|
126
|
+
normalize_collations!(local_dump_path)
|
|
127
|
+
with_maintenance_mode(remote: false) do
|
|
128
|
+
run mysql_import_command(local_dump_path, local_options[:database])
|
|
129
|
+
run_wpcli_search_replace(remote_options, local_options, :vhost)
|
|
130
|
+
run_wpcli_search_replace(remote_options, local_options, :wordpress_path)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Wraps the block in `wp maintenance-mode activate` / `deactivate` on the
|
|
135
|
+
# target when global.maintenance_mode (or WORDMOVE_MAINTENANCE_MODE) is
|
|
136
|
+
# set, so visitors see a maintenance page instead of a half-adapted site.
|
|
137
|
+
# Deactivation always runs, even when the block raises.
|
|
138
|
+
def with_maintenance_mode(remote:)
|
|
139
|
+
return yield unless maintenance_mode?
|
|
140
|
+
|
|
141
|
+
side = remote ? remote_options : local_options
|
|
142
|
+
runner = remote ? method(:remote_run) : method(:run)
|
|
143
|
+
runner.call(SqlAdapter::Wpcli.maintenance_mode_command(:activate, side[:wordpress_path]))
|
|
144
|
+
begin
|
|
145
|
+
yield
|
|
146
|
+
ensure
|
|
147
|
+
runner.call(SqlAdapter::Wpcli.maintenance_mode_command(:deactivate,
|
|
148
|
+
side[:wordpress_path]))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def maintenance_mode?
|
|
153
|
+
env = ENV.fetch('WORDMOVE_MAINTENANCE_MODE', nil)
|
|
154
|
+
return %w[1 true yes on].include?(env.strip.downcase) unless env.nil? || env.strip.empty?
|
|
155
|
+
|
|
156
|
+
options.dig(:global, :maintenance_mode) == true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def after_pull_cleanup!
|
|
160
|
+
local_delete(local_dump_path)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def adapt_remote_db_after_import!
|
|
164
|
+
remote_run_wpcli_search_replace(local_options, remote_options, :vhost)
|
|
165
|
+
remote_run_wpcli_search_replace(local_options, remote_options, :wordpress_path)
|
|
166
|
+
rescue ShellCommandError => e
|
|
167
|
+
logger.error "The remote database was imported but could not be adapted: #{e.message}"
|
|
168
|
+
logger.error "The remote site may now reference the local vhost or path. " \
|
|
169
|
+
"A backup of the remote database taken before the import is at: " \
|
|
170
|
+
"#{local_gzipped_backup_path}"
|
|
171
|
+
raise
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def wpcli_search_replace(from, to, config_key, path:, remote:)
|
|
175
|
+
return if options[:no_adapt]
|
|
176
|
+
|
|
177
|
+
logger.task_step !remote, "adapt dump for #{config_key}"
|
|
178
|
+
return if simulate?
|
|
179
|
+
|
|
180
|
+
SqlAdapter::Wpcli.new(from, to, config_key, path, remote: remote).command
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def run_wpcli_search_replace(from, to, config_key)
|
|
184
|
+
command = wpcli_search_replace(
|
|
185
|
+
from, to, config_key, path: local_options[:wordpress_path], remote: false
|
|
186
|
+
)
|
|
187
|
+
run(command) if command
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def remote_run_wpcli_search_replace(from, to, config_key)
|
|
191
|
+
command = wpcli_search_replace(
|
|
192
|
+
from, to, config_key, path: remote_options[:wordpress_path], remote: true
|
|
193
|
+
)
|
|
194
|
+
remote_run(command) if command
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Directory transfers go through rsync (Photocopier), which honours the
|
|
198
|
+
# --dry-run option set in #initialize when simulating.
|
|
199
|
+
%w[get_directory put_directory].each do |command|
|
|
200
|
+
define_method "remote_#{command}" do |*args|
|
|
201
|
+
logger.task_step false, "#{command}: #{args.join(' ')}"
|
|
202
|
+
@copier.send(command, *args)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Single file transfers and deletes go through the system scp/ssh binaries.
|
|
207
|
+
def remote_get(remote_path, local_path)
|
|
208
|
+
logger.task_step false, "get: #{remote_path} #{local_path}"
|
|
209
|
+
return true if simulate?
|
|
210
|
+
|
|
211
|
+
ensure_success!(@runner.get(remote_path, local_path), "get #{remote_path}")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def remote_put(local_path, remote_path)
|
|
215
|
+
logger.task_step false, "put: #{local_path} #{remote_path}"
|
|
216
|
+
return true if simulate?
|
|
217
|
+
|
|
218
|
+
ensure_success!(@runner.put(local_path, remote_path), "put #{remote_path}")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def remote_delete(remote_path)
|
|
222
|
+
logger.task_step false, "delete: #{remote_path}"
|
|
223
|
+
return true if simulate?
|
|
224
|
+
|
|
225
|
+
ensure_success!(@runner.delete(remote_path), "delete #{remote_path}")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def remote_run(command)
|
|
229
|
+
logger.task_step false, command
|
|
230
|
+
return true if simulate?
|
|
231
|
+
|
|
232
|
+
ensure_success!(@runner.run(command), command)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def ensure_success!(result, description)
|
|
236
|
+
_stdout, stderr, exit_code = result
|
|
237
|
+
return true if exit_code.zero?
|
|
238
|
+
|
|
239
|
+
raise(
|
|
240
|
+
ShellCommandError,
|
|
241
|
+
"Error code #{exit_code} returned by command \"#{description}\": #{stderr}"
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def download_remote_db(local_gizipped_dump_path)
|
|
246
|
+
remote_dump_path = remote_wp_content_dir.path("dump.sql")
|
|
247
|
+
# dump remote db into file
|
|
248
|
+
remote_run mysql_dump_command(remote_options[:database], remote_dump_path)
|
|
249
|
+
remote_run compress_command(remote_dump_path)
|
|
250
|
+
remote_dump_path += '.gz'
|
|
251
|
+
# download remote dump
|
|
252
|
+
remote_get(remote_dump_path, local_gizipped_dump_path)
|
|
253
|
+
remote_delete(remote_dump_path)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def import_remote_dump(local_gizipped_dump_path)
|
|
257
|
+
remote_dump_path = remote_wp_content_dir.path("dump.sql")
|
|
258
|
+
remote_gizipped_dump_path = remote_dump_path + '.gz'
|
|
259
|
+
|
|
260
|
+
remote_put(local_gizipped_dump_path, remote_gizipped_dump_path)
|
|
261
|
+
remote_run uncompress_command(remote_gizipped_dump_path)
|
|
262
|
+
remote_run mysql_import_command(remote_dump_path, remote_options[:database])
|
|
263
|
+
remote_delete(remote_dump_path)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
%w[uploads themes plugins mu_plugins languages].each do |task|
|
|
267
|
+
define_method "push_#{task}" do
|
|
268
|
+
logger.task "Pushing #{task.titleize}"
|
|
269
|
+
local_path = local_options[:wordpress_path]
|
|
270
|
+
remote_path = remote_options[:wordpress_path]
|
|
271
|
+
|
|
272
|
+
remote_put_directory(local_path, remote_path,
|
|
273
|
+
push_exclude_paths(task), push_inlcude_paths(task))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
define_method "pull_#{task}" do
|
|
277
|
+
logger.task "Pulling #{task.titleize}"
|
|
278
|
+
local_path = local_options[:wordpress_path]
|
|
279
|
+
remote_path = remote_options[:wordpress_path]
|
|
280
|
+
|
|
281
|
+
remote_get_directory(remote_path, local_path,
|
|
282
|
+
pull_exclude_paths(task), pull_include_paths(task))
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def push_inlcude_paths(task)
|
|
287
|
+
Pathname.new(send(:"local_#{task}_dir").relative_path)
|
|
288
|
+
.ascend
|
|
289
|
+
.each_with_object([]) do |directory, array|
|
|
290
|
+
path = directory.to_path
|
|
291
|
+
path.prepend('/') unless path.match? %r{^/}
|
|
292
|
+
path.concat('/') unless path.match? %r{/$}
|
|
293
|
+
array << path
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def push_exclude_paths(task)
|
|
298
|
+
Pathname.new(send(:"local_#{task}_dir").relative_path)
|
|
299
|
+
.dirname
|
|
300
|
+
.ascend
|
|
301
|
+
.each_with_object([]) do |directory, array|
|
|
302
|
+
path = directory.to_path
|
|
303
|
+
path.prepend('/') unless path.match? %r{^/}
|
|
304
|
+
path.concat('/') unless path.match? %r{/$}
|
|
305
|
+
path.concat('*')
|
|
306
|
+
array << path
|
|
307
|
+
end
|
|
308
|
+
.concat(paths_to_exclude)
|
|
309
|
+
.push('/*')
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def pull_include_paths(task)
|
|
313
|
+
Pathname.new(send(:"remote_#{task}_dir").relative_path)
|
|
314
|
+
.ascend
|
|
315
|
+
.each_with_object([]) do |directory, array|
|
|
316
|
+
path = directory.to_path
|
|
317
|
+
path.prepend('/') unless path.match? %r{^/}
|
|
318
|
+
path.concat('/') unless path.match? %r{/$}
|
|
319
|
+
array << path
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def pull_exclude_paths(task)
|
|
324
|
+
Pathname.new(send(:"remote_#{task}_dir").relative_path)
|
|
325
|
+
.dirname
|
|
326
|
+
.ascend
|
|
327
|
+
.each_with_object([]) do |directory, array|
|
|
328
|
+
path = directory.to_path
|
|
329
|
+
path.prepend('/') unless path.match? %r{^/}
|
|
330
|
+
path.concat('/') unless path.match? %r{/$}
|
|
331
|
+
path.concat('*')
|
|
332
|
+
array << path
|
|
333
|
+
end
|
|
334
|
+
.concat(paths_to_exclude)
|
|
335
|
+
.push('/*')
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module Wordmove
|
|
2
|
+
class Doctor
|
|
3
|
+
class Movefile
|
|
4
|
+
MANDATORY_SECTIONS = %i[local].freeze
|
|
5
|
+
OPTIONAL_SECTIONS = %i[global].freeze
|
|
6
|
+
attr_reader :movefile, :contents, :root_keys
|
|
7
|
+
|
|
8
|
+
def initialize(name = nil, dir = '.')
|
|
9
|
+
@movefile = Wordmove::Movefile.new(name, dir)
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
@contents = movefile.fetch
|
|
13
|
+
@root_keys = contents.keys
|
|
14
|
+
rescue Psych::SyntaxError
|
|
15
|
+
movefile.logger.error "Your movefile is not parsable due to a syntax error" \
|
|
16
|
+
"so we can't continue to validate it."
|
|
17
|
+
movefile.logger.debug "You could try to use https://yamlvalidator.com/ to" \
|
|
18
|
+
"get a clue about the problem."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate!
|
|
23
|
+
return false unless root_keys
|
|
24
|
+
|
|
25
|
+
MANDATORY_SECTIONS.each do |key|
|
|
26
|
+
movefile.logger.task "Validating movefile section: #{key}"
|
|
27
|
+
validate_mandatory_section(key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
OPTIONAL_SECTIONS.each do |key|
|
|
31
|
+
next unless root_keys.delete(key)
|
|
32
|
+
|
|
33
|
+
movefile.logger.task "Validating movefile section: #{key}"
|
|
34
|
+
validate_section(key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
root_keys.each do |remote|
|
|
38
|
+
movefile.logger.task "Validating movefile section: #{remote}"
|
|
39
|
+
validate_remote_section(remote)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def validate_section(key)
|
|
46
|
+
validator = validator_for(key)
|
|
47
|
+
|
|
48
|
+
errors = validator.validate(contents[key].deep_stringify_keys)
|
|
49
|
+
|
|
50
|
+
if errors && errors.empty?
|
|
51
|
+
movefile.logger.success "Formal validation passed"
|
|
52
|
+
|
|
53
|
+
return true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
errors.each do |e|
|
|
57
|
+
movefile.logger.error "[#{e.path}] #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_mandatory_section(key)
|
|
62
|
+
return false unless root_keys.delete(key) do
|
|
63
|
+
movefile.logger.error "#{key} section not present"
|
|
64
|
+
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
validate_section(key)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_remote_section(key)
|
|
72
|
+
return false unless validate_protocol_presence(contents[key].keys)
|
|
73
|
+
|
|
74
|
+
validate_prefix_collisions(key)
|
|
75
|
+
validate_section(key)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_prefix_collisions(key)
|
|
79
|
+
movefile.prefix_collisions(key).each do |short, long|
|
|
80
|
+
movefile.logger.error "\"#{short}\" is a prefix of \"#{long}\": wp search-replace " \
|
|
81
|
+
"would rewrite the longer value too. Use distinct vhosts " \
|
|
82
|
+
"and wordpress_paths for local and #{key}."
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validate_protocol_presence(keys)
|
|
87
|
+
if keys.include?(:ftp)
|
|
88
|
+
movefile.logger.error "This remote is configured with `ftp`, but FTP support was " \
|
|
89
|
+
"removed in wordmove-ng 6.0. Switch it to `ssh`."
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
return true if keys.include?(:ssh)
|
|
93
|
+
|
|
94
|
+
movefile.logger.error "This remote has no ssh protocol defined"
|
|
95
|
+
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validator_for(key)
|
|
100
|
+
suffix = if (MANDATORY_SECTIONS + OPTIONAL_SECTIONS).include? key
|
|
101
|
+
key
|
|
102
|
+
else
|
|
103
|
+
'remote'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
schema = Kwalify::Yaml.load_file("#{__dir__}/../assets/wordmove_schema_#{suffix}.yml")
|
|
107
|
+
|
|
108
|
+
Kwalify::Validator.new(schema)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module Wordmove
|
|
2
|
+
class Doctor
|
|
3
|
+
class Mysql
|
|
4
|
+
attr_reader :config, :logger
|
|
5
|
+
|
|
6
|
+
def initialize(movefile_name = nil, movefile_dir = '.')
|
|
7
|
+
@logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
|
|
8
|
+
begin
|
|
9
|
+
@config = Wordmove::Movefile.new(movefile_name, movefile_dir).fetch[:local][:database]
|
|
10
|
+
rescue Psych::SyntaxError
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def check!
|
|
16
|
+
logger.task "Checking local database commands and connection"
|
|
17
|
+
|
|
18
|
+
return logger.error "Can't connect to mysql using your movefile.yml" if config.nil?
|
|
19
|
+
|
|
20
|
+
mysql_client_doctor
|
|
21
|
+
mysqldump_doctor
|
|
22
|
+
mysql_server_doctor
|
|
23
|
+
mysql_database_doctor
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def mysql_client_doctor
|
|
29
|
+
if mysql_client_in_path?
|
|
30
|
+
logger.success "`mysql`/`mariadb` command is in $PATH"
|
|
31
|
+
else
|
|
32
|
+
logger.error "`mysql`/`mariadb` command is not in $PATH"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mysqldump_doctor
|
|
37
|
+
if mysqldump_in_path?
|
|
38
|
+
logger.success "`mysqldump`/`mariadb-dump` command is in $PATH"
|
|
39
|
+
else
|
|
40
|
+
logger.error "`mysqldump`/`mariadb-dump` command is not in $PATH"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def mysql_server_doctor
|
|
45
|
+
command = mysql_command
|
|
46
|
+
|
|
47
|
+
if system(command, out: File::NULL, err: File::NULL)
|
|
48
|
+
logger.success "Successfully connected to the MySQL server"
|
|
49
|
+
else
|
|
50
|
+
logger.error <<-LONG
|
|
51
|
+
We can't connect to the MySQL server using credentials
|
|
52
|
+
specified in the Movefile. Double check them or try
|
|
53
|
+
to debug your system configuration.
|
|
54
|
+
|
|
55
|
+
The command used to test was:
|
|
56
|
+
|
|
57
|
+
#{command}
|
|
58
|
+
LONG
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def mysql_database_doctor
|
|
63
|
+
command = mysql_command(database: config[:name])
|
|
64
|
+
|
|
65
|
+
if system(command, out: File::NULL, err: File::NULL)
|
|
66
|
+
logger.success "Successfully connected to the database"
|
|
67
|
+
else
|
|
68
|
+
logger.error <<-LONG
|
|
69
|
+
We can't connect to the database using credentials
|
|
70
|
+
specified in the Movefile, or the database does not
|
|
71
|
+
exists. Double check them or try to debug your
|
|
72
|
+
system configuration.
|
|
73
|
+
|
|
74
|
+
The command used to test was:
|
|
75
|
+
|
|
76
|
+
#{command}
|
|
77
|
+
LONG
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def mysql_command(database: nil)
|
|
82
|
+
mysql_options = config[:mysql_options]
|
|
83
|
+
command = [mysql_client_binary]
|
|
84
|
+
command << "--host=#{Shellwords.escape(config[:host])}" if config[:host].present?
|
|
85
|
+
command << "--port=#{Shellwords.escape(config[:port])}" if config[:port].present?
|
|
86
|
+
if (socket = mysql_socket_option)
|
|
87
|
+
command << "--socket=#{Shellwords.escape(socket)}"
|
|
88
|
+
end
|
|
89
|
+
command << "--user=#{Shellwords.escape(config[:user])}" if config[:user].present?
|
|
90
|
+
if config[:password].present?
|
|
91
|
+
command << "--password=#{Shellwords.escape(config[:password])}"
|
|
92
|
+
end
|
|
93
|
+
command << Shellwords.split(mysql_options) if mysql_options.present?
|
|
94
|
+
command << database if database.present?
|
|
95
|
+
command << "-e'QUIT'"
|
|
96
|
+
command.join(" ")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def mysql_client_binary
|
|
100
|
+
'$(command -v mariadb >/dev/null 2>&1 && echo mariadb || echo mysql)'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def mysql_client_in_path?
|
|
104
|
+
system("which mysql", out: File::NULL) || system("which mariadb", out: File::NULL)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def mysqldump_in_path?
|
|
108
|
+
system("which mysqldump", out: File::NULL) || system("which mariadb-dump", out: File::NULL)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mysql_socket_option
|
|
112
|
+
return if mysql_option_includes_socket?(config[:mysql_options])
|
|
113
|
+
|
|
114
|
+
config[:socket].presence || mysql_socket_from_options(config[:mysqldump_options])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def mysql_option_includes_socket?(option_string)
|
|
118
|
+
mysql_socket_from_options(option_string).present?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mysql_socket_from_options(option_string)
|
|
122
|
+
return if option_string.to_s.empty?
|
|
123
|
+
|
|
124
|
+
args = Shellwords.split(option_string)
|
|
125
|
+
socket_index = args.index('--socket')
|
|
126
|
+
return args[socket_index + 1] if socket_index && args[socket_index + 1]
|
|
127
|
+
|
|
128
|
+
args.each do |arg|
|
|
129
|
+
return Regexp.last_match(1) if arg.match(/\A--socket=(.+)\z/)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Wordmove
|
|
2
|
+
class Doctor
|
|
3
|
+
class Rsync
|
|
4
|
+
attr_reader :logger
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def check!
|
|
11
|
+
logger.task "Checking rsync"
|
|
12
|
+
|
|
13
|
+
version_output = `rsync --version | head -n1 2>&1`
|
|
14
|
+
|
|
15
|
+
if version_output.downcase.include?("openrsync")
|
|
16
|
+
protocol_version = version_output[/protocol version (\d+)/i, 1]
|
|
17
|
+
logger.success "openrsync detected (protocol version #{protocol_version || 'unknown'})"
|
|
18
|
+
elsif (match = /\d+\.\d+\.\d+/.match(version_output))
|
|
19
|
+
logger.success "rsync is installed at version #{match[0]}"
|
|
20
|
+
else
|
|
21
|
+
logger.error "rsync not found or the version could not be detected. " \
|
|
22
|
+
"Output was: #{version_output.strip}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|