212
135
# Possibly, we could use urlparse.urljoin() here, but
213
136
# I'm concerned about when it chooses to strip the last
214
137
# portion of the path, and when it doesn't.
215
return '/'.join(basepath) or '/'
138
return '/'.join(basepath)
217
140
def abspath(self, relpath):
218
141
"""Return the full url to the given relative path.
219
142
This can be supplied with a string or a list
221
144
path = self._abspath(relpath)
222
return self._unparse_url(path)
145
return urlparse.urlunparse((self._proto,
146
self._host, path, '', '', ''))
224
148
def has(self, relpath):
225
"""Does the target location exist?"""
226
# FIXME jam 20060516 We *do* ask about directories in the test suite
227
# We don't seem to in the actual codebase
228
# XXX: I assume we're never asked has(dirname) and thus I use
229
# the FTP size command and assume that if it doesn't raise,
231
abspath = self._abspath(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,
233
156
f = self._get_FTP()
234
mutter('FTP has check: %s => %s', relpath, abspath)
236
mutter("FTP has: %s", abspath)
157
s = f.size(self._abspath(relpath))
158
mutter("FTP has: %s" % self._abspath(relpath))
238
except ftplib.error_perm, e:
239
if ('is a directory' in str(e).lower()):
240
mutter("FTP has dir: %s: %s", abspath, e)
242
mutter("FTP has not: %s: %s", abspath, e)
160
except ftplib.error_perm:
161
mutter("FTP has not: %s" % self._abspath(relpath))
245
def get(self, relpath, decode=False, retries=0):
164
def get(self, relpath, decode=False):
246
165
"""Get the file at the given relative path.
248
167
:param relpath: The relative path to the file
249
:param retries: Number of retries after temporary failures so far
252
169
We're meant to return a file-like object which bzr will
253
170
then read from. For now we do this via the magic of StringIO
255
# TODO: decode should be deprecated
257
mutter("FTP get: %s", self._abspath(relpath))
173
mutter("FTP get: %s" % self._abspath(relpath))
258
174
f = self._get_FTP()
260
176
f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
263
179
except ftplib.error_perm, e:
264
raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
265
except ftplib.error_temp, e:
266
if retries > _number_of_retries:
267
raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
268
% self.abspath(relpath),
271
warning("FTP temporary error: %s. Retrying.", str(e))
272
self._FTP_instance = None
273
return self.get(relpath, decode, retries+1)
275
if retries > _number_of_retries:
276
raise errors.TransportError("FTP control connection closed during GET %s."
277
% self.abspath(relpath),
280
warning("FTP control connection closed. Trying to reopen.")
281
time.sleep(_sleep_between_retries)
282
self._FTP_instance = None
283
return self.get(relpath, decode, retries+1)
180
raise NoSuchFile(self.abspath(relpath), extra=str(e))
285
def put(self, relpath, fp, mode=None, retries=0):
182
def put(self, relpath, fp, mode=None):
286
183
"""Copy the file-like or string object into the location.
288
185
:param relpath: Location to put the contents, relative to base.
289
:param fp: File-like or string object.
290
:param retries: Number of retries after temporary failures so far
186
:param f: File-like or string object.
187
TODO: jam 20051215 This should be an atomic put, not overwritting files in place
293
188
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
295
abspath = self._abspath(relpath)
296
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
os.getpid(), random.randint(0,0x7FFFFFFF))
298
190
if not hasattr(fp, 'read'):
299
191
fp = StringIO(fp)
301
mutter("FTP put: %s", abspath)
193
mutter("FTP put: %s" % self._abspath(relpath))
302
194
f = self._get_FTP()
304
f.storbinary('STOR '+tmp_abspath, fp)
305
f.rename(tmp_abspath, abspath)
306
except (ftplib.error_temp,EOFError), e:
307
warning("Failure during ftp PUT. Deleting temporary file.")
309
f.delete(tmp_abspath)
311
warning("Failed to delete temporary file on the"
312
" server.\nFile: %s", tmp_abspath)
195
f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
315
196
except ftplib.error_perm, e:
316
self._translate_perm_error(e, abspath, extra='could not store')
317
except ftplib.error_temp, e:
318
if retries > _number_of_retries:
319
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
320
% self.abspath(relpath), orig_error=e)
322
warning("FTP temporary error: %s. Retrying.", str(e))
323
self._FTP_instance = None
324
self.put(relpath, fp, mode, retries+1)
326
if retries > _number_of_retries:
327
raise errors.TransportError("FTP control connection closed during PUT %s."
328
% self.abspath(relpath), orig_error=e)
330
warning("FTP control connection closed. Trying to reopen.")
331
time.sleep(_sleep_between_retries)
332
self._FTP_instance = None
333
self.put(relpath, fp, mode, retries+1)
197
raise TransportError(orig_error=e)
335
199
def mkdir(self, relpath, mode=None):
336
200
"""Create a directory at the given path."""
337
abspath = self._abspath(relpath)
339
mutter("FTP mkd: %s", abspath)
342
except ftplib.error_perm, e:
343
self._translate_perm_error(e, abspath,
344
unknown_exc=errors.FileExists)
346
def rmdir(self, rel_path):
347
"""Delete the directory at rel_path"""
348
abspath = self._abspath(rel_path)
350
mutter("FTP rmd: %s", abspath)
353
except ftplib.error_perm, e:
354
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
356
def append(self, relpath, f, mode=None):
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):
357
216
"""Append the text in the file-like object into the final
360
abspath = self._abspath(relpath)
361
if self.has(relpath):
362
ftp = self._get_FTP()
363
result = ftp.size(abspath)
367
mutter("FTP appe to %s", abspath)
368
self._try_append(relpath, f.read(), mode)
372
def _try_append(self, relpath, text, mode=None, retries=0):
373
"""Try repeatedly to append the given text to the file at relpath.
375
This is a recursive function. On errors, it will be called until the
376
number of retries is exceeded.
379
abspath = self._abspath(relpath)
380
mutter("FTP appe (try %d) to %s", retries, abspath)
381
ftp = self._get_FTP()
382
ftp.voidcmd("TYPE I")
383
cmd = "APPE %s" % abspath
384
conn = ftp.transfercmd(cmd)
388
self._setmode(relpath, mode)
390
except ftplib.error_perm, e:
391
self._translate_perm_error(e, abspath, extra='error appending',
392
unknown_exc=errors.NoSuchFile)
393
except ftplib.error_temp, e:
394
if retries > _number_of_retries:
395
raise errors.TransportError("FTP temporary error during APPEND %s." \
396
"Aborting." % abspath, orig_error=e)
398
warning("FTP temporary error: %s. Retrying.", str(e))
399
self._FTP_instance = None
400
self._try_append(relpath, text, mode, retries+1)
402
def _setmode(self, relpath, mode):
403
"""Set permissions on a path.
405
Only set permissions if the FTP server supports the 'SITE CHMOD'
409
mutter("FTP site chmod: setting permissions to %s on %s",
410
str(mode), self._abspath(relpath))
411
ftp = self._get_FTP()
412
cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
414
except ftplib.error_perm, e:
415
# Command probably not available on this server
416
warning("FTP Could not set permissions to %s on %s. %s",
417
str(mode), self._abspath(relpath), str(e))
419
# TODO: jam 20060516 I believe ftp allows you to tell an ftp server
420
# to copy something to another machine. And you may be able
421
# to give it its own address as the 'to' location.
422
# So implement a fancier 'copy()'
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()')
424
225
def move(self, rel_from, rel_to):
425
226
"""Move the item at rel_from to the location at rel_to"""
426
abs_from = self._abspath(rel_from)
427
abs_to = self._abspath(rel_to)
429
mutter("FTP mv: %s => %s", abs_from, abs_to)
228
mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
229
self._abspath(rel_to)))
430
230
f = self._get_FTP()
431
f.rename(abs_from, abs_to)
231
f.rename(self._abspath(rel_from), self._abspath(rel_to))
432
232
except ftplib.error_perm, e:
433
self._translate_perm_error(e, abs_from,
434
extra='unable to rename to %r' % (rel_to,),
435
unknown_exc=errors.PathError)
233
raise TransportError(orig_error=e)
439
235
def delete(self, relpath):
440
236
"""Delete the item at relpath"""
441
abspath = self._abspath(relpath)
443
mutter("FTP rm: %s", abspath)
238
mutter("FTP rm: %s" % self._abspath(relpath))
444
239
f = self._get_FTP()
240
f.delete(self._abspath(relpath))
446
241
except ftplib.error_perm, e:
447
self._translate_perm_error(e, abspath, 'error deleting',
448
unknown_exc=errors.NoSuchFile)
242
raise TransportError(orig_error=e)
450
244
def listable(self):
451
245
"""See Transport.listable."""
513
306
return self.lock_read(relpath)
516
class FtpServer(Server):
517
"""Common code for SFTP server facilities."""
521
self._ftp_server = None
523
self._async_thread = None
528
"""Calculate an ftp url to this server."""
529
return 'ftp://foo:bar@localhost:%d/' % (self._port)
531
# def get_bogus_url(self):
532
# """Return a URL which cannot be connected to."""
533
# return 'ftp://127.0.0.1:1'
535
def log(self, message):
536
"""This is used by medusa.ftp_server to log connections, etc."""
537
self.logs.append(message)
542
raise RuntimeError('Must have medusa to run the FtpServer')
544
self._root = os.getcwdu()
545
self._ftp_server = _ftp_server(
546
authorizer=_test_authorizer(root=self._root),
548
port=0, # bind to a random port
550
logger_object=self # Use FtpServer.log() for messages
552
self._port = self._ftp_server.getsockname()[1]
553
# Don't let it loop forever, or handle an infinite number of requests.
554
# In this case it will run for 100s, or 1000 requests
555
self._async_thread = threading.Thread(target=asyncore.loop,
556
kwargs={'timeout':0.1, 'count':1000})
557
self._async_thread.setDaemon(True)
558
self._async_thread.start()
561
"""See bzrlib.transport.Server.tearDown."""
562
# have asyncore release the channel
563
self._ftp_server.del_channel()
565
self._async_thread.join()
570
_test_authorizer = None
574
global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
577
import medusa.filesys
578
import medusa.ftp_server
584
class test_authorizer(object):
585
"""A custom Authorizer object for running the test suite.
587
The reason we cannot use dummy_authorizer, is because it sets the
588
channel to readonly, which we don't always want to do.
591
def __init__(self, root):
594
def authorize(self, channel, username, password):
595
"""Return (success, reply_string, filesystem)"""
597
return 0, 'No Medusa.', None
599
channel.persona = -1, -1
600
if username == 'anonymous':
601
channel.read_only = 1
603
channel.read_only = 0
605
return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
608
class ftp_channel(medusa.ftp_server.ftp_channel):
609
"""Customized ftp channel"""
611
def log(self, message):
612
"""Redirect logging requests."""
613
mutter('_ftp_channel: %s', message)
615
def log_info(self, message, type='info'):
616
"""Redirect logging requests."""
617
mutter('_ftp_channel %s: %s', type, message)
619
def cmd_rnfr(self, line):
620
"""Prepare for renaming a file."""
621
self._renaming = line[1]
622
self.respond('350 Ready for RNTO')
623
# TODO: jam 20060516 in testing, the ftp server seems to
624
# check that the file already exists, or it sends
625
# 550 RNFR command failed
627
def cmd_rnto(self, line):
628
"""Rename a file based on the target given.
630
rnto must be called after calling rnfr.
632
if not self._renaming:
633
self.respond('503 RNFR required first.')
634
pfrom = self.filesystem.translate(self._renaming)
635
self._renaming = None
636
pto = self.filesystem.translate(line[1])
638
os.rename(pfrom, pto)
639
except (IOError, OSError), e:
640
# TODO: jam 20060516 return custom responses based on
641
# why the command failed
642
self.respond('550 RNTO failed: %s' % (e,))
644
self.respond('550 RNTO failed')
645
# For a test server, we will go ahead and just die
648
self.respond('250 Rename successful.')
650
def cmd_size(self, line):
651
"""Return the size of a file
653
This is overloaded to help the test suite determine if the
654
target is a directory.
657
if not self.filesystem.isfile(filename):
658
if self.filesystem.isdir(filename):
659
self.respond('550 "%s" is a directory' % (filename,))
661
self.respond('550 "%s" is not a file' % (filename,))
663
self.respond('213 %d'
664
% (self.filesystem.stat(filename)[stat.ST_SIZE]),)
666
def cmd_mkd(self, line):
667
"""Create a directory.
669
Overloaded because default implementation does not distinguish
670
*why* it cannot make a directory.
673
self.command_not_understood (string.join (line))
677
self.filesystem.mkdir (path)
678
self.respond ('257 MKD command successful.')
679
except (IOError, OSError), e:
680
self.respond ('550 error creating directory: %s' % (e,))
682
self.respond ('550 error creating directory.')
685
class ftp_server(medusa.ftp_server.ftp_server):
686
"""Customize the behavior of the Medusa ftp_server.
688
There are a few warts on the ftp_server, based on how it expects
692
ftp_channel_class = ftp_channel
694
def __init__(self, *args, **kwargs):
695
mutter('Initializing _ftp_server: %r, %r', args, kwargs)
696
medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
698
def log(self, message):
699
"""Redirect logging requests."""
700
mutter('_ftp_server: %s', message)
702
def log_info(self, message, type='info'):
703
"""Override the asyncore.log_info so we don't stipple the screen."""
704
mutter('_ftp_server %s: %s', type, message)
706
_test_authorizer = test_authorizer
707
_ftp_channel = ftp_channel
708
_ftp_server = ftp_server
713
309
def get_test_permutations():
714
310
"""Return the permutations to be used in testing."""
715
if not _setup_medusa():
716
warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
719
return [(FtpTransport, FtpServer)]
311
warn("There are no FTP transport provider tests yet.")