#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2009 Maximillian Dornseif <md@hudora.de>
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.

"""
This script replicates databases from one CouchDB server to an other.

This is mainly for backup purposes or "priming" a new server before
setting up trigger based replication. But you can also use the
'--continuous' option to set up automatic replication on newer
CouchDB versions.

Use 'python replicate.py --help' to get more detailed usage instructions.
"""

from couchdb import http, client
import optparse
import sys
import time
import urllib
import urlparse
import fnmatch

def findpath(parser, s):
    '''returns (server url, path component)'''

    if s == '.':
        return client.DEFAULT_BASE_URL, ''
    if not s.startswith('http'):
        return client.DEFAULT_BASE_URL, s

    bits = urlparse.urlparse(s)
    res = http.Resource('%s://%s/' % (bits.scheme, bits.netloc), None)
    parts = bits.path.split('/')[1:]
    if not parts[-1]:
        parts = parts[:-1]

    cut = None
    for i in range(0, len(parts) + 1):
        try:
            data = res.get_json(parts[:i])[2]
        except Exception:
            data = None
        if data and 'couchdb' in data:
            cut = i

    if cut is None:
        raise parser.error("'%s' does not appear to be a CouchDB" % s)

    base = res.url + (parts[:cut] and '/'.join(parts[:cut]) or '')
    return base, '/'.join(parts[cut:])

def main():

    usage = '%prog [options] <source> <target>'
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('--continuous',
        action='store_true',
        dest='continuous',
        help='trigger continuous replication in cochdb')
    parser.add_option('--compact',
        action='store_true',
        dest='compact',
        help='compact target database after replication')

    options, args = parser.parse_args()
    if len(args) != 2:
        raise parser.error('need source and target arguments')

    # set up server objects

    src, tgt = args
    sbase, spath = findpath(parser, src)
    source = client.Server(sbase)
    tbase, tpath = findpath(parser, tgt)
    target = client.Server(tbase)

    # check database name specs

    if '*' in tpath:
        raise parser.error('invalid target path: must be single db or empty')
    elif '*' in spath and tpath:
        raise parser.error('target path must be empty with multiple sources')

    all = sorted(i for i in source if i[0] != '_') # Skip reserved names.
    if not spath:
        raise parser.error('source database must be specified')
    else:
        databases = [(i, i) for i in all if fnmatch.fnmatchcase(i, spath)]

    if not databases:
        raise parser.error("no source databases match glob '%s'" % spath)

    # do the actual replication

    for sdb, tdb in databases:

        start = time.time()
        print sdb, '->', tdb,
        sys.stdout.flush()

        if tdb not in target:
            target.create(tdb)
            print "created",
            sys.stdout.flush()

        sdb = '%s%s' % (sbase, urllib.quote(sdb, ''))
        if options.continuous:
            target.replicate(sdb, tdb, continuous=options.continuous)
        else:
            target.replicate(sdb, tdb)
        print '%.1fs' % (time.time() - start)
        sys.stdout.flush()

    if options.compact:
        for (sdb, tdb) in databases:
            print 'compact', tdb
            target[tdb].compact()

if __name__ == '__main__':
    main()

@@ -1059,24 +1059,34 @@
 %5B1:%5D%0A    if 
+parts and 
 not parts%5B-1

Allow URLs without a path component.