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
data/lib/wordmove/cli.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Wordmove
|
|
2
|
+
class CLI < Thor
|
|
3
|
+
map %w[--version -v] => :__print_version
|
|
4
|
+
|
|
5
|
+
desc "--version, -v", "Print the version"
|
|
6
|
+
def __print_version
|
|
7
|
+
puts Wordmove::VERSION
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
desc "init", "Generates a brand new movefile.yml"
|
|
11
|
+
def init
|
|
12
|
+
Wordmove::Generators::Movefile.start
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "doctor", "Do some local configuration and environment checks"
|
|
16
|
+
def doctor
|
|
17
|
+
Wordmove::Doctor.start
|
|
18
|
+
rescue Wordmove::MovefileNotFound => e
|
|
19
|
+
logger.error(e.message)
|
|
20
|
+
exit 1
|
|
21
|
+
rescue Psych::SyntaxError => e
|
|
22
|
+
logger.error("Your movefile is not parsable due to a syntax error: #{e.message}")
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
shared_options = {
|
|
27
|
+
wordpress: { aliases: "-w", type: :boolean },
|
|
28
|
+
uploads: { aliases: "-u", type: :boolean },
|
|
29
|
+
themes: { aliases: "-t", type: :boolean },
|
|
30
|
+
plugins: { aliases: "-p", type: :boolean },
|
|
31
|
+
mu_plugins: { aliases: "-m", type: :boolean },
|
|
32
|
+
languages: { aliases: "-l", type: :boolean },
|
|
33
|
+
db: { aliases: "-d", type: :boolean },
|
|
34
|
+
verbose: { aliases: "-v", type: :boolean },
|
|
35
|
+
simulate: { aliases: "-s", type: :boolean },
|
|
36
|
+
environment: { aliases: "-e" },
|
|
37
|
+
config: { aliases: "-c" },
|
|
38
|
+
debug: { type: :boolean },
|
|
39
|
+
no_adapt: { type: :boolean },
|
|
40
|
+
all: { type: :boolean }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
no_tasks do
|
|
44
|
+
def handle_options(options)
|
|
45
|
+
wordpress_options.each do |task|
|
|
46
|
+
yield task if options[task] || (options["all"] && options[task] != false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def wordpress_options
|
|
51
|
+
%w[wordpress uploads themes plugins mu_plugins languages db]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ensure_wordpress_options_presence!(options)
|
|
55
|
+
return if (options.keys & (wordpress_options + ["all"])).present?
|
|
56
|
+
|
|
57
|
+
puts "No options given. See wordmove --help"
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def logger
|
|
62
|
+
Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc "list", "List all environments and vhosts"
|
|
67
|
+
shared_options.each do |option, args|
|
|
68
|
+
method_option option, args
|
|
69
|
+
end
|
|
70
|
+
def list
|
|
71
|
+
Wordmove::EnvironmentsList.print(options)
|
|
72
|
+
rescue Wordmove::MovefileNotFound => e
|
|
73
|
+
logger.error(e.message)
|
|
74
|
+
exit 1
|
|
75
|
+
rescue Psych::SyntaxError => e
|
|
76
|
+
logger.error("Your movefile is not parsable due to a syntax error: #{e.message}")
|
|
77
|
+
exit 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc "pull", "Pulls WP data from remote host to the local machine"
|
|
81
|
+
shared_options.each do |option, args|
|
|
82
|
+
method_option option, args
|
|
83
|
+
end
|
|
84
|
+
def pull
|
|
85
|
+
ensure_wordpress_options_presence!(options)
|
|
86
|
+
begin
|
|
87
|
+
deployer = Wordmove::Deployer::Base.deployer_for(options.deep_symbolize_keys)
|
|
88
|
+
rescue MovefileNotFound => e
|
|
89
|
+
logger.error(e.message)
|
|
90
|
+
exit 1
|
|
91
|
+
rescue Psych::SyntaxError => e
|
|
92
|
+
logger.error("Your movefile is not parsable due to a syntax error: #{e.message}")
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Wordmove::Hook.run(:pull, :before, options)
|
|
97
|
+
|
|
98
|
+
guardian = Wordmove::Guardian.new(options: options, action: :pull)
|
|
99
|
+
|
|
100
|
+
handle_options(options) do |task|
|
|
101
|
+
deployer.send("pull_#{task}") if guardian.allows(task.to_sym)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Wordmove::Hook.run(:pull, :after, options)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
desc "push", "Pushes WP data from local machine to remote host"
|
|
108
|
+
shared_options.each do |option, args|
|
|
109
|
+
method_option option, args
|
|
110
|
+
end
|
|
111
|
+
def push
|
|
112
|
+
ensure_wordpress_options_presence!(options)
|
|
113
|
+
begin
|
|
114
|
+
deployer = Wordmove::Deployer::Base.deployer_for(options.deep_symbolize_keys)
|
|
115
|
+
rescue MovefileNotFound => e
|
|
116
|
+
logger.error(e.message)
|
|
117
|
+
exit 1
|
|
118
|
+
rescue Psych::SyntaxError => e
|
|
119
|
+
logger.error("Your movefile is not parsable due to a syntax error: #{e.message}")
|
|
120
|
+
exit 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Wordmove::Hook.run(:push, :before, options)
|
|
124
|
+
|
|
125
|
+
guardian = Wordmove::Guardian.new(options: options, action: :push)
|
|
126
|
+
|
|
127
|
+
handle_options(options) do |task|
|
|
128
|
+
deployer.send("push_#{task}") if guardian.allows(task.to_sym)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Wordmove::Hook.run(:push, :after, options)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module Wordmove
|
|
5
|
+
module Deployer
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :options, :logger, :environment
|
|
8
|
+
|
|
9
|
+
SANDBOX_MAGIC_COMMENT = '/*!999999- enable the sandbox mode */'.freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def deployer_for(cli_options)
|
|
13
|
+
movefile = Wordmove::Movefile.new(cli_options[:config])
|
|
14
|
+
movefile.load_dotenv(cli_options)
|
|
15
|
+
|
|
16
|
+
options = movefile.fetch.merge! cli_options
|
|
17
|
+
environment = movefile.environment(cli_options)
|
|
18
|
+
|
|
19
|
+
# localとenvironmentのdatabaseにoriginを追加
|
|
20
|
+
options[:local][:database][:origin] = 'local'
|
|
21
|
+
options[environment][:database][:origin] = 'remote'
|
|
22
|
+
|
|
23
|
+
if options[environment][:ftp]
|
|
24
|
+
raise NoAdapterFound,
|
|
25
|
+
"FTP support was removed in wordmove-ng 6.0, but the \"#{environment}\" " \
|
|
26
|
+
"environment is configured with an `ftp` block. Switch it to `ssh`, or " \
|
|
27
|
+
"keep using the legacy `wordmove` 5.x gem for FTP-only hosts."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
warn_about_removed_sql_adapter(options, movefile)
|
|
31
|
+
|
|
32
|
+
return SSH.new(environment, options) if options[environment][:ssh]
|
|
33
|
+
|
|
34
|
+
raise NoAdapterFound, "No valid adapter found."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `global.sql_adapter` no longer selects anything: URL/path adaptation is
|
|
38
|
+
# always done with wp-cli on the target. Warn once so people notice.
|
|
39
|
+
def warn_about_removed_sql_adapter(options, movefile)
|
|
40
|
+
adapter = options.dig(:global, :sql_adapter)
|
|
41
|
+
return if adapter.nil? || adapter.to_s == 'wpcli'
|
|
42
|
+
|
|
43
|
+
logger(movefile.secrets).warn(
|
|
44
|
+
"`global.sql_adapter: #{adapter}` is ignored since wordmove-ng 6.0; database " \
|
|
45
|
+
"adaptation always uses wp-cli on the target. Remove the key from your movefile."
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def current_dir
|
|
50
|
+
'.'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def logger(secrets)
|
|
54
|
+
Logger.new(STDOUT, secrets).tap { |l| l.level = Logger::DEBUG }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize(environment, options = {})
|
|
59
|
+
@environment = environment.to_sym
|
|
60
|
+
@options = options
|
|
61
|
+
|
|
62
|
+
movefile_secrets = Wordmove::Movefile.new(options[:config]).secrets
|
|
63
|
+
@logger = self.class.logger(movefile_secrets)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def push_db
|
|
67
|
+
logger.task "Pushing Database"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pull_db
|
|
71
|
+
logger.task "Pulling Database"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def exclude_dir_contents(path)
|
|
75
|
+
"#{path}/*"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def push_wordpress
|
|
79
|
+
logger.task "Pushing wordpress core"
|
|
80
|
+
|
|
81
|
+
local_path = local_options[:wordpress_path]
|
|
82
|
+
remote_path = remote_options[:wordpress_path]
|
|
83
|
+
exclude_wp_content = exclude_dir_contents(local_wp_content_dir.relative_path)
|
|
84
|
+
exclude_paths = paths_to_exclude.push(exclude_wp_content)
|
|
85
|
+
|
|
86
|
+
remote_put_directory(local_path, remote_path, exclude_paths)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def pull_wordpress
|
|
90
|
+
logger.task "Pulling wordpress core"
|
|
91
|
+
|
|
92
|
+
local_path = local_options[:wordpress_path]
|
|
93
|
+
remote_path = remote_options[:wordpress_path]
|
|
94
|
+
exclude_wp_content = exclude_dir_contents(remote_wp_content_dir.relative_path)
|
|
95
|
+
exclude_paths = paths_to_exclude.push(exclude_wp_content)
|
|
96
|
+
|
|
97
|
+
remote_get_directory(remote_path, local_path, exclude_paths)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
protected
|
|
101
|
+
|
|
102
|
+
def paths_to_exclude
|
|
103
|
+
remote_options[:exclude] || []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def run(command)
|
|
107
|
+
logger.task_step true, command
|
|
108
|
+
return true if simulate?
|
|
109
|
+
|
|
110
|
+
system(command)
|
|
111
|
+
raise ShellCommandError, "Return code reports an error" unless $CHILD_STATUS.success?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def simulate?
|
|
115
|
+
options[:simulate]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
[
|
|
119
|
+
WordpressDirectory::Path::WP_CONTENT,
|
|
120
|
+
WordpressDirectory::Path::PLUGINS,
|
|
121
|
+
WordpressDirectory::Path::MU_PLUGINS,
|
|
122
|
+
WordpressDirectory::Path::THEMES,
|
|
123
|
+
WordpressDirectory::Path::UPLOADS,
|
|
124
|
+
WordpressDirectory::Path::LANGUAGES
|
|
125
|
+
].each do |type|
|
|
126
|
+
%i[remote local].each do |location|
|
|
127
|
+
define_method "#{location}_#{type}_dir" do
|
|
128
|
+
options = send("#{location}_options")
|
|
129
|
+
WordpressDirectory.new(type, options)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def mysql_dump_command(options, save_to_path)
|
|
135
|
+
command = [mysql_dump_binary]
|
|
136
|
+
command << "--host=#{Shellwords.escape(options[:host])}" if options[:host].present?
|
|
137
|
+
command << "--port=#{Shellwords.escape(options[:port])}" if options[:port].present?
|
|
138
|
+
if (socket = mysql_socket_option(options, :mysqldump_options))
|
|
139
|
+
command << "--socket=#{Shellwords.escape(socket)}"
|
|
140
|
+
end
|
|
141
|
+
command << "--user=#{Shellwords.escape(options[:user])}" if options[:user].present?
|
|
142
|
+
if options[:password].present?
|
|
143
|
+
command << "--password=#{Shellwords.escape(options[:password])}"
|
|
144
|
+
end
|
|
145
|
+
command << "--result-file=#{Shellwords.escape(save_to_path)}"
|
|
146
|
+
if options[:mysqldump_options].present?
|
|
147
|
+
command << Shellwords.split(options[:mysqldump_options])
|
|
148
|
+
end
|
|
149
|
+
command << Shellwords.escape(options[:name])
|
|
150
|
+
command.join(" ")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def mysql_import_command(dump_path, options)
|
|
154
|
+
mysql_command = mysql_client_command(options, import: true)
|
|
155
|
+
escaped_dump_path = Shellwords.escape(dump_path)
|
|
156
|
+
|
|
157
|
+
init_commands = [
|
|
158
|
+
"SET autocommit=0",
|
|
159
|
+
"SET FOREIGN_KEY_CHECKS=0"
|
|
160
|
+
].join('; ')
|
|
161
|
+
|
|
162
|
+
[
|
|
163
|
+
"first_line=$(head -n 1 #{escaped_dump_path} 2>/dev/null || true)",
|
|
164
|
+
'tmp_dump="$(mktemp)"',
|
|
165
|
+
%(if [ "$first_line" = '#{SANDBOX_MAGIC_COMMENT}' ]; then),
|
|
166
|
+
"tail -n +2 #{escaped_dump_path} > \"$tmp_dump\"",
|
|
167
|
+
'else',
|
|
168
|
+
"cat #{escaped_dump_path} > \"$tmp_dump\"",
|
|
169
|
+
'fi',
|
|
170
|
+
'printf "\\\\nCOMMIT;\\\\n" >> "$tmp_dump"',
|
|
171
|
+
"#{mysql_command} --init-command=\"#{init_commands}\" < \"$tmp_dump\"",
|
|
172
|
+
'import_status=$?',
|
|
173
|
+
'rm -f "$tmp_dump"',
|
|
174
|
+
'exit $import_status'
|
|
175
|
+
].join("\n")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def compress_command(path)
|
|
179
|
+
"gzip -9 -f #{Shellwords.escape(path)}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def uncompress_command(path)
|
|
183
|
+
"gzip -d -f #{Shellwords.escape(path)}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def local_delete(path)
|
|
187
|
+
logger.task_step true, "delete: '#{path}'"
|
|
188
|
+
File.delete(path) unless simulate?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def save_local_db(local_dump_path)
|
|
192
|
+
# dump local mysql into file
|
|
193
|
+
run mysql_dump_command(local_options[:database], local_dump_path)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def remote_options
|
|
197
|
+
options[environment].clone
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def local_options
|
|
201
|
+
options[:local].clone
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def mysql_client_binary
|
|
205
|
+
'$(command -v mariadb >/dev/null 2>&1 && echo mariadb || echo mysql)'
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def mysql_dump_binary
|
|
209
|
+
'$(command -v mariadb-dump >/dev/null 2>&1 && echo mariadb-dump || echo mysqldump)'
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def mysql_client_command(options, import: false)
|
|
213
|
+
mysql_options = options[:mysql_options]
|
|
214
|
+
command = [mysql_client_binary]
|
|
215
|
+
command << "--host=#{Shellwords.escape(options[:host])}" if options[:host].present?
|
|
216
|
+
command << "--port=#{Shellwords.escape(options[:port])}" if options[:port].present?
|
|
217
|
+
if (socket = mysql_socket_option(options, :mysql_options))
|
|
218
|
+
command << "--socket=#{Shellwords.escape(socket)}"
|
|
219
|
+
end
|
|
220
|
+
command << "--user=#{Shellwords.escape(options[:user])}" if options[:user].present?
|
|
221
|
+
if options[:password].present?
|
|
222
|
+
command << "--password=#{Shellwords.escape(options[:password])}"
|
|
223
|
+
end
|
|
224
|
+
command << "--database=#{Shellwords.escape(options[:name])}"
|
|
225
|
+
command << "--force" if import
|
|
226
|
+
if !mysql_options.to_s.match?(/--(?:skip-)?binary-mode\b/) && import
|
|
227
|
+
command << "--binary-mode"
|
|
228
|
+
end
|
|
229
|
+
command << Shellwords.split(mysql_options) if mysql_options.present?
|
|
230
|
+
command.join(" ")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def normalize_collations!(dump_path)
|
|
234
|
+
return if simulate?
|
|
235
|
+
|
|
236
|
+
mappings = collation_fallbacks
|
|
237
|
+
charset_map = charset_fallbacks
|
|
238
|
+
return if mappings.empty? && charset_map.empty?
|
|
239
|
+
|
|
240
|
+
collation_matcher = Regexp.union(mappings.keys) unless mappings.empty?
|
|
241
|
+
temp_dump = Tempfile.new(['wordmove-collation', '.sql'])
|
|
242
|
+
begin
|
|
243
|
+
File.open(dump_path, 'rb') do |input|
|
|
244
|
+
File.open(temp_dump.path, 'wb') do |output|
|
|
245
|
+
input.each_line do |line|
|
|
246
|
+
unless collation_matcher && line.match?(collation_matcher)
|
|
247
|
+
output.write(line)
|
|
248
|
+
next
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Ensure we do not explode on invalid byte sequences coming from dumps
|
|
252
|
+
safe_line = begin
|
|
253
|
+
line.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: '')
|
|
254
|
+
rescue StandardError
|
|
255
|
+
line.dup.force_encoding(Encoding::UTF_8)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
replaced_collation = false
|
|
259
|
+
mappings.each do |pattern, replacement|
|
|
260
|
+
next unless safe_line.match?(pattern)
|
|
261
|
+
|
|
262
|
+
if replacement.is_a?(Hash)
|
|
263
|
+
safe_line = safe_line.gsub(pattern, replacement[:collation])
|
|
264
|
+
replaced_collation = true
|
|
265
|
+
if replacement[:charset]
|
|
266
|
+
safe_line = safe_line.gsub(/utf8mb3\b/, replacement[:charset])
|
|
267
|
+
end
|
|
268
|
+
else
|
|
269
|
+
safe_line = safe_line.gsub(pattern, replacement)
|
|
270
|
+
replaced_collation = true
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# If we replaced collations but still see utf8mb3, upgrade charset to avoid mismatches
|
|
275
|
+
if replaced_collation
|
|
276
|
+
charset_map.each do |pattern, replacement|
|
|
277
|
+
safe_line = safe_line.gsub(pattern, replacement)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
output.write(safe_line)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
FileUtils.mv(temp_dump.path, dump_path)
|
|
286
|
+
ensure
|
|
287
|
+
temp_dump.close!
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def collation_fallbacks
|
|
292
|
+
raw_mappings = options.dig(:global, :collation_fallbacks) || default_collation_fallbacks
|
|
293
|
+
return {} unless raw_mappings
|
|
294
|
+
|
|
295
|
+
raw_mappings.each_with_object({}) do |(pattern, replacement), memo|
|
|
296
|
+
normalized_pattern = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern.to_s))
|
|
297
|
+
memo[normalized_pattern] = normalize_replacement(replacement)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def default_collation_fallbacks
|
|
302
|
+
{
|
|
303
|
+
/utf8mb3_uca\d+_ai_ci/ => { collation: 'utf8mb4_unicode_ci', charset: 'utf8mb4' },
|
|
304
|
+
/utf8mb3_uca\d+_as_cs/ => { collation: 'utf8mb4_unicode_ci', charset: 'utf8mb4' },
|
|
305
|
+
/utf8mb3_unicode_520_ci/ => { collation: 'utf8mb4_unicode_ci', charset: 'utf8mb4' }
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def charset_fallbacks
|
|
310
|
+
raw_mappings = options.dig(:global, :charset_fallbacks) || default_charset_fallbacks
|
|
311
|
+
return {} unless raw_mappings
|
|
312
|
+
|
|
313
|
+
raw_mappings.each_with_object({}) do |(pattern, replacement), memo|
|
|
314
|
+
normalized_pattern = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern.to_s))
|
|
315
|
+
memo[normalized_pattern] = replacement
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def default_charset_fallbacks
|
|
320
|
+
{
|
|
321
|
+
/utf8mb3\b/ => 'utf8mb4'
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def normalize_replacement(replacement)
|
|
326
|
+
return replacement if replacement.is_a?(Hash)
|
|
327
|
+
|
|
328
|
+
{ collation: replacement, charset: nil }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def mysql_socket_option(options, legacy_options_key)
|
|
332
|
+
return if mysql_option_includes_socket?(options[legacy_options_key])
|
|
333
|
+
|
|
334
|
+
options[:socket].presence
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def mysql_option_includes_socket?(option_string)
|
|
338
|
+
mysql_socket_from_options(option_string).present?
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def mysql_socket_from_options(option_string)
|
|
342
|
+
return if option_string.blank?
|
|
343
|
+
|
|
344
|
+
args = Shellwords.split(option_string)
|
|
345
|
+
socket_index = args.index('--socket')
|
|
346
|
+
return args[socket_index + 1] if socket_index && args[socket_index + 1]
|
|
347
|
+
|
|
348
|
+
args.each do |arg|
|
|
349
|
+
return Regexp.last_match(1) if arg.match(/\A--socket=(.+)\z/)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|