ftpd 0.2.2 → 0.3.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/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
|