#!/usr/bin/env python
# -*- Mode: Python; tab-width: 4 -*-
#
# Zope Dumper - A Zope Exporting tool
#
# Copyright (C) 2005-2015 Gianluigi Tiesi <sherpya@netfarm.it>
# Copyright (C) 2005-2015 NetFarm S.r.l.  [http://www.netfarm.it]
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
# ======================================================================
## @file zopedumper.py
## Zope Export Tool

'''Zope Dumper - A Zope Exporting tool'''
__version__ = '2.0'

import os
import sys
import httplib
import socket
from getopt import getopt, GetoptError
from urlparse import urlparse
from gzip import GzipFile
from bz2 import BZ2File

try:
    import xml.etree.cElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET

BLACKLIST = [ '/', '/Control_Panel/', '/temp_folder/' ]
EXPORTURL = 'manage_exportObject?download:int=-1'
BLOCKSIZE = 16 * 1024

SHORTOPT = 'hvt:snz:j:u:p:'
LONGOPT  = [ 'help', 'verbose', 'progress', 'dry-run', 'gzip=', 'bzip2=', 'timeout=', 'username=', 'password=' ]

def usage():
    print '''usage: %s [options] base-url out-dir
        options:
        -h, --help           Get usage help
        -v, --verbose        Verbose execution
        -s, --progress       Display progress while downloading (implies verbose)
        -n, --dry-run        Dry run (no actions)
        -z, --gzip=level     Compress output files using gzip
        -j, --bzip2=level    Compress output files using bzip2
        -t, --timeout=sec    Set reponse timeout
        -u, --username=user  Basic auth username
        -p, --password=pass  Basic auth password''' % sys.argv[0]
    sys.exit(1)

def getFolderList(conn, extra_hdrs, path):
    headers = extra_hdrs.copy()
    headers['Content-Type'] = 'text/xml; charset="utf-8"'
    headers['Depth'] = '1'

    conn.request('PROPFIND', path, None, headers)
    response = conn.getresponse(False)

    if response.status != httplib.MULTI_STATUS:
        response.close()
        if response.status == httplib.UNAUTHORIZED:
            raise Exception('Unauthorized to access the resource %s' % path)
        elif response.status == httplib.NOT_FOUND:
            raise Exception('Non-existent path %s' % path) 
        else:
            raise Exception('Invalid response status code %s' % response.status)

    content = response.read()
    response.close()

    root = ET.fromstring(content)

    for response in root.findall('.//{DAV:}response'):
        href = response.find('{DAV:}href')
        if href is None or href.text in BLACKLIST: continue
        resourcetype = response.find('.//{DAV:}resourcetype')
        if resourcetype is not None and resourcetype.find('{DAV:}collection') is not None:
            yield href.text

def dumpFolder(conn, extra_hdr, path, filename):
    if dryrun:
        print 'I would export %s to %s' % (path, filename)
        return

    url = path + EXPORTURL
    conn.request('GET', url, None, extra_hdr)
    response = conn.getresponse(False)

    status = response.status
    if status != httplib.OK:
        # Bobo-Exception-Type gone?
        error = response.msg.getheader('Bobo-Exception-Type', httplib.responses.get(status, 'Unknown Error'))
        raise Exception('Error Dumping %s: %d %s' % (url, status, error))

    if not response.length:
        raise Exception('Zero length response')

    ctype = response.msg.getheader('Content-Type') 
    if ctype != 'application/data':
        raise Exception('Invalid response Content-Type: %s' % ctype) 

    if verbose:
        sys.stdout.write('Exporting %s to %s' % (path, filename))
        if progress:
            sys.stdout.write(':   0%')
        else:
            sys.stdout.write('\n')
        sys.stdout.flush()

    total = response.length
    mul = 100.0 / total

    parent = os.path.dirname(filename)
    try:
        os.makedirs(parent)
    except OSError: pass

    if not os.path.isdir(parent):
        raise Exception('Unable to create %s' % parent)

    with writer(filename, 'wb', level) as fd:
        last = 0
        while True:
            data = response.read(BLOCKSIZE)

            if not data:
                break

            fd.write(data)

            if not progress:
                continue

            perc = int(100.0 - (response.length * mul))
            if perc != last:
                last = perc
                sys.stdout.write('\b\b\b\b%3d%%' % perc)
                sys.stdout.flush()

    if progress:
        sys.stdout.write('\n')
        sys.stdout.flush()

    if response.length:
        raise Exception('Error the file is incomplete')

def uncompressed(filename, mode, compress):
    return open(filename, mode)

if __name__ == '__main__':
    try:
        opts, args = getopt(sys.argv[1:], SHORTOPT, LONGOPT)
    except GetoptError:
        usage()

    if len(args) != 2:
        usage()

    baseurl = args[0]
    outdir = args[1]

    headers  = { 'User-Agent' : 'Zope Dumper v%s' % __version__ }
    username = password = None
    verbose  = progress = dryrun = False
    gzip     = bzip2 = None
    timeout  = socket._GLOBAL_DEFAULT_TIMEOUT
    writer   = uncompressed
    level    = fileext  = ''

    for opt, arg in opts:
        if opt in ('-h', '--help'):
            usage()

        if opt in ('-v', '--verbose'):
            verbose = True
        elif opt in ('-s', '--progress'):
            progress = True
        elif opt in ('-n', '--dry-run'):
            dryrun = True
        elif opt in ('-z', '--gzip'):
            level = gzip = arg
            writer = GzipFile
            fileext = '.gz'
        elif opt in ('-j', '--bzip2'):
            level = bzip2 = arg
            writer = BZ2File
            fileext = '.bz2' 
        elif opt in ('-u', '--username'):
            username = arg
        elif opt in ('-p', '--password'):
            password = arg
        elif opt in ('-t', '--timeout'):
            try:
                timeout = float(arg)
            except ValueError:
                print 'Invalid timeout value', arg
                sys.exit(1)

    if (gzip is not None) and (bzip2 is not None):
        print 'gzip and bzip2 compressions are exclusive'
        sys.exit(1)

    if level == '': level = 9
    try:
        level = int(level)
        if not 0 <= level <= 9:
            raise ValueError
    except ValueError:
        print 'Invalid compression level %s' % level

    if progress:
        verbose = True

    if username and password:
        authcookie = '%s:%s' % (username, password)
        headers['Authorization'] = 'Basic %s' % authcookie.encode('base64').strip()

    if verbose and (timeout != socket._GLOBAL_DEFAULT_TIMEOUT):
        print 'Response timeout is %f secs' % timeout

    base = urlparse(baseurl)

    if base.scheme not in ('http', 'https'):
        print 'Unsupported URL scheme in %s' % baseurl
        sys.exit(1)

    conn = httplib.HTTPConnection(base.hostname, base.port)
    for folder in getFolderList(conn, headers, base.path):
        filename = os.path.join(outdir, folder[1:-1] + '.zexp' + fileext)
        try:
            dumpFolder(conn, headers, folder, filename)
        finally:
            conn.close()
