1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1
# Copyright (C) 2005 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
40
38
from warnings import warn
47
from bzrlib.trace import mutter, warning
48
40
from bzrlib.transport import (
53
from bzrlib.transport.local import LocalURLServer
45
import bzrlib.errors as errors
46
from bzrlib.trace import mutter, warning
56
49
_have_medusa = False
129
122
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
130
123
if self._port is not None:
131
124
netloc = '%s:%d' % (netloc, self._port)
135
return urlparse.urlunparse((proto, netloc, path, '', '', ''))
125
return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
137
127
def _get_FTP(self):
138
128
"""Return the ftplib.FTP instance for this object."""
202
190
def _abspath(self, relpath):
203
191
assert isinstance(relpath, basestring)
204
relpath = urlutils.unescape(relpath)
205
if relpath.startswith('/'):
208
basepath = self._path.split('/')
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('/')
209
199
if len(basepath) > 0 and basepath[-1] == '':
210
200
basepath = basepath[:-1]
211
for p in relpath.split('/'):
201
for p in relpath_parts:
213
203
if len(basepath) == 0:
214
204
# In most filesystems, a request for the parent
222
212
# Possibly, we could use urlparse.urljoin() here, but
223
213
# I'm concerned about when it chooses to strip the last
224
214
# portion of the path, and when it doesn't.
226
# XXX: It seems that ftplib does not handle Unicode paths
227
# at the same time, medusa won't handle utf8 paths
228
# So if we .encode(utf8) here, then we get a Server failure.
229
# while if we use str(), we get a UnicodeError, and the test suite
230
# just skips testing UnicodePaths.
231
return str('/'.join(basepath) or '/')
215
return '/'.join(basepath) or '/'
233
217
def abspath(self, relpath):
234
218
"""Return the full url to the given relative path.
298
282
self._FTP_instance = None
299
283
return self.get(relpath, decode, retries+1)
301
def put_file(self, relpath, fp, mode=None, retries=0):
285
def put(self, relpath, fp, mode=None, retries=0):
302
286
"""Copy the file-like or string object into the location.
304
288
:param relpath: Location to put the contents, relative to base.
306
290
:param retries: Number of retries after temporary failures so far
307
291
for this operation.
309
TODO: jam 20051215 ftp as a protocol seems to support chmod, but
293
TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
312
295
abspath = self._abspath(relpath)
313
296
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
314
297
os.getpid(), random.randint(0,0x7FFFFFFF))
315
if getattr(fp, 'read', None) is None:
298
if not hasattr(fp, 'read'):
316
299
fp = StringIO(fp)
318
301
mutter("FTP put: %s", abspath)
319
302
f = self._get_FTP()
321
304
f.storbinary('STOR '+tmp_abspath, fp)
322
self._rename_and_overwrite(tmp_abspath, abspath, f)
305
f.rename(tmp_abspath, abspath)
323
306
except (ftplib.error_temp,EOFError), e:
324
307
warning("Failure during ftp PUT. Deleting temporary file.")
332
315
except ftplib.error_perm, e:
333
self._translate_perm_error(e, abspath, extra='could not store',
334
unknown_exc=errors.NoSuchFile)
316
self._translate_perm_error(e, abspath, extra='could not store')
335
317
except ftplib.error_temp, e:
336
318
if retries > _number_of_retries:
337
319
raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
340
322
warning("FTP temporary error: %s. Retrying.", str(e))
341
323
self._FTP_instance = None
342
self.put_file(relpath, fp, mode, retries+1)
324
self.put(relpath, fp, mode, retries+1)
344
326
if retries > _number_of_retries:
345
327
raise errors.TransportError("FTP control connection closed during PUT %s."
348
330
warning("FTP control connection closed. Trying to reopen.")
349
331
time.sleep(_sleep_between_retries)
350
332
self._FTP_instance = None
351
self.put_file(relpath, fp, mode, retries+1)
333
self.put(relpath, fp, mode, retries+1)
353
335
def mkdir(self, relpath, mode=None):
354
336
"""Create a directory at the given path."""
371
353
except ftplib.error_perm, e:
372
354
self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
374
def append_file(self, relpath, f, mode=None):
356
def append(self, relpath, f, mode=None):
375
357
"""Append the text in the file-like object into the final
439
421
# to give it its own address as the 'to' location.
440
422
# So implement a fancier 'copy()'
442
def rename(self, rel_from, rel_to):
443
abs_from = self._abspath(rel_from)
444
abs_to = self._abspath(rel_to)
445
mutter("FTP rename: %s => %s", abs_from, abs_to)
447
return self._rename(abs_from, abs_to, f)
449
def _rename(self, abs_from, abs_to, f):
451
f.rename(abs_from, abs_to)
452
except ftplib.error_perm, e:
453
self._translate_perm_error(e, abs_from,
454
': unable to rename to %r' % (abs_to))
456
424
def move(self, rel_from, rel_to):
457
425
"""Move the item at rel_from to the location at rel_to"""
458
426
abs_from = self._abspath(rel_from)
461
429
mutter("FTP mv: %s => %s", abs_from, abs_to)
462
430
f = self._get_FTP()
463
self._rename_and_overwrite(abs_from, abs_to, f)
431
f.rename(abs_from, abs_to)
464
432
except ftplib.error_perm, e:
465
433
self._translate_perm_error(e, abs_from,
466
434
extra='unable to rename to %r' % (rel_to,),
467
435
unknown_exc=errors.PathError)
469
def _rename_and_overwrite(self, abs_from, abs_to, f):
470
"""Do a fancy rename on the remote server.
472
Using the implementation provided by osutils.
474
osutils.fancy_rename(abs_from, abs_to,
475
rename_func=lambda p1, p2: self._rename(p1, p2, f),
476
unlink_func=lambda p: self._delete(p, f))
478
439
def delete(self, relpath):
479
440
"""Delete the item at relpath"""
480
441
abspath = self._abspath(relpath)
482
self._delete(abspath, f)
484
def _delete(self, abspath, f):
486
443
mutter("FTP rm: %s", abspath)
487
445
f.delete(abspath)
488
446
except ftplib.error_perm, e:
489
447
self._translate_perm_error(e, abspath, 'error deleting',
496
454
def list_dir(self, relpath):
497
455
"""See Transport.list_dir."""
498
basepath = self._abspath(relpath)
499
mutter("FTP nlst: %s", basepath)
457
mutter("FTP nlst: %s", self._abspath(relpath))
459
basepath = self._abspath(relpath)
502
460
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 (".", "..")]
503
466
except ftplib.error_perm, e:
504
467
self._translate_perm_error(e, relpath, extra='error with list_dir')
505
# If FTP.nlst returns paths prefixed by relpath, strip 'em
506
if paths and paths[0].startswith(basepath):
507
entries = [path[len(basepath)+1:] for path in paths]
510
# Remove . and .. if present
511
return [urlutils.escape(entry) for entry in entries
512
if entry not in ('.', '..')]
514
469
def iter_files_recursive(self):
515
470
"""See Transport.iter_files_recursive.
518
473
mutter("FTP iter_files_recursive")
519
474
queue = list(self.list_dir("."))
521
relpath = queue.pop(0)
476
relpath = urllib.quote(queue.pop(0))
522
477
st = self.stat(relpath)
523
478
if stat.S_ISDIR(st.st_mode):
524
479
for i, basename in enumerate(self.list_dir(relpath)):
581
536
"""This is used by medusa.ftp_server to log connections, etc."""
582
537
self.logs.append(message)
584
def setUp(self, vfs_server=None):
585
541
if not _have_medusa:
586
542
raise RuntimeError('Must have medusa to run the FtpServer')
588
assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
589
"FtpServer currently assumes local transport, got %s" % vfs_server
591
544
self._root = os.getcwdu()
592
545
self._ftp_server = _ftp_server(
593
546
authorizer=_test_authorizer(root=self._root),
599
552
self._port = self._ftp_server.getsockname()[1]
600
553
# Don't let it loop forever, or handle an infinite number of requests.
601
554
# In this case it will run for 100s, or 1000 requests
602
self._async_thread = threading.Thread(
603
target=FtpServer._asyncore_loop_ignore_EBADF,
555
self._async_thread = threading.Thread(target=asyncore.loop,
604
556
kwargs={'timeout':0.1, 'count':1000})
605
557
self._async_thread.setDaemon(True)
606
558
self._async_thread.start()
612
564
asyncore.close_all()
613
565
self._async_thread.join()
616
def _asyncore_loop_ignore_EBADF(*args, **kwargs):
617
"""Ignore EBADF during server shutdown.
619
We close the socket to get the server to shutdown, but this causes
620
select.select() to raise EBADF.
623
asyncore.loop(*args, **kwargs)
624
except select.error, e:
625
if e.args[0] != errno.EBADF:
629
568
_ftp_channel = None
630
569
_ftp_server = None
695
634
pfrom = self.filesystem.translate(self._renaming)
696
635
self._renaming = None
697
636
pto = self.filesystem.translate(line[1])
698
if os.path.exists(pto):
699
self.respond('550 RNTO failed: file exists')
702
638
os.rename(pfrom, pto)
703
639
except (IOError, OSError), e:
704
640
# TODO: jam 20060516 return custom responses based on
705
641
# why the command failed
706
# (bialix 20070418) str(e) on Python 2.5 @ Windows
707
# sometimes don't provide expected error message;
708
# so we obtain such message via os.strerror()
709
self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
642
self.respond('550 RNTO failed: %s' % (e,))
711
644
self.respond('550 RNTO failed')
712
645
# For a test server, we will go ahead and just die
737
670
*why* it cannot make a directory.
739
672
if len (line) != 2:
740
self.command_not_understood(''.join(line))
673
self.command_not_understood (string.join (line))
744
677
self.filesystem.mkdir (path)
745
678
self.respond ('257 MKD command successful.')
746
679
except (IOError, OSError), e:
747
# (bialix 20070418) str(e) on Python 2.5 @ Windows
748
# sometimes don't provide expected error message;
749
# so we obtain such message via os.strerror()
750
self.respond ('550 error creating directory: %s' %
751
os.strerror(e.errno))
680
self.respond ('550 error creating directory: %s' % (e,))
753
682
self.respond ('550 error creating directory.')