|
| 1 | +#!/usr/bin/python |
| 2 | + |
| 3 | +import logging |
| 4 | +import optparse |
| 5 | +import os |
| 6 | +import shutil |
| 7 | +import stat |
| 8 | +import sys |
| 9 | + |
| 10 | +import afs.fs |
| 11 | + |
| 12 | +logger = logging.getLogger('delete') |
| 13 | +whoami = os.path.basename(sys.argv[0]) |
| 14 | + |
| 15 | +def debug_callback(option, opt_str, value, parser): |
| 16 | + """ |
| 17 | + An OptionParser callback that enables debugging. |
| 18 | + """ |
| 19 | + all_loggers = [logger.name, 'libdelete'] |
| 20 | + loggers = [x.strip() for x in value.split(',')] |
| 21 | + if value.lower() == 'all': |
| 22 | + loggers = all_loggers |
| 23 | + else: |
| 24 | + if not set(loggers) == set(all_loggers): |
| 25 | + parser.error('Valid debug targets: {0}'.format( |
| 26 | + ", ".join(all_loggers))) |
| 27 | + for l in loggers: |
| 28 | + logging.getLogger(l).setLevel(logging.DEBUG) |
| 29 | + |
| 30 | +def ask(question, *args, **kwargs): |
| 31 | + """ |
| 32 | + Ask a question, possibly prepended with the name of the program |
| 33 | + and determine whether the user answered in the affirmative |
| 34 | + """ |
| 35 | + yes = ('y', 'yes') |
| 36 | + prepend = '' if kwargs.get('nowhoami', False) else "{0}: ".format(whoami) |
| 37 | + try: |
| 38 | + return raw_input("%s%s " % (prepend, |
| 39 | + question % args)).strip().lower() in yes |
| 40 | + except KeyboardInterrupt: |
| 41 | + sys.exit(0) |
| 42 | + |
| 43 | +def perror(message, **kwargs): |
| 44 | + """ |
| 45 | + Format an error message, log it in the debug log |
| 46 | + and maybe also print it to stderr. |
| 47 | + """ |
| 48 | + should_print = not kwargs.pop('_maybe', False) |
| 49 | + msg = "{0}: {1}".format(whoami, message.format(**kwargs)) |
| 50 | + logger.debug("Error: %s", msg) |
| 51 | + if should_print: |
| 52 | + print >>sys.stderr, msg |
| 53 | + |
| 54 | +def actually_delete(filename, options): |
| 55 | + """ |
| 56 | + Actually delete the file. |
| 57 | + """ |
| 58 | + logger.debug("actually_delete(%s)", filename) |
| 59 | + if options.interactive and not ask('remove %s?', filename): |
| 60 | + return False |
| 61 | + if not options.force and \ |
| 62 | + not os.path.islink(filename) and \ |
| 63 | + not os.access(filename, os.W_OK): |
| 64 | + if not ask("File %s not writeable. Delete anyway?", |
| 65 | + filename): |
| 66 | + return False |
| 67 | + if options.noop: |
| 68 | + print >>sys.stderr, "{0}: {1} would be removed".format(whoami, filename) |
| 69 | + return True |
| 70 | + (dirname, basename) = os.path.split(filename) |
| 71 | + newname = os.path.join(dirname, '.#' + basename) |
| 72 | + if os.path.exists(newname): |
| 73 | + # Yes, it just unconditionally scribbles over things, and always has. |
| 74 | + if os.path.isdir(newname) and not os.path.islink(newname): |
| 75 | + shutil.rmtree(newname) |
| 76 | + else: |
| 77 | + os.path.unlink(newname) |
| 78 | + assert not os.path.exists(newname), "Fatal error: new path exists" |
| 79 | + logger.debug("Move %s to %s", filename, newname) |
| 80 | + # Maybe we can just use os.rename here |
| 81 | + shutil.move(filename, newname) |
| 82 | + # None means use the current time |
| 83 | + os.utime(newname, None) |
| 84 | + return True |
| 85 | + |
| 86 | +def delete(filename, options): |
| 87 | + logger.debug("delete(%s)", filename) |
| 88 | + if not os.path.lexists(filename): |
| 89 | + perror('{filename}: No such file or directory', |
| 90 | + filename=filename, _maybe=options.report_errors) |
| 91 | + return False |
| 92 | + if os.path.isdir(filename) and not os.path.islink(filename): |
| 93 | + if os.path.basename(filename) in ('.', '..'): |
| 94 | + perror("Cannot delete '.' or '..'", _maybe=options.report_errors) |
| 95 | + return False |
| 96 | + if options.filesonly: |
| 97 | + if not options.recursive: |
| 98 | + perror("{filename}: can't delete (not file)", |
| 99 | + filename=filename, _maybe=options.report_errors) |
| 100 | + return False |
| 101 | + for x in libdelete.dir_listing(filename): |
| 102 | + logger.debug("Recursively deleting %s", x) |
| 103 | + if not delete(x, options): |
| 104 | + logger.debug("Recursive delete failed") |
| 105 | + return False |
| 106 | + return actually_delete(x, options) |
| 107 | + |
| 108 | + else: |
| 109 | + try: |
| 110 | + is_empty = libdelete.empty_directory(filename) |
| 111 | + except OSError as e: |
| 112 | + # Do we want to only do this if emulating rm? |
| 113 | + print >>sys.stderr, ": ".join((whoami, e.filename, e.strerror)) |
| 114 | + return False |
| 115 | + if is_empty: |
| 116 | + return actually_delete(filename, options) |
| 117 | + if options.directoriesonly or not options.recursive: |
| 118 | + perror("{filename}: can't delete (directory not empty)", |
| 119 | + filename=filename, _maybe=options.report_errors) |
| 120 | + elif options.recursive: |
| 121 | + for x in libdelete.dir_listing(filename): |
| 122 | + logger.debug("Recursively deleting %s", x) |
| 123 | + if not delete(x, options): |
| 124 | + logger.debug("Recursively delete failed") |
| 125 | + return False |
| 126 | + return actually_delete(filename, options) |
| 127 | + |
| 128 | + # Not a directory |
| 129 | + else: |
| 130 | + if options.directoriesonly: |
| 131 | + perror("{filename}: can't delete (not directory)", |
| 132 | + filename=filename, _maybe=options.report_errors) |
| 133 | + else: |
| 134 | + return actually_delete(filename, options) |
| 135 | + |
| 136 | +def main(): |
| 137 | + parser = optparse.OptionParser(usage="%prog [options] filename ...") |
| 138 | + # This is probably a terrible idea, but the old code did it |
| 139 | + linked_to_rm = whoami == "rm" |
| 140 | + linked_to_rmdir = whoami == "rmdir" |
| 141 | + parser.add_option( |
| 142 | + "-r", dest="recursive", action="store_true", default=False, |
| 143 | + help="Recursively delete non-empty directories") |
| 144 | + parser.add_option( |
| 145 | + "-f", dest="force", action="store_true", default=False, |
| 146 | + help="Do not ask questions or report errors about nonexistent files") |
| 147 | + parser.add_option( |
| 148 | + "-i", dest="interactive", action="store_true", default=False, |
| 149 | + help="Prompt for confirmation before deleting each file/directory") |
| 150 | + parser.add_option( |
| 151 | + "-n", dest="noop", action="store_true", default=False, |
| 152 | + help="Don't actually delete, just print what would be deleted") |
| 153 | + parser.add_option( |
| 154 | + "-v", dest="verbose", action="store_true", default=False, |
| 155 | + help="Print each filename as it is deleted") |
| 156 | + parser.add_option( |
| 157 | + "-e", dest="emulate_rm", action="store_true", |
| 158 | + default=(linked_to_rm or linked_to_rmdir), |
| 159 | + help="Emulate the pecularities of rm and rmdir") |
| 160 | + parser.add_option( |
| 161 | + "-F", dest="filesonly", action="store_true", default=linked_to_rm, |
| 162 | + help="Remove files only (refuse to remove even non-empty directories)") |
| 163 | + parser.add_option( |
| 164 | + "-D", dest="directoriesonly", action="store_true", |
| 165 | + default=linked_to_rmdir, |
| 166 | + help="Only remove empty directories (refuse to remove files)") |
| 167 | + parser.add_option( |
| 168 | + "--debug", action="callback", type='string', help="Enable debugging (logger target or 'all')", |
| 169 | + callback=debug_callback, metavar='target') |
| 170 | + (options, args) = parser.parse_args() |
| 171 | + if options.filesonly and options.directoriesonly: |
| 172 | + parser.error("-F and -D are mutually exclusive") |
| 173 | + if options.recursive and options.directoriesonly: |
| 174 | + parser.error("-r and -D are mutually exclusive") |
| 175 | + if len(args) < 1: |
| 176 | + parser.error("No files or directories specified.") |
| 177 | + options.report_errors = not options.emulate_rm or not options.force |
| 178 | + errors = 0 |
| 179 | + for filename in args: |
| 180 | + # Because you know _someone_ will try it |
| 181 | + if len(filename.rstrip('/')) < 1: |
| 182 | + print >>sys.stderr, "That's not a good idea." |
| 183 | + sys.exit(1) |
| 184 | + # Trailing slashes make bad things happen |
| 185 | + if not delete(filename.rstrip('/'), options): |
| 186 | + errors = 1 |
| 187 | + return errors |
| 188 | + |
| 189 | +if __name__ == "__main__": |
| 190 | + logging.basicConfig(level=logging.WARNING) |
| 191 | + sys.exit(main()) |
0 commit comments