From 75aec1cdfcb49f3e581af5d5fda43860afa17e97 Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Wed, 15 Dec 2010 23:38:00 +0200 Subject: [PATCH] First working version --- aejis_backup.gemspec | 29 +++++++++++ bin/backup | 28 ++++++++++ lib/aejis_backup.rb | 68 ++++++++++++++++++++----- lib/aejis_backup/adapter/abstract.rb | 31 +++-------- lib/aejis_backup/adapter/archive.rb | 53 ++++++++++++++++++- lib/aejis_backup/adapter/postgresql.rb | 9 ++-- lib/aejis_backup/backup.rb | 20 ++++++++ lib/aejis_backup/configuration.rb | 37 ++++++++------ lib/aejis_backup/helpers.rb | 34 +++++++++++++ lib/aejis_backup/store.rb | 1 + lib/aejis_backup/store/abstract.rb | 10 ++++ lib/aejis_backup/store/local.rb | 14 +++++ lib/aejis_backup/store/s3.rb | 43 +++++----------- spec/aejis_backup/configuration_spec.rb | 37 +++++++++----- 14 files changed, 315 insertions(+), 99 deletions(-) create mode 100644 aejis_backup.gemspec create mode 100755 bin/backup create mode 100644 lib/aejis_backup/backup.rb create mode 100644 lib/aejis_backup/helpers.rb create mode 100644 lib/aejis_backup/store/abstract.rb create mode 100644 lib/aejis_backup/store/local.rb diff --git a/aejis_backup.gemspec b/aejis_backup.gemspec new file mode 100644 index 0000000..cd61739 --- /dev/null +++ b/aejis_backup.gemspec @@ -0,0 +1,29 @@ +spec = Gem::Specification.new do |s| + s.name = "aejis_backup" + + s.author = "Andrey Savchenko" + s.email = "andrey@aejis.eu" + s.license = 'BSD' + + s.version = "0.1.0" + s.date = Time.now + + s.homepage = "http://github.com/Aejis/backup" + s.summary = "Swiss knife for unix backups" + s.description = "Gem for easy backup your database and files. Supported databases: Postgresql. Supported storages: Amazon S3, local disk" + + s.files = Dir.glob("{bin,lib,spec}/**/*") + ["README"] + + s.bindir = 'bin' + s.executables = ["backup"] + s.default_executable= 'backup' + + s.require_path = "lib" + + s.test_files = Dir.glob("spec/**/*") + + s.add_development_dependency "rspec", ">= 2.0.0" + s.add_development_dependency "fog", ">= 0.3.20" + + s.requirements = ["GNU or BSD tar"] +end \ No newline at end of file diff --git a/bin/backup b/bin/backup new file mode 100755 index 0000000..ff6a3c9 --- /dev/null +++ b/bin/backup @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby + +require 'optparse' +require 'rubygems' +require 'aejis_backup' + +options = {} +optparse = OptionParser.new do |opts| + opts.banner = "\nUsage: backup [options]\n " + + opts.on('-c', '--config [PATH]', "Path to backup configuration") do |config| + options[:config] = config + end + + opts.on('-b x,y,z', Array, "List of backups to run") do |list| + options[:list] = list + end + + opts.on_tail('-s', '--silent', 'Do not print messages to STDOUT') do + BACKUP_SILENT = true + end +end + +optparse.parse! + +AejisBackup.config_path = options[:config] +AejisBackup.load +AejisBackup.run!(options[:list]) \ No newline at end of file diff --git a/lib/aejis_backup.rb b/lib/aejis_backup.rb index c50251d..eb3491a 100644 --- a/lib/aejis_backup.rb +++ b/lib/aejis_backup.rb @@ -1,18 +1,62 @@ +require "fileutils" require "rubygems" +require "aejis_backup/helpers" +require "aejis_backup/configuration" module AejisBackup - # def config_setter(*names) - # names.each do |name| - # instance_eval %{ - # def #{name}(#{name}=nil) - # #{name} ? (@#{name} = #{name}) : @#{name} - # end - # alias :#{name}= :#{name} - # } - # end - # end + class < true) + say("Done!", :yellow) + end + end + end +end \ No newline at end of file diff --git a/lib/aejis_backup/adapter/abstract.rb b/lib/aejis_backup/adapter/abstract.rb index bf627d9..4a6bf73 100644 --- a/lib/aejis_backup/adapter/abstract.rb +++ b/lib/aejis_backup/adapter/abstract.rb @@ -1,38 +1,19 @@ module AejisBackup module Adapter class Abstract + include AejisBackup::Helpers + + def initialize + config_accessor :user, :password, :database + end # Set config in one line, useful with authomatic #rails method def set_config(conf) (@user, @password, @database = conf[:user], conf[:password], conf[:database]) or raise ArgumentError end -# config_setter :user, :password, :database - - # Set login for access - def user(user=nil) - user ? (@user = user) : @user - end - alias :user= :user - - # Set password for access - def password(password=nil) - password ? (@password = password) : @password - end - alias :password= :password - - # Set database name - def database(database=nil) - database ? (@database = database) : @database - end - alias :database= :database - - def backup_name - Time.now.to_i # TODO - add name - end - # Get data for backup - def get! + def get!(tmpfile) end end end diff --git a/lib/aejis_backup/adapter/archive.rb b/lib/aejis_backup/adapter/archive.rb index d27549c..9b14359 100644 --- a/lib/aejis_backup/adapter/archive.rb +++ b/lib/aejis_backup/adapter/archive.rb @@ -1,6 +1,57 @@ module AejisBackup module Adapter - class Archive < Abstract + class Archive + include AejisBackup::Helpers + + def initialize + config_accessor :path, :format + compression_level 6 + end + + def compression_level(level=false) + return @compression_level unless level + if level.is_a?(Symbol) or level.is_a?(String) + @compression_level = case level.to_sym + when :lowest + 1 + when :low + 3 + when :middle + 5 + when :high + 7 + when :highest + 9 + end + elsif level.is_a? Numeric + level = level.round + @compression_level = if level < 1 + 1 + elsif level > 9 + 9 + end + end + end + + def get!(tmpfile) + dir, target = File.dirname(path), File.basename(path) + run("tar -C #{dir} -c #{target} | #{archive(tmpfile)}", "Archiving #{path} with #{format}", :green) + end + + private + + def archive(tmpfile) + case format + when :bz2, :bzip, :bzip2 + "bzip2 -z -#{compression_level} > #{tmpfile}.tar.bz2" + when :gz + "gzip -#{compression_level} > #{tmpfile}.tar.gz" + when :xz + "xz -z -#{compression_level} > #{tmpfile}.tar.xz" + else + "bzip2 -z -#{compression_level} > #{tmpfile}.tar.bz2" + end + end end end end \ No newline at end of file diff --git a/lib/aejis_backup/adapter/postgresql.rb b/lib/aejis_backup/adapter/postgresql.rb index cca482f..3cfeac0 100644 --- a/lib/aejis_backup/adapter/postgresql.rb +++ b/lib/aejis_backup/adapter/postgresql.rb @@ -2,9 +2,9 @@ module AejisBackup module Adapter class Postgresql < Abstract - def get! + def get!(tmpfile) ENV['PGPASSWORD'] = password - run "pg_dump -U #{user} #{arguments} #{database}" + run("pg_dump -U #{user} --file=#{tmpfile}.sql #{arguments} #{database}", "Dump database #{database}", :green) ENV['PGPASSWORD'] = nil end @@ -12,9 +12,8 @@ def get! def arguments args = [] - args << "--file=#{backup_name}.sql" - args << "--format=t" # Compress dump - args << "--compress=8" # TODO - compression level + args << "--format=t" + # TODO - compression level args.join(' ') end end diff --git a/lib/aejis_backup/backup.rb b/lib/aejis_backup/backup.rb new file mode 100644 index 0000000..303a2dc --- /dev/null +++ b/lib/aejis_backup/backup.rb @@ -0,0 +1,20 @@ +module AejisBackup + class Backup + def initialize + @tmpdir = '/tmp' + end + + def sources(*names) + names.length == 0 ? @sources : (@sources = names) + end + alias :source :sources + + def target(name=false) + name ? (@target = name) : @target + end + + def tmpdir(path=false) + path ? (@tmpdir = path) : @tmpdir + end + end +end \ No newline at end of file diff --git a/lib/aejis_backup/configuration.rb b/lib/aejis_backup/configuration.rb index cdae9fa..5b31efb 100644 --- a/lib/aejis_backup/configuration.rb +++ b/lib/aejis_backup/configuration.rb @@ -1,15 +1,16 @@ require "aejis_backup/adapter" require "aejis_backup/store" +require "aejis_backup/backup" module AejisBackup class Configuration - def initialize(&block) + def initialize @sources = Hash.new @storages = Hash.new - instance_exec(&block) + @backups = Hash.new end - attr_reader :sources, :storages + attr_reader :sources, :storages, :backups def source(name, adapter, &block) if adapter.is_a?(Hash) @@ -22,15 +23,15 @@ def source(name, adapter, &block) required_adapter = case adapter.to_sym when :archive - AejisBackup::Adapter::Archive + Adapter::Archive when :mongo - AejisBackup::Adapter::Mongo + Adapter::Mongo when :mysql - AejisBackup::Adapter::Mysql + Adapter::Mysql when :pg, :postgres, :postgresql - AejisBackup::Adapter::Postgresql + Adapter::Postgresql when :sqlite, :sqlite3 - AejisBackup::Adapter::Sqlite + Adapter::Sqlite else raise "Adapter #{adapter.to_s} not known" end @@ -43,19 +44,19 @@ def storage(name, store, &block) raise "#{name.to_s.capitalize} storage not configured" unless block_given? required_store = case store.to_sym when :cloudfiles - AejisBackup::Store::CloudFiles + Store::CloudFiles when :dropbox - AejisBackup::Store::Dropbox + Store::Dropbox when :ftp - AejisBackup::Store::Ftp + Store::Ftp when :local - AejisBackup::Store::Local + Store::Local when :s3, :amazon - AejisBackup::Store::S3 + Store::S3 when :scp - AejisBackup::Store::Scp + Store::Scp when :sftp - AejisBackup::Store::Sftp + Store::Sftp else raise "Store #{adapter.to_s} not known" end @@ -64,6 +65,12 @@ def storage(name, store, &block) @storages[name] = store end + def backup(name, &block) + backup = Backup.new() + backup.instance_eval(&block) + @backups[name] = backup + end + def rails(environment=:production) raise "You are not inside rails or use rails 2.X" unless defined?(Rails) rails_config = ::Rails.configuration.database_configuration[environment.to_s] diff --git a/lib/aejis_backup/helpers.rb b/lib/aejis_backup/helpers.rb new file mode 100644 index 0000000..d718607 --- /dev/null +++ b/lib/aejis_backup/helpers.rb @@ -0,0 +1,34 @@ +#TODO - make log and alert helpers +module AejisBackup + module Helpers + def config_accessor(*names) + names.each do |name| + instance_eval %{ + def #{name}(#{name}=nil) + #{name} ? (@#{name} = #{name}) : @#{name} + end + alias :#{name}= :#{name} + } + end + end + + def say(message, color=:null) + colors = { + :null => "0", + :red => "31", + :green => "32", + :yellow => "33", + :purple => "35", + :cyan => "36" + } + unless defined?(::BACKUP_SILENT) + puts STDOUT.tty? ? "\e[#{colors[color.to_sym]}m#{message}\e[0m" : message + end + end + + def run(cmd, message=false, color=:null) + say(message, color) if message + `#{cmd}` + end + end +end \ No newline at end of file diff --git a/lib/aejis_backup/store.rb b/lib/aejis_backup/store.rb index 5fd24df..3d264aa 100644 --- a/lib/aejis_backup/store.rb +++ b/lib/aejis_backup/store.rb @@ -1,5 +1,6 @@ module AejisBackup module Store + autoload :Abstract, 'aejis_backup/store/abstract' autoload :CloudFiles, 'aejis_backup/store/cloud_files' autoload :Dropbox, 'aejis_backup/store/dropbox' autoload :Ftp, 'aejis_backup/store/ftp' diff --git a/lib/aejis_backup/store/abstract.rb b/lib/aejis_backup/store/abstract.rb new file mode 100644 index 0000000..5de1eb9 --- /dev/null +++ b/lib/aejis_backup/store/abstract.rb @@ -0,0 +1,10 @@ +module AejisBackup + module Store + class Abstract + include AejisBackup::Helpers + + def store!(file) + end + end + end +end \ No newline at end of file diff --git a/lib/aejis_backup/store/local.rb b/lib/aejis_backup/store/local.rb new file mode 100644 index 0000000..05ff6da --- /dev/null +++ b/lib/aejis_backup/store/local.rb @@ -0,0 +1,14 @@ +module AejisBackup + module Store + class Local < Abstract + def initialize + config_accessor :path + end + + def store!(file) + FileUtils.makedirs(path) unless File.exists?(path) + run("cp #{file} #{path}", "Copying backup to #{path}", :green) + end + end + end +end \ No newline at end of file diff --git a/lib/aejis_backup/store/s3.rb b/lib/aejis_backup/store/s3.rb index 137850f..0d659ae 100644 --- a/lib/aejis_backup/store/s3.rb +++ b/lib/aejis_backup/store/s3.rb @@ -2,43 +2,28 @@ module AejisBackup module Store - class S3 + class S3 < Abstract + def initialize + config_accessor :access_key_id, :secret_access_key, :host, :region, :bucket + end + def connection - @connection ||= Fog::AWS::Storage.new( + connection_config = { :aws_access_key_id => access_key_id, :aws_secret_access_key => secret_access_key, :host => host - ) + } + connection_config[:scheme] = use_ssl? ? 'https' : 'http' + @connection ||= Fog::AWS::Storage.new(connection_config) end - def store!(files) + def store!(file) + say("Upload backup to Amazon storage", :green) connection.put_bucket(bucket) - files.each do |file| - file_name = File.basename(file) - file_body = File.open(file) - connection.put_object(bucket, file_name, file_body) - end - end - - def access_key_id(access_key=false) - access_key ? (@access_key = access_key) : @access_key - end - alias :access_key_id= :access_key_id - - def secret_access_key(secret_access_key=false) - secret_access_key ? (@secret_access_key = secret_access_key) : @secret_access_key - end - alias :secret_access_key= :secret_access_key - - def host(host=false) - host ? (@host = host) : @host - end - alias :host= :host - - def bucket(bucket=false) - bucket ? (@bucket = host) : @bucket + file_name = File.basename(file) + file_body = File.open(file) + connection.put_object(bucket, file_name, file_body) end - alias :host= :host def use_ssl @ssl = true diff --git a/spec/aejis_backup/configuration_spec.rb b/spec/aejis_backup/configuration_spec.rb index 1045433..147f511 100644 --- a/spec/aejis_backup/configuration_spec.rb +++ b/spec/aejis_backup/configuration_spec.rb @@ -1,22 +1,14 @@ require 'spec_helper' describe AejisBackup::Configuration do - it "should set adapter" do - config = AejisBackup::Configuration.new do + + before :each do + AejisBackup.config do source('mydb', :postgresql) do user 'rails' password 'password' database 'test_production' end - end - source = config.sources['mydb'] - source.user.should eq('rails') - source.password.should eq('password') - source.database.should eq('test_production') - end - - it "should set storage" do - config = AejisBackup::Configuration.new do storage('ptico-s3', :s3) do access_key_id 'zzzaaa' secret_access_key 'secret_access_key' @@ -24,8 +16,29 @@ bucket '/bucket/backups/test/' use_ssl end + backup(:mybackup) do + source 'mydb' + target 'ptico-s3' + end end - storage = config.storages['ptico-s3'] + @config = AejisBackup.config + end + + it "should set adapter" do + source = @config.sources['mydb'] + source.user.should eq('rails') + source.password.should eq('password') + source.database.should eq('test_production') + end + + it "should set storage" do + storage = @config.storages['ptico-s3'] storage.access_key_id.should eq('zzzaaa') end + + it "should configure backups" do + backup = @config.backups[:mybackup] + backup.sources.should eq(['mydb']) + backup.target.should eq('ptico-s3') + end end \ No newline at end of file