Skip to content

Commit 0a05908

Browse files
jdreedvasilvv
authored andcommitted
Initial checkin of Python implementation
1 parent 96b62a2 commit 0a05908

File tree

6 files changed

+854
-0
lines changed

6 files changed

+854
-0
lines changed

delete

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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())

expunge

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/python
2+
3+
import logging
4+
import optparse
5+
import os
6+
import sys
7+
8+
import libdelete
9+
10+
header = "The following deleted files are going to be expunged:\n"
11+
footer = """
12+
The above files, which have been marked for deletion, are about to be
13+
expunged forever! Make sure you don't need any of them before continuing.
14+
"""
15+
confirmation = "Do you wish to continue [return = no]? "
16+
17+
logger = logging.getLogger('expunge')
18+
whoami = os.path.basename(sys.argv[0])
19+
20+
def debug_callback(option, opt_str, value, parser):
21+
"""
22+
An OptionParser callback that enables debugging.
23+
"""
24+
all_loggers = [logger.name, 'libdelete']
25+
loggers = [x.strip() for x in value.split(',')]
26+
if value.lower() == 'all':
27+
loggers = all_loggers
28+
else:
29+
if not set(loggers) == set(all_loggers):
30+
parser.error('Valid debug targets: {0}'.format(
31+
", ".join(all_loggers)))
32+
for l in loggers:
33+
logging.getLogger(l).setLevel(logging.DEBUG)
34+
35+
def ask(question, *args, **kwargs):
36+
"""
37+
Ask a question, possibly prepended with the name of the program
38+
and determine whether the user answered in the affirmative
39+
"""
40+
yes = ('y', 'yes')
41+
prepend = '' if kwargs.get('nowhoami', False) else "{0}: ".format(whoami)
42+
try:
43+
return raw_input("%s%s " % (prepend,
44+
question % args)).strip().lower() in yes
45+
except KeyboardInterrupt:
46+
sys.exit(0)
47+
48+
def perror(message, **kwargs):
49+
"""
50+
Format an error message, log it in the debug log
51+
and maybe also print it to stderr.
52+
"""
53+
should_print = not kwargs.pop('_maybe', False)
54+
msg = "{0}: {1}".format(whoami, message.format(**kwargs))
55+
logger.debug("Error: %s", msg)
56+
if should_print:
57+
print >>sys.stderr, msg
58+
59+
def getsize(path):
60+
size = os.path.getsize(path)
61+
return (size, "(%dKB)" % (libdelete.to_kb(size),))
62+
63+
def expunge(deleted_files, options):
64+
expunged_size = 0
65+
errors = 0
66+
if options.listfiles:
67+
print header
68+
print libdelete.format_columns(sorted(
69+
[libdelete.relpath(
70+
libdelete.undeleted_name(x)) for x in deleted_files]))
71+
print footer
72+
if not options.force and \
73+
not ask(confirmation, nowhoami=True):
74+
logger.debug("User failed to confirm; exiting")
75+
sys.exit(0)
76+
for f in deleted_files:
77+
logger.debug("Processing %s", f)
78+
real_name = libdelete.relpath(libdelete.undeleted_name(f))
79+
logger.debug("Undeleted name: %s", real_name)
80+
if options.interactive or options.yieldsize or options.verbose:
81+
size, size_str = (0, '(??KB)')
82+
try:
83+
size, size_str = getsize(f)
84+
except OSError as e:
85+
perror('{filename}: {error} while getting size',
86+
filename=e.filename, error=e.strerror)
87+
errors = 1
88+
logger.debug("Size, size_str = %s, %s", size, size_str)
89+
expunged_size += size
90+
if options.interactive:
91+
filetype = 'directory' if os.path.isdir(f) else ''
92+
# This is correct. We do not display sizes of directories
93+
# when prompting, but do display them below. Because why not.
94+
if not ask('Expunge %s%s%s?', filetype, real_name,
95+
'' if os.path.isdir(f) else size_str):
96+
logger.debug("User failed to confirm, exiting...")
97+
# We exit here, not keep going, as the original code did
98+
sys.exit(errors)
99+
if options.verbose:
100+
print "{whoami}: {path} {size} {maybe}expunged ({total}KB total)".format(whoami=whoami, path=f, size=size_str, maybe='would be ' if options.noop else '', total=libdelete.to_kb(expunged_size))
101+
if not options.noop:
102+
if os.path.isdir(f) and not os.path.islink(f):
103+
logger.debug("rmdir: %s", f)
104+
os.rmdir(f)
105+
else:
106+
logger.debug("unlink: %s", f)
107+
os.unlink(f)
108+
109+
if options.yieldsize:
110+
print "Total expunged: {0}KB".format(libdelete.to_kb(expunged_size))
111+
return errors
112+
113+
def parse_options():
114+
parser = optparse.OptionParser(usage="%prog [options] filename ...")
115+
parser.add_option(
116+
"-l", dest="listfiles", action="store_true", default=False,
117+
help="List files before expunging")
118+
parser.add_option(
119+
"-r", dest="recursive", action="store_true", default=False,
120+
help="Recursively delete non-empty directories")
121+
parser.add_option(
122+
"-f", dest="force", action="store_true", default=False,
123+
help="Do not ask questions or report errors about nonexistent files")
124+
parser.add_option(
125+
"-i", dest="interactive", action="store_true", default=False,
126+
help="Prompt for confirmation before deleting each file/directory")
127+
parser.add_option(
128+
"-n", dest="noop", action="store_true", default=False,
129+
help="Don't actually delete, just print what would be deleted")
130+
parser.add_option(
131+
"-v", dest="verbose", action="store_true", default=False,
132+
help="Print each filename as it is deleted")
133+
parser.add_option(
134+
"-t", dest="timev", action="store", type="int", default=0,
135+
help="Only list n-day-or-older files", metavar="n")
136+
parser.add_option(
137+
"-y", dest="yieldsize", action="store_true", default=False,
138+
help="Report total space taken up by files")
139+
parser.add_option(
140+
"-s", dest="f_links", action="store_true", default=False,
141+
help="Follow symlinks to directories")
142+
parser.add_option(
143+
"-m", dest="f_mounts", action="store_true", default=False,
144+
help="Follow mount points")
145+
parser.add_option(
146+
"--debug", action="callback", type='string', help="Enable debugging (logger target or 'all')",
147+
callback=debug_callback, metavar='target')
148+
(options, args) = parser.parse_args()
149+
if options.noop:
150+
# -n implies -v
151+
options.verbose = True
152+
return (options, args)
153+
154+
def main():
155+
rv = 0
156+
if ((whoami == "purge") and len(sys.argv) > 1):
157+
if (len(sys.argv) == 2) and (sys.argv[1] == '--debug'):
158+
sys.argv.append('all')
159+
else:
160+
print >>sys.stderr, "purge does not take any arguments or options."
161+
sys.exit(1)
162+
(options, args) = parse_options()
163+
if (whoami == "purge"):
164+
args = [os.path.expanduser('~')]
165+
options.recursive = True
166+
options.listfiles = True
167+
if len(args) < 1:
168+
args.append('.')
169+
deleted_files = []
170+
for filename in args:
171+
try:
172+
deleted_files += libdelete.find_deleted_files(
173+
filename,
174+
follow_links=options.f_links,
175+
follow_mounts=options.f_mounts,
176+
recurse_undeleted_subdirs=options.recursive or None,
177+
recurse_deleted_subdirs=True,
178+
n_days=options.timev)
179+
except libdelete.DeleteError as e:
180+
perror(e.message)
181+
rv = 1
182+
logger.debug("Found %d files", len(deleted_files))
183+
if len(deleted_files):
184+
# Sort them so we're deleting leaves first
185+
# Doesn't cover all corner cases, but covers everything the old
186+
# code supported. In particular, weird symlinks will make this
187+
# sad
188+
deleted_files.sort(reverse=True, key=lambda x: x.count(os.path.sep))
189+
rv += expunge(deleted_files, options)
190+
191+
return rv
192+
193+
if __name__ == "__main__":
194+
logging.basicConfig(level=logging.WARNING)
195+
sys.exit(main())

0 commit comments

Comments
 (0)