lib/stockboy/providers/ftp.rb
require 'stockboy/provider'
module Stockboy::Providers
# Get data from a remote FTP server
#
# Allows for selecting the appropriate file to be read from the given
# directory by glob pattern or regex pattern (glob string is more efficient
# for listing files from FTP). By default the +:last+ file in the list is
# used, but can be controlled by sorting and reducing with the {#pick}
# option.
#
# == Job template DSL
#
# provider :ftp do
# host 'ftp.example.com'
# username 'example'
# password '424242'
# file_dir 'data/daily'
# file_name /report-[0-9]+\.csv/
# pick ->(list) { list[-2] }
# end
#
class FTP < Stockboy::Provider
require_relative 'ftp/ftp_adapter'
require_relative 'ftp/sftp_adapter'
# @!group Options
# Host name or IP address for FTP server connection
#
# @!attribute [rw] host
# @return [String]
# @example
# host "ftp.example.com"
#
dsl_attr :host
# Use a passive or active connection
#
# @!attribute [rw] passive
# @return [Boolean]
# @example
# passive true
#
dsl_attr :passive
# User name for connection credentials
#
# @!attribute [rw] username
# @return [String]
# @example
# username "arthur"
#
dsl_attr :username
# Password for connection credentials
#
# @!attribute [rw] password
# @return [String]
# @example
# password "424242"
#
dsl_attr :password
# Use binary mode for file transfers
#
# @!attribute [rw] binary
# @return [Boolean]
# @example
# binary true
#
dsl_attr :binary
# Use SFTP protocol for file transfers
#
# @!attribute [rw] secure
# @return [Boolean]
# @example
# secure true
#
dsl_attr :secure
# @macro provider.file_options
dsl_attr :file_name
dsl_attr :file_dir
dsl_attr :file_newer, alias: :since
dsl_attr :file_smaller
dsl_attr :file_larger
# @macro provider.pick_option
dsl_attr :pick
# @!endgroup
# Initialize a new FTP provider
#
def initialize(opts={}, &block)
super(opts, &block)
@host = opts[:host]
@passive = opts[:passive]
@username = opts[:username]
@password = opts[:password]
@secure = opts[:secure]
@binary = opts[:binary]
@file_dir = opts[:file_dir]
@file_name = opts[:file_name]
@file_newer = opts[:file_newer]
@file_smaller = opts[:file_smaller]
@file_larger = opts[:file_larger]
@pick = opts[:pick] || :last
DSL.new(self).instance_eval(&block) if block_given?
@open_adapter = nil
end
def adapter_class
secure ? SFTPAdapter : FTPAdapter
end
def adapter
return yield @open_adapter if @open_adapter
adapter_class.new(self).open do |ftp|
@open_adapter = ftp
ftp.chdir file_dir if file_dir
response = yield ftp
@open_adapter = nil
response
end
rescue adapter_class.exception_class => e
errors << e.message
logger.warn e.message
nil
end
def client
adapter { |ftp| yield ftp.client }
end
def matching_file
return @matching_file if @matching_file
adapter do |ftp|
file_listing = ftp.list_files
@matching_file = pick_from file_listing.select(&file_name_matcher)
end
end
def delete_data
raise Stockboy::OutOfSequence, "must confirm #matching_file or calling #data" unless picked_matching_file?
adapter do |ftp|
logger.info "FTP deleting file #{host} #{file_dir}/#{matching_file}"
ftp.delete matching_file
matching_file
end
end
def clear
super
@matching_file = nil
@data_time = nil
@data_size = nil
end
private
def fetch_data
adapter do |ftp|
validate_file(matching_file)
if valid?
logger.debug "FTP getting file #{inspect_matching_file}"
@data = ftp.download(matching_file)
logger.debug "FTP got file #{inspect_matching_file} (#{data_size} bytes)"
end
end
!@data.nil?
end
def picked_matching_file?
!!@matching_file
end
def validate
errors << "host must be specified" if host.blank?
errors << "file_name must be specified" if file_name.blank?
errors.empty?
end
def file_name_matcher
case file_name
when Regexp
->(i) { i =~ file_name }
when String
->(i) { ::File.fnmatch(file_name, i) }
end
end
def validate_file(data_file)
return errors << "No matching files" unless data_file
validate_file_newer(data_file)
validate_file_smaller(data_file)
validate_file_larger(data_file)
end
def validate_file_newer(data_file)
@data_time ||= adapter { |ftp| ftp.modification_time(data_file) }
if file_newer and @data_time < file_newer
errors << "No new files since #{file_newer}"
end
end
def validate_file_smaller(data_file)
@data_size ||= adapter { |ftp| ftp.size(data_file) }
if file_smaller and @data_size > file_smaller
errors << "File size larger than #{file_smaller}"
end
end
def validate_file_larger(data_file)
@data_size ||= adapter { |ftp| ftp.size(data_file) }
if file_larger and @data_size < file_larger
errors << "File size smaller than #{file_larger}"
end
end
def inspect_matching_file
"#{host} #{file_dir}/#{matching_file}"
end
end
end