encbs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "fog"
4
+ gem "slop"
5
+ gem "ruby-progressbar"
6
+
7
+ group :development do
8
+ gem "bundler", "~> 1.0.0"
9
+ gem "jeweler", "~> 1.6.0"
10
+ gem "rcov", ">= 0"
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Timothy Klim
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = encbs
2
+
3
+ Simple backup system for pushing into cloud.
4
+
5
+ == Contributing to encbs
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Timothy Klim. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "encbs"
18
+ gem.homepage = "http://github.com/TimothyKlim/encbs"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Simple backup system for pushing into cloud}
21
+ gem.description = %Q{Simple backup system for pushing into cloud}
22
+ gem.email = "klimtimothy@gmail.com"
23
+ gem.authors = ["Timothy Klim"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "encbs #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/encbs ADDED
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path("../../lib/", __FILE__))
3
+
4
+ require 'rubygems'
5
+ require 'yaml'
6
+ require 'digest'
7
+ require 'fileutils'
8
+ require 'openssl'
9
+ require 'socket'
10
+ require 'helpers'
11
+
12
+ safe_require do
13
+ require 'slop'
14
+ require 'fog'
15
+ require 'progressbar'
16
+ end
17
+
18
+ require 'backup'
19
+
20
+ opts = Slop.parse :help => true do
21
+ on :a, :add, "Add path to backup", true
22
+ on :b, :bucket, "Set Amazon S3 bucket to backup", true
23
+ on :k, :key, "Set API key to access Amazon S3", true
24
+ on :s, :secret, "Set API secret to access Amazon S3", true
25
+ on :c, :config, "Use config file to upload backup", true #TODO
26
+ on :colorize, "Colorize print to console"
27
+ on :d, :date, "Date for backup restore (default: last)", true
28
+ on :g, :generate, "Generate RSA keys (option: 4096, 2048)", true
29
+ on :h, :hostname, "Set hostname (default: system)", true
30
+ on :i, :increment, "Use increment mode for backup (default: false)"
31
+ on :j, :jar, "Versions of jar (option: hash or path)", true
32
+ on :t, :token, "RSA Key to encrypt/decrypt backup data", true
33
+ on :l, :local, "Backup in local directory", true
34
+ on :list, "List of jars"
35
+ on :r, :rescue, "Return data from backup (option: jar, path or filter)", true
36
+ on :t, :to, "Path to recovery (default: /)", true
37
+ on :v, :verbose, "Verbose mode"
38
+
39
+ banner "Usage:\n $ encbs [options]\n\nOptions:"
40
+ end
41
+
42
+ if ARGV.empty?
43
+ puts opts.help
44
+
45
+ exit
46
+ end
47
+
48
+ $PRINT_VERBOSE = opts.verbose?
49
+ $COLORIZE = opts.colorize?
50
+
51
+ #if opts.generate?
52
+ # puts "Generate 4096 bits RSA keys"
53
+ # Crypto::create_keys(
54
+ # File.join(Dir.getwd, "rsa_key"),
55
+ # File.join(Dir.getwd, "rsa_key.pub")
56
+ # )
57
+ # puts "Done!"
58
+ #
59
+ # exit
60
+ #end
61
+
62
+ if opts.local?
63
+ try_create_dir opts[:local]
64
+ @backup = Backup::Instance.new opts[:local]
65
+ else
66
+ [:key, :secret, :bucket].each do |arg|
67
+ puts_fail "Argument '--#{arg}' should not be empty" if opts[arg].nil?
68
+ end
69
+ @backup = Backup::Instance.new(
70
+ "backups",
71
+ true,
72
+ :bucket => opts[:bucket],
73
+ :key => opts[:key],
74
+ :secret => opts[:secret]
75
+ )
76
+ end
77
+
78
+ @backup.hostname = opts[:hostname] if opts.hostname?
79
+
80
+ if opts.list?
81
+ jars_list = @backup.jars
82
+
83
+ unless jars_list.empty?
84
+ puts "List of jars:\n"
85
+ jars_list.keys.sort.each do |key|
86
+ puts " #{key.dark_green}: #{jars_list[key]}"
87
+ end
88
+ else
89
+ puts "Nothing to listing."
90
+ end
91
+
92
+ exit
93
+ end
94
+
95
+ #TODO: AES or RSA
96
+ # @backup.key = opts[:key] if opts.key?
97
+
98
+ if opts.date?
99
+ date = opts[:date].split("-")
100
+
101
+ unless date.length == 1
102
+ @start_date = Backup::Timestamp.parse_timestamp date[0]
103
+ @end_date = Backup::Timestamp.parse_timestamp date[1], true
104
+
105
+ puts_fail "Last date less than start date" if start_date > end_date
106
+ else
107
+ @start_date = Backup::Timestamp.parse_timestamp date[0]
108
+ @end_date = Backup::Timestamp.parse_timestamp date[0], true
109
+ end
110
+ else
111
+ @start_date = nil
112
+ @end_date = Time.now.utc
113
+ end
114
+
115
+ if opts.jar?
116
+ opts[:jar].split(" ").each do |jar|
117
+ versions = @backup.jar_versions(jar)
118
+
119
+ unless versions.empty?
120
+ puts "Versions of backup '#{jar}':"
121
+
122
+ versions.each do |version|
123
+ puts " => #{version.dark_green}: #{Backup::Timestamp.to_str(version)}"
124
+ end
125
+ else
126
+ puts "Versions doesn't exists for jar: #{jar}"
127
+ end
128
+ end
129
+
130
+ exit
131
+ end
132
+
133
+ #TODO: Support rescue option as hash
134
+ if opts.rescue?
135
+ paths = opts[:rescue].split(" ")
136
+ jars_list = @backup.jars
137
+
138
+ include_path = lambda {|path| jars_list.keys.include? path}
139
+
140
+ jars_hashes = paths.map do |path|
141
+ path = File.expand_path path
142
+
143
+ unless include_path[path] or include_path["#{path}/"]
144
+ puts_fail "Jar \"#{path}\" not exists."
145
+ end
146
+
147
+ jars_list[path] || jars_list["#{path}/"]
148
+ end
149
+
150
+ if opts.to?
151
+ @to = File.expand_path opts[:to]
152
+ try_create_dir @to
153
+ else
154
+ @to = "/"
155
+ end
156
+
157
+ #TODO: Confirm flag
158
+ #TODO: Empty destination directory
159
+
160
+ @index = {}
161
+
162
+ jars_hashes.each do |hash|
163
+ versions = @backup.jar_versions(hash)
164
+ # puts "Versions: #{versions}" #FIXME
165
+
166
+ last_version = Backup::Timestamp.last_from(versions, @end_date, @start_date)
167
+
168
+ unless last_version.nil?
169
+ @index[hash] = last_version
170
+ else
171
+ error_path = "#{Backup::Jar.hash_to_path(@backup.root_path, hash)}"
172
+ start_date = Backup::Timestamp.to_s(@start_date)
173
+ end_date = Backup::Timestamp.to_s(@end_date)
174
+
175
+ unless @end_date == @start_date
176
+ puts_fail "Nothing found for #{error_path}, between date: #{start_date} - #{end_date}"
177
+ else
178
+ puts_fail "Nothing found for #{error_path}, for date: #{end_date}"
179
+ end
180
+ end
181
+ end
182
+
183
+ @index.each do |hash, timestamp|
184
+ # puts "#{hash}: #{timestamp}" #FIXME
185
+ @backup.restore_jar_to(hash, timestamp, @to)
186
+ end
187
+
188
+ puts "Done!".green
189
+ exit
190
+ end
191
+
192
+ if opts.add?
193
+ paths = opts[:add].split(" ")
194
+
195
+ paths = paths.map do |path|
196
+ path = File.expand_path path
197
+ puts_fail "Path \"#{path}\" not exists." unless File.exists? path
198
+
199
+ path
200
+ end
201
+
202
+ paths.each do |path|
203
+ @backup.create! path, opts.increment?
204
+ end
205
+
206
+ puts "Done!".green
207
+ end
data/encbs.gemspec ADDED
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{encbs}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Timothy Klim"]
12
+ s.date = %q{2011-05-11}
13
+ s.default_executable = %q{encbs}
14
+ s.description = %q{Simple backup system for pushing into cloud}
15
+ s.email = %q{klimtimothy@gmail.com}
16
+ s.executables = ["encbs"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "bin/encbs",
28
+ "encbs.gemspec",
29
+ "lib/backup.rb",
30
+ "lib/backup/file_item.rb",
31
+ "lib/backup/file_item/base.rb",
32
+ "lib/backup/file_item/cloud.rb",
33
+ "lib/backup/file_item/local.rb",
34
+ "lib/backup/jar.rb",
35
+ "lib/backup/timestamp.rb",
36
+ "lib/crypto.rb",
37
+ "lib/helpers.rb",
38
+ "test/helper.rb",
39
+ "test/test_backup.rb",
40
+ "test/test_backup_file_item.rb",
41
+ "test/test_backup_timestamp.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/TimothyKlim/encbs}
44
+ s.licenses = ["MIT"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.6.2}
47
+ s.summary = %q{Simple backup system for pushing into cloud}
48
+
49
+ if s.respond_to? :specification_version then
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<fog>, [">= 0"])
54
+ s.add_runtime_dependency(%q<slop>, [">= 0"])
55
+ s.add_runtime_dependency(%q<ruby-progressbar>, [">= 0"])
56
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
57
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.0"])
58
+ s.add_development_dependency(%q<rcov>, [">= 0"])
59
+ else
60
+ s.add_dependency(%q<fog>, [">= 0"])
61
+ s.add_dependency(%q<slop>, [">= 0"])
62
+ s.add_dependency(%q<ruby-progressbar>, [">= 0"])
63
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
64
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
65
+ s.add_dependency(%q<rcov>, [">= 0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<fog>, [">= 0"])
69
+ s.add_dependency(%q<slop>, [">= 0"])
70
+ s.add_dependency(%q<ruby-progressbar>, [">= 0"])
71
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
72
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
73
+ s.add_dependency(%q<rcov>, [">= 0"])
74
+ end
75
+ end
76
+
@@ -0,0 +1,37 @@
1
+ module Backup
2
+ module FileItem
3
+ class Base
4
+ def semantic_path(path)
5
+ if Dir.exists? path
6
+ path += '/'
7
+ else
8
+ path
9
+ end
10
+ end
11
+
12
+ def stat(file, timestamp = nil)
13
+ files = {}
14
+
15
+ stat = File.new(file).stat
16
+ files[file] = {
17
+ :uid => stat.uid,
18
+ :gid => stat.gid,
19
+ :mode => stat.mode
20
+ }
21
+ files[file][:timestamp] = timestamp if timestamp
22
+
23
+ unless Dir.exists?(file)
24
+ files[file][:checksum] = Digest::MD5.hexdigest(File.open(file).read)
25
+ end
26
+
27
+ files
28
+ rescue Exception => e
29
+ STDERR.puts e
30
+ end
31
+
32
+ def file_hash(file)
33
+ Digest::MD5.hexdigest file
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ require 'backup/file_item/base'
2
+
3
+ module Backup
4
+ module FileItem
5
+ class Cloud < Backup::FileItem::Base
6
+ attr_reader :key, :secret, :backet, :provider
7
+
8
+ def initialize(args = {})
9
+ puts_fail "Empty hash in Cloud initialize method" if args.empty?
10
+
11
+ [:key, :secret, :bucket].each do |arg|
12
+ puts_fail "'#{arg.to_s.green}' should not be empty" if args[arg].nil?
13
+ instance_eval %{@#{arg} = args[:#{arg}]}
14
+ end
15
+
16
+ try_to_connect_with_cloud
17
+ end
18
+
19
+ def create_directory_once(*directories)
20
+ # Nothing happen
21
+ end
22
+
23
+ def create_file_once(file, data)
24
+ try_to_work_with_cloud do
25
+ @directory.files.create(
26
+ :key => delete_slashes(file),
27
+ :body => data
28
+ )
29
+ end
30
+ end
31
+
32
+ def read_file(file)
33
+ try_to_work_with_cloud do
34
+ file = delete_slashes(file)
35
+ remote_file = @directory.files.get(file)
36
+ remote_file.body if remote_file
37
+ end
38
+ end
39
+
40
+ def dir(path, mask = "*")
41
+ path = delete_slashes(path)
42
+ mask = mask.gsub('.', '\.').gsub('*', '[^\/]')
43
+
44
+ files = @directory.files.all(
45
+ :prefix => path,
46
+ :max_keys => 30_000
47
+ ).map &:key
48
+
49
+ files.map do |item|
50
+ match = item.match(/^#{path}\/([^\/]+#{mask}).*$/)
51
+ match[1] if match
52
+ end.compact.uniq
53
+ end
54
+
55
+ private
56
+
57
+ def delete_slashes(str)
58
+ str.chop! if str =~ /\/$/
59
+ str = str[1, str.length] if str =~ /^\//
60
+ str
61
+ end
62
+
63
+ def try_to_work_with_cloud(&block)
64
+ begin
65
+ yield
66
+ rescue Exception => e
67
+ try_to_connect_with_cloud
68
+
69
+ yield
70
+ end
71
+ end
72
+
73
+ def try_to_connect_with_cloud
74
+ begin
75
+ @connection = ::Fog::Storage.new(
76
+ :provider => 'AWS',
77
+ :aws_secret_access_key => @secret,
78
+ :aws_access_key_id => @key
79
+ )
80
+
81
+ @directory = @connection.directories.get(@bucket)
82
+ rescue Exception => e
83
+ puts_verbose e.message
84
+ puts_fail "403 Forbidden"
85
+ end
86
+
87
+ puts_fail "Bucket '#{@bucket}' is not exists." if @directory.nil?
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,26 @@
1
+ require 'backup/file_item/base'
2
+
3
+ module Backup
4
+ module FileItem
5
+ class Local < Backup::FileItem::Base
6
+ def create_directory_once(*directories)
7
+ directories.each do |path|
8
+ FileUtils.mkdir_p(path) unless Dir.exists?(path)
9
+ end
10
+ end
11
+
12
+ def create_file_once(file, data)
13
+ date = date.read if date.is_a? File
14
+ File.open(file, "w").puts(data) unless File.exists?(file)
15
+ end
16
+
17
+ def read_file(file)
18
+ open(file).read if File.exists? file
19
+ end
20
+
21
+ def dir(path, mask = "*")
22
+ Dir["#{path}/#{mask}"]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ require 'backup/file_item/local'
2
+ require 'backup/file_item/cloud'
3
+
4
+ module Backup
5
+ module FileItem
6
+ def self.for(type, *args)
7
+ case type
8
+ when :cloud
9
+ Backup::FileItem::Cloud.new *args
10
+ when :local
11
+ Backup::FileItem::Local.new
12
+
13
+ else
14
+ puts_fail "Unknown '#{type}' type for FileItem"
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/backup/jar.rb ADDED
@@ -0,0 +1,163 @@
1
+ module Backup
2
+ class Jar
3
+ def initialize(file_item, root_path, local_path)
4
+ @root_path = root_path
5
+ @local_path = local_path
6
+ @timestamp = Backup::Timestamp.create
7
+ @file_item = file_item
8
+ end
9
+
10
+ def jar_hash
11
+ Digest::MD5.hexdigest(@local_path)
12
+ end
13
+
14
+ def save(increment = false)
15
+ unless increment
16
+ @local_files = hash_local_files
17
+ else
18
+ @local_files = {}
19
+ current_files = hash_local_files
20
+
21
+ last_timestamp = Jar.jar_versions(@root_path, jar_hash, true).last
22
+
23
+ if last_timestamp.nil?
24
+ puts_fail "First you must create a full backup for #{@local_path.dark_green}"
25
+ end
26
+
27
+ last_index = Jar.fetch_index_for(@root_path, jar_hash, last_timestamp)
28
+
29
+ current_files.keys.each do |file|
30
+ @local_files[file] = current_files[file]
31
+
32
+ #TODO: Cut to a new method {
33
+ current = current_files[file].dup
34
+ current.delete(:timestamp)
35
+
36
+ unless last_index[file].nil?
37
+ backup = last_index[file].dup
38
+ backup.delete(:timestamp)
39
+
40
+ if (current == backup) or
41
+ (!current[:checksum].nil? and current[:checksum] == backup[:checksum])
42
+
43
+ @local_files[file][:timestamp] = last_index[file][:timestamp]
44
+ end
45
+ end
46
+ # }
47
+ end
48
+ end
49
+
50
+ @file_item.create_directory_once meta_jars_path, meta_jar_path, jar_data_path
51
+ @file_item.create_file_once(
52
+ "#{meta_jars_path}/#{jar_hash}",
53
+ @file_item.semantic_path(@local_path)
54
+ )
55
+ @file_item.create_file_once(
56
+ "#{meta_jar_path}/#{@timestamp}.yml",
57
+ @local_files.to_yaml
58
+ )
59
+
60
+ if @file_item.is_a? Backup::FileItem::Cloud
61
+ pbar = ProgressBar.new(
62
+ "Uploading",
63
+ @local_files.keys.count
64
+ )
65
+ else
66
+ pbar = ProgressBar.new(
67
+ "Copying",
68
+ @local_files.keys.count
69
+ )
70
+ end
71
+
72
+ pbar.bar_mark = '*'
73
+
74
+ @local_files.keys.each do |file|
75
+ unless Dir.exists?(file)
76
+ @file_item.create_file_once "#{jar_data_path}/#{@file_item.file_hash file}",
77
+ File.open(file)
78
+ pbar.inc
79
+ end
80
+ end
81
+
82
+ pbar.finish
83
+ end
84
+
85
+ def hash_local_files
86
+ files = {}
87
+
88
+ puts_verbose "Create index for #{@local_path.dark_green}"
89
+
90
+ if Dir.exists? @local_path
91
+ matches = Dir.glob(File.join(@local_path, "/**/*"), File::FNM_DOTMATCH)
92
+
93
+ matches = matches.select do |match|
94
+ match[/\/..$/].nil? and match[/\/.$/].nil?
95
+ end
96
+
97
+ matches << @local_path
98
+
99
+ matches.each do |match|
100
+ files.merge!(@file_item.stat(match, @timestamp))
101
+ end
102
+ else
103
+ files = @file_item.stat(@local_path, @timestamp)
104
+ end
105
+
106
+ files
107
+ end
108
+
109
+ class << self
110
+ def hash_to_path(file_item, root_path, hash)
111
+ file_item.read_file("#{root_path}/meta/jars/#{hash}").chomp
112
+ rescue Errno::ENOENT
113
+ ""
114
+ end
115
+
116
+ def all(file_item, root_path)
117
+ hashes = file_item.dir("#{root_path}/meta/jars").map do |backup|
118
+ backup[/[0-9a-z]{32}$/]
119
+ end.compact.sort
120
+
121
+ result = {}
122
+
123
+ hashes.each do |hash|
124
+ jar_local_path = Jar.hash_to_path(file_item, root_path, hash)
125
+ result[jar_local_path] = hash unless jar_local_path.empty?
126
+ end
127
+
128
+ result
129
+ end
130
+
131
+ def jar_versions(file_item, root_path, jar, hash = false)
132
+ jar = jar.chop if jar =~ /\/$/
133
+ jar = Digest::MD5.hexdigest(jar) unless hash
134
+
135
+ meta_jar_path = "#{root_path}/meta/#{jar}"
136
+
137
+ file_item.dir(meta_jar_path, "*.yml").map do |file|
138
+ match = file.match(/^\/?([0-9]{12}).yml$/)
139
+ match[1] if match
140
+ end.compact.sort
141
+ end
142
+
143
+ def fetch_index_for(file_item, root_path, hash, timestamp)
144
+ index = file_item.read_file "#{root_path}/meta/#{hash}/#{timestamp}.yml"
145
+ YAML::load(index) unless index.nil?
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def meta_jars_path
152
+ "#{@root_path}/meta/jars"
153
+ end
154
+
155
+ def meta_jar_path
156
+ "#{@root_path}/meta/#{jar_hash}"
157
+ end
158
+
159
+ def jar_data_path
160
+ "#{@root_path}/#{jar_hash}/#{@timestamp}"
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,48 @@
1
+ module Backup
2
+ class Timestamp
3
+ def self.parse_timestamp(version, last = false)
4
+ version = version.gsub(".", "").gsub(" ", "").gsub(":", "")
5
+
6
+ puts_fail "Invalid date format: #{version}" unless version.match /[0-9]{6,}/
7
+
8
+ year, month, day, hour, min, sec =
9
+ version.split(/([0-9]{2})/).map do |date|
10
+ date.to_i unless date.empty?
11
+ end.compact
12
+
13
+ if last
14
+ hour = 23 if hour.nil?
15
+ min = 59 if min.nil?
16
+ sec = 59 if sec.nil?
17
+ end
18
+
19
+ time = Time.new(year + 2000, month, day, hour, min, sec, 0)
20
+ end
21
+
22
+ def self.last_from(list, end_date, start_date = nil)
23
+ list.sort.reverse.find do |version|
24
+ version = Backup::Timestamp.parse_timestamp version
25
+
26
+ unless start_date.nil?
27
+ version >= start_date and version <= end_date
28
+ else
29
+ version <= end_date
30
+ end
31
+ end
32
+ end
33
+
34
+ def self.create(time = nil)
35
+ time = time.nil? ? Time.now : time
36
+
37
+ time.utc.strftime "%y%m%d%H%M%S"
38
+ end
39
+
40
+ def self.to_str(version)
41
+ to_s parse_timestamp(version)
42
+ end
43
+
44
+ def self.to_s(time)
45
+ time.strftime "%y.%m.%d %H:%M:%S" if time.is_a? Time
46
+ end
47
+ end
48
+ end
data/lib/backup.rb ADDED
@@ -0,0 +1,120 @@
1
+ require 'backup/file_item'
2
+ require 'backup/timestamp'
3
+ require 'backup/jar'
4
+ require 'crypto'
5
+
6
+ module Backup
7
+ class Instance
8
+ attr_reader :root_path, :timestamp, :hostname, :file_item
9
+
10
+ def initialize(root_path, cloud = false, *args)
11
+ if cloud
12
+ @file_item = Backup::FileItem.for :cloud, *args
13
+ else
14
+ @file_item = Backup::FileItem.for :local
15
+ end
16
+
17
+ @hostname = Socket.gethostname
18
+ @root_path = "#{root_path}/#{@hostname}"
19
+ @timestamp = Backup::Timestamp.create
20
+ end
21
+
22
+ def hostname=(host)
23
+ @hostname = host
24
+ @root_path = "#{root_path}/#{@hostname}"
25
+ end
26
+
27
+ def key=(path)
28
+ @key = open(path).read
29
+ end
30
+
31
+ def create!(local_path, increment = false)
32
+ jar = Jar.new(@file_item, @root_path, local_path)
33
+ jar.save(increment)
34
+ end
35
+
36
+ def jars
37
+ Jar.all(@file_item, @root_path)
38
+ end
39
+
40
+ def jar_versions(jar)
41
+ Jar.jar_versions(@file_item, @root_path, jar, !!jar[/^[0-9a-z]{32}$/])
42
+ end
43
+
44
+ def restore_jar_to(hash, timestamp, to)
45
+ files = Jar.fetch_index_for(@file_item, @root_path, hash, timestamp)
46
+
47
+ files.keys.sort.each do |file|
48
+ restore_file = File.join(to, file)
49
+ current_file = files[file]
50
+
51
+ if current_file[:checksum].nil?
52
+ try_create_dir restore_file
53
+
54
+ File.chmod current_file[:mode], restore_file
55
+ File.chown current_file[:uid], current_file[:gid], restore_file
56
+
57
+ file_ok = @file_item.stat(restore_file)[restore_file]
58
+
59
+ check_mode(restore_file, file_ok[:mode], current_file[:mode])
60
+ check_rights(
61
+ restore_file,
62
+ file_ok[:uid],
63
+ file_ok[:gid],
64
+ current_file[:uid],
65
+ current_file[:gid]
66
+ )
67
+ else
68
+ try_create_dir(File.dirname restore_file)
69
+
70
+ begin
71
+ File.open(restore_file, "w") do |f|
72
+ f.chmod current_file[:mode]
73
+ f.chown current_file[:uid], current_file[:gid]
74
+
75
+ remote_path = "#{@root_path}/#{hash}/#{current_file[:timestamp]}"
76
+ remote_path += "/#{@file_item.file_hash file}"
77
+
78
+ data = @file_item.read_file remote_path
79
+ f.puts data
80
+ end
81
+
82
+ file_ok = @file_item.stat(restore_file)[restore_file]
83
+
84
+ check_mode(restore_file, file_ok[:mode], current_file[:mode])
85
+ check_rights(
86
+ restore_file,
87
+ file_ok[:uid],
88
+ file_ok[:gid],
89
+ current_file[:uid],
90
+ current_file[:gid]
91
+ )
92
+ rescue Errno::EACCES
93
+ puts_fail "Permission denied for #{restore_file.dark_green}"
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+
101
+ def self.fetch_versions_of_backup(path)
102
+ Dir["#{path}/*"].map do |backup|
103
+ backup.match(/[0-9]{12}$/)[0] if backup.match(/[0-9]{12}$/)
104
+ end.compact.sort
105
+ end
106
+
107
+ def self.aes(command, key, data)
108
+ aes = OpenSSL::Cipher::Cipher.new('aes-256-cbc').send(command)
109
+ aes.key = key
110
+ aes.update(data) << aes.final
111
+ end
112
+
113
+ def self.encrypt_data(key, data)
114
+ Backup::aes(:encrypt, key, data) unless data.empty?
115
+ end
116
+
117
+ def self.decrypt_data(key, data)
118
+ Backup::aes(:decrypt, key, data)
119
+ end
120
+ end
data/lib/crypto.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Crypto
5
+
6
+ def self.create_keys(priv = "rsa_key", pub = "#{priv}.pub", bits = 4096)
7
+ private_key = OpenSSL::PKey::RSA.new(bits)
8
+ File.open(priv, "w+") { |fp| fp << private_key.to_s }
9
+ File.open(pub, "w+") { |fp| fp << private_key.public_key.to_s }
10
+ private_key
11
+ end
12
+
13
+ class Key
14
+ def initialize(data)
15
+ @public = (data =~ /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/).nil?
16
+ @key = OpenSSL::PKey::RSA.new(data)
17
+ end
18
+
19
+ def self.from_file(filename)
20
+ self.new File.read( filename )
21
+ end
22
+
23
+ def encrypt(text)
24
+ @key.send("#{key_type}_encrypt", text)
25
+ end
26
+
27
+ def decrypt(text)
28
+ @key.send("#{key_type}_decrypt", text)
29
+ end
30
+
31
+ def private?
32
+ !@public
33
+ end
34
+
35
+ def public?
36
+ @public
37
+ end
38
+
39
+ def key_type
40
+ @public ? :public : :private
41
+ end
42
+ end
43
+ end
data/lib/helpers.rb ADDED
@@ -0,0 +1,80 @@
1
+ def puts_fail(msg)
2
+ STDERR.puts "#{"Error: ".red}#{msg}"
3
+
4
+ exit msg.length
5
+ end
6
+
7
+ def puts_verbose(msg)
8
+ puts msg if $PRINT_VERBOSE
9
+ end
10
+
11
+ def print_verbose(msg)
12
+ print msg if $PRINT_VERBOSE
13
+ end
14
+
15
+ def safe_require(&block)
16
+ yield
17
+ rescue Exception => e
18
+ puts_fail %Q{This script use these gems: fog, slop.
19
+ Make sure that you have them all.
20
+ If you don't have, you may install them: $ gem install fog slop ruby-progressbar
21
+ }
22
+ end
23
+
24
+ def try_create_dir(dir)
25
+ begin
26
+ FileUtils.mkdir_p dir unless Dir.exists? dir
27
+ rescue Errno::EACCES
28
+ puts_fail "Permission denied for #{dir.dark_green}"
29
+ end
30
+ end
31
+
32
+ def check_mode(file, first, second)
33
+ unless first == second
34
+ puts_fail "Permission wasn't changed for #{file.dark_green}"
35
+ end
36
+ end
37
+
38
+ def check_rights(file, first_uid, first_gid, second_uid, second_gid)
39
+ unless first_uid == second_uid and first_gid == second_gid
40
+ puts_fail "Group and user wasn't change for #{file.dark_green}"
41
+ end
42
+ end
43
+
44
+ class String
45
+ def red
46
+ colorize(self, "\e[1m\e[31m")
47
+ end
48
+
49
+ def green
50
+ colorize(self, "\e[1m\e[32m")
51
+ end
52
+
53
+ def dark_green
54
+ colorize(self, "\e[32m")
55
+ end
56
+
57
+ def yellow
58
+ colorize(self, "\e[1m\e[33m")
59
+ end
60
+
61
+ def blue
62
+ colorize(self, "\e[1m\e[34m")
63
+ end
64
+
65
+ def dark_blue
66
+ colorize(self, "\e[34m")
67
+ end
68
+
69
+ def pur
70
+ colorize(self, "\e[1m\e[35m")
71
+ end
72
+
73
+ def colorize(text, color_code)
74
+ if $COLORIZE
75
+ "#{color_code}#{text}\e[0m"
76
+ else
77
+ text
78
+ end
79
+ end
80
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift(File.expand_path("../../lib/", __FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'yaml'
5
+ require 'digest'
6
+ require 'fileutils'
7
+ require 'openssl'
8
+ require 'socket'
9
+ require 'helpers'
10
+ require 'progressbar'
11
+ require 'test/unit'
12
+
13
+ require 'backup'
14
+
15
+ def puts_fail msg
16
+ raise msg
17
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ class TestBackup < Test::Unit::TestCase
4
+ def setup
5
+ @backup = Backup::Instance.new(
6
+ File.expand_path("../fixtures/backups", __FILE__),
7
+ false
8
+ )
9
+ end
10
+
11
+ def test_backup_attributes
12
+
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ require File.expand_path('../helper', __FILE__)
2
+
3
+ class BackupFileItemTest < Test::Unit::TestCase
4
+ def setup
5
+ @file_item = Backup::FileItem.for :local
6
+ end
7
+
8
+ def test_semantic_path
9
+ assert_equal __FILE__, @file_item.semantic_path(__FILE__)
10
+ assert_equal File.dirname(__FILE__) + '/',
11
+ @file_item.semantic_path(File.dirname(__FILE__))
12
+ end
13
+
14
+ def test_file_stat
15
+ file = @file_item.stat(
16
+ __FILE__,
17
+ Backup::Timestamp.create
18
+ )
19
+ key = file.keys.first
20
+
21
+ assert_not_nil file[key][:uid]
22
+ assert_not_nil file[key][:gid]
23
+ assert_not_nil file[key][:mode]
24
+ assert_not_nil file[key][:checksum]
25
+ assert_not_nil file[key][:timestamp]
26
+ end
27
+
28
+ def test_directory_stat
29
+ file = @file_item.stat(
30
+ File.dirname(__FILE__),
31
+ Backup::Timestamp.create
32
+ )
33
+ key = file.keys.first
34
+
35
+ assert_not_nil file[key][:uid]
36
+ assert_not_nil file[key][:gid]
37
+ assert_not_nil file[key][:mode]
38
+ assert_nil file[key][:checksum]
39
+ assert_not_nil file[key][:timestamp]
40
+ end
41
+ end
@@ -0,0 +1,73 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ class BackupTimestampTest < Test::Unit::TestCase
4
+ def test_parse_timestamp
5
+ assert_equal Backup::Timestamp.parse_timestamp("110102201130"),
6
+ Time.new(2011, 01, 02, 20, 11, 30, 0)
7
+
8
+ assert_equal Backup::Timestamp.parse_timestamp("11.01.02. 20:11:30"),
9
+ Time.new(2011, 01, 02, 20, 11, 30, 0)
10
+
11
+ assert_equal Backup::Timestamp.parse_timestamp("1101022011"),
12
+ Time.new(2011, 01, 02, 20, 11, 0, 0)
13
+ assert_equal Backup::Timestamp.parse_timestamp("11010220"),
14
+ Time.new(2011, 01, 02, 20, 0, 0, 0)
15
+ assert_equal Backup::Timestamp.parse_timestamp("110102"),
16
+ Time.new(2011, 01, 02, 0, 0, 0, 0)
17
+
18
+ assert_equal Backup::Timestamp.parse_timestamp("1101022011", true),
19
+ Time.new(2011, 01, 02, 20, 11, 59, 0)
20
+ assert_equal Backup::Timestamp.parse_timestamp("11010220", true),
21
+ Time.new(2011, 01, 02, 20, 59, 59, 0)
22
+ assert_equal Backup::Timestamp.parse_timestamp("110102", true),
23
+ Time.new(2011, 01, 02, 23, 59, 59, 0)
24
+
25
+ assert_not_equal Backup::Timestamp.parse_timestamp("110102201130"),
26
+ Time.new(2011, 01, 02, 20, 11, 31, 0)
27
+
28
+ assert_raise(RuntimeError) { Backup::Timestamp.parse_timestamp("11d10.,130") }
29
+ end
30
+
31
+ def test_last_from
32
+ timestamps = ["110130090812", "110130103412", "110106121234"]
33
+
34
+ assert_equal("110130103412",
35
+ Backup::Timestamp.last_from(timestamps,
36
+ Backup::Timestamp.parse_timestamp("110130", true)))
37
+ assert_equal("110130090812",
38
+ Backup::Timestamp.last_from(timestamps,
39
+ Backup::Timestamp.parse_timestamp("11013009", true)))
40
+ assert_equal("110106121234",
41
+ Backup::Timestamp.last_from(timestamps,
42
+ Backup::Timestamp.parse_timestamp("110106", true)))
43
+
44
+ assert_equal("110130103412",
45
+ Backup::Timestamp.last_from(timestamps,
46
+ Backup::Timestamp.parse_timestamp("110130", true),
47
+ Backup::Timestamp.parse_timestamp("110130")))
48
+ assert_equal("110106121234",
49
+ Backup::Timestamp.last_from(timestamps,
50
+ Backup::Timestamp.parse_timestamp("110110", true),
51
+ Backup::Timestamp.parse_timestamp("110101")))
52
+ assert_equal("110130090812",
53
+ Backup::Timestamp.last_from(timestamps,
54
+ Backup::Timestamp.parse_timestamp("1101300908", true),
55
+ Backup::Timestamp.parse_timestamp("110130090811")))
56
+
57
+ end
58
+
59
+ def test_create_timestamp
60
+ time = Time.new(2011, 01, 02, 23, 59, 59, 0)
61
+
62
+ assert_equal(Backup::Timestamp.create.length, 12)
63
+ assert_equal(Backup::Timestamp.create(time), "110102235959")
64
+ end
65
+
66
+ def test_formatted_timestamp
67
+ time = Time.new(2011, 01, 02, 23, 59, 30, 0)
68
+
69
+ assert_equal Backup::Timestamp.to_s(time), "11.01.02 23:59:30"
70
+ assert_equal Backup::Timestamp.to_s(51), nil
71
+ assert_equal Backup::Timestamp.to_str("110102235930"), "11.01.02 23:59:30"
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: encbs
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Timothy Klim
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-11 00:00:00 +04:00
14
+ default_executable: encbs
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: fog
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: ruby-progressbar
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: bundler
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: 1.0.0
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: jeweler
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: 1.6.0
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: rcov
73
+ requirement: &id006 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: *id006
82
+ description: Simple backup system for pushing into cloud
83
+ email: klimtimothy@gmail.com
84
+ executables:
85
+ - encbs
86
+ extensions: []
87
+
88
+ extra_rdoc_files:
89
+ - LICENSE.txt
90
+ - README.rdoc
91
+ files:
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.rdoc
95
+ - Rakefile
96
+ - VERSION
97
+ - bin/encbs
98
+ - encbs.gemspec
99
+ - lib/backup.rb
100
+ - lib/backup/file_item.rb
101
+ - lib/backup/file_item/base.rb
102
+ - lib/backup/file_item/cloud.rb
103
+ - lib/backup/file_item/local.rb
104
+ - lib/backup/jar.rb
105
+ - lib/backup/timestamp.rb
106
+ - lib/crypto.rb
107
+ - lib/helpers.rb
108
+ - test/helper.rb
109
+ - test/test_backup.rb
110
+ - test/test_backup_file_item.rb
111
+ - test/test_backup_timestamp.rb
112
+ has_rdoc: true
113
+ homepage: http://github.com/TimothyKlim/encbs
114
+ licenses:
115
+ - MIT
116
+ post_install_message:
117
+ rdoc_options: []
118
+
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 4533740644498179754
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: "0"
136
+ requirements: []
137
+
138
+ rubyforge_project:
139
+ rubygems_version: 1.6.2
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Simple backup system for pushing into cloud
143
+ test_files: []
144
+