ruby_clone 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,94 @@
1
+ require 'pty'
2
+
3
+ module RubyClone
4
+
5
+ class RSync
6
+
7
+ attr_writer :dry_run
8
+
9
+ def initialize(output)
10
+ @configurations = { options: '-Cavh --stats --progress', show_command: true, show_output: true }
11
+ @exclude_patterns = []
12
+ @include_patterns = []
13
+ @output = output
14
+ @profiles = {}
15
+ end
16
+
17
+ def profiles=(profile)
18
+ @profiles[profile.name] = profile
19
+ @last_profile = profile
20
+ end
21
+
22
+ def last_profile
23
+ @last_profile
24
+ end
25
+
26
+ def update_configurations(configurations)
27
+ @configurations.merge! configurations
28
+ end
29
+
30
+ def exclude_pattern=(path)
31
+ @exclude_patterns << path
32
+ end
33
+
34
+ def include_pattern=(path)
35
+ @include_patterns << path
36
+ end
37
+
38
+ def rsync_command(profile_name)
39
+ profile = @profiles[profile_name.to_s]
40
+
41
+ if profile
42
+ raise SyntaxError, "Empty Profile not allowed for profile #{profile} with no 'from folder'" unless profile.from_folder
43
+ raise SyntaxError, "Empty Profile not allowed for profile #{profile} with no 'to folder'" unless profile.to_folder
44
+
45
+ create_rsync_command profile
46
+ else
47
+ raise ArgumentError, "Profile #{profile_name} not found"
48
+ end
49
+ end
50
+
51
+ def run(profile_name)
52
+ command = rsync_command(profile_name)
53
+ @output.puts "\n#{command}\n\n" if @configurations[:show_command]
54
+
55
+ pty = @pty || PTY
56
+
57
+ pty.spawn(command) do |r, w, pid|
58
+ r.each { |line| print line } if @configurations[:show_output]
59
+ end
60
+
61
+ profile = @profiles[profile_name.to_s]
62
+ profile.to_folder.delete_files
63
+ end
64
+
65
+ private
66
+
67
+ def create_rsync_command(profile)
68
+ command = "rsync #{@configurations[:options]} "
69
+
70
+ command << "-n " if @dry_run
71
+
72
+ command << @include_patterns.map { |e| "--include=#{e}" }.join(" ") + " "
73
+ command << @exclude_patterns.map { |e| "--exclude=#{e}" }.join(" ")
74
+
75
+ command << " #{profile.from_folder.to_command}"
76
+ command << " #{profile.to_folder.to_command}"
77
+ command << " #{create_ssh_command(profile)}"
78
+
79
+ command << " #{profile.from_folder} #{profile.to_folder}"
80
+ command.gsub(/\s+/, " ")
81
+ end
82
+
83
+ def create_ssh_command(profile)
84
+ from_folder = profile.from_folder
85
+ to_folder = profile.to_folder
86
+
87
+ if from_folder.ssh? && to_folder.ssh?
88
+ raise SyntaxError, 'The source and destination cannot both be remote.'
89
+ elsif from_folder.ssh? || to_folder.ssh?
90
+ "-e ssh"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module RubyClone
2
+ VERSION = "1.0.0"
3
+ end
data/lib/ruby_clone.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "ruby_clone/version"
2
+
3
+ require 'ruby_clone/backup_utils'
4
+
5
+ require 'ruby_clone/rsync'
6
+
7
+ require 'ruby_clone/dsl'
8
+
9
+ require 'ruby_clone/profile'
10
+
11
+ require 'ruby_clone/options'
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ruby_clone/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Frederico Benevides"]
6
+ gem.email = ["fredbene@gmail.com"]
7
+ gem.description = %q{Ruby clone is a command line tool to work with Rsync using DSL!}
8
+ gem.summary = %q{Ruby_clone is high level script of Rsync. Use Ruby DSL to work with RSync! }
9
+ gem.homepage = "http://github.com/fredericobenevides/ruby_clone"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ruby_clone"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RubyClone::VERSION
17
+
18
+ gem.add_development_dependency "guard", "~> 1.3.2"
19
+ gem.add_development_dependency "guard-rspec", "~> 1.2.1"
20
+ gem.add_development_dependency "rspec", "~> 2.11.0"
21
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ module RubyClone
4
+
5
+ describe BackupUtils do
6
+
7
+ describe "Deleting files" do
8
+
9
+ before(:each) do
10
+ @ruby_clone_suffix = '_rbcl'
11
+
12
+ @paths = [ "/my_path/sub_path1/file1#{@ruby_clone_suffix}_20120921",
13
+ "/my_path/sub_path1/file1#{@ruby_clone_suffix}_20120922",
14
+ "/my_path/sub_path1/file1#{@ruby_clone_suffix}_20120923",
15
+
16
+ "/my_path/sub_path1/file2#{@ruby_clone_suffix}_20120922",
17
+ "/my_path/sub_path2/file1#{@ruby_clone_suffix}_20120922",
18
+ "/my_path/sub_path2/file2#{@ruby_clone_suffix}_20120923"]
19
+
20
+ File.stub(:exists?).with(any_args()).and_return(true)
21
+
22
+ FileTest.stub(:file?).with(any_args()).and_return false
23
+ FileTest.stub(:file?).with(@paths[0]).and_return true
24
+ FileTest.stub(:file?).with(@paths[1]).and_return true
25
+ FileTest.stub(:file?).with(@paths[2]).and_return true
26
+ FileTest.stub(:file?).with(@paths[3]).and_return true
27
+ FileTest.stub(:file?).with(@paths[4]).and_return true
28
+ FileTest.stub(:file?).with(@paths[5]).and_return true
29
+
30
+ Find.stub(:find).and_yield('/my_path').and_yield('/my_path/sub_path1')
31
+ .and_yield(@paths[0]).and_yield(@paths[1]).and_yield(@paths[2]).and_yield(@paths[3])
32
+ .and_yield('/my_path/sub_path2')
33
+ .and_yield(@paths[4]).and_yield(@paths[5])
34
+
35
+ end
36
+
37
+ describe "valid" do
38
+
39
+ it "should create a hash with [path_files_without_suffix, path_files] that points to the files with same name" do
40
+ paths = BackupUtils.find_files_with_same_name '/my_path', @ruby_clone_suffix
41
+
42
+ paths.should == { '/my_path/sub_path1/file1' => [@paths[0], @paths[1], @paths[2]],
43
+ '/my_path/sub_path1/file2' => [@paths[3]],
44
+ '/my_path/sub_path2/file1' => [@paths[4]],
45
+ '/my_path/sub_path2/file2' => [@paths[5]] }
46
+ end
47
+
48
+ it "should delete old files with same name but different 'suffix' when passed the limit" do
49
+ FileUtils.should_receive(:remove_entry).with(@paths[0])
50
+ FileUtils.should_receive(:remove_entry).with(@paths[1])
51
+ FileUtils.should_not_receive(:remove_entry).with(@paths[2])
52
+
53
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 1)
54
+ end
55
+
56
+ end
57
+
58
+ describe "invalid" do
59
+
60
+ it "should not delete files with path that doesn't exist" do
61
+ File.stub(:exists?).with(any_args()).and_return(false)
62
+ Find.should_not_receive(:find)
63
+
64
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 5)
65
+ end
66
+
67
+ it "should not delete files with same name but different 'suffix' when it didn't pass the limit" do
68
+ FileUtils.should_not_receive(:remove_entry)
69
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 5)
70
+ end
71
+
72
+ it "should not delete files with same name but different 'suffix' when the found files is on the limit" do
73
+ FileUtils.should_not_receive(:remove_entry)
74
+ BackupUtils.delete_files('/my_path', @ruby_clone_suffix, 3)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,259 @@
1
+ require 'spec_helper'
2
+
3
+ module RubyClone
4
+
5
+ describe "DSL" do
6
+
7
+ before(:all) do
8
+ class DummyClass
9
+ end
10
+ DummyClass.extend RubyClone::DSL
11
+
12
+ @rsync_options = '-Cavh --stats --progress'
13
+ @rsync_command = "rsync #{@rsync_options}"
14
+ @folders = "/from_folder /to_folder"
15
+ end
16
+
17
+ before(:each) do
18
+ @rsync = DummyClass.rsync_new_instance
19
+ end
20
+
21
+ describe "Creating a Profile" do
22
+
23
+ describe "valid" do
24
+
25
+ it "should create the following command 'rsync -Cav --stats /from_folder /to_folder'" do
26
+ DummyClass.profile('backup1') do
27
+ DummyClass.from('/from_folder')
28
+ DummyClass.to('/to_folder')
29
+ end
30
+
31
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} #{@folders}"
32
+ end
33
+
34
+ end
35
+
36
+ describe "invalid" do
37
+
38
+ it "should raise SyntaxError with message 'Empty Profile not allowed' for profile with no block" do
39
+ lambda do
40
+ DummyClass.profile('backup')
41
+ end.should raise_error(SyntaxError, 'Empty Profile not allowed')
42
+ end
43
+
44
+ it "should raise SyntaxError with message 'Empty Profile not allowed' for empty block" do
45
+ lambda do
46
+ DummyClass.profile('backup') { }
47
+ end.should raise_error(SyntaxError, 'Empty Profile not allowed')
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#config" do
53
+
54
+ it "should as default have a default configurations" do
55
+
56
+ DummyClass.profile('backup1') do
57
+ DummyClass.from('/from_folder')
58
+ DummyClass.to('/to_folder')
59
+ end
60
+
61
+ @rsync.instance_eval { @configurations }.should == { options: @rsync_options, show_command: true, show_output: true }
62
+ end
63
+
64
+ it "should change the default configurations when the config has new values" do
65
+ DummyClass.config options: '-Cav', show_command: false, show_output: false
66
+
67
+ DummyClass.profile('backup1') do
68
+ DummyClass.from('/from_folder')
69
+ DummyClass.to('/to_folder')
70
+ end
71
+
72
+ @rsync.instance_eval { @configurations }.should == { options: '-Cav', show_command: false, show_output: false }
73
+ end
74
+ end
75
+
76
+ describe "#from" do
77
+
78
+ it "should create exclude paths just for the profile 'backup1' if exclude DSL is not on the top" do
79
+ DummyClass.profile('backup1') do
80
+ DummyClass.from('/from_folder') do
81
+ DummyClass.exclude_pattern '/exclude_pattern1'
82
+ end
83
+ DummyClass.to('/to_folder')
84
+ end
85
+
86
+ DummyClass.profile('backup2') do
87
+ DummyClass.from('/from_folder')
88
+ DummyClass.to('/to_folder')
89
+ end
90
+
91
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --exclude=/exclude_pattern1 #{@folders}"
92
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} #{@folders}"
93
+ end
94
+
95
+ it "should create include just for the profile 'backup1' if include_pattern DSL is set up only in the profile" do
96
+ DummyClass.profile('backup1') do
97
+ DummyClass.from('/from_folder') do
98
+ DummyClass.include_pattern '/include_pattern1'
99
+ end
100
+ DummyClass.to('/to_folder')
101
+ end
102
+
103
+ DummyClass.profile('backup2') do
104
+ DummyClass.from('/from_folder')
105
+ DummyClass.to('/to_folder')
106
+ end
107
+
108
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --include=/include_pattern1 #{@folders}"
109
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} #{@folders}"
110
+ end
111
+
112
+ it "should have the following options '-e ssh user@server:/from_folder /to_folder' when is ssh" do
113
+ DummyClass.profile('backup1') do
114
+ DummyClass.from('/from_folder', ssh: "user@server")
115
+ DummyClass.to('/to_folder')
116
+ end
117
+
118
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} -e ssh user@server:/from_folder /to_folder"
119
+ end
120
+ end
121
+
122
+ describe "#to" do
123
+
124
+ it "should have '--delete' when options 'delete' is set up" do
125
+ DummyClass.profile('backup1') do
126
+ DummyClass.from('/from_folder')
127
+ DummyClass.to('/to_folder', delete: true)
128
+ end
129
+
130
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --delete #{@folders}"
131
+ end
132
+
133
+ it "should have '--delete-excluded' when options 'delete_excluded' is set up" do
134
+ DummyClass.profile('backup1') do
135
+ DummyClass.from('/from_folder')
136
+ DummyClass.to('/to_folder', delete_excluded: true)
137
+ end
138
+
139
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --delete-excluded #{@folders}"
140
+ end
141
+ end
142
+
143
+ describe "#exclude" do
144
+
145
+ it "should create exclude paths for all profiles if exclude DSL is on the top" do
146
+ DummyClass.exclude_pattern '/exclude_top_path'
147
+
148
+ DummyClass.profile('backup1') do
149
+ DummyClass.from('/from_folder')
150
+ DummyClass.to('/to_folder')
151
+ end
152
+
153
+ DummyClass.profile('backup2') do
154
+ DummyClass.from('/from_folder')
155
+ DummyClass.to('/to_folder')
156
+ end
157
+
158
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --exclude=/exclude_top_path #{@folders}"
159
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --exclude=/exclude_top_path #{@folders}"
160
+ end
161
+
162
+ it "should include the exclude path from the top and profile if exclude DSL are set up in the top and in profile" do
163
+ DummyClass.exclude_pattern 'exclude_top_path'
164
+
165
+ DummyClass.profile('backup1') do
166
+ DummyClass.from('/from_folder') do
167
+ DummyClass.exclude_pattern'/exclude_pattern1'
168
+ end
169
+ DummyClass.to('/to_folder')
170
+ end
171
+
172
+ DummyClass.profile('backup2') do
173
+ DummyClass.from('/from_folder')
174
+ DummyClass.to('/to_folder')
175
+ end
176
+
177
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --exclude=exclude_top_path --exclude=/exclude_pattern1 #{@folders}"
178
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --exclude=exclude_top_path #{@folders}"
179
+ end
180
+
181
+ end
182
+
183
+ describe "#include_pattern" do
184
+
185
+ it "should create include for all profiles if include_pattern DSL is on the top" do
186
+ DummyClass.include_pattern '/include_top_path'
187
+
188
+ DummyClass.profile('backup1') do
189
+ DummyClass.from('/from_folder')
190
+ DummyClass.to('/to_folder')
191
+ end
192
+
193
+ DummyClass.profile('backup2') do
194
+ DummyClass.from('/from_folder')
195
+ DummyClass.to('/to_folder')
196
+ end
197
+
198
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --include=/include_top_path #{@folders}"
199
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --include=/include_top_path #{@folders}"
200
+ end
201
+
202
+ it "should create the include on the top and in profile if include_pattern DSL are set up in the top and in profile" do
203
+ DummyClass.include_pattern '/include_top_path'
204
+
205
+ DummyClass.profile('backup1') do
206
+ DummyClass.from('/from_folder') do
207
+ DummyClass.include_pattern '/include_pattern1'
208
+ end
209
+ DummyClass.to('/to_folder')
210
+ end
211
+
212
+ DummyClass.profile('backup2') do
213
+ DummyClass.from('/from_folder')
214
+ DummyClass.to('/to_folder')
215
+ end
216
+
217
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} --include=/include_top_path --include=/include_pattern1 #{@folders}"
218
+ @rsync.rsync_command('backup2').should == "#{@rsync_command} --include=/include_top_path #{@folders}"
219
+ end
220
+
221
+ end
222
+
223
+ describe "#backup" do
224
+
225
+ it "should include the backup commands when the backup is set up" do
226
+ DummyClass.profile('backup1') do
227
+ DummyClass.from('/from_folder')
228
+ DummyClass.to('/to_folder') do
229
+ DummyClass.backup('/backup_folder')
230
+ end
231
+ end
232
+
233
+ @rsync.rsync_command('backup1').should =~ /#{@rsync_command} -b --suffix=_rbcl_\d{8} --backup-dir=\/backup_folder #{@folders}/
234
+ end
235
+
236
+ it "should include the suffix when the option suffix: is set up" do
237
+ DummyClass.profile('backup1') do
238
+ DummyClass.from('/from_folder')
239
+ DummyClass.to('/to_folder') do
240
+ DummyClass.backup('/backup_folder', suffix: '_my_suffix')
241
+ end
242
+ end
243
+
244
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} -b --suffix=_rbcl_my_suffix --backup-dir=/backup_folder #{@folders}"
245
+ end
246
+
247
+ it "should not include any suffix if the option disable_suffix is true" do
248
+ DummyClass.profile('backup1') do
249
+ DummyClass.from('/from_folder')
250
+ DummyClass.to('/to_folder') do
251
+ DummyClass.backup('/backup_folder', suffix: '_my_suffix', disable_suffix: true)
252
+ end
253
+ end
254
+
255
+ @rsync.rsync_command('backup1').should == "#{@rsync_command} -b --backup-dir=/backup_folder #{@folders}"
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+
4
+ module RubyClone
5
+
6
+ describe Options do
7
+
8
+ before(:each) do
9
+ @output = StringIO.new
10
+ @options = Options.new(@output)
11
+ end
12
+
13
+ describe "Help messages" do
14
+
15
+ it "should show if ARGV is empty" do
16
+ opts = @options.parse([])
17
+ opts.on.to_s.should =~ /-h, --help\s+This message.*/
18
+ end
19
+
20
+ it "should show if ARGV have the option '-h'" do
21
+ @options.parse(%w[-h])
22
+
23
+ @output.seek 0
24
+ @output.read.should =~ /-h, --help\s+This message.*/
25
+ end
26
+
27
+ it "should not show if ARGV have another option that exists and is not '-h'" do
28
+ @options.parse(%w[-b /my_file])
29
+
30
+ @output.seek 0
31
+ @output.read.should_not =~ /-h, --help\s+This message.*/
32
+ end
33
+
34
+ it "should have the banner with message 'Usage: ruby_clone [options] profile'" do
35
+ opts = @options.parse([])
36
+ opts.banner.should == 'Usage: ruby_clone [options] profile'
37
+ end
38
+ end
39
+
40
+ describe "Backup file" do
41
+
42
+ it "should as default the backup file path be '~/.ruby_clone'" do
43
+ @options.parse(%w[backup_profile])
44
+ @options.configurations[:backup_file].should == '~/.ruby_clone'
45
+ end
46
+
47
+ it "should set a new backup file location with the options is '-b'" do
48
+ @options.parse(%w[-b /my_file backup_profile])
49
+ @options.configurations[:backup_file].should == '/my_file'
50
+ end
51
+ end
52
+
53
+ it "should set dry-run mode when the option is '-d'" do
54
+ @options.parse(%w[-d])
55
+ @options.configurations[:dry_run].should be_true
56
+ end
57
+
58
+ end
59
+
60
+ end