monster_remote 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,84 +4,140 @@ module Monster
4
4
  module Remote
5
5
  module Wrappers
6
6
 
7
- class MonsterRemoteNetFTPWrapper < StandardError; end
8
- class NetFTPPermissionDenied < MonsterRemoteNetFTPWrapper; end
9
-
10
7
  class NetFTP
11
8
 
12
- def initialize(provider = Net::FTP)
13
- @provider = provider
14
- @filters = []
15
- @nslt = {}
9
+ def initialize(driver=Net::FTP)
10
+ @driver = driver
16
11
  end
17
12
 
18
- def open(host, port, user, pass)
19
- @provider.open(host) do |ftp|
20
- @ftp = ftp
21
- @ftp.connect(host, port)
22
- @ftp.login(user, pass)
23
- yield(self, ftp)
13
+ def open(host, user, password, port, &block)
14
+ ftp = @driver.new
15
+ ftp.connect(host, port)
16
+ ftp.login(user, password)
17
+ if block
18
+ block.call(NetFTPHandler.new(ftp), ftp)
24
19
  end
20
+ ftp.close
25
21
  end
26
22
 
27
- def copy_dir(local_dir, remote_dir)
28
- create_if_not_exists(remote_dir)
29
- local_structure = filter(Dir.entries(local_dir))
30
- local_structure.each do |entry|
31
- local_path = File.join(local_dir, entry)
32
- remote_path = File.join(remote_dir, entry)
33
- copy(local_path, remote_path)
23
+ end# NetFTP
24
+
25
+ class NetFTPHandler
26
+
27
+ def initialize(ftp)
28
+ @ftp = ftp
29
+ end
30
+
31
+ def create_dir(dir)
32
+ pwd = @ftp.pwd
33
+
34
+ dirs = dirs_in_path(dir)
35
+ root_dir_name = dirs.shift
36
+
37
+ create_and_chdir(root_dir_name)
38
+
39
+ if dirs.size > 0
40
+ dirs.each do |dir|
41
+ create_and_chdir(dir)
42
+ end
34
43
  end
44
+
45
+ @ftp.chdir(pwd)
35
46
  end
36
47
 
37
- def add_filter(filter)
38
- @filters ||= []
39
- @filters << filter
48
+ def remove_dir(dir)
49
+ pwd = @ftp.pwd
50
+ dirs = dirs_in_path(dir)
51
+ final_dir = dirs.pop
52
+ dirs.each { |dir| @ftp.chdir(dir) }
53
+ empty_and_remove_dir(final_dir)
54
+ while(final_dir = dirs.pop)
55
+ @ftp.chdir("..")
56
+ empty_and_remove_dir(final_dir)
57
+ end
58
+ @ftp.chdir(pwd)
40
59
  end
41
60
 
42
- private
43
- def copy(from, to)
44
- if(Dir.exists?(from))
45
- copy_dir(from, to)
61
+ def copy_file(from, to)
62
+ if (dirs = dirs_in_path(to)).size > 1
63
+ file = dirs.pop
64
+ create_dir(dirs.join("/"))
65
+
66
+ pwd = @ftp.pwd
67
+ dirs.each { |dir| @ftp.chdir(dir) }
68
+ @ftp.putbinaryfile(from, file)
69
+ @ftp.chdir(pwd)
46
70
  else
47
- copy_file(from, to)
71
+ @ftp.putbinaryfile(from, to)
48
72
  end
49
73
  end
50
74
 
51
- def copy_file(from, to)
52
- @ftp.putbinaryfile(from, to)
75
+ def remove_file(file)
76
+ @ftp.delete(file)
53
77
  end
54
78
 
