Mercurial > hrm
changeset 15:a628c9eac0ba
Renamed Kharon to hrm, for 'humane rm' or 'hades rm'.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Tue, 09 Dec 2008 13:20:34 -0800 |
parents | 1b35fa4d855e |
children | 14f4251b156c |
files | hrm.py kharon.py |
diffstat | 2 files changed, 208 insertions(+), 208 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hrm.py Tue Dec 09 13:20:34 2008 -0800 @@ -0,0 +1,208 @@ +#! /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: +# +# * Consider adding support for "max size" based removal, so that +# if a certain quota is exceeded, the minimum number of files are +# removed to satisfy the quota. +# +# * Optional log messages for deletions. +# +# * Make the transactions atomic, so that if an error occurs partway +# through removing a set of files, the ones that were deleted +# before the error happened are undeleted. + +import os +import re +import sys +import json +import subprocess +import distutils.dir_util +import time +from optparse import OptionParser + +# Number of hours that must pass before we purge removed files. +HOURS_TO_PURGE = 48 + +# The above value expressed as seconds. +SECS_TO_PURGE = (60 * # Seconds in a minute + 60 * # Minutes in an hour + HOURS_TO_PURGE) + +# Directory to store state information and removed files (for undo). +HADES_DIR = os.path.expanduser('~/.Hades') + +# Where to store state information. +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) + +def purge_old_files(verbose): + paths = [os.path.abspath(os.path.join(HADES_DIR, name)) + for name in os.listdir(HADES_DIR)] + + min_mtime = time.time() - SECS_TO_PURGE + to_purge = [path for path in paths + if os.path.isdir(path) + and os.stat(path).st_mtime < min_mtime] + + if verbose: + ids = [str(int(os.path.split(dirname)[-1])) for + dirname in to_purge] + print('Purging transactions: %s' % ', '.join(ids)) + + for dirname in to_purge: + distutils.dir_util.remove_tree(dirname) + +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) + + purge_old_files(options.verbose) + + 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)
--- a/kharon.py Tue Dec 09 13:19:00 2008 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,208 +0,0 @@ -#! /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: -# -# * Consider adding support for "max size" based removal, so that -# if a certain quota is exceeded, the minimum number of files are -# removed to satisfy the quota. -# -# * Optional log messages for deletions. -# -# * Make the transactions atomic, so that if an error occurs partway -# through removing a set of files, the ones that were deleted -# before the error happened are undeleted. - -import os -import re -import sys -import json -import subprocess -import distutils.dir_util -import time -from optparse import OptionParser - -# Number of hours that must pass before we purge removed files. -HOURS_TO_PURGE = 48 - -# The above value expressed as seconds. -SECS_TO_PURGE = (60 * # Seconds in a minute - 60 * # Minutes in an hour - HOURS_TO_PURGE) - -# Directory to store state information and removed files (for undo). -HADES_DIR = os.path.expanduser('~/.Hades') - -# Where to store state information. -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) - -def purge_old_files(verbose): - paths = [os.path.abspath(os.path.join(HADES_DIR, name)) - for name in os.listdir(HADES_DIR)] - - min_mtime = time.time() - SECS_TO_PURGE - to_purge = [path for path in paths - if os.path.isdir(path) - and os.stat(path).st_mtime < min_mtime] - - if verbose: - ids = [str(int(os.path.split(dirname)[-1])) for - dirname in to_purge] - print('Purging transactions: %s' % ', '.join(ids)) - - for dirname in to_purge: - distutils.dir_util.remove_tree(dirname) - -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) - - purge_old_files(options.verbose) - - 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)