Skip to content

Commit

Permalink
Added support for SFTP protocols.
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael van Rooijen committed Mar 4, 2011
1 parent 1535fee commit a54cf21
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 5 deletions.
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ PATH
dropbox (~> 1.2.3)
fog (~> 0.5.3)
mail (~> 2.2.15)
net-scp (~> 1.0.4)
net-sftp (~> 2.0.5)
thor (~> 0.14.6)

GEM
Expand Down Expand Up @@ -48,6 +50,10 @@ GEM
mime-types (1.16)
mocha (0.9.12)
multipart-post (1.1.0)
net-scp (1.0.4)
net-ssh (>= 1.99.1)
net-sftp (2.0.5)
net-ssh (>= 2.0.9)
net-ssh (2.1.0)
nokogiri (1.4.4)
notifiers (1.1.0)
Expand Down
10 changes: 6 additions & 4 deletions backup.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ Gem::Specification.new do |gem|

##
# Production gem dependencies
gem.add_dependency 'thor', ['~> 0.14.6']
gem.add_dependency 'fog', ['~> 0.5.3' ]
gem.add_dependency 'dropbox', ['~> 1.2.3' ]
gem.add_dependency 'mail', ['~> 2.2.15']
gem.add_dependency 'thor', ['~> 0.14.6'] # CLI
gem.add_dependency 'fog', ['~> 0.5.3' ] # Amazon S3, Rackspace Cloud Files
gem.add_dependency 'dropbox', ['~> 1.2.3' ] # Dropbox
gem.add_dependency 'mail', ['~> 2.2.15'] # Mail
gem.add_dependency 'net-sftp', ['~> 2.0.5' ] # SFTP Protocol
gem.add_dependency 'net-scp', ['~> 1.0.4' ] # SCP Protocol

end
4 changes: 3 additions & 1 deletion lib/backup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module Backup
# You can do:
# database MySQL do |mysql|
DATABASES = ['MySQL', 'PostgreSQL', 'MongoDB', 'Redis']
STORAGES = ['S3', 'CloudFiles', 'Dropbox', 'FTP']
STORAGES = ['S3', 'CloudFiles', 'Dropbox', 'FTP', 'SFTP']
COMPRESSORS = ['Gzip']
ENCRYPTORS = ['OpenSSL']
NOTIFIERS = ['Mail']
Expand Down Expand Up @@ -65,6 +65,7 @@ module Storage
autoload :CloudFiles, File.join(CONFIGURATION_PATH, 'storage', 'cloudfiles')
autoload :Dropbox, File.join(CONFIGURATION_PATH, 'storage', 'dropbox')
autoload :FTP, File.join(CONFIGURATION_PATH, 'storage', 'ftp')
autoload :SFTP, File.join(CONFIGURATION_PATH, 'storage', 'sftp')
end

module Database
Expand All @@ -85,6 +86,7 @@ module Storage
autoload :CloudFiles, File.join(STORAGE_PATH, 'cloudfiles')
autoload :Dropbox, File.join(STORAGE_PATH, 'dropbox')
autoload :FTP, File.join(STORAGE_PATH, 'ftp')
autoload :SFTP, File.join(STORAGE_PATH, 'sftp')
end

##
Expand Down
25 changes: 25 additions & 0 deletions lib/backup/configuration/storage/sftp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# encoding: utf-8

module Backup
module Configuration
module Storage
class SFTP < Base
class << self

##
# Server credentials
attr_accessor :username, :password

##
# Server IP Address and SFTP port
attr_accessor :ip, :port

##
# Path to store backups to
attr_accessor :path

end
end
end
end
end
102 changes: 102 additions & 0 deletions lib/backup/storage/sftp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# encoding: utf-8

##
# Only load the Net::SFTP library/gem when the Backup::Storage::SFTP class is loaded
require 'net/sftp'

module Backup
module Storage
class SFTP < Base

##
# Server credentials
attr_accessor :username, :password

##
# Server IP Address and SFTP port
attr_accessor :ip, :port

##
# Path to store backups to
attr_accessor :path

