#!/usr/bin/python2.4
# -*- mode: python -*-
#
# Copyright (c) 2005 Specifix, Inc.
#
# This program is distributed under the terms of the Common Public License,
# version 1.0. A copy of this license should have been distributed with this
# source file in a file called LICENSE. If it is not present, the license
# is always available at http://www.opensource.org/licenses/cpl.php.
#
# This program is distributed in the hope that it will be useful, but
# without any waranty; without even the implied warranty of merchantability
# or fitness for a particular purpose. See the Common Public License for
# full details.
#

"""
convertcontents examines the old filesystem-based datastore (which is
used to store the pristine copies of config files) on a system to see
if there are missing contents, then converts it to the new SQL-based
datastore.  If the --repair flag is given, errors will be corrected
before the conversion is performed.  If unrepaired errors remain, the
conversion is not performed.
"""

import os
import sys
import shutil
if os.path.dirname(sys.argv[0]) != '.':
    if sys.argv[0][0] == '/':
	fullPath = os.path.dirname(sys.argv[0])
    else:
	fullPath = os.getcwd() + '/' + os.path.dirname(sys.argv[0])
else:
    fullPath = os.getcwd()

sys.path.append(os.path.dirname(fullPath))

import conarycfg
import datastore
import deps
import files
from lib import options, sha1helper, util
from repository import netclient
import sqlite3
from local import database

def usage():
    print 'usage: %s [--root rootpath] [--repair]' %sys.argv[0]

def getTroveInfo(sqldb, instanceId):
    "return troveName, version, and flavor for a given db's instanceId"
    cu = sqldb.db.cursor()
    cu.execute('SELECT troveName, flavor FROM DBInstances '
               '  JOIN DBFlavors on DBInstances.flavorId = DBFlavors.flavorId '
               'WHERE instanceId=?', (instanceId))
    troveName, frozenFlavor = cu.next()
    flavor = deps.deps.ThawDependencySet(frozenFlavor)
    version = sqldb.instances.getVersion(instanceId)
    return troveName, version, flavor

def main(argv):
    sys.excepthook = util.genExcepthook()
    cfg = conarycfg.ConaryConfiguration()

    argDef = {}
    argDef['repair'] = options.NO_PARAM

    cfgMap = {'root': 'root'}
    try:
        argSet, otherArgs = options.processArgs(argDef, cfgMap, cfg, usage,
                                                argv=argv)
    except options.OptionError, s:
        print s
        sys.exit(1)

    repair = argSet.has_key('repair')

    dbPath = os.sep.join((cfg.root, cfg.dbPath, 'conarydb'))
    db = database.Database(cfg.root, cfg.dbPath)
    sqldb = db.db

    contentsPath = os.sep.join((cfg.root, cfg.dbPath, 'contents'))
    try:
        contentsStore = datastore.DataStore(contentsPath)
    except IOError, e:
        print 'error opening filesystem-based data store:', str(e)
        sys.exit(1)

    # list of needed (fileId, fileVersion) tuples
    neededFiles = []
    # list of needed contents sha1 hashes
    neededSha1s = []
    sha1Counts = {}
    # errors becomes true if uncorrected errors remain
    errors = False

    print 'checking filesystem-based contents store for missing files'
    # loop over all the files in the database
    cu = sqldb.db.cursor()
    cu.execute('SELECT streamId, pathId, path, versionId, instanceId, stream '
               'FROM DBTroveFiles')
    for streamId, pathId, path, versionId, instanceId, stream in cu:
        f = files.ThawFile(stream, pathId)
        # if it's a config file, check to make sure that it is in the
        # data store
        if f.flags.isConfig():
            h = sha1helper.sha1ToString(f.contents.sha1())
            if not contentsStore.hasFile(h):
                # uh-oh, we're missing contents. print an error and make note
                troveName, troveVersion, flavor = getTroveInfo(sqldb, instanceId)
                fileVersion = sqldb.versionTable.getBareId(versionId)
                print troveName, troveVersion.asString(), path, \
                      '(sha1: %s)' %h, 'missing from data store'
                neededFiles.append((f.fileId(), fileVersion))
                neededSha1s.append(h)
            else:
                # excellent, not missing the contents.  let's count
                # how many instances we have
                sha1Counts[h] = sha1Counts.get(h, 0) + 1

    print 'checking filesystem-based contents store for broken refcounts'
    for h, count in sha1Counts.iteritems():
        path = contentsStore.hashToPath(h)
        countPath = path + '#'
        if os.path.exists(countPath):
            realCount = int(open(countPath, 'r').readline())
        else:
            realCount = 1
        if realCount != count:
            print ('count file incorrect for %s. '
                   'expected %d got %d' %(h, count, realCount))
            if repair:
                if count == 0:
                    # this can't happen
                    raise AssertionError
                elif count == 1:
                    # remove the link incorrect count file
                    os.unlink(countPath)
                else:
                    # write the correct value to the count file
                    # NOTE - there's no locking here, we're assuming
                    # that no one else is using the data store
                    f = open(countPath, 'w')
                    f.write('%d\n' %count)
                    f.close()
            else:
                errors = True

    if neededFiles:
        # we're missing files, repair or set errors to True
        if repair:
            print 'retrieving missing datastore contents from repository'
            # set up a network client using the repositoryMap - hopefully
            # it has servers for any version string that's in the database
            repos = netclient.NetworkRepositoryClient(cfg.repositoryMap)
            # request the contents we're missing
            contents = repos.getFileContents(neededFiles)
            print 'storing contents'
            # loop over the contents we got back and the list of sha1s
            # we're missing
            for fileObj, h in zip(contents, neededSha1s):
                # save the contents we got in the datastore
                contentsStore.addFile(fileObj.get(), h)
                # double check that the contents sha1 matches what we expect
                h2 = sha1helper.sha1String(contentsStore.openFile(h).read())
                h2 = sha1helper.sha1ToString(h2)
                assert(h == h2)
        else:
            errors = True

    # don't let broken datastores be converted
    if errors:
        print 'There are errors in the current data store.  Re-run this script'
        print 'with the --repair flag to correct them.'
        sys.exit(1)

    print 'converting filesystem-based datastore to SQL-based datastore'
    # loop over the streams again
    cu.execute('SELECT stream FROM DBTroveFiles')
    for (stream,) in cu:
        f = files.ThawFile(stream, None)
        if f.flags.isConfig():
            # add file from the old contents store to the new one
            h = sha1helper.sha1ToString(f.contents.sha1())
            f = contentsStore.openFile(h)
            db.contentsStore.addFile(f, h)
    db.commit()
    
    del contentsStore
    # the old contents store is of no use to use.  remove it.
    shutil.rmtree(contentsPath)
    
if __name__ == '__main__':
    main(sys.argv)

