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 ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+
6
+ test_client.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in em-ftp-client.gemspec
4
+ gemspec
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,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/*_test.rb'
8
+ test.verbose = true
9
+ end
10
+
@@ -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,7 @@
1
+ require 'eventmachine'
2
+ require 'net/ftp/list'
3
+
4
+ require 'em-ftp-client/control_connection'
5
+ require 'em-ftp-client/data_connection'
6
+ require 'em-ftp-client/session'
7
+ require 'em-ftp-client/sync_session'
@@ -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,7 @@
1
+ module Em
2
+ module Ftp
3
+ module Client
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require File.join(File.dirname(__FILE__), "..", "lib", "em-ftp-client")
5
+
6
+ require 'test/unit'
7
+ require 'mocha'
8
+ require 'shoulda'
9
+ require 'redgreen' unless '1.9'.respond_to?(:force_encoding)
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