216
140
# Possibly, we could use urlparse.urljoin() here, but
217
141
# I'm concerned about when it chooses to strip the last
218
142
# portion of the path, and when it doesn't.
220
# XXX: It seems that ftplib does not handle Unicode paths
221
# at the same time, medusa won't handle utf8 paths
222
# So if we .encode(utf8) here, then we get a Server failure.
223
# while if we use str(), we get a UnicodeError, and the test suite
224
# just skips testing UnicodePaths.
225
return str('/'.join(basepath) or '/')
143
return '/'.join(basepath)
227
145
def abspath(self, relpath):
228
146
"""Return the full url to the given relative path.
229
147
This can be supplied with a string or a list
231
149
path = self._abspath(relpath)
232
return self._unparse_url(path)
150
return urlparse.urlunparse((self._proto,
151
self._host, path, '', '', ''))
234
153
def has(self, relpath):
235
"""Does the target location exist?"""
236
# FIXME jam 20060516 We *do* ask about directories in the test suite
237
# We don't seem to in the actual codebase
238
# XXX: I assume we're never asked has(dirname) and thus I use
239
# the FTP size command and assume that if it doesn't raise,
241
abspath = self._abspath(relpath)
154
"""Does the target location exist?
156
XXX: I assume we're never asked has(dirname) and thus I use
157
the FTP size command and assume that if it doesn't raise,
243
161
f = self._get_FTP()
244
mutter('FTP has check: %s => %s', relpath, abspath)
246
mutter("FTP has: %s", abspath)
162
s = f.size(self._abspath(relpath))
163
mutter("FTP has: %s" % self._abspath(relpath))
248
except ftplib.error_perm, e:
249
if ('is a directory' in str(e).lower()):
250
mutter("FTP has dir: %s: %s", abspath, e)
252
mutter("FTP has not: %s: %s", abspath, e)
165
except ftplib.error_perm:
166
mutter("FTP has not: %s" % self._abspath(relpath))
255
def get(self, relpath, decode=False, retries=0):
169
def get(self, relpath, decode=False):
256
170
"""Get the file at the given relative path.
258
172
:param relpath: The relative path to the file
259
:param retries: Number of retries after temporary failures so far
262
174
We're meant to return a file-like object which bzr will
263
175
then read from. For now we do this via the magic of StringIO
265
# TODO: decode should be deprecated
267
mutter("FTP get: %s", self._abspath(relpath))
178
mutter("FTP get: %s" % self._abspath(relpath))
268
179
f = self._get_FTP()
270
181
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
273
184
except ftplib.error_perm, e:
274
raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
275
except ftplib.error_temp, e:
276
if retries > _number_of_retries:
277
raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
278
% self.abspath(relpath),
281
warning("FTP temporary error: %s. Retrying.", str(e))
282
self._FTP_instance = None
283
return self.get(relpath, decode, retries+1)
285
if retries > _number_of_retries:
286
raise errors.TransportError("FTP control connection closed during GET %s."
287
% self.abspath(relpath),
290
warning("FTP control connection closed. Trying to reopen.")
291
time.sleep(_sleep_between_retries)
292
self._FTP_instance = None
293
return self.get(relpath, decode, retries+1)
185
raise NoSuchFile(msg="Error retrieving %s: %s"
186
% (self.abspath(relpath), str(e)),
295
def put_file(self, relpath, fp, mode=None, retries=0):
189
def put(self, relpath, fp):
296
190
"""Copy the file-like or string object into the location.
298
192
:param relpath: Location to put the contents, relative to base.
299
:param fp: File-like or string object.
300
:param retries: Number of retries after temporary failures so far
303
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
193
:param f: File-like or string object.
305
abspath = self._abspath(relpath)
306
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
307
os.getpid(), random.randint(0,0x7FFFFFFF))
308
if getattr(fp, 'read', None) is None:
195
if not hasattr(fp, 'read'):
309
196
fp = StringIO(fp)
311
mutter("FTP put: %s", abspath)
198
mutter("FTP put: %s" % self._abspath(relpath))
312
199
f = self._get_FTP()
314
f.storbinary('STOR '+tmp_abspath, fp)
315
f.rename(tmp_abspath, abspath)
316
except (ftplib.error_temp,EOFError), e:
317
warning("Failure during ftp PUT. Deleting temporary file.")
319
f.delete(tmp_abspath)
321
warning("Failed to delete temporary file on the"
322
" server.\nFile: %s", tmp_abspath)
200
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
325
201
except ftplib.error_perm, e:
326
self._translate_perm_error(e, abspath, extra='could not store')
327
except ftplib.error_temp, e:
328
if retries > _number_of_retries:
329
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
330
% self.abspath(relpath), orig_error=e)
332
warning("FTP temporary error: %s. Retrying.", str(e))
333
self._FTP_instance = None
334
self.put_file(relpath, fp, mode, retries+1)
336
if retries > _number_of_retries:
337
raise errors.TransportError("FTP control connection closed during PUT %s."
338
% self.abspath(relpath), orig_error=e)
340
warning("FTP control connection closed. Trying to reopen.")
341
time.sleep(_sleep_between_retries)
342
self._FTP_instance = None
343
self.put_file(relpath, fp, mode, retries+1)
202
raise FtpTransportError(orig_error=e)
345
def mkdir(self, relpath, mode=None):
204
def mkdir(self, relpath):
346
205
"""Create a directory at the given path."""
347
abspath = self._abspath(relpath)
349
mutter("FTP mkd: %s", abspath)
352
except ftplib.error_perm, e:
353
self._translate_perm_error(e, abspath,
354
unknown_exc=errors.FileExists)
356
def rmdir(self, rel_path):
357
"""Delete the directory at rel_path"""
358
abspath = self._abspath(rel_path)
360
mutter("FTP rmd: %s", abspath)
363
except ftplib.error_perm, e:
364
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
366
def append_file(self, relpath, f, mode=None):
207
mutter("FTP mkd: %s" % self._abspath(relpath))
210
f.mkd(self._abspath(relpath))
211
except ftplib.error_perm, e:
213
if 'File exists' in s:
214
# Swallow attempts to mkdir something which is already
215
# present. Hopefully this will shush some errors.
219
except ftplib.error_perm, e:
220
raise FtpTransportError(orig_error=e)
222
def append(self, relpath, f):
367
223
"""Append the text in the file-like object into the final
370
abspath = self._abspath(relpath)
371
if self.has(relpath):
372
ftp = self._get_FTP()
373
result = ftp.size(abspath)
377
mutter("FTP appe to %s", abspath)
378
self._try_append(relpath, f.read(), mode)
382
def _try_append(self, relpath, text, mode=None, retries=0):
383
"""Try repeatedly to append the given text to the file at relpath.
385
This is a recursive function. On errors, it will be called until the
386
number of retries is exceeded.
389
abspath = self._abspath(relpath)
390
mutter("FTP appe (try %d) to %s", retries, abspath)
391
ftp = self._get_FTP()
392
ftp.voidcmd("TYPE I")
393
cmd = "APPE %s" % abspath
394
conn = ftp.transfercmd(cmd)
398
self._setmode(relpath, mode)
400
except ftplib.error_perm, e:
401
self._translate_perm_error(e, abspath, extra='error appending',
402
unknown_exc=errors.NoSuchFile)
403
except ftplib.error_temp, e:
404
if retries > _number_of_retries:
405
raise errors.TransportError("FTP temporary error during APPEND %s." \
406
"Aborting." % abspath, orig_error=e)
408
warning("FTP temporary error: %s. Retrying.", str(e))
409
self._FTP_instance = None
410
self._try_append(relpath, text, mode, retries+1)
412
def _setmode(self, relpath, mode):
413
"""Set permissions on a path.
415
Only set permissions if the FTP server supports the 'SITE CHMOD'
419
mutter("FTP site chmod: setting permissions to %s on %s",
420
str(mode), self._abspath(relpath))
421
ftp = self._get_FTP()
422
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
424
except ftplib.error_perm, e:
425
# Command probably not available on this server
426
warning("FTP Could not set permissions to %s on %s. %s",
427
str(mode), self._abspath(relpath), str(e))
429
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
430
# to copy something to another machine. And you may be able
431
# to give it its own address as the 'to' location.
432
# So implement a fancier 'copy()'
226
raise TransportNotPossible('ftp does not support append()')
228
def copy(self, rel_from, rel_to):
229
"""Copy the item at rel_from to the location at rel_to"""
230
raise TransportNotPossible('ftp does not (yet) support copy()')
434
232
def move(self, rel_from, rel_to):
435
233
"""Move the item at rel_from to the location at rel_to"""
436
abs_from = self._abspath(rel_from)
437
abs_to = self._abspath(rel_to)
439
mutter("FTP mv: %s => %s", abs_from, abs_to)
235
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
236
self._abspath(rel_to)))
440
237
f = self._get_FTP()
441
f.rename(abs_from, abs_to)
238
f.rename(self._abspath(rel_from), self._abspath(rel_to))
442
239
except ftplib.error_perm, e:
443
self._translate_perm_error(e, abs_from,
444
extra='unable to rename to %r' % (rel_to,),
445
unknown_exc=errors.PathError)
240
raise FtpTransportError(orig_error=e)
449
242
def delete(self, relpath):
450
243
"""Delete the item at relpath"""
451
abspath = self._abspath(relpath)
453
mutter("FTP rm: %s", abspath)
245
mutter("FTP rm: %s" % self._abspath(relpath))
454
246
f = self._get_FTP()
247
f.delete(self._abspath(relpath))
456
248
except ftplib.error_perm, e:
457
self._translate_perm_error(e, abspath, 'error deleting',
458
unknown_exc=errors.NoSuchFile)
249
raise FtpTransportError(orig_error=e)
460
251
def listable(self):
461
252
"""See Transport.listable."""
524
311
:return: A lock object, which should be passed to Transport.unlock()
526
313
return self.lock_read(relpath)
529
class FtpServer(Server):
530
"""Common code for SFTP server facilities."""
534
self._ftp_server = None
536
self._async_thread = None
541
"""Calculate an ftp url to this server."""
542
return 'ftp://foo:bar@localhost:%d/' % (self._port)
544
# def get_bogus_url(self):
545
# """Return a URL which cannot be connected to."""
546
# return 'ftp://127.0.0.1:1'
548
def log(self, message):
549
"""This is used by medusa.ftp_server to log connections, etc."""
550
self.logs.append(message)
555
raise RuntimeError('Must have medusa to run the FtpServer')
557
self._root = os.getcwdu()
558
self._ftp_server = _ftp_server(
559
authorizer=_test_authorizer(root=self._root),
561
port=0, # bind to a random port
563
logger_object=self # Use FtpServer.log() for messages
565
self._port = self._ftp_server.getsockname()[1]
566
# Don't let it loop forever, or handle an infinite number of requests.
567
# In this case it will run for 100s, or 1000 requests
568
self._async_thread = threading.Thread(target=asyncore.loop,
569
kwargs={'timeout':0.1, 'count':1000})
570
self._async_thread.setDaemon(True)
571
self._async_thread.start()
574
"""See bzrlib.transport.Server.tearDown."""
575
# have asyncore release the channel
576
self._ftp_server.del_channel()
578
self._async_thread.join()
583
_test_authorizer = None
587
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
590
import medusa.filesys
591
import medusa.ftp_server
597
class test_authorizer(object):
598
"""A custom Authorizer object for running the test suite.
600
The reason we cannot use dummy_authorizer, is because it sets the
601
channel to readonly, which we don't always want to do.
604
def __init__(self, root):
607
def authorize(self, channel, username, password):
608
"""Return (success, reply_string, filesystem)"""
610
return 0, 'No Medusa.', None
612
channel.persona = -1, -1
613
if username == 'anonymous':
614
channel.read_only = 1
616
channel.read_only = 0
618
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
621
class ftp_channel(medusa.ftp_server.ftp_channel):
622
"""Customized ftp channel"""
624
def log(self, message):
625
"""Redirect logging requests."""
626
mutter('_ftp_channel: %s', message)
628
def log_info(self, message, type='info'):
629
"""Redirect logging requests."""
630
mutter('_ftp_channel %s: %s', type, message)
632
def cmd_rnfr(self, line):
633
"""Prepare for renaming a file."""
634
self._renaming = line[1]
635
self.respond('350 Ready for RNTO')
636
# TODO: jam 20060516 in testing, the ftp server seems to
637
# check that the file already exists, or it sends
638
# 550 RNFR command failed
640
def cmd_rnto(self, line):
641
"""Rename a file based on the target given.
643
rnto must be called after calling rnfr.
645
if not self._renaming:
646
self.respond('503 RNFR required first.')
647
pfrom = self.filesystem.translate(self._renaming)
648
self._renaming = None
649
pto = self.filesystem.translate(line[1])
651
os.rename(pfrom, pto)
652
except (IOError, OSError), e:
653
# TODO: jam 20060516 return custom responses based on
654
# why the command failed
655
self.respond('550 RNTO failed: %s' % (e,))
657
self.respond('550 RNTO failed')
658
# For a test server, we will go ahead and just die
661
self.respond('250 Rename successful.')
663
def cmd_size(self, line):
664
"""Return the size of a file
666
This is overloaded to help the test suite determine if the
667
target is a directory.
670
if not self.filesystem.isfile(filename):
671
if self.filesystem.isdir(filename):
672
self.respond('550 "%s" is a directory' % (filename,))
674
self.respond('550 "%s" is not a file' % (filename,))
676
self.respond('213 %d'
677
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
679
def cmd_mkd(self, line):
680
"""Create a directory.
682
Overloaded because default implementation does not distinguish
683
*why* it cannot make a directory.
686
self.command_not_understood(''.join(line))
690
self.filesystem.mkdir (path)
691
self.respond ('257 MKD command successful.')
692
except (IOError, OSError), e:
693
self.respond ('550 error creating directory: %s' % (e,))
695
self.respond ('550 error creating directory.')
698
class ftp_server(medusa.ftp_server.ftp_server):
699
"""Customize the behavior of the Medusa ftp_server.
701
There are a few warts on the ftp_server, based on how it expects
705
ftp_channel_class = ftp_channel
707
def __init__(self, *args, **kwargs):
708
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
709
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
711
def log(self, message):
712
"""Redirect logging requests."""
713
mutter('_ftp_server: %s', message)
715
def log_info(self, message, type='info'):
716
"""Override the asyncore.log_info so we don't stipple the screen."""
717
mutter('_ftp_server %s: %s', type, message)
719
_test_authorizer = test_authorizer
720
_ftp_channel = ftp_channel
721
_ftp_server = ftp_server
726
def get_test_permutations():
727
"""Return the permutations to be used in testing."""
728
if not _setup_medusa():
729
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
732
return [(FtpTransport, FtpServer)]