221
140
# Possibly, we could use urlparse.urljoin() here, but
222
141
# I'm concerned about when it chooses to strip the last
223
142
# portion of the path, and when it doesn't.
225
# XXX: It seems that ftplib does not handle Unicode paths
226
# at the same time, medusa won't handle utf8 paths
227
# So if we .encode(utf8) here, then we get a Server failure.
228
# while if we use str(), we get a UnicodeError, and the test suite
229
# just skips testing UnicodePaths.
230
return str('/'.join(basepath) or '/')
143
return '/'.join(basepath)
232
145
def abspath(self, relpath):
233
146
"""Return the full url to the given relative path.
234
147
This can be supplied with a string or a list
236
149
path = self._abspath(relpath)
237
return self._unparse_url(path)
150
return urlparse.urlunparse((self._proto,
151
self._host, path, '', '', ''))
239
153
def has(self, relpath):
240
"""Does the target location exist?"""
241
# FIXME jam 20060516 We *do* ask about directories in the test suite
242
# We don't seem to in the actual codebase
243
# XXX: I assume we're never asked has(dirname) and thus I use
244
# the FTP size command and assume that if it doesn't raise,
246
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,
248
161
f = self._get_FTP()
249
mutter('FTP has check: %s => %s', relpath, abspath)
251
mutter("FTP has: %s", abspath)
162
s = f.size(self._abspath(relpath))
163
mutter("FTP has: %s" % self._abspath(relpath))
253
except ftplib.error_perm, e:
254
if ('is a directory' in str(e).lower()):
255
mutter("FTP has dir: %s: %s", abspath, e)
257
mutter("FTP has not: %s: %s", abspath, e)
165
except ftplib.error_perm:
166
mutter("FTP has not: %s" % self._abspath(relpath))
260
def get(self, relpath, decode=False, retries=0):
169
def get(self, relpath, decode=False):
261
170
"""Get the file at the given relative path.
263
172
:param relpath: The relative path to the file
264
:param retries: Number of retries after temporary failures so far
267
174
We're meant to return a file-like object which bzr will
268
175
then read from. For now we do this via the magic of StringIO
270
# TODO: decode should be deprecated
272
mutter("FTP get: %s", self._abspath(relpath))
178
mutter("FTP get: %s" % self._abspath(relpath))
273
179
f = self._get_FTP()
275
181
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
278
184
except ftplib.error_perm, e:
279
raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
280
except ftplib.error_temp, e:
281
if retries > _number_of_retries:
282
raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
283
% self.abspath(relpath),
286
warning("FTP temporary error: %s. Retrying.", str(e))
287
self._FTP_instance = None
288
return self.get(relpath, decode, retries+1)
290
if retries > _number_of_retries:
291
raise errors.TransportError("FTP control connection closed during GET %s."
292
% self.abspath(relpath),
295
warning("FTP control connection closed. Trying to reopen.")
296
time.sleep(_sleep_between_retries)
297
self._FTP_instance = None
298
return self.get(relpath, decode, retries+1)
185
raise NoSuchFile(msg="Error retrieving %s: %s"
186
% (self.abspath(relpath), str(e)),
300
def put_file(self, relpath, fp, mode=None, retries=0):
189
def put(self, relpath, fp):
301
190
"""Copy the file-like or string object into the location.
303
192
:param relpath: Location to put the contents, relative to base.
304
:param fp: File-like or string object.
305
:param retries: Number of retries after temporary failures so far
308
TODO: jam 20051215 ftp as a protocol seems to support chmod, but
193
:param f: File-like or string object.
311
abspath = self._abspath(relpath)
312
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
313
os.getpid(), random.randint(0,0x7FFFFFFF))
314
if getattr(fp, 'read', None) is None:
195
if not hasattr(fp, 'read'):
315
196
fp = StringIO(fp)
317
mutter("FTP put: %s", abspath)
198
mutter("FTP put: %s" % self._abspath(relpath))
318
199
f = self._get_FTP()
320
f.storbinary('STOR '+tmp_abspath, fp)
321
self._rename_and_overwrite(tmp_abspath, abspath, f)
322
except (ftplib.error_temp,EOFError), e:
323
warning("Failure during ftp PUT. Deleting temporary file.")
325
f.delete(tmp_abspath)
327
warning("Failed to delete temporary file on the"
328
" server.\nFile: %s", tmp_abspath)
200
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
331
201
except ftplib.error_perm, e:
332
self._translate_perm_error(e, abspath, extra='could not store')
333
except ftplib.error_temp, e:
334
if retries > _number_of_retries:
335
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
336
% self.abspath(relpath), orig_error=e)
338
warning("FTP temporary error: %s. Retrying.", str(e))
339
self._FTP_instance = None
340
self.put_file(relpath, fp, mode, retries+1)
342
if retries > _number_of_retries:
343
raise errors.TransportError("FTP control connection closed during PUT %s."
344
% self.abspath(relpath), orig_error=e)
346
warning("FTP control connection closed. Trying to reopen.")
347
time.sleep(_sleep_between_retries)
348
self._FTP_instance = None
349
self.put_file(relpath, fp, mode, retries+1)
202
raise FtpTransportError(orig_error=e)
351
def mkdir(self, relpath, mode=None):
204
def mkdir(self, relpath):
352
205
"""Create a directory at the given path."""
353
abspath = self._abspath(relpath)
355
mutter("FTP mkd: %s", abspath)
358
except ftplib.error_perm, e:
359
self._translate_perm_error(e, abspath,
360
unknown_exc=errors.FileExists)
362
def rmdir(self, rel_path):
363
"""Delete the directory at rel_path"""
364
abspath = self._abspath(rel_path)
366
mutter("FTP rmd: %s", abspath)
369
except ftplib.error_perm, e:
370
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
372
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):
373
223
"""Append the text in the file-like object into the final
376
abspath = self._abspath(relpath)
377
if self.has(relpath):
378
ftp = self._get_FTP()
379
result = ftp.size(abspath)
383
mutter("FTP appe to %s", abspath)
384
self._try_append(relpath, f.read(), mode)
388
def _try_append(self, relpath, text, mode=None, retries=0):
389
"""Try repeatedly to append the given text to the file at relpath.
391
This is a recursive function. On errors, it will be called until the
392
number of retries is exceeded.
395
abspath = self._abspath(relpath)
396
mutter("FTP appe (try %d) to %s", retries, abspath)
397
ftp = self._get_FTP()
398
ftp.voidcmd("TYPE I")
399
cmd = "APPE %s" % abspath
400
conn = ftp.transfercmd(cmd)
404
self._setmode(relpath, mode)
406
except ftplib.error_perm, e:
407
self._translate_perm_error(e, abspath, extra='error appending',
408
unknown_exc=errors.NoSuchFile)
409
except ftplib.error_temp, e:
410
if retries > _number_of_retries:
411
raise errors.TransportError("FTP temporary error during APPEND %s." \
412
"Aborting." % abspath, orig_error=e)
414
warning("FTP temporary error: %s. Retrying.", str(e))
415
self._FTP_instance = None
416
self._try_append(relpath, text, mode, retries+1)
418
def _setmode(self, relpath, mode):
419
"""Set permissions on a path.
421
Only set permissions if the FTP server supports the 'SITE CHMOD'
425
mutter("FTP site chmod: setting permissions to %s on %s",
426
str(mode), self._abspath(relpath))
427
ftp = self._get_FTP()
428
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
430
except ftplib.error_perm, e:
431
# Command probably not available on this server
432
warning("FTP Could not set permissions to %s on %s. %s",
433
str(mode), self._abspath(relpath), str(e))
435
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
436
# to copy something to another machine. And you may be able
437
# to give it its own address as the 'to' location.
438
# So implement a fancier 'copy()'
440
def rename(self, rel_from, rel_to):
441
abs_from = self._abspath(rel_from)
442
abs_to = self._abspath(rel_to)
443
mutter("FTP rename: %s => %s", abs_from, abs_to)
445
return self._rename(abs_from, abs_to, f)
447
def _rename(self, abs_from, abs_to, f):
449
f.rename(abs_from, abs_to)
450
except ftplib.error_perm, e:
451
self._translate_perm_error(e, abs_from,
452
': unable to rename to %r' % (abs_to))
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()')
454
232
def move(self, rel_from, rel_to):
455
233
"""Move the item at rel_from to the location at rel_to"""
456
abs_from = self._abspath(rel_from)
457
abs_to = self._abspath(rel_to)
459
mutter("FTP mv: %s => %s", abs_from, abs_to)
235
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
236
self._abspath(rel_to)))
460
237
f = self._get_FTP()
461
self._rename_and_overwrite(abs_from, abs_to, f)
238
f.rename(self._abspath(rel_from), self._abspath(rel_to))
462
239
except ftplib.error_perm, e:
463
self._translate_perm_error(e, abs_from,
464
extra='unable to rename to %r' % (rel_to,),
465
unknown_exc=errors.PathError)
467
def _rename_and_overwrite(self, abs_from, abs_to, f):
468
"""Do a fancy rename on the remote server.
470
Using the implementation provided by osutils.
472
osutils.fancy_rename(abs_from, abs_to,
473
rename_func=lambda p1, p2: self._rename(p1, p2, f),
474
unlink_func=lambda p: self._delete(p, f))
240
raise FtpTransportError(orig_error=e)
476
242
def delete(self, relpath):
477
243
"""Delete the item at relpath"""
478
abspath = self._abspath(relpath)
480
self._delete(abspath, f)
482
def _delete(self, abspath, f):
484
mutter("FTP rm: %s", abspath)
245
mutter("FTP rm: %s" % self._abspath(relpath))
247
f.delete(self._abspath(relpath))
486
248
except ftplib.error_perm, e:
487
self._translate_perm_error(e, abspath, 'error deleting',
488
unknown_exc=errors.NoSuchFile)
249
raise FtpTransportError(orig_error=e)
490
251
def listable(self):
491
252
"""See Transport.listable."""
554
311
:return: A lock object, which should be passed to Transport.unlock()
556
313
return self.lock_read(relpath)
559
class FtpServer(Server):
560
"""Common code for SFTP server facilities."""
564
self._ftp_server = None
566
self._async_thread = None
571
"""Calculate an ftp url to this server."""
572
return 'ftp://foo:bar@localhost:%d/' % (self._port)
574
# def get_bogus_url(self):
575
# """Return a URL which cannot be connected to."""
576
# return 'ftp://127.0.0.1:1'
578
def log(self, message):
579
"""This is used by medusa.ftp_server to log connections, etc."""
580
self.logs.append(message)
582
def setUp(self, vfs_server=None):
584
raise RuntimeError('Must have medusa to run the FtpServer')
586
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
587
"FtpServer currently assumes local transport, got %s" % vfs_server
589
self._root = os.getcwdu()
590
self._ftp_server = _ftp_server(
591
authorizer=_test_authorizer(root=self._root),
593
port=0, # bind to a random port
595
logger_object=self # Use FtpServer.log() for messages
597
self._port = self._ftp_server.getsockname()[1]
598
# Don't let it loop forever, or handle an infinite number of requests.
599
# In this case it will run for 100s, or 1000 requests
600
self._async_thread = threading.Thread(
601
target=FtpServer._asyncore_loop_ignore_EBADF,
602
kwargs={'timeout':0.1, 'count':1000})
603
self._async_thread.setDaemon(True)
604
self._async_thread.start()
607
"""See bzrlib.transport.Server.tearDown."""
608
# have asyncore release the channel
609
self._ftp_server.del_channel()
611
self._async_thread.join()
614
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
615
"""Ignore EBADF during server shutdown.
617
We close the socket to get the server to shutdown, but this causes
618
select.select() to raise EBADF.
621
asyncore.loop(*args, **kwargs)
622
except select.error, e:
623
if e.args[0] != errno.EBADF:
629
_test_authorizer = None
633
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
636
import medusa.filesys
637
import medusa.ftp_server
643
class test_authorizer(object):
644
"""A custom Authorizer object for running the test suite.
646
The reason we cannot use dummy_authorizer, is because it sets the
647
channel to readonly, which we don't always want to do.
650
def __init__(self, root):
653
def authorize(self, channel, username, password):
654
"""Return (success, reply_string, filesystem)"""
656
return 0, 'No Medusa.', None
658
channel.persona = -1, -1
659
if username == 'anonymous':
660
channel.read_only = 1
662
channel.read_only = 0
664
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
667
class ftp_channel(medusa.ftp_server.ftp_channel):
668
"""Customized ftp channel"""
670
def log(self, message):
671
"""Redirect logging requests."""
672
mutter('_ftp_channel: %s', message)
674
def log_info(self, message, type='info'):
675
"""Redirect logging requests."""
676
mutter('_ftp_channel %s: %s', type, message)
678
def cmd_rnfr(self, line):
679
"""Prepare for renaming a file."""
680
self._renaming = line[1]
681
self.respond('350 Ready for RNTO')
682
# TODO: jam 20060516 in testing, the ftp server seems to
683
# check that the file already exists, or it sends
684
# 550 RNFR command failed
686
def cmd_rnto(self, line):
687
"""Rename a file based on the target given.
689
rnto must be called after calling rnfr.
691
if not self._renaming:
692
self.respond('503 RNFR required first.')
693
pfrom = self.filesystem.translate(self._renaming)
694
self._renaming = None
695
pto = self.filesystem.translate(line[1])
696
if os.path.exists(pto):
697
self.respond('550 RNTO failed: file exists')
700
os.rename(pfrom, pto)
701
except (IOError, OSError), e:
702
# TODO: jam 20060516 return custom responses based on
703
# why the command failed
704
# (bialix 20070418) str(e) on Python 2.5 @ Windows
705
# sometimes don't provide expected error message;
706
# so we obtain such message via os.strerror()
707
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
709
self.respond('550 RNTO failed')
710
# For a test server, we will go ahead and just die
713
self.respond('250 Rename successful.')
715
def cmd_size(self, line):
716
"""Return the size of a file
718
This is overloaded to help the test suite determine if the
719
target is a directory.
722
if not self.filesystem.isfile(filename):
723
if self.filesystem.isdir(filename):
724
self.respond('550 "%s" is a directory' % (filename,))
726
self.respond('550 "%s" is not a file' % (filename,))
728
self.respond('213 %d'
729
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
731
def cmd_mkd(self, line):
732
"""Create a directory.
734
Overloaded because default implementation does not distinguish
735
*why* it cannot make a directory.
738
self.command_not_understood(''.join(line))
742
self.filesystem.mkdir (path)
743
self.respond ('257 MKD command successful.')
744
except (IOError, OSError), e:
745
# (bialix 20070418) str(e) on Python 2.5 @ Windows
746
# sometimes don't provide expected error message;
747
# so we obtain such message via os.strerror()
748
self.respond ('550 error creating directory: %s' %
749
os.strerror(e.errno))
751
self.respond ('550 error creating directory.')
754
class ftp_server(medusa.ftp_server.ftp_server):
755
"""Customize the behavior of the Medusa ftp_server.
757
There are a few warts on the ftp_server, based on how it expects
761
ftp_channel_class = ftp_channel
763
def __init__(self, *args, **kwargs):
764
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
765
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
767
def log(self, message):
768
"""Redirect logging requests."""
769
mutter('_ftp_server: %s', message)
771
def log_info(self, message, type='info'):
772
"""Override the asyncore.log_info so we don't stipple the screen."""
773
mutter('_ftp_server %s: %s', type, message)
775
_test_authorizer = test_authorizer
776
_ftp_channel = ftp_channel
777
_ftp_server = ftp_server
782
def get_test_permutations():
783
"""Return the permutations to be used in testing."""
784
if not _setup_medusa():
785
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
788
return [(FtpTransport, FtpServer)]