#!/usr/bin/env python
#@+leo-ver=4
#@+node:@file gmailfs.py
#@@first
#
#    Copyright (C) 2004  Richard Jones  <richard followed by funny at sign then jones then a dot then name>
#
#    GmailFS - Gmail Filesystem Version 0.8.0
#    This program can be distributed under the terms of the GNU GPL.
#    See the file COPYING.
#

"""
GmailFS provides a filesystem using a Google Gmail account as its storage medium
"""

#@+others
#@+node:imports

import fuse
from fuse import Fuse
import os
from errno import *
from stat import *
from os.path import abspath, expanduser

fuse.fuse_python_api = (0, 2)


import thread
import quopri
import libgmail
from lgconstants import *

import sys,traceback,re,string,time,tempfile,array,logging,logging.handlers
   
try:
    import OpenSSLProxy
except:
    pass

#@-node:imports

# Globals
DefaultUsername = 'defaultUser'
DefaultPassword = 'defaultPassword'
DefaultFsname = 'gmailfs'
References={}

GMAILFS_VERSION = '3'

PathStartDelim  = '__a__'
PathEndDelim    = '__b__'
FileStartDelim  = '__c__'
FileEndDelim    = '__d__'
LinkStartDelim  = '__e__'
LinkEndDelim    = '__f__'
MagicStartDelim = '__g__'
MagicEndDelim   = '__h__'

InodeTag ='i'
DevTag = 'd'
NumberLinksTag = 'k'
FsNameTag = 'q'
ModeTag = 'e'
UidTag = 'u'
GidTag = 'g'
SizeTag = 's'
AtimeTag = 'a'
MtimeTag = 'm'
CtimeTag = 'c'
BlockSizeTag = 'z'
VersionTag = 'v'

RefInodeTag = 'r'
FileNameTag = 'n'
PathNameTag = 'p'
LinkToTag = 'l'

NumberQueryRetries = 3

regexObjectTrailingMB = re.compile(r'\s?MB$')

def _addLoggingHandlerHelper(handler):
    """ Sets our default formatter on the log handler before adding it to
        the log object. """
    handler.setFormatter(defaultLogFormatter)
    log.addHandler(handler)

def GmailConfig(fname):
    import ConfigParser
    cp = ConfigParser.ConfigParser()
    global References
    global DefaultUsername, DefaultPassword, DefaultFsname
    global NumberQueryRetries
    if cp.read(fname) == []:
      log.warning("Unable to read configuration file: " + str(fname))
      return

    sections = cp.sections()
    if "account" in sections:
      options = cp.options("account")
      if "username" in options:
          DefaultUsername = cp.get("account", "username")
      if "password" in options:
          DefaultPassword = cp.get("account", "password")
    else:
      log.error("Unable to find GMail account configuration")

    if "filesystem" in sections:
      options = cp.options("filesystem")
      if "fsname" in options:
          DefaultFsname = cp.get("filesystem", "fsname")
    else:
      log.warning("Using default file system (Dangerous!)")
      
    if "logs" in sections:
      options = cp.options("logs")
      if "level" in options:
        level = cp.get("logs", "level")
        log.setLevel(logging._levelNames[level])
      if "logfile" in options:
        logfile = abspath(expanduser(cp.get("logs", "logfile")))
        log.removeHandler(defaultLoggingHandler)
        _addLoggingHandlerHelper(logging.handlers.RotatingFileHandler(logfile, "a", 5242880, 3))
        
    if "connection" in sections:
      options = cp.options("connection")
      try:
        if "proxy" in options:
          OpenSSLProxy.OpenSSLInstallHandler(cp.get("connection", "proxy"))
        else:
          OpenSSLProxy.OpenSSLInstallHandler()
      except:
        log.error("OpenSSLProxy is missing. Can't use HTTPS proxy!");

      if "retries" in options:
          NumberQueryRetries = cp.getint("connection", "retries")

    if "references" in sections:
      options = cp.options("references")
      for option in options:
          record = cp.get("references",option)
          fields = record.split(':')
          if len(fields)<1 or len(fields)>3:
              log.warning("Invalid reference '%s' in configuration." % (record))
              continue
          reference = reference_class(*fields)
          References[option] = reference
                                                                                
class reference_class:
    def __init__(self,fsname,username=None,password=None):
      self.fsname = fsname
      if username is None or username == '':
          self.username = DefaultUsername
      else:
          self.username = username
      if password is None or password == '':
          self.password = DefaultPassword
      else:
          self.password = password

# This ensures backwards compatability where
# old filesystems were stored with 7bit encodings
# but new ones are all quoted printable
def fixQuotedPrintable(body):
    # first remove headers
    newline = body.find("\r\n\r\n")
    if newline >= 0:
	body = body[newline:]
    fixed = body
    if re.search("Content-Transfer-Encoding: quoted",body):
        fixed = quopri.decodestring(body)
    # Map unicode     
    return fixed.replace('\u003d','=')

