forked from mit-athena/delete
-
Notifications
You must be signed in to change notification settings - Fork 0
/
libdelete.py
180 lines (159 loc) · 6.31 KB
/
libdelete.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
Library and helper functions for the delete(1) suite of tools, including
delete, expunge, lsdel, purge, and undelete.
"""
import errno
import glob
import logging
import os
import re
import sys
import stat
WILDCARDS_RE = re.compile('([*?[])')
KILO = 1024
logger = logging.getLogger('libdelete')
have_AFS = True
try:
import afs.fs
except ImportError:
logger.warn("AFS support unavailable")
have_AFS = False
class DeleteError(Exception):
pass
def chunks(seq, size):
"""
Break a sequence up into size chunks
"""
return (seq[pos:pos + size] for pos in xrange(0, len(seq), size))
def format_columns(items, singlecol=False, width=80):
"""
Pretty-print in optional multi-column format, with padding/spread.
"""
if singlecol:
return "\n".join(items)
# The old code printed column-first, not row-first.
# I can't be convinced to care.
if len(items) < 1:
return ""
col_width = max(len(x) for x in items) + 2
if col_width > width:
return "\n".join(items)
n_cols = width // col_width
padding = (80 - (n_cols * col_width)) // n_cols
rv = []
for c in chunks(items, n_cols):
rv.append("".join(item.ljust(col_width + padding) for item in c))
return "\n".join(rv)
def is_mountpoint(path):
if os.path.ismount(path):
return True
if have_AFS and afs.fs.inafs(os.path.abspath(path)):
afs.fs.whichcell(path)
try:
return afs.fs.lsmount(path) is not None
except OSError as e:
logger.debug("Got exception while checking mount point: %s", e)
return False
def has_wildcards(string):
return WILDCARDS_RE.search(string) is not None
def is_deleted(path):
"""
Return True if the file has been 'deleted' by delete(1)
"""
return os.path.basename(path).startswith('.#')
def dir_listing(path):
"""
A directory listing with the full path.
"""
return [os.path.join(path, x) for x in os.listdir(path)]
def empty_directory(path):
"""
Return True if the directory is "empty" (that is, any entries
in it have been deleted)
"""
return all(is_deleted(x) for x in dir_listing(path))
def relpath(path):
"""
For relative paths that begin with '.', strip off the leading
stuff.
"""
return path[2:] if path.startswith('./') else path
def undeleted_name(path):
"""
Return the undeleted name of a file. Only the last component
is changed. If it's in a chain of deleted directories, those
are still printed with the leading '.#' for compatibility.
"""
parts = os.path.split(path)
if parts[1].startswith('.#'):
return os.path.join(parts[0],
parts[1][2:])
else:
return path
def n_days_old(path, n):
if n < 0:
raise ValueError("n must not be negative")
if n == 0:
# All extant files are, by definition, 0 days old
return True
mtime = os.path.getmtime(path)
logger.debug("%s modified %d sec ago", path, mtime)
return ((time.time() - mtime) >= (86400 * n))
def escape_meta(path):
return WILDCARDS_RE.sub(r'[\1]', path)
def to_kb(size):
return int(round(float(size) / KILO))
def find_deleted_files(file_or_pattern, follow_links=False,
follow_mounts=False, recurse_undeleted_subdirs=None,
recurse_deleted_subdirs=None, n_days=0):
logger.debug("find_deleted_files(%s, links=%s, mounts=%s, recurse_un=%s, recurse_del=%s, ndays=%s)",
file_or_pattern, follow_links, follow_mounts, recurse_undeleted_subdirs,
recurse_deleted_subdirs, n_days)
rv = []
# In AFS, without tokens, this is very slow. "Don't do that."
# The old code called readdir() and lstat'd everything before following.
# The old code also re-implemented glob() with BREs, and we're not doing that.
file_list = glob.glob(file_or_pattern) + glob.glob('.#' + file_or_pattern)
if len(file_list) == 0:
raise DeleteError("{0}: {1}".format(file_or_pattern,
"No match" if has_wildcards(file_or_pattern) else os.strerror(errno.ENOENT)))
for filename in file_list:
logger.debug("Examining %s", filename)
if os.path.isdir(filename):
logger.debug("%s is a directory", filename)
if os.path.islink(filename) and not follow_links:
logger.debug("Skipping symlink: %s", filename)
continue
if is_mountpoint(filename) and not follow_mounts:
logger.debug("Skipping mountpoint: %s", filename)
continue
if ((is_deleted(filename) and (recurse_deleted_subdirs != False)) or \
(not is_deleted(filename) and (recurse_undeleted_subdirs != False))):
# NOTE: recurse_undeleted_subdirs is being abused as a tristate with 'None'
# meaning "do it on the first time only.
logger.debug("Recursing into %sdeleted directory: %s",
"un" if not is_deleted(filename) else "",
filename)
try:
for item in dir_listing(filename):
# Escape metachars before recursing because filenames
# can in fact contain metacharacters.
rv += find_deleted_files(escape_meta(item), follow_links, follow_mounts,
False if recurse_undeleted_subdirs is None else recurse_undeleted_subdirs,
False if recurse_deleted_subdirs is None else recurse_deleted_subdirs,
n_days)
except OSError as e:
perror('{filename}: {error}', filename=e.filename,
error=e.strerror)
if is_deleted(filename):
try:
if not n_days_old(filename, n_days):
logger.debug("%s is not %d days old, skipping",
filename, n_days)
continue
except OSError as e:
perror('{filename}: {error} while checking age',
filename=e.filename, error=e.strerror)
logger.debug("Adding: %s", filename)
rv.append(filename)
return rv