em-ftp-client 0.0.1
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.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README +38 -0
- data/Rakefile +10 -0
- data/em-ftp-client.gemspec +30 -0
- data/lib/em-ftp-client.rb +7 -0
- data/lib/em-ftp-client/control_connection.rb +274 -0
- data/lib/em-ftp-client/data_connection.rb +31 -0
- data/lib/em-ftp-client/session.rb +53 -0
- data/lib/em-ftp-client/sync_session.rb +57 -0
- data/lib/em-ftp-client/version.rb +7 -0
- data/test/control_connection_test.rb +196 -0
- data/test/helper.rb +9 -0
- metadata +165 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2011 Ben Hughes
|
2
|
+
Copyright (c) 2011 NabeWise Media, Inc.
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
21
|
+
|
data/README
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
em-ftp-client is a simple EventMachine based FTP Client supporting operations
|
2
|
+
on stream data. The primary interface is EventMachine::FtpClient::Session. A
|
3
|
+
standard usage would look like:
|
4
|
+
|
5
|
+
require 'eventmachine'
|
6
|
+
require 'em-ftp-client'
|
7
|
+
|
8
|
+
EM.run do
|
9
|
+
EM::FtpClient::Session.new("0.0.0.0",
|
10
|
+
:username => "test",
|
11
|
+
:password => "1234") do |ftp|
|
12
|
+
|
13
|
+
ftp.list do |l1|
|
14
|
+
puts l1
|
15
|
+
puts
|
16
|
+
ftp.cwd("files") do
|
17
|
+
ftp.list do |l2|
|
18
|
+
puts l2
|
19
|
+
puts
|
20
|
+
ftp.stream {|d| puts "STREAMING: #{d.inspect}" }
|
21
|
+
ftp.get("two.txt") do |t1|
|
22
|
+
puts "COMPLETED"
|
23
|
+
puts t1
|
24
|
+
puts
|
25
|
+
EM.stop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
This library also includes the class SyncSession, which uses fibers and works
|
34
|
+
with em-synchrony.
|
35
|
+
|
36
|
+
TODO:
|
37
|
+
|
38
|
+
Add support for put/STOR operations
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "em-ftp-client/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "em-ftp-client"
|
7
|
+
s.version = Em::Ftp::Client::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ben Hughes"]
|
10
|
+
s.email = ["ben@pixelmachine.org"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{EventMachine FTP client}
|
13
|
+
s.description = %q{An FTP client designed to work well with EventMachine}
|
14
|
+
|
15
|
+
s.rubyforge_project = "em-ftp-client"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
|
23
|
+
s.add_dependency('eventmachine')
|
24
|
+
s.add_dependency('net-ftp-list')
|
25
|
+
|
26
|
+
s.add_development_dependency('shoulda')
|
27
|
+
s.add_development_dependency('mocha')
|
28
|
+
s.add_development_dependency('redgreen')
|
29
|
+
s.add_development_dependency('rake')
|
30
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module FtpClient
|
3
|
+
class ControlConnection < Connection
|
4
|
+
include Protocols::LineText2
|
5
|
+
|
6
|
+
attr_accessor :username, :password
|
7
|
+
|
8
|
+
attr_reader :responder
|
9
|
+
|
10
|
+
class InvalidResponseFormat < RuntimeError; end
|
11
|
+
|
12
|
+
class Response
|
13
|
+
attr_reader :code, :body, :parent
|
14
|
+
def initialize(code=nil, body=nil)
|
15
|
+
@code = code
|
16
|
+
@body = body
|
17
|
+
@empty = true
|
18
|
+
@complete = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def complete?
|
22
|
+
@complete
|
23
|
+
end
|
24
|
+
|
25
|
+
def mark?
|
26
|
+
code && code[0,1] == "1"
|
27
|
+
end
|
28
|
+
|
29
|
+
def success?
|
30
|
+
complete? && code && (code[0,1] == "2" || code[0,1] == "3")
|
31
|
+
end
|
32
|
+
|
33
|
+
def failure?
|
34
|
+
complete? && code && (code[0,1] == "4" || code[0,1] == "5")
|
35
|
+
end
|
36
|
+
|
37
|
+
def <<(line)
|
38
|
+
# For an empty response
|
39
|
+
if @empty
|
40
|
+
# If the response is a valid format
|
41
|
+
if line =~ /^[1-5]\d{2}( |-)/
|
42
|
+
# If the response is multiline
|
43
|
+
if line[3,1] == '-'
|
44
|
+
@code, @body = line.chomp.split('-', 2)
|
45
|
+
# If the response is single line
|
46
|
+
elsif line[3,1] == ' '
|
47
|
+
@code, @body = line.chomp.split(' ', 2)
|
48
|
+
@complete = true
|
49
|
+
end
|
50
|
+
@empty = false
|
51
|
+
else
|
52
|
+
raise InvalidResponseFormat.new(line.chomp)
|
53
|
+
end
|
54
|
+
# If the response is a continuation of a multiline response
|
55
|
+
else
|
56
|
+
# If this response is terminal
|
57
|
+
if line[0..3] == "#{code} "
|
58
|
+
@body += "\n#{line.chomp.split(' ', 2)[1]}"
|
59
|
+
@complete = true
|
60
|
+
# Otherwise continue hoping and wishing for it to be terminated
|
61
|
+
else
|
62
|
+
@body += "\n#{line.chomp}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
line
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
end
|
72
|
+
|
73
|
+
def post_init
|
74
|
+
@data_connection = nil
|
75
|
+
@response = Response.new
|
76
|
+
@responder = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def connection_completed
|
80
|
+
@responder = :receive_greetings
|
81
|
+
end
|
82
|
+
|
83
|
+
def error(e)
|
84
|
+
@errback.call(e) if @errback
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive_line(line)
|
88
|
+
@response << line
|
89
|
+
if @response.complete?
|
90
|
+
# get a new fresh response ready
|
91
|
+
old_response = @response
|
92
|
+
@response = Response.new
|
93
|
+
|
94
|
+
# dispatch appropriately
|
95
|
+
if old_response.success?
|
96
|
+
send(@responder, old_response) if @responder
|
97
|
+
elsif old_response.mark?
|
98
|
+
#maybe notice the mark or something
|
99
|
+
elsif old_response.failure?
|
100
|
+
error(old_response)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
rescue InvalidResponseFormat => e
|
104
|
+
@response = Response.new
|
105
|
+
error(e)
|
106
|
+
end
|
107
|
+
|
108
|
+
def data_connection_closed(data)
|
109
|
+
@data_buffer = data
|
110
|
+
@data_connection = nil
|
111
|
+
send(@responder) if @responder
|
112
|
+
end
|
113
|
+
|
114
|
+
def callback(&blk)
|
115
|
+
@callback = blk
|
116
|
+
end
|
117
|
+
|
118
|
+
def errback(&blk)
|
119
|
+
@errback = blk
|
120
|
+
end
|
121
|
+
|
122
|
+
def call_callback(*args)
|
123
|
+
old_callback = @callback
|
124
|
+
@callback = nil
|
125
|
+
old_callback.call(*args) if old_callback
|
126
|
+
end
|
127
|
+
|
128
|
+
def call_errback(*args)
|
129
|
+
@errback.call(*args) if @errback
|
130
|
+
@errback = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
# commands
|
134
|
+
def user(name)
|
135
|
+
send_data("USER #{name}\r\n")
|
136
|
+
@responder = :user_response
|
137
|
+
end
|
138
|
+
|
139
|
+
def pass(word)
|
140
|
+
send_data("PASS #{word}\r\n")
|
141
|
+
@responder = :password_response
|
142
|
+
end
|
143
|
+
|
144
|
+
def type(t)
|
145
|
+
send_data("TYPE #{t}\r\n")
|
146
|
+
@responder = :type_response
|
147
|
+
end
|
148
|
+
|
149
|
+
def cwd(dir)
|
150
|
+
send_data("CWD #{dir}\r\n")
|
151
|
+
@responder = :cwd_response
|
152
|
+
end
|
153
|
+
|
154
|
+
def pwd
|
155
|
+
send_data("PWD\r\n")
|
156
|
+
@responder = :pwd_response
|
157
|
+
end
|
158
|
+
|
159
|
+
def pasv
|
160
|
+
send_data("PASV\r\n")
|
161
|
+
@responder = :pasv_response
|
162
|
+
end
|
163
|
+
|
164
|
+
def retr(file)
|
165
|
+
send_data("RETR #{file}\r\n")
|
166
|
+
@responder = :retr_response
|
167
|
+
end
|
168
|
+
|
169
|
+
def list
|
170
|
+
send_data("LIST\r\n")
|
171
|
+
@responder = :list_response
|
172
|
+
end
|
173
|
+
|
174
|
+
# handlers
|
175
|
+
|
176
|
+
# Called after initial connection
|
177
|
+
def receive_greetings(banner)
|
178
|
+
if banner.code == "220"
|
179
|
+
user username
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Called when a response for the USER verb is received
|
184
|
+
def user_response(response)
|
185
|
+
pass password
|
186
|
+
end
|
187
|
+
|
188
|
+
# Called when a response for the PASS verb is received
|
189
|
+
def password_response(response)
|
190
|
+
type "I"
|
191
|
+
end
|
192
|
+
|
193
|
+
# Called when a response for the TYPE verb is received
|
194
|
+
def type_response(response)
|
195
|
+
@responder = nil
|
196
|
+
call_callback
|
197
|
+
end
|
198
|
+
|
199
|
+
# Called when a response for the CWD or CDUP is received
|
200
|
+
def cwd_response(response)
|
201
|
+
@responder = nil
|
202
|
+
call_callback
|
203
|
+
end
|
204
|
+
|
205
|
+
# Called when a response for the PWD verb is received
|
206
|
+
#
|
207
|
+
# Calls out with the result to the callback given to pwd
|
208
|
+
def pwd_response(response)
|
209
|
+
@responder = nil
|
210
|
+
call_callback(response.body)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Called when a response for the PASV verb is received
|
214
|
+
#
|
215
|
+
# Opens a new data connection and executes the callback
|
216
|
+
def pasv_response(response)
|
217
|
+
@responder = nil
|
218
|
+
if response.code == "227"
|
219
|
+
if m = /(\d{1,3},\d{1,3},\d{1,3},\d{1,3}),(\d+),(\d+)/.match(response.body)
|
220
|
+
host_ip = m[1].gsub(",", ".")
|
221
|
+
host_port = m[2].to_i*256 + m[3].to_i
|
222
|
+
pasv_callback = @callback
|
223
|
+
@data_connection = EM.connect(host_ip, host_port, DataConnection)
|
224
|
+
@data_connection.on_connect &pasv_callback
|
225
|
+
@data_connection.callback {|data| data_connection_closed(data) }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def retr_response(response=nil)
|
231
|
+
if response && response.code != "226"
|
232
|
+
@data_connection.close_connection
|
233
|
+
@responder = nil
|
234
|
+
error(response)
|
235
|
+
end
|
236
|
+
|
237
|
+
if response && @data_connection
|
238
|
+
#well we still gots to wait for the file
|
239
|
+
elsif @data_connection
|
240
|
+
#well we need to wait for a response
|
241
|
+
else
|
242
|
+
@responder = nil
|
243
|
+
old_data_buffer = @data_buffer
|
244
|
+
@data_buffer = nil
|
245
|
+
call_callback(old_data_buffer)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def list_response(response=nil)
|
250
|
+
if response && response.code != "226"
|
251
|
+
@data_connection.close_connection
|
252
|
+
@responder = nil
|
253
|
+
error(response)
|
254
|
+
end
|
255
|
+
|
256
|
+
if response && @data_connection
|
257
|
+
#well we still gots to wait for the file
|
258
|
+
elsif @data_connection
|
259
|
+
#well we need to wait for a response
|
260
|
+
else
|
261
|
+
@responder = nil
|
262
|
+
old_data_buffer = @data_buffer
|
263
|
+
@data_buffer = nil
|
264
|
+
# parse it into a real form
|
265
|
+
file_list = old_data_buffer.split("\r\n").map do |line|
|
266
|
+
::Net::FTP::List.parse(line)
|
267
|
+
end
|
268
|
+
call_callback(file_list)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module FtpClient
|
3
|
+
class DataConnection < Connection
|
4
|
+
include Deferrable
|
5
|
+
|
6
|
+
def on_connect(&blk); @on_connect = blk; end
|
7
|
+
|
8
|
+
def stream(&blk); @stream = blk; end
|
9
|
+
|
10
|
+
def post_init
|
11
|
+
@buf = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection_completed
|
15
|
+
@on_connect.call(self) if @on_connect
|
16
|
+
end
|
17
|
+
|
18
|
+
def receive_data(data)
|
19
|
+
@buf += data
|
20
|
+
if @stream
|
21
|
+
@stream.call(@buf)
|
22
|
+
@buf = ''
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def unbind
|
27
|
+
succeed(@buf)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module FtpClient
|
3
|
+
# Main class for interacting with a server
|
4
|
+
class Session
|
5
|
+
attr_accessor :username, :password, :port
|
6
|
+
|
7
|
+
attr_reader :control_connection
|
8
|
+
|
9
|
+
def initialize(url, options={}, &cb)
|
10
|
+
self.username = options[:username] || "anonymous"
|
11
|
+
self.password = options[:password] || "anonymous"
|
12
|
+
self.port = options[:port] || 21
|
13
|
+
|
14
|
+
@control_connection = EM.connect(url, port, ControlConnection)
|
15
|
+
@control_connection.username = username
|
16
|
+
@control_connection.password = password
|
17
|
+
@control_connection.callback do
|
18
|
+
cb.call(self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def pwd(&cb)
|
23
|
+
control_connection.callback(&cb)
|
24
|
+
control_connection.pwd
|
25
|
+
end
|
26
|
+
|
27
|
+
def cwd(dir, &cb)
|
28
|
+
control_connection.callback(&cb)
|
29
|
+
control_connection.cwd(dir)
|
30
|
+
end
|
31
|
+
|
32
|
+
def list(&cb)
|
33
|
+
control_connection.callback do
|
34
|
+
control_connection.callback(&cb)
|
35
|
+
control_connection.list
|
36
|
+
end
|
37
|
+
control_connection.pasv
|
38
|
+
end
|
39
|
+
|
40
|
+
def stream(&cb); @stream = cb; end
|
41
|
+
|
42
|
+
def get(file, &cb)
|
43
|
+
control_connection.callback do |data_connection|
|
44
|
+
data_connection.stream(&@stream) if @stream
|
45
|
+
control_connection.callback(&cb)
|
46
|
+
control_connection.retr file
|
47
|
+
end
|
48
|
+
control_connection.pasv
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
begin
|
2
|
+
require 'fiber'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module FtpClient
|
6
|
+
class SyncSession < Session
|
7
|
+
def initialize(url, options={})
|
8
|
+
f = Fiber.current
|
9
|
+
super url, options do |conn|
|
10
|
+
f.resume(conn)
|
11
|
+
end
|
12
|
+
|
13
|
+
Fiber.yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def pwd
|
17
|
+
f = Fiber.current
|
18
|
+
super do |arg|
|
19
|
+
f.resume(arg)
|
20
|
+
end
|
21
|
+
|
22
|
+
Fiber.yield
|
23
|
+
end
|
24
|
+
|
25
|
+
def cwd(dir)
|
26
|
+
f = Fiber.current
|
27
|
+
super dir do
|
28
|
+
f.resume
|
29
|
+
end
|
30
|
+
|
31
|
+
Fiber.yield
|
32
|
+
end
|
33
|
+
|
34
|
+
def list
|
35
|
+
f = Fiber.current
|
36
|
+
super do |data|
|
37
|
+
f.resume(data)
|
38
|
+
end
|
39
|
+
|
40
|
+
Fiber.yield
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(file)
|
44
|
+
f = Fiber.current
|
45
|
+
|
46
|
+
super file do |data|
|
47
|
+
f.resume(data)
|
48
|
+
end
|
49
|
+
|
50
|
+
Fiber.yield
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue LoadError
|
56
|
+
# Could not load fiber support
|
57
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "helper")
|
2
|
+
|
3
|
+
class ControlConnectionTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@control_connection = EventMachine::FtpClient::ControlConnection.new(:foo)
|
6
|
+
end
|
7
|
+
|
8
|
+
context "Response" do
|
9
|
+
setup do
|
10
|
+
@response = EM::FtpClient::ControlConnection::Response.new
|
11
|
+
end
|
12
|
+
|
13
|
+
context "single line" do
|
14
|
+
context "success response" do
|
15
|
+
setup do
|
16
|
+
@code = "200"
|
17
|
+
@body = "OK Yeah"
|
18
|
+
@response << "#{@code} #{@body}\r\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
should "be complete" do
|
22
|
+
assert @response.complete?
|
23
|
+
end
|
24
|
+
|
25
|
+
should "be successful" do
|
26
|
+
assert @response.success?
|
27
|
+
assert !@response.failure?
|
28
|
+
end
|
29
|
+
|
30
|
+
should "have code and body" do
|
31
|
+
assert_equal @code, @response.code
|
32
|
+
assert_equal @body, @response.body
|
33
|
+
end
|
34
|
+
|
35
|
+
should "not be a mark" do
|
36
|
+
assert !@response.mark?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "failure response" do
|
41
|
+
setup do
|
42
|
+
@code = "500"
|
43
|
+
@body = "No Soup For You"
|
44
|
+
@response << "#{@code} #{@body}\r\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
should "be complete" do
|
48
|
+
assert @response.complete?
|
49
|
+
end
|
50
|
+
|
51
|
+
should "be a failure" do
|
52
|
+
assert !@response.success?
|
53
|
+
assert @response.failure?
|
54
|
+
end
|
55
|
+
|
56
|
+
should "have code and body" do
|
57
|
+
assert_equal @code, @response.code
|
58
|
+
assert_equal @body, @response.body
|
59
|
+
end
|
60
|
+
|
61
|
+
should "not be a mark" do
|
62
|
+
assert !@response.mark?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "mark response" do
|
67
|
+
setup do
|
68
|
+
@code = "150"
|
69
|
+
@body = "Roger that"
|
70
|
+
@response << "#{@code} #{@body}\r\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
should "be complete" do
|
74
|
+
assert @response.complete?
|
75
|
+
end
|
76
|
+
|
77
|
+
should "not be a failure or a success" do
|
78
|
+
assert !@response.success?
|
79
|
+
assert !@response.failure?
|
80
|
+
end
|
81
|
+
|
82
|
+
should "have code and body" do
|
83
|
+
assert_equal @code, @response.code
|
84
|
+
assert_equal @body, @response.body
|
85
|
+
end
|
86
|
+
|
87
|
+
should "be a mark" do
|
88
|
+
assert @response.mark?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "multiline" do
|
94
|
+
context "valid" do
|
95
|
+
should "work" do
|
96
|
+
@code = "226"
|
97
|
+
@body = ["Oh yeah", "Its on", "And done"]
|
98
|
+
@response << "#{@code}-#{@body[0]}\r\n"
|
99
|
+
assert !@response.complete?
|
100
|
+
@response << "#{@body[1]}\r\n"
|
101
|
+
assert !@response.complete?
|
102
|
+
@response << "#{@code} #{@body[2]}\r\n"
|
103
|
+
assert @response.complete?
|
104
|
+
assert_equal @code, @response.code
|
105
|
+
assert_equal @body.join("\n"), @response.body
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "attempted to be closed with a different code" do
|
110
|
+
should "not complete" do
|
111
|
+
@code = "226"
|
112
|
+
@response << "#{@code}-Foo\r\n"
|
113
|
+
assert !@response.complete?
|
114
|
+
@response << "227 Done\r\n"
|
115
|
+
assert !@response.complete?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "invalid response" do
|
121
|
+
should "raise an invalid response format error" do
|
122
|
+
assert_raise EM::FtpClient::ControlConnection::InvalidResponseFormat do
|
123
|
+
@response << "THIS IS SPARTA\r\n"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_basic_login
|
130
|
+
r = EM::FtpClient::ControlConnection::Response
|
131
|
+
@control_connection.username = "kingsly"
|
132
|
+
@control_connection.password = "password"
|
133
|
+
|
134
|
+
@control_connection.stubs(:send_data => true)
|
135
|
+
|
136
|
+
@control_connection.connection_completed
|
137
|
+
assert_equal :receive_greetings, @control_connection.responder
|
138
|
+
@control_connection.receive_greetings(r.new("220", "Come on in"))
|
139
|
+
assert_equal :user_response, @control_connection.responder
|
140
|
+
@control_connection.user_response(r.new("331", "Cool"))
|
141
|
+
assert_equal :password_response, @control_connection.responder
|
142
|
+
@control_connection.password_response(r.new("230", "Awesome"))
|
143
|
+
assert_equal :type_response, @control_connection.responder
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_protocol_interaction
|
147
|
+
@control_connection.username = "kingsly"
|
148
|
+
@control_connection.password = "password"
|
149
|
+
|
150
|
+
@control_connection.connection_completed
|
151
|
+
|
152
|
+
started = false
|
153
|
+
working_dir = nil
|
154
|
+
|
155
|
+
@data_host = "127.0.0.1"
|
156
|
+
@data_port = 56789
|
157
|
+
|
158
|
+
@data_host_string = "127,0,0,1,221,213"
|
159
|
+
|
160
|
+
@data_connection = EM::FtpClient::DataConnection.new(:foo)
|
161
|
+
EventMachine.expects(:connect).with(@data_host, @data_port,
|
162
|
+
EM::FtpClient::DataConnection).
|
163
|
+
returns(@data_connection)
|
164
|
+
|
165
|
+
pasv_callback_called = false
|
166
|
+
|
167
|
+
[[nil, nil, "220 Come on in"],
|
168
|
+
[nil, "USER kingsly", "331 Cool"],
|
169
|
+
[nil, "PASS password", "230 Awesome"],
|
170
|
+
[nil, "TYPE I", "200 So Say We All", lambda { started = true }],
|
171
|
+
[[:pwd], "PWD", "257 \"/foo\"", lambda{|d| working_dir = d }],
|
172
|
+
[[:pasv], "PASV", "227 =#{@data_host_string}", lambda{ pasv_callback_called }]].each do |set|
|
173
|
+
@control_connection.expects(:send_data).with(set[1]+"\r\n") if set[1]
|
174
|
+
end.each do |set|
|
175
|
+
@control_connection.callback(&set[3]) if set[3]
|
176
|
+
@control_connection.send(*set[0]) if set[0]
|
177
|
+
@control_connection.receive_line(set[2]+"\r\n") if set[2]
|
178
|
+
end
|
179
|
+
|
180
|
+
@control_connection.expects(:send_data).with("RETR foo.txt\r\n")
|
181
|
+
@control_connection.retr("foo.txt")
|
182
|
+
|
183
|
+
retr_completed = false
|
184
|
+
@control_connection.callback {|data| assert_equal "Bar", data; retr_completed = true }
|
185
|
+
assert !retr_completed
|
186
|
+
@control_connection.receive_line("226 Hooray\r\n")
|
187
|
+
assert !retr_completed
|
188
|
+
@data_connection.receive_data("Bar")
|
189
|
+
@data_connection.unbind
|
190
|
+
assert retr_completed
|
191
|
+
|
192
|
+
assert started
|
193
|
+
assert_equal "\"/foo\"", working_dir
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-ftp-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ben Hughes
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-29 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: eventmachine
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: net-ftp-list
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: shoulda
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :development
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: mocha
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :development
|
76
|
+
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: redgreen
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
type: :development
|
90
|
+
version_requirements: *id005
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rake
|
93
|
+
prerelease: false
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
type: :development
|
104
|
+
version_requirements: *id006
|
105
|
+
description: An FTP client designed to work well with EventMachine
|
106
|
+
email:
|
107
|
+
- ben@pixelmachine.org
|
108
|
+
executables: []
|
109
|
+
|
110
|
+
extensions: []
|
111
|
+
|
112
|
+
extra_rdoc_files: []
|
113
|
+
|
114
|
+
files:
|
115
|
+
- .gitignore
|
116
|
+
- Gemfile
|
117
|
+
- LICENSE
|
118
|
+
- README
|
119
|
+
- Rakefile
|
120
|
+
- em-ftp-client.gemspec
|
121
|
+
- lib/em-ftp-client.rb
|
122
|
+
- lib/em-ftp-client/control_connection.rb
|
123
|
+
- lib/em-ftp-client/data_connection.rb
|
124
|
+
- lib/em-ftp-client/session.rb
|
125
|
+
- lib/em-ftp-client/sync_session.rb
|
126
|
+
- lib/em-ftp-client/version.rb
|
127
|
+
- test/control_connection_test.rb
|
128
|
+
- test/helper.rb
|
129
|
+
has_rdoc: true
|
130
|
+
homepage: ""
|
131
|
+
licenses: []
|
132
|
+
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
none: false
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
hash: 3
|
144
|
+
segments:
|
145
|
+
- 0
|
146
|
+
version: "0"
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
hash: 3
|
153
|
+
segments:
|
154
|
+
- 0
|
155
|
+
version: "0"
|
156
|
+
requirements: []
|
157
|
+
|
158
|
+
rubyforge_project: em-ftp-client
|
159
|
+
rubygems_version: 1.4.1
|
160
|
+
signing_key:
|
161
|
+
specification_version: 3
|
162
|
+
summary: EventMachine FTP client
|
163
|
+
test_files:
|
164
|
+
- test/control_connection_test.rb
|
165
|
+
- test/helper.rb
|