# coding: utf-8 require 'net/ftp' require 'fig/file_not_found_error' require 'fig/logging' require 'fig/network_error' require 'fig/protocol' require 'fig/protocol/netrc_enabled' require 'fig/url' module Fig; end module Fig::Protocol; end # File transfers via FTP class Fig::Protocol::FTP include Fig::Protocol include Fig::Protocol::NetRCEnabled def initialize(login) @login = login initialize_netrc end def download_list(uri) ftp = Net::FTP.new(uri.host) ftp_login(ftp, uri.host, :prompt_for_login) ftp.chdir(uri.path) dirs = ftp.nlst ftp.close download_ftp_list(uri, dirs) end # Determine whether we need to update something. Returns nil to indicate # "don't know". def path_up_to_date?(uri, path, prompt_for_login) begin ftp = Net::FTP.new(uri.host) ftp_login(ftp, uri.host, prompt_for_login) if ftp.size(uri.path) != ::File.size(path) return false end if ftp.mtime(uri.path) <= ::File.mtime(path) return true end return false rescue Net::FTPPermError => error Fig::Logging.debug error.message raise Fig::FileNotFoundError.new error.message, uri rescue SocketError => error Fig::Logging.debug error.message raise Fig::FileNotFoundError.new error.message, uri end end # Returns whether the file was not downloaded because the file already # exists and is already up-to-date. def download(uri, path, prompt_for_login) begin ftp = Net::FTP.new(uri.host) ftp_login(ftp, uri.host, prompt_for_login) if ( ::File.exist?(path) \ && ftp.mtime(uri.path) <= ::File.mtime(path) \ && ftp.size(uri.path) == ::File.size(path) ) Fig::Logging.debug "#{path} is up to date." return false else log_download(uri, path) ftp.getbinaryfile(uri.path, path, 256*1024) return true end rescue Net::FTPPermError => error Fig::Logging.debug error.message raise Fig::FileNotFoundError.new error.message, uri rescue SocketError => error Fig::Logging.debug error.message raise Fig::FileNotFoundError.new error.message, uri rescue Errno::ETIMEDOUT => error Fig::Logging.debug error.message raise Fig::FileNotFoundError.new error.message, uri end end def upload(local_file, uri) ftp_uri = Fig::URL.parse(ENV['FIG_REMOTE_URL']) ftp_root_path = ftp_uri.path ftp_root_dirs = ftp_uri.path.split('/') remote_publish_path = uri.path[0, uri.path.rindex('/')] remote_publish_dirs = remote_publish_path.split('/') # Use array subtraction to deduce which project/version folder to upload # to, i.e. [1,2,3] - [2,3,4] = [1] remote_project_dirs = remote_publish_dirs - ftp_root_dirs Net::FTP.open(uri.host) do |ftp| ftp_login(ftp, uri.host, :prompt_for_login) # Assume that the FIG_REMOTE_URL path exists. ftp.chdir(ftp_root_path) remote_project_dirs.each do |dir| # Can't automatically create parent directories, so do it manually. if ftp.nlst().index(dir).nil? ftp.mkdir(dir) ftp.chdir(dir) else ftp.chdir(dir) end end ftp.putbinaryfile(local_file) end end private def ftp_login(ftp, host, prompt_for_login) begin if @login authentication = get_authentication_for host, prompt_for_login if authentication ftp.login authentication.username, authentication.password else ftp.login end else ftp.login end ftp.passive = true rescue Net::FTPPermError => error raise Fig::NetworkError.new "Could not log in: #{error.message}" end return end def download_ftp_list(uri, dirs) # Run a bunch of these in parallel since they're slow as hell num_threads = (ENV['FIG_FTP_THREADS'] || '16').to_i threads = [] all_packages = [] (0..num_threads-1).each { |num| all_packages[num] = [] } (0..num_threads-1).each do |num| threads << Thread.new do packages = all_packages[num] ftp = Net::FTP.new(uri.host) ftp_login(ftp, uri.host, :prompt_for_login) ftp.chdir(uri.path) pos = num while pos < dirs.length pkg = dirs[pos] begin ftp.nlst(dirs[pos]).each do |ver| packages << pkg + '/' + ver end rescue Net::FTPPermError # Ignore this error because it's indicative of the FTP library # encountering a file or directory that it does not have # permission to open. Fig needs to be able to have secure # repos/packages and there is no way easy way to deal with the # permissions issues other than consuming these errors. # # Actually, with FTP, you can't tell the difference between a # file not existing and not having permission to access it (which # is probably a good thing). end pos += num_threads end ftp.close end end threads.each { |thread| thread.join } all_packages.flatten.sort end end