274 lines · 8.1 KB · Python
RawDownload
#!/usr/bin/env python # client.py - A client providing high level methods for accessing a Styx server.## Copyright (C) 2018 David Boddie <david@boddie.org.uk>## This program is free software: you can redistribute it and/or modify# it under the terms of the GNU General Public License as published by# the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with this program.  If not, see <http://www.gnu.org/licenses/>. import socketimport styx class ClientError(Exception):    pass class Client:     MSIZE = 16384    MAXWELEM = 16        def __init__(self, host = None, port = None, uname = None, aname = None):            self.reset()                self.uname = uname        self.aname = aname                if host != None and port != None:            self.connect(host, port, uname, aname)        def reset(self):            self.host = None        self.port = None                self.socket = None        self.msize = Client.MSIZE                # Maintain an fid for the root of the file server and one for what we        # consider to be the current directory.        self.root_fid = 0        self.current_fid = None                # Keep a collection of replies in case they arrive in an order we don't        # expect.        self.replies = {}        def connect(self, host, port, uname, aname):            try:            s = socket.socket()            s.connect((host, port))        except socket.error:            raise ClientError("Failed to connect to %s:%i." % (host, port))                self.host = host        self.port = port        self.socket = s                # Negotiate a version and maximum message size.        reply = self.send(styx.Tversion(tag=0, msize=self.msize, version=u"9P2000"))                if reply.version != u"9P2000":            raise ClientError("Server's protocol version '%s' is not supported." % reply.version)                self.msize = min(self.msize, reply.msize)                reply = self.send(styx.Tattach(tag=1, fid=0, afid=0, uname=uname, aname=aname))                self.root_fid = self.current_fid = 0        self.fids = set([self.root_fid])        def disconnect(self):            # Clunk the root fid close the socket.        self.send(styx.Tclunk(tag=2, fid=self.root_fid))        self.socket.close()                self.reset()        def send(self, msg):            msg.encode(self.socket)                tag = msg.tag                while tag not in self.replies:                    reply = styx.decode(sock=self.socket)            if reply.tag == tag:                break            else:                self.replies[tag] = reply        else:            reply = self.replies[tag]            del self.replies[tag]                if isinstance(reply, styx.Rerror):            raise ClientError(reply.ename)                return reply        def _clunk(self, fid):            self.send(styx.Tclunk(tag=2, fid=fid))        self.fids.remove(fid)        def _clunk_old(self, fid):            # Clunk the fid only if it is not the root fid.        if fid != self.root_fid:            self._clunk(fid)        def _stat(self, fid):            reply = self.send(styx.Tstat(tag=2, fid=fid))        return reply.stat        def _walk(self, path):            elements = path.split("/")        fid = self.current_fid                while True:                    # Limit the number of path elements to walk.            pieces = elements[:Client.MAXWELEM]            elements = elements[Client.MAXWELEM:]                        # Use one of the allocated fids for the end point of the walk.            newfid = self._next_fid(fid)                        reply = self.send(styx.Twalk(tag=2, fid=fid, newfid=newfid,                                         wname=pieces))                        if reply.nwqid < len(pieces):                raise ClientError("No such file or directory: %s" % \                    "/".join(pieces[:reply.nwqid]))                        # Clunk the old fid if it is an intermediate fid used to walk part            # of the path. The first fid is the current fid, and we don't want            # to clunk that here.            if fid != self.current_fid:                self._clunk_old(fid)                        # If there are no more path elements to walk then return the newfid.            if not elements:                return newfid                        # Use the newfid as the new starting point and prepare to use the            # other allocated fid for the new end point.            fid = newfid        def _next_fid(self, fid):            next_fid = fid + 1                # If the set of fids is sparse then start at zero in order to find a gap.        if next_fid > len(self.fids):            next_fid = 0                # Find the first fid that is not in the set.        while next_fid in self.fids:            next_fid += 1                self.fids.add(next_fid)        return next_fid        def ls(self, path = "", details = False):            newfid = self._walk(path)                # Determine whether the object is a file or directory.        s = self._stat(newfid)                if s.mode & styx.Stat.DMDIR:            self.send(styx.Topen(tag=2, fid=newfid, mode=0))                        data = ""            amount = self.msize - 24                        while True:                                reply = self.send(styx.Tread(                    tag=2, fid=newfid, offset=len(data), count=amount))                data += reply.data                                if len(reply.data) == 0:                    break                        info = styx.Stat().decode(data=data)        else:            # If it is a file then just return the existing information.            info = [s]                # Release the fid for the file so that it can be reused.        self._clunk_old(newfid)                if not details:            info = map(lambda x: (x.name, x.uid, x.gid, x.mode), info)                return info        def cd(self, path):            newfid = self._walk(path)                self._clunk_old(self.current_fid)        self.current_fid = newfid        def mkdir(self, path, perm):            pieces = path.split("/")                # Walk to the directory regardless of whether it is the current        # directory because the fid will be reused for the new directory.                if len(pieces) > 1:            fid = self._walk("/".join(path[:-1]))            name = path[-1]        else:            # Walk to the current directory.            fid = self._walk("")            name = path                self.send(styx.Tcreate(tag=2, fid=fid, name=name,            perm=styx.Stat.DMDIR | perm, mode=0))                # Release the fid so that it can be reused.        self._clunk_old(fid)        def create(self, path, perm, mode):            pieces = path.split("/")                # Walk to the directory regardless of whether it is the current        # directory because the fid will be reused for the new file.                if len(pieces) > 1:            fid = self._walk("/".join(path[:-1]))            name = path[-1]        else:            # Walk to the current directory.            fid = self._walk("")            name = path                self.send(styx.Tcreate(tag=2, fid=fid, name=name, perm=perm, mode=mode))                # Release the fid so that it can be reused.        self._clunk_old(fid)        def open(self, path, mode):            fid = self._walk(path)                reply = self.send(styx.Topen(tag=2, fid=fid, mode=mode))                return styx.File(fid, mode, self)        def read(self, fid, offset, count):            reply = self.send(styx.Tread(tag=2, fid=fid, offset=offset, count=count))                return reply.data