##
# Creates a new instance of the SFTP storage object
# First it sets the defaults (if any exist) and then evaluates
# the configuration block which may overwrite these defaults
def initialize(&block)
load_defaults!
instance_eval(&block) if block_given?
@time = TIME
@path = path.sub(/^\~\//, '')
end

##
# This is the remote path to where the backup files will be stored
def remote_path
File.join(path, TRIGGER)
end

##
# Performs the backup transfer
def perform!
transfer!
cycle!
end

private

##
# Establishes a connection to the remote server and returns the Net::SFTP object.
# Not doing any instance variable caching because this object gets persisted in YAML
# format to a file and will issues. This, however has no impact on performance since it only
# gets invoked once per object for a #transfer! and once for a remove! Backups run in the
# background anyway so even if it were a bit slower it shouldn't matter.
def connection
Net::SFTP.start(ip, username, :password => password, :port => port)
end

##
# Transfers the archived file to the specified remote server
def transfer!
Logger.message("#{ self.class } started transferring \"#{ remote_file }\".")
create_remote_directories!
connection.upload!(
File.join(local_path, local_file),
File.join(remote_path, remote_file)
)
end

##
# Removes the transferred archive file from the server
def remove!
begin
connection.remove!(
File.join(remote_path, remote_file)
)
rescue Net::SFTP::StatusException
Logger.warn "Could not remove file \"#{ File.join(remote_path, remote_file) }\"."
end
end

##
# Creates (if they don't exist yet) all the directories on the remote
# server in order to upload the backup file. Net::SFTP 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::SFTP raises an exception when
# the directory it's trying ot create already exists, so we have rescue it
def create_remote_directories!
path_parts = Array.new
remote_path.split('/').each do |path_part|
path_parts << path_part
begin
connection.mkdir!(path_parts.join('/'))
rescue Net::SFTP::StatusException
end
end
end

end
end
end
40 changes: 40 additions & 0 deletions spec/configuration/storage/sftp_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# encoding: utf-8

require File.dirname(__FILE__) + '/../../spec_helper'

describe Backup::Configuration::Storage::SFTP do
before do
Backup::Configuration::Storage::SFTP.defaults do |sftp|
sftp.username = 'my_username'
sftp.password = 'my_password'
sftp.ip = '123.45.678.90'
sftp.port = 22
sftp.path = '~/backups/'
sftp.keep = 20
end
end

it 'should set the default sftp configuration' do
sftp = Backup::Configuration::Storage::SFTP
sftp.username.should == 'my_username'
sftp.password.should == 'my_password'
sftp.ip.should == '123.45.678.90'
sftp.port.should == 22
sftp.path.should == '~/backups/'
sftp.keep.should == 20
end

describe '#clear_defaults!' do
it 'should clear all the defaults, resetting them to nil' do
Backup::Configuration::Storage::SFTP.clear_defaults!

sftp = Backup::Configuration::Storage::SFTP
sftp.username.should == nil
sftp.password.should == nil
sftp.ip.should == nil
sftp.port.should == nil
sftp.path.should == nil
sftp.keep.should == nil
end
end
end
119 changes: 119 additions & 0 deletions spec/storage/sftp_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# encoding: utf-8

require File.dirname(__FILE__) + '/../spec_helper'

describe Backup::Storage::SFTP do

let(:sftp) do
Backup::Storage::SFTP.new do |sftp|
sftp.username = 'my_username'
sftp.password = 'my_password'
sftp.ip = '123.45.678.90'
sftp.port = 22
sftp.path = '~/backups/'
sftp.keep = 20
end
end

before do
Backup::Configuration::Storage::SFTP.clear_defaults!
end

it 'should have defined the configuration properly' do
sftp.username.should == 'my_username'
sftp.password.should == 'my_password'
sftp.ip.should == '123.45.678.90'
sftp.port.should == 22
sftp.path.should == 'backups/'
sftp.keep.should == 20
end

it 'should use the defaults if a particular attribute has not been defined' do
Backup::Configuration::Storage::SFTP.defaults do |sftp|
sftp.username = 'my_default_username'
sftp.password = 'my_default_password'
sftp.path = '~/backups'
end

sftp = Backup::Storage::SFTP.new do |sftp|
sftp.password = 'my_password'
sftp.ip = '123.45.678.90'
end

sftp.username.should == 'my_default_username'
sftp.password.should == 'my_password'
sftp.ip.should == '123.45.678.90'
sftp.port.should == nil
end

describe '#connection' do
it 'should establish a connection to the remote server using the provided ip address and credentials' do
Net::SFTP.expects(:start).with('123.45.678.90', 'my_username', :password => 'my_password', :port => 22)
sftp.send(:connection)
end
end

describe '#transfer!' do
let(:connection) { mock('Fog::Storage') }

before do
Net::SFTP.stubs(:start).returns(connection)
sftp.stubs(:create_remote_directories!)
Backup::Logger.stubs(:message)
end

it 'should transfer the provided file to the path' do
Backup::Model.new('blah', 'blah') {}
file = mock("Backup::Storage::SFTP::File")

sftp.expects(:create_remote_directories!)
connection.expects(:upload!).with(
File.join(Backup::TMP_PATH, "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar"),
File.join('backups/myapp', "#{ Backup::TIME }.#{ Backup::TRIGGER }.tar")
)

sftp.send(:transfer!)
end
end

describe '#remove!' do
let(:connection) { mock('Net::SFTP') }

before do
Net::SFTP.stubs(:start).returns(connection)
end

it 'should remove the file from the remote server path' do
connection.expects(:remove!).with("backups/myapp/#{ Backup::TIME }.#{ Backup::TRIGGER }.tar")
sftp.send(:remove!)
end
end

describe '#create_remote_directories!' do
let(:connection) { mock('Net::SFTP') }

before do
Net::SFTP.stubs(:start).returns(connection)
end

it 'should properly create remote directories one by one' do
sftp.path = 'backups/some_other_folder/another_folder'

connection.expects(:mkdir!).with('backups')
connection.expects(:mkdir!).with('backups/some_other_folder')
connection.expects(:mkdir!).with('backups/some_other_folder/another_folder')
connection.expects(:mkdir!).with('backups/some_other_folder/another_folder/myapp')

sftp.send(:create_remote_directories!)
end
end

describe '#perform' do
it 'should invoke transfer! and cycle!' do
sftp.expects(:transfer!)
sftp.expects(:cycle!)
sftp.perform!
end
end

end

0 comments on commit a54cf21

Please sign in to comment.