Skip to content

Commit 95731c6

Browse files
committed
Add a script to remove file-spam from a tracker.
See scripts/spam-remover.
1 parent 84e612a commit 95731c6

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Features:
1515
for this is currently considered experimental. The current interface
1616
is registerClearCacheCallback(self, method, param) where method is
1717
called with param as the only parameter. (Ralf Schlatterbeck)
18+
- Add a script to remove file-spam from a tracker, see
19+
scripts/spam-remover. (Ralf Schlatterbeck)
1820

1921
Fixed:
2022

scripts/spam-remover

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#! /usr/bin/env python
2+
# Copyright (C) 2012 Dr. Ralf Schlatterbeck Open Source Consulting.
3+
# Reichergasse 131, A-3411 Weidling.
4+
# Web: http://www.runtux.com Email: [email protected]
5+
# All rights reserved
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in
15+
# all copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
_doc = '''
26+
%prog [options]
27+
Remove file attachment spam from a tracker:
28+
- Edit the journal of the given issue(s) and remove the links to the
29+
spam-files
30+
- Set the contents of the spam-files involved to zero length
31+
WARNING:
32+
This is a dangerous operation as it will edit the history *and* remove
33+
data that is not in the journal (the contents of files). Be careful with
34+
the file pattern (start of filename) you specify!
35+
'''
36+
37+
import sys
38+
from optparse import OptionParser
39+
from roundup import instance, hyperdb
40+
41+
def main():
42+
cmd = OptionParser(usage=_doc)
43+
cmd.add_option \
44+
( "-i", "--instance"
45+
, help = "Instance home"
46+
, default = "."
47+
)
48+
cmd.add_option \
49+
( "-d", "--designator"
50+
, dest = "designators"
51+
, help = "Item designator for issue(s), to remove files from,\n"
52+
"e.g. issue4711"
53+
, action = "append"
54+
, default = []
55+
)
56+
cmd.add_option \
57+
( "-f", "--filename"
58+
, dest = "filenames"
59+
, help = "Exact spam-filename to remove from issue(s)"
60+
, action = "append"
61+
, default = []
62+
)
63+
cmd.add_option \
64+
( "-a", "--action", "--no-dry-run"
65+
, dest = "doit"
66+
, help = "Don't perform any action by default unless specified"
67+
, action = "store_true"
68+
)
69+
cmd.add_option \
70+
( "-s", "--file-start-pattern"
71+
, dest = "file_pattern"
72+
, help = "Start of spam-filename to remove from issue(s)"
73+
, action = "append"
74+
, default = []
75+
)
76+
cmd.add_option \
77+
( "-u", "--spam-user"
78+
, dest = "users"
79+
, help = "Username that created the spam-files to remove"
80+
, action = "append"
81+
, default = []
82+
)
83+
cmd.add_option \
84+
( "-q", "--quiet"
85+
, dest = "quiet"
86+
, help = "Be quiet about what we're doing"
87+
, action = "store_true"
88+
)
89+
opt, args = cmd.parse_args()
90+
# open the instance
91+
if len(args):
92+
print >> sys.stderr, "This command doesn't take arguments"
93+
cmd.show_help()
94+
tracker = instance.open(opt.instance)
95+
db = tracker.open('admin')
96+
users = dict.fromkeys (db.user.lookup(u) for u in opt.users)
97+
files_to_remove = {}
98+
for fn in opt.filenames:
99+
for fid in db.files.filter(None,name=fn):
100+
if db.file.get(fid,'name') == fn:
101+
files_to_remove[fid] = True
102+
for fn in opt.file_pattern:
103+
for fid in db.files.filter(None,name=fn):
104+
if db.file.get(fid,'name').startswith(fn):
105+
files_to_remove[fid] = True
106+
files_found = {}
107+
for d in opt.designators:
108+
clsname, id = hyperdb.splitDesignator(d)
109+
cls = db.getclass(clsname)
110+
issuefiles = dict.fromkeys(cls.get (id, 'files'))
111+
for fid in issuefiles.keys():
112+
f = db.file.getnode(fid)
113+
if fid in files_to_remove or f.creator in users:
114+
files_to_remove[fid] = True
115+
files_found[fid] = True
116+
if not opt.quiet:
117+
print "deleting file %s from issue" % f
118+
del issuefiles[fid]
119+
if opt.doit:
120+
cls.set(id, files=issuefiles.keys())
121+
journal = oldjournal = db.getjournal(clsname, id)
122+
# do this twice, we may have file-removals *before* file
123+
# additions for files to delete and may discover mid-journal
124+
# that there are new files to remove
125+
for x in xrange(2):
126+
newjournal = []
127+
for j in journal:
128+
if j[3] == 'set' and 'files' in j[4]:
129+
changes = dict(j[4]['files'])
130+
# only consider file additions by this user
131+
if j[2] in users and '+' in changes:
132+
f = dict.fromkeys(changes['+'])
133+
files_found.update(f)
134+
files_to_remove.update(f)
135+
del changes['+']
136+
# change dict in-place, don't use iteritems
137+
for k, v in changes.items():
138+
new_f = []
139+
for f in v:
140+
if f in files_to_remove:
141+
files_found[f] = True
142+
else:
143+
new_f.append(f)
144+
if new_f :
145+
changes[k] = new_f
146+
else:
147+
del changes[k]
148+
msg = []
149+
if not opt.quiet:
150+
msg.append ("Old journal entry: %s" % str(j))
151+
if changes:
152+
j[4]['files'] = tuple(changes.iteritems())
153+
else:
154+
del j[4]['files']
155+
if j[4]:
156+
newjournal.append(j)
157+
if not opt.quiet:
158+
msg.append ("New journal entry: %s" % str(j))
159+
elif not opt.quiet:
160+
msg.append ("deleted")
161+
if len(msg) == 2 and msg[0][4:] != msg[1][4:]:
162+
for m in msg:
163+
print m
164+
else:
165+
newjournal.append(j)
166+
journal = newjournal
167+
if newjournal != oldjournal and opt.doit:
168+
db.setjournal(clsname, id, newjournal)
169+
if opt.doit:
170+
for f in files_found:
171+
db.file.set(f, content='')
172+
db.commit()
173+
else:
174+
print "Database not changed"
175+
176+
177+
if __name__ == '__main__':
178+
main()

0 commit comments

Comments
 (0)