el_finder_ftp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+ module ElFinderFtp
2
+ class FtpAuthenticationError < RuntimeError
3
+ end
4
+ 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