#!/usr/bin/env python 
# -*- coding: UTF-8 -*-

# afiolzofs : Support to mount afio archives with lzop compression.

# Copyright (c) 2010, Yoshiteru Ishimaru
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#    * Neither the name of the Yoshiteru Ishimaru nor the names of its contributors
#      may be used to endorse or promote products derived from this software
#      without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# 0.0.4 (27 Nov 2010)
#  support extended ASCII format
#  fix "unlink error"
#  fix "other magic number"
#  not to use defaultdict
#  fix "rename error"
#  change inode structure
# 0.0.3 (26 Nov 2010)
#  fix "cannot mount XXX<space>XXX.afio.lzo"
# 0.0.2 (26 Nov 2010)
#  BUG fix "REG FILE"

# 0.0.1 (21 Nov 2010)
#  New release
#  This program is based on fusepy. And it is started by coping the 'fuse.py' and 'memory.py' 
#
# Copyright of the fuse.py and memory.py
#
# Copyright (c) 2008 Giorgos Verigakis <verigak@gmail.com> 
#  
# Permission to use, copy, modify, and distribute this software for any 
# purpose with or without fee is hereby granted, provided that the above 
# copyright notice and this permission notice appear in all copies. 
#  
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 


#from collections import defaultdict 
from errno import ENOENT 
from stat import S_IFDIR, S_IFLNK, S_IFREG
from stat import S_IFMT ,S_ISDIR, S_ISLNK, S_ISREG
from sys import argv, exit 
from time import time,mktime ,strptime
 
from fuse import FUSE, FuseOSError, Operations, LoggingMixIn 
import subprocess
import pwd
import grp
import os

#### Get information #### 
def _lzop(data,compress=False):
        if compress:	args=['lzop','-c9']
        else:		args=['lzop','-dc']
	proc = subprocess.Popen(args,
        	stdin=subprocess.PIPE,
		stdout=subprocess.PIPE,
		close_fds=True,
		)
	pid = os.fork()
	if pid==0: # child
		proc.stdout.close()
		proc.stdin.write(data)
		proc.stdin.flush()
		proc.stdin.close()
		os._exit(os.EX_OSERR)
	else:      # parent
		proc.stdin.close()
		data=proc.stdout.read()
                proc.stdout.close()
                return data