55
- def filter(dir_structure)
56
- if(@filters.empty?)
57
- @filters << ContentNameBasedFilter.new.reject([".", ".."])
79
+ private
80
+
81
+ def entry_from_regex_on_path(regex, path)
82
+ (matcher = regex.match(path)) && matcher[2].strip
83
+ end
84
+
85
+ def empty_dir(dir)
86
+ pwd = @ftp.pwd
87
+ @ftp.chdir(dir)
88
+ res = @ftp.list;
89
+ res.shift
90
+
91
+ res.each do |item|
92
+ if dir = entry_from_regex_on_path(/(^d.*)(\s.*)/i, item)
93
+ remove_dir(dir)
94
+ end
95
+
96
+ if file = entry_from_regex_on_path(/(^-.*)(\s.*)/i, item)
97
+ remove_file(file)
98
+ end
58
99
  end
59
100
 
60
- allowed = dir_structure
61
- @filters.each do |f|
62
- allowed = f.filter(allowed)
101
+ @ftp.chdir(pwd)
102
+ end
103
+
104
+ def empty_and_remove_dir(dir)
105
+ empty_dir(dir)
106
+ if(@ftp.nlst.include?(dir))
107
+ @ftp.rmdir(dir)
63
108
  end
64
- allowed
65
109
  end
66
110
 
67
- def create_remote_dir(dir)
68
- dirname = File.dirname(dir)
69
- dir_content = @nslt[dirname] || @ftp.nlst(dirname)
70
- dir_exists = dir_content.include? dir
71
- @ftp.mkdir(dir) unless dir_exists
111
+ def create_and_chdir(dir)
112
+ create_dir_if_not_exists(dir)
113
+ @ftp.chdir(dir)
114
+ end
115
+
116
+ def dirs_in_path(dir)
117
+ dirs_in_path = dir.gsub(/\.*\/$/, "").split("/")
72
118
  end
73
119
 
74
- def create_if_not_exists(dir)
120
+ def is_new_dir?(dir)
121
+ is_new_dir = true
75
122
  begin
76
- create_remote_dir(dir)
77
- rescue Net::FTPPermError => e
78
- denied = NetFTPPermissionDenied.new(e)
79
- raise denied, e.message
123
+ is_new_dir = @ftp.nlst(dir).empty?
124
+ rescue Net::FTPTempError => e
125
+ is_unexpected_error = !e.message.include?("450")
126
+ if is_unexpected_error
127
+ raise(e, e.message, caller)
128
+ end
129
+ is_new_dir = !e.message.include?("No files found")
80
130
  end
131
+ return is_new_dir
81
132
  end
82
133
 
83
- end # NetFTP
134
+ def create_dir_if_not_exists(dir)
135
+ if is_new_dir?(dir)
136
+ @ftp.mkdir(dir)
137
+ end
138
+ end
139
+ end# NetFTPHandler
84
140
 
85
- end
86
- end
87
- end
141
+ end# Wrappers
142
+ end# Remote
143
+ end# Monster
@@ -1,3 +1,15 @@
1
- require 'monster/remote/sync'
2
1
  require 'monster/remote/wrappers/net_ftp'
