|
| 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