def _filetype(data):
	pipe=subprocess.Popen("file -" ,shell=True,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
	pipe.stdin.write(data);			pipe.stdin.close()
	s=pipe.stdout.read().split();		pipe.stdout.close()
	return " ".join(s[1:4])
  
#### FUSE Operation class ####
class Afiolzofs(LoggingMixIn, Operations):		 # for debug
#class afiolzofs(Operations):				# for normal use
    """Example memory filesystem. Supports only one level of files.""" 
     
    def __init__(self,mountpoint): 
        self.mountpoint=mountpoint
#        self.inode = defaultdict(dict) 
        self.ino = {} # for read afio file
        self.fd = 0 
        now = time() 
        inode = dict(
			st_mode=(S_IFDIR | 0755), 
			st_ctime=now,
			st_mtime=now, 
			st_atime=now,
			st_uid=os.getuid(),
			st_gid=os.getgid(),
			st_nlink=2,
			i_data={},
		) 
        inode['st_ino']=id(inode)
        inode['i_data']['.']=inode
        self.inode = inode
    #### local function ####
#    def _getinode(self,path):
#	if path=="/":
#	  dentry=["/","."]
#	else:
#	  dentry=os.path.split(path)
#	try:
#	  return self.inode[dentry[0]][dentry[1]]
#	except:
#	  raise FuseOSError(ENOENT) 
#    def _getinodedir(self,path):
#	if path=="/":
#	  dentry=["/","."]
#	else:
#	  dentry=os.path.split(path)
#	try:
#	  inode_dir=self.inode[dentry[0]]
#	  inode=inode_dir[dentry[1]]
#	  return inode,inode_dir,dentry[1]
#	except:
#	  raise FuseOSError(ENOENT) 
    def _getinode(self,path):
	if path=="/":
	  dentry=["/","."]
	  return self.inode
	else:
	  dentry=os.path.split(path)
	try:
	  inode_dir=self._getinode(dentry[0])
	  inode=inode_dir['i_data'][dentry[1]]
	  return inode
	except:
	  raise FuseOSError(ENOENT) 
    def _getinodedir(self,path):
	if path=="/":
	  dentry=["/","."]
	  # what should be return here?
	else:
	  dentry=os.path.split(path)
	try:
	  inode_dir=self._getinode(dentry[0])
	  inode=inode_dir['i_data'][dentry[1]]
	  return inode,inode_dir,dentry[1]
	except:
	  raise FuseOSError(ENOENT) 
    def _addfile(self,path,inode):
	dentry=os.path.split(path)
	inode_dir=self._getinode(dentry[0])
	inode_dir['i_data'][dentry[1]] = inode
    def _adddir(self,path,inode):
	dentry=os.path.split(path)
	inode_dir=self._getinode(dentry[0])
	print ">>>>>>>>>>>>>>>>",inode_dir,dentry[1]
	inode_dir['i_data'][dentry[1]] = inode
        inode_dir['st_nlink'] += 1
    def _setnlink(self,inode):
	ino=inode['i_ino']
	inode2=self.ino.get(ino,None)
	if inode2==None:
	  self.ino[ino]=inode
	  return inode,True
	else:
	  return inode2,False

    def read_afio_item(self,inode):
        f=open(inode['i_abspath'],"r")
        f.seek(inode['i_startaddr'])
        data=f.read(inode['i_readsize'])
        f.close()
        compfunc=inode['i_compfunc']
        if compfunc != None:data=compfunc(data)       
        return data

    def load_afio_file(self,f=None,seek=None,target="",abspath=""):
          f.seek(seek)
          hdr=f.read(6)
	  if hdr=='070707': 	# old ASCII magic number
            hdr2=f.read(70) 	# length: 70
            inode=dict(
	      i_dev=		int(hdr2[0:6],8),
	      i_ino=		int(hdr2[6:12],8),
	      st_mode=		int(hdr2[12:18],8),
	      st_uid=		int(hdr2[18:24],8),
	      st_gid=		int(hdr2[24:30],8),
	      st_nlink=		int(hdr2[30:36],8),
	      rdev=		int(hdr2[36:42],8),
	      st_mtime=		int(hdr2[42:53],8),
	      i_namelength=	int(hdr2[53:59],8),
	      st_size=		int(hdr2[59:70],8),
	    )
	    inode['st_ctime']=inode['st_mtime']
	    inode['st_atime']=inode['st_mtime']
	    inode['i_readsize']=inode['st_size']
            inode['i_filepath']=f.read(inode['i_namelength'])[:-1]	# next field is path name
            inode['i_startaddr']=seek+6+70+inode['i_namelength']	# calc data address
	    #print id(inode)
	    print "|23456|23456|23456|23456|23456|23456|23456|23456|23456789ab|23456|23456789ab|..."
	    print "|  hdr|  dev|  ino| mode|  uid|  gid|nlink| rdev|     mtime|nmlen|      size|pathname"
            print "%s%s%s" % (hdr,hdr2,inode['i_filepath'])
	  elif hdr=='070717': 	# extended ASCII magic number
	    hdr2=f.read(75) 	# length 75
            inode=dict(
	      i_dev=		int(hdr2[0:6],8),
	      i_ino=		int(hdr2[6:17],8),
	      st_mode=		int(hdr2[17:23],8),
	      st_uid=		int(hdr2[23:29],8),
	      st_gid=		int(hdr2[29:35],8),
	      st_nlink=		int(hdr2[35:41],8),
	      rdev=		int(hdr2[41:47],8),
	      st_mtime=		int(hdr2[47:58],8),
	      i_namelength=	int(hdr2[58:64],8),
	      st_size=		int(hdr2[64:75],8),
	    )
	    inode['st_ctime']=inode['st_mtime']
	    inode['st_atime']=inode['st_mtime']
	    inode['i_readsize']=inode['st_size']
            inode['i_filepath']=f.read(inode['i_namelength'])[:-1]	# next field is path name
            inode['i_startaddr']=seek+6+75+inode['i_namelength']	# calc data address
	    print "|23456|23456|23456789ab|23456|23456|23456|23456|23456|23456789ab|23456|23456789ab|..."
	    print "|  hdr|  dev|       ino| mode|  uid|  gid|nlink| rdev|     mtime|nmlen|      size|pathname"
            print "%s%s%s" % (hdr,hdr2,inode['i_filepath'])
	  elif hdr=='070727':	# large ASCII magic number		not implemented yet
	    hdr2=f.read(110) 	# length 110
	    print "|23456|2345678|234567890123456m|23456|2345678|2345678|2345678|2345678|234567890123456n|234|234|234s|234567890123456:|..."
	    print "|  hdr|    dev|            inoM|  mod|    uid|    gid|  nlink|   rdev|          mtimeN|nml|flg|xszS|           size:|pathname"
            print "%s%s" % (hdr,hdr2)
            inode={}
	    return
	  elif hdr=='070701': 	# cpio new ASCII magic number		 not implemented yet
            hdr2=f.read(110) 	# length: 110
            inode={}
	    return
	  elif hdr=='070702': 	# cpio new ASCII magic number with CRC	 not implemented yet
            #hdr2=f.read(110) 	# length: ??
            inode={}
	    return
	  elif hdr=='070703': 	# Tcpio magic number of TI/E		 not implemented yet
            #hdr2=f.read(110) 	# length: ??
	    return
          else:
            return
	  inode['st_ino']=id(inode)
          inode['i_readfunc']=self.read_afio_item
          inode['i_compfunc']=None
          inode['i_abspath']=abspath	# abspath of the afio.lzo file
          mode=inode['st_mode']
          if S_ISDIR(mode):		# DIR
	    if inode['st_nlink']>2:inode,new=self._setnlink(inode)
	    path='%s/%s' % (target,inode['i_filepath'])
            inode['i_data']={}		# for DIR ({}: real empty dir entry)
            self._adddir(path,inode)
          elif S_ISLNK(mode):		# SLINK
	    if inode['st_nlink']>1:inode,new=self._setnlink(inode)
	    else: new=True
            if new:inode['i_data'] = f.read(inode['i_readsize'])	# for Symlink ("xxx": linked source name)
	    path='%s/%s' % (target,inode['i_filepath'])
	    self._addfile(path,inode)
          elif S_ISREG(mode):		# REG FILE
	    # '.z' and 'rdev & 1==0' mean compressed file in afio header
            if ((inode['rdev'] & 1)== 0) and (inode['i_filepath'][-2:] =='.z'):	# Chake Compressed or Not.
              inode['i_filepath']=inode['i_filepath'][:-2]				# remove ".z"
              size=inode['i_readsize']
              if size>50:size=50							# read first 50 bytes
              top50=f.read(size)
	      filetype=_filetype(top50)
              if filetype=='lzop compressed data':
                inode['i_compfunc']=_lzop
		# read size from lzop header
		inode['st_size']=int("%02x%02x%02x%02x"%(ord(top50[38]),ord(top50[39]),ord(top50[40]),ord(top50[41])),16)
	    inode['i_data']=None	# for REG FILE (None: not read data yet)
	    path='%s/%s' % (target,inode['i_filepath'])
	    if inode['st_nlink']>1:inode,new=self._setnlink(inode)
	    self._addfile(path,inode)
            print "============================== normal file ",path
	  else:
	    path='%s/%s' % (target,inode['i_filepath'])
	    print "============ not implimented ===================",path

    def load_afio_files(self,targetdir,sourceabspath):
        pipe=subprocess.Popen('afio -tvBZ "%s"' % sourceabspath, shell=True,stdout=subprocess.PIPE).stdout
        self.ino = {} # clear ino dict
        f=open(sourceabspath,"r")
        for line in pipe:
          i=int(line.split(None,2)[0])
          self.load_afio_file(f,i,targetdir,sourceabspath)
        f.close()  

    def _check_symlink(self,target,source):
        targetpath=os.path.split(target)
        if targetpath[0]=="/": # if target is root of the mountpoint
          targetname=".%s" % targetpath[1]
          targetdir=os.path.join(targetpath[0],targetname)
          sourceabspath=os.path.normpath(os.path.join(self.mountpoint,source))
          f=open(sourceabspath)
          top50=f.read(50);f.close()
          filetype=_filetype(top50)
	  if "ASCII cpio archive":
	    st=os.stat(sourceabspath)
            inode=dict(
			st_mode=(S_IFDIR | 0755), 
			st_ctime=st.st_ctime,
			st_mtime=st.st_mtime, 
			st_atime=st.st_atime,
			st_uid=st.st_uid,
			st_gid=st.st_gid,
			st_nlink=2,
			st_blksize=st.st_blksize,
			st_blocks=st.st_blocks,
			st_dev=st.st_dev,
			st_ino=st.st_ino,
			st_rdev=st.st_rdev,
			st_size=st.st_size,
			i_data={}
		)
	    inode['st_ino']=id(inode)
	    inode['i_data']['.']=inode
	    self._adddir(targetdir,inode)
            self.load_afio_files(targetdir,sourceabspath)
            return targetname
          else:
            return source
	return source
    ######## FUSE Operations ########
    #### Start File System ####     
    def statfs(self, path): 
        return dict(f_bsize=512, f_blocks=4096, f_bavail=2048) 
    #### Attr ####     
    def getattr(self, path, fh=None): 
        #if path not in self.files: 
        #    raise FuseOSError(ENOENT) 
	print "getattr ===> ",path
        inode=self._getinode(path)
	print "getattr ===> ",inode
        return inode
    def chmod(self, path, mode): 
        inode=self._getinode(path)
        inode['st_mode'] &= 0770000 
        inode['st_mode'] |= mode 
        return 0 
    def chown(self, path, uid, gid): 
        inode=self._getinode(path)
        inode['st_uid'] = uid 
        inode['st_gid'] = gid 
    def utimens(self, path, times=None): 
        now = time() 
        atime, mtime = times if times else (now, now) 
        inode=self._getinode(path)
	inode['st_atime'] = atime 
	inode['st_mtime'] = mtime 
    #### XAttr ####     
    def listxattr(self, path): 
        inode=self._getinode(path)
        attrs = inode.get('attrs', {}) 
        return attrs.keys() 
    def getxattr(self, path, name, position=0): 
        inode=self._getinode(path)
        attrs = inode.get('attrs', {}) 
        try: 
            return attrs[name] 
        except KeyError: 
            return ''       # Should return ENOATTR ?
    def setxattr(self, path, name, value, options, position=0):         # Ignore options 
        inode=self._getinode(path)
        attrs = inode.setdefault('attrs', {}) 
        attrs[name] = value 
    def removexattr(self, path, name): 
        inode=self._getinode(path)
        attrs = inode.get('attrs', {}) 
        try: 
            del attrs[name] 
        except KeyError: 
            pass        # Should return ENOATTR ?
    #### Create or Remove Dir ####
    def readdir(self, path, fh): 
	inode_dir=self._getinode(path)
        dirnames=[name for name in inode_dir['i_data']]
	dirnames.append('..')
	return dirnames
    def mkdir(self, path, mode): 
	now=time()
        inode = dict(
		st_mode=(S_IFDIR | mode), 
		st_nlink=2, 
                st_size=0, 
		st_ctime=now, 
		st_mtime=now, 
		st_atime=now,
		st_uid=os.getuid(),
		st_gid=os.getgid(),
		i_data={}		# {}: real empty dir entries
		) 
        inode['st_ino']=id(inode)
        inode['i_data']['.']=inode
	self._adddir(path,inode)
    def rmdir(self, path): 
	dentry=os.path.split(path)
        inode_dir=self._getinode(dentry[0])
        inode_dir['i_data'].pop(dentry[1])
        inode_dir['st_nlink'] -= 1 
    #### Create or Remove File ####     
    def create(self, path, mode): 
        inode=dict(
		st_mode=(S_IFREG | mode), 
		st_nlink=1, 
		st_size=0, 
		st_ctime=time(), 
		st_mtime=time(), 
		st_atime=time(),
		st_uid=os.getuid(),
		st_gid=os.getgid(),
		i_data=''
		)
        inode['st_ino']=id(inode)
	self._addfile(path,inode)
        self.fd += 1 
        return self.fd 
    def unlink(self, path): 
        inode,inode_dir,filename=self._getinodedir(path)
	inode['st_nlink'] -= 1
        inode_dir['i_data'].pop(filename)
    def rename(self, old, new): 
        inode,old_inode_dir,old_filename=self._getinodedir(old)
	dentry_new=os.path.split(new)
	new_inode_dir=self._getinode(dentry_new[0])
	new_filename=dentry_new[1]
        mode=inode['st_mode']
        if S_ISDIR(mode):
	  new_inode_dir['i_data'][new_filename]=inode
	  new_inode_dir['st_nlink'] += 1 
	  old_inode_dir['i_data'].pop(old_filename)		# fix me !! if new==old , new is include old path
	  old_inode_dir['st_nlink'] -= 1 
	else:
	  new_inode_dir['i_data'][new_filename]=inode
	  old_inode_dir['i_data'].pop(old_filename)		# fix me !! if new==old
    def link(self, target, source):
        inode,old_inode_dir,old_filename=self._getinodedir(source)
	dentry_new=os.path.split(target)
	new_inode_dir=self._getinode(dentry_new[0])
	new_filename=dentry_new[1]
        if S_ISDIR(mode):					# fix me !!! no link if DIR
	  new_inode_dir['i_data'][new_filename]=inode
	  new_inode_dir['st_nlink'] += 1 
	  inode['st_nlink'] += 1 
	else:
	  new_inode_dir['i_data'][new_filename]=inode
	  inode['st_nlink'] += 1 
    #### Sym Link ####
    def readlink(self, path): 
        inode=self._getinode(path)
        return inode['i_data'] 
    def symlink(self, target, source): 
        source=self._check_symlink(target,source)	# if mount root then mount afio.lzo file
        # make normal symlink
	inode = dict(
		st_mode=(S_IFLNK | 0777), 
		st_nlink=1,
		st_size=len(source),
		st_uid=os.getuid(),
		st_gid=os.getgid(),
		i_data= source,
		) 
        inode['st_ino']=id(inode)
	self._addfile(target,inode)
    #### Read or Write File ####
    def open(self, path, flags): 
        self.fd += 1 
        return self.fd 
    def read(self, path, size, offset, fh): 
	print fh
        inode=self._getinode(path)
        data=inode.get('i_data',None)  
        if data==None:
          data=inode['i_readfunc'](inode)
	  inode['i_data']=data
        return data[offset:(offset + size)] 
    def write(self, path, data, offset, fh): 
        inode=self._getinode(path)
        data_old=inode.get('i_data',None)  
        if data_old==None:
          data_old=inode['i_readfunc'](inode)
	  inode['i_data']=data_old
	data_new=data_old[:offset] + data    ##### fix me !
        inode['i_data'] = data_new
        inode['st_size'] = len(data_new) 
        return len(data) 
    def truncate(self, path, length, fh=None): 
        inode=self._getinode(path)
        inode['i_data'] = inode['i_data'][:length] 
        inode['st_size'] = length 
    #### Release Flush Fsync ####
    def release(self, path, fh):
	print fh
        return 0
    def flush(self, path, fh):
	print fh
        return 0
    def fsync(self, path, datasync, fh):
	print fh
        return 0
    def fsyncdir(self, path, datasync, fh):
	print fh
        return 0
 
if __name__ == "__main__": 
    if len(argv) != 2: 
        print 'usage: %s <mountpoint>' % argv[0] 
        print '  make symlink "old ASCII cpio" or "afio" file in the mountpoint root to mount the archive'  
        exit(1)
    mountpoint=os.path.abspath(argv[1])
    afiolzofs= Afiolzofs(mountpoint)
    fuse = FUSE(afiolzofs, mountpoint,foreground=True) 

