1#!/usr/bin/env python
2"""
3# OpenWRT download directory cleanup utility.
4# Delete all but the very last version of the program tarballs.
5#
6# Copyright (c) 2010 Michael Buesch <mb@bu3sch.de>
7"""
8
9import sys
10import os
11import re
12import getopt
13
14# Commandline options
15opt_dryrun = False
16
17
18def parseVer_1234(match, filepath):
19	progname = match.group(1)
20	progversion = (int(match.group(2)) << 64) |\
21		      (int(match.group(3)) << 48) |\
22		      (int(match.group(4)) << 32) |\
23		      (int(match.group(5)) << 16)
24	return (progname, progversion)
25
26def parseVer_123(match, filepath):
27	progname = match.group(1)
28	try:
29		patchlevel = match.group(5)
30	except (IndexError), e:
31		patchlevel = None
32	if patchlevel:
33		patchlevel = ord(patchlevel[0])
34	else:
35		patchlevel = 0
36	progversion = (int(match.group(2)) << 64) |\
37		      (int(match.group(3)) << 48) |\
38		      (int(match.group(4)) << 32) |\
39		      patchlevel
40	return (progname, progversion)
41
42def parseVer_12(match, filepath):
43	progname = match.group(1)
44	try:
45		patchlevel = match.group(4)
46	except (IndexError), e:
47		patchlevel = None
48	if patchlevel:
49		patchlevel = ord(patchlevel[0])
50	else:
51		patchlevel = 0
52	progversion = (int(match.group(2)) << 64) |\
53		      (int(match.group(3)) << 48) |\
54		      patchlevel
55	return (progname, progversion)
56
57def parseVer_r(match, filepath):
58	progname = match.group(1)
59	progversion = (int(match.group(2)) << 64)
60	return (progname, progversion)
61
62def parseVer_ymd(match, filepath):
63	progname = match.group(1)
64	progversion = (int(match.group(2)) << 64) |\
65		      (int(match.group(3)) << 48) |\
66		      (int(match.group(4)) << 32)
67	return (progname, progversion)
68
69def parseVer_GIT(match, filepath):
70	progname = match.group(1)
71	st = os.stat(filepath)
72	progversion = int(st.st_mtime) << 64
73	return (progname, progversion)
74
75extensions = (
76	".tar.gz",
77	".tar.bz2",
78	".orig.tar.gz",
79	".orig.tar.bz2",
80	".zip",
81	".tgz",
82	".tbz",
83)
84
85versionRegex = (
86	(re.compile(r"(.+)[-_]([0-9a-fA-F]{40,40})"), parseVer_GIT),		# xxx-GIT_SHASUM
87	(re.compile(r"(.+)[-_](\d+)\.(\d+)\.(\d+)\.(\d+)"), parseVer_1234),	# xxx-1.2.3.4
88	(re.compile(r"(.+)[-_](\d\d\d\d)-?(\d\d)-?(\d\d)"), parseVer_ymd),	# xxx-YYYY-MM-DD
89	(re.compile(r"(.+)[-_](\d+)\.(\d+)\.(\d+)(\w?)"), parseVer_123),	# xxx-1.2.3a
90	(re.compile(r"(.+)[-_](\d+)_(\d+)_(\d+)"), parseVer_123),		# xxx-1_2_3
91	(re.compile(r"(.+)[-_](\d+)\.(\d+)(\w?)"), parseVer_12),		# xxx-1.2a
92	(re.compile(r"(.+)[-_]r?(\d+)"), parseVer_r),				# xxx-r1111
93)
94
95blacklist = [
96	("linux",		re.compile(r"linux-.*")),
97	("gcc",			re.compile(r"gcc-.*")),
98	("wl_apsta",		re.compile(r"wl_apsta.*")),
99	(".fw",			re.compile(r".*\.fw")),
100	(".arm",		re.compile(r".*\.arm")),
101	(".bin",		re.compile(r".*\.bin")),
102	("rt-firmware",		re.compile(r"RT[\d\w]+_Firmware.*")),
103]
104
105class EntryParseError(Exception): pass
106
107class Entry:
108	def __init__(self, directory, filename):
109		self.directory = directory
110		self.filename = filename
111		self.progname = ""
112		self.fileext = ""
113
114		for ext in extensions:
115			if filename.endswith(ext):
116				filename = filename[0:0-len(ext)]
117				self.fileext = ext
118				break
119		else:
120			print self.filename, "has an unknown file-extension"
121			raise EntryParseError("ext")
122		for (regex, parseVersion) in versionRegex:
123			match = regex.match(filename)
124			if match:
125				(self.progname, self.version) = parseVersion(
126					match, directory + "/" + filename + self.fileext)
127				break
128		else:
129			print self.filename, "has an unknown version pattern"
130			raise EntryParseError("ver")
131
132	def deleteFile(self):
133		path = (self.directory + "/" + self.filename).replace("//", "/")
134		print "Deleting", path
135		if not opt_dryrun:
136			os.unlink(path)
137
138	def __eq__(self, y):
139		return self.filename == y.filename
140
141	def __ge__(self, y):
142		return self.version >= y.version
143
144def usage():
145	print "OpenWRT download directory cleanup utility"
146	print "Usage: " + sys.argv[0] + " [OPTIONS] <path/to/dl>"
147	print ""
148	print " -d|--dry-run            Do a dry-run. Don't delete any files"
149	print " -B|--show-blacklist     Show the blacklist and exit"
150	print " -w|--whitelist ITEM     Remove ITEM from blacklist"
151
152def main(argv):
153	global opt_dryrun
154
155	try:
156		(opts, args) = getopt.getopt(argv[1:],
157			"hdBw:",
158			[ "help", "dry-run", "show-blacklist", "whitelist=", ])
159		if len(args) != 1:
160			usage()
161			return 1
162	except getopt.GetoptError:
163		usage()
164		return 1
165	directory = args[0]
166	for (o, v) in opts:
167		if o in ("-h", "--help"):
168			usage()
169			return 0
170		if o in ("-d", "--dry-run"):
171			opt_dryrun = True
172		if o in ("-w", "--whitelist"):
173			for i in range(0, len(blacklist)):
174				(name, regex) = blacklist[i]
175				if name == v:
176					del blacklist[i]
177					break
178			else:
179				print "Whitelist error: Item", v,\
180				      "is not in blacklist"
181				return 1
182		if o in ("-B", "--show-blacklist"):
183			for (name, regex) in blacklist:
184				print name
185			return 0
186
187	# Create a directory listing and parse the file names.
188	entries = []
189	for filename in os.listdir(directory):
190		if filename == "." or filename == "..":
191			continue
192		for (name, regex) in blacklist:
193			if regex.match(filename):
194				if opt_dryrun:
195					print filename, "is blacklisted"
196				break
197		else:
198			try:
199				entries.append(Entry(directory, filename))
200			except (EntryParseError), e: pass
201
202	# Create a map of programs
203	progmap = {}
204	for entry in entries:
205		if entry.progname in progmap.keys():
206			progmap[entry.progname].append(entry)
207		else:
208			progmap[entry.progname] = [entry,]
209
210	# Traverse the program map and delete everything but the last version
211	for prog in progmap:
212		lastVersion = None
213		versions = progmap[prog]
214		for version in versions:
215			if lastVersion is None or version >= lastVersion:
216				lastVersion = version
217		if lastVersion:
218			for version in versions:
219				if version != lastVersion:
220					version.deleteFile()
221			if opt_dryrun:
222				print "Keeping", lastVersion.filename
223
224	return 0
225
226if __name__ == "__main__":
227	sys.exit(main(sys.argv))
228