/
sftp.py
250 lines (221 loc) · 9.37 KB
/
sftp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"""SFTP transport module."""
from omnisync.transportmount import TransportInterface
from omnisync.fileobject import FileObject
from omnisync import urlfunctions
import getpass
import time
import errno
class SFTPTransport(TransportInterface):
"""SFTP transport class."""
# Transports should declare the protocols attribute to specify the protocol(s)
# they can handle.
protocols = ("sftp", )
# Inform whether this transport's URLs use a hostname. The difference between http://something
# and file://something is that in the former "something" is a hostname, but in the latter it's
# a path.
uses_hostname = True
# listdir_attributes is a set that contains the file attributes that listdir()
# supports.
listdir_attributes = set(("size", "mtime", "atime", "perms", "owner", "group"))
# Conversely, for getattr().
getattr_attributes = set(("size", "mtime", "atime", "perms", "owner", "group"))
# List the attributes setattr() can set.
setattr_attributes = set(("mtime", "atime", "perms", "owner", "group"))
# Define attributes that can be used to decide whether a file has been changed
# or not.
evaluation_attributes = set(("size", "mtime"))
# The preferred buffer size for reads/writes.
buffer_size = 2**15
def __init__(self):
self._file_handle = None
self._connection = None
self._transport = None
def _get_filename(self, url):
"""Retrieve the local filename from a given URL."""
split_url = urlfunctions.url_split(url, uses_hostname=self.uses_hostname)
# paths are relative unless they start with two //
path = split_url.path
if len(path) > 1 and path.startswith("/"):
path = path[1:]
return path
# Transports should also implement the following methods:
def add_options(self):
"""Return the desired command-line plugin options.
Returns a tuple of ((args), {kwargs}) items for optparse's add_option().
"""
return ()
def connect(self, url, config):
"""Initiate a connection to the remote host."""
options = config.full_options
# Make the import global.
global paramiko
try:
# We import paramiko only when we need it because its import is really slow.
import paramiko
except ImportError:
print "SFTP: You will need to install the paramiko library to have sftp support."
raise
url = urlfunctions.url_split(url)
if not url.port:
url.port = 22
self._transport = paramiko.Transport((url.hostname, url.port))
username = url.username
if not url.username:
if hasattr(options, "username"):
username = options.username
else:
url.username = getpass.getuser()
password = url.password
if not url.password:
if hasattr(options, "password"):
password = options.password
else:
password = getpass.getpass(
"SFTP: Please enter the password for %s@%s:" % (url.username, url.hostname)
)
self._transport.connect(username=username, password=password)
self._connection = paramiko.SFTPClient.from_transport(self._transport)
def disconnect(self):
"""Disconnect from the remote server."""
self._transport.close()
def open(self, url, mode="rb"):
"""Open a file in _mode_ to prepare for I/O.
Raises IOError if anything goes wrong.
"""
if self._file_handle:
raise IOError, "Another file is already open."
self._file_handle = self._connection.open(self._get_filename(url), mode)
def read(self, size):
"""Read _size_ bytes from the open file."""
return self._file_handle.read(size)
def write(self, data):
"""Write _data_ to the open file."""
self._file_handle.write(data)
def remove(self, url):
"""Remove the specified file."""
try:
self._connection.remove(self._get_filename(url))
except IOError:
return False
else:
return True
def rmdir(self, url):
"""Remove the specified directory non-recursively."""
try:
self._connection.rmdir(self._get_filename(url))
except IOError:
return False
else:
return True
def close(self):
"""Close the open file."""
if self._file_handle:
self._file_handle.close()
self._file_handle = None
def mkdir(self, url):
"""Recursively make the given directories at the current URL."""
# Recursion is not needed for anything but the first directory, so we need to be able to
# do it.
current_path = ""
error = False
for component in self._get_filename(url).split("/"):
current_path += component + "/"
try:
self._connection.mkdir(current_path)
except IOError, failure:
if failure.errno != errno.EEXIST:
error = True
else:
error = False
return error
def listdir(self, url):
"""Retrieve a directory listing of the given location.
Returns a list of (url, attribute_dict) tuples if the given URL is a directory,
False otherwise. URLs should be absolute, including protocol, etc.
attribute_dict is a dictionary of {key: value} pairs for any applicable
attributes from ("size", "mtime", "atime", "ctime", "isdir").
"""
url = urlfunctions.append_slash(url, True)
try:
dir_list = self._connection.listdir_attr(self._get_filename(url))
except IOError:
return False
file_list = []
for item in dir_list:
file_list.append(FileObject(self, url + item.filename,
{"size": item.st_size,
"mtime": item.st_mtime,
"atime": item.st_atime,
"perms": item.st_mode,
"owner": item.st_uid,
"group": item.st_gid,
}))
return file_list
def isdir(self, url):
"""Return True if the given URL is a directory, False if it is a file or
does not exist."""
try:
# paramiko doesn't allow you to check any other way.
self._connection.listdir(self._get_filename(url))
except IOError, failure:
if failure.errno == errno.ENOENT:
return False
else:
raise
else:
return True
def getattr(self, url, attributes):
"""Retrieve as many file attributes as we can, at the very *least* the requested ones.
Returns a dictionary of {"attribute": "value"}, or {"attribute": None} if the file does
not exist.
"""
if set(attributes) - self.getattr_attributes:
raise NotImplementedError, "Some requested attributes are not implemented."
try:
statinfo = self._connection.stat(self._get_filename(url))
except IOError:
return dict([(x, None) for x in self.getattr_attributes])
# Turn times to ints because checks fail sometimes due to rounding errors.
return {"size": statinfo.st_size,
"mtime": int(statinfo.st_mtime),
"atime": int(statinfo.st_atime),
"perms": statinfo.st_mode,
"owner": statinfo.st_uid,
"group": statinfo.st_gid,
}
def setattr(self, url, attributes):
"""Set a file's attributes if possible."""
filename = self._get_filename(url)
if "atime" in attributes or "mtime" in attributes:
atime = attributes.get("atime", time.time())
mtime = attributes.get("mtime", time.time())
try:
self._connection.utime(filename, (atime, mtime))
except IOError:
print "SFTP: Permission denied, could not set atime/mtime."
if "perms" in attributes:
try:
self._connection.chmod(filename, attributes["perms"])
except IOError:
print "SFTP: Permission denied, could not set perms."
if "owner" in attributes or "group" in attributes:
# If we're missing one, get it.
if not "owner" in attributes or not "group" in attributes:
stat = self._connection.stat(filename)
owner = attributes.get("owner", stat.st_uid)
group = attributes.get("group", stat.st_gid)
else:
owner = attributes["owner"]
group = attributes["group"]
try:
self._connection.chown(filename, owner, group)
except IOError:
print "SFTP: Permission denied, could not set uid/gid."
def exists(self, url):
"""Return True if a given path exists, False otherwise."""
try:
self._connection.stat(self._get_filename(url))
except IOError:
return False
else:
return True