-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwp-push-remote.sh
More file actions
1219 lines (1073 loc) · 45.7 KB
/
wp-push-remote.sh
File metadata and controls
1219 lines (1073 loc) · 45.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# Ubuntu 22.04+ compatible script
# Requires: bash 5.0+, WP-CLI, rsync, openssh-client
# Check bash version (require 5.0+ for Ubuntu 22.04+)
if ((BASH_VERSINFO[0] < 5)); then
echo "ERROR: This script requires Bash 5.0 or higher (current: $BASH_VERSION)"
echo "Ubuntu 22.04+ should have bash 5.1+ by default."
exit 1
fi
script_version="2.1.1"
# Author: gb@wpnet.nz
# Description: Push a site from SOURCE server to REMOTE. Run this script from the SOURCE server.
# Requirements: WP-CLI installed on source and remote
# wp-cli.yml to be configured in the source and remote site owner's home directory, with the correct path to the WP installation
# Target OS: Ubuntu 22.04 LTS or higher
####################################################################################
# COLOR DEFINITIONS FOR BETTER UX
####################################################################################
# Check if terminal supports colors
if [[ -t 1 ]]; then
# Colors
COLOR_RESET='\033[0m'
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'
COLOR_YELLOW='\033[0;33m'
COLOR_BLUE='\033[0;34m'
COLOR_MAGENTA='\033[0;35m'
COLOR_CYAN='\033[0;36m'
COLOR_WHITE='\033[1;37m'
# Bold colors
COLOR_BOLD_GREEN='\033[1;32m'
COLOR_BOLD_YELLOW='\033[1;33m'
COLOR_BOLD_BLUE='\033[1;34m'
COLOR_BOLD_CYAN='\033[1;36m'
else
# No colors
COLOR_RESET=''
COLOR_RED=''
COLOR_GREEN=''
COLOR_YELLOW=''
COLOR_BLUE=''
COLOR_MAGENTA=''
COLOR_CYAN=''
COLOR_WHITE=''
COLOR_BOLD_GREEN=''
COLOR_BOLD_YELLOW=''
COLOR_BOLD_BLUE=''
COLOR_BOLD_CYAN=''
fi
####################################################################################
# HELPER FUNCTIONS
####################################################################################
# Print functions
print_header() {
echo -e "\n${COLOR_BOLD_CYAN}==== $1 ====${COLOR_RESET}"
}
print_info() {
echo -e "${COLOR_CYAN}[INFO]${COLOR_RESET} $1"
}
print_success() {
echo -e "${COLOR_BOLD_GREEN}[SUCCESS]${COLOR_RESET} $1"
}
print_warning() {
echo -e "${COLOR_BOLD_YELLOW}[WARNING]${COLOR_RESET} $1"
}
print_error() {
echo -e "${COLOR_RED}[ERROR]${COLOR_RESET} $1"
}
print_step() {
echo -e "\n${COLOR_BOLD_BLUE}++++ $1${COLOR_RESET}"
}
# Help function
show_help() {
echo -e "${COLOR_BOLD_CYAN}WP Push Remote v${script_version}${COLOR_RESET}"
echo -e "${COLOR_WHITE}Push a WordPress site from SOURCE server to REMOTE using WP-CLI and rsync${COLOR_RESET}"
echo ""
echo -e "${COLOR_BOLD_GREEN}USAGE:${COLOR_RESET}"
echo " $0 [OPTIONS]"
echo ""
echo -e "${COLOR_BOLD_GREEN}OPTIONS:${COLOR_RESET}"
echo -e " ${COLOR_YELLOW}-h, --help${COLOR_RESET} Show this help message"
echo -e " ${COLOR_YELLOW}-u, --unattended${COLOR_RESET} Run in unattended mode (no prompts)"
echo -e " ${COLOR_YELLOW}-i, --install-for-user${COLOR_RESET} Install script to a user's site directory (skips push operation)"
echo -e " ${COLOR_YELLOW}-c, --config${COLOR_RESET} Prompt for all configuration settings"
echo -e " ${COLOR_YELLOW}-D, --del-ssh-key${COLOR_RESET} Delete SSH key pairs for remote user (skips push operation)"
echo ""
echo -e " ${COLOR_YELLOW}-e, --exclude ${COLOR_RESET}LIST Space-delimited list of paths to exclude (quote the list)"
echo -e " Example: -e \"wp-content/plugins wp-content/themes/mytheme myfile.js\""
echo -e " ${COLOR_YELLOW}-p, --install-plugins${COLOR_RESET} LIST Space-delimited list of plugins to install"
echo -e " Example: --install-plugins \"woocommerce contact-form-7\""
echo -e " ${COLOR_YELLOW}-r, --remote-cmds${COLOR_RESET} CMD Run custom commands on remote (quote the commands)"
echo -e " Example: --remote-cmds \"wp theme install twentytwenty\""
echo ""
echo -e " ${COLOR_BOLD_CYAN}Option Flags:${COLOR_RESET}"
echo -e " ${COLOR_YELLOW}--search-replace${COLOR_RESET} Run wp search-replace (default: yes)"
echo -e " ${COLOR_YELLOW}--no-search-replace${COLOR_RESET} Skip wp search-replace"
echo -e " ${COLOR_YELLOW}--files-only${COLOR_RESET} Skip database operations (default: no)"
echo -e " ${COLOR_YELLOW}--no-db-import${COLOR_RESET} Don't import database on remote (default: no)"
echo -e " ${COLOR_YELLOW}--exclude-wpconfig${COLOR_RESET} Exclude wp-config.php (default: yes)"
echo -e " ${COLOR_YELLOW}--no-exclude-wpconfig${COLOR_RESET} Include wp-config.php in sync"
echo -e " ${COLOR_YELLOW}--disable-wp-debug${COLOR_RESET} Disable WP_DEBUG temporarily (default: no)"
echo -e " ${COLOR_YELLOW}--all-tables-with-prefix${COLOR_RESET} Use --all-tables-with-prefix for wp search-replace (default: no)"
echo ""
echo -e "${COLOR_BOLD_GREEN}EXAMPLES:${COLOR_RESET}"
echo " # Run with interactive prompts for configuration"
echo " $0 --config"
echo ""
echo " # Run in unattended mode with custom exclusions"
echo " $0 -u -e \"uploads .maintenance .git\""
echo ""
echo " # Files only, no database operations"
echo " $0 --files-only"
echo ""
echo " # Disable search-replace operation"
echo " $0 --no-search-replace"
echo ""
echo " # Delete SSH key pairs for remote user"
echo " $0 --del-ssh-key"
echo ""
echo -e "${COLOR_BOLD_GREEN}REQUIREMENTS:${COLOR_RESET}"
echo " - WP-CLI installed on both source and remote servers"
echo " - SSH access to remote server (ssh key pair generator included)"
echo ""
echo -e "${COLOR_BOLD_GREEN}CONFIGURATION:${COLOR_RESET}"
echo " Configuration is saved to ~/.wp-push-remote.conf after using --config"
echo " and automatically loaded on subsequent runs."
echo ""
echo " Default path structure: /sites/{domain}/files"
echo " URLs and search-replace paths are auto-detected from your configuration."
echo ""
echo " Use --config to configure or reconfigure settings interactively."
echo ""
exit 0
}
####################################################################################
# DEFAULT CONFIGURATION - Can be overridden via --config option
####################################################################################
# Configuration file location
config_file="${HOME}/.wp-push-remote.conf"
# SOURCE
source_path_prefix="" # use trailing slash
source_webroot="files" # no preceding or trailing slash
# REMOTE
remote_ip_address=""
remote_user=""
remote_path_prefix="" # use trailing slash
remote_webroot="files" # no preceding or trailing slash
plugins_to_install="" # space separated list of plugins to install on remote
# WP-CLI search-replace (will be auto-derived from paths if not set)
# rewrites for URLs
wp_search_replace_source_url=''
wp_search_replace_remote_url=''
# rewrites for file paths
wp_search_replace_source_path=''
wp_search_replace_remote_path=''
# Options flags (1 = YES, 0 = NO)
do_search_replace=1 # run 'wp search-replace' on remote, once for URLs and once for file paths
files_only=0 # don't do a database dump & import
no_db_import=0 # don't run db import on remote
install_plugins=0 # install plugins on remote
remote_commands="" # custom commands to run on remote
exclude_wpconfig=1 # exclude the wp-config.php file from the rsync to remote, you probably don't want to change this
unattended_mode=0 # flag for unattended mode
disable_wp_debug=0 # disable WP_DEBUG on remote for the duration of the push, then revert it back to the original state
prompt_config=0 # flag to prompt for configuration
delete_ssh_keys=0 # flag to delete SSH key pairs
all_tables_with_prefix=0 # use --all-tables-with-prefix option for wp search-replace commands
install_for_user=0 # flag to install script for a user
# Load saved configuration if it exists
load_config() {
if [[ -f "$config_file" ]]; then
print_info "Loading saved configuration from $config_file"
source "$config_file"
fi
}
# Save configuration to file
save_config() {
cat > "$config_file" << EOF
# WP Push Remote Configuration
# Generated on $(date)
source_path_prefix="$source_path_prefix"
source_webroot="$source_webroot"
remote_ip_address="$remote_ip_address"
remote_user="$remote_user"
remote_path_prefix="$remote_path_prefix"
remote_webroot="$remote_webroot"
EOF
chmod 600 "$config_file"
print_success "Configuration saved to $config_file"
}
# Function to delete SSH key pairs
delete_ssh_key_pairs() {
print_header "SSH KEY DELETION"
# Check if configuration is loaded
if [[ -z "$remote_user" ]]; then
print_error "No configuration found. Please run with --config first."
exit 1
fi
# Find matching SSH keys
local ssh_dir="${HOME}/.ssh"
local key_pattern="id_*_remote_${remote_user}"
print_info "Searching for SSH key pairs matching pattern: ${key_pattern}"
# Find all matching keys
local found_keys=0
local deleted_keys=0
# Look for Ed25519 keys
if [[ -f "${ssh_dir}/id_ed25519_remote_${remote_user}" ]]; then
found_keys=$((found_keys + 1))
print_step "Found Ed25519 key pair: id_ed25519_remote_${remote_user}"
if [[ -f "${ssh_dir}/id_ed25519_remote_${remote_user}.pub" ]]; then
print_info " - Private key: ${ssh_dir}/id_ed25519_remote_${remote_user}"
print_info " - Public key: ${ssh_dir}/id_ed25519_remote_${remote_user}.pub"
fi
rm -fv "${ssh_dir}/id_ed25519_remote_${remote_user}" "${ssh_dir}/id_ed25519_remote_${remote_user}.pub"
deleted_keys=$((deleted_keys + 1))
fi
# Look for RSA keys
if [[ -f "${ssh_dir}/id_rsa_remote_${remote_user}" ]]; then
found_keys=$((found_keys + 1))
print_step "Found RSA key pair: id_rsa_remote_${remote_user}"
if [[ -f "${ssh_dir}/id_rsa_remote_${remote_user}.pub" ]]; then
print_info " - Private key: ${ssh_dir}/id_rsa_remote_${remote_user}"
print_info " - Public key: ${ssh_dir}/id_rsa_remote_${remote_user}.pub"
fi
rm -fv "${ssh_dir}/id_rsa_remote_${remote_user}" "${ssh_dir}/id_rsa_remote_${remote_user}.pub"
deleted_keys=$((deleted_keys + 1))
fi
if [[ $found_keys -eq 0 ]]; then
print_warning "No SSH key pairs found for remote user '${remote_user}'"
else
print_success "Deleted ${deleted_keys} SSH key pair(s) for remote user '${remote_user}'"
print_warning "IMPORTANT: You must MANUALLY remove the public key from the remote server's authorized_keys file:"
print_warning " Remote user: ${remote_user}"
print_warning " Remote location: ~/.ssh/authorized_keys"
print_warning " Look for keys with 'remote_${remote_user}' in the comment"
fi
exit 0
}
# Function to install script for a user
install_for_user() {
print_header "INSTALL SCRIPT FOR USER"
# Check if /sites directory exists
if [[ ! -d /sites ]]; then
print_error "/sites directory does not exist"
print_info "This feature requires a /sites directory structure"
exit 1
fi
print_info "Searching for WordPress installations in /sites/*/files/ ..."
# Find all directories that match the pattern /sites/*/files/
local sites=()
while IFS= read -r -d '' files_dir; do
# Get the parent directory (one level above files)
local site_dir=$(dirname "$files_dir")
# Only add if it's a valid path structure
if [[ -d "$site_dir" ]]; then
sites+=("$site_dir")
fi
done < <(find /sites -maxdepth 2 -type d -name "files" -print0 2>/dev/null)
if [[ ${#sites[@]} -eq 0 ]]; then
print_error "No sites found in /sites/*/files/ pattern"
exit 1
fi
print_success "Found ${#sites[@]} site(s)"
echo ""
print_info "Select installation location:"
echo ""
# Display numbered list
for i in "${!sites[@]}"; do
echo " $((i+1)). ${sites[$i]}"
done
echo ""
read -r -p "$(echo -e "${COLOR_CYAN}Enter the number of your choice:${COLOR_RESET} ")" choice
# Validate input
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [[ $choice -lt 1 ]] || [[ $choice -gt ${#sites[@]} ]]; then
print_error "Invalid selection"
exit 1
fi
local selected_site="${sites[$((choice-1))]}"
local bin_dir="${selected_site}/.local/bin"
local install_path="${bin_dir}/wp-push-remote"
# Create .local/bin directory if it doesn't exist
if [[ ! -d "$bin_dir" ]]; then
print_info "Creating directory: ${bin_dir}"
if mkdir -p "$bin_dir"; then
print_success "Directory created successfully"
else
print_error "Failed to create directory"
exit 1
fi
fi
print_step "Installing script to: ${install_path}"
# Get the script path (the currently running script)
local script_path="$(readlink -f "$0")"
# Create a temporary copy with the install-for-user option disabled
local temp_script=$(mktemp) || {
print_error "Failed to create temporary file"
exit 1
}
# Copy the script and replace the install-for-user option with a disabled version
print_info "Creating modified version of script (with --install-for-user disabled)..."
# Use awk to replace the install-for-user case blocks with error messages
# and remove the help text line
# This handles both occurrences (in fallback and main getopt parsing)
awk '
# Remove the help text line for --install-for-user
/Install script to a user.*site directory/ {
next
}
/^[[:space:]]*-i[|]--install-for-user[)]/ {
# Capture the leading whitespace for proper indentation
match($0, /^[[:space:]]*/)
saved_indent = substr($0, 1, RLENGTH)
print $0 " # DISABLED IN INSTALLED VERSION"
in_install_block = 1
next
}
in_install_block && /^[[:space:]]*install_for_user=1/ {
# Capture the indentation of content lines
match($0, /^[[:space:]]*/)
content_indent = substr($0, 1, RLENGTH)
in_install_block = 2
next
}
in_install_block == 2 && /^[[:space:]]*;;$/ {
# Use the captured content indentation
print content_indent "print_error \"The --install-for-user option is disabled in this installed copy\""
print content_indent "exit 1"
print $0
in_install_block = 0
next
}
in_install_block { next }
{ print }
' "$script_path" > "$temp_script"
# Now copy the modified script to the target location
if cp "$temp_script" "$install_path"; then
print_success "Script copied to ${install_path}"
else
print_error "Failed to copy script"
rm -f "$temp_script"
exit 1
fi
# Clean up temp file
rm -f "$temp_script"
# Extract site owner from the path (assuming /sites/domain/ ownership)
local site_owner
# Try GNU stat first, then BSD stat format
if stat -c '%U' "$selected_site" >/dev/null 2>&1; then
site_owner=$(stat -c '%U' "$selected_site")
elif stat -f '%Su' "$selected_site" >/dev/null 2>&1; then
site_owner=$(stat -f '%Su' "$selected_site")
else
site_owner=""
fi
if [[ -z "$site_owner" ]]; then
print_warning "Could not detect site owner, using current user"
site_owner=$(whoami)
fi
print_info "Setting ownership to ${site_owner}:${site_owner}"
# Set ownership on the .local/bin directory and the script
if chown -R "${site_owner}:${site_owner}" "$bin_dir" 2>/dev/null; then
print_success "Ownership set successfully"
else
print_warning "Failed to set ownership (may require sudo)"
print_info "You may need to run: sudo chown -R ${site_owner}:${site_owner} ${bin_dir}"
fi
print_info "Setting executable permission (0700)"
if chmod 0700 "$install_path"; then
print_success "Executable permission set to 0700"
else
print_error "Failed to set executable permission"
exit 1
fi
print_success "Installation complete!"
print_info "Script installed at: ${install_path}"
print_info "User can now run: ${install_path}"
exit 0
}
# Extract domain from path (e.g., /sites/example.com/ -> example.com)
extract_domain_from_path() {
local path="$1"
# Remove trailing slash and extract domain between /sites/ and next /
echo "$path" | sed -E 's#^.*/sites/([^/]+).*$#\1#'
}
# Derive URL from path (e.g., /sites/example.com/files -> //example.com)
derive_url_from_path() {
local path_prefix="$1"
local domain=$(extract_domain_from_path "$path_prefix")
if [[ -n "$domain" && "$domain" != "$path_prefix" ]]; then
echo "//$domain"
else
echo ""
fi
}
# Excludes for rsync to remote (edit as required)
excludes=(.git .maintenance wp-content/cache wp-content/uploads/wp-migrate-db /wp-content/updraft)
# Or just add to the array like this:
# excludes+=(.user.ini)
####################################################################################
# NO MORE EDITING BELOW THIS LINE!
####################################################################################
# Cleanup function for script interruption
cleanup_on_exit() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
print_error "\nScript interrupted or failed with exit code: $exit_code"
# Clean up any temporary database exports
if [[ -n "${source_path}" ]] && [[ -n "${db_export_prefix}" ]] && [[ -n "${rnd_str_key}" ]]; then
if ls "${source_path}/${db_export_prefix}"*"${rnd_str_key}.sql" >/dev/null 2>&1; then
print_info "Cleaning up temporary database export files..."
rm -f "${source_path}/${db_export_prefix}"*"${rnd_str_key}.sql"
fi
fi
fi
}
# Set trap for cleanup
trap cleanup_on_exit EXIT INT TERM
# Validate configuration
validate_config() {
local errors=0
# Check OS (Ubuntu only)
if [[ ! -f /etc/os-release ]]; then
print_error "Cannot detect OS. This script is designed for Ubuntu 22.04+."
errors=$((errors + 1))
else
source /etc/os-release
if [[ "$ID" != "ubuntu" ]]; then
print_warning "This script is optimized for Ubuntu. Detected: $ID"
print_info "Continuing anyway, but some features may not work as expected."
elif [[ -n "$VERSION_ID" ]]; then
# Pure bash version comparison (no bc required)
local version_major="${VERSION_ID%%.*}"
local version_minor="${VERSION_ID#*.}"
if (( version_major < 24 || (version_major == 24 && ${version_minor%%.*} < 4) )); then
print_warning "This script is optimized for Ubuntu 22.04+. Detected: Ubuntu $VERSION_ID"
fi
fi
fi
# Check required commands
local required_cmds=("wp" "rsync" "ssh" "ssh-keygen")
for cmd in "${required_cmds[@]}"; do
if ! command_exists "$cmd"; then
print_error "Required command not found: $cmd"
case $cmd in
wp)
print_info "Install WP-CLI: curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x wp-cli.phar && sudo mv wp-cli.phar /usr/local/bin/wp"
;;
rsync|ssh|ssh-keygen)
print_info "Install with: sudo apt install openssh-client rsync"
;;
esac
errors=$((errors + 1))
fi
done
if [[ -z "$remote_ip_address" ]]; then
print_error "Remote IP address is not set!"
errors=$((errors + 1))
fi
if [[ -z "$remote_user" ]]; then
print_error "Remote user is not set!"
errors=$((errors + 1))
fi
if [[ -z "$source_path_prefix" ]]; then
print_error "Source path prefix is not set!"
errors=$((errors + 1))
fi
if [[ -z "$remote_path_prefix" ]]; then
print_error "Remote path prefix is not set!"
errors=$((errors + 1))
fi
if [[ $errors -gt 0 ]]; then
print_error "Configuration validation failed. Please set required variables."
print_info "Use --config to set configuration interactively or edit the script."
exit 1
fi
}
# Normalize paths to ensure trailing slashes where needed
normalize_paths() {
# Add trailing slash if not present
[[ "$source_path_prefix" != */ ]] && source_path_prefix="${source_path_prefix}/"
[[ "$remote_path_prefix" != */ ]] && remote_path_prefix="${remote_path_prefix}/"
# Remove leading/trailing slashes from webroot
source_webroot="${source_webroot#/}"
source_webroot="${source_webroot%/}"
remote_webroot="${remote_webroot#/}"
remote_webroot="${remote_webroot%/}"
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Generate random string using Ubuntu's tools
generate_random_string() {
# Ubuntu 22.04+ has md5sum by default
echo "$RANDOM" | md5sum | head -c 12
}
# Function to handle user prompts
function user_prompt() {
if [[ $unattended_mode -eq 0 ]]; then
while true; do
read -p "$(echo -e "\\n${COLOR_YELLOW}CONFIRM:${COLOR_RESET} ${1} ${COLOR_GREEN}Are you sure? [Yes/no]${COLOR_RESET} ") " user_input
case $user_input in
[Yy]* ) return 0;;
"" ) return 0;;
[Nn]* ) return 1;;
* ) echo -e "${COLOR_YELLOW}Please respond yes [Y/y/{enter}] or no [n].${COLOR_RESET}";;
esac
done
else
print_info "CONFIRM: ${1} - Assuming YES in unattended mode."
return 0
fi
}
# Function to handle user prompts with default NO
function user_prompt_default_no() {
if [[ $unattended_mode -eq 0 ]]; then
while true; do
read -p "$(echo -e "\\n${COLOR_YELLOW}CONFIRM:${COLOR_RESET} ${1} ${COLOR_GREEN}[y/yes to confirm, Enter to skip]${COLOR_RESET} ") " user_input
case $user_input in
[Yy]* ) return 0;;
"" ) return 1;;
[Nn]* ) return 1;;
* ) echo -e "${COLOR_YELLOW}Please respond yes [Y/y] to confirm or press Enter to skip.${COLOR_RESET}";;
esac
done
else
print_info "CONFIRM: ${1} - Assuming NO in unattended mode."
return 1
fi
}
# Function to prompt for configuration
function prompt_for_config() {
print_header "CONFIGURATION SETUP"
print_info "Let's configure the SOURCE and REMOTE settings."
print_info "Press Enter to accept defaults shown in [brackets]"
echo ""
# SOURCE configuration
print_step "SOURCE Configuration"
# Detect current domain from hostname or use saved value
local current_domain=$(hostname -f 2>/dev/null || hostname)
local default_source_prefix="${source_path_prefix:-/sites/${current_domain}/}"
local default_source_webroot="${source_webroot:-files}"
read -p "$(echo -e "${COLOR_CYAN}Source path prefix${COLOR_RESET} [${default_source_prefix}]: ")" input_source_path_prefix
source_path_prefix="${input_source_path_prefix:-$default_source_prefix}"
read -p "$(echo -e "${COLOR_CYAN}Source webroot${COLOR_RESET} [${default_source_webroot}]: ")" input_source_webroot
source_webroot="${input_source_webroot:-$default_source_webroot}"
# REMOTE configuration
print_step "REMOTE Configuration"
# Extract source domain for remote default
local source_domain=$(extract_domain_from_path "$source_path_prefix")
local default_remote_prefix="${remote_path_prefix:-/sites/${source_domain}/}"
local default_remote_webroot="${remote_webroot:-files}"
read -p "$(echo -e "${COLOR_CYAN}Remote IP address or hostname${COLOR_RESET} [${remote_ip_address}]: ")" input_remote_ip
remote_ip_address="${input_remote_ip:-$remote_ip_address}"
read -p "$(echo -e "${COLOR_CYAN}Remote SSH user${COLOR_RESET} [${remote_user:-$(whoami)}]: ")" input_remote_user
remote_user="${input_remote_user:-${remote_user:-$(whoami)}}"
read -p "$(echo -e "${COLOR_CYAN}Remote path prefix${COLOR_RESET} [${default_remote_prefix}]: ")" input_remote_path_prefix
remote_path_prefix="${input_remote_path_prefix:-$default_remote_prefix}"
read -p "$(echo -e "${COLOR_CYAN}Remote webroot${COLOR_RESET} [${default_remote_webroot}]: ")" input_remote_webroot
remote_webroot="${input_remote_webroot:-$default_remote_webroot}"
# Save configuration
save_config
}
####################################################################################
# Process command line arguments
####################################################################################
# Parse long options
TEMP=$(getopt -o huicDe:r:p: --long help,unattended,install-for-user,config,del-ssh-key,exclude:,search-replace,no-search-replace,files-only,no-db-import,install-plugins:,remote-cmds:,exclude-wpconfig,no-exclude-wpconfig,disable-wp-debug,all-tables-with-prefix -n "$0" -- "$@" 2>/dev/null)
# Check for getopt errors
if [[ $? -ne 0 ]]; then
# Fallback to basic getopts if getopt is not available or fails
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
;;
-u|--unattended)
unattended_mode=1
shift
;;
-i|--install-for-user)
install_for_user=1
shift
;;
-c|--config)
prompt_config=1
shift
;;
-D|--del-ssh-key)
delete_ssh_keys=1
shift
;;
-e|--exclude)
if [[ -z "$2" ]]; then
print_error "--exclude requires an argument"
exit 1
fi
# Parse space-delimited list and add to excludes array
read -ra exclude_items <<< "$2"
excludes+=("${exclude_items[@]}")
shift 2
;;
--search-replace)
do_search_replace=1
shift
;;
--no-search-replace)
do_search_replace=0
shift
;;
--files-only)
files_only=1
shift
;;
--no-db-import)
no_db_import=1
shift
;;
-p|--install-plugins)
if [[ -z "$2" ]]; then
print_error "--install-plugins requires a space-delimited list of plugins"
exit 1
fi
plugins_to_install="$2"
install_plugins=1
shift 2
;;
-r|--remote-cmds)
if [[ -z "$2" ]]; then
print_error "--remote-cmds requires a quoted string of commands"
exit 1
fi
remote_commands="$2"
shift 2
;;
--exclude-wpconfig)
exclude_wpconfig=1
shift
;;
--no-exclude-wpconfig)
exclude_wpconfig=0
shift
;;
--disable-wp-debug)
disable_wp_debug=1
shift
;;
--all-tables-with-prefix)
all_tables_with_prefix=1
shift
;;
*)
print_error "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
else
eval set -- "$TEMP"
while true; do
case "$1" in
-h|--help)
show_help
;;
-u|--unattended)
unattended_mode=1
shift
;;
-i|--install-for-user)
install_for_user=1
shift
;;
-c|--config)
prompt_config=1
shift
;;
-D|--del-ssh-key)
delete_ssh_keys=1
shift
;;
-e|--exclude)
# Parse space-delimited list and add to excludes array
read -ra exclude_items <<< "$2"
excludes+=("${exclude_items[@]}")
shift 2
;;
--search-replace)
do_search_replace=1
shift
;;
--no-search-replace)
do_search_replace=0
shift
;;
--files-only)
files_only=1
shift
;;
--no-db-import)
no_db_import=1
shift
;;
-p|--install-plugins)
plugins_to_install="$2"
install_plugins=1
shift 2
;;
-r|--remote-cmds)
remote_commands="$2"
shift 2
;;
--exclude-wpconfig)
exclude_wpconfig=1
shift
;;
--no-exclude-wpconfig)
exclude_wpconfig=0
shift
;;
--disable-wp-debug)
disable_wp_debug=1
shift
;;
--all-tables-with-prefix)
all_tables_with_prefix=1
shift
;;
--)
shift
break
;;
*)
print_error "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
fi
####################################################################################
# Set up
####################################################################################
# Clear the screen
clear
# Show banner
print_header "WP Push Remote v${script_version}"
# Load saved configuration (unless prompting for new config)
if [[ $prompt_config -eq 0 ]]; then
load_config
fi
# Prompt for configuration if requested
if [[ $prompt_config -eq 1 ]]; then
prompt_for_config
fi
# Handle SSH key deletion if requested
if [[ $delete_ssh_keys -eq 1 ]]; then
delete_ssh_key_pairs
fi
# Handle install-for-user if requested
if [[ $install_for_user -eq 1 ]]; then
install_for_user
fi
# Normalize paths
normalize_paths
# Validate configuration
validate_config
# Check for WP-CLI
if ! command_exists wp; then
print_error "WP-CLI is not installed or not in PATH"
print_info "Please install WP-CLI: https://wp-cli.org/#installing"
exit 1
fi
# Set up random rnd_str for database backup filename
rnd_str=$(generate_random_string)
rnd_str_key="38fh"
# Set paths / prefixes
db_export_prefix="wp_db_export_"
source_path="${source_path_prefix}${source_webroot}"
remote_path="${remote_path_prefix}${remote_webroot}"
source_db_name="${db_export_prefix}${rnd_str}${rnd_str_key}.sql"
current_user=$(whoami)
# Auto-assign paths for search-replace if not already set
if [[ -z "$wp_search_replace_source_path" ]]; then
wp_search_replace_source_path="$source_path"
fi
if [[ -z "$wp_search_replace_remote_path" ]]; then
wp_search_replace_remote_path="$remote_path"
fi
if (( exclude_wpconfig == 1 )); then
excludes+=(wp-config.php)
fi
# Get hostname IP (handle multiple IPs)
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}' || hostname)
print_step "START WP PUSH site FROM ${local_ip} TO ${remote_ip_address}"
print_info "Script: 'wp-push-remote.sh' v${script_version}"
# Try to detect source URL only if WordPress is installed and accessible
if [[ -f "${source_path}/wp-config.php" ]]; then
source_url=$(wp option get siteurl --path="${source_path}" 2>/dev/null || echo "")
if [[ -n "$source_url" ]]; then
print_info "Source URL: ${source_url}"
# Assign detected source URL to search-replace variable if not already set
if [[ -z "$wp_search_replace_source_url" ]]; then
wp_search_replace_source_url="$source_url"
fi
else
print_info "Source URL: Unable to detect (WP-CLI may not be configured)"
fi
else
print_info "Source URL: Not detected (WordPress not found at ${source_path})"
fi
echo -e "${COLOR_CYAN}Source:${COLOR_RESET} ${current_user}@${source_path}"
echo -e "${COLOR_CYAN}Remote:${COLOR_RESET} ${remote_user}@${remote_ip_address}:${remote_path}"
echo -e "${COLOR_CYAN}Excludes:${COLOR_RESET} ${excludes[*]}"
if [[ -n "${plugins_to_install}" ]]; then
echo -e "${COLOR_CYAN}Plugins to install:${COLOR_RESET} ${plugins_to_install}"
fi
if [[ -n "${remote_commands}" ]]; then
echo -e "${COLOR_CYAN}Remote commands:${COLOR_RESET} ${remote_commands}"
fi
# Display option flags
print_info "Configuration Flags:"
echo -e " ${COLOR_CYAN}do_search_replace:${COLOR_RESET} ${do_search_replace}"
echo -e " ${COLOR_CYAN}files_only:${COLOR_RESET} ${files_only}"
echo -e " ${COLOR_CYAN}no_db_import:${COLOR_RESET} ${no_db_import}"
echo -e " ${COLOR_CYAN}exclude_wpconfig:${COLOR_RESET} ${exclude_wpconfig}"
echo -e " ${COLOR_CYAN}disable_wp_debug:${COLOR_RESET} ${disable_wp_debug}"
echo -e " ${COLOR_CYAN}all_tables_with_prefix:${COLOR_RESET} ${all_tables_with_prefix}"
# Check for existing SSH keys (Ed25519 preferred, RSA fallback)
ssh_key_path=""
if [[ -f ~/.ssh/id_ed25519_remote_${remote_user} ]]; then
ssh_key_path=~/.ssh/id_ed25519_remote_${remote_user}
print_info "Using existing Ed25519 SSH key: ${ssh_key_path}"
elif [[ -f ~/.ssh/id_rsa_remote_${remote_user} ]]; then
ssh_key_path=~/.ssh/id_rsa_remote_${remote_user}
print_info "Using existing RSA SSH key: ${ssh_key_path}"
fi
# If no key exists, offer to generate one
if [[ -z "$ssh_key_path" ]]; then
if [[ $unattended_mode -eq 0 ]]; then
if ( user_prompt "No SSH key found - OK to generate one now?" ); then
# Generate SSH key (ed25519 is preferred on Ubuntu 22.04+ for better performance and security)
print_info "Generating Ed25519 SSH key (recommended for Ubuntu 22.04+)..."
if ssh-keygen -t ed25519 -C "${current_user}@${local_ip} - Added by wp-push-remote.sh" -f ~/.ssh/id_ed25519_remote_${remote_user} -N ""; then
# Set proper permissions
chmod 600 ~/.ssh/id_ed25519_remote_${remote_user}
chmod 644 ~/.ssh/id_ed25519_remote_${remote_user}.pub
ssh_key_path=~/.ssh/id_ed25519_remote_${remote_user}
print_success "SSH key generated: ${ssh_key_path}"
echo -e "\n${COLOR_BOLD_YELLOW}Public key:${COLOR_RESET}\n"
cat ${ssh_key_path}.pub
echo -e "\n\n${COLOR_BOLD_YELLOW}IMPORTANT:${COLOR_RESET} Add this key to the REMOTE server's authorized_keys file for user '${remote_user}'"
else
print_error "Failed to generate SSH key"
exit 1
fi
else
print_error "ABORTED!"
exit 1
fi
else
print_warning "No SSH key found - Skipping key generation in unattended mode."
print_warning "Script may fail if SSH authentication is not configured."
# Set a default path anyway for potential failure later
ssh_key_path=~/.ssh/id_ed25519_remote_${remote_user}
fi
fi
if [[ $unattended_mode -eq 0 ]]; then
if ( user_prompt_default_no "Test the connection to the remote server?" ); then
print_step "Testing the connection: ssh ${remote_user}@${remote_ip_address}"
print_info "If you get a password prompt, then the key is not set up correctly."
sleep 1
ssh -q -t -i "${ssh_key_path}" ${remote_user}@${remote_ip_address} << EOF
shopt -s dotglob
echo -e "\n${COLOR_BOLD_GREEN}SUCCESS! Connected to REMOTE: \$(whoami)@\$(hostname) (\$(hostname -I))${COLOR_RESET}"
echo -e "Returning to the local server ..."
sleep 1
EOF
else
print_warning "Connection NOT tested!"
fi