require 'right_publish/profile' require 'right_publish/storage' require 'time' module RightPublish class RepoManager @@repository_type_regex = /\A(\w+)_repo/ @@repository_table = {} def self.get_repository(type) @@repository_table[type].new(type) if @@repository_table[type] end def self.repo_types() type_hash = {} @@repository_table.each_key { |k| type_hash[@@repository_type_regex.match(k.to_s)[1]] = k } type_hash end def self.register_repo(repo_type) repo_key = repo_type::REPO_KEY if repo_type.respond_to?(:new) && @@repository_type_regex.match(repo_key.to_s) @@repository_table[repo_key] = repo_type RightPublish::Profile.instance.register_section(repo_key, repo_type::REPO_OPTIONS) else raise TypeError end nil end end class Repo LOCK_FILE_NAME = 'lock.txt' LOCK_PERIOD = 30 * 60 # 30 minutes LOCK_RETRY_INTERVAL = 1 * 60 # 1 minute def initialize(option_key) @repository_type ||= option_key end # Sync files from remote to local storage def pull() Profile.log("Fetching #{repo_human_name} files from remote...") begin sync_dirs(remote_storage, local_storage, repo_config[:subdir]) rescue Exception => e RightPublish::Profile.log("Could not synchronize storage:\n\t#{e}", :error) raise RuntimeError, "pull from remote failed." end end # Sync files from local to remote storage def push() Profile.log("Committing local #{repo_human_name} files to remote...") begin sync_dirs(local_storage, remote_storage, repo_config[:subdir]) rescue Exception => e RightPublish::Profile.log("Could not sychronize storage:\n\t#{e}", :error) raise RuntimeError, "push to remote failed." end end # Create an index.html file to enable browsing of this repo's subdir. # @option options [String] :subdir a subdirectory (common prefix); only files matching this prefix will be included # @option options [Array] :filter a whitelist of String glob patterns to filter files, i.e. ["*.rpm", "*.deb"] def annotate(options={}) Profile.log("Creating HTML directory listing for #{repo_human_name} files...") options[:subdir] ||= repo_config[:subdir] files = [] RightPublish::Storage.ls(local_storage, :subdir => options[:subdir]) do |file| files << file end # Build merged options hash for annotation, based on profile config plus some options # passed into our class. html_options = Profile.config[:annotation] ? Profile.config[:annotation].dup : {} html_options[:filter] = options[:filter] if options.key?(:filter) strip = options[:subdir].split('/').size html = RightPublish::Annotation.generate_html(files, strip, html_options) output = File.join(repo_config[:subdir], 'index.html') local_dir = local_storage.get_directories local_dir.files.create(:key => output, :body => html) end # Perform one-shot publish of one or more packages. # "One-shot" means: pull, add, annotate and push. def publish(file_or_dir, target) lock do pull add(file_or_dir, target) annotate push end end # Add a new package to local storage and reindex if necessary def add(file_or_dir, targe) raise NotImplementedError, "Subclass responsibility" end private def lock(&block) while( (_lock_time = lock_time) != nil && Time.now.utc <= _lock_time ) do Profile.log "Lock is set to expire at #{_lock_time}, waiting for #{LOCK_RETRY_INTERVAL} second(s) ..." sleep LOCK_RETRY_INTERVAL end file = update_lock_time(LOCK_PERIOD) Profile.log "#{repo_human_name} has been locked for #{LOCK_PERIOD/60} minute(s)." yield file.destroy Profile.log "#{repo_human_name} has been unlocked." end def lock_time if lock_file return Time.parse(lock_file.body) else nil end end def update_lock_time(timeout) remote_storage.get_directories.files.create(:key => lock_file_path, :body => (Time.now.utc + timeout).to_s, :acl=>'public-read', :content_type => 'text/plain') end def lock_file remote_storage.get_directories.files.get(lock_file_path) rescue Excon::Errors::NotFound nil end def lock_file_path File.join(repo_config[:subdir], LOCK_FILE_NAME) end def build_glob(ext) # Transform array into glob compliant pattern '{'.concat Array(ext).join(',').concat '}' end def do_in_subdir(subdir) full_path = File.expand_path(File.join(Profile.config[:local_storage][:cache_dir], subdir)) FileUtils.mkdir_p full_path Dir.chdir(full_path) { yield } end def get_pkg_list(file_or_dir, ext=nil) pkg_list = [] file_or_dir = Array(file_or_dir) file_or_dir.each do |path| path = path.gsub("\\", "/") pkg_list << if File.directory?(path) glob_filter = (ext && "*.#{build_glob(ext)}") || "*" Dir.glob(File.join(path, glob_filter)) else path.split(',') end end pkg_list.flatten! fail("No packages found") if pkg_list.empty? pkg_list.each do |pkg_path| fail("\"#{pkg_path}\" does not appear to be a valid package for the repository.") \ unless File.file?(pkg_path) && Array(ext).any? { |e| pkg_path.end_with?(e) } yield pkg_path if block_given? end if ext pkg_list end # For automation, we want to send the password, however gpg uses getpass # c function, which interacts directly with /dev/pty instead of stdin/stdout # to hide the password as its typed in. So, we need to allocate a pty. # We do this by shelling out to expect script (tcl based). Ruby 1.8 # implementation of "expect" is broken and has some race conditions with # waiting for a child processes to exist, so don't use that. def shellout_with_password(cmd) password = repo_config[:gpg_password] raise Exception, ":gpg_password must be supplied when signing packages" unless password ENV['GPG_PASSWORD'] = password bin_dir = File.expand_path("../../../bin", __FILE__) autosign = File.join(bin_dir, "autosign.expect") system("#{autosign} #{cmd}") end # proxy method def remote_storage get_storage(Profile.config[:remote_storage][:provider]) end # proxy method def local_storage get_storage(Profile.config[:local_storage][:provider]) end def get_storage(provider) type = RightPublish::StorageManager.storage_types[provider] RightPublish::StorageManager.get_storage(type) end def install_file(file, dest) Profile.log("#{file} => #{dest}") local_dir = local_storage.get_directories File.open(file, "rb") { |chunk| local_dir.files.create(:key => File.join(dest, File.basename(file)), :body => chunk) } end def prune_all(glob_pattern) Profile.log("Pruning: #{glob_pattern}") Dir.glob(glob_pattern) { |f| File.unlink(f) } end def repo_config() Profile.config[@repository_type] end def repo_human_name @repository_type.to_s.sub(/_/, ' ') end def sync_dirs(src, dest, subdir='') RightPublish::Storage.sync_dirs(src, dest, :subdir => subdir, :sweep => true) end end end require 'right_publish/repos/apt' require 'right_publish/repos/gem' require 'right_publish/repos/msi' require 'right_publish/repos/yum' require 'right_publish/repos/zypp'