#!/usr/bin/env python
### depends on https://github.com/xolox/python-rotate-backups -- check this url for understanding rotating parameters
import argparse

import os
import subprocess
import shutil
import zipfile
import datetime
import tempfile
from contextlib import contextmanager

### READ INPUT
parser = argparse.ArgumentParser(description='Odoo backup tool.')
parser.add_argument('-d', '--database', dest='database', nargs='+', help='database for backup')
parser.add_argument('--no-save-filestore', dest='save_filestore', action='store_false', help='skip filestore to save disk space')
parser.add_argument('--no-rotate', dest='rotate', action='store_false', help='skip backups rotating')
parser.add_argument('-p', '--path', dest='path', default='/tmp/', help='path to save backup')
parser.add_argument('-c', '--odoo-config', dest='odoo_config', default='/etc/odoo/odoo-server.conf', help='odoo config file')
parser.add_argument('--hourly', dest='hourly', default='24', help='how many hourly backups to preserve')
parser.add_argument('--daily', dest='daily', default='7', help='how many daily backups to preserve')
parser.add_argument('--weekly', dest='weekly', default='4', help='how many weekly backups to preserve')
parser.add_argument('--monthly', dest='monthly', default='12', help='how many monthly backups to preserve')
parser.add_argument('--yearly', dest='yearly', default='always', help='how many yearly backups to preserve')

#parser.add_argument('--odoo-source', dest='odoo_source', default='/usr/local/src/odoo/', help='odoo source dir')

args = parser.parse_args()

def get_odoo_config():
    import ConfigParser
    p = ConfigParser.ConfigParser()
    p.read(args.odoo_config)
    res = {}
    for (name,value) in p.items('options'):
        if value=='True' or value=='true':
            value = True
        if value=='False' or value=='false':
            value = False
        res[name] = value
    return res

odoo_config = get_odoo_config()


### EXECUTE


#@_set_pg_password_in_environment # see openerp/service/db.py
def dump_sql(db, dump_file):
    cmd = ['pg_dump', '--format=p', '--no-owner', '--file=' + dump_file]
    if odoo_config.get('db_user'):
        cmd.append('--username=' + odoo_config.get('db_user'))
    if odoo_config.get('db_host'):
        cmd.append('--host=' + odoo_config.get('db_host'))
    if odoo_config.get('db_port'):
        cmd.append('--port=' + str(odoo_config.get('db_port')))
    cmd.append(db)

    if exec_pg_command(*cmd):
        print ' '.join(cmd)
        raise Exception("Couldn't dump database")

def backup(db, dump_dir):
    odoo_data_dir = odoo_config.get('data_dir', '~/.local/share/Odoo/')
    filestore = os.path.join(odoo_data_dir, 'filestore', db)
    if args.save_filestore:
        os.symlink(filestore, os.path.join(dump_dir, 'filestore'))

    dump_file = os.path.join(dump_dir, 'dump.sql')
    dump_sql(db, dump_file)

    dump_archive  = "%(db)s_%(timestamp)s_%(mark)s.dump" % {
        'db': db,
        'timestamp': datetime.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%SZ"),
        'mark': 'full' if args.save_filestore else 'quick',
    }
    with open(dump_archive, 'w') as stream:
        zip_dir(dump_dir, stream, include_dir=False)
    return dump_archive

def rotate(backup_dir):
    cmd = ['rotate-backups']
    for period in ('hourly', 'daily', 'weekly', 'monthly', 'yearly'):
        cmd.extend(['--%s' % period, getattr(args, period) ] )
    cmd.append(backup_dir)
    cmd.extend(['2>', '/dev/null'])
    os.system(' '.join(cmd))

def main():
    for db in args.database:
        backup_dir = os.path.join(args.path, db, 'full' if args.save_filestore else 'quick')
        if not os.path.exists(backup_dir):
            os.system('mkdir -p %s' % backup_dir)

        with tempdir() as dump_dir:
            dump_archive = backup(db, dump_dir)
            shutil.move(dump_archive, os.path.join(backup_dir, dump_archive))

        if args.rotate:
            rotate(backup_dir)

### TOOLS

def find_pg_tool(name):
    path = None
    #if config['pg_path'] and config['pg_path'] != 'None':
    #    path = config['pg_path']
    try:
        return which(name, path=path)
    except IOError:
        return None

def exec_pg_command(name, *args):
    prog = find_pg_tool(name)
    if not prog:
        raise Exception('Couldn\'t find %s' % name)
    args2 = (prog,) + args

    with open(os.devnull) as dn:
        return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)

