view profilemanager/manager.py @ 40:34c740d1962d

abstract out adding an entry to profiles.ini
author Jeff Hammel <jhammel@mozilla.com>
date Thu, 06 May 2010 18:53:24 -0700
parents 30fc269a15c8
children 769447f8cd08
line wrap: on
line source

"""
manage Mozilla/Firefox profiles
"""

import os
import shutil
import string
import ConfigParser
from random import Random
from utils import format_tabular
from ConfigParser import SafeConfigParser as ConfigParser

# Changes to profiles.ini:
# - add a ``backups`` field for each profile:
# Backups = /path/to/backup:1273104310 /path/to/other/backup:1273104355

class ProfileNotFound(Exception):
    """
    exception when a profile is specified but is not present in a given
    .ini file
    """

class ProfileManager(object):

    def __init__(self, profiles):
        """
        - profiles: filesystem path to profiles.ini file
        """
        self.profiles = profiles
        self.profile_dir = os.path.abspath(os.path.dirname(profiles))

    ### (public) API

    def new(self, name, directory=None, hash=True):
        """
        generate a new clean profile
        - name: name of the profile to generate
        - directory: where to create the profile [DEFAULT: relative to profiles.ini]
        - hash: whether to generate a a random hash tag
        """

        # path to the profile directory
        dirname = name
        relative = False
        if hash:
            dirname = '%s.%s' % (self.hash(), dirname)
        if not directory:
            directory = self.profile_dir
            relative = True
        path = os.path.join(directory, dirname)

        # create directory
        # TODO: (optionally) pre-populate the directory a la Firefox
        os.mkdir(path)

        # update profiles.ini
        self.add(name, relative and dirname or path, relative)
        
        # return the directory name
        return path

    def remove(self, name, delete=True):
        """
        remove a profile from profiles.ini
        - delete: delete the profile directory as well
        """
        parser = self.parser()
        section = self.section(name, parser)
        if section is None:
            raise ProfileNotFound('Profile %s not found in %s' % (profile, self.profiles))
        if delete: # remove the profile from disk
            shutil.rmtree(self.path(name))
        parser.remove_section(section)
        parser.write(file(self.profiles, 'w'))

    def list(self, directories=False):
        """
        lists the profiles available in the config file
        - directories : display the directories
        """
        profiles = self.profiles_dict()
        if not directories:
            return sorted(profiles.keys())
        return dict([(name, self.path(name)) for name in profiles.keys()])

    def clone(self, source, dest, add=True):
        """
        clones the profile `source` and output to `dest`
        - add: add the profile to the profiles.ini file
        """
        source_path = self.path(source)    # fs path of the `from` profile

        # dest: fs path to back up to
        relative = False
        if os.path.sep in dest:
            if not os.path.isabs(dest):
                dest = os.path.abspath(dest)
            dirname = dest
            name = os.path.basename(dest)
        else:
            dirname = name = dest
            relative = True
            if not os.path.dirname(dest):
                dest = '%s.%s' % (self.hash(), dest)
            dest = os.path.join(self.profile_dir, dest)

        # ensure name is not already present
        assert name not in self.profiles_dict(), 'Profile "%s" already in %s' % (name, self.profiles)

        # update profiles.ini
        if add:
            self.add(name, dirname, relative)

        # copy the files
        shutil.copytree(source_path, dest, symlinks=False)
            

    def backup(self, profile, dest=None):
        """
        backup the profile
        - profile: name of the profile to be backed up
        - dest: name of the destination (optional)
        """
        if dest is None:
            dest = '%s.%d.bak' % (profile, int(time.time()))
        self.clone(profile, dest, hash=False)
        # TODO: add something like
        # `Backup=$(profile)s.$(datestamp)s.bak`
        # to self.profiles

    def backups(self, profile=None):
        """
        list backups for a given profile, or all profiles if the
        profile is not given;  returns a list of backups if
        profile is given or a dictionary of lists otherwise
        """
        if profile is None:
            # all profiles
            retval = {}
            return retval
        # TODO

    def restore(self, profile, date=None, delete=False):
        """
        restore the profile from a backup
        the most recent backup is used unless `date` is given
        - date : date to restore from
        - delete : delete the backup after restoration
        """

        # get the possible backups
        backups = self.backups(profile)
        # TODO: check to see if these all exist (print warnings if not)

        # restore the backup over ``profile``

        if delete: # delete the backup
            # delete the directory
            # delete the entry from ``profiles.ini``
            # if there are no backups, delete the ``backups`` line
            pass #TODO
        

    def merge(self, output, *profiles):
        """merge a set of profiles (not trivial!)"""
        raise NotImplementedError

    ### internal functions

    def add(self, profile, path, relative=True):
        """
        add a profile entry to profiles.ini
        """
        assert name not in self.profiles_dict(), 'Profile "%s" already in %s' % (name, self.profiles)
        parser = self.parser()
        ctr = 0
        section = 'Profile%d' % ctr # unsure of this naming convention
        while section in parser.sections():
            ctr += 1
            section = 'Profile%d' % ctr
        parser.add_section(section)
        parser.set(section, 'Name', profile)
        parser.set(section, 'IsRelative', '%d' % int(relative))
        parser.set(section, 'Path', relative and dirname or path)
        if not ctr:
            parser.set(section, 'Default', '1')
        parser.write(file(self.profiles, 'w'))


    def path(self, profile):
        """returns the path to the profile"""
        profile = self.profile_dict(profile)
        if profile.get('isrelative', None) == '1':
            return os.path.join(self.profile_dir, profile['path'])
        return profile['path']

    def parser(self):
        """
        return a ConfigParser instance appropriate to profiles.ini
        """
        parser = ConfigParser()
        parser.read(self.profiles)
        return parser


    def section(self, profile, parser=None):
        """
        returns the name of the section that a profile is in or None
        if not found
        """
        if parser is None:
            parser = self.parser()
        for section in parser.sections():
            if not parser.has_option(section, 'name'):
                continue # not a profile
            if parser.get(section, 'name') == profile:
                return section

    def profile_dict(self, profile):
        """
        return option dictionary for a single profile
        """
        parser = self.parser()
        section = self.section(profile, parser)
        if section is None:
            raise ProfileNotFound('Profile %s not found in %s' % (profile, self.profiles))
        return dict(parser.items(section))

    def profiles_dict(self):
        """
        return nested dict of all profiles
        """
        # assumes profiles have unique names
        parser = self.parser()
        retval = {}
        for section in parser.sections():
            if section == 'General':
                continue
            try:
                name = parser.get(section, 'name')
            except ConfigParser.NoOptionError:
                continue
            retval[name] = self.profile_dict(name)
        return retval


    def hash(self):
        """
        generate a random hash for a new profile
        """
        population = string.lowercase + string.digits
        return ''.join(Random().sample(population, 8))