def _getMessagesByQuery(ga,queryString):
    tries = 0
    while tries<NumberQueryRetries:
        tries+=1
        try:
            folder = ga.getMessagesByQuery(queryString+" subject:"+FsNameTag+"="+MagicStartDelim+ fsNameVar +MagicEndDelim)
            return folder
        except:
            log.error("Exception getting query:"+queryString)

NumberSendRetries = 3            

#@+node:_sendMessages
def _sendMessage(ga,gmsg):
    """
    Send an already composed GmailComposedMessage message to the users own email address
    """
    
    tries = 0
    while tries<NumberSendRetries:
        tries+=1
        try:
            if ga.sendMessage(gmsg):
                log.debug("Sent message ok")
                return 1
            else:
                log.info("Sent message failed")
        except:
            traceback.print_exc(file=sys.stdout)
            log.info("Sent message failed")
        
    log.error("Send failed too many times")
    return 0
#@+node:_sendMessages

def _pathSeparatorEncode(path):
    s1 = re.sub("/","__fs__",path)
    s2 = re.sub("-","__mi__",s1)
    return re.sub("\+","__pl__",s2)

def _pathSeparatorDecode(path):
    s1 = re.sub("__fs__","/",path)
    s2 = re.sub("__mi__","-",path)
    return re.sub("__pl__","+",path)


def _logException(msg):
    traceback.print_exc(file=sys.stderr)
    log.exception(msg)

#@+node:class GmailInode
class GmailInode:

    """
    Class used to store gmailfs inode details
    """

    #@+node:__init__
    def __init__(self, msg,ga):
        try:
            self.version = 2
            self.ino = 0
            self.mode = 0
            self.dev = 0
            self.nlink = 0
            self.uid = 0
            self.gid = 0
            self.size = 0
            self.atime = 0
            self.mtime = 0
            self.ctime = 0
            self.blocksize = DefaultBlockSize
            self.ga = ga
            self.msg = msg
            m = re.match(VersionTag+'=(.*) '+RefInodeTag+'=(.*) '+FsNameTag+'='+MagicStartDelim+'(.*)'+MagicEndDelim,msg.subject.replace('\u003d','='))
            matchInode = m.group(2)

            log.debug("trying to get inode with:"+matchInode+" from subject:"+msg.subject)
            self.inode_msg = self.getinodeMsg(matchInode)
            self.setInode()
        except:
            _logException("got exception when getmessages")
    #@-node:__init__

    #@+node:getinodeMsg
    def getinodeMsg(self,inodeNumber):
        """
        Get Gmail message handle for the message containing inodeNumber
        """
        
        folder = _getMessagesByQuery(self.ga,'subject:'+InodeTag+'='+inodeNumber+'')
        if len(folder)!=1:
          log.error("could not find inode msg with inodenumber:"+inodeNumber)
          return None;
        if len(folder._threads[0])!=1:
          return None;
        thread = folder._threads[0]
        if not thread._messages:
          thread._messages = thread._getMessages(thread)

        return thread._messages[len(thread._messages)-1]
    #@-node:getinodeMsg
    
    def update(self):
        """
        Sync's the state of this inode back to the users gmail account
        """
        
        timeString = str(int(time.time()))
        subject = (VersionTag+"="+GMAILFS_VERSION+
                  " "+InodeTag+"="+str(self.ino)+
	          " "+DevTag+"="+str(self.dev)+
		  " "+NumberLinksTag+"="+str(self.nlink)+
		  " "+FsNameTag+"="+MagicStartDelim+ fsNameVar +MagicEndDelim )
        body    = (ModeTag+"="+str(self.mode)+
	          " "+UidTag+"="+str(self.uid)+
	          " "+GidTag+"="+str(self.gid)+
	          " "+SizeTag+"="+str(self.size)+
	          " "+AtimeTag+"="+str(self.atime)+
	          " "+MtimeTag+"="+timeString+
	          " "+CtimeTag+"="+str(self.ctime)+
	          " "+BlockSizeTag+"="+str(self.blocksize) )
        gmsg = libgmail.GmailComposedMessage(username, subject, body)
        if _sendMessage(self.ga,gmsg):
            log.debug("Sent "+subject+" ok")
            if (self.inode_msg):
                log.debug("trashing old inode:"+str(self.inode_msg.subject))
                self.ga.trashMessage(self.inode_msg)
            self.inode_msg = self.getinodeMsg(str(self.ino))
        else:
            e = OSError("Couldnt send mesg:"+gmsg.subject)
            e.errno = ENOSPC
            raise e
        

    def setInode(self):
        """
        Setup the inode instances members from the gmail inode message
        """
        
        try:
            subject = self.inode_msg.subject.replace('\u003d','=')
            body = self.inode_msg.source
	    body = fixQuotedPrintable(body)
            log.debug("setting inode from subject:"+subject)
            log.debug("and body:"+body)
            m = re.match(VersionTag+'=(.*) '+InodeTag+'=(.*) '+DevTag+'=(.*) '+NumberLinksTag+'=(.*) '+FsNameTag+'='+MagicStartDelim+'(.*)'+
                         MagicEndDelim, subject)
            self.version = int(m.group(1))
            self.ino = int(m.group(2))
            self.dev = int(m.group(3))
            self.nlink = int(m.group(4))
            #quotedEquals = "=(?:3D)?(.*)"
            quotedEquals = "=(.*)"
            m = re.search(re.compile(
	                  ModeTag+quotedEquals+' ?'+UidTag+quotedEquals+' ?'+
  			  GidTag+quotedEquals+' ?'+
                          SizeTag+quotedEquals+' ?'+
			  AtimeTag+quotedEquals+' ?'+
			  MtimeTag+quotedEquals+' ?'+
                          CtimeTag+quotedEquals+' ?'+
			  BlockSizeTag+quotedEquals, re.DOTALL),body)
	    self.mode = int(m.group(1))
            self.uid = int(m.group(2))
            self.gid = int(m.group(3))
            self.size = int(m.group(4))
            self.atime = int(m.group(5))
            self.mtime = int(m.group(6))
            self.ctime = int(m.group(7))
            self.blocksize = int(m.group(8))
        except:
            _logException("got exception when setInode")
            self.ino = None

    def unlink(self, path):
        """
        Delete this inode and all of its data blocks
        """
        log.debug("unlink path:"+path+" with nlinks:"+str(self.nlink))
        self.nlink-=1
        if self.mode & S_IFDIR:
            log.debug("unlinking dir")
            self.nlink-=1
        else:
            log.debug("unlinking file")
            

        self.ga.trashMessage(self.msg)
        if self.nlink<1:
           log.debug("deleting inode or block nlink:"+str(self.nlink))
           self.ga.trashMessage(self.inode_msg)
           subject = 'b='+str(self.ino)+''
           folder = _getMessagesByQuery(self.ga,'subject:'+subject)
           for thread in folder:
               for msg in thread:
                   self.ga.trashMessage(msg)
        else:
           log.debug("about to update inode")
           self.update()
           log.debug("not deleting inode or block nlink:"+str(self.nlink))

        
