]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/mirrors.py
CSS: Add CSS for file listings
[ipfire.org.git] / src / backend / mirrors.py
CommitLineData
940227cb
MT
1#!/usr/bin/python
2
9068dba1 3import datetime
940227cb 4import logging
0673d1b0 5import math
54af860e 6import os.path
0673d1b0 7import random
940227cb
MT
8import socket
9import time
10import tornado.httpclient
9068dba1 11import tornado.netutil
11347e46 12import urllib.parse
940227cb 13
11347e46 14from .misc import Object
95483f04 15from .decorators import *
60024cc8 16
9068dba1 17class Downloads(Object):
60024cc8
MT
18 @property
19 def total(self):
20 ret = self.db.get("SELECT COUNT(*) AS total FROM log_download")
21
22 return ret.total
23
24 @property
25 def today(self):
9068dba1 26 ret = self.db.get("SELECT COUNT(*) AS today FROM log_download WHERE date::date = NOW()::date")
60024cc8
MT
27
28 return ret.today
29
30 @property
31 def yesterday(self):
9068dba1 32 ret = self.db.get("SELECT COUNT(*) AS yesterday FROM log_download WHERE date::date = (NOW() - INTERVAL '1 day')::date")
60024cc8
MT
33
34 return ret.yesterday
35
36 @property
37 def daily_map(self):
66862195
MT
38 ret = self.db.query("WITH downloads AS (SELECT * FROM log_download \
39 WHERE DATE(date) BETWEEN (NOW()::date - INTERVAL '30 days') AND DATE(NOW())) \
40 SELECT DATE(date) AS date, COUNT(*) AS count FROM downloads \
41 GROUP BY DATE(date) ORDER BY date")
60024cc8
MT
42
43 return ret
44
45 def get_countries(self, duration="all"):
46 query = "SELECT country_code, count(country_code) AS count FROM log_download"
47
48 if duration == "today":
9068dba1 49 query += " WHERE date::date = NOW()::date"
60024cc8
MT
50
51 query += " GROUP BY country_code ORDER BY count DESC"
52
53 results = self.db.query(query)
66862195 54 ret = []
60024cc8
MT
55
56 count = sum([o.count for o in results])
9068dba1
MT
57 if count:
58 for res in results:
66862195 59 ret.append((res.country_code, res.count / count))
60024cc8
MT
60
61 return ret
62
63 def get_mirror_load(self, duration="all"):
64 query = "SELECT mirror, COUNT(mirror) AS count FROM log_download"
65
66 if duration == "today":
9068dba1 67 query += " WHERE date::date = NOW()::date"
60024cc8
MT
68
69 query += " GROUP BY mirror ORDER BY count DESC"
70
71 results = self.db.query(query)
72 ret = {}
73
74 count = sum([o.count for o in results])
9068dba1
MT
75 if count:
76 for res in results:
77 mirror = self.mirrors.get(res.mirror)
78 ret[mirror.hostname] = res.count / count
60024cc8
MT
79
80 return ret
81
82
9068dba1 83class Mirrors(Object):
95483f04
MT
84 def _get_mirrors(self, query, *args):
85 res = self.db.query(query, *args)
86
87 for row in res:
88 yield Mirror(self.backend, row.id, data=row)
89
90 def __iter__(self):
91 mirrors = self._get_mirrors("SELECT * FROM mirrors \
92 WHERE enabled IS TRUE ORDER BY hostname")
93
94 return iter(mirrors)
95
940227cb 96 def check_all(self):
95483f04 97 for mirror in self:
940227cb
MT
98 mirror.check()
99
100 def get(self, id):
9068dba1 101 return Mirror(self.backend, id)
940227cb
MT
102
103 def get_by_hostname(self, hostname):
5488a9f4 104 ret = self.db.get("SELECT * FROM mirrors WHERE hostname = %s", hostname)
940227cb 105
5488a9f4
MT
106 if ret:
107 return Mirror(self.backend, ret.id, ret)
940227cb 108
54af860e
MT
109 def get_with_file(self, filename, country=None):
110 # XXX quick and dirty solution - needs a performance boost
111 mirror_ids = [m.mirror for m in self.db.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename)]
112
113 #if country:
114 # # Sort out all mirrors that are not preferred to the given country
115 # for mirror in self.get_for_country(country):
116 # if not mirror.id in mirror_ids:
117 # mirror_ids.remove(mirror.id)
118
119 mirrors = []
120 for mirror_id in mirror_ids:
121 mirror = self.get(mirror_id)
122 if not mirror.state == "UP":
123 continue
124 mirrors.append(mirror)
125
54af860e
MT
126 return mirrors
127
5488a9f4 128 def get_for_location(self, location, max_distance=4000, filename=None):
119f55d7 129 if not location:
5488a9f4
MT
130 return []
131
132 if filename:
133 res = self.db.query("\
134 WITH client AS (SELECT point(%s, %s) AS location) \
135 SELECT * FROM mirrors WHERE mirrors.state = %s \
136 AND mirrors.id IN ( \
137 SELECT mirror FROM mirror_files WHERE filename = %s \
138 ) AND mirrors.id IN ( \
139 SELECT id FROM mirrors_locations, client \
140 WHERE geodistance(mirrors_locations.location, client.location) <= %s \
141 )",
142 location.latitude, location.longitude, "UP", filename, max_distance)
143 else:
144 res = self.db.query("\
145 WITH client AS (SELECT point(%s, %s) AS location) \
146 SELECT * FROM mirrors WHERE mirrors.state = %s AND mirrors.id IN ( \
147 SELECT id FROM mirrors_locations, client \
148 WHERE geodistance(mirrors_locations.location, client.location) <= %s \
149 )",
150 location.latitude, location.longitude, "UP", max_distance)
9068dba1
MT
151
152 mirrors = []
5488a9f4
MT
153 for row in res:
154 mirror = Mirror(self.backend, row.id, row)
155 mirrors.append(mirror)
0673d1b0 156
1b048628 157 return sorted(mirrors, reverse=True)
0673d1b0 158
edd297c4
MT
159 def get_all_files(self):
160 files = []
161
95483f04 162 for mirror in self:
edd297c4
MT
163 if not mirror.state == "UP":
164 continue
165
166 for file in mirror.filelist:
167 if not file in files:
168 files.append(file)
169
170 return files
171
5488a9f4
MT
172 def get_random(self, filename=None):
173 if filename:
174 ret = self.db.get("SELECT * FROM mirrors WHERE state = %s \
175 AND mirrors.id IN (SELECT mirror FROM mirror_files \
176 WHERE filename = %s) ORDER BY RANDOM() LIMIT 1", "UP", filename)
177 else:
178 ret = self.db.get("SELECT * FROM mirrors WHERE state = %s \
179 ORDER BY RANDOM() LIMIT 1", "UP")
180
181 if ret:
182 return Mirror(self.backend, ret.id, ret)
183
184 def file_exists(self, filename):
185 ret = self.db.get("SELECT 1 FROM mirror_files \
186 WHERE filename = %s LIMIT 1", filename)
187
188 if ret:
189 return True
190
191 return False
192
940227cb 193
9068dba1
MT
194class MirrorSet(Object):
195 def __init__(self, backend, mirrors):
196 Object.__init__(self, backend)
197
0673d1b0
MT
198 self._mirrors = mirrors
199
200 def __add__(self, other):
201 mirrors = []
202
203 for mirror in self._mirrors + other._mirrors:
204 if mirror in mirrors:
205 continue
206
207 mirrors.append(mirror)
208
9068dba1 209 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
210
211 def __sub__(self, other):
212 mirrors = self._mirrors[:]
213
214 for mirror in other._mirrors:
215 if mirror in mirrors:
216 mirrors.remove(mirror)
217
9068dba1 218 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
219
220 def __iter__(self):
221 return iter(self._mirrors)
222
223 def __len__(self):
224 return len(self._mirrors)
225
226 def __str__(self):
227 return "<MirrorSet %s>" % ", ".join([m.hostname for m in self._mirrors])
228
0673d1b0
MT
229 def get_with_file(self, filename):
230 with_file = [m.mirror for m in self.db.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename)]
231
232 mirrors = []
233 for mirror in self._mirrors:
234 if mirror.id in with_file:
235 mirrors.append(mirror)
236
9068dba1 237 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
238
239 def get_random(self):
240 mirrors = []
241 for mirror in self._mirrors:
f1f7eb7e 242 for i in range(0, mirror.priority):
0673d1b0
MT
243 mirrors.append(mirror)
244
245 return random.choice(mirrors)
246
9068dba1
MT
247 def get_for_location(self, location):
248 distance = 2500
0673d1b0
MT
249 mirrors = []
250
119f55d7 251 if location:
9068dba1 252 while len(mirrors) <= 3 and distance <= 8000:
119f55d7
MT
253 for mirror in self._mirrors:
254 if mirror in mirrors:
255 continue
0673d1b0 256
9068dba1
MT
257 mirror_distance = mirror.distance_to(location)
258 if mirror_distance is None:
259 continue
260
261 if mirror_distance <= distance:
119f55d7 262 mirrors.append(mirror)
0673d1b0 263
119f55d7 264 distance *= 1.2
0673d1b0 265
9068dba1 266 return MirrorSet(self.backend, mirrors)
0673d1b0
MT
267
268 def get_with_state(self, state):
269 mirrors = []
270
271 for mirror in self._mirrors:
272 if mirror.state == state:
273 mirrors.append(mirror)
274
9068dba1
MT
275 return MirrorSet(self.backend, mirrors)
276
0673d1b0 277
9068dba1 278class Mirror(Object):
95483f04
MT
279 def init(self, id, data=None):
280 self.id = id
281 self.data = data
940227cb 282
95483f04
MT
283 def __str__(self):
284 return self.hostname
119f55d7 285
54af860e
MT
286 def __repr__(self):
287 return "<%s %s>" % (self.__class__.__name__, self.url)
288
95483f04
MT
289 def __eq__(self, other):
290 if isinstance(other, self.__class__):
291 return self.id == other.id
9068dba1 292
95483f04
MT
293 def __lt__(self, other):
294 if isinstance(other, self.__class__):
295 return self.hostname < other.hostname
940227cb 296
95483f04
MT
297 @lazy_property
298 def url(self):
10cdef58 299 url = "%s://%s" % ("https" if self.supports_https else "http", self.hostname)
95483f04 300
940227cb
MT
301 if not self.path.startswith("/"):
302 url += "/"
95483f04 303
940227cb 304 url += "%s" % self.path
95483f04 305
940227cb
MT
306 if not self.path.endswith("/"):
307 url += "/"
95483f04 308
940227cb
MT
309 return url
310
9068dba1
MT
311 @property
312 def hostname(self):
95483f04 313 return self.data.hostname
9068dba1
MT
314
315 @property
316 def path(self):
95483f04 317 return self.data.path
940227cb 318
a69e87a1
MT
319 @property
320 def supports_https(self):
95483f04 321 return self.data.supports_https
10cdef58 322
940227cb
MT
323 @property
324 def address(self):
199b04e7 325 for addr in self.addresses4:
b898caea
MT
326 return addr
327
199b04e7 328 for addr in self.addresses6:
b898caea 329 return addr
940227cb 330
9068dba1
MT
331 @property
332 def owner(self):
95483f04 333 return self.data.owner
9068dba1 334
95483f04 335 @lazy_property
0673d1b0 336 def location(self):
95483f04 337 return self.geoip.get_location(self.address)
0673d1b0
MT
338
339 @property
340 def latitude(self):
9068dba1
MT
341 if self.location:
342 return self.location.latitude
0673d1b0
MT
343
344 @property
345 def longitude(self):
9068dba1
MT
346 if self.location:
347 return self.location.longitude
0673d1b0 348
940227cb
MT
349 @property
350 def country_code(self):
9068dba1
MT
351 if self.location:
352 return self.location.country
940227cb 353
0673d1b0
MT
354 @property
355 def country_name(self):
95483f04 356 return self.geoip.get_country_name(self.country_code)
0673d1b0 357
95483f04 358 @lazy_property
9068dba1 359 def asn(self):
95483f04 360 return self.geoip.get_asn(self.address)
0673d1b0 361
940227cb
MT
362 @property
363 def filelist(self):
364 filelist = self.db.query("SELECT filename FROM mirror_files WHERE mirror=%s ORDER BY filename", self.id)
365 return [f.filename for f in filelist]
366
54af860e
MT
367 @property
368 def prefix(self):
54af860e
MT
369 return ""
370
9068dba1 371 def build_url(self, filename):
11347e46 372 return urllib.parse.urljoin(self.url, filename)
9068dba1
MT
373
374 @property
375 def last_update(self):
95483f04 376 return self.data.last_update
9068dba1
MT
377
378 @property
379 def state(self):
95483f04 380 return self.data.state
9068dba1 381
940227cb
MT
382 def set_state(self, state):
383 logging.info("Setting state of %s to %s" % (self.hostname, state))
384
385 if self.state == state:
386 return
387
9068dba1 388 self.db.execute("UPDATE mirrors SET state = %s WHERE id = %s", state, self.id)
940227cb
MT
389
390 # Reload changed settings
95483f04 391 self.data["state"] = state
940227cb 392
9068dba1
MT
393 @property
394 def enabled(self):
95483f04 395 return self.data.enabled
9068dba1
MT
396
397 @property
398 def disabled(self):
399 return not self.enabled
400
940227cb
MT
401 def check(self):
402 logging.info("Running check for mirror %s" % self.hostname)
403
3ead0979
MT
404 self.db.execute("UPDATE mirrors SET address = %s WHERE id = %s",
405 self.address, self.id)
406
940227cb
MT
407 self.check_timestamp()
408 self.check_filelist()
409
410 def check_state(self):
411 logging.debug("Checking state of mirror %s" % self.id)
412
9068dba1 413 if not self.enabled:
940227cb 414 self.set_state("DOWN")
9068dba1
MT
415 return
416
417 now = datetime.datetime.utcnow()
418
419 time_delta = now - self.last_update
420 time_diff = time_delta.total_seconds()
940227cb 421
9068dba1
MT
422 time_down = self.settings.get_int("mirrors_time_down", 3*24*60*60)
423 if time_diff >= time_down:
940227cb 424 self.set_state("DOWN")
9068dba1 425 return
940227cb 426
9068dba1
MT
427 time_outofsync = self.settings.get_int("mirrors_time_outofsync", 6*60*60)
428 if time_diff >= time_outofsync:
429 self.set_state("OUTOFSYNC")
940227cb
MT
430 return
431
9068dba1
MT
432 self.set_state("UP")
433
434 def check_timestamp(self):
940227cb
MT
435 http = tornado.httpclient.AsyncHTTPClient()
436
437 http.fetch(self.url + ".timestamp",
54af860e 438 headers={ "Pragma" : "no-cache" },
940227cb
MT
439 callback=self.__check_timestamp_response)
440
441 def __check_timestamp_response(self, response):
442 if response.error:
443 logging.debug("Error getting timestamp from %s" % self.hostname)
a3ee39ce 444 self.set_state("DOWN")
940227cb
MT
445 return
446
447 try:
448 timestamp = int(response.body.strip())
449 except ValueError:
450 timestamp = 0
451
ea324f48 452 timestamp = datetime.datetime.utcfromtimestamp(timestamp)
9068dba1
MT
453
454 self.db.execute("UPDATE mirrors SET last_update = %s WHERE id = %s",
940227cb
MT
455 timestamp, self.id)
456
457 # Reload changed settings
95483f04 458 self.data["timestamp"] = timestamp
940227cb
MT
459
460 self.check_state()
461
462 logging.info("Successfully updated timestamp from %s" % self.hostname)
463
464 def check_filelist(self):
54af860e 465 # XXX need to remove data from disabled mirrors
9068dba1 466 if not self.enabled:
940227cb
MT
467 return
468
469 http = tornado.httpclient.AsyncHTTPClient()
470
471 http.fetch(self.url + ".filelist",
54af860e 472 headers={ "Pragma" : "no-cache" },
940227cb
MT
473 callback=self.__check_filelist_response)
474
475 def __check_filelist_response(self, response):
476 if response.error:
477 logging.debug("Error getting timestamp from %s" % self.hostname)
478 return
479
56b9c1d8 480 files = self.filelist
940227cb
MT
481
482 for file in response.body.splitlines():
56b9c1d8
MT
483 file = os.path.join(self.prefix, file)
484
485 if file in files:
486 files.remove(file)
487 continue
488
940227cb 489 self.db.execute("INSERT INTO mirror_files(mirror, filename) VALUES(%s, %s)",
56b9c1d8
MT
490 self.id, file)
491
492 for file in files:
493 self.db.execute("DELETE FROM mirror_files WHERE mirror=%s AND filename=%s",
494 self.id, file)
940227cb
MT
495
496 logging.info("Successfully updated mirror filelist from %s" % self.hostname)
497
54af860e
MT
498 @property
499 def prefer_for_countries(self):
95483f04 500 countries = self.data.get("prefer_for_countries", "")
0673d1b0
MT
501 if countries:
502 return sorted(countries.split(", "))
54af860e 503
0673d1b0
MT
504 return []
505
506 @property
507 def prefer_for_countries_names(self):
9068dba1
MT
508 countries = [self.geoip.get_country_name(c.upper()) for c in self.prefer_for_countries]
509
510 return sorted(countries)
54af860e 511
119f55d7 512 def distance_to(self, location, ignore_preference=False):
0673d1b0 513 if not location:
9068dba1 514 return None
940227cb 515
9068dba1
MT
516 country_code = None
517 if location.country:
518 country_code = location.country.lower()
519
520 if not ignore_preference and country_code in self.prefer_for_countries:
0673d1b0
MT
521 return 0
522
9068dba1
MT
523 # http://www.movable-type.co.uk/scripts/latlong.html
524
525 if self.latitude is None:
526 return None
527
528 if self.longitude is None:
529 return None
530
531 earth = 6371 # km
532 delta_lat = math.radians(self.latitude - location.latitude)
533 delta_lon = math.radians(self.longitude - location.longitude)
534
535 lat1 = math.radians(self.latitude)
536 lat2 = math.radians(location.latitude)
537
538 a = math.sin(delta_lat / 2) ** 2
539 a += math.cos(lat1) * math.cos(lat2) * (math.sin(delta_lon / 2) ** 2)
0673d1b0 540
9068dba1
MT
541 b1 = math.sqrt(a)
542 b2 = math.sqrt(1 - a)
0673d1b0 543
9068dba1
MT
544 c = 2 * math.atan2(b1, b2)
545
546 return c * earth
0673d1b0 547
0673d1b0
MT
548 @property
549 def priority(self):
95483f04 550 return self.data.get("priority", 10)
940227cb 551
bd17b7d1
MT
552 @property
553 def development(self):
95483f04 554 return self.data.get("mirrorlist_devel", False)
bd17b7d1
MT
555
556 @property
557 def mirrorlist(self):
95483f04 558 return self.data.get("mirrorlist", False)
9068dba1
MT
559
560 @property
561 def addresses(self):
562 if not hasattr(self, "__addresses"):
b898caea
MT
563 try:
564 addrinfo = socket.getaddrinfo(self.hostname, 0, socket.AF_UNSPEC, socket.SOCK_STREAM)
565 except:
566 raise Exception("Could not resolve %s" % self.hostname)
9068dba1
MT
567
568 ret = []
569 for family, socktype, proto, canonname, address in addrinfo:
570 if family == socket.AF_INET:
571 address, port = address
572 elif family == socket.AF_INET6:
573 address, port, flowid, scopeid = address
574 ret.append((family, address))
575
576 self.__addresses = ret
577
578 return self.__addresses
579
580 @property
581 def addresses6(self):
582 return [address for family, address in self.addresses if family == socket.AF_INET6]
583
584 @property
585 def addresses4(self):
586 return [address for family, address in self.addresses if family == socket.AF_INET]