queue_rss.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. #! /usr/bin/env python3
  2. # Generate two rss feeds for a directory with .changes file
  3. # License: GPL v2 or later
  4. # Author: Filippo Giunchedi <filippo@debian.org>
  5. # Version: 0.5
  6. import html
  7. import os
  8. import os.path
  9. import pickle
  10. import re
  11. import sys
  12. import time
  13. from datetime import datetime
  14. from email.utils import parseaddr
  15. from optparse import OptionParser
  16. import PyRSS2Gen
  17. from debian.deb822 import Changes
  18. inrss_filename = "NEW_in.rss"
  19. outrss_filename = "NEW_out.rss"
  20. db_filename = "status.db"
  21. parser = OptionParser()
  22. parser.set_defaults(
  23. queuedir="queue", outdir="out", datadir="status", logdir="log", max_entries="30"
  24. )
  25. parser.add_option("-q", "--queuedir", dest="queuedir", help="The queue dir (%default)")
  26. parser.add_option(
  27. "-o", "--outdir", dest="outdir", help="The output directory (%default)"
  28. )
  29. parser.add_option("-d", "--datadir", dest="datadir", help="The data dir (%default)")
  30. parser.add_option(
  31. "-l", "--logdir", dest="logdir", help="The ACCEPT/REJECT dak log dir (%default)"
  32. )
  33. parser.add_option(
  34. "-m",
  35. "--max-entries",
  36. dest="max_entries",
  37. type="int",
  38. help="Max number of entries to keep (%default)",
  39. )
  40. class Status:
  41. def __init__(self):
  42. self.feed_in = PyRSS2Gen.RSS2(
  43. title="Packages entering NEW",
  44. link="https://ftp-master.debian.org/new.html",
  45. description="Debian packages entering the NEW queue",
  46. )
  47. self.feed_out = PyRSS2Gen.RSS2(
  48. title="Packages leaving NEW",
  49. link="https://ftp-master.debian.org/new.html",
  50. description="Debian packages leaving the NEW queue",
  51. )
  52. self.queue = {}
  53. def purge_old_items(feed, max):
  54. """Purge RSSItem from feed, no more than max."""
  55. if feed.items is None or len(feed.items) == 0:
  56. return False
  57. feed.items = feed.items[:max]
  58. return True
  59. def parse_changes(fname):
  60. """Parse a .changes file named fname.
  61. Return {fname: parsed}"""
  62. m = Changes(open(fname))
  63. wanted_fields = set(
  64. [
  65. "Source",
  66. "Version",
  67. "Architecture",
  68. "Distribution",
  69. "Date",
  70. "Changed-By",
  71. "Description",
  72. "Changes",
  73. ]
  74. )
  75. if not set(m.keys()).issuperset(wanted_fields):
  76. return None
  77. return {os.path.basename(fname): m}
  78. def parse_queuedir(dir):
  79. """Parse dir for .changes files.
  80. Return a dictionary {filename: parsed_file}"""
  81. if not os.path.exists(dir):
  82. return None
  83. res = {}
  84. for fname in os.listdir(dir):
  85. if not fname.endswith(".changes"):
  86. continue
  87. parsed = parse_changes(os.path.join(dir, fname))
  88. if parsed:
  89. res.update(parsed)
  90. return res
  91. def parse_leave_reason(fname):
  92. """Parse a dak log file fname for ACCEPT/REJECT reason from process-new.
  93. Return a dictionary {filename: reason}"""
  94. reason_re = re.compile(r".+\|process-new\|(.+)\|NEW (ACCEPT|REJECT)\|(\S+)")
  95. try:
  96. f = open(fname)
  97. except OSError as e:
  98. print("Can't open %s: %s" % (fname, e), file=sys.stderr)
  99. return {}
  100. res = {}
  101. for line in f.readlines():
  102. m = reason_re.search(line)
  103. if m:
  104. res[m.group(3)] = (m.group(2), m.group(1))
  105. f.close()
  106. return res
  107. def add_rss_item(status, msg, direction):
  108. if direction == "in":
  109. feed = status.feed_in
  110. title = "%s %s entered NEW" % (msg["Source"], msg["Version"])
  111. pubdate = msg["Date"]
  112. elif direction == "out":
  113. feed = status.feed_out
  114. if "Leave-Reason" in msg:
  115. title = "%s %s left NEW (%s)" % (
  116. msg["Source"],
  117. msg["Version"],
  118. msg["Leave-Reason"][0],
  119. )
  120. else:
  121. title = "%s %s left NEW" % (msg["Source"], msg["Version"])
  122. pubdate = datetime.utcnow()
  123. else:
  124. return False
  125. description = "<pre>Description: %s\nChanges: %s\n</pre>" % (
  126. html.escape(msg["Description"]),
  127. html.escape(msg["Changes"]),
  128. )
  129. link = "https://ftp-master.debian.org/new/%s_%s.html" % (
  130. msg["Source"],
  131. msg["Version"],
  132. )
  133. guid = msg["Checksums-Sha256"][0]["sha256"]
  134. if "Processed-By" in msg:
  135. author = msg["Processed-By"]
  136. else:
  137. changedby = parseaddr(msg["Changed-By"])
  138. author = "%s (%s)" % (changedby[1], changedby[0])
  139. feed.items.insert(
  140. 0,
  141. PyRSS2Gen.RSSItem(
  142. title,
  143. pubDate=pubdate,
  144. description=description,
  145. author=html.escape(author),
  146. link=link,
  147. guid=guid,
  148. ),
  149. )
  150. def update_feeds(curqueue, status, settings):
  151. # inrss -> append all items in curqueue not in status.queue
  152. # outrss -> append all items in status.queue not in curqueue
  153. leave_reason = None
  154. # logfile from dak's process-new
  155. reason_log = os.path.join(settings.logdir, time.strftime("%Y-%m"))
  156. for name, parsed in curqueue.items():
  157. if name not in status.queue:
  158. # new package
  159. add_rss_item(status, parsed, "in")
  160. for name, parsed in status.queue.items():
  161. if name not in curqueue:
  162. # removed package, try to find out why
  163. if leave_reason is None:
  164. leave_reason = parse_leave_reason(reason_log)
  165. if leave_reason and name in leave_reason:
  166. parsed["Leave-Reason"] = leave_reason[name][0]
  167. parsed["Processed-By"] = leave_reason[name][1] + "@debian.org"
  168. add_rss_item(status, parsed, "out")
  169. if __name__ == "__main__":
  170. (settings, args) = parser.parse_args()
  171. if not os.path.exists(settings.outdir):
  172. print("Outdir '%s' does not exists" % settings.outdir, file=sys.stderr)
  173. parser.print_help()
  174. sys.exit(1)
  175. if not os.path.exists(settings.datadir):
  176. print("Datadir '%s' does not exists" % settings.datadir, file=sys.stderr)
  177. parser.print_help()
  178. sys.exit(1)
  179. status_db = os.path.join(settings.datadir, db_filename)
  180. try:
  181. with open(status_db, "rb") as fh:
  182. try:
  183. status = pickle.load(fh, encoding="utf-8")
  184. except UnicodeDecodeError:
  185. fh.seek(0)
  186. status = pickle.load(fh, encoding="latin-1")
  187. except OSError:
  188. status = Status()
  189. current_queue = parse_queuedir(settings.queuedir)
  190. update_feeds(current_queue, status, settings)
  191. purge_old_items(status.feed_in, settings.max_entries)
  192. purge_old_items(status.feed_out, settings.max_entries)
  193. feed_in_file = os.path.join(settings.outdir, inrss_filename)
  194. feed_out_file = os.path.join(settings.outdir, outrss_filename)
  195. try:
  196. status.feed_in.write_xml(open(feed_in_file, "w+"), "utf-8")
  197. status.feed_out.write_xml(open(feed_out_file, "w+"), "utf-8")
  198. except OSError as why:
  199. print("Unable to write feeds:", why, file=sys.stderr)
  200. sys.exit(1)
  201. status.queue = current_queue
  202. try:
  203. with open(status_db, "wb+") as fh:
  204. pickle.dump(status, fh)
  205. except OSError as why:
  206. print("Unable to save status:", why, file=sys.stderr)
  207. sys.exit(1)
  208. # vim:et:ts=4