135
218
# Possibly, we could use urlparse.urljoin() here, but
136
219
# I'm concerned about when it chooses to strip the last
137
220
# portion of the path, and when it doesn't.
138
return '/'.join(basepath)
222
# XXX: It seems that ftplib does not handle Unicode paths
223
# at the same time, medusa won't handle utf8 paths
224
# So if we .encode(utf8) here, then we get a Server failure.
225
# while if we use str(), we get a UnicodeError, and the test suite
226
# just skips testing UnicodePaths.
227
return str('/'.join(basepath) or '/')
140
229
def abspath(self, relpath):
141
230
"""Return the full url to the given relative path.
142
231
This can be supplied with a string or a list
144
233
path = self._abspath(relpath)
145
return urlparse.urlunparse((self._proto,
146
self._host, path, '', '', ''))
234
return self._unparse_url(path)
148
236
def has(self, relpath):
149
"""Does the target location exist?
151
XXX: I assume we're never asked has(dirname) and thus I use
152
the FTP size command and assume that if it doesn't raise,
237
"""Does the target location exist?"""
238
# FIXME jam 20060516 We *do* ask about directories in the test suite
239
# We don't seem to in the actual codebase
240
# XXX: I assume we're never asked has(dirname) and thus I use
241
# the FTP size command and assume that if it doesn't raise,
243
abspath = self._abspath(relpath)
156
245
f = self._get_FTP()
157
s = f.size(self._abspath(relpath))
158
mutter("FTP has: %s" % self._abspath(relpath))
246
mutter('FTP has check: %s => %s', relpath, abspath)
248
mutter("FTP has: %s", abspath)
160
except ftplib.error_perm:
161
mutter("FTP has not: %s" % self._abspath(relpath))
250
except ftplib.error_perm, e:
251
if ('is a directory' in str(e).lower()):
252
mutter("FTP has dir: %s: %s", abspath, e)
254
mutter("FTP has not: %s: %s", abspath, e)
164
def get(self, relpath, decode=False):
257
def get(self, relpath, decode=False, retries=0):
165
258
"""Get the file at the given relative path.
167
260
:param relpath: The relative path to the file
261
:param retries: Number of retries after temporary failures so far
169
264
We're meant to return a file-like object which bzr will
170
265
then read from. For now we do this via the magic of StringIO
267
# TODO: decode should be deprecated
173
mutter("FTP get: %s" % self._abspath(relpath))
269
mutter("FTP get: %s", self._abspath(relpath))
174
270
f = self._get_FTP()
176
272
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
179
275
except ftplib.error_perm, e:
180
raise NoSuchFile(self.abspath(relpath), extra=extra)
276
raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
277
except ftplib.error_temp, e:
278
if retries > _number_of_retries:
279
raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
280
% self.abspath(relpath),
283
warning("FTP temporary error: %s. Retrying.", str(e))
284
self._FTP_instance = None
285
return self.get(relpath, decode, retries+1)
287
if retries > _number_of_retries:
288
raise errors.TransportError("FTP control connection closed during GET %s."
289
% self.abspath(relpath),
292
warning("FTP control connection closed. Trying to reopen.")
293
time.sleep(_sleep_between_retries)
294
self._FTP_instance = None
295
return self.get(relpath, decode, retries+1)
182
def put(self, relpath, fp, mode=None):
297
def put(self, relpath, fp, mode=None, retries=0):
183
298
"""Copy the file-like or string object into the location.
185
300
:param relpath: Location to put the contents, relative to base.
186
:param f: File-like or string object.
187
TODO: jam 20051215 This should be an atomic put, not overwritting files in place
301
:param fp: File-like or string object.
302
:param retries: Number of retries after temporary failures so far
188
305
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
307
abspath = self._abspath(relpath)
308
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
309
os.getpid(), random.randint(0,0x7FFFFFFF))
190
310
if not hasattr(fp, 'read'):
191
311
fp = StringIO(fp)
193
mutter("FTP put: %s" % self._abspath(relpath))
313
mutter("FTP put: %s", abspath)
194
314
f = self._get_FTP()
195
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
316
f.storbinary('STOR '+tmp_abspath, fp)
317
f.rename(tmp_abspath, abspath)
318
except (ftplib.error_temp,EOFError), e:
319
warning("Failure during ftp PUT. Deleting temporary file.")
321
f.delete(tmp_abspath)
323
warning("Failed to delete temporary file on the"
324
" server.\nFile: %s", tmp_abspath)
196
327
except ftplib.error_perm, e:
197
raise TransportError(orig_error=e)
328
self._translate_perm_error(e, abspath, extra='could not store')
329
except ftplib.error_temp, e:
330
if retries > _number_of_retries:
331
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
332
% self.abspath(relpath), orig_error=e)
334
warning("FTP temporary error: %s. Retrying.", str(e))
335
self._FTP_instance = None
336
self.put(relpath, fp, mode, retries+1)
338
if retries > _number_of_retries:
339
raise errors.TransportError("FTP control connection closed during PUT %s."
340
% self.abspath(relpath), orig_error=e)
342
warning("FTP control connection closed. Trying to reopen.")
343
time.sleep(_sleep_between_retries)
344
self._FTP_instance = None
345
self.put(relpath, fp, mode, retries+1)
199
347
def mkdir(self, relpath, mode=None):
200
348
"""Create a directory at the given path."""
202
mutter("FTP mkd: %s" % self._abspath(relpath))
205
f.mkd(self._abspath(relpath))
206
except ftplib.error_perm, e:
208
if 'File exists' in s:
209
raise FileExists(self.abspath(relpath), extra=s)
212
except ftplib.error_perm, e:
213
raise TransportError(orig_error=e)
215
def append(self, relpath, f):
349
abspath = self._abspath(relpath)
351
mutter("FTP mkd: %s", abspath)
354
except ftplib.error_perm, e:
355
self._translate_perm_error(e, abspath,
356
unknown_exc=errors.FileExists)
358
def rmdir(self, rel_path):
359
"""Delete the directory at rel_path"""
360
abspath = self._abspath(rel_path)
362
mutter("FTP rmd: %s", abspath)
365
except ftplib.error_perm, e:
366
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
368
def append(self, relpath, f, mode=None):
216
369
"""Append the text in the file-like object into the final
219
raise TransportNotPossible('ftp does not support append()')
221
def copy(self, rel_from, rel_to):
222
"""Copy the item at rel_from to the location at rel_to"""
223
raise TransportNotPossible('ftp does not (yet) support copy()')
372
abspath = self._abspath(relpath)
373
if self.has(relpath):
374
ftp = self._get_FTP()
375
result = ftp.size(abspath)
379
mutter("FTP appe to %s", abspath)
380
self._try_append(relpath, f.read(), mode)
384
def _try_append(self, relpath, text, mode=None, retries=0):
385
"""Try repeatedly to append the given text to the file at relpath.
387
This is a recursive function. On errors, it will be called until the
388
number of retries is exceeded.
391
abspath = self._abspath(relpath)
392
mutter("FTP appe (try %d) to %s", retries, abspath)
393
ftp = self._get_FTP()
394
ftp.voidcmd("TYPE I")
395
cmd = "APPE %s" % abspath
396
conn = ftp.transfercmd(cmd)
400
self._setmode(relpath, mode)
402
except ftplib.error_perm, e:
403
self._translate_perm_error(e, abspath, extra='error appending',
404
unknown_exc=errors.NoSuchFile)
405
except ftplib.error_temp, e:
406
if retries > _number_of_retries:
407
raise errors.TransportError("FTP temporary error during APPEND %s." \
408
"Aborting." % abspath, orig_error=e)
410
warning("FTP temporary error: %s. Retrying.", str(e))
411
self._FTP_instance = None
412
self._try_append(relpath, text, mode, retries+1)
414
def _setmode(self, relpath, mode):
415
"""Set permissions on a path.
417
Only set permissions if the FTP server supports the 'SITE CHMOD'
421
mutter("FTP site chmod: setting permissions to %s on %s",
422
str(mode), self._abspath(relpath))
423
ftp = self._get_FTP()
424
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
426
except ftplib.error_perm, e:
427
# Command probably not available on this server
428
warning("FTP Could not set permissions to %s on %s. %s",
429
str(mode), self._abspath(relpath), str(e))
431
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
432
# to copy something to another machine. And you may be able
433
# to give it its own address as the 'to' location.
434
# So implement a fancier 'copy()'
225
436
def move(self, rel_from, rel_to):
226
437
"""Move the item at rel_from to the location at rel_to"""
438
abs_from = self._abspath(rel_from)
439
abs_to = self._abspath(rel_to)
228
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
229
self._abspath(rel_to)))
441
mutter("FTP mv: %s => %s", abs_from, abs_to)
230
442
f = self._get_FTP()
231
f.rename(self._abspath(rel_from), self._abspath(rel_to))
443
f.rename(abs_from, abs_to)
232
444
except ftplib.error_perm, e:
233
raise TransportError(orig_error=e)
445
self._translate_perm_error(e, abs_from,
446
extra='unable to rename to %r' % (rel_to,),
447
unknown_exc=errors.PathError)
235
451
def delete(self, relpath):
236
452
"""Delete the item at relpath"""
453
abspath = self._abspath(relpath)
238
mutter("FTP rm: %s" % self._abspath(relpath))
455
mutter("FTP rm: %s", abspath)
239
456
f = self._get_FTP()
240
f.delete(self._abspath(relpath))
241
458
except ftplib.error_perm, e:
242
raise TransportError(orig_error=e)
459
self._translate_perm_error(e, abspath, 'error deleting',
460
unknown_exc=errors.NoSuchFile)
244
462
def listable(self):
245
463
"""See Transport.listable."""
304
526
:return: A lock object, which should be passed to Transport.unlock()
306
528
return self.lock_read(relpath)
531
class FtpServer(Server):
532
"""Common code for SFTP server facilities."""
536
self._ftp_server = None
538
self._async_thread = None
543
"""Calculate an ftp url to this server."""
544
return 'ftp://foo:bar@localhost:%d/' % (self._port)
546
# def get_bogus_url(self):
547
# """Return a URL which cannot be connected to."""
548
# return 'ftp://127.0.0.1:1'
550
def log(self, message):
551
"""This is used by medusa.ftp_server to log connections, etc."""
552
self.logs.append(message)
557
raise RuntimeError('Must have medusa to run the FtpServer')
559
self._root = os.getcwdu()
560
self._ftp_server = _ftp_server(
561
authorizer=_test_authorizer(root=self._root),
563
port=0, # bind to a random port
565
logger_object=self # Use FtpServer.log() for messages
567
self._port = self._ftp_server.getsockname()[1]
568
# Don't let it loop forever, or handle an infinite number of requests.
569
# In this case it will run for 100s, or 1000 requests
570
self._async_thread = threading.Thread(target=asyncore.loop,
571
kwargs={'timeout':0.1, 'count':1000})
572
self._async_thread.setDaemon(True)
573
self._async_thread.start()
576
"""See bzrlib.transport.Server.tearDown."""
577
# have asyncore release the channel
578
self._ftp_server.del_channel()
580
self._async_thread.join()
585
_test_authorizer = None
589
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
592
import medusa.filesys
593
import medusa.ftp_server
599
class test_authorizer(object):
600
"""A custom Authorizer object for running the test suite.
602
The reason we cannot use dummy_authorizer, is because it sets the
603
channel to readonly, which we don't always want to do.
606
def __init__(self, root):
609
def authorize(self, channel, username, password):
610
"""Return (success, reply_string, filesystem)"""
612
return 0, 'No Medusa.', None
614
channel.persona = -1, -1
615
if username == 'anonymous':
616
channel.read_only = 1
618
channel.read_only = 0
620
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
623
class ftp_channel(medusa.ftp_server.ftp_channel):
624
"""Customized ftp channel"""
626
def log(self, message):
627
"""Redirect logging requests."""
628
mutter('_ftp_channel: %s', message)
630
def log_info(self, message, type='info'):
631
"""Redirect logging requests."""
632
mutter('_ftp_channel %s: %s', type, message)
634
def cmd_rnfr(self, line):
635
"""Prepare for renaming a file."""
636
self._renaming = line[1]
637
self.respond('350 Ready for RNTO')
638
# TODO: jam 20060516 in testing, the ftp server seems to
639
# check that the file already exists, or it sends
640
# 550 RNFR command failed
642
def cmd_rnto(self, line):
643
"""Rename a file based on the target given.
645
rnto must be called after calling rnfr.
647
if not self._renaming:
648
self.respond('503 RNFR required first.')
649
pfrom = self.filesystem.translate(self._renaming)
650
self._renaming = None
651
pto = self.filesystem.translate(line[1])
653
os.rename(pfrom, pto)
654
except (IOError, OSError), e:
655
# TODO: jam 20060516 return custom responses based on
656
# why the command failed
657
self.respond('550 RNTO failed: %s' % (e,))
659
self.respond('550 RNTO failed')
660
# For a test server, we will go ahead and just die
663
self.respond('250 Rename successful.')
665
def cmd_size(self, line):
666
"""Return the size of a file
668
This is overloaded to help the test suite determine if the
669
target is a directory.
672
if not self.filesystem.isfile(filename):
673
if self.filesystem.isdir(filename):
674
self.respond('550 "%s" is a directory' % (filename,))
676
self.respond('550 "%s" is not a file' % (filename,))
678
self.respond('213 %d'
679
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
681
def cmd_mkd(self, line):
682
"""Create a directory.
684
Overloaded because default implementation does not distinguish
685
*why* it cannot make a directory.
688
self.command_not_understood(''.join(line))
692
self.filesystem.mkdir (path)
693
self.respond ('257 MKD command successful.')
694
except (IOError, OSError), e:
695
self.respond ('550 error creating directory: %s' % (e,))
697
self.respond ('550 error creating directory.')
700
class ftp_server(medusa.ftp_server.ftp_server):
701
"""Customize the behavior of the Medusa ftp_server.
703
There are a few warts on the ftp_server, based on how it expects
707
ftp_channel_class = ftp_channel
709
def __init__(self, *args, **kwargs):
710
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
711
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
713
def log(self, message):
714
"""Redirect logging requests."""
715
mutter('_ftp_server: %s', message)
717
def log_info(self, message, type='info'):
718
"""Override the asyncore.log_info so we don't stipple the screen."""
719
mutter('_ftp_server %s: %s', type, message)
721
_test_authorizer = test_authorizer
722
_ftp_channel = ftp_channel
723
_ftp_server = ftp_server
728
def get_test_permutations():
729
"""Return the permutations to be used in testing."""
730
if not _setup_medusa():
731
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
734
return [(FtpTransport, FtpServer)]