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.
@@ -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