3
- require 'monster/remote/content_name_based_filter'
2
+ require 'monster/remote/filters/filter'
3
+ require 'monster/remote/filters/name_based_filter'
4
+ require 'monster/remote/configuration'
5
+ require 'monster/remote/sync'
6
+ require 'monster/remote/cli'
7
+
8
+ module Monster
9
+ module Remote
10
+
11
+ class MissingProtocolWrapperError < StandardError; end
12
+ class MissingLocalDirError < StandardError; end
13
+ class MissingRemoteDirError < StandardError; end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module Monster
2
+ module Remote
3
+ VERSION = "0.1.0"
4
+ NAME = File.basename(__FILE__)
5
+ end
6
+ end
7
+
8
+ require 'monster/remote'
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib/', __FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "monster_remote"
8
+ s.version = "0.1.0"
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Ricardo Valeriano"]
11
+ s.email = ["ricardo.valeriano@gmail.com"]
12
+ s.homepage = "http://github.com/ricardovaleriano/monster_remote"
13
+ s.summary = "Publish your jekyll blog via ftp easy as pie"
14
+ s.description = "This gem allow you publish your jekyll static site via FTP, easy as pie."
15
+
16
+ s.required_rubygems_version = ">= 0"
17
+
18
+ s.add_development_dependency "rspec"
19
+ s.add_development_dependency "fakefs"
20
+
21
+ s.require_path = 'lib'
22
+
23
+ s.files = ["Gemfile", "LICENSE", "README.md", "Rakefile", "bin/monster_remote", "lib/monster/remote.rb", "lib/monster/remote/cli.rb", "lib/monster/remote/configuration.rb", "lib/monster/remote/filters/filter.rb", "lib/monster/remote/filters/name_based_filter.rb", "lib/monster/remote/sync.rb", "lib/monster/remote/wrappers/net_ftp.rb", "lib/monster_remote.rb", "monster_remote.gemspec", "spec/monster/remote/cli_spec.rb", "spec/monster/remote/configuration_spec.rb", "spec/monster/remote/filters/filter_spec.rb", "spec/monster/remote/filters/name_based_filter_spec.rb", "spec/monster/remote/sync_spec.rb", "spec/monster/remote/wrappers/net_ftp_spec.rb", "spec/spec_helper.rb"]
24
+ s.test_files = ["spec/monster/remote/cli_spec.rb", "spec/monster/remote/configuration_spec.rb", "spec/monster/remote/filters/filter_spec.rb", "spec/monster/remote/filters/name_based_filter_spec.rb", "spec/monster/remote/sync_spec.rb", "spec/monster/remote/wrappers/net_ftp_spec.rb", "spec/spec_helper.rb"]
25
+
26
+
27
+ s.executables = ["monster_remote"]
28
+
29
+ end
@@ -0,0 +1,237 @@
1
+ module Monster
2
+ module Remote
3
+
4
+ describe CLI do
5
+
6
+ module ::Kernel
7
+ def rescuing_exit
8
+ yield
9
+ rescue SystemExit
10
+ end
11
+ end
12
+
13
+ def executable
14
+ File.join(File.expand_path("../../../../", __FILE__), "bin/monster_remote")
15
+ end
16
+
17
+ before(:all) do
18
+ @current_dir = Dir.pwd
19
+ @net_ftp = Monster::Remote::Wrappers::NetFTP
20
+ end
21
+
22
+ before do
23
+ @syncer = double("sync contract").as_null_object
24
+ @syncer.stub(:new).and_return(@syncer)
25
+ @out = double("out contract").as_null_object
26
+ @in = double("in contract").as_null_object
27
+ @password = "123"
28
+ @in.stub(:gets).and_return(@password)
29
+ @wrapper = double("fake wrapper").as_null_object
30
+
31
+ @cli = CLI.new(@syncer, @out, @in)
32
+ end
33
+
34
+ it "-v returns the version" do
35
+ rescuing_exit { @cli.run(["-v"]) == Monster::Remote::VERSION }
36
+ end
37
+
38
+ context "-p (wait for passowrd)" do
39
+
40
+ it "calls #print with 'password:' on the output" do
41
+ rescuing_exit do
42
+ @out.should_receive(:print).with("password:")
43
+ @cli.run(["-p"])
44
+ end
45
+ end
46
+
47
+ it "calls #gets on the input" do
48
+ rescuing_exit do
49
+ @in.should_receive(:gets)
50
+ @cli.run(["-p"])
51
+ end
52
+ end
53
+
54
+ end# wait for password
55
+
56
+ context "using the 'sync' interface" do
57
+
58
+ before do
59
+ @dirname = File.basename(@current_dir)
60
+ Monster::Remote::Wrappers::NetFTP = @wrapper
61
+ end
62
+
63
+ it "--ftp -p awaits for password even if another flags/options" do
64
+ rescuing_exit do
65
+ @in.should_receive(:gets)
66
+ @cli.run(["--ftp", "-p"])
67
+ end
68
+ end
69
+
70
+ it "--ftp and ::new parameters" do
71
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, @dirname, nil)
72
+ rescuing_exit do
73
+ @cli.run(["--ftp"])
74
+ end
75
+ end
76
+
77
+ it "uses NetFTP as default wrapper" do
78
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, @dirname, nil)
79
+ rescuing_exit do
80
+ @cli.run(["-p"])
81
+ end
82
+ end
83
+
84
+ it "--verbose turn on the syncer verbosity" do
85
+ out = STDOUT.clone
86
+ STDOUT = double("omg my own stdout")
87
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, @dirname, STDOUT)
88
+ rescuing_exit do
89
+ @cli.run(["--verbose"])
90
+ end
91
+ STDOUT = out
92
+ end
93
+
94
+ it "-l allow configure the local dir" do
95
+ local_dir = "opa/lele"
96
+ @syncer.should_receive(:new).with(@wrapper, local_dir, File.basename(local_dir), nil)
97
+ rescuing_exit do
98
+ @cli.run(["-l", local_dir])
99
+ end
100
+ end
101
+
102
+ it "-r allow specify the remote dir" do
103
+ remote = "test/omg/gogo"
104
+ @syncer.should_receive(:new).with(@wrapper, @current_dir, remote, nil)
105
+ rescuing_exit do
106
+ @cli.run(["-r", remote])
107
+ end
108
+ end
109
+
110
+ it "start sync with default configurations" do
111
+ @syncer.should_receive(:start).with(nil, nil, "localhost", 21)
112
+ rescuing_exit do
113
+ @cli.run
114
+ end
115
+ end
116
+
117
+ it "-H allow specify the server host" do
118
+ host = "borba"
119
+ @syncer.should_receive(:start).with(nil, nil, host, 21)
120
+ rescuing_exit do
121
+ @cli.run(["-H", host])
122
+ end
123
+ end
124
+
125
+ it "-P allow specify the host port" do
126
+ host = "borba"
127
+ port = "portalhes"
128
+ @syncer.should_receive(:start).with(nil, nil, host, port)
129
+ rescuing_exit do
130
+ @cli.run(["-H", host, "-P", port])
131
+ end
132
+ end
133
+
134
+ it "-u specify the user" do
135
+ user = "omg-my-user"
136
+ @syncer.should_receive(:start).with(user, nil, "localhost", 21)
137
+ rescuing_exit do
138
+ @cli.run(["-u", user])
139
+ end
140
+ end
141
+
142
+ it "-p specify the password" do
143
+ user = "omg-my-user"
144
+ @syncer.should_receive(:start).with(user, @password, "localhost", 21)
145
+ rescuing_exit do
146
+ @cli.run(["-u", user, "-p"])
147
+ end
148
+ end
149
+
150
+ end# sync
151
+
152
+ context "using configs from config file" do
153
+ before(:all) do
154
+ FileUtils.mkdir_p(spec_tmp) unless File.directory?(spec_tmp)
155
+ @file = File.join(spec_tmp, "_config.yml")
156
+ @pwd = Dir.pwd
157
+
158
+ Dir.chdir(spec_tmp)
159
+ file_content = "
160
+ monster:
161
+ remote:
162
+ host: host
163
+ port: 333
164
+ user: user
165
+ pass: false
166
+ local_dir: local
167
+ remote_dir: remote
168
+ "
169
+ File.open(@file, "w") do |f|
170
+ f.write(file_content)
171
+ end
172
+ end
173
+
174
+ it "calls #start with configs from file" do
175
+ @syncer.should_receive(:start).with("user", nil, "host", 333)
176
+ rescuing_exit do
177
+ @cli.run
178
+ end
179
+ end
180
+
181
+ it "calls ::new with configs from file" do
182
+ Monster::Remote::Wrappers::NetFTP = @wrapper
183
+ @syncer.should_receive(:new).with(@wrapper, "local", "remote", nil)
184
+ rescuing_exit do
185
+ @cli.run
186
+ end
187
+ end
188
+
189
+ it "command line options should override configs" do
190
+ @syncer.should_receive(:start).with("omg_my_user", nil, "monster", 333)
191
+ rescuing_exit do
192
+ @cli.run(["-u", "omg_my_user", "-H", "monster"])
193
+ end
194
+ end
195
+
196
+ it "should wait for password if configs says so" do
197
+ file_content = "
198
+ monster:
199
+ remote:
200
+ host: host
201
+ port: 333
202
+ user: user
203
+ pass: true
204
+ local_dir: local
205
+ remote_dir:
206
+ "
207
+ File.open(@file, "w") do |f|
208
+ f.write(file_content)
209
+ end
210
+ @syncer.should_receive(:start).with("user", @password, "host", 333)
211
+ rescuing_exit do
212
+ @cli.run
213
+ end
214
+ end
215
+
216
+ after(:all) do
217
+ FileUtils.rm_rf(spec_tmp) if File.directory?(spec_tmp)
218
+ Dir.chdir(@pwd)
219
+ end
220
+
221
+ end# _config.yml
222
+
223
+ context "executable" do
224
+
225
+ it "-v returns the version" do
226
+ `#{executable} -v`.strip.should == Monster::Remote::VERSION
227
+ end# -v
228
+
229
+ end# executable
230
+
231
+ after(:all) do
232
+ Monster::Remote::Wrappers::NetFTP = @net_ftp
233
+ end
234
+
235
+ end# CLI
236
+ end
237
+ end
@@ -0,0 +1,124 @@
1
+ module Monster
2
+ module Remote
3
+
4
+ describe Configuration do
5
+ def minimal_config_file(host="host")
6
+ "monster:
7
+ remote:
8
+ host: #{host}
9
+ "
10
+ end
11
+
12
+ def path_to(file="o_m_g.yml")
13
+ File.join(spec_tmp, file)
14
+ end
15
+
16
+ before(:all) do
17
+ FileUtils.mkdir_p(spec_tmp)
18
+ @file_content = minimal_config_file << "
19
+ port: 333
20
+ user: user
21
+ pass: true
22
+ local_dir: local
23
+ remote_dir:
24
+ "
25
+ end
26
+
27
+ it "ignores the abscence of config file" do
28
+ lambda { Configuration.new }.should_not raise_error
29
+ end
30
+
31
+ context "specific file on the constructor, properties" do
32
+
33
+ before(:all) do
34
+ @file = "o_m_g.yml"
35
+ File.open(path_to(@file), "w") do |f|
36
+ f.write(@file_content)
37
+ end
38
+ @conf = Configuration.new(path_to(@file))
39
+ end
40
+
41
+ it "#host" do
42
+ @conf.host.should == "host"
43
+ end
44
+
45
+ it "#port" do
46
+ @conf.port.should == 333
47
+ end
48
+
49
+ it "#user" do
50
+ @conf.user.should == "user"
51
+ end
52
+
53
+ it "#password_required?" do
54
+ @conf.password_required?.should be_true
55
+ end
56
+
57
+ it "#local_dir" do
58
+ @conf.local_dir.should == "local"
59
+ end
60
+
61
+ it "#remote_dir" do
62
+ @conf.remote_dir.should be_nil
63
+ end
64
+
65
+ after(:all) do
66
+ FileUtils.rm(path_to(@file)) if File.exists?(path_to(@file))
67
+ end
68
+
69
+ end# specific file
70
+
71
+ context "uses _config.yml as default configuration" do
72
+
73
+ before(:all) do
74
+ @pwd = Dir.pwd
75
+ Dir.chdir(spec_tmp)
76
+ @file = "_config.yml"
77
+ File.open(path_to(@file), "w") do |f|
78
+ f.write(minimal_config_file("borba"))
79
+ end
80
+ @conf = Configuration.new
81
+ end
82
+
83
+ it "gets configs from _config.yml by default" do
84
+ @conf.host.should == "borba"
85
+ end
86
+
87
+ after(:all) do
88
+ FileUtils.rm(@file)
89
+ Dir.chdir(@pwd)
90
+ end
91
+
92
+ end# defaults to _config.yml
93
+
94
+ context "uses .monster.yml as fallback for _config.yml" do
95
+
96
+ before(:all) do
97
+ @pwd = Dir.pwd
98
+ Dir.chdir(spec_tmp)
99
+ @file = ".monster.yml"
100
+ File.open(path_to(@file), "w") do |f|
101
+ f.write(minimal_config_file("lacatumba"))
102
+ end
103
+ @conf = Configuration.new
104
+ end
105
+
106
+ it "gets configs from .monster.yml" do
107
+ @conf.host.should == "lacatumba"
108
+ end
109
+
110
+ after(:all) do
111
+ FileUtils.rm(@file)
112
+ Dir.chdir(@pwd)
113
+ end
114
+
115
+ end
116
+
117
+ after(:all) do
118
+ FileUtils.rm_rf(spec_tmp) if File.exists?(spec_tmp)
119
+ end
120
+
121
+ end# Configuration
122
+
123
+ end# Remote
124
+ end# Monpatster
@@ -0,0 +1,16 @@
1
+ module Monster
2
+ module Remote
3
+ module Filters
4
+
5
+ describe Filter do
6
+
7
+ it "accept a block with rejection logic, the list is passed as argument" do
8
+ filter = Filter.new
9
+ filter.reject lambda{ |entries| entries.reject{|entry| entry != "borba"} }
10
+ filter.filter(["a", "b", "borba"]).should == ["borba"]
11
+ end
12
+ end # describe Filter
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,57 @@
1
+ module Monster
2
+ module Remote
3
+ module Filters
4
+
5
+ describe NameBasedFilter do
6
+
7
+ def create_fake_dirs
8
+ @all_entries.each do |dir|
9
+ FileUtils.mkdir_p(File.join(@root_dir, dir))
10
+ end
11
+ end
12
+
13
+ before(:all) do
14
+ FakeFS.activate!
15
+ @root_dir = "/jajaja"
16
+ @allowed = ["I_CAN_BE_FILE", "ME_CAN"]
17
+ @forbidden = [".", "..", ".mafagafanho", "borba"]
18
+ @all_entries = @allowed + @forbidden
19
+ create_fake_dirs
20
+ end
21
+
22
+ before do
23
+ @filter = subject
24
+ end
25
+
26
+ it "accepts a dir name and return only allowed entries" do
27
+ @filter.reject @forbidden
28
+ @filter.filter(@root_dir).should == @allowed
29
+ end
30
+
31
+ context "#reject, configuring forbbiden names" do
32
+
33
+ it "accept string as parameter" do
34
+ @filter.reject "."
35
+ @filter.reject ".."
36
+ @filter.filter(["a", ".", ".."]).should == ["a"]
37
+ end
38
+
39
+ it "or any Enumerable" do
40
+ rejecting = ["a", "bb", "ccc"]
41
+ @filter.reject rejecting
42
+ @filter.filter(rejecting + ["opalhes"]).should == ["opalhes"]
43
+ end
44
+
45
+ it "accept a block with rejection logic, the list is passed as argument" do
46
+ @filter.reject lambda{ |entries| entries.reject{|entry| entry != "borba"} }
47
+ @filter.filter(["a", "b", "borba"]).should == ["borba"]
48
+ end
49
+ end # #reject
50
+
51
+ after(:all) do
52
+ FakeFS.deactivate!
53
+ end
54
+ end # describe ContentNameBasedFilter
55
+ end
56
+ end
57
+ end