1
# -*- test-case-name: twisted.vfs.test.test_sftp -*-
7
from twisted.python import components, log, util
8
from twisted.conch.avatar import ConchUser
9
from twisted.conch.interfaces import ISession, ISFTPFile
10
from twisted.conch.ssh.filetransfer import ISFTPServer, FileTransferServer
11
from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
12
from twisted.conch.ssh.filetransfer import SFTPError
13
from twisted.conch.ssh.filetransfer import FX_PERMISSION_DENIED, FX_FAILURE
14
from twisted.conch.ssh.filetransfer import FX_NO_SUCH_FILE, FX_OP_UNSUPPORTED
15
from twisted.conch.ssh.filetransfer import FX_NOT_A_DIRECTORY
16
from twisted.conch.ssh.filetransfer import FX_FILE_IS_A_DIRECTORY
17
from twisted.conch.ssh.filetransfer import FX_FILE_ALREADY_EXISTS
18
from twisted.conch.ssh import session
19
from twisted.conch.ls import lsLine
20
from twisted.internet import defer
22
from twisted.vfs import ivfs, pathutils
24
def translateErrors(function):
25
"""Decorator that catches VFSErrors and re-raises them as the corresponding
28
def f(*args, **kwargs):
30
result = function(*args, **kwargs)
31
if isinstance(result, defer.Deferred):
32
result.addErrback(_ebtranslateErrors)
34
except ivfs.PermissionError, e:
35
raise SFTPError(FX_PERMISSION_DENIED, str(e))
36
except ivfs.NotFoundError, e:
37
raise SFTPError(FX_NO_SUCH_FILE, e.args[0])
38
except ivfs.AlreadyExistsError, e:
39
raise SFTPError(FX_FILE_ALREADY_EXISTS, e.args[0])
40
except ivfs.VFSError, e:
41
raise SFTPError(FX_FAILURE, str(e))
42
except NotImplementedError, e:
43
raise SFTPError(FX_OP_UNSUPPORTED, str(e))
45
util.mergeFunctionMetadata(function, f)
49
def _ebtranslateErrors(failure):
50
"""This just re-raises the failure so that the translateErrors decorator
51
around this errback can intercept it if it wants to."""
52
failure.raiseException()
53
_ebtranslateErrors = translateErrors(_ebtranslateErrors)
56
class AdaptFileSystemUserToISFTP:
58
zope.interface.implements(ISFTPServer)
60
def __init__(self, avatar):
64
self.filesystem = avatar.filesystem
66
def _setAttrs(self, path, attrs):
68
NOTE: this function assumes it runs as the logged-in user:
69
i.e. under _runAsUser()
71
if attrs.has_key("uid") and attrs.has_key("gid"):
72
os.lchown(path, attrs["uid"], attrs["gid"])
73
if attrs.has_key("permissions"):
74
os.chmod(path, attrs["permissions"])
75
if attrs.has_key("atime") and attrs.has_key("mtime"):
76
os.utime(path, (attrs["atime"]. attrs["mtime"]))
78
def gotVersion(self, otherVersion, extData):
81
def openFile(self, filename, flags, attrs):
85
if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
86
openFlags = os.O_RDONLY
87
if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
89
openFlags = os.O_WRONLY
90
if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
93
if flags & FXF_APPEND == FXF_APPEND:
95
openFlags |= os.O_APPEND
96
if flags & FXF_CREAT == FXF_CREAT:
98
openFlags |= os.O_CREAT
99
if flags & FXF_TRUNC == FXF_TRUNC:
100
openFlags |= os.O_TRUNC
101
if flags & FXF_EXCL == FXF_EXCL:
104
# XXX Once we change readChunk/writeChunk we'll have to wrap
105
# child in something that implements those.
107
pathSegments = self.filesystem.splitPath(filename)
108
dirname, basename = pathSegments[:-1], pathSegments[-1]
109
parentNode = self.filesystem.fetch('/'.join(dirname))
111
child = parentNode.createFile(basename, exclusive)
112
elif parentNode.exists(basename):
113
child = parentNode.child(basename)
114
if not ivfs.IFileSystemLeaf.providedBy(child):
115
raise SFTPError(FX_FILE_IS_A_DIRECTORY, filename)
117
raise SFTPError(FX_NO_SUCH_FILE, filename)
118
child.open(openFlags)
119
return AdaptFileSystemLeafToISFTPFile(child)
120
openFile = translateErrors(openFile)
122
def removeFile(self, filename):
123
node = self.filesystem.fetch(filename)
124
if not ivfs.IFileSystemLeaf.providedBy(node):
125
raise SFTPError(FX_FILE_IS_A_DIRECTORY, filename)
127
removeFile = translateErrors(removeFile)
129
def renameFile(self, oldpath, newpath):
131
targetNode = self.filesystem.fetch(newpath)
132
except (ivfs.NotFoundError, KeyError):
133
# Something with the new name already exists.
136
if ivfs.IFileSystemContainer(targetNode, None):
137
# The target node is a container. We assume the caller means to
138
# move the source node into the container rather than replace
139
# it, and adjust newpath accordingly.
140
newpath = self.filesystem.joinPath(
141
newpath, self.filesystem.basename(oldpath))
142
old = self.filesystem.fetch(oldpath)
144
renameFile = translateErrors(renameFile)
146
def makeDirectory(self, path, attrs):
147
dirname = self.filesystem.dirname(path)
148
basename = self.filesystem.basename(path)
149
return self.filesystem.fetch(dirname).createDirectory(basename)
150
makeDirectory = translateErrors(makeDirectory)
152
def removeDirectory(self, path):
153
self.filesystem.fetch(path).remove()
154
removeDirectory = translateErrors(removeDirectory)
156
def openDirectory(self, path):
157
directory = self.filesystem.fetch(path)
158
if not ivfs.IFileSystemContainer.providedBy(directory):
159
raise SFTPError(FX_NOT_A_DIRECTORY, path)
161
def __init__(self, iter):
168
(name, attrs) = self.iter.next()
174
s.st_mode = attrs["permissions"]
175
s.st_uid = attrs["uid"]
176
s.st_gid = attrs["gid"]
177
s.st_size = attrs["size"]
178
s.st_mtime = attrs["mtime"]
179
s.st_nlink = attrs["nlink"]
180
return ( name, lsLine(name, s), attrs )
186
iter([(name, _attrify(file))
187
for (name, file) in self.filesystem.fetch(path).children()]))
188
openDirectory = translateErrors(openDirectory)
190
def getAttrs(self, path, followLinks):
191
node = self.filesystem.fetch(path)
192
return _attrify(node)
193
getAttrs = translateErrors(getAttrs)
195
def setAttrs(self, path, attrs):
196
node = self.filesystem.fetch(path)
198
# XXX: setMetadata isn't yet part of the IFileSystemNode interface
199
# (but it should be). So we catch AttributeError, and translate it
200
# to NotImplementedError because it's slightly nicer for clients.
201
node.setMetadata(attrs)
202
except AttributeError:
203
raise NotImplementedError("NO SETATTR")
204
setAttrs = translateErrors(setAttrs)
206
def readLink(self, path):
207
raise NotImplementedError("NO LINK")
209
def makeLink(self, linkPath, targetPath):
210
raise NotImplementedError("NO LINK")
212
def realPath(self, path):
213
return self.filesystem.absPath(path)
216
class AdaptFileSystemLeafToISFTPFile:
217
zope.interface.implements(ISFTPFile)
219
def __init__(self, original):
220
self.original = original
223
return self.original.close()
225
def readChunk(self, offset, length):
226
return self.original.readChunk(offset, length)
228
def writeChunk(self, offset, data):
229
return self.original.writeChunk(offset, data)
232
return _attrify(self.original)
234
def setAttrs(self, attrs):
236
# XXX: setMetadata isn't yet part of the IFileSystemNode interface
237
# (but it should be). So we catch AttributeError, and translate it
238
# to NotImplementedError because it's slightly nicer for clients.
239
self.original.setMetadata(attrs)
240
except AttributeError:
241
raise NotImplementedError("NO SETATTR")
244
class VFSConchSession:
245
zope.interface.implements(ISession)
246
def __init__(self, avatar):
248
def openShell(self, proto):
249
self.avatar.conn.transport.transport.loseConnection()
250
def getPty(self, term, windowSize, modes):
253
log.msg('shell closed')
256
class VFSConchUser(ConchUser):
257
def __init__(self, username, root):
258
ConchUser.__init__(self)
259
self.username = username
260
self.filesystem = pathutils.FileSystem(root)
262
self.listeners = {} # dict mapping (interface, port) -> listener
263
self.channelLookup.update(
264
{"session": session.SSHSession})
265
self.subsystemLookup.update(
266
{"sftp": FileTransferServer})
269
# XXX - this may be broken
270
log.msg('avatar %s logging out (%i)' % (self.username, len(self.listeners)))
274
meta = node.getMetadata()
275
permissions = meta.get('permissions', None)
276
if permissions is None:
277
if ivfs.IFileSystemContainer.providedBy(node):
282
return {'permissions': permissions,
283
'size': meta.get('size', 0),
284
'uid': meta.get('uid', 0),
285
'gid': meta.get('gid', 0),
286
'atime': meta.get('atime', time.time()),
287
'mtime': meta.get('mtime', time.time()),
288
'nlink': meta.get('nlink', 1)
293
components.registerAdapter(AdaptFileSystemUserToISFTP, VFSConchUser, ISFTPServer)
294
components.registerAdapter(VFSConchSession, VFSConchUser, ISession)