ftpd 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog.md +31 -3
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/README.md +0 -8
- data/VERSION +1 -1
- data/doc/references.md +7 -0
- data/examples/example.rb +7 -0
- data/features/example/eplf.feature +14 -0
- data/features/example/step_definitions/example_server.rb +9 -1
- data/features/ftp_server/list.feature +8 -1
- data/features/ftp_server/name_list.feature +1 -1
- data/features/step_definitions/list.rb +10 -0
- data/features/support/example_server.rb +3 -2
- data/features/support/test_server.rb +0 -8
- data/ftpd.gemspec +12 -2
- data/lib/ftpd.rb +5 -0
- data/lib/ftpd/command_sequence_checker.rb +3 -2
- data/lib/ftpd/disk_file_system.rb +62 -74
- data/lib/ftpd/file_info.rb +115 -0
- data/lib/ftpd/ftp_server.rb +8 -0
- data/lib/ftpd/list_format/eplf.rb +74 -0
- data/lib/ftpd/list_format/ls.rb +154 -0
- data/lib/ftpd/session.rb +31 -7
- data/spec/disk_file_system_spec.rb +69 -70
- data/spec/file_info_spec.rb +59 -0
- data/spec/list_format/eplf_spec.rb +62 -0
- data/spec/list_format/ls_spec.rb +270 -0
- data/spec/spec_helper.rb +3 -0
- metadata +26 -3
@@ -0,0 +1,115 @@
|
|
1
|
+
module Ftpd
|
2
|
+
|
3
|
+
# Information about a file object (file, directory, symlink, etc.)
|
4
|
+
|
5
|
+
class FileInfo
|
6
|
+
|
7
|
+
# @return [String] The file's type, as returned by File.lstat
|
8
|
+
# One of:
|
9
|
+
# * 'file'
|
10
|
+
# * 'directory'
|
11
|
+
# * 'characterSpecial'
|
12
|
+
# * 'blockSpecial'
|
13
|
+
# * 'fifo'
|
14
|
+
# * 'link'
|
15
|
+
# * 'socket'
|
16
|
+
# * 'unknown'
|
17
|
+
|
18
|
+
attr_reader :ftype
|
19
|
+
|
20
|
+
# @return [String] The group name
|
21
|
+
|
22
|
+
attr_reader :group
|
23
|
+
|
24
|
+
# @return [Integer] The mode bits, as returned by File::Stat#mode
|
25
|
+
# The bits are:
|
26
|
+
# * 0 - others have execute permission
|
27
|
+
# * 1 - others have write permission
|
28
|
+
# * 2 - others have read permission
|
29
|
+
# * 3 - group has execute permission
|
30
|
+
# * 4 - group has write permission
|
31
|
+
# * 5 - group has read permission
|
32
|
+
# * 6 - owner has execute permission
|
33
|
+
# * 7 - owner has write permission
|
34
|
+
# * 8 - owner has read permission
|
35
|
+
# * 9 - sticky bit
|
36
|
+
# * 10 - set-group-ID bit
|
37
|
+
# * 11 - set UID bit
|
38
|
+
# Other bits may be present; they are ignored
|
39
|
+
|
40
|
+
attr_reader :mode
|
41
|
+
|
42
|
+
# @return [Time] The modification time
|
43
|
+
|
44
|
+
attr_reader :mtime
|
45
|
+
|
46
|
+
# @return [Integer] The number of hard links
|
47
|
+
|
48
|
+
attr_reader :nlink
|
49
|
+
|
50
|
+
# @return [String] The owner name
|
51
|
+
|
52
|
+
attr_reader :owner
|
53
|
+
|
54
|
+
# @return [Integer] The size, in bytes
|
55
|
+
|
56
|
+
attr_reader :size
|
57
|
+
|
58
|
+
# @return [String] The object's path
|
59
|
+
|
60
|
+
attr_reader :path
|
61
|
+
|
62
|
+
# @return [String] The object's identifier
|
63
|
+
#
|
64
|
+
# This uniquely identifies the file: Two objects with the same
|
65
|
+
# identifier are expected to refer to the same file or directory.
|
66
|
+
#
|
67
|
+
# On a disk file system, might be _dev_._inode_,
|
68
|
+
# e.g. "8388621.48598"
|
69
|
+
#
|
70
|
+
# This is optional and does not have to be set. If set, it is
|
71
|
+
# used in EPLF output.
|
72
|
+
|
73
|
+
attr_reader :identifier
|
74
|
+
|
75
|
+
# Create a new instance. See the various attributes for argument
|
76
|
+
# details.
|
77
|
+
#
|
78
|
+
# @param opts [Hash] The file attributes
|
79
|
+
# @option opts [String] :ftype The file type
|
80
|
+
# @option opts [String] :group The group name
|
81
|
+
# @option opts [String] :identifier The object's identifier
|
82
|
+
# @option opts [Integer] :mode The mode bits
|
83
|
+
# @option opts [Time] :mtime The modification time
|
84
|
+
# @option opts [Integer] :nlink The number of hard links
|
85
|
+
# @option opts [String] :owner The owner name
|
86
|
+
# @option opts [Integer] :size The size
|
87
|
+
# @option opts [String] :path The object's path
|
88
|
+
|
89
|
+
def initialize(opts)
|
90
|
+
@ftype = opts[:ftype]
|
91
|
+
@group = opts[:group]
|
92
|
+
@identifier = opts[:identifier]
|
93
|
+
@mode = opts[:mode]
|
94
|
+
@mtime = opts[:mtime]
|
95
|
+
@nlink = opts[:nlink]
|
96
|
+
@owner = opts[:owner]
|
97
|
+
@path = opts[:path]
|
98
|
+
@size = opts[:size]
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return true if the object is a file
|
102
|
+
|
103
|
+
def file?
|
104
|
+
@ftype == 'file'
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return true if the object is a directory
|
108
|
+
|
109
|
+
def directory?
|
110
|
+
@ftype == 'directory'
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
data/lib/ftpd/ftp_server.rb
CHANGED
@@ -25,6 +25,12 @@ module Ftpd
|
|
25
25
|
|
26
26
|
attr_accessor :response_delay
|
27
27
|
|
28
|
+
# The class for formatting for LIST output. Defaults to
|
29
|
+
# {Ftpd::ListFormat::Ls}. Changes to this attribute only take
|
30
|
+
# effect for new sessions.
|
31
|
+
|
32
|
+
attr_accessor :list_formatter
|
33
|
+
|
28
34
|
# Create a new FTP server. The server won't start until the
|
29
35
|
# #start method is called.
|
30
36
|
#
|
@@ -48,6 +54,7 @@ module Ftpd
|
|
48
54
|
@debug_path = '/dev/stdout'
|
49
55
|
@debug = false
|
50
56
|
@response_delay = 0
|
57
|
+
@list_formatter = ListFormat::Ls
|
51
58
|
end
|
52
59
|
|
53
60
|
private
|
@@ -57,6 +64,7 @@ module Ftpd
|
|
57
64
|
:driver => @driver,
|
58
65
|
:debug => @debug,
|
59
66
|
:debug_path => debug_path,
|
67
|
+
:list_formatter => @list_formatter,
|
60
68
|
:response_delay => response_delay,
|
61
69
|
:tls => @tls).run
|
62
70
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Ftpd
|
2
|
+
module ListFormat
|
3
|
+
|
4
|
+
# Easily Parsed LIST Format (EPLF) Directory formatter
|
5
|
+
# See: {http://cr.yp.to/ftp/list/eplf.html}
|
6
|
+
|
7
|
+
class Eplf
|
8
|
+
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# Create a new formatter for a file object
|
12
|
+
# @param file_info [FileInfo]
|
13
|
+
|
14
|
+
def initialize(file_info)
|
15
|
+
@file_info = file_info
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return the formatted directory entry.
|
19
|
+
# For example:
|
20
|
+
# +i8388621.48598,m824253270,r,s612, 514.html
|
21
|
+
# Note: The calling code adds the \r\n
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"+%s\t%s" % [facts, filename]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def facts
|
30
|
+
[
|
31
|
+
retrievable_fact,
|
32
|
+
cwd_target_fact,
|
33
|
+
size_fact,
|
34
|
+
mtime_fact,
|
35
|
+
identifier_fact,
|
36
|
+
].compact.join(',')
|
37
|
+
end
|
38
|
+
|
39
|
+
def retrievable_fact
|
40
|
+
'r' if retrievable?
|
41
|
+
end
|
42
|
+
|
43
|
+
def cwd_target_fact
|
44
|
+
'/' if cwd_target?
|
45
|
+
end
|
46
|
+
|
47
|
+
def size_fact
|
48
|
+
"s#{@file_info.size}" if retrievable?
|
49
|
+
end
|
50
|
+
|
51
|
+
def mtime_fact
|
52
|
+
"m#{@file_info.mtime.to_i}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def identifier_fact
|
56
|
+
"i#{@file_info.identifier}" if @file_info.identifier
|
57
|
+
end
|
58
|
+
|
59
|
+
def filename
|
60
|
+
File.basename(@file_info.path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def retrievable?
|
64
|
+
@file_info.file?
|
65
|
+
end
|
66
|
+
|
67
|
+
def cwd_target?
|
68
|
+
@file_info.directory?
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Ftpd
|
2
|
+
module ListFormat
|
3
|
+
|
4
|
+
# Directory formatter that approximates the output of "ls -l"
|
5
|
+
|
6
|
+
class Ls
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# Create a new formatter for a file object
|
11
|
+
# @param file_info [FileInfo]
|
12
|
+
|
13
|
+
def initialize(file_info)
|
14
|
+
@file_info = file_info
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return the formatted directory entry, for example:
|
18
|
+
# -rw-r--r-- 1 user group Mar 3 08:38 foo
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
'%s%s %d %-8s %-8s %8d %s %s' % [
|
22
|
+
file_type,
|
23
|
+
file_mode_letters,
|
24
|
+
@file_info.nlink,
|
25
|
+
@file_info.owner,
|
26
|
+
@file_info.group,
|
27
|
+
@file_info.size,
|
28
|
+
format_time(@file_info.mtime),
|
29
|
+
filename,
|
30
|
+
]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
SIX_MONTHS = 180 * 24 * 60 * 60
|
36
|
+
|
37
|
+
def filename
|
38
|
+
File.basename(@file_info.path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def file_type
|
42
|
+
FileType.letter(@file_info.ftype)
|
43
|
+
end
|
44
|
+
|
45
|
+
def file_mode_letters
|
46
|
+
FileMode.new(@file_info.mode).letters
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.format_time(mtime)
|
50
|
+
age = Time.now - mtime
|
51
|
+
format = '%b %e ' + if age < 0 || age > SIX_MONTHS
|
52
|
+
' %Y'
|
53
|
+
else
|
54
|
+
'%H:%M'
|
55
|
+
end
|
56
|
+
mtime.strftime(format)
|
57
|
+
end
|
58
|
+
def_delegator self, :format_time
|
59
|
+
|
60
|
+
# Map file type strings to ls file type letters
|
61
|
+
|
62
|
+
class FileType
|
63
|
+
|
64
|
+
# Map a file type string to a file type letter.
|
65
|
+
# @param ftype [String] file type as returned by File::Stat#ftype
|
66
|
+
# @return [String] File type letter
|
67
|
+
|
68
|
+
def self.letter(ftype)
|
69
|
+
case ftype
|
70
|
+
when 'file'
|
71
|
+
'-'
|
72
|
+
when 'directory'
|
73
|
+
'd'
|
74
|
+
when 'characterSpecial'
|
75
|
+
'c'
|
76
|
+
when 'blockSpecial'
|
77
|
+
'b'
|
78
|
+
when 'fifo'
|
79
|
+
'p'
|
80
|
+
when 'link'
|
81
|
+
'l'
|
82
|
+
when 'socket'
|
83
|
+
's'
|
84
|
+
else # 'unknown', etc.
|
85
|
+
'?'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# Map file mode bits into ls style file mode letters
|
92
|
+
|
93
|
+
class FileMode
|
94
|
+
|
95
|
+
# @param mode [Integer] File mode bits, as returned by
|
96
|
+
# File::Stat#mode
|
97
|
+
|
98
|
+
def initialize(mode)
|
99
|
+
@mode = mode
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return the mode bits as ls style letters.
|
103
|
+
# For example, "-rw-r--r--"
|
104
|
+
|
105
|
+
def letters
|
106
|
+
[
|
107
|
+
triad(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, SET_UID, 'Ss'),
|
108
|
+
triad(GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, SET_GID, 'Ss'),
|
109
|
+
triad(OTHER_READ, OTHER_WRITE, OTHER_EXECUTE, STICKY, 'Tt'),
|
110
|
+
].join
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def bit(bit_number)
|
116
|
+
@mode >> bit_number & 1
|
117
|
+
end
|
118
|
+
|
119
|
+
def triad(read_bit, write_bit, execute_bit, special_bit, special_letters)
|
120
|
+
execute_chars = if bit(special_bit) != 0
|
121
|
+
special_letters
|
122
|
+
else
|
123
|
+
'-x'
|
124
|
+
end
|
125
|
+
[
|
126
|
+
pick_char('-r', read_bit),
|
127
|
+
pick_char('-w', write_bit),
|
128
|
+
pick_char(execute_chars, execute_bit),
|
129
|
+
]
|
130
|
+
end
|
131
|
+
|
132
|
+
def pick_char(s, bit_number)
|
133
|
+
s[bit(bit_number), 1]
|
134
|
+
end
|
135
|
+
|
136
|
+
OTHER_EXECUTE = 0
|
137
|
+
OTHER_WRITE = 1
|
138
|
+
OTHER_READ = 2
|
139
|
+
GROUP_EXECUTE = 3
|
140
|
+
GROUP_WRITE = 4
|
141
|
+
GROUP_READ = 5
|
142
|
+
OWNER_EXECUTE = 6
|
143
|
+
OWNER_WRITE = 7
|
144
|
+
OWNER_READ = 8
|
145
|
+
STICKY = 9
|
146
|
+
SET_GID = 10
|
147
|
+
SET_UID = 11
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
data/lib/ftpd/session.rb
CHANGED
@@ -15,6 +15,7 @@ module Ftpd
|
|
15
15
|
@name_prefix = '/'
|
16
16
|
@debug_path = opts[:debug_path]
|
17
17
|
@debug = opts[:debug]
|
18
|
+
@list_formatter = opts[:list_formatter]
|
18
19
|
@data_type = 'A'
|
19
20
|
@mode = 'S'
|
20
21
|
@format = 'N'
|
@@ -164,24 +165,23 @@ module Ftpd
|
|
164
165
|
def cmd_list(argument)
|
165
166
|
close_data_server_socket_when_done do
|
166
167
|
ensure_logged_in
|
167
|
-
ensure_file_system_supports :
|
168
|
+
ensure_file_system_supports :dir
|
169
|
+
ensure_file_system_supports :file_info
|
168
170
|
path = argument
|
169
171
|
path ||= '.'
|
170
172
|
path = File.expand_path(path, @name_prefix)
|
171
|
-
list
|
172
|
-
transmit_file(list, 'A')
|
173
|
+
transmit_file(list(path), 'A')
|
173
174
|
end
|
174
175
|
end
|
175
176
|
|
176
177
|
def cmd_nlst(argument)
|
177
178
|
close_data_server_socket_when_done do
|
178
179
|
ensure_logged_in
|
179
|
-
ensure_file_system_supports :
|
180
|
+
ensure_file_system_supports :dir
|
180
181
|
path = argument
|
181
182
|
path ||= '.'
|
182
183
|
path = File.expand_path(path, @name_prefix)
|
183
|
-
|
184
|
-
transmit_file(list, 'A')
|
184
|
+
transmit_file(name_list(path), 'A')
|
185
185
|
end
|
186
186
|
end
|
187
187
|
|
@@ -657,7 +657,7 @@ module Ftpd
|
|
657
657
|
def generate_suffix
|
658
658
|
set = ('a'..'z').to_a
|
659
659
|
8.times.map do
|
660
|
-
set.
|
660
|
+
set[rand(set.size)]
|
661
661
|
end.join
|
662
662
|
end
|
663
663
|
|
@@ -679,5 +679,29 @@ module Ftpd
|
|
679
679
|
checker
|
680
680
|
end
|
681
681
|
|
682
|
+
def list(path)
|
683
|
+
format_list(path_list(path))
|
684
|
+
end
|
685
|
+
|
686
|
+
def format_list(paths)
|
687
|
+
paths.map do |path|
|
688
|
+
file_info = @file_system.file_info(path)
|
689
|
+
@list_formatter.new(file_info).to_s + "\n"
|
690
|
+
end.join
|
691
|
+
end
|
692
|
+
|
693
|
+
def name_list(path)
|
694
|
+
path_list(path).map do |path|
|
695
|
+
File.basename(path) + "\n"
|
696
|
+
end.join
|
697
|
+
end
|
698
|
+
|
699
|
+
def path_list(path)
|
700
|
+
if @file_system.directory?(path)
|
701
|
+
path = File.join(path, '*')
|
702
|
+
end
|
703
|
+
@file_system.dir(path).sort
|
704
|
+
end
|
705
|
+
|
682
706
|
end
|
683
707
|
end
|