#@-node:class GmailInode

#@+node:class OpenFile
class OpenFile:
    """
    Class holding any currently open files, includes cached instance of the last data block retrieved
    """
    
    def __init__(self, inode):
        self.inode = inode
        self.tmpfile = None
        self.blocksRead = 0
        self.needsWriting = 0
        self.blocksize = inode.blocksize
        self.buffer = list(" "*self.blocksize)
        self.currentOffset = -1
        self.lastBlock = 0
        self.lastBlockRead = -1
        self.lastBlockBuffer = []

    def close(self):
        """
        Closes this file by committing any changes to the users gmail account
        """
        
        if self.needsWriting:
           self.commitToGmail()


    def write(self,buf,off):
        """
        Write data to file from buf, offset by off bytes into the file
        """
        
        buflen = len(buf)
        towrite = buflen
        #if self.currentOffset == -1 or off<self.currentOffset or off>self.currentOffset+self.blocksize:
        #    self.commitToGmail()
        #    self.currentOffset = (off/self.blocksize)*self.blocksize+(off/self.blocksize)
        #    self.buffer = self.readFromGmail(self.currentOffset/self.blocksize,1)

        if self.currentOffset == -1 or off<self.currentOffset or off>self.currentOffset:
            self.commitToGmail()
            self.currentOffset = off;
            self.buffer = self.readFromGmail(self.currentOffset/self.blocksize,1)
            
        currentBlock = self.currentOffset/self.blocksize
        written = 0
        while towrite>0:
            thiswrote = min(towrite,min(self.blocksize-(self.currentOffset%self.blocksize),self.blocksize))
            log.debug("wrote "+str(thiswrote)+" bytes off:"+str(off)+" self.currentOffset:"+str(self.currentOffset))
            self.buffer[self.currentOffset%self.blocksize:] = buf[written:written+thiswrote]
            towrite -= thiswrote
            written += thiswrote
            self.currentOffset += thiswrote
            self.lastBlock=currentBlock
            if self.currentOffset/self.blocksize>currentBlock:
                self.needsWriting = 1
                self.commitToGmail()
                currentBlock+=1
                if towrite>0:
                    self.buffer = self.readFromGmail(currentBlock,1)
            else:
                self.needsWriting = 1

        if off+buflen>self.inode.size:
            self.inode.size=off+buflen
            
        return buflen

    def commitToGmail(self):
        """
        Send any unsaved data to users gmail account as an attachment
        """
        
        if not self.needsWriting:
            return 1
        
        ga = self.inode.ga
        subject = ('b='+str(self.inode.ino)+
	          ' x='+str(self.lastBlock)+
		  ' '+FsNameTag+'='+MagicStartDelim+ fsNameVar +MagicEndDelim )
        tmpf = tempfile.NamedTemporaryFile()

        arr = array.array('c')
        arr.fromlist(self.buffer)
        if log.isEnabledFor(logging.DEBUG):
            log.debug("wrote to tmp file:"+arr.tostring())
        tmpf.write(arr.tostring())
        tmpf.flush()
        gmsg = libgmail.GmailComposedMessage(username, subject, fsNameVar,filenames = [tmpf.name])
        if _sendMessage(ga,gmsg):
            log.debug("Sent write commit ok")
            self.needsWriting=0
            self.inode.update()
            tmpf.close()
            return 1
        else:
            log.error("Sent write commit failed")
            tmpf.close()
            return 0

    

    def read(self,readlen,offset):
        """
        Read readlen bytes from an open file from position offset bytes into the files data
        """
        
        readlen = min(self.inode.size-offset,readlen)
        outbuf = list(" "*readlen)
        toread = readlen;
        upto=0;
        while toread>0:
           readoffset = (offset+upto)%self.blocksize
           thisread = min(toread,min(self.blocksize-(readoffset%self.blocksize),self.blocksize))
           outbuf[upto:] = self.readFromGmail((offset+upto)/self.blocksize,0)[readoffset:readoffset+thisread]
           upto+=thisread
           toread-=thisread
           log.debug("still to read:"+str(toread))
        return outbuf

        return retbuf

    def readFromGmail(self,readblock,deleteAfter):
        """
        Read data block with block number 'readblock' for this file from users gmail account, if 'deleteAfter' is
        true then the block will be removed from Gmail after reading
        """
        
        log.debug("about to try and find inode:"+str(self.inode.ino)+" blocknumber:"+str(readblock))
        if self.lastBlockRead == readblock:
            contentList = list(" "*self.blocksize)
	    contentList[0:] = self.lastBlockBuffer
            return contentList
        ga = self.inode.ga
        subject = 'b='+str(self.inode.ino)+' x='+str(readblock)+''
        folder = _getMessagesByQuery(ga,'subject:'+subject)
        if len(folder)!=1:
            return list(" "*self.blocksize)
        if len(folder._threads[0])!=1:
           return list(" "*self.blocksize)
        thread = folder._threads[0]
        if not thread._messages:
            thread._messages = thread._getMessages(thread)
        msg = thread._messages[len(thread._messages)-1]
        log.debug("got msg with subject:"+msg.subject)
        log.debug("got "+str(len(msg.attachments))+" attachments")
        a = msg.attachments[0]
        if log.isEnabledFor(logging.DEBUG):
            log.debug("read from gmail:"+a.content)
        if deleteAfter:
            ga.trashMessage(msg)
        self.lastBlockRead = readblock
        self.lastBlockBuffer = a.content
        contentList = list(" "*self.blocksize)
        contentList[0:] = a.content
        return contentList

