wordpress-deploy 1.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|