]>
Commit | Line | Data |
---|---|---|
940227cb MT |
1 | #!/usr/bin/python |
2 | ||
3c4f2edc | 3 | import hashlib |
940227cb | 4 | import logging |
3c4f2edc MT |
5 | import os |
6 | import re | |
fadcfd00 | 7 | import urllib |
940227cb MT |
8 | import urlparse |
9 | ||
9068dba1 | 10 | import database |
fadcfd00 | 11 | import tracker |
9068dba1 | 12 | from misc import Object |
fadcfd00 | 13 | |
9068dba1 MT |
14 | class File(Object): |
15 | def __init__(self, backend, release, id, data=None): | |
16 | Object.__init__(self, backend) | |
940227cb | 17 | |
938d083d MT |
18 | self.id = id |
19 | self._release = release | |
940227cb MT |
20 | |
21 | # get all data from database | |
9068dba1 | 22 | self.__data = data |
940227cb | 23 | |
9068dba1 MT |
24 | def __cmp__(self, other): |
25 | return cmp(self.prio, other.prio) | |
938d083d MT |
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 | |
940227cb MT |
42 | |
43 | @property | |
44 | def type(self): | |
fadcfd00 MT |
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: | |
80594ae3 MT |
54 | if "downloader" in filename: |
55 | return "xen-downloader" | |
56 | ||
fadcfd00 MT |
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 | ||
3230a47f MT |
68 | elif "armv5tel" in filename and "scon" in filename: |
69 | return "armv5tel-scon" | |
70 | ||
fadcfd00 MT |
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" | |
940227cb MT |
82 | |
83 | @property | |
84 | def url(self): | |
9068dba1 | 85 | baseurl = self.settings.get("download_url", "http://downloads.ipfire.org") |
940227cb MT |
86 | |
87 | return urlparse.urljoin(baseurl, self.filename) | |
88 | ||
89 | @property | |
90 | def desc(self): | |
91 | _ = lambda x: x | |
92 | ||
93 | descriptions = { | |
fadcfd00 | 94 | "armv5tel" : _("Image for the armv5tel architecture"), |
3230a47f | 95 | "armv5tel-scon" : _("armv5tel image for boards with serial console"), |
940227cb MT |
96 | "iso" : _("Installable CD image"), |
97 | "torrent" : _("Torrent file"), | |
98 | "flash" : _("Flash image"), | |
ec6ab977 | 99 | "alix" : _("Flash image for devices with serial console"), |
940227cb MT |
100 | "usbfdd" : _("USB FDD Image"), |
101 | "usbhdd" : _("USB HDD Image"), | |
102 | "xen" : _("Pregenerated Xen image"), | |
80594ae3 | 103 | "xen-downloader": _("Xen-Image Generator"), |
940227cb MT |
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, | |
3230a47f MT |
120 | "armv5tel" : 40, |
121 | "armv5tel-scon" : 41, | |
940227cb | 122 | "xen" : 50, |
80594ae3 | 123 | "xen-downloader": 51, |
940227cb MT |
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 = { | |
fadcfd00 | 136 | "armv5tel" : _("This image runs on many ARM-based boards"), |
3230a47f | 137 | "armv5tel-scon" : _("This image runs on ARM boards with a serial console"), |
940227cb MT |
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."), | |
80594ae3 | 145 | "xen-downloader": _("Generator for creating a Xen image."), |
940227cb MT |
146 | } |
147 | ||
148 | try: | |
149 | return remarks[self.type] | |
150 | except KeyError: | |
151 | return _("Unknown image type") | |
152 | ||
153 | @property | |
154 | def sha1(self): | |
938d083d | 155 | return self.data.get("sha1") |
940227cb MT |
156 | |
157 | @property | |
158 | def filename(self): | |
938d083d | 159 | return self.data.get("filename") |
940227cb | 160 | |
c9698a0b MT |
161 | @property |
162 | def basename(self): | |
163 | return os.path.basename(self.filename) | |
164 | ||
60024cc8 MT |
165 | @property |
166 | def size(self): | |
938d083d | 167 | return self.data.get("filesize") |
60024cc8 MT |
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 | ||
fadcfd00 MT |
177 | return "N/A" |
178 | ||
179 | @property | |
180 | def torrent_hash(self): | |
938d083d | 181 | return self.data.get("torrent_hash", None) |
fadcfd00 MT |
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 | |
60024cc8 | 198 | |
938d083d MT |
199 | @property |
200 | def seeders(self): | |
201 | if not self.torrent_hash: | |
202 | return | |
203 | ||
6d524cba | 204 | return self.backend.tracker.complete(self.torrent_hash) |
938d083d MT |
205 | |
206 | @property | |
207 | def peers(self): | |
208 | if not self.torrent_hash: | |
209 | return | |
210 | ||
6d524cba | 211 | return self.backend.tracker.incomplete(self.torrent_hash) |
938d083d | 212 | |
940227cb | 213 | |
9068dba1 MT |
214 | class Release(Object): |
215 | def __init__(self, backend, id, data=None): | |
216 | Object.__init__(self, backend) | |
940227cb MT |
217 | self.id = id |
218 | ||
219 | # get all data from database | |
9068dba1 | 220 | self.__data = data or self.db.get("SELECT * FROM releases WHERE id = %s", self.id) |
940227cb MT |
221 | assert self.__data |
222 | ||
223 | self.__files = [] | |
224 | ||
225 | def __repr__(self): | |
226 | return "<%s %s>" % (self.__class__.__name__, self.name) | |
227 | ||
9068dba1 MT |
228 | def __cmp__(self, other): |
229 | return cmp(self.id, other.id) | |
230 | ||
940227cb MT |
231 | @property |
232 | def files(self): | |
233 | if not self.__files: | |
9068dba1 MT |
234 | files = self.db.query("SELECT * FROM files WHERE releases = %s \ |
235 | AND NOT filename LIKE '%%.torrent'", self.id) | |
940227cb | 236 | |
9068dba1 MT |
237 | self.__files = [File(self.backend, self, f.id, f) for f in files] |
238 | self.__files.sort() | |
940227cb MT |
239 | |
240 | return self.__files | |
241 | ||
938d083d MT |
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 | ||
940227cb MT |
254 | @property |
255 | def name(self): | |
9068dba1 MT |
256 | return self.__data.name |
257 | ||
258 | @property | |
259 | def sname(self): | |
260 | return self.__data.sname | |
940227cb MT |
261 | |
262 | @property | |
263 | def stable(self): | |
9068dba1 | 264 | return self.__data.stable |
940227cb MT |
265 | |
266 | @property | |
267 | def published(self): | |
9068dba1 | 268 | return self.__data.published |
940227cb | 269 | |
9068dba1 | 270 | date = published |
940227cb | 271 | |
3c4f2edc MT |
272 | @property |
273 | def path(self): | |
9068dba1 | 274 | return self.__data.path |
3c4f2edc | 275 | |
940227cb MT |
276 | def get_file(self, type): |
277 | for file in self.files: | |
278 | if file.type == type: | |
279 | return file | |
280 | ||
3c4f2edc MT |
281 | def __file_hash(self, filename): |
282 | sha1 = hashlib.sha1() | |
283 | ||
284 | with open(filename) as f: | |
b5f4eef0 MT |
285 | buf_size = 1024 |
286 | buf = f.read(buf_size) | |
287 | while buf: | |
288 | sha1.update(buf) | |
289 | buf = f.read(buf_size) | |
3c4f2edc MT |
290 | |
291 | return sha1.hexdigest() | |
292 | ||
3c4f2edc MT |
293 | def scan_files(self, basepath="/srv/mirror0"): |
294 | if not self.path: | |
295 | return | |
296 | ||
297 | path = os.path.join(basepath, self.path) | |
921d98cc MT |
298 | if not os.path.exists(path): |
299 | return | |
300 | ||
fadcfd00 MT |
301 | files = self.db.query("SELECT filename FROM files WHERE releases = %s", self.id) |
302 | files = [f.filename for f in files] | |
3c4f2edc MT |
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 | ||
9068dba1 | 323 | logging.info("Hashing %s..." % filename) |
3c4f2edc MT |
324 | filehash = self.__file_hash(filename) |
325 | filesize = os.path.getsize(filename) | |
3c4f2edc | 326 | |
fadcfd00 MT |
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() | |
3c4f2edc | 367 | |
37b5c0cf MT |
368 | def supports_arch(self, arch): |
369 | # Currently there is nothing else than i586 supported | |
370 | if arch == "i586": | |
371 | return True | |
372 | ||
373 | return False | |
374 | ||
375 | def supports_platform(self, platform): | |
376 | # Currently there is nothing else than pcbios supported | |
377 | if platform == "pcbios": | |
378 | return True | |
379 | ||
380 | return False | |
381 | ||
9068dba1 MT |
382 | def is_netboot_capable(self): |
383 | return self.path and "ipfire-2.x" in self.path | |
384 | ||
37b5c0cf MT |
385 | def netboot_kernel_url(self, arch, platform): |
386 | assert self.supports_arch(arch) | |
387 | assert self.supports_platform(platform) | |
388 | ||
9068dba1 | 389 | return "http://downloads.ipfire.org/%s/images/vmlinuz" % self.path |
940227cb | 390 | |
37b5c0cf MT |
391 | def netboot_initrd_url(self, arch, platform): |
392 | assert self.supports_arch(arch) | |
393 | assert self.supports_platform(platform) | |
394 | ||
9068dba1 | 395 | return "http://downloads.ipfire.org/%s/images/instroot" % self.path |
940227cb | 396 | |
37b5c0cf MT |
397 | def netboot_args(self, arch, platform): |
398 | return "" | |
399 | ||
2fed2438 MT |
400 | @property |
401 | def news_id(self): | |
402 | return self.__data.news_id | |
403 | ||
404 | @property | |
405 | def news(self): | |
406 | if not hasattr(self, "_news"): | |
407 | self._news = self.backend.news.get(self.news_id) | |
408 | ||
409 | return self._news | |
410 | ||
940227cb | 411 | |
9068dba1 | 412 | class Releases(Object): |
940227cb | 413 | def get_by_id(self, id): |
9068dba1 MT |
414 | ret = self.db.get("SELECT * FROM releases WHERE id = %s", id) |
415 | ||
416 | if ret: | |
417 | return Release(self.backend, ret.id, data=ret) | |
418 | ||
419 | def get_by_sname(self, sname): | |
420 | ret = self.db.get("SELECT * FROM releases WHERE sname = %s", sname) | |
421 | ||
422 | if ret: | |
423 | return Release(self.backend, ret.id, data=ret) | |
940227cb | 424 | |
9de13943 MT |
425 | def get_by_news_id(self, news_id): |
426 | ret = self.db.get("SELECT * FROM releases WHERE news_id = %s", news_id) | |
427 | ||
428 | if ret: | |
429 | return Release(self.backend, ret.id, data=ret) | |
430 | ||
9068dba1 MT |
431 | def get_latest(self, stable=True): |
432 | ret = self.db.get("SELECT * FROM releases WHERE published IS NOT NULL AND published <= NOW() \ | |
433 | AND stable = %s ORDER BY published DESC LIMIT 1", stable) | |
940227cb | 434 | |
9068dba1 MT |
435 | if ret: |
436 | return Release(self.backend, ret.id, data=ret) | |
940227cb | 437 | |
0aab96e0 MT |
438 | def get_latest_unstable(self): |
439 | ret = self.db.get("SELECT * FROM releases r1 \ | |
440 | WHERE r1.published IS NOT NULL AND r1.published <= NOW() \ | |
441 | AND stable = %s AND NOT EXISTS ( \ | |
442 | SELECT * FROM releases r2 WHERE r2.stable = %s AND \ | |
443 | r2.published IS NOT NULL AND r2.published >= r1.published \ | |
444 | ) ORDER BY r1.published DESC LIMIT 1", False, True) | |
445 | ||
446 | if ret: | |
447 | return Release(self.backend, ret.id, data=ret) | |
448 | ||
940227cb | 449 | def get_stable(self): |
9068dba1 MT |
450 | query = self.db.query("SELECT * FROM releases \ |
451 | WHERE published IS NOT NULL AND published <= NOW() AND stable = TRUE \ | |
452 | ORDER BY published DESC") | |
940227cb | 453 | |
9068dba1 MT |
454 | releases = [] |
455 | for row in query: | |
456 | release = Release(self.backend, row.id, data=row) | |
457 | releases.append(release) | |
458 | ||
459 | return releases | |
940227cb MT |
460 | |
461 | def get_unstable(self): | |
9068dba1 MT |
462 | query = self.db.query("SELECT * FROM releases \ |
463 | WHERE published IS NOT NULL AND published <= NOW() AND stable = FALSE \ | |
464 | ORDER BY published DESC") | |
465 | ||
466 | releases = [] | |
467 | for row in query: | |
468 | release = Release(self.backend, row.id, data=row) | |
469 | releases.append(release) | |
940227cb | 470 | |
9068dba1 | 471 | return releases |
940227cb MT |
472 | |
473 | def get_all(self): | |
9068dba1 MT |
474 | query = self.db.query("SELECT * FROM releases \ |
475 | WHERE published IS NOT NULL AND published <= NOW() \ | |
476 | ORDER BY published DESC") | |
940227cb | 477 | |
9068dba1 MT |
478 | releases = [] |
479 | for row in query: | |
480 | release = Release(self.backend, row.id, data=row) | |
481 | releases.append(release) | |
482 | ||
483 | return releases | |
940227cb | 484 | |
d0eee7f8 MT |
485 | def _get_all(self): |
486 | query = self.db.query("SELECT * FROM releases ORDER BY published DESC") | |
487 | ||
488 | releases = [] | |
489 | for row in query: | |
490 | release = Release(self.backend, row.id, data=row) | |
491 | releases.append(release) | |
492 | ||
493 | return releases | |
494 | ||
938d083d MT |
495 | def get_file_for_torrent_hash(self, torrent_hash): |
496 | file = self.db.get("SELECT id, releases FROM files WHERE torrent_hash = %s LIMIT 1", | |
fadcfd00 MT |
497 | torrent_hash) |
498 | ||
499 | if not file: | |
500 | return | |
501 | ||
ea324f48 MT |
502 | release = Release(self.backend, file.releases) |
503 | file = File(self.backend, release, file.id) | |
938d083d MT |
504 | |
505 | return file |