#@-node:class OpenFile

#@+node:class Gmailfs
class Gmailfs(Fuse):

    #@	@+others

    
    #@+node:__init__
    def __init__(self, extraOpts, mountpoint, *args, **kw):
        Fuse.__init__(self, *args, **kw)
        self.fuse_args.mountpoint = mountpoint
        self.optdict = extraOpts
        log.info("Mountpoint: %s" % mountpoint)
	# obfuscate sensitive fields before logging
	loggableOptdict = self.optdict.copy()
	loggableOptdict['password'] = '*' * 8
	log.info("Named mount options: %s" % (loggableOptdict,))

        # do stuff to set up your filesystem here, if you want

        self.openfiles = {}
        self.inodeCache = {}

        global DefaultBlockSize
        
        global fsNameVar
        global password
        global username

        DefaultBlockSize = 5*1024*1024
        
        fsNameVar = DefaultFsname
        password = DefaultPassword
        username = DefaultUsername
	
	options_required = 1
	if self.optdict.has_key("reference"):
	    try:
		reference = References[self.optdict['reference']]
		username = reference.username
		password = reference.password
		fsNameVar = reference.fsname
	    except:
		log.error("Invalid reference supplied. Using defaults.")
	    else:
		options_required = 0

        if not self.optdict.has_key("username"):
	    if options_required:
	        log.warning('mount: warning, should mount with username=gmailuser option, using default')
        else:
            username = self.optdict['username']

        if not self.optdict.has_key("password"):
	    if options_required:
        	log.warning('mount: warning, should mount with password=gmailpass option, using default')
        else:
            password = self.optdict['password']

        if not self.optdict.has_key("fsname"):
	    if options_required:
        	log.warning('mount: warning, should mount with fsname=name option, using default')
        else:
            fsNameVar = self.optdict['fsname']

        if self.optdict.has_key("blocksize"):
            DefaultBlockSize = int(self.optdict['blocksize'])

        self.ga = libgmail.GmailAccount(username, password)
        self.ga.login()
        if username.find("@")<0:
            username = username+"@gmail.com"
        log.info("Connected to gmail")
        #thread.start_new_thread(self.mythread, ())
        pass
    #@-node:__init__

    #@+node:mythread
    def mythread(self):
    
        """
        The beauty of the FUSE python implementation is that with the python interp
        running in foreground, you can have threads
        """    
        log.debug("mythread: started")
        #while 1:
        #    time.sleep(120)
        #    print "mythread: ticking"
    
    #@-node:mythread

    #@+node:attribs
    flags = 1
    
    #@-node:attribs
    
    class GmailStat(fuse.Stat):
        def __init__(self):
            self.st_mode = 0
            self.st_ino = 0
            self.st_dev = 0
            self.st_nlink = 0
            self.st_uid = 0
            self.st_gid = 0
            self.st_size = 0
            self.st_atime = 0
            self.st_mtime = 0
            self.st_ctime = 0

    #@+node:getattr
    def getattr(self, path):
        st = Gmailfs.GmailStat();
        log.debug("get stat:"+path)
        #st_mode (protection bits)
        #st_ino (inode number)
        #st_dev (device)
        #st_nlink (number of hard links)
        #st_uid (user ID of owner)
        #st_gid (group ID of owner)
        #st_size (size of file, in bytes)
        #st_atime (time of most recent access)
        #st_mtime (time of most recent content modification)
        #st_ctime (time of most recent content modification or metadata change).


        if path == '/':
            inode=self.getinode(path)
            if not inode:
                self._mkfileOrDir("/",None,S_IFDIR|S_IRUSR|S_IXUSR|S_IWUSR|S_IRGRP|S_IXGRP|S_IXOTH|S_IROTH,-1,1,2)
                inode = self.getinode(path)
        else:
            inode = self.getinode(path)
            
        if inode:
            if log.isEnabledFor(logging.DEBUG):
                log.debug("inode "+str(inode))
            st.st_mode = inode.mode
            st.st_ino = inode.ino
            st.st_dev = inode.dev
            st.st_nlink = inode.nlink
            st.st_uid = inode.uid
            st.st_gid = inode.gid
            st.st_size = inode.size
            st.st_atime = inode.atime
            st.st_mtime = inode.mtime
            st.st_ctme = inode.ctime
