lib/backup/storage/ftp.rb
require "net/ftp"
module Backup
module Storage
class FTP < Base
include Storage::Cycler
##
# Server credentials
attr_accessor :username, :password
##
# Server IP Address and FTP port
attr_accessor :ip, :port
##
# Use passive mode?
attr_accessor :passive_mode
##
# Configure connection open and read timeouts.
# Net::FTP's open_timeout and read_timeout will both be configured using
# this setting.
# @!attribute [rw] timeout
# @param [Integer|Float]
# @return [Integer|Float]
attr_accessor :timeout
def initialize(model, storage_id = nil)
super
@port ||= 21
@path ||= "backups"
@passive_mode ||= false
@timeout ||= nil
path.sub!(/^~\//, "")
end
private
##
# Establishes a connection to the remote server
#
# Note:
# Since the FTP port is defined as a constant in the Net::FTP class, and
# might be required to change by the user, we dynamically remove and
# re-add the constant with the provided port value
def connection
if Net::FTP.const_defined?(:FTP_PORT)
Net::FTP.send(:remove_const, :FTP_PORT)
end; Net::FTP.send(:const_set, :FTP_PORT, port)
# Ensure default passive mode to false.
# Note: The default passive setting changed between Ruby 2.2 and 2.3
if Net::FTP.respond_to?(:default_passive=)
Net::FTP.default_passive = false
end
Net::FTP.open(ip, username, password) do |ftp|
if timeout
ftp.open_timeout = timeout
ftp.read_timeout = timeout
end
ftp.passive = true if passive_mode
yield ftp
end
end
def transfer!
connection do |ftp|
create_remote_path(ftp)
package.filenames.each do |filename|
src = File.join(Config.tmp_path, filename)
dest = File.join(remote_path, filename)
Logger.info "Storing '#{ip}:#{dest}'..."
ftp.put(src, dest)
end
end
end
# Called by the Cycler.
# Any error raised will be logged as a warning.
def remove!(package)
Logger.info "Removing backup package dated #{package.time}..."
remote_path = remote_path_for(package)
connection do |ftp|
package.filenames.each do |filename|
ftp.delete(File.join(remote_path, filename))
end
ftp.rmdir(remote_path)
end
end
##
# Creates (if they don't exist yet) all the directories on the remote
# server in order to upload the backup file. Net::FTP does not support
# paths to directories that don't yet exist when creating new
# directories. Instead, we split the parts up in to an array (for each
# '/') and loop through that to create the directories one by one.
# Net::FTP raises an exception when the directory it's trying to create
# already exists, so we have rescue it
def create_remote_path(ftp)
path_parts = []
remote_path.split("/").each do |path_part|
path_parts << path_part
begin
ftp.mkdir(path_parts.join("/"))
rescue Net::FTPPermError; end
end
end
end
end
end