el_finder_ftp 1.0.0
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/.autotest +23 -0
- data/.gitignore +5 -0
- data/.yardopts +7 -0
- data/Gemfile +4 -0
- data/README.md +115 -0
- data/Rakefile +14 -0
- data/TODO +5 -0
- data/el_finder_ftp.gemspec +29 -0
- data/lib/el_finder_ftp.rb +13 -0
- data/lib/el_finder_ftp/action.rb +32 -0
- data/lib/el_finder_ftp/base64.rb +24 -0
- data/lib/el_finder_ftp/connector.rb +618 -0
- data/lib/el_finder_ftp/ftp_adapter.rb +309 -0
- data/lib/el_finder_ftp/ftp_authentication_error.rb +4 -0
- data/lib/el_finder_ftp/ftp_pathname.rb +161 -0
- data/lib/el_finder_ftp/image.rb +30 -0
- data/lib/el_finder_ftp/mime_type.rb +83 -0
- data/lib/el_finder_ftp/pathname.rb +197 -0
- data/lib/el_finder_ftp/railties.rb +7 -0
- data/lib/el_finder_ftp/version.rb +5 -0
- metadata +150 -0
@@ -0,0 +1,309 @@
|
|
1
|
+
require 'net/ftp/list'
|
2
|
+
|
3
|
+
module ElFinderFtp
|
4
|
+
class FtpAdapter
|
5
|
+
attr_reader :connection, :server
|
6
|
+
|
7
|
+
def initialize(server)
|
8
|
+
@server = {
|
9
|
+
response_cache_expiry_seconds: 30,
|
10
|
+
passive: false,
|
11
|
+
}.merge(server)
|
12
|
+
|
13
|
+
@cached_responses = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def connect
|
17
|
+
unless connected?
|
18
|
+
ElFinderFtp::Connector.logger.info " \e[1;32mFTP:\e[0m Connecting to #{server[:host]} as #{server[:username]}"
|
19
|
+
@connection = Net::FTP.new( server[:host], server[:username], server[:password] )
|
20
|
+
@connection.passive = server[:passive]
|
21
|
+
end
|
22
|
+
|
23
|
+
@connection
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
if connected?
|
28
|
+
ElFinderFtp::Connector.logger.info " \e[1;32mFTP:\e[0m Closing connection to #{server[:host]}"
|
29
|
+
@connection.close
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def connected?
|
34
|
+
self.connection && !self.connection.closed?
|
35
|
+
end
|
36
|
+
|
37
|
+
def children(pathname, with_directory)
|
38
|
+
cached :children, pathname do
|
39
|
+
ftp_context do
|
40
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Fetching children of #{pathname}"
|
41
|
+
list("-la", pathname).map { |e|
|
42
|
+
entry = Net::FTP::List.parse(e)
|
43
|
+
|
44
|
+
# Skip . and .. entries
|
45
|
+
next if entry.basename =~ /^\.+$/
|
46
|
+
# ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Seeing #{e}"
|
47
|
+
|
48
|
+
if with_directory
|
49
|
+
pathname.fullpath + ::ElFinderFtp::FtpPathname.new(self, entry)
|
50
|
+
else
|
51
|
+
::ElFinderFtp::FtpPathname.new(self, entry)
|
52
|
+
end
|
53
|
+
}.compact
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def touch(pathname, options={})
|
59
|
+
unless exist?(pathname)
|
60
|
+
ftp_context do
|
61
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Touching #{pathname}"
|
62
|
+
empty_file = StringIO.new("")
|
63
|
+
# File does not exist, create
|
64
|
+
begin
|
65
|
+
storlines("STOR #{pathname}", empty_file)
|
66
|
+
ensure
|
67
|
+
empty_file.close
|
68
|
+
end
|
69
|
+
end
|
70
|
+
clear_cache(pathname)
|
71
|
+
end
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def exist?(pathname)
|
76
|
+
cached :exist?, pathname do
|
77
|
+
ftp_context do
|
78
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Testing existence of #{pathname}"
|
79
|
+
begin
|
80
|
+
# Test if the file exists
|
81
|
+
size(pathname.to_s)
|
82
|
+
true
|
83
|
+
rescue Net::FTPPermError => ex
|
84
|
+
# Can't "size" directories, but the error returned is different than if the file
|
85
|
+
# doesn't exist at all
|
86
|
+
if ex.message.match /(?:The system cannot find the file specified|Could not get file size)/
|
87
|
+
false
|
88
|
+
else
|
89
|
+
true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def path_type(pathname)
|
97
|
+
cached :path_type, pathname do
|
98
|
+
ftp_context do
|
99
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Getting type of #{pathname}"
|
100
|
+
begin
|
101
|
+
chdir(pathname.to_s)
|
102
|
+
:directory
|
103
|
+
rescue Net::FTPPermError => e
|
104
|
+
:file
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def size(pathname)
|
111
|
+
cached :size, pathname do
|
112
|
+
ftp_context do
|
113
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Getting size of #{pathname}"
|
114
|
+
begin
|
115
|
+
size(pathname.to_s)
|
116
|
+
rescue Net::FTPPermError => e
|
117
|
+
nil
|
118
|
+
rescue Net::FTPReplyError => e
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def mtime(pathname)
|
126
|
+
cached :mtime, pathname do
|
127
|
+
ftp_context do
|
128
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Getting modified time of #{pathname}"
|
129
|
+
begin
|
130
|
+
mtime(pathname.to_s)
|
131
|
+
rescue Net::FTPPermError => e
|
132
|
+
# This command doesn't work on directories
|
133
|
+
0
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def rename(pathname, new_name)
|
140
|
+
ftp_context do
|
141
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Renaming #{pathname} to #{new_name}"
|
142
|
+
rename(pathname.to_s, new_name.to_s)
|
143
|
+
end
|
144
|
+
clear_cache(pathname)
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Both rename and move perform an FTP RNFR/RNTO (rename). Move differs because
|
149
|
+
# it first changes to the parent of the source pathname and uses a relative path for
|
150
|
+
# the RNFR. This seems to allow the (Microsoft) FTP server to rename a directory
|
151
|
+
# into another directory (e.g. /subdir/target -> /target )
|
152
|
+
def move(pathname, new_name)
|
153
|
+
ftp_context(pathname.dirname) do
|
154
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Moving #{pathname} to #{new_name}"
|
155
|
+
rename(pathname.basename.to_s, new_name.to_s)
|
156
|
+
end
|
157
|
+
clear_cache(pathname)
|
158
|
+
clear_cache(new_name)
|
159
|
+
end
|
160
|
+
|
161
|
+
def mkdir(pathname)
|
162
|
+
ftp_context do
|
163
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Creating directory #{pathname}"
|
164
|
+
mkdir(pathname.to_s)
|
165
|
+
end
|
166
|
+
clear_cache(pathname)
|
167
|
+
end
|
168
|
+
|
169
|
+
def rmdir(pathname)
|
170
|
+
ftp_context do
|
171
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Removing directory #{pathname}"
|
172
|
+
rmdir(pathname.to_s)
|
173
|
+
end
|
174
|
+
clear_cache(pathname)
|
175
|
+
end
|
176
|
+
|
177
|
+
def delete(pathname)
|
178
|
+
ftp_context do
|
179
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Deleting #{pathname}"
|
180
|
+
if pathname.directory?
|
181
|
+
rmdir(pathname.to_s)
|
182
|
+
else
|
183
|
+
delete(pathname.to_s)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
clear_cache(pathname)
|
187
|
+
end
|
188
|
+
|
189
|
+
def retrieve(pathname)
|
190
|
+
ftp_context do
|
191
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Retrieving #{pathname}"
|
192
|
+
content = StringIO.new()
|
193
|
+
begin
|
194
|
+
retrbinary("RETR #{pathname}", 10240) do |block|
|
195
|
+
content << block
|
196
|
+
end
|
197
|
+
content.string
|
198
|
+
ensure
|
199
|
+
content.close
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def store(pathname, content)
|
205
|
+
ftp_context do
|
206
|
+
ElFinderFtp::Connector.logger.debug " \e[1;32mFTP:\e[0m Storing #{pathname}"
|
207
|
+
# If content is a string, wrap it in a StringIO
|
208
|
+
content = StringIO.new(content) if content.is_a? String
|
209
|
+
begin
|
210
|
+
storbinary("STOR #{pathname}", content, 10240)
|
211
|
+
ensure
|
212
|
+
content.close if content.respond_to?(:close)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
clear_cache(pathname)
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
##
|
221
|
+
# Creates an FTP connection, if necessary, and executes the given block
|
222
|
+
# in the context of that connection. If a pathname is provided, it is
|
223
|
+
# used to set the current working directory first
|
224
|
+
def ftp_context(pathname = nil, &block)
|
225
|
+
begin
|
226
|
+
connect
|
227
|
+
|
228
|
+
self.connection.chdir(pathname) unless pathname.nil?
|
229
|
+
|
230
|
+
self.connection.instance_eval &block
|
231
|
+
rescue Net::FTPPermError => ex
|
232
|
+
if ex.message =~ /(?:User cannot log in|Login incorrect)/
|
233
|
+
ElFinderFtp::Connector.logger.info " \e[1;32mFTP:\e[0m Authentication required: #{ex}"
|
234
|
+
raise FtpAuthenticationError.new(ex.message)
|
235
|
+
else
|
236
|
+
ElFinderFtp::Connector.logger.error " \e[1;32mFTP:\e[0m Operation failed with error #{ex}"
|
237
|
+
raise
|
238
|
+
end
|
239
|
+
rescue Net::FTPReplyError => ex
|
240
|
+
if ex.message =~ /(?:Password required|Login incorrect)/
|
241
|
+
ElFinderFtp::Connector.logger.info " \e[1;32mFTP:\e[0m Authentication required: #{ex}"
|
242
|
+
raise FtpAuthenticationError.new(ex.message)
|
243
|
+
else
|
244
|
+
ElFinderFtp::Connector.logger.error " \e[1;32mFTP:\e[0m Operation failed with error #{ex}"
|
245
|
+
raise
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# Remove all entries for the given pathname (and its parent folder)
|
252
|
+
# from the FTP cache
|
253
|
+
def clear_cache(pathname)
|
254
|
+
@cached_responses.delete(pathname.to_s)
|
255
|
+
|
256
|
+
if pathname.to_s != '/' && pathname.to_s != '.'
|
257
|
+
# Clear parent of this entry, too
|
258
|
+
@cached_responses.delete(pathname.dirname.to_s)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
##
|
263
|
+
# Looks in the cache for an entry for the given pathname and operation,
|
264
|
+
# returning the cached result if one is found. If one is not found, the given
|
265
|
+
# block is invoked and its result is stored in the cache and returned
|
266
|
+
#
|
267
|
+
# The FTP cache is used to prevent redundant FTP queries for information such as a
|
268
|
+
# file's size, or a directory's contents, during a *single* FTP session.
|
269
|
+
def cached(operation, pathname)
|
270
|
+
response = cache_get(operation, pathname)
|
271
|
+
unless response.nil?
|
272
|
+
return response
|
273
|
+
end
|
274
|
+
|
275
|
+
response = yield
|
276
|
+
cache_put operation, pathname, response
|
277
|
+
|
278
|
+
response
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# Store an FTP response in the cache
|
283
|
+
def cache_put(operation, pathname, response)
|
284
|
+
@cached_responses[pathname.to_s] = {} unless @cached_responses.include?(pathname.to_s)
|
285
|
+
|
286
|
+
@cached_responses[pathname.to_s][operation] = {
|
287
|
+
timestamp: Time.now,
|
288
|
+
response: response
|
289
|
+
}
|
290
|
+
end
|
291
|
+
|
292
|
+
##
|
293
|
+
# Fetch an FTP response from the cache
|
294
|
+
def cache_get(operation, pathname)
|
295
|
+
if @cached_responses.include?(pathname.to_s) && @cached_responses[pathname.to_s].include?(operation)
|
296
|
+
response = @cached_responses[pathname.to_s][operation]
|
297
|
+
|
298
|
+
max_staleness = Time.now - @server[:response_cache_expiry_seconds]
|
299
|
+
|
300
|
+
if response[:timestamp] < max_staleness
|
301
|
+
@cached_responses[pathname.to_s].delete(operation)
|
302
|
+
nil
|
303
|
+
else
|
304
|
+
response[:response]
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module ElFinderFtp
|
2
|
+
class FtpPathname < Pathname
|
3
|
+
attr_reader :adapter
|
4
|
+
|
5
|
+
def initialize(adapter, list_entry_or_name, attrs = {})
|
6
|
+
@adapter = adapter
|
7
|
+
|
8
|
+
if list_entry_or_name.is_a? ElFinderFtp::FtpPathname
|
9
|
+
super(list_entry_or_name.to_s)
|
10
|
+
self.attrs = list_entry_or_name.attrs
|
11
|
+
elsif list_entry_or_name.is_a? Net::FTP::List::Entry
|
12
|
+
super(list_entry_or_name.basename)
|
13
|
+
|
14
|
+
if list_entry_or_name.dir?
|
15
|
+
@size = 0
|
16
|
+
@type = :directory
|
17
|
+
else
|
18
|
+
@type = :file
|
19
|
+
@size = list_entry_or_name.filesize
|
20
|
+
end
|
21
|
+
|
22
|
+
@time = list_entry_or_name.mtime
|
23
|
+
else
|
24
|
+
super(list_entry_or_name)
|
25
|
+
self.attrs = attrs
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def +(other)
|
30
|
+
other = FtpPathname.new(adapter, other) unless FtpPathname === other
|
31
|
+
FtpPathname.new(adapter, plus(@path, other.to_s), other.attrs)
|
32
|
+
end
|
33
|
+
|
34
|
+
def attrs
|
35
|
+
{
|
36
|
+
type: @type,
|
37
|
+
time: @time,
|
38
|
+
size: @size
|
39
|
+
}
|
40
|
+
end
|
41
|
+
def attrs=(val)
|
42
|
+
@time = val[:time]
|
43
|
+
@type = val[:type]
|
44
|
+
@size = val[:size]
|
45
|
+
end
|
46
|
+
|
47
|
+
def atime
|
48
|
+
mtime
|
49
|
+
end
|
50
|
+
|
51
|
+
def ctime
|
52
|
+
mtime
|
53
|
+
end
|
54
|
+
|
55
|
+
def mtime
|
56
|
+
@time ||= adapter.mtime(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def cleanpath
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def exist?
|
64
|
+
adapter.exist?( self )
|
65
|
+
end
|
66
|
+
|
67
|
+
def directory?
|
68
|
+
type == :directory
|
69
|
+
end
|
70
|
+
|
71
|
+
def readable?
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def writable?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def symlink?
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
def file?
|
84
|
+
type == :file
|
85
|
+
end
|
86
|
+
|
87
|
+
def realpath
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def ftype
|
92
|
+
type.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
def type
|
96
|
+
@type ||= adapter.path_type(self)
|
97
|
+
end
|
98
|
+
|
99
|
+
def size
|
100
|
+
unless @type == :directory
|
101
|
+
@size ||= adapter.size(self)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def touch
|
106
|
+
adapter.touch(self)
|
107
|
+
end
|
108
|
+
|
109
|
+
def rename(to)
|
110
|
+
adapter.rename(self, to)
|
111
|
+
end
|
112
|
+
|
113
|
+
def mkdir
|
114
|
+
adapter.mkdir(self)
|
115
|
+
@type = :directory
|
116
|
+
@size = 0
|
117
|
+
end
|
118
|
+
|
119
|
+
def rmdir
|
120
|
+
adapter.rmdir(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
def unlink
|
124
|
+
adapter.delete(self)
|
125
|
+
end
|
126
|
+
|
127
|
+
def read
|
128
|
+
adapter.retrieve(self)
|
129
|
+
end
|
130
|
+
|
131
|
+
def write(content)
|
132
|
+
adapter.store(self, content)
|
133
|
+
@size = nil
|
134
|
+
end
|
135
|
+
|
136
|
+
def executable?
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def pipe?
|
141
|
+
false
|
142
|
+
end
|
143
|
+
|
144
|
+
# These methods are part of the base class, but need to be handled specially
|
145
|
+
# since they return new instances of this class
|
146
|
+
# The code below unwraps the pathname, invokces the original method on it,
|
147
|
+
# and then wraps the result into a new, properly constructed instance of this class
|
148
|
+
{
|
149
|
+
'dirname' => { :args => '(*args)' },
|
150
|
+
'basename' => { :args => '(*args)' },
|
151
|
+
'cleanpath' => { :args => '(*args)' },
|
152
|
+
}.each_pair do |meth, opts|
|
153
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
154
|
+
def #{meth}#{opts[:args]}
|
155
|
+
v = ::Pathname.new(self.to_s).#{meth}#{opts[:args]}
|
156
|
+
self.class.new(@adapter, v.to_s)
|
157
|
+
end
|
158
|
+
METHOD
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|