#            statTuple = (inode.mode,inode.ino,inode.dev,inode.nlink,inode.uid,
#                         inode.gid,inode.size,inode.atime,inode.mtime,
#                         inode.ctime)
            if log.isEnabledFor(logging.DEBUG):
                log.debug("statsTuple "+str(st))
            return st
        else:
            e = OSError("No such file"+path)
            e.errno = ENOENT
            raise e
    #@-node:getattr
    
    #@+node:readlink
    def readlink(self, path):
        log.debug("readlink: path='%s'" % path)
        inode = self.getinode(path)
        if not (inode.mode & S_IFLNK):
            e = OSError("Not a link"+path)
            e.errno = EINVAL
            raise e
        log.debug("about to follow link in body:"+inode.msg.source)
	body = fixQuotedPrintable(inode.msg.source)
        m = re.search(LinkToTag+'='+LinkStartDelim+'(.*)'+
                      LinkEndDelim,body)
        return m.group(1)
    #@-node:readlink
    

   #@+node:readdir
    def readdir(self, path, offset):
        try:
            log.debug("at top of readdir");
            log.debug("getting dir "+path)
            fspath = _pathSeparatorEncode(path)
            log.debug("querying for:"+''+PathNameTag+'='+PathStartDelim+
                      fspath+PathEndDelim)
            # FIX need to check if directory exists and return error if it doesnt, actually
            # this may be done for us by fuse
            folder = _getMessagesByQuery(self.ga,''+PathNameTag+'='+
                                         PathStartDelim+fspath+PathEndDelim)
            log.debug("got folder ")
            lst = []
            for dirlink in ".", "..":
                lst.append(dirlink)
                
            for thread in folder:
               assert len(thread) == 1
               for msg in thread:
                 log.debug("thread.summary is " + thread.snippet)
                 m = re.search(FileNameTag+'='+FileStartDelim+'(.*)'+
                               FileEndDelim, thread.snippet)
                 if (m):
                     # Match succeeded, we got the whole filename.
                     log.debug("Used summary for filename")
                     filename = m.group(1)
                 else:
                     # Filename was too long, have to fetch message.
                     log.debug("Long filename, had to fetch message")
		     body = fixQuotedPrintable(msg.source)
                     m = re.search(FileNameTag+'='+FileStartDelim+'(.*)'+
                                   FileEndDelim, body)
                     filename = m.group(1)

                 log.debug("Found file:"+filename)
                 # this test for length is a special case hack for the root directory to prevent ls /gmail_root
                 # returning "". This is hack is requried due to adding modifiable root directory as an afterthought, rather
                 # than designed in at the start.
                 if len(filename)>0:
                     lst.append(filename)
        except:
            _logException("got exception when getmessages")
            lst = None
        #return map(lambda x: (x,0), lst)
        for r in lst:
	    yield fuse.Direntry(r)
    #@-node:getdir
    
    #@+node:unlink
    def unlink(self, path):
        log.debug("unlink called on:"+path)
        try:
            inode = self.getinode(path)
            inode.unlink(path)
            #del self.inodeCache[path]
            # this cache flushing in unfortunate but currently necessary
            # to avoid problems with hard links losing track of
            # number of the number of links
            self.inodeCache = {}
            return 0
        except:
            _logException("Error unlinking file"+path)
            e = OSError("Error unlinking file"+path)
            e.errno = EINVAL
            raise e
    #@-node:unlink

    #@+node:rmdir
    def rmdir(self, path):
        log.debug("rmdir called on:"+path)
        #this is already checked before rmdir is even called
        #dirlist = self.getdir(path)
        #if len(dirlist)>0:
        #    e = OSError("directory not empty"+path)
        #    e.errno = ENOTEMPTY
        #    raise e
        inode = self.getinode(path)
        inode.unlink(path)
        #del self.inodeCache[path]

        # this cache flushing in unfortunate but currently necessary
        # to avoid problems with hard links losing track of
        # number of the number of links
        self.inodeCache = {}
        
        # update number of links in parent directory
        ind = string.rindex(path,'/')
        parentdir = path[:ind]
        log.debug("about to rmdir with parentdir:"+parentdir)
        if len(parentdir)==0:
            parentdir = "/"
            
        parentdirinode = self.getinode(parentdir)
        parentdirinode.nlink-=1
        parentdirinode.update()
        del self.inodeCache[parentdir]
        return 0
        
    #@-node:rmdir
    
    #@+node:symlink
    def symlink(self, path, path1):
        log.debug("symlink: path='%s', path1='%s'" % (path, path1))
        self._mkfileOrDir(path1,path,S_IFLNK|S_IRWXU|S_IRWXG|S_IRWXO,-1,0,1)
    #@-node:symlink

    #@+node:rename
    def rename(self, path, path1):
        log.debug("rename from:"+path+" to:"+path1)
        msg = self.getinodemsg(path)

        ind = string.rindex(path1,'/')
        log.debug("ind:"+str(ind))
        dirpath = path1[:ind]
        if len(dirpath)==0:
            dirpath="/"
        name = path1[ind+1:]
        log.debug("dirpath:"+dirpath+" name:"+name)
        fspath = _pathSeparatorEncode(dirpath)
        
        m = re.match(VersionTag+'=(.*) '+RefInodeTag+'=(.*) '+FsNameTag+'='+MagicStartDelim+'(.*)'+
                     MagicEndDelim, msg.subject)
        subject = ( VersionTag+"="+GMAILFS_VERSION+
                  " "+RefInodeTag+"="+m.group(2)+
	          " "+FsNameTag+"="+MagicStartDelim+ fsNameVar +MagicEndDelim )
	bodytmp = fixQuotedPrintable(msg.source)
        m = re.search(FileNameTag+'='+FileStartDelim+'(.*)'+FileEndDelim+
	             ' '+PathNameTag+'='+PathStartDelim+'(.*)'+PathEndDelim+
		     ' '+LinkToTag+'='+LinkStartDelim+'(.*)'+LinkEndDelim,
                      bodytmp)
        body = (FileNameTag+"="+FileStartDelim+ name +FileEndDelim+
	       " "+PathNameTag+"="+PathStartDelim+ fspath +PathEndDelim+
	       " "+LinkToTag+"="+LinkStartDelim+ m.group(3) +LinkEndDelim )
        gmsg = libgmail.GmailComposedMessage(username, subject, body)
        if _sendMessage(self.ga,gmsg):
            log.debug("Sent "+subject+" ok")
            self.ga.trashMessage(msg)
            if self.inodeCache.has_key(path):
                #del self.inodeCache[path]
                # this cache flushing in unfortunate but currently necessary
                # to avoid problems with hard links losing track of
                # number of the number of links
                self.inodeCache = {}
            return 0
        else:
            e = OSError("Couldnt send mesg"+path)
            e.errno = ENOSPC
            raise e

    #@-node:rename
    
    #@+node:link
    def link(self, path, path1):
        log.debug("hard link: path='%s', path1='%s'" % (path, path1))
        inode = self.getinode(path)
        if not (inode.mode & S_IFREG):
            e = OSError("hard links only supported for regular files not directories:"+path)
            e.errno = EPERM
            raise e
        inode.nlink+=1
        inode.update()
        self._mkfileOrDir(path1,None,inode.mode,inode.ino,0,1)
        return 0
    #@-node:link
    
    #@+node:chmod
    def chmod(self, path, mode):
        log.debug("chmod called with path:"+path+" mode:"+str(mode))
        inode = self.getinode(path)
        inode.mode = (inode.mode & ~(S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO)) | mode
        inode.update()
        return 0
    #@-node:chmod

    #@+node:chown
    def chown(self, path, user, group):
        log.debug("chown called with user:"+str(user)+" and group:"+str(group))
        inode = self.getinode(path)
        inode.uid = user
        inode.gid = group
        inode.update()
        return 0
    #@-node:chown

    #@+node:truncate
    def truncate(self, path, size):
        log.debug("truncate "+path+" to size:"+str(size))
        inode = self.getinode(path)
        # this is VERY lazy, we leave the truncated data around
        # it WILL be harvested when we grow the file again or
        # when we delete the file but should probably FIX
        inode.size = size;
        inode.update()
        return 0
    #@-node:truncate

    #@+node:mknod
    def mknod(self, path, mode, dev):
    	""" Python has no os.mknod, so we can only do some things """
        log.debug("mknod:"+path)
	if S_ISREG(mode) | S_ISFIFO(mode) | S_ISSOCK(mode):
            self._mkfileOrDir(path,None,mode,-1,0,1)
    	    #open(path, "w")
    	else:
    		return -EINVAL
    #@-node:mknod

    def _mkfileOrDir(self,path,path2,mode,inodenumber,size,nlinks):
        ind = string.rindex(path,'/')
        log.debug("ind:"+str(ind))
        dirpath = path[:ind]
        if len(dirpath)==0:
            dirpath="/"
        name = path[ind+1:]
        log.debug("dirpath:"+dirpath+" name:"+name)
        fspath = _pathSeparatorEncode(dirpath)
        if path2 == None:
            path2 = ""
        if inodenumber==-1:
            inodeno = int(time.time())
        else:
            inodeno = inodenumber
            
        subject = (VersionTag+"="+GMAILFS_VERSION+
                  " "+RefInodeTag+"="+str(inodeno)+
	          " "+FsNameTag+"="+MagicStartDelim+ fsNameVar +MagicEndDelim )
        body    = (""+FileNameTag+"="+FileStartDelim+  name      +FileEndDelim+
	          " "+PathNameTag+"="+PathStartDelim+  fspath    +PathEndDelim+
		  " "+LinkToTag+"="+LinkStartDelim+  path2     +LinkEndDelim  )
        gmsg = libgmail.GmailComposedMessage(username, subject, body)
        if _sendMessage(self.ga,gmsg):
            log.debug("Sent "+subject+" ok")
        else:
            e = OSError("Couldnt send mesg in _mkfileOrDir "+path)
            e.errno = ENOSPC
            raise e
        # only create inode if number not provided
        if inodenumber==-1:
            timeString = str(int(time.time()))
            subject = (VersionTag+"="+GMAILFS_VERSION+
                      " "+InodeTag+"="+str(inodeno)+
	              " "+DevTag+"=11 "+NumberLinksTag+"="+str(nlinks)+
		      " "+FsNameTag+"="+MagicStartDelim+ fsNameVar +MagicEndDelim )
            body = (""+ModeTag+"="+str(mode)+
	           " "+UidTag+"="+str(os.getuid())+" "+GidTag+"="+str(os.getgid())+" "+SizeTag+"="+str(size)+
		   " "+AtimeTag+"="+timeString+
		   " "+MtimeTag+"="+timeString+
		   " "+CtimeTag+"="+timeString+
		   " "+BlockSizeTag+"="+str(DefaultBlockSize) )
            gmsg = libgmail.GmailComposedMessage(username, subject, body)
            if _sendMessage(self.ga,gmsg):
                log.debug("Sent "+subject+" ok")
            else:
                e = OSError("Couldnt send mesg"+path)
                e.errno = ENOSPC
                raise e

    #@+node:mkdir
    def mkdir(self, path, mode):
        log.debug("mkdir path:"+path+" mode:"+str(mode))
        self._mkfileOrDir(path,None,mode|S_IFDIR, -1,1,2)
        inode = self.getinode(path)
        ind = string.rindex(path,'/')
        log.debug("ind:"+str(ind))
        parentdir = path[:ind]
        if len(parentdir)==0:
            parentdir="/"
        parentdirinode = self.getinode(parentdir)
        parentdirinode.nlink+=1
        parentdirinode.update()
        del self.inodeCache[parentdir]
    #@-node:mkdir

    #@+node:utime
    def utime(self, path, times):
        log.debug("utime for path:"+path+" times:"+str(times))
        inode = self.getinode(path)
        inode.atime = times[0]
        inode.mtime = times[1]
        return 0
    #@-node:utime

    #@+node:open
    def open(self, path, flags):
        log.debug("gmailfs.py:Gmailfs:open: %s" % path)
        try:
            inode = self.getinode(path)
            f = OpenFile(inode)
            self.openfiles[path] = f
            return 0
        except:
            _logException("Error opening file"+path)
            e = OSError("Error opening file"+path)
            e.errno = EINVAL
            raise e
    #@-node:open

    #@+node:read
    def read(self, path, readlen, offset):
        try:
            log.debug("gmailfs.py:Gmailfs:read: %s" % path)
            log.debug("reading len:"+str(readlen)+" offset:"+str(offset))
            f = self.openfiles[path]
            buf = f.read(readlen,offset)
            arr = array.array('c')
            arr.fromlist(buf)
            rets = arr.tostring()

            return rets
        except:
            _logException("Error reading file"+path)
            e = OSError("Error reading file"+path)
            e.errno = EINVAL
            raise e
    
    #@-node:read

    #@+node:write
    def write(self, path, buf, off):
        try:
            log.debug("gmailfs.py:Gmailfs:write: %s" % path)
            if log.isEnabledFor(logging.DEBUG):
                log.debug("writing:"+str(buf))
            f = self.openfiles[path]
            written = f.write(buf,off)
    	    return written
        except:
            _logException("Error opening file"+path)
            e = OSError("Error opening file"+path)
            e.errno = EINVAL
            raise e
    #@-node:write

    #@+node:release
    def release(self, path, flags):
        log.debug("gmailfs.py:Gmailfs:release: %s %s" % (path, flags))
        f = self.openfiles[path]
        f.close()
        del self.openfiles[path]
        return 0
    #@-node:release

    #@+node:statfs
    def statfs(self):
        """
        Should return a tuple with the following 6 elements:
            - blocksize - size of file blocks, in bytes
            - totalblocks - total number of blocks in the filesystem
            - freeblocks - number of free blocks
            - availblocks - number of blocks available to non-superuser
            - totalfiles - total number of file inodes
            - freefiles - nunber of free file inodes
    
        Feel free to set any of the above values to 0, which tells
        the kernel that the info is not available.
        """
        st = fuse.StatVfs()
        block_size = 1024
        quotaInfo = self.ga.getQuotaInfo()
        quotaMbUsed = quotaInfo[QU_SPACEUSED]
        quotaMbTotal = quotaInfo[QU_QUOTA]
        blocks = int(regexObjectTrailingMB.sub('', quotaMbTotal)) * 1024 * 1024 / block_size
        quotaPercent = quotaInfo[QU_PERCENT]
        blocks_free = blocks - int(regexObjectTrailingMB.sub('', quotaMbUsed)) * 1024 * 1024 / block_size
        blocks_avail = blocks_free # I guess...
        log.debug("%s of %s used. (%s)\n" % (quotaMbUsed, quotaMbTotal, quotaPercent))
        log.debug("Blocks: %s free, %s total\n" % (blocks_free, blocks))
        files = 0
        files_free = 0
        namelen = 80
        st.f_bsize = block_size
	st.f_frsize = block_size
	st.f_blocks = blocks
	st.f_bfree = blocks_free
	st.f_bavail = blocks_avail
	st.f_files = files
	st.f_ffree = files_free
	return st
    #@-node:statfs

    #@+node:fsync
    def fsync(self, path, isfsyncfile):
        log.debug("gmailfs.py:Gmailfs:fsync: path=%s, isfsyncfile=%s" % (path, isfsyncfile))
        inode = self.getinode(path)
        f = self.openfiles[path]
        f.commitToGmail()
        return 0
    
    #@-node:fsync



    def getinodemsg(self, path):
      try:
          log.debug("check getnodemsg:"+path)
          ind = string.rindex(path,'/')
          log.debug("ind:"+str(ind))
          dirpath = path[:ind]
          if len(dirpath)==0:
              dirpath="/"
          name = path[ind+1:]
          log.debug("dirpath:"+dirpath+" name:"+name)
          fspath = _pathSeparatorEncode(dirpath)
          folder = _getMessagesByQuery(self.ga, FileNameTag+'='+
                                       FileStartDelim+name+FileEndDelim+
                                       ' '+PathNameTag+'='+PathStartDelim+
                                       fspath +PathEndDelim)
				       
          if len(folder)!=1:
              return None;
          if len(folder._threads[0])!=1:
              return None;
          thread = folder._threads[0]
          if not thread._messages:
            thread._messages = thread._getMessages(thread)
          return thread._messages[len(thread._messages)-1]
      except:
          _logException("no slash in path:"+path)
          return None


    def getinode(self, path):
        if self.inodeCache.has_key(path):
            return self.inodeCache[path]
        
        msg = self.getinodemsg(path)

        if msg == None:
            return None

        inode = GmailInode(msg,self.ga)
        if inode:
            self.inodeCache[path] = inode
            
        return inode
    
    #@-others
    
#@-node:class Gmailfs
#@+node:mainline

# Setup logging
log = logging.getLogger('gmailfs')
defaultLogLevel = logging.WARNING
log.setLevel(defaultLogLevel)
defaultLogFormatter = logging.Formatter("%(asctime)s %(levelname)-10s %(message)s", "%x %X")
    
# log to stdout while parsing the config while
defaultLoggingHandler = logging.StreamHandler(sys.stdout)
_addLoggingHandlerHelper(defaultLoggingHandler)

GmailConfig(['/etc/gmailfs.conf',os.path.expanduser('~/.gmailfs')])
try:
    libgmail.ConfigLogs(log)
except:
    pass

def main(mountpoint, namedOptions):
    server = Gmailfs(namedOptions,mountpoint,version="gmailfs 0.8.0",usage='',dash_s_do='setsingle')
#    server.parser.mountpoint = mountpoint
    server.parse(errex=1)
    server.flags = 0
    server.multithreaded = False;
    server.main()

if __name__ == '__main__':
    main()
    
#@-node:mainline
#@-others
#@-node:@file gmailfs.py
#@-leo
