view kharon.py @ 12:97fb91f12cb7

Added BSD license, TODOs, fixed a bug in config file writing.
author Atul Varma <varmaa@toolness.com>
date Mon, 08 Dec 2008 15:58:51 -0800
parents 220a8a38dedd
children 54668eda7f1d
line wrap: on
line source

#! /usr/bin/env python3.0
# ----------------------------------------------------------------------------
# Copyright (c) 2006, Atul Varma
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#
#   * Redistributions in binary form must reproduce the above
#     copyright notice, this list of conditions and the following
#     disclaimer in the documentation and/or other materials provided
#     with the distribution.
#
#   * Neither the name of Kharon nor the names of its
#     contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

'''
   This is Kharon, a drop-in replacement for rm, with support for undo.
'''

# TODO:
#
#   * Add support for expiration, so that if removed files aren't undeleted
#     within a certain amount of time, they are purged.
#
#   * Optional log messages for deletions.

import os
import re
import sys
import json
import subprocess
import distutils.dir_util
from optparse import OptionParser

HADES_DIR = os.path.expanduser('~/.Hades')
STATE_FILENAME = os.path.join(HADES_DIR, 'state.json')

class Config(object):
    def __init__(self, filename):
        self.__filename = filename
        self.nextid = 0
        if os.path.exists(self.__filename):
            self.__dict__.update(json.load(open(self.__filename, 'r')))
        else:
            self.save()

    def save(self):
        state = {}
        keys = [key for key in self.__dict__
                if not key.startswith('_')]
        for key in keys:
            state[key] = self.__dict__[key]
        json.dump(state, open(self.__filename, 'w'))

def shell(*params):
    popen = subprocess.Popen(params)
    popen.wait()
    if popen.returncode:
        raise Exception('Process failed: %s' % repr(params))

def dir_for_trans(transid):
    return os.path.join(HADES_DIR, '%.9d' % transid)

if __name__ == '__main__':
    if not (os.path.exists(HADES_DIR) and os.path.isdir(HADES_DIR)):
        shell('mkdir', HADES_DIR)

    parser = OptionParser(description=__import__(__name__).__doc__)
    parser.add_option('-u', '--undo',
                      dest='undo', action='store_true', default=False,
                      help='undo a transaction (default is latest).')
    parser.add_option('-v', '--verbose',
                      dest='verbose', action='store_true', default=False,
                      help='be verbose.')

    args = []
    for arg in sys.argv[1:]:
        # Filter out redundant parameters to the traditional 'rm'
        # command.
        if not re.match('-[dfiRrv]+', arg):
            args.append(arg)
        elif 'v' in arg:
            args.append('-v')
    (options, args) = parser.parse_args(args)

    config = Config(STATE_FILENAME)

    if options.undo:
        transactions = []
        for arg in args:
            try:
                transid = int(arg)
            except ValueError:
                print('Unknown transaction ID: %s' % arg)
                sys.exit(-1)
            transactions.append(transid)
        if not transactions and config.nextid:
            transactions.append(config.nextid - 1)
        for transid in transactions:
            dirname = dir_for_trans(transid)
            if not os.path.exists(dirname):
                print('Transaction ID %d does not exist.' % transid)
                sys.exit(-1)

        transactions.sort(reverse=True)

        for transid in transactions:
            transdirname = dir_for_trans(transid)
            for dirpath, dirnames, filenames in os.walk(transdirname):
                relpath = dirpath[len(transdirname)+1:]
                contents = dirnames + filenames
                for name in contents:
                    srcpath = os.path.join(dirpath, name)
                    destpath = os.path.join('/', relpath, name)
                    if not os.path.exists(destpath):
                        if os.path.isdir(srcpath):
                            dirnames.remove(name)
                        print('Restoring %s.' % destpath)
                        shell('mv', srcpath, destpath)
        print('Done.')
    else:
        if not args:
            parser.print_help()
            sys.exit(-1)

        files = []
        for arg in args:
            filename = os.path.abspath(os.path.expanduser(arg))
            if not os.path.exists(filename):
                print('File does not exist: %s' % arg)
                sys.exit(-1)
            realpath = os.path.realpath(filename)
            if (realpath.startswith(HADES_DIR) or
                HADES_DIR.startswith(realpath)):
                print('Cannot move files in or above %s.' % HADES_DIR)
                sys.exit(-1)
            files.append(filename)

        thisid = config.nextid
        basedir = dir_for_trans(thisid)
        shell('mkdir', basedir)
        config.nextid = thisid + 1
        config.save()

        print('The transaction ID for this operation is %s.' % thisid)

        for source in files:
            if options.verbose:
                print('Removing %s.' % source)
            dest = os.path.join(basedir, source[1:])
            distutils.dir_util.mkpath(os.path.dirname(dest))
            shell('mv', source, dest)