1
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3
# Copyright 2002 Ben Escoto <ben@emerose.org>
4
# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
6
# This file is part of duplicity.
8
# Duplicity is free software; you can redistribute it and/or modify it
9
# under the terms of the GNU General Public License as published by the
10
# Free Software Foundation; either version 2 of the License, or (at your
11
# option) any later version.
13
# Duplicity is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
# General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with duplicity; if not, write to the Free Software Foundation,
20
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25
import duplicity.backend
26
from duplicity.errors import * #@UnusedWildImport
27
from duplicity import globals, tempdir, util
29
class RsyncBackend(duplicity.backend.Backend):
30
"""Connect to remote store using rsync
32
rsync backend contributed by Sebastian Wilhelmi <seppi@seppi.de>
33
rsyncd auth, alternate port support
34
Copyright 2010 by Edgar Soldin <edgar.soldin@web.de>
37
def __init__(self, parsed_url):
38
"""rsyncBackend initializer"""
39
duplicity.backend.Backend.__init__(self, parsed_url)
41
rsyncd module url: rsync://[user:password@]host[:port]::[/]modname/path
42
Note: 3.0.7 is picky about syntax use either 'rsync://' or '::'
43
cmd: rsync [--port=port] host::modname/path
45
rsync via ssh/rsh url: rsync://user@host[:port]://some_absolute_path
46
-or- rsync://user@host[:port]:/some_relative_path
47
cmd: rsync -e 'ssh [-p=port]' [user@]host:[/]path
49
host = parsed_url.hostname
51
# RSYNC_RSH from calling shell might conflict with our settings
52
if 'RSYNC_RSH' in os.environ:
53
del os.environ['RSYNC_RSH']
54
if self.over_rsyncd():
56
(path, port) = self.get_rsync_path()
57
self.url_string = "%s::%s" % (host, path.lstrip('/:'))
59
port = " --port=%s" % port
61
if parsed_url.path.startswith("//"):
62
# its an absolute path
63
self.url_string = "%s:/%s" % (host, parsed_url.path.lstrip('/'))
66
self.url_string = "%s:%s" % (host, parsed_url.path.lstrip('/'))
68
port = " -p %s" % parsed_url.port
69
# add trailing slash if missing
70
if self.url_string[-1] != '/':
71
self.url_string += '/'
73
if parsed_url.username:
74
if self.over_rsyncd():
75
os.environ['USER'] = parsed_url.username
77
self.url_string = parsed_url.username + "@" + self.url_string
78
# password?, don't ask if none was given
79
self.use_getpass = False
80
password = self.get_password()
82
os.environ['RSYNC_PASSWORD'] = password
83
if self.over_rsyncd():
86
portOption = " -e 'ssh -oBatchMode=yes%s'" % port
87
rsyncOptions = globals.rsync_options
89
rsyncOptions= " " + rsyncOptions
91
self.cmd = "rsync%s%s" % (portOption, rsyncOptions)
93
def over_rsyncd(self):
94
url = self.parsed_url.url_string
95
if re.search('::[^:]*$', url):
100
def get_rsync_path(self):
101
url = self.parsed_url.url_string
102
m = re.search("(:\d+|)?::([^:]*)$", url)
104
return m.group(2), m.group(1).lstrip(':')
105
raise InvalidBackendURL("Could not determine rsync path: %s"
106
"" % self.munge_password( url ) )
108
def run_command(self, commandline):
109
result, stdout, stderr = self.subprocess_popen_persist(commandline)
110
return result, stdout
112
def put(self, source_path, remote_filename = None):
113
"""Use rsync to copy source_dir/filename to remote computer"""
114
if not remote_filename:
115
remote_filename = source_path.get_filename()
116
remote_path = os.path.join(self.url_string, remote_filename)
117
commandline = "%s %s %s" % (self.cmd, source_path.name, remote_path)
118
self.run_command(commandline)
120
def get(self, remote_filename, local_path):
121
"""Use rsync to get a remote file"""
122
remote_path = os.path.join (self.url_string, remote_filename)
123
commandline = "%s %s %s" % (self.cmd, remote_path, local_path.name)
124
self.run_command(commandline)
126
if not local_path.exists():
127
raise BackendException("File %s not found" % local_path.name)
133
if len (line) > 4 and line[4] != '.':
137
commandline = "%s %s" % (self.cmd, self.url_string)
138
result, stdout = self.run_command(commandline)
139
return filter(lambda x: x, map (split, stdout.split('\n')))
141
def delete(self, filename_list):
143
delete_list = filename_list
144
dont_delete_list = []
145
for file in self.list ():
146
if file in delete_list:
147
delete_list.remove (file)
149
dont_delete_list.append (file)
150
if len (delete_list) > 0:
151
raise BackendException("Files %s not found" % str (delete_list))
153
dir = tempfile.mkdtemp()
154
exclude, exclude_name = tempdir.default().mkstemp_file()
155
to_delete = [exclude_name]
156
for file in dont_delete_list:
157
path = os.path.join (dir, file)
158
to_delete.append (path)
160
print >>exclude, file
163
commandline = ("%s --recursive --delete --exclude-from=%s %s/ %s" %
164
(self.cmd, exclude_name, dir, self.url_string))
165
self.run_command(commandline)
166
for file in to_delete:
167
util.ignore_missing(os.unlink, file)
170
duplicity.backend.register_backend("rsync", RsyncBackend)