def zip_dir(path, stream, include_dir=True):      # TODO add ignore list
    path = os.path.normpath(path)
    len_prefix = len(os.path.dirname(path)) if include_dir else len(path)
    if len_prefix:
        len_prefix += 1

    with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as zipf:
        for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
            for fname in filenames:
                bname, ext = os.path.splitext(fname)
                ext = ext or bname
                if ext not in ['.pyc', '.pyo', '.swp', '.DS_Store']:
                    path = os.path.normpath(os.path.join(dirpath, fname))
                    if os.path.isfile(path):
                        zipf.write(path, path[len_prefix:])

@contextmanager
def tempdir():
    tmpdir = tempfile.mkdtemp()
    try:
        yield tmpdir
    finally:
        shutil.rmtree(tmpdir)

import sys
from os import access, defpath, pathsep, environ, F_OK, R_OK, W_OK, X_OK
from os.path import exists, dirname, split, join

windows = sys.platform.startswith('win')

defpath = environ.get('PATH', defpath).split(pathsep)

if windows:
    defpath.insert(0, '.') # can insert without checking, when duplicates are removed
    # given the quite usual mess in PATH on Windows, let's rather remove duplicates
    seen = set()
    defpath = [dir for dir in defpath if dir.lower() not in seen and not seen.add(dir.lower())]
    del seen

    defpathext = [''] + environ.get('PATHEXT',
        '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC').lower().split(pathsep)
else:
    defpathext = ['']

def which_files(file, mode=F_OK | X_OK, path=None, pathext=None):
    """ Locate a file in a path supplied as a part of the file name,
        or the user's path, or a supplied path.
        The function yields full paths (not necessarily absolute paths),
        in which the given file name matches an existing file in a directory on the path.

        >>> def test_which(expected, *args, **argd):
        ...     result = list(which_files(*args, **argd))
        ...     assert result == expected, 'which_files: %s != %s' % (result, expected)
        ...
        ...     try:
        ...         result = [ which(*args, **argd) ]
        ...     except IOError:
        ...         result = []
        ...     assert result[:1] == expected[:1], 'which: %s != %s' % (result[:1], expected[:1])

        >>> if windows: cmd = environ['COMSPEC']
        >>> if windows: test_which([cmd], 'cmd')
        >>> if windows: test_which([cmd], 'cmd.exe')
        >>> if windows: test_which([cmd], 'cmd', path=dirname(cmd))
        >>> if windows: test_which([cmd], 'cmd', pathext='.exe')
        >>> if windows: test_which([cmd], cmd)
        >>> if windows: test_which([cmd], cmd, path='<nonexistent>')
        >>> if windows: test_which([cmd], cmd, pathext='<nonexistent>')
        >>> if windows: test_which([cmd], cmd[:-4])
        >>> if windows: test_which([cmd], cmd[:-4], path='<nonexistent>')

        >>> if windows: test_which([], 'cmd', path='<nonexistent>')
        >>> if windows: test_which([], 'cmd', pathext='<nonexistent>')
        >>> if windows: test_which([], '<nonexistent>/cmd')
        >>> if windows: test_which([], cmd[:-4], pathext='<nonexistent>')

        >>> if not windows: sh = '/bin/sh'
        >>> if not windows: test_which([sh], 'sh')
        >>> if not windows: test_which([sh], 'sh', path=dirname(sh))
        >>> if not windows: test_which([sh], 'sh', pathext='<nonexistent>')
        >>> if not windows: test_which([sh], sh)
        >>> if not windows: test_which([sh], sh, path='<nonexistent>')
        >>> if not windows: test_which([sh], sh, pathext='<nonexistent>')

        >>> if not windows: test_which([], 'sh', mode=W_OK)  # not running as root, are you?
        >>> if not windows: test_which([], 'sh', path='<nonexistent>')
        >>> if not windows: test_which([], '<nonexistent>/sh')
    """
    filepath, file = split(file)

    if filepath:
        path = (filepath,)
    elif path is None:
        path = defpath
    elif isinstance(path, str):
        path = path.split(pathsep)

    if pathext is None:
        pathext = defpathext
    elif isinstance(pathext, str):
        pathext = pathext.split(pathsep)

    if not '' in pathext:
        pathext.insert(0, '') # always check command without extension, even for custom pathext

    for dir in path:
        basepath = join(dir, file)
        for ext in pathext:
            fullpath = basepath + ext
            if exists(fullpath) and access(fullpath, mode):
                yield fullpath

def which(file, mode=F_OK | X_OK, path=None, pathext=None):
    """ Locate a file in a path supplied as a part of the file name,
        or the user's path, or a supplied path.
        The function returns full path (not necessarily absolute path),
        in which the given file name matches an existing file in a directory on the path,
        or raises IOError(errno.ENOENT).

        >>> # for doctest see which_files()
    """
    try:
        return iter(which_files(file, mode, path, pathext)).next()
    except StopIteration:
        try:
            from errno import ENOENT
        except ImportError:
            ENOENT = 2
        raise IOError(ENOENT, '%s not found' % (mode & X_OK and 'command' or 'file'), file)


if __name__ == '__main__':
    main()