-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is lifted from the FileDepotFtp code in the manageiq repo, and is just some shared methods around connecting to an FTP endpoint. Adds some tests around it as well.
- Loading branch information
1 parent
04552b0
commit afa4832
Showing
2 changed files
with
268 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
require 'net/ftp' | ||
require 'logger' | ||
|
||
# Helper methods for net/ftp based classes and files. | ||
# | ||
# Will setup a `@ftp` attr_accessor to be used as the return value for | ||
# `.connect`, the main method being provided in this class. Will also setup | ||
# logging if not already done for the particular class (that follows the | ||
# VmdbLogger conventions, and setup a `uri` attr_accessor if that doesn't | ||
# already exist. | ||
module MiqFtpLib | ||
def self.included(klass) | ||
klass.send(:attr_accessor, :ftp) | ||
end | ||
|
||
def connect(cred_hash = nil) | ||
host = URI(uri).hostname | ||
|
||
begin | ||
_log.info("Connecting to FTP host #{host_ref}...") | ||
@ftp = Net::FTP.new(host) | ||
# Use passive mode to avoid firewall issues see http://slacksite.com/other/ftp.html#passive | ||
@ftp.passive = true | ||
# @ftp.debug_mode = true if settings[:debug] # TODO: add debug option | ||
creds = cred_hash ? [cred_hash[:username], cred_hash[:password]] : login_credentials | ||
@ftp.login(*creds) | ||
_log.info("Successfully connected FTP host #{host_ref}...") | ||
rescue SocketError => err | ||
_log.error("Failed to connect. #{err.message}") | ||
raise | ||
rescue Net::FTPPermError => err | ||
_log.error("Failed to login. #{err.message}") | ||
raise | ||
else | ||
@ftp | ||
end | ||
end | ||
|
||
def file_exists?(file_or_directory) | ||
!ftp.nlst(file_or_directory.to_s).empty? | ||
rescue Net::FTPPermError | ||
false | ||
end | ||
|
||
private | ||
|
||
def host_ref | ||
return @host_ref if @host_ref | ||
@host_ref = URI(uri).hostname | ||
@host_ref << " (#{name})" if respond_to?(:name) | ||
@host_ref | ||
end | ||
|
||
def create_directory_structure(directory_path) | ||
pwd = ftp.pwd | ||
directory_path.to_s.split('/').each do |directory| | ||
unless ftp.nlst.include?(directory) | ||
_log.info("creating #{directory}") | ||
ftp.mkdir(directory) | ||
end | ||
ftp.chdir(directory) | ||
end | ||
ftp.chdir(pwd) | ||
end | ||
|
||
def with_connection(cred_hash = nil) | ||
raise _("no block given") unless block_given? | ||
_log.info("Connecting through #{self.class.name}: [#{host_ref}]") | ||
begin | ||
connect(cred_hash) | ||
yield @ftp | ||
ensure | ||
@ftp.try(:close) && @ftp = nil | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
require 'util/miq_ftp_lib' | ||
|
||
class FTPKlass | ||
include MiqFtpLib | ||
|
||
attr_accessor :uri | ||
|
||
def self.instance_logger | ||
Logger.new(File::NULL) # null logger (for testing) | ||
end | ||
|
||
private | ||
|
||
def _log | ||
self.class.instance_logger | ||
end | ||
end | ||
|
||
class OtherFTPKlass | ||
include MiqFtpLib | ||
|
||
attr_accessor :uri | ||
|
||
def _log | ||
private_log_method | ||
end | ||
|
||
private | ||
|
||
def private_log_method | ||
Logger.new(File::NULL) # null logger (for testing) | ||
end | ||
|
||
def login_credentials | ||
%w(ftpuser ftppass) | ||
end | ||
end | ||
|
||
shared_examples "connecting" do |valid_cred_hash| | ||
let(:cred_hash) { valid_cred_hash } | ||
|
||
before { subject.uri = "ftp://localhost" } | ||
|
||
it "logs in with valid credentials" do | ||
expect { subject.connect(cred_hash) }.not_to raise_error | ||
end | ||
|
||
it "sets the connection to passive" do | ||
subject.connect(cred_hash) | ||
expect(subject.ftp.passive).to eq(true) | ||
end | ||
|
||
context "with an invalid ftp credentials" do | ||
let(:cred_hash) { { :username => "invalid", :password => "alsoinvalid" } } | ||
|
||
it "raises a Net::FTPPermError" do | ||
expect { subject.connect(cred_hash) }.to raise_error(Net::FTPPermError) | ||
end | ||
end | ||
end | ||
|
||
shared_examples "with a connection" do |valid_cred_hash| | ||
let(:cred_hash) { valid_cred_hash } | ||
let(:error_msg) { "no block given" } | ||
|
||
before do | ||
subject.uri = "ftp://localhost" | ||
allow(subject).to receive(:_).with(error_msg).and_return(error_msg) | ||
end | ||
|
||
def with_connection(&block) | ||
subject.send(:with_connection, cred_hash, &block) | ||
end | ||
|
||
def get_socket(ftp) | ||
ftp.instance_variable_get(:@sock).instance_variable_get(:@io) | ||
end | ||
|
||
it "passes the ftp object to the block" do | ||
with_connection do |ftp| | ||
expect(ftp).to be_a(Net::FTP) | ||
expect(subject.ftp).to be(ftp) | ||
end | ||
end | ||
|
||
it "closes the ftp connection after the block is finished" do | ||
ftp_instance = subject.connect(cred_hash) | ||
# stub further calls to `#connect` | ||
expect(subject).to receive(:connect).and_return(ftp_instance) | ||
|
||
with_connection { |ftp| } | ||
expect(subject.ftp).to eq(nil) | ||
expect(ftp_instance.closed?).to eq(true) | ||
end | ||
|
||
it "raises an error if no block is given" do | ||
expect { with_connection }.to raise_error(RuntimeError, error_msg) | ||
end | ||
end | ||
|
||
describe MiqFtpLib do | ||
subject { FTPKlass.new } | ||
|
||
describe "when included" do | ||
it "has a `ftp` accessor" do | ||
ftp_instance = Net::FTP.new | ||
subject.ftp = ftp_instance | ||
|
||
expect(subject.ftp).to eq ftp_instance | ||
end | ||
end | ||
|
||
describe "#connect", :with_ftp_server do | ||
context "with credentials hash" do | ||
subject { FTPKlass.new } | ||
|
||
include_examples "connecting", :username => "ftpuser", :password => "ftppass" | ||
end | ||
|
||
context "with login_credentials method" do | ||
subject { OtherFTPKlass.new } | ||
|
||
include_examples "connecting" | ||
end | ||
end | ||
|
||
describe "#with_connection", :with_ftp_server do | ||
context "with credentials hash" do | ||
subject { FTPKlass.new } | ||
|
||
include_examples "with a connection", :username => "ftpuser", :password => "ftppass" | ||
end | ||
|
||
context "with login_credentials method" do | ||
subject { OtherFTPKlass.new } | ||
|
||
include_examples "with a connection" | ||
end | ||
end | ||
|
||
describe "#file_exists?", :with_ftp_server do | ||
let(:existing_file) { File.basename(existing_ftp_file) } | ||
|
||
subject { FTPKlass.new.tap { |ftp| ftp.uri = "ftp://localhost" } } | ||
before { subject.connect(valid_ftp_creds) } | ||
|
||
it "returns true if the file exists" do | ||
expect(subject.file_exists?(existing_file)).to eq(true) | ||
end | ||
|
||
it "returns false if the file does not exist" do | ||
expect(subject.file_exists?("#{existing_file}.fake")).to eq(false) | ||
end | ||
end | ||
|
||
# Note: Don't use `file_exists?` to try and test the directory existance. | ||
# Most FTP implementations will send the results of `nlst` as the contents of | ||
# a directory if a directory is given. | ||
# | ||
# In our current implementation, this will return a empty list if the | ||
# directory is empty, thus causing the check to fail. Testing against the | ||
# `ftp.nlst(parent_dir)` will make sure the directory in question is included | ||
# in it's parent. | ||
describe "#create_directory_structure", :with_ftp_server do | ||
subject { OtherFTPKlass.new.tap { |ftp| ftp.uri = "ftp://localhost" } } | ||
before { subject.connect(valid_ftp_creds) } | ||
|
||
it "creates a new nested directory" do | ||
new_dir = "foo/bar/baz" | ||
parent_dir = File.dirname(new_dir) | ||
|
||
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(false) | ||
subject.send(:create_directory_structure, new_dir) | ||
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(true) | ||
end | ||
|
||
context "to an existing directory" do | ||
it "creates the nested directory without messing with the existing" do | ||
existing_dir = existing_ftp_dir | ||
new_dir = File.join(existing_ftp_dir, "foo/bar/baz") | ||
parent_dir = File.dirname(new_dir) | ||
|
||
expect(subject.ftp.nlst.include?(existing_dir)).to eq(true) | ||
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(false) | ||
|
||
subject.send(:create_directory_structure, new_dir) | ||
expect(subject.ftp.nlst.include?(existing_dir)).to eq(true) | ||
expect(subject.ftp.nlst(parent_dir).include?("baz")).to eq(true) | ||
end | ||
end | ||
end | ||
end |