ftpd 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ftpd might be problematic. Click here for more details.
- data/Changelog.md +41 -0
- data/README.md +70 -3
- data/VERSION +1 -1
- data/examples/example.rb +2 -1
- data/features/ftp_server/allo.feature +33 -0
- data/features/ftp_server/command_errors.feature +0 -2
- data/features/ftp_server/delete.feature +7 -0
- data/features/ftp_server/get.feature +7 -0
- data/features/ftp_server/list.feature +6 -0
- data/features/ftp_server/name_list.feature +6 -0
- data/features/ftp_server/put.feature +7 -0
- data/features/ftp_server/step_definitions/test_server.rb +14 -4
- data/features/ftp_server/syst.feature +18 -0
- data/features/step_definitions/{error.rb → error_replies.rb} +0 -0
- data/features/step_definitions/generic_send.rb +9 -0
- data/features/step_definitions/success_replies.rb +7 -0
- data/features/step_definitions/system.rb +7 -0
- data/features/support/test_client.rb +2 -1
- data/features/support/test_server.rb +144 -41
- data/ftpd.gemspec +11 -4
- data/lib/ftpd/disk_file_system.rb +266 -101
- data/lib/ftpd/session.rb +66 -35
- data/spec/disk_file_system_spec.rb +4 -4
- data/spec/file_system_error_translator_spec.rb +43 -0
- metadata +12 -5
data/ftpd.gemspec
CHANGED
@@ -5,18 +5,19 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "ftpd"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Wayne Conrad"]
|
12
|
-
s.date = "2013-02-
|
13
|
-
s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, and
|
12
|
+
s.date = "2013-02-24"
|
13
|
+
s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, and can be used as part of a test fixture or to embed in another program."
|
14
14
|
s.email = "wconrad@yagni.com"
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE.md",
|
17
17
|
"README.md"
|
18
18
|
]
|
19
19
|
s.files = [
|
20
|
+
"Changelog.md",
|
20
21
|
"Gemfile",
|
21
22
|
"Gemfile.lock",
|
22
23
|
"LICENSE.md",
|
@@ -27,6 +28,7 @@ Gem::Specification.new do |s|
|
|
27
28
|
"examples/hello_world.rb",
|
28
29
|
"features/example/example.feature",
|
29
30
|
"features/example/step_definitions/example_server.rb",
|
31
|
+
"features/ftp_server/allo.feature",
|
30
32
|
"features/ftp_server/command_errors.feature",
|
31
33
|
"features/ftp_server/concurrent_sessions.feature",
|
32
34
|
"features/ftp_server/debug.feature",
|
@@ -49,6 +51,7 @@ Gem::Specification.new do |s|
|
|
49
51
|
"features/ftp_server/step_definitions/debug.rb",
|
50
52
|
"features/ftp_server/step_definitions/test_server.rb",
|
51
53
|
"features/ftp_server/syntax_errors.feature",
|
54
|
+
"features/ftp_server/syst.feature",
|
52
55
|
"features/ftp_server/type.feature",
|
53
56
|
"features/step_definitions/client.rb",
|
54
57
|
"features/step_definitions/client_and_server_files.rb",
|
@@ -57,8 +60,9 @@ Gem::Specification.new do |s|
|
|
57
60
|
"features/step_definitions/connect.rb",
|
58
61
|
"features/step_definitions/delete.rb",
|
59
62
|
"features/step_definitions/directories.rb",
|
60
|
-
"features/step_definitions/
|
63
|
+
"features/step_definitions/error_replies.rb",
|
61
64
|
"features/step_definitions/file_structure.rb",
|
65
|
+
"features/step_definitions/generic_send.rb",
|
62
66
|
"features/step_definitions/get.rb",
|
63
67
|
"features/step_definitions/invalid_commands.rb",
|
64
68
|
"features/step_definitions/line_endings.rb",
|
@@ -72,6 +76,8 @@ Gem::Specification.new do |s|
|
|
72
76
|
"features/step_definitions/quit.rb",
|
73
77
|
"features/step_definitions/server_files.rb",
|
74
78
|
"features/step_definitions/stop_server.rb",
|
79
|
+
"features/step_definitions/success_replies.rb",
|
80
|
+
"features/step_definitions/system.rb",
|
75
81
|
"features/step_definitions/type.rb",
|
76
82
|
"features/support/env.rb",
|
77
83
|
"features/support/example_server.rb",
|
@@ -106,6 +112,7 @@ Gem::Specification.new do |s|
|
|
106
112
|
"sandbox/em-server.rb",
|
107
113
|
"spec/disk_file_system_spec.rb",
|
108
114
|
"spec/exception_translator_spec.rb",
|
115
|
+
"spec/file_system_error_translator_spec.rb",
|
109
116
|
"spec/spec_helper.rb",
|
110
117
|
"spec/translate_exceptions_spec.rb"
|
111
118
|
]
|
@@ -1,136 +1,301 @@
|
|
1
1
|
module Ftpd
|
2
2
|
|
3
|
-
# An FTP file system mapped to a disk directory. This can serve as
|
4
|
-
# a template for creating your own specialized driver.
|
5
|
-
#
|
6
|
-
# Some methods may raise FileSystemError; some may not. The
|
7
|
-
# predicates (methods ending with a question mark) may not; other
|
8
|
-
# methods may. FileSystemError is the _only_ exception which a file
|
9
|
-
# system driver may raise.
|
10
|
-
|
11
3
|
class DiskFileSystem
|
12
4
|
|
13
|
-
|
5
|
+
# DiskFileSystem mixin for path expansion. Used by every command
|
6
|
+
# that accesses the disk file system.
|
14
7
|
|
15
|
-
|
16
|
-
# fully qualified.
|
8
|
+
module PathExpansion
|
17
9
|
|
18
|
-
|
19
|
-
|
20
|
-
translate_exception SystemCallError
|
21
|
-
end
|
10
|
+
# Set the data directory, the root of the disk file system.
|
11
|
+
# data_dir should be an absolute path.
|
22
12
|
|
23
|
-
|
24
|
-
|
25
|
-
|
13
|
+
def set_data_dir(data_dir)
|
14
|
+
@data_dir = data_dir
|
15
|
+
end
|
26
16
|
|
27
|
-
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
|
32
|
-
end
|
17
|
+
# Expand an ftp_path to an absolute file system path.
|
18
|
+
#
|
19
|
+
# ftp_path is an absolute path relative to the FTP file system.
|
20
|
+
# @data_dir is an absolute path relative to the disk file system.
|
21
|
+
# The return value is an absolute path relative to the disk file system.
|
33
22
|
|
34
|
-
|
23
|
+
def expand_ftp_path(ftp_path)
|
24
|
+
File.expand_path(File.join(@data_dir, ftp_path))
|
25
|
+
end
|
35
26
|
|
36
|
-
def exists?(ftp_path)
|
37
|
-
File.exists?(expand_ftp_path(ftp_path))
|
38
27
|
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class DiskFileSystem
|
31
|
+
|
32
|
+
# DiskFileSystem mixin providing file attributes. These are used,
|
33
|
+
# alone or in combination, by nearly every command that accesses the
|
34
|
+
# disk file system.
|
35
|
+
|
36
|
+
module Accessors
|
37
|
+
|
38
|
+
# Return true if the path is accessible to the user. This will be
|
39
|
+
# called for put, get and directory lists, so the file or
|
40
|
+
# directory named by the path may not exist.
|
41
|
+
#
|
42
|
+
# Called for:
|
43
|
+
# * STOR
|
44
|
+
# * RETR
|
45
|
+
# * DELE
|
46
|
+
# * CWD
|
47
|
+
|
48
|
+
def accessible?(ftp_path)
|
49
|
+
# The server should never try to access a path outside of the
|
50
|
+
# directory (such as '../foo'), but if it does, we'll catch it
|
51
|
+
# here.
|
52
|
+
expand_ftp_path(ftp_path).start_with?(@data_dir)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return true if the file or directory path exists.
|
56
|
+
#
|
57
|
+
# Called for:
|
58
|
+
# * STOR (with directory)
|
59
|
+
# * RETR
|
60
|
+
# * DELE
|
61
|
+
# * CWD
|
62
|
+
|
63
|
+
def exists?(ftp_path)
|
64
|
+
File.exists?(expand_ftp_path(ftp_path))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return true if the path exists and is a directory.
|
68
|
+
#
|
69
|
+
# Called for:
|
70
|
+
# * CWD
|
39
71
|
|
40
|
-
|
72
|
+
def directory?(ftp_path)
|
73
|
+
File.directory?(expand_ftp_path(ftp_path))
|
74
|
+
end
|
41
75
|
|
42
|
-
def directory?(ftp_path)
|
43
|
-
File.directory?(expand_ftp_path(ftp_path))
|
44
76
|
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class DiskFileSystem
|
80
|
+
|
81
|
+
# DiskFileSystem mixin providing file deletion
|
82
|
+
|
83
|
+
module Delete
|
84
|
+
|
85
|
+
include TranslateExceptions
|
45
86
|
|
46
|
-
|
87
|
+
# Remove a file. Can raise FileSystemError.
|
88
|
+
#
|
89
|
+
# Called for:
|
90
|
+
# * DELE
|
91
|
+
#
|
92
|
+
# If missing, then these commands are not supported.
|
93
|
+
|
94
|
+
def delete(ftp_path)
|
95
|
+
FileUtils.rm expand_ftp_path(ftp_path)
|
96
|
+
end
|
97
|
+
translate_exceptions :delete
|
47
98
|
|
48
|
-
def delete(ftp_path)
|
49
|
-
FileUtils.rm expand_ftp_path(ftp_path)
|
50
99
|
end
|
51
|
-
|
100
|
+
end
|
101
|
+
|
102
|
+
class DiskFileSystem
|
103
|
+
|
104
|
+
# DiskFileSystem mixin providing file reading
|
52
105
|
|
53
|
-
|
106
|
+
module Read
|
107
|
+
|
108
|
+
include TranslateExceptions
|
109
|
+
|
110
|
+
# Read a file into memory. Can raise FileSystemError.
|
111
|
+
#
|
112
|
+
# Called for:
|
113
|
+
# * RETR
|
114
|
+
#
|
115
|
+
# If missing, then these commands are not supported.
|
116
|
+
|
117
|
+
def read(ftp_path)
|
118
|
+
File.open(expand_ftp_path(ftp_path), 'rb', &:read)
|
119
|
+
end
|
120
|
+
translate_exceptions :read
|
54
121
|
|
55
|
-
def read(ftp_path)
|
56
|
-
File.open(expand_ftp_path(ftp_path), 'rb', &:read)
|
57
122
|
end
|
58
|
-
|
123
|
+
end
|
124
|
+
|
125
|
+
class DiskFileSystem
|
59
126
|
|
60
|
-
#
|
127
|
+
# DiskFileSystem mixin providing file writing
|
61
128
|
|
62
|
-
|
63
|
-
|
64
|
-
|
129
|
+
module Write
|
130
|
+
|
131
|
+
include TranslateExceptions
|
132
|
+
|
133
|
+
# Write a file to disk. Can raise FileSystemError.
|
134
|
+
#
|
135
|
+
# Called for:
|
136
|
+
# * STOR
|
137
|
+
#
|
138
|
+
# If missing, then these commands are not supported.
|
139
|
+
|
140
|
+
def write(ftp_path, contents)
|
141
|
+
File.open(expand_ftp_path(ftp_path), 'wb') do |file|
|
142
|
+
file.write contents
|
143
|
+
end
|
65
144
|
end
|
145
|
+
translate_exceptions :write
|
146
|
+
|
66
147
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
def list_long(ftp_path)
|
98
|
-
ls(ftp_path, '-l')
|
148
|
+
end
|
149
|
+
|
150
|
+
class DiskFileSystem
|
151
|
+
|
152
|
+
# Ls interface used by List and NameList
|
153
|
+
|
154
|
+
module Ls
|
155
|
+
|
156
|
+
def ls(ftp_path, option)
|
157
|
+
path = expand_ftp_path(ftp_path)
|
158
|
+
dirname = File.dirname(path)
|
159
|
+
filename = File.basename(path)
|
160
|
+
command = [
|
161
|
+
'ls',
|
162
|
+
option,
|
163
|
+
filename,
|
164
|
+
'2>&1',
|
165
|
+
].compact.join(' ')
|
166
|
+
if File.exists?(dirname)
|
167
|
+
list = Dir.chdir(dirname) do
|
168
|
+
`#{command}`
|
169
|
+
end
|
170
|
+
else
|
171
|
+
list = ''
|
172
|
+
end
|
173
|
+
list = "" if $? != 0
|
174
|
+
list = list.gsub(/^total \d+\n/, '')
|
175
|
+
end
|
176
|
+
|
99
177
|
end
|
100
178
|
|
101
|
-
|
102
|
-
|
103
|
-
|
179
|
+
end
|
180
|
+
|
181
|
+
class DiskFileSystem
|
182
|
+
|
183
|
+
# DiskFileSystem mixin providing directory listing
|
184
|
+
|
185
|
+
module List
|
186
|
+
|
187
|
+
include TranslateExceptions
|
188
|
+
|
189
|
+
# Get a file list, long form. Can raise FileSystemError. This
|
190
|
+
# returns a long-form directory listing. The FTP standard does
|
191
|
+
# not specify the format of the listing, but many systems emit a
|
192
|
+
# *nix style directory listing:
|
193
|
+
#
|
194
|
+
# -rw-r--r-- 1 wayne wayne 4 Feb 18 18:36 a
|
195
|
+
# -rw-r--r-- 1 wayne wayne 8 Feb 18 18:36 b
|
196
|
+
#
|
197
|
+
# some emit a Windows style listing. Some emit EPLF (Easily
|
198
|
+
# Parsed List Format):
|
199
|
+
#
|
200
|
+
# +i8388621.48594,m825718503,r,s280, djb.html
|
201
|
+
# +i8388621.50690,m824255907,/, 514
|
202
|
+
# +i8388621.48598,m824253270,r,s612, 514.html
|
203
|
+
#
|
204
|
+
# EPLF is a draft internet standard for the output of LIST:
|
205
|
+
#
|
206
|
+
# http://cr.yp.to/ftp/list/eplf.html
|
207
|
+
#
|
208
|
+
# Some FTP clients know how to parse EPLF; those clients will
|
209
|
+
# display the EPLF in a more user-friendly format. Clients that
|
210
|
+
# don't recognize EPLF will display it raw. The advantages of
|
211
|
+
# EPLF are that it's easier for clients to parse, and the client
|
212
|
+
# can display the LIST output in any format it likes.
|
213
|
+
#
|
214
|
+
# This class emits a *nix style listing. It does so by shelling
|
215
|
+
# to the "ls" command, so it won't run on Windows at all.
|
216
|
+
#
|
217
|
+
# Called for:
|
218
|
+
# * LIST
|
219
|
+
#
|
220
|
+
# If missing, then these commands are not supported.
|
221
|
+
|
222
|
+
def list(ftp_path)
|
223
|
+
ls(ftp_path, '-l')
|
224
|
+
end
|
104
225
|
|
105
|
-
def list_short(ftp_path)
|
106
|
-
ls(ftp_path, '-1')
|
107
226
|
end
|
227
|
+
end
|
108
228
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
229
|
+
class DiskFileSystem
|
230
|
+
|
231
|
+
# DiskFileSystem mixin providing directory name listing
|
232
|
+
|
233
|
+
module NameList
|
234
|
+
|
235
|
+
include Ls
|
236
|
+
|
237
|
+
# Get a file list, short form. Can raise FileSystemError.
|
238
|
+
#
|
239
|
+
# This returns one filename per line, and nothing else
|
240
|
+
#
|
241
|
+
# Called for:
|
242
|
+
# * NLST
|
243
|
+
#
|
244
|
+
# If missing, then these commands are not supported.
|
245
|
+
|
246
|
+
def name_list(ftp_path)
|
247
|
+
ls(ftp_path, '-1')
|
127
248
|
end
|
128
|
-
|
129
|
-
list = list.gsub(/^total \d+\n/, '')
|
249
|
+
|
130
250
|
end
|
251
|
+
end
|
252
|
+
|
253
|
+
class DiskFileSystem
|
254
|
+
|
255
|
+
# DiskFileSystem "omnibus" mixin, which pulls in mixins which are
|
256
|
+
# likely to be needed by any DiskFileSystem.
|
131
257
|
|
132
|
-
|
133
|
-
|
258
|
+
module Base
|
259
|
+
include TranslateExceptions
|
260
|
+
include DiskFileSystem::Accessors
|
261
|
+
include DiskFileSystem::PathExpansion
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
|
266
|
+
# An FTP file system mapped to a disk directory. This can serve as
|
267
|
+
# a template for creating your own specialized driver.
|
268
|
+
#
|
269
|
+
# Some methods may raise FileSystemError; some may not. The
|
270
|
+
# predicates (methods ending with a question mark) may not; other
|
271
|
+
# methods may. FileSystemError is the _only_ exception which a file
|
272
|
+
# system driver may raise.
|
273
|
+
#
|
274
|
+
# The class is divided into modules that may be included piecemeal.
|
275
|
+
# By including some mixins and not others, you can compose a disk
|
276
|
+
# file system driver "a la cart." This is useful if you want an FTP
|
277
|
+
# server that, for example, allows reading but not writing files.
|
278
|
+
|
279
|
+
class DiskFileSystem
|
280
|
+
|
281
|
+
include DiskFileSystem::Base
|
282
|
+
|
283
|
+
# Mixins that make available commands or groups of commands. Each
|
284
|
+
# can be safely left out with the only effect being to make One or
|
285
|
+
# more commands be unimplemented.
|
286
|
+
|
287
|
+
include DiskFileSystem::Delete
|
288
|
+
include DiskFileSystem::List
|
289
|
+
include DiskFileSystem::NameList
|
290
|
+
include DiskFileSystem::Read
|
291
|
+
include DiskFileSystem::Write
|
292
|
+
|
293
|
+
# Make a new instance to serve a directory. data_dir should be an
|
294
|
+
# absolute path.
|
295
|
+
|
296
|
+
def initialize(data_dir)
|
297
|
+
set_data_dir data_dir
|
298
|
+
translate_exception SystemCallError
|
134
299
|
end
|
135
300
|
|
136
301
|
end
|