wordpress-deploy 1.0.0.alpha1
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.
- data/.gitignore +2 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +71 -0
- data/LICENSE.md +24 -0
- data/README.md +22 -0
- data/Rakefile +7 -0
- data/bin/wp-deploy +11 -0
- data/lib/wordpress_deploy/cli/helpers.rb +91 -0
- data/lib/wordpress_deploy/cli/utility.rb +53 -0
- data/lib/wordpress_deploy/config.rb +68 -0
- data/lib/wordpress_deploy/database/base.rb +53 -0
- data/lib/wordpress_deploy/database/mysql.rb +159 -0
- data/lib/wordpress_deploy/errors.rb +70 -0
- data/lib/wordpress_deploy/logger.rb +152 -0
- data/lib/wordpress_deploy/pipeline.rb +110 -0
- data/lib/wordpress_deploy/storage/base.rb +99 -0
- data/lib/wordpress_deploy/storage/ftp.rb +133 -0
- data/lib/wordpress_deploy/storage/local.rb +82 -0
- data/lib/wordpress_deploy/storage/scp.rb +99 -0
- data/lib/wordpress_deploy/storage/sftp.rb +108 -0
- data/lib/wordpress_deploy/version.rb +3 -0
- data/lib/wordpress_deploy.rb +61 -0
- data/spec/config_spec.rb +16 -0
- data/spec/spec_helper.rb +5 -0
- data/wordpress_deploy.gemspec +26 -0
- metadata +201 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module WordpressDeploy
|
4
|
+
module Logger
|
5
|
+
class << self
|
6
|
+
|
7
|
+
attr_accessor :quiet
|
8
|
+
|
9
|
+
##
|
10
|
+
# Outputs a messages to the console and writes it to the backup.log
|
11
|
+
def message(string)
|
12
|
+
to_console loggify(string, :message, :green)
|
13
|
+
to_file loggify(string, :message)
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Outputs an error to the console and writes it to the backup.log
|
18
|
+
# Called when an Exception has caused the backup process to abort.
|
19
|
+
def error(string)
|
20
|
+
to_console loggify(string, :error, :red), true
|
21
|
+
to_file loggify(string, :error)
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Outputs a notice to the console and writes it to the backup.log
|
26
|
+
# Sets #has_warnings? true so :on_warning notifications will be sent
|
27
|
+
def warn(string)
|
28
|
+
@has_warnings = true
|
29
|
+
to_console loggify(string, :warning, :yellow), true
|
30
|
+
to_file loggify(string, :warning)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Outputs the data as if it were a regular 'puts' command,
|
34
|
+
# but also logs it to the backup.log
|
35
|
+
def normal(string)
|
36
|
+
to_console loggify(string)
|
37
|
+
to_file loggify(string)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Silently logs data to the log file
|
42
|
+
def silent(string)
|
43
|
+
to_file loggify(string, :silent)
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Returns an Array of all messages written to the log file for this session
|
48
|
+
def messages
|
49
|
+
@messages ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns true if any warnings have been issued
|
54
|
+
def has_warnings?
|
55
|
+
@has_warnings ||= false
|
56
|
+
end
|
57
|
+
|
58
|
+
def clear!
|
59
|
+
messages.clear
|
60
|
+
@has_warnings = false
|
61
|
+
end
|
62
|
+
|
63
|
+
def truncate!(max_bytes = 500_000)
|
64
|
+
log_file = File.join(Config.log_path, 'backup.log')
|
65
|
+
return unless File.exist?(log_file)
|
66
|
+
|
67
|
+
if File.stat(log_file).size > max_bytes
|
68
|
+
FileUtils.mv(log_file, log_file + '~')
|
69
|
+
File.open(log_file + '~', 'r') do |io_in|
|
70
|
+
File.open(log_file, 'w') do |io_out|
|
71
|
+
io_in.seek(-max_bytes, IO::SEEK_END) && io_in.gets
|
72
|
+
while line = io_in.gets
|
73
|
+
io_out.puts line
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
FileUtils.rm_f(log_file + '~')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
##
|
84
|
+
# Returns the time in [YYYY/MM/DD HH:MM:SS] format
|
85
|
+
def time
|
86
|
+
Time.now.strftime("%Y/%m/%d %H:%M:%S")
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Receives a String, or an Object that responds to #to_s (e.g. an
|
91
|
+
# Exception), from one of the messaging methods and converts it into an
|
92
|
+
# Array of Strings, split on newline separators. Each line is then
|
93
|
+
# formatted into a log format based on the given options, and the Array
|
94
|
+
# returned to be passed to to_console() and/or to_file().
|
95
|
+
def loggify(string, type = false, color = false)
|
96
|
+
lines = string.to_s.split("\n")
|
97
|
+
if type
|
98
|
+
type = send(color, type) if color
|
99
|
+
time_now = time
|
100
|
+
lines.map {|line| "[#{time_now}][#{type}] #{line}" }
|
101
|
+
else
|
102
|
+
lines
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Receives an Array of Strings to be written to the console.
|
108
|
+
def to_console(lines, stderr = false)
|
109
|
+
return if quiet
|
110
|
+
lines.each {|line| stderr ? Kernel.warn(line) : puts(line) }
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Receives an Array of Strings to be written to the log file.
|
115
|
+
def to_file(lines)
|
116
|
+
File.open(File.join(Config.log_path, 'backup.log'), 'a') do |file|
|
117
|
+
lines.each {|line| file.puts line }
|
118
|
+
end
|
119
|
+
messages.push(*lines)
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Invokes the #colorize method with the provided string
|
124
|
+
# and the color code "32" (for green)
|
125
|
+
def green(string)
|
126
|
+
colorize(string, 32)
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Invokes the #colorize method with the provided string
|
131
|
+
# and the color code "33" (for yellow)
|
132
|
+
def yellow(string)
|
133
|
+
colorize(string, 33)
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Invokes the #colorize method the with provided string
|
138
|
+
# and the color code "31" (for red)
|
139
|
+
def red(string)
|
140
|
+
colorize(string, 31)
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Wraps the provided string in colorizing tags to provide
|
145
|
+
# easier to view output to the client
|
146
|
+
def colorize(string, code)
|
147
|
+
"\e[#{code}m#{string}\e[0m"
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module WordpressDeploy
|
4
|
+
class Pipeline
|
5
|
+
include WordpressDeploy::CLI::Helpers
|
6
|
+
|
7
|
+
attr_reader :stderr, :errors
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@commands = []
|
11
|
+
@errors = []
|
12
|
+
@stderr = ''
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Adds a command to be executed in the pipeline.
|
17
|
+
# Each command will be run in the order in which it was added,
|
18
|
+
# with it's output being piped to the next command.
|
19
|
+
def <<(command)
|
20
|
+
@commands << command
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Runs the command line from `#pipeline` and collects STDOUT/STDERR.
|
25
|
+
# STDOUT is then parsed to determine the exit status of each command.
|
26
|
+
# For each command with a non-zero exit status, a SystemCallError is
|
27
|
+
# created and added to @errors. All STDERR output is set in @stderr.
|
28
|
+
#
|
29
|
+
# Note that there is no accumulated STDOUT from the commands themselves.
|
30
|
+
# Also, the last command should not attempt to write to STDOUT.
|
31
|
+
# Any output on STDOUT from the final command will be sent to STDERR.
|
32
|
+
# This in itself will not cause #run to fail, but will log warnings
|
33
|
+
# when all commands exit with non-zero status.
|
34
|
+
#
|
35
|
+
# Use `#success?` to determine if all commands in the pipeline succeeded.
|
36
|
+
# If `#success?` returns `false`, use `#error_messages` to get an error report.
|
37
|
+
def run
|
38
|
+
Open4.popen4(pipeline) do |pid, stdin, stdout, stderr|
|
39
|
+
pipestatus = stdout.read.gsub("\n", '').split(':').sort
|
40
|
+
pipestatus.each do |status|
|
41
|
+
index, exitstatus = status.split('|').map(&:to_i)
|
42
|
+
if exitstatus > 0
|
43
|
+
command = command_name(@commands[index])
|
44
|
+
@errors << SystemCallError.new(
|
45
|
+
"'#{ command }' returned exit code: #{ exitstatus }", exitstatus
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@stderr = stderr.read.strip
|
50
|
+
end
|
51
|
+
Logger.warn(stderr_messages) if success? && stderr_messages
|
52
|
+
rescue Exception => e
|
53
|
+
raise Errors::Pipeline::ExecutionError.wrap(e)
|
54
|
+
end
|
55
|
+
|
56
|
+
def success?
|
57
|
+
@errors.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Returns a multi-line String, reporting all STDERR messages received
|
62
|
+
# from the commands in the pipeline (if any), along with the SystemCallError
|
63
|
+
# (Errno) message for each command which had a non-zero exit status.
|
64
|
+
#
|
65
|
+
# Each error is wrapped by WordpressDeploy::Errors to provide formatting.
|
66
|
+
def error_messages
|
67
|
+
@error_messages ||= (stderr_messages || '') +
|
68
|
+
"The following system errors were returned:\n" +
|
69
|
+
@errors.map {|err| Errors::Error.wrap(err).message }.join("\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
##
|
75
|
+
# Each command is added as part of the pipeline, grouped with an `echo`
|
76
|
+
# command to pass along the command's index in @commands and it's exit status.
|
77
|
+
# The command's STDERR is redirected to FD#4, and the `echo` command to
|
78
|
+
# report the "index|exit status" is redirected to FD#3.
|
79
|
+
# Each command's STDOUT will be connected to the STDIN of the next subshell.
|
80
|
+
# The entire pipeline is run within a container group, which redirects
|
81
|
+
# FD#3 to STDOUT and FD#4 to STDERR so these can be collected.
|
82
|
+
# FD#1 is redirected to STDERR so that any output from the final command
|
83
|
+
# on STDOUT will generate warnings, since the final command should not
|
84
|
+
# attempt to write to STDOUT, as this would interfere with collecting
|
85
|
+
# the exit statuses.
|
86
|
+
#
|
87
|
+
# There is no guarantee as to the order of this output, which is why the
|
88
|
+
# command's index in @commands is passed along with it's exit status.
|
89
|
+
# And, if multiple commands output messages on STDERR, those messages
|
90
|
+
# may be interleaved. Interleaving of the "index|exit status" outputs
|
91
|
+
# should not be an issue, given the small byte size of the data being written.
|
92
|
+
def pipeline
|
93
|
+
parts = []
|
94
|
+
@commands.each_with_index do |command, index|
|
95
|
+
parts << %Q[{ #{ command } 2>&4 ; echo "#{ index }|$?:" >&3 ; }]
|
96
|
+
end
|
97
|
+
%Q[{ #{ parts.join(' | ') } } 3>&1 1>&2 4>&2]
|
98
|
+
end
|
99
|
+
|
100
|
+
def stderr_messages
|
101
|
+
@stderr_messages ||= @stderr.empty? ? false : <<-EOS.gsub(/^ +/, ' ')
|
102
|
+
Pipeline STDERR Messages:
|
103
|
+
(Note: may be interleaved if multiple commands returned error messages)
|
104
|
+
|
105
|
+
#{ @stderr }
|
106
|
+
EOS
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module WordpressDeploy
|
4
|
+
module Storage
|
5
|
+
class Base
|
6
|
+
include WordpressDeploy::Configuration::Helpers
|
7
|
+
|
8
|
+
##
|
9
|
+
# Sets the limit to how many backups to keep in the remote location.
|
10
|
+
# If exceeded, the oldest will be removed to make room for the newest
|
11
|
+
attr_accessor :keep
|
12
|
+
|
13
|
+
##
|
14
|
+
# (Optional)
|
15
|
+
# User-defined string used to uniquely identify multiple storages of the
|
16
|
+
# same type. This will be appended to the YAML storage file used for
|
17
|
+
# cycling backups.
|
18
|
+
attr_accessor :storage_id
|
19
|
+
|
20
|
+
##
|
21
|
+
# Creates a new instance of the storage object
|
22
|
+
# * Called with super(model, storage_id) from each subclass
|
23
|
+
def initialize(model, storage_id = nil)
|
24
|
+
load_defaults!
|
25
|
+
@model = model
|
26
|
+
@storage_id = storage_id
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Performs the backup transfer
|
31
|
+
def perform!
|
32
|
+
@package = @model.package
|
33
|
+
transfer!
|
34
|
+
cycle!
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
##
|
40
|
+
# Provider defaults to false. Overridden when using a service-based
|
41
|
+
# storage such as Amazon S3, Rackspace Cloud Files or Dropbox
|
42
|
+
def provider
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Each subclass must define a +path+ where remote files will be stored
|
48
|
+
def path; end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Return the storage name, with optional storage_id
|
52
|
+
def storage_name
|
53
|
+
self.class.to_s.sub('WordpressDeploy::', '') +
|
54
|
+
(storage_id ? " (#{storage_id})" : '')
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Returns the local path
|
59
|
+
# This is where any Package to be transferred is located.
|
60
|
+
def local_path
|
61
|
+
Config.tmp_path
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns the remote path for the given Package
|
66
|
+
# This is where the Package will be stored, or was previously stored.
|
67
|
+
def remote_path_for(package)
|
68
|
+
File.join(path, package.trigger, package.time)
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Yields two arguments to the given block: "local_file, remote_file"
|
73
|
+
# The local_file is the full file name:
|
74
|
+
# e.g. "2011.08.30.11.00.02.backup.tar.enc"
|
75
|
+
# The remote_file is the full file name, minus the timestamp:
|
76
|
+
# e.g. "backup.tar.enc"
|
77
|
+
def files_to_transfer_for(package)
|
78
|
+
package.filenames.each do |filename|
|
79
|
+
yield filename, filename[20..-1]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
alias :transferred_files_for :files_to_transfer_for
|
83
|
+
|
84
|
+
##
|
85
|
+
# Adds the current package being stored to the YAML cycle data file
|
86
|
+
# and will remove any old Package file(s) when the storage limit
|
87
|
+
# set by #keep is exceeded. Any errors raised while attempting to
|
88
|
+
# remove older packages will be rescued and a warning will be logged
|
89
|
+
# containing the original error message.
|
90
|
+
def cycle!
|
91
|
+
return unless keep.to_i > 0
|
92
|
+
Logger.message "#{ storage_name }: Cycling Started..."
|
93
|
+
Cycler.cycle!(self, @package)
|
94
|
+
Logger.message "#{ storage_name }: Cycling Complete!"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
##
|
4
|
+
# Only load the Net::FTP library/gem when the WordpressDeploy::Storage::FTP class is loaded
|
5
|
+
require 'net/ftp'
|
6
|
+
require 'pathname'
|
7
|
+
require 'action_view'
|
8
|
+
|
9
|
+
module WordpressDeploy
|
10
|
+
module Storage
|
11
|
+
class FTP < Base
|
12
|
+
include ActionView::Helpers::NumberHelper
|
13
|
+
|
14
|
+
##
|
15
|
+
# Server credentials
|
16
|
+
attr_accessor :username, :password
|
17
|
+
|
18
|
+
##
|
19
|
+
# Server IP Address and FTP port
|
20
|
+
attr_accessor :ip, :port
|
21
|
+
alias :hostname :ip
|
22
|
+
|
23
|
+
##
|
24
|
+
# Path to store backups to
|
25
|
+
attr_accessor :path
|
26
|
+
|
27
|
+
##
|
28
|
+
# use passive mode?
|
29
|
+
attr_accessor :passive_mode
|
30
|
+
|
31
|
+
##
|
32
|
+
# the remote path
|
33
|
+
attr_accessor :remote_path
|
34
|
+
|
35
|
+
##
|
36
|
+
# Creates a new instance of the storage object
|
37
|
+
def initialize(model, storage_id = nil, &block)
|
38
|
+
super(model, storage_id)
|
39
|
+
|
40
|
+
@hostname = hostname
|
41
|
+
@username = username
|
42
|
+
@password = password
|
43
|
+
@remote_path = remote_path
|
44
|
+
@remote_directores = []
|
45
|
+
|
46
|
+
@port ||= 21
|
47
|
+
@path ||= 'backups'
|
48
|
+
@passive_mode ||= false
|
49
|
+
|
50
|
+
instance_eval(&block) if block_given?
|
51
|
+
|
52
|
+
@path = path.sub(/^\~\//, '')
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
##
|
58
|
+
# Establishes a connection to the remote server
|
59
|
+
#
|
60
|
+
# Note:
|
61
|
+
# Since the FTP port is defined as a constant in the Net::FTP class, and
|
62
|
+
# might be required to change by the user, we dynamically remove and
|
63
|
+
# re-add the constant with the provided port value
|
64
|
+
def connection
|
65
|
+
if Net::FTP.const_defined?(:FTP_PORT)
|
66
|
+
Net::FTP.send(:remove_const, :FTP_PORT)
|
67
|
+
end; Net::FTP.send(:const_set, :FTP_PORT, port)
|
68
|
+
|
69
|
+
Net::FTP.open(ip, username, password) do |ftp|
|
70
|
+
ftp.passive = true if passive_mode
|
71
|
+
yield ftp
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Transfers the archived file to the specified remote server
|
77
|
+
def transfer!
|
78
|
+
remote_path = remote_path_for(@package)
|
79
|
+
|
80
|
+
connection do |ftp|
|
81
|
+
create_remote_path(remote_path, ftp)
|
82
|
+
|
83
|
+
files_to_transfer_for(@package) do |local_file, remote_file|
|
84
|
+
Logger.message "#{storage_name} started transferring " +
|
85
|
+
"'#{ local_file }' to '#{ ip }'."
|
86
|
+
ftp.put(
|
87
|
+
File.join(local_path, local_file),
|
88
|
+
File.join(remote_path, remote_file)
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Removes the transferred archive file(s) from the storage location.
|
96
|
+
# Any error raised will be rescued during Cycling
|
97
|
+
# and a warning will be logged, containing the error message.
|
98
|
+
def remove!(package)
|
99
|
+
remote_path = remote_path_for(package)
|
100
|
+
|
101
|
+
connection do |ftp|
|
102
|
+
transferred_files_for(package) do |local_file, remote_file|
|
103
|
+
Logger.message "#{storage_name} started removing " +
|
104
|
+
"'#{ local_file }' from '#{ ip }'."
|
105
|
+
|
106
|
+
ftp.delete(File.join(remote_path, remote_file))
|
107
|
+
end
|
108
|
+
|
109
|
+
ftp.rmdir(remote_path)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Creates (if they don't exist yet) all the directories on the remote
|
115
|
+
# server in order to upload the backup file. Net::FTP does not support
|
116
|
+
# paths to directories that don't yet exist when creating new
|
117
|
+
# directories. Instead, we split the parts up in to an array (for each
|
118
|
+
# '/') and loop through that to create the directories one by one.
|
119
|
+
# Net::FTP raises an exception when the directory it's trying to create
|
120
|
+
# already exists, so we have rescue it
|
121
|
+
def create_remote_path(remote_path, ftp)
|
122
|
+
path_parts = Array.new
|
123
|
+
remote_path.split('/').each do |path_part|
|
124
|
+
path_parts << path_part
|
125
|
+
begin
|
126
|
+
ftp.mkdir(path_parts.join('/'))
|
127
|
+
rescue Net::FTPPermError; end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module WordpressDeploy
|
4
|
+
module Storage
|
5
|
+
class Local < Base
|
6
|
+
|
7
|
+
##
|
8
|
+
# Path where the backup will be stored.
|
9
|
+
attr_accessor :path
|
10
|
+
|
11
|
+
##
|
12
|
+
# Creates a new instance of the storage object
|
13
|
+
def initialize(model, storage_id = nil, &block)
|
14
|
+
super(model, storage_id)
|
15
|
+
|
16
|
+
@path ||= File.join(
|
17
|
+
File.expand_path(ENV['HOME'] || ''),
|
18
|
+
'backups'
|
19
|
+
)
|
20
|
+
|
21
|
+
instance_eval(&block) if block_given?
|
22
|
+
|
23
|
+
@path = File.expand_path(@path)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
##
|
29
|
+
# Transfers the archived file to the specified path
|
30
|
+
def transfer!
|
31
|
+
remote_path = remote_path_for(@package)
|
32
|
+
FileUtils.mkdir_p(remote_path)
|
33
|
+
|
34
|
+
files_to_transfer_for(@package) do |local_file, remote_file|
|
35
|
+
Logger.message "#{storage_name} started transferring '#{ local_file }'."
|
36
|
+
|
37
|
+
src_path = File.join(local_path, local_file)
|
38
|
+
dst_path = File.join(remote_path, remote_file)
|
39
|
+
FileUtils.send(transfer_method, src_path, dst_path)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Removes the transferred archive file(s) from the storage location.
|
45
|
+
# Any error raised will be rescued during Cycling
|
46
|
+
# and a warning will be logged, containing the error message.
|
47
|
+
def remove!(package)
|
48
|
+
remote_path = remote_path_for(package)
|
49
|
+
|
50
|
+
messages = []
|
51
|
+
transferred_files_for(package) do |local_file, remote_file|
|
52
|
+
messages << "#{storage_name} started removing '#{ local_file }'."
|
53
|
+
end
|
54
|
+
Logger.message messages.join("\n")
|
55
|
+
|
56
|
+
FileUtils.rm_r(remote_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Set and return the transfer method.
|
61
|
+
# If this Local Storage is not the last Storage for the Model,
|
62
|
+
# force the transfer to use a *copy* operation and issue a warning.
|
63
|
+
def transfer_method
|
64
|
+
return @transfer_method if @transfer_method
|
65
|
+
|
66
|
+
if self == @model.storages.last
|
67
|
+
@transfer_method = :mv
|
68
|
+
else
|
69
|
+
Logger.warn Errors::Storage::Local::TransferError.new(<<-EOS)
|
70
|
+
Local File Copy Warning!
|
71
|
+
The final backup file(s) for '#{@model.label}' (#{@model.trigger})
|
72
|
+
will be *copied* to '#{remote_path_for(@package)}'
|
73
|
+
To avoid this, when using more than one Storage, the 'Local' Storage
|
74
|
+
should be added *last* so the files may be *moved* to their destination.
|
75
|
+
EOS
|
76
|
+
@transfer_method = :cp
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
##
|
4
|
+
# Only load the Net::SSH and Net::SCP library/gems
|
5
|
+
# when the WordpressDeploy::Storage::SCP class is loaded
|
6
|
+
WordpressDeploy::Dependency.load('net-ssh')
|
7
|
+
WordpressDeploy::Dependency.load('net-scp')
|
8
|
+
|
9
|
+
module WordpressDeploy
|
10
|
+
module Storage
|
11
|
+
class SCP < Base
|
12
|
+
|
13
|
+
##
|
14
|
+
# Server credentials
|
15
|
+
attr_accessor :username, :password
|
16
|
+
|
17
|
+
##
|
18
|
+
# Server IP Address and SCP port
|
19
|
+
attr_accessor :ip, :port
|
20
|
+
|
21
|
+
##
|
22
|
+
# Path to store backups to
|
23
|
+
attr_accessor :path
|
24
|
+
|
25
|
+
##
|
26
|
+
# Creates a new instance of the storage object
|
27
|
+
def initialize(model, storage_id = nil, &block)
|
28
|
+
super(model, storage_id)
|
29
|
+
|
30
|
+
@port ||= 22
|
31
|
+
@path ||= 'backups'
|
32
|
+
|
33
|
+
instance_eval(&block) if block_given?
|
34
|
+
|
35
|
+
@path = path.sub(/^\~\//, '')
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
##
|
41
|
+
# Establishes a connection to the remote server
|
42
|
+
# and yields the Net::SSH connection.
|
43
|
+
# Net::SCP will use this connection to transfer backups
|
44
|
+
def connection
|
45
|
+
Net::SSH.start(
|
46
|
+
ip, username, :password => password, :port => port
|
47
|
+
) {|ssh| yield ssh }
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Transfers the archived file to the specified remote server
|
52
|
+
def transfer!
|
53
|
+
remote_path = remote_path_for(@package)
|
54
|
+
|
55
|
+
connection do |ssh|
|
56
|
+
ssh.exec!("mkdir -p '#{ remote_path }'")
|
57
|
+
|
58
|
+
files_to_transfer_for(@package) do |local_file, remote_file|
|
59
|
+
Logger.message "#{storage_name} started transferring " +
|
60
|
+
"'#{local_file}' to '#{ip}'."
|
61
|
+
|
62
|
+
ssh.scp.upload!(
|
63
|
+
File.join(local_path, local_file),
|
64
|
+
File.join(remote_path, remote_file)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Removes the transferred archive file(s) from the storage location.
|
72
|
+
# Any error raised will be rescued during Cycling
|
73
|
+
# and a warning will be logged, containing the error message.
|
74
|
+
def remove!(package)
|
75
|
+
remote_path = remote_path_for(package)
|
76
|
+
|
77
|
+
messages = []
|
78
|
+
transferred_files_for(package) do |local_file, remote_file|
|
79
|
+
messages << "#{storage_name} started removing " +
|
80
|
+
"'#{local_file}' from '#{ip}'."
|
81
|
+
end
|
82
|
+
Logger.message messages.join("\n")
|
83
|
+
|
84
|
+
errors = []
|
85
|
+
connection do |ssh|
|
86
|
+
ssh.exec!("rm -r '#{remote_path}'") do |ch, stream, data|
|
87
|
+
errors << data if stream == :stderr
|
88
|
+
end
|
89
|
+
end
|
90
|
+
unless errors.empty?
|
91
|
+
raise Errors::Storage::SCP::SSHError,
|
92
|
+
"Net::SSH reported the following errors:\n" +
|
93
|
+
errors.join("\n")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|