7184b229023863b4342237cd0462bbd105ab495d
[people/shoehn/ipfire.org.git] / webapp / backend / releases.py
1 #!/usr/bin/python
2
3 import hashlib
4 import logging
5 import os
6 import re
7 import urllib
8 import urlparse
9
10 import database
11 import tracker
12 from misc import Object
13
14 class File(Object):
15         def __init__(self, backend, release, id, data=None):
16                 Object.__init__(self, backend)
17
18                 self.id = id
19                 self._release = release
20
21                 # get all data from database
22                 self.__data = data
23
24         def __cmp__(self, other):
25                 return cmp(self.prio, other.prio)
26
27         @property
28         def data(self):
29                 if self.__data is None:
30                         self.__data = self.db.get("SELECT * FROM files WHERE id = %s", self.id)
31                         assert self.__data
32
33                 return self.__data
34
35         @property
36         def release(self):
37                 if not self._release:
38                         release_id = self.data.get("releases")
39                         self._release = Release(release_id)
40
41                 return self._release
42
43         @property
44         def type(self):
45                 filename = self.filename
46
47                 if filename.endswith(".iso"):
48                         return "iso"
49
50                 elif filename.endswith(".torrent"):
51                         return "torrent"
52
53                 elif "xen" in filename:
54                         if "downloader" in filename:
55                                 return "xen-downloader"
56
57                         return "xen"
58
59                 elif "sources" in filename:
60                         return "source"
61
62                 elif "usb-fdd" in filename:
63                         return "usbfdd"
64
65                 elif "usb-hdd" in filename:
66                         return "usbhdd"
67
68                 elif "armv5tel" in filename and "scon" in filename:
69                         return "armv5tel-scon"
70
71                 elif "armv5tel" in filename:
72                         return "armv5tel"
73
74                 elif "scon" in filename:
75                         return "alix"
76
77                 elif filename.endswith(".img.gz"):
78                         return "flash"
79
80                 else:
81                         return "unknown"
82
83         @property
84         def url(self):
85                 baseurl = self.settings.get("download_url", "http://downloads.ipfire.org")
86
87                 return urlparse.urljoin(baseurl, self.filename)
88
89         @property
90         def desc(self):
91                 _ = lambda x: x
92
93                 descriptions = {
94                         "armv5tel"      : _("Image for the armv5tel architecture"),
95                         "armv5tel-scon" : _("armv5tel image for boards with serial console"),
96                         "iso"           : _("Installable CD image"),
97                         "torrent"       : _("Torrent file"),
98                         "flash"         : _("Flash image"),
99                         "alix"          : _("Flash image for devices with serial console"),
100                         "usbfdd"        : _("USB FDD Image"),
101                         "usbhdd"        : _("USB HDD Image"),
102                         "xen"           : _("Pregenerated Xen image"),
103                         "xen-downloader": _("Xen-Image Generator"),
104                 }
105
106                 try:
107                         return descriptions[self.type]
108                 except KeyError:
109                         return _("Unknown image type")
110
111         @property
112         def prio(self):
113                 priorities = {
114                         "iso"           : 10,
115                         "torrent"       : 20,
116                         "flash"         : 40,
117                         "alix"          : 41,
118                         "usbfdd"        : 31,
119                         "usbhdd"        : 30,
120                         "armv5tel"      : 40,
121                         "armv5tel-scon" : 41,
122                         "xen"           : 50,
123                         "xen-downloader": 51,
124                 }
125                 
126                 try:
127                         return priorities[self.type]
128                 except KeyError:
129                         return 999
130
131         @property
132         def rem(self):
133                 _ = lambda x: x
134         
135                 remarks = {
136                         "armv5tel"      : _("This image runs on many ARM-based boards"),
137                         "armv5tel-scon" : _("This image runs on ARM boards with a serial console"),
138                         "iso"           : _("Use this image to burn a CD and install IPFire from it."),
139                         "torrent"       : _("Download the CD image from the torrent network."),
140                         "flash"         : _("An image that is meant to run on embedded devices."),
141                         "alix"          : _("Flash image where a serial console is enabled by default."),
142                         "usbfdd"        : _("Install IPFire from a floppy-formated USB key."),
143                         "usbhdd"        : _("If the floppy image doesn't work, use this image instead."),
144                         "xen"           : _("A ready-to-run image for Xen."),
145                         "xen-downloader": _("Generator for creating a Xen image."),
146                 }
147
148                 try:
149                         return remarks[self.type]
150                 except KeyError:
151                         return _("Unknown image type")
152
153         @property
154         def sha1(self):
155                 return self.data.get("sha1")
156
157         @property
158         def filename(self):
159                 return self.data.get("filename")
160
161         @property
162         def basename(self):
163                 return os.path.basename(self.filename)
164
165         @property
166         def size(self):
167                 return self.data.get("filesize")
168
169         @property
170         def arch(self):
171                 known_arches = ("i586", "arm")
172
173                 for arch in known_arches:
174                         if arch in self.basename:
175                                 return arch
176
177                 return "N/A"
178
179         @property
180         def torrent_hash(self):
181                 return self.data.get("torrent_hash", None)
182
183         @property
184         def magnet_link(self):
185                 # Don't return anything if we have no torrent hash.
186                 if self.torrent_hash is None:
187                         return
188
189                 s = "magnet:?xt=urn:btih:%s" % self.torrent_hash
190
191                 #s += "&xl=%d" % self.size
192                 s += "&dn=%s" % urllib.quote(self.basename)
193
194                 # Add our tracker.
195                 s += "&tr=http://tracker.ipfire.org:6969/announce"
196
197                 return s
198
199         @property
200         def seeders(self):
201                 if not self.torrent_hash:
202                         return
203
204                 return self.backend.tracker.complete(self.torrent_hash)
205
206         @property
207         def peers(self):
208                 if not self.torrent_hash:
209                         return
210
211                 return self.backend.tracker.incomplete(self.torrent_hash)
212
213
214 class Release(Object):
215         def __init__(self, backend, id, data=None):
216                 Object.__init__(self, backend)
217                 self.id = id
218
219                 # get all data from database
220                 self.__data = data or self.db.get("SELECT * FROM releases WHERE id = %s", self.id)
221                 assert self.__data
222
223                 self.__files = []
224
225         def __repr__(self):
226                 return "<%s %s>" % (self.__class__.__name__, self.name)
227
228         def __cmp__(self, other):
229                 return cmp(self.id, other.id)
230
231         @property
232         def files(self):
233                 if not self.__files:
234                         files = self.db.query("SELECT * FROM files WHERE releases = %s \
235                                 AND NOT filename LIKE '%%.torrent'", self.id)
236
237                         self.__files = [File(self.backend, self, f.id, f) for f in files]
238                         self.__files.sort()
239
240                 return self.__files
241
242         @property
243         def torrents(self):
244                 torrents = []
245
246                 for file in self.files:
247                         if not file.torrent_hash:
248                                 continue
249
250                         torrents.append(file)
251
252                 return torrents
253
254         @property
255         def name(self):
256                 return self.__data.name
257
258         @property
259         def sname(self):
260                 return self.__data.sname
261
262         @property
263         def stable(self):
264                 return self.__data.stable
265
266         @property
267         def published(self):
268                 return self.__data.published
269
270         date = published
271
272         @property
273         def path(self):
274                 return self.__data.path
275
276         def get_file(self, type):
277                 for file in self.files:
278                         if file.type == type:
279                                 return file
280
281         def __file_hash(self, filename):
282                 sha1 = hashlib.sha1()
283
284                 with open(filename) as f:
285                         buf_size = 1024
286                         buf = f.read(buf_size)
287                         while buf:
288                                 sha1.update(buf)
289                                 buf = f.read(buf_size)
290
291                 return sha1.hexdigest()
292
293         def scan_files(self, basepath="/srv/mirror0"):
294                 if not self.path:
295                         return
296
297                 path = os.path.join(basepath, self.path)
298                 if not os.path.exists(path):
299                         return
300
301                 files = self.db.query("SELECT filename FROM files WHERE releases = %s", self.id)
302                 files = [f.filename for f in files]
303
304                 # Make files that do not exists not loadable.
305                 for filename in files:
306                         _filename = os.path.join(basepath, filename)
307                         if not os.path.exists(_filename):
308                                 self.db.execute("UPDATE files SET loadable='N' WHERE filename = %s", filename)
309
310                 for filename in os.listdir(path):
311                         filename = os.path.join(path, filename)
312
313                         if os.path.isdir(filename):
314                                 continue
315
316                         _filename = re.match(".*(releases/.*)", filename).group(1)
317                         if _filename in files:
318                                 continue
319
320                         if filename.endswith(".md5"):
321                                 continue
322
323                         logging.info("Hashing %s..." % filename)
324                         filehash = self.__file_hash(filename)
325                         filesize = os.path.getsize(filename)
326
327                         # Check if there is a torrent download available for this file:
328                         torrent_hash = ""
329                         torrent_file = "%s.torrent" % filename
330                         if os.path.exists(torrent_file):
331                                 torrent_hash = self.torrent_read_hash(torrent_file)
332
333                         self.db.execute("INSERT INTO files(releases, filename, filesize, \
334                                 sha1, torrent_hash) VALUES(%s, %s, %s, %s, %s)",
335                                 self.id, _filename, filesize, filehash, torrent_hash)
336
337                 # Search for all files that miss a torrent hash.
338                 files = self.db.query("SELECT id, filename FROM files \
339                         WHERE releases = %s AND torrent_hash IS NULL", self.id)
340
341                 for file in files:
342                         path = os.path.join(basepath, file.filename)
343
344                         torrent_file = "%s.torrent" % path
345                         if os.path.exists(torrent_file):
346                                 torrent_hash = self.torrent_read_hash(torrent_file)
347
348                                 self.db.execute("UPDATE files SET torrent_hash = %s WHERE id = %s",
349                                         torrent_hash, file.id)
350
351         def torrent_read_hash(self, filename):
352                 f = None
353                 try:
354                         f = open(filename, "rb")
355
356                         metainfo = tracker.bdecode(f.read())
357                         metainfo = tracker.bencode(metainfo["info"])
358
359                         hash = hashlib.sha1()
360                         hash.update(metainfo)
361
362                         return hash.hexdigest()
363
364                 finally:
365                         if f:
366                                 f.close()
367
368         def is_netboot_capable(self):
369                 return self.path and "ipfire-2.x" in self.path
370
371         @property
372         def netboot_kernel(self):
373                 return "http://downloads.ipfire.org/%s/images/vmlinuz" % self.path
374
375         @property
376         def netboot_initrd(self):
377                 return "http://downloads.ipfire.org/%s/images/instroot" % self.path
378
379         @property
380         def news_id(self):
381                 return self.__data.news_id
382
383         @property
384         def news(self):
385                 if not hasattr(self, "_news"):
386                         self._news = self.backend.news.get(self.news_id)
387
388                 return self._news
389
390
391 class Releases(Object):
392         def get_by_id(self, id):
393                 ret = self.db.get("SELECT * FROM releases WHERE id = %s", id)
394
395                 if ret:
396                         return Release(self.backend, ret.id, data=ret)
397
398         def get_by_sname(self, sname):
399                 ret = self.db.get("SELECT * FROM releases WHERE sname = %s", sname)
400
401                 if ret:
402                         return Release(self.backend, ret.id, data=ret)
403
404         def get_by_news_id(self, news_id):
405                 ret = self.db.get("SELECT * FROM releases WHERE news_id = %s", news_id)
406
407                 if ret:
408                         return Release(self.backend, ret.id, data=ret)
409
410         def get_latest(self, stable=True):
411                 ret = self.db.get("SELECT * FROM releases WHERE published IS NOT NULL AND published <= NOW() \
412                         AND stable = %s ORDER BY published DESC LIMIT 1", stable)
413
414                 if ret:
415                         return Release(self.backend, ret.id, data=ret)
416
417         def get_latest_unstable(self):
418                 ret = self.db.get("SELECT * FROM releases r1 \
419                         WHERE r1.published IS NOT NULL AND r1.published <= NOW() \
420                         AND stable = %s AND NOT EXISTS ( \
421                                 SELECT * FROM releases r2 WHERE r2.stable = %s AND \
422                                         r2.published IS NOT NULL AND r2.published >= r1.published \
423                                 ) ORDER BY r1.published DESC LIMIT 1", False, True)
424
425                 if ret:
426                         return Release(self.backend, ret.id, data=ret)
427
428         def get_stable(self):
429                 query = self.db.query("SELECT * FROM releases \
430                         WHERE published IS NOT NULL AND published <= NOW() AND stable = TRUE \
431                         ORDER BY published DESC")
432
433                 releases = []
434                 for row in query:
435                         release = Release(self.backend, row.id, data=row)
436                         releases.append(release)
437
438                 return releases
439
440         def get_unstable(self):
441                 query = self.db.query("SELECT * FROM releases \
442                         WHERE published IS NOT NULL AND published <= NOW() AND stable = FALSE \
443                         ORDER BY published DESC")
444
445                 releases = []
446                 for row in query:
447                         release = Release(self.backend, row.id, data=row)
448                         releases.append(release)
449
450                 return releases
451
452         def get_all(self):
453                 query = self.db.query("SELECT * FROM releases \
454                         WHERE published IS NOT NULL AND published <= NOW() \
455                         ORDER BY published DESC")
456
457                 releases = []
458                 for row in query:
459                         release = Release(self.backend, row.id, data=row)
460                         releases.append(release)
461
462                 return releases
463
464         def _get_all(self):
465                 query = self.db.query("SELECT * FROM releases ORDER BY published DESC")
466
467                 releases = []
468                 for row in query:
469                         release = Release(self.backend, row.id, data=row)
470                         releases.append(release)
471
472                 return releases
473
474         def get_file_for_torrent_hash(self, torrent_hash):
475                 file = self.db.get("SELECT id, releases FROM files WHERE torrent_hash = %s LIMIT 1",
476                         torrent_hash)
477
478                 if not file:
479                         return
480
481                 release = Release(self.backend, file.releases)
482                 file = File(self.backend, release, file.id)
483
484                 return file