"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/stored_file.py $
$Id: stored_file.py 23441 2004-02-05 22:00:46Z dbinger $

Store a file in the filesystem.
"""

import os, string, grp
import shutil
import time
from mx import DateTime
from dulcinea import site_util
from dulcinea.spec import any, mapping
from dulcinea.typeutils import typecheck
from dulcinea.base import DulcineaPersistent, PersistentMapping
from dulcinea.user import DulcineaUser

ALLOWABLE_SPECIAL_CHARS = "-_.@"
_last_name = None


def path_check(path):
    """(path:string) -> string | None

    Return a lower-case and normalized version of the 'path' after
    checking for allowable characters.
    Return None if path has '..' or any special characters
    other than what's in string.lowercase, string.digits, and
    the ALLOWABLE_SPECIAL_CHARS string
    """
    if '..' in path:
        return None
    path = path.lower()
    for letter in path:
        if (letter != '/' and letter not in string.lowercase and
            letter not in string.digits and
            letter not in ALLOWABLE_SPECIAL_CHARS):
            return None
    return path


class StoredFile (DulcineaPersistent):
    """Class for reading from or writing to a stored file

    A StoredFile is a wrapper around a file in a place in the
    filesystem known as the 'file-store'.

    StoredFiles can be instantiated with a fresh source file to move a new
    file into the file-store, or instantiated by referencing an existing
    file in the file-store.

    Instance attributes:
      path : string
        Path to the physical file containing the stored file's data.
      full_path : string
        The full path with nothing omitted.
      filename : string
        Filename to use when downloading this file
      mime_type : string
        MIME type of the contents of this file.
      description : string
        Description of this file.
      owner : DulcineaUser
        The file's owner
      date : DateTime
        timestamp
    """

    MIME_TYPES = [("image/gif", "GIF image"),
                  ("image/jpeg", "JPEG image"),
                  ("image/png", "PNG image"),
                  ("image/tiff", "TIFF image"),
                  ("image/x-ms-bmp", "BMP image"),
                  ("text/plain", "plain text"),
                  ("text/html", "HTML text"),
                  ("text/rtf", "richtext document"),
                  ("application/pdf", "PDF document"),
                  ("application/msword", "Word document"),
                  ("application/vnd.ms-powerpoint", "PowerPoint document"),
                  ("application/postscript", "PostScript file"),
                  ("application/octet-stream", "Binary data")]


    path_is = str
    full_path_is = str
    filename_is = str
    mime_type_is = any(*[pair[0] for pair in MIME_TYPES])
    description_is = any(str, None)
    owner_is = DulcineaUser
    date_is = DateTime.DateTimeType

    def __init__(self, path, root_dir, source_path=None):
        """(path: str, root_dir : string, source_path : string)

        If 'source_path' exists, copy the file specified to the file-store
        location specified in 'path'.  'path' specifies a relative path, so
        the full path is root_dir + path.

        If 'path' contains directories, traverse into those directories
        creating them as needed. 'source_path' must be readable if specified.
        """
        self.description = None
        self.owner = None
        self.date = DateTime.now()

        # Check path for safety (no '..' components, that sort of thing)
        path = os.path.normpath(path)
        self.path = path_check(path)
        assert self.path, 'Bad name: %r' % path
        self.filename = os.path.basename(self.path)

        self.full_path = os.path.join(root_dir, self.path)
        assert not os.path.exists(self.full_path), (
            'path %r already exists' % self.full_path)

        if source_path:
            # Copy the file specified in source_path into the file-store
            f = open(source_path, mode='rb')
            f2 = self.open(mode='wb')
            shutil.copyfileobj(f, f2)
            f.close()
            f2.close()

        self.mime_type = self.guess_mime_type()

    def __repr__(self):
        return '<%s: path=%r>' % (self.__class__.__name__, self.path)

    def get_path(self):
        return self.path

    def guess_mime_type(self):
        p = os.popen("file -b --mime '%s' 2>/dev/null" % self.get_full_path())
        mime_type = p.read().strip()
        if ';' in mime_type:
            mime_type = mime_type.split(';')[0]
        status = p.close() # 'file' seems to always return error status 0!
        # only allow MIME types we know about
        for (type, description) in self.MIME_TYPES:
            if type == mime_type:
                break
        else:
            mime_type = "application/octet-stream"
        return mime_type

    def open(self, mode='rb'):
        """(mode:string) -> file
        Returns a Python file object opened for reading or writing,
        depending on the 'mode' parameter.  This can be used to read or
        update the file's content.
        """
        full_path = self.get_full_path()
        if 'w' in mode:
            dir = os.path.dirname(full_path)
            if not os.path.exists(dir):
                os.makedirs(dir, mode=0775)
        file = open(full_path, mode=mode)
        return file

    def chgrp(self, grp_id):
        """(grp_id : str | int)
        Change the group ownership of this file to the specified ID.
        """
        if isinstance(grp_id, str):
            grnam, grpass, gid, grmembers = grp.getgrnam(grp_id)
            grp_id = gid
        os.chown(self.get_full_path(), -1, grp_id)

    def set_mime_type(self, mime_type):
        """(mime_type:string)
        Sets the MIME type for the file.
        """
        # Check there's a single '/' in the MIME type
        if mime_type.count('/') != 1:
            raise ValueError, "Invalid MIME type %r" % mime_type
        self.mime_type = mime_type

    def get_mime_type(self):
        return self.mime_type

    def set_filename(self, filename):
        self.filename = filename

    def get_filename(self):
        return self.filename

    def set_description(self, description):
        self.description = description

    def get_description(self):
        return self.description

    def set_date(self, date):
        typecheck(date, DateTime.DateTimeType)
        self.date = date

    def get_date(self):
        return self.date

    def set_owner(self, owner):
        typecheck(owner, DulcineaUser)
        self.owner = owner

    def get_owner(self):
        return self.owner

    def get_size(self):
        """() -> int
        Return the size of the file, measured in bytes, or None if
        the file doesn't exist.
        """
        path = self.get_full_path()
        if not os.path.exists(path):
            return None
        stats = os.stat(path)
        return stats.st_size

    def get_full_path(self):
        """() -> string
        """
        return self.full_path

    def has_manage_access(self, user):
        return (user is self.owner or user.is_admin())


class FileDatabase (DulcineaPersistent):
    """
    Instance attributes:
      root_directory: string
      files : PersistentMapping { string : StoredFile }
      """

    root_directory_is = str
    files = mapping({str: StoredFile}, PersistentMapping)

    def __init__(self, root_directory=None):
        if root_directory is None:
            site = os.environ['SITE']
            config = site_util.get_config()
            root_directory = config.get(site, 'file-store',
                                        fallback='/tmp')
        self.root_directory = root_directory
        self.files = PersistentMapping()

    def get_file(self, path):
        return self.files[path]

    def new_file(self, source_path=None, allowed_mime_types=None):
        """(source_path : string, allowed_mime_types : [string]) -> StoredFile
        Returns a new StoredFile object.  The path for the file is generated
        using the current time, and will be unique (Note: for this process).
        """
        global _last_name
        while 1:
            now = str(long(10*time.time()))
            if now != _last_name:
                break
            time.sleep(0.1)
        _last_name = now
        path = '%s/%s/%s/' % (now[-1], now[-3:-1], now)
        sf = StoredFile(path, self.root_directory, source_path=source_path)
        if allowed_mime_types and sf.get_mime_type() not in allowed_mime_types:
            raise TypeError, "MIME type '%s' not allowed, allowed types: %s" % (
                sf.get_mime_type(), allowed_mime_types)
        self.add_file(sf)
        return sf

    def add_file(self, stored_file):
        self.files[stored_file.path] = stored_file

    def remove_file(self, stored_file, unlink=False):
        del self.files[stored_file.path]
        if unlink:
            os.unlink(stored_file.get_full_path())
