~mterry/duplicity/gdrive

« back to all changes in this revision

Viewing changes to duplicity/backends.py

  • Committer: bescoto
  • Date: 2002-10-29 01:49:46 UTC
  • Revision ID: vcs-imports@canonical.com-20021029014946-3m4rmm5plom7pl6q
Initial checkin

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2002 Ben Escoto
 
2
#
 
3
# This file is part of duplicity.
 
4
#
 
5
# duplicity is free software; you can redistribute it and/or modify it
 
6
# under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA
 
8
# 02139, USA; either version 2 of the License, or (at your option) any
 
9
# later version; incorporated herein by reference.
 
10
 
 
11
"""Provides functions and classes for getting/sending files to destination"""
 
12
 
 
13
import os
 
14
import log, path, dup_temp, file_naming
 
15
 
 
16
class BackendException(Exception): pass
 
17
 
 
18
def get_backend(url_string):
 
19
        """Return Backend object from url string, or None if not a url string
 
20
 
 
21
        url strings are like
 
22
        scp://foobar:password@hostname.net:124/usr/local.  If a protocol
 
23
        is unsupported a fatal error will be raised.
 
24
 
 
25
        """
 
26
        global protocol_class_dict
 
27
        def bad_url(message = None):
 
28
                if message:
 
29
                        err_string = "Bad URL string '%s': %s" % (url_string, message)
 
30
                else: err_string = "Bad URL string '%s'" % url_string
 
31
                log.FatalError(err_string)
 
32
 
 
33
        colon_position = url_string.find(":")
 
34
        if colon_position < 1: return None
 
35
        protocol = url_string[:colon_position]
 
36
        if url_string[colon_position+1:colon_position+3] != '//': return None
 
37
        remaining_string = url_string[colon_position+3:]
 
38
        
 
39
        try: backend, separate_host = protocol_class_dict[protocol]
 
40
        except KeyError: bad_url("Unknown protocol '%s'" % protocol)
 
41
        assert not separate_host, "This part isn't done yet"
 
42
 
 
43
        return backend(remaining_string)
 
44
 
 
45
 
 
46
class Backend:
 
47
        """Represent a connection to the destination device/computer
 
48
 
 
49
        Classes that subclass this should implement the put, get, list,
 
50
        and delete methods.
 
51
 
 
52
        """
 
53
        def init(self, some_arguments): pass
 
54
 
 
55
        def put(self, source_path, remote_filename = None):
 
56
                """Transfer source_path (Path object) to remote_filename (string)
 
57
 
 
58
                If remote_filename is None, get the filename from the last
 
59
                path component of pathname.
 
60
 
 
61
                """
 
62
                if not remote_filename: remote_filename = source_path.get_filename()
 
63
                pass
 
64
 
 
65
        def get(self, remote_filename, local_path):
 
66
                """Retrieve remote_filename and place in local_path"""
 
67
                pass
 
68
        
 
69
        def list(self):
 
70
                """Return list of filenames (strings) present in backend"""
 
71
                pass
 
72
 
 
73
        def delete(self, filename_list):
 
74
                """Delete each filename in filename_list, in order if possible"""
 
75
                pass
 
76
 
 
77
        def run_command(self, commandline):
 
78
                """Run given commandline with logging and error detection"""
 
79
                log.Log("Running '%s'" % commandline, 4)
 
80
                if os.system(commandline):
 
81
                        raise BackendException("Error running '%s'" % commandline)
 
82
 
 
83
        def popen(self, commandline):
 
84
                """Run command and return stdout results"""
 
85
                log.Log("Reading results of '%s'" % commandline, 4)
 
86
                fout = os.popen(commandline)
 
87
                results = fout.read()
 
88
                if fout.close():
 
89
                        raise BackendException("Error running '%s'" % commandline)
 
90
                return results
 
91
 
 
92
        def get_fileobj_read(self, filename, parseresults = None):
 
93
                """Return fileobject opened for reading of filename on backend
 
94
 
 
95
                The file will be downloaded first into a temp file.  When the
 
96
                returned fileobj is closed, the temp file will be deleted.
 
97
 
 
98
                """
 
99
                if not parseresults:
 
100
                        parseresults = file_naming.parse(filename)
 
101
                        assert parseresults, "Filename not correctly parsed"
 
102
                tdp = dup_temp.new_tempduppath(parseresults)
 
103
                self.get(filename, tdp)
 
104
                tdp.setdata()
 
105
                return tdp.filtered_open_with_delete("rb")
 
106
 
 
107
        def get_fileobj_write(self, filename, parseresults = None):
 
108
                """Return fileobj opened for writing, write to backend on close
 
109
 
 
110
                The file will be encoded as specified in parseresults (or as
 
111
                read from the filename), and stored in a temp file until it
 
112
                can be copied over and deleted.
 
113
 
 
114
                """
 
115
                if not parseresults:
 
116
                        parseresults = file_naming.parse(filename)
 
117
                        assert parseresults, "Filename not correctly parsed"
 
118
                tdp = dup_temp.new_tempduppath(parseresults)
 
119
 
 
120
                def close_file_hook():
 
121
                        """This is called when returned fileobj is closed"""
 
122
                        self.put(tdp, filename)
 
123
                        tdp.delete()
 
124
 
 
125
                fh = dup_temp.FileobjHooked(tdp.filtered_open("wb"))
 
