1
# Copyright (C) 2005 Canonical Ltd
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
38
40
from warnings import warn
47
from bzrlib.trace import mutter, warning
40
48
from bzrlib.transport import (
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter, warning
53
from bzrlib.transport.local import LocalURLServer
49
56
_have_medusa = False
122
129
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
123
130
if self._port is not None:
124
131
netloc = '%s:%d' % (netloc, self._port)
125
return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
135
return urlparse.urlunparse((proto, netloc, path, '', '', ''))
127
137
def _get_FTP(self):
128
138
"""Return the ftplib.FTP instance for this object."""
190
201
def _abspath(self, relpath):
191
202
assert isinstance(relpath, basestring)
192
relpath = urllib.unquote(relpath)
193
relpath_parts = relpath.split('/')
194
if len(relpath_parts) > 1:
195
if relpath_parts[0] == '':
196
raise ValueError("path %r within branch %r seems to be absolute"
197
% (relpath, self._path))
198
basepath = self._path.split('/')
203
relpath = urlutils.unescape(relpath)
204
if relpath.startswith('/'):
207
basepath = self._path.split('/')
199
208
if len(basepath) > 0 and basepath[-1] == '':
200
209
basepath = basepath[:-1]
201
for p in relpath_parts:
210
for p in relpath.split('/'):
203
212
if len(basepath) == 0:
204
213
# In most filesystems, a request for the parent
212
221
# Possibly, we could use urlparse.urljoin() here, but
213
222
# I'm concerned about when it chooses to strip the last
214
223
# portion of the path, and when it doesn't.
215
return '/'.join(basepath) or '/'
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 '/')
217
232
def abspath(self, relpath):
218
233
"""Return the full url to the given relative path.
282
297
self._FTP_instance = None
283
298
return self.get(relpath, decode, retries+1)
285
def put(self, relpath, fp, mode=None, retries=0):
300
def put_file(self, relpath, fp, mode=None, retries=0):
286
301
"""Copy the file-like or string object into the location.
288
303
:param relpath: Location to put the contents, relative to base.
290
305
:param retries: Number of retries after temporary failures so far
291
306
for this operation.
293
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
308
TODO: jam 20051215 ftp as a protocol seems to support chmod, but
295
311
abspath = self._abspath(relpath)
296
312
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
313
os.getpid(), random.randint(0,0x7FFFFFFF))
298
if not hasattr(fp, 'read'):
314
if getattr(fp, 'read', None) is None:
299
315
fp = StringIO(fp)
301
317
mutter("FTP put: %s", abspath)
302
318
f = self._get_FTP()
304
320
f.storbinary('STOR '+tmp_abspath, fp)
305
f.rename(tmp_abspath, abspath)
321
self._rename_and_overwrite(tmp_abspath, abspath, f)
306
322
except (ftplib.error_temp,EOFError), e:
307
323
warning("Failure during ftp PUT. Deleting temporary file.")
322
338
warning("FTP temporary error: %s. Retrying.", str(e))
323
339
self._FTP_instance = None
324
self.put(relpath, fp, mode, retries+1)
340
self.put_file(relpath, fp, mode, retries+1)
326
342
if retries > _number_of_retries:
327
343
raise errors.TransportError("FTP control connection closed during PUT %s."
330
346
warning("FTP control connection closed. Trying to reopen.")
331
347
time.sleep(_sleep_between_retries)
332
348
self._FTP_instance = None
333
self.put(relpath, fp, mode, retries+1)
349
self.put_file(relpath, fp, mode, retries+1)
335
351
def mkdir(self, relpath, mode=None):
336
352
"""Create a directory at the given path."""
353
369
except ftplib.error_perm, e:
354
370
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
356
def append(self, relpath, f, mode=None):
372
def append_file(self, relpath, f, mode=None):
357
373
"""Append the text in the file-like object into the final
421
437
# to give it its own address as the 'to' location.
422
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))
424
454
def move(self, rel_from, rel_to):
425
455
"""Move the item at rel_from to the location at rel_to"""
426
456
abs_from = self._abspath(rel_from)
429
459
mutter("FTP mv: %s => %s", abs_from, abs_to)
430
460
f = self._get_FTP()
431
f.rename(abs_from, abs_to)
461
self._rename_and_overwrite(abs_from, abs_to, f)
432
462
except ftplib.error_perm, e:
433
463
self._translate_perm_error(e, abs_from,
434
464
extra='unable to rename to %r' % (rel_to,),
435
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))
439
476
def delete(self, relpath):
440
477
"""Delete the item at relpath"""
441
478
abspath = self._abspath(relpath)
480
self._delete(abspath, f)
482
def _delete(self, abspath, f):
443
484
mutter("FTP rm: %s", abspath)
445
485
f.delete(abspath)
446
486
except ftplib.error_perm, e:
447
487
self._translate_perm_error(e, abspath, 'error deleting',
454
494
def list_dir(self, relpath):
455
495
"""See Transport.list_dir."""
496
basepath = self._abspath(relpath)
497
mutter("FTP nlst: %s", basepath)
457
mutter("FTP nlst: %s", self._abspath(relpath))
459
basepath = self._abspath(relpath)
460
500
paths = f.nlst(basepath)
461
# If FTP.nlst returns paths prefixed by relpath, strip 'em
462
if paths and paths[0].startswith(basepath):
463
paths = [path[len(basepath)+1:] for path in paths]
464
# Remove . and .. if present, and return
465
return [path for path in paths if path not in (".", "..")]
466
501
except ftplib.error_perm, e:
467
502
self._translate_perm_error(e, relpath, extra='error with list_dir')
503
# If FTP.nlst returns paths prefixed by relpath, strip 'em
504
if paths and paths[0].startswith(basepath):
505
entries = [path[len(basepath)+1:] for path in paths]
508
# Remove . and .. if present
509
return [urlutils.escape(entry) for entry in entries
510
if entry not in ('.', '..')]
469
512
def iter_files_recursive(self):
470
513
"""See Transport.iter_files_recursive.
473
516
mutter("FTP iter_files_recursive")
474
517
queue = list(self.list_dir("."))
476
relpath = urllib.quote(queue.pop(0))
519
relpath = queue.pop(0)
477
520
st = self.stat(relpath)
478
521
if stat.S_ISDIR(st.st_mode):
479
522
for i, basename in enumerate(self.list_dir(relpath)):
536
579
"""This is used by medusa.ftp_server to log connections, etc."""
537
580
self.logs.append(message)
582
def setUp(self, vfs_server=None):
541
583
if not _have_medusa:
542
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
544
589
self._root = os.getcwdu()
545
590
self._ftp_server = _ftp_server(
546
591
authorizer=_test_authorizer(root=self._root),
552
597
self._port = self._ftp_server.getsockname()[1]
553
598
# Don't let it loop forever, or handle an infinite number of requests.
554
599
# In this case it will run for 100s, or 1000 requests
555
self._async_thread = threading.Thread(target=asyncore.loop,
600
self._async_thread = threading.Thread(
601
target=FtpServer._asyncore_loop_ignore_EBADF,
556
602
kwargs={'timeout':0.1, 'count':1000})
557
603
self._async_thread.setDaemon(True)
558
604
self._async_thread.start()
564
610
asyncore.close_all()
565
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:
568
627
_ftp_channel = None
569
628
_ftp_server = None
634
693
pfrom = self.filesystem.translate(self._renaming)
635
694
self._renaming = None
636
695
pto = self.filesystem.translate(line[1])
696
if os.path.exists(pto):
697
self.respond('550 RNTO failed: file exists')
638
700
os.rename(pfrom, pto)
639
701
except (IOError, OSError), e:
640
702
# TODO: jam 20060516 return custom responses based on
641
703
# why the command failed
642
self.respond('550 RNTO failed: %s' % (e,))
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))
644
709
self.respond('550 RNTO failed')
645
710
# For a test server, we will go ahead and just die
670
735
*why* it cannot make a directory.
672
737
if len (line) != 2:
673
self.command_not_understood (string.join (line))
738
self.command_not_understood(''.join(line))
677
742
self.filesystem.mkdir (path)
678
743
self.respond ('257 MKD command successful.')
679
744
except (IOError, OSError), e:
680
self.respond ('550 error creating directory: %s' % (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))
682
751
self.respond ('550 error creating directory.')