#!/usr/bin/env ruby ## # Load RubyGems for Ruby <= 1.8.7 require 'rubygems' require 'tempfile' require 'fileutils' ## # Load Thor for the CLI and POpen4 for reading unix process status begin require 'thor' require 'popen4' rescue LoadError puts "\nBackup requires Thor to load the CLI (Command Line Interface) and POpen4 to determine the status of unix processes." puts "Please install both Thor and POpen4 first:\n\ngem install thor -v '~> 0.14.6'\ngem install popen4 -v '~> 0.1.2'" exit 1 end ## # Load the Backup source require File.expand_path("../../lib/backup", __FILE__) ## # Build the Backup Command Line Interface using Thor class BackupCLI < Thor include Thor::Actions TEMPLATE_DIR = File.expand_path("../../lib/templates", __FILE__) ## # [Perform] # Performs the backup process. The only required option is the --trigger [-t]. # If the other options (--config-file, --data-path, --cache--path, --tmp-path) aren't specified # it'll fallback to the (good) defaults method_option :trigger, :type => :string, :aliases => ['-t', '--triggers'], :required => true method_option :config_file, :type => :string, :aliases => '-c' method_option :data_path, :type => :string, :aliases => '-d' method_option :log_path, :type => :string, :aliases => '-l' method_option :cache_path, :type => :string method_option :tmp_path, :type => :string method_option :quiet, :type => :boolean, :aliases => '-q' desc 'perform', "Performs the backup for the specified trigger.\n" + "You may perform multiple backups by providing multiple triggers, separated by commas.\n\n" + "Example:\n\s\s$ backup perform --triggers backup1,backup2,backup3,backup4\n\n" + "This will invoke 4 backups, and they will run in the order specified (not asynchronous)." def perform ## # Overwrites the CONFIG_FILE location, if --config-file was specified if options[:config_file] Backup.send(:remove_const, :CONFIG_FILE) Backup.send(:const_set, :CONFIG_FILE, options[:config_file]) end ## # Overwrites the DATA_PATH location, if --data-path was specified if options[:data_path] Backup.send(:remove_const, :DATA_PATH) Backup.send(:const_set, :DATA_PATH, options[:data_path]) end ## # Overwrites the LOG_PATH location, if --log-path was specified if options[:log_path] Backup.send(:remove_const, :LOG_PATH) Backup.send(:const_set, :LOG_PATH, options[:log_path]) end ## # Overwrites the CACHE_PATH location, if --cache-path was specified if options[:cache_path] Backup.send(:remove_const, :CACHE_PATH) Backup.send(:const_set, :CACHE_PATH, options[:cache_path]) end ## # Overwrites the TMP_PATH location, if --tmp-path was specified if options[:tmp_path] Backup.send(:remove_const, :TMP_PATH) Backup.send(:const_set, :TMP_PATH, options[:tmp_path]) end ## # Silence Backup::Logger from printing to STDOUT, if --quiet was specified if options[:quiet] Backup::Logger.send(:const_set, :QUIET, options[:quiet]) end ## # Ensure the CACHE_PATH, TMP_PATH and LOG_PATH are created if they do not yet exist Array.new([Backup::CACHE_PATH, Backup::TMP_PATH, Backup::LOG_PATH]).each do |path| FileUtils.mkdir_p(path) end ## # Prepare all trigger names by splitting them by ',' # and finding trigger names matching wildcard triggers = options[:trigger].split(",") triggers.map!(&:strip).map!{ |t| t.include?(Backup::Finder::WILDCARD) ? Backup::Finder.new(t).matching : t }.flatten! #triggers.unique! # Uncomment if its undesirable to call triggers twice ## # Process each trigger triggers.each do |trigger| ## # Defines the TRIGGER constant Backup.send(:const_set, :TRIGGER, trigger) ## # Define the TIME constants Backup.send(:const_set, :TIME, Time.now.strftime("%Y.%m.%d.%H.%M.%S")) ## # Ensure DATA_PATH and DATA_PATH/TRIGGER are created if they do not yet exist FileUtils.mkdir_p(File.join(Backup::DATA_PATH, Backup::TRIGGER)) ## # Parses the backup configuration file and returns the model instance by trigger model = Backup::Finder.new(trigger).find ## # Runs the returned model Backup::Logger.message "Performing backup for #{model.label}!" model.perform! ## # Removes the TRIGGER constant Backup.send(:remove_const, :TRIGGER) if defined? Backup::TRIGGER ## # Removes the TIME constant Backup.send(:remove_const, :TIME) if defined? Backup::TIME ## # Reset the Backup::Model.current to nil for the next potential run Backup::Model.current = nil ## # Reset the Backup::Model.all to an empty array since this will be # re-filled during the next Backup::Finder.new(arg1, arg2).find Backup::Model.all = Array.new ## # Reset the Backup::Model.extension to 'tar' so it's at it's # initial state when the next Backup::Model initializes Backup::Model.extension = 'tar' end end ## # [Generate] # Generates a configuration file based on the arguments passed in. # For example, running $ backup generate --databases='mongodb' will generate a pre-populated # configuration file with a base MongoDB setup desc 'generate', 'Generates configuration blocks based on the arguments you pass in' method_option :path, :type => :string method_option :databases, :type => :string method_option :storages, :type => :string method_option :syncers, :type => :string method_option :encryptors, :type => :string method_option :compressors, :type => :string method_option :notifiers, :type => :string method_option :archives, :type => :boolean def generate temp_file = Tempfile.new('backup.rb') temp_file << File.read( File.join(TEMPLATE_DIR, 'readme') ) temp_file << "Backup::Model.new(:my_backup, 'My Backup') do\n\n" if options[:archives] temp_file << File.read( File.join(TEMPLATE_DIR, 'archive') ) + "\n\n" end [:databases, :storages, :syncers, :encryptors, :compressors, :notifiers].each do |item| if options[item] options[item].split(',').map(&:strip).uniq.each do |entry| if File.exist?( File.join(TEMPLATE_DIR, item.to_s[0..-2], entry) ) temp_file << File.read( File.join(TEMPLATE_DIR, item.to_s[0..-2], entry) ) + "\n\n" end end end end temp_file << "end\n\n" temp_file.close path = options[:path] || Backup::PATH config = File.join(path, 'config.rb') if overwrite?(config) FileUtils.mkdir_p(path) File.open(config, 'w') do |file| file.write( File.read(temp_file.path) ) puts "Generated configuration file in '#{ config }'" end end temp_file.unlink end ## # [Decrypt] # Shorthand for decrypting encrypted files desc 'decrypt', 'Decrypts encrypted files' method_option :encryptor, :type => :string, :required => true method_option :in, :type => :string, :required => true method_option :out, :type => :string, :required => true method_option :base64, :type => :boolean, :default => false def decrypt case options[:encryptor].downcase when 'openssl' base64 = options[:base64] ? '-base64' : '' %x[openssl aes-256-cbc -d #{base64} -in '#{options[:in]}' -out '#{options[:out]}'] when 'gpg' %x[gpg -o '#{options[:out]}' -d '#{options[:in]}'] else puts "Unknown encryptor: #{options[:encryptor]}" puts "Use either 'openssl' or 'gpg'" end end ## # [Dependencies] # Returns a list of Backup's dependencies desc 'dependencies', 'Display the list of dependencies for Backup, or install them through Backup.' method_option :install, :type => :string method_option :list, :type => :boolean def dependencies unless options.any? puts puts "To display a list of available dependencies, run:\n\n" puts " backup dependencies --list" puts puts "To install one of these dependencies (with the correct version), run:\n\n" puts " backup dependencies --install " exit end if options[:list] Backup::Dependency.all.each do |name, gemspec| puts puts name puts "--------------------------------------------------" puts "version: #{gemspec[:version]}" puts "lib required: #{gemspec[:require]}" puts "used for: #{gemspec[:for]}" end end if options[:install] puts puts "Installing \"#{options[:install]}\" version \"#{Backup::Dependency.all[options[:install]][:version]}\".." puts "If this doesn't work, please issue the following command yourself:\n\n" puts " gem install #{options[:install]} -v '#{Backup::Dependency.all[options[:install]][:version]}'\n\n" puts "Please wait..\n\n" puts %x[gem install #{options[:install]} -v '#{Backup::Dependency.all[options[:install]][:version]}'] end end ## # [Version] # Returns the current version of the Backup gem map '-v' => :version desc 'version', 'Display installed Backup version' def version puts "Backup #{Backup::Version.current}" end private ## # Helper method for asking the user if he/she wants to overwrite the file def overwrite?(path) if File.exist?(path) return yes? "A configuration file already exists in #{ path }. Do you want to overwrite? [y/n]" end true end end ## # Enable the CLI for the Backup binary BackupCLI.start