126
                fh.addhook(close_file_hook)
 
127
                return fh
 
128
 
 
129
        def get_data(self, filename, parseresults = None):
 
130
                """Retrieve a file from backend, process it, return contents"""
 
131
                fin = self.get_fileobj_read(filename, parseresults)
 
132
                buf = fin.read()
 
133
                assert not fin.close()
 
134
                return buf
 
135
 
 
136
        def put_data(self, buffer, filename, parseresults = None):
 
137
                """Put buffer into filename on backend after processing"""
 
138
                fout = self.get_fileobj_write(filename, parseresults)
 
139
                fout.write(buffer)
 
140
                assert not fout.close()
 
141
 
 
142
 
 
143
class LocalBackend(Backend):
 
144
        """Use this backend when saving to local disk
 
145
 
 
146
        Urls look like file://testfiles/output.  Relative to root can be
 
147
        gotten with extra slash (file:///usr/local).
 
148
 
 
149
        """
 
150
        def __init__(self, directory_name):
 
151
                self.remote_pathdir = path.Path(directory_name)
 
152
 
 
153
        def put(self, source_path, remote_filename = None, rename = None):
 
154
                """If rename is set, try that first, copying if doesn't work"""
 
155
                if not remote_filename: remote_filename = source_path.get_filename()
 
156
                target_path = self.remote_pathdir.append(remote_filename)
 
157
                log.Log("Writing %s" % target_path.name, 6)
 
158
                if rename:
 
159
                        try: source_path.rename(target_path)
 
160
                        except OSError: pass
 
161
                        else: return
 
162
                target_path.writefileobj(source_path.open("rb"))
 
163
 
 
164
        def get(self, filename, local_path):
 
165
                """Get file and put in local_path (Path object)"""
 
166
                source_path = self.remote_pathdir.append(filename)
 
167
                local_path.writefileobj(source_path.open("rb"))
 
168
 
 
169
        def list(self):
 
170
                """List files in that directory"""
 
171
                return self.remote_pathdir.listdir()
 
172
 
 
173
        def delete(self, filename_list):
 
174
                """Delete all files in filename list"""
 
175
                try:
 
176
                        for filename in filename_list:
 
177
                                self.remote_pathdir.append(filename).delete()
 
178
                except OSError, e: raise BackendException(str(e))
 
179
 
 
180
 
 
181
class scpBackend(Backend):
 
182
        """This backend copies files using scp.  List not supported"""
 
183
        def __init__(self, url_string):
 
184
                """scpBackend initializer
 
185
 
 
186
                Here url_string is something like
 
187
                username@host.net/file/whatever, which is produced after the 
 
188
                'scp://' of a url is stripped.
 
189
 
 
190
                """
 
191
                comps = url_string.split("/")
 
192
                self.host_string = comps[0] # of form user@hostname
 
193
                self.remote_dir = "/".join(comps[1:]) # can be empty string
 
194
                if self.remote_dir: self.remote_prefix = self.remote_dir + "/"
 
195
                else: self.remote_prefix = ""
 
196
 
 
197
        def put(self, source_path, remote_filename = None):
 
198
                """Use scp to copy source_dir/filename to remote computer"""
 
199
                if not remote_filename: remote_filename = source_path.get_filename()
 
200
                commandline = "scp %s %s:%s%s" % \
 
201
                                          (source_path.name, self.host_string,
 
202
                                           self.remote_prefix, remote_filename)
 
203
                self.run_command(commandline)
 
204
 
 
205
        def get(self, remote_filename, local_path):
 
206
                """Use scp to get a remote file"""
 
207
                commandline = "scp %s:%s%s %s" % \
 
208
                                          (self.host_string, self.remote_prefix,
 
209
                                           remote_filename, local_path.name)
 
210
                self.run_command(commandline)
 
211
                local_path.setdata()
 
212
                if not local_path.exists():
 
213
                        raise BackendException("File %s not found" % local_path.name)
 
214
                
 
215
        def list(self):
 
216
                """List files available for scp
 
217
 
 
218
                Note that this command can get confused when dealing with
 
219
                files with newlines in them, as the embedded newlines cannot
 
220
                be distinguished from the file boundaries.
 
221
 
 
222
                """
 
223
                commandline = "ssh %s ls %s" % (self.host_string, self.remote_dir)
 
224
                return filter(lambda x: x, self.popen(commandline).split("\n"))
 
225
 
 
226
        def delete(self, filename_list):
 
227
                """Runs ssh rm to delete files.  Files must not require quoting"""
 
228
                pathlist = map(lambda fn: self.remote_prefix + fn, filename_list)
 
229
                commandline = "ssh %s rm %s" % \
 
230
                                          (self.host_string, " ".join(pathlist))
 
231
                self.run_command(commandline)
 
232
 
 
233
 
 
234
class sftpBackend(Backend):
 
235
        """This backend uses sftp to perform file operations"""
 
236
        pass # Do this later
 
237
 
 
238
 
 
239
# Dictionary relating protocol strings to tuples (backend_object,
 
240
# separate_host).  If separate_host is true, get_backend() above will
 
241
# parse the url further to try to extract a hostname, protocol, etc.
 
242
protocol_class_dict = {"scp": (scpBackend, 0),
 
243
                                           "ssh": (scpBackend, 0),
 
244
                                           "file": (LocalBackend, 0)}