]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/mirrors.py
10 import tornado
.httpclient
11 import tornado
.netutil
14 from .misc
import Object
16 class Downloads(Object
):
19 ret
= self
.db
.get("SELECT COUNT(*) AS total FROM log_download")
25 ret
= self
.db
.get("SELECT COUNT(*) AS today FROM log_download WHERE date::date = NOW()::date")
31 ret
= self
.db
.get("SELECT COUNT(*) AS yesterday FROM log_download WHERE date::date = (NOW() - INTERVAL '1 day')::date")
37 ret
= self
.db
.query("WITH downloads AS (SELECT * FROM log_download \
38 WHERE DATE(date) BETWEEN (NOW()::date - INTERVAL '30 days') AND DATE(NOW())) \
39 SELECT DATE(date) AS date, COUNT(*) AS count FROM downloads \
40 GROUP BY DATE(date) ORDER BY date")
44 def get_countries(self
, duration
="all"):
45 query
= "SELECT country_code, count(country_code) AS count FROM log_download"
47 if duration
== "today":
48 query
+= " WHERE date::date = NOW()::date"
50 query
+= " GROUP BY country_code ORDER BY count DESC"
52 results
= self
.db
.query(query
)
55 count
= sum([o
.count
for o
in results
])
58 ret
.append((res
.country_code
, res
.count
/ count
))
62 def get_mirror_load(self
, duration
="all"):
63 query
= "SELECT mirror, COUNT(mirror) AS count FROM log_download"
65 if duration
== "today":
66 query
+= " WHERE date::date = NOW()::date"
68 query
+= " GROUP BY mirror ORDER BY count DESC"
70 results
= self
.db
.query(query
)
73 count
= sum([o
.count
for o
in results
])
76 mirror
= self
.mirrors
.get(res
.mirror
)
77 ret
[mirror
.hostname
] = res
.count
/ count
82 class Mirrors(Object
):
84 for mirror
in self
.get_all():
88 return Mirror(self
.backend
, id)
91 res
= self
.db
.query("SELECT * FROM mirrors WHERE enabled = %s", True)
95 mirror
= Mirror(self
.backend
, row
.id, row
)
96 mirrors
.append(mirror
)
98 return MirrorSet(self
.backend
, sorted(mirrors
))
100 def get_by_hostname(self
, hostname
):
101 ret
= self
.db
.get("SELECT * FROM mirrors WHERE hostname = %s", hostname
)
104 return Mirror(self
.backend
, ret
.id, ret
)
106 def get_with_file(self
, filename
, country
=None):
107 # XXX quick and dirty solution - needs a performance boost
108 mirror_ids
= [m
.mirror
for m
in self
.db
.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename
)]
111 # # Sort out all mirrors that are not preferred to the given country
112 # for mirror in self.get_for_country(country):
113 # if not mirror.id in mirror_ids:
114 # mirror_ids.remove(mirror.id)
117 for mirror_id
in mirror_ids
:
118 mirror
= self
.get(mirror_id
)
119 if not mirror
.state
== "UP":
121 mirrors
.append(mirror
)
125 def get_for_location(self
, location
, max_distance
=4000, filename
=None):
130 res
= self
.db
.query("\
131 WITH client AS (SELECT point(%s, %s) AS location) \
132 SELECT * FROM mirrors WHERE mirrors.state = %s \
133 AND mirrors.id IN ( \
134 SELECT mirror FROM mirror_files WHERE filename = %s \
135 ) AND mirrors.id IN ( \
136 SELECT id FROM mirrors_locations, client \
137 WHERE geodistance(mirrors_locations.location, client.location) <= %s \
139 location
.latitude
, location
.longitude
, "UP", filename
, max_distance
)
141 res
= self
.db
.query("\
142 WITH client AS (SELECT point(%s, %s) AS location) \
143 SELECT * FROM mirrors WHERE mirrors.state = %s AND mirrors.id IN ( \
144 SELECT id FROM mirrors_locations, client \
145 WHERE geodistance(mirrors_locations.location, client.location) <= %s \
147 location
.latitude
, location
.longitude
, "UP", max_distance
)
151 mirror
= Mirror(self
.backend
, row
.id, row
)
152 mirrors
.append(mirror
)
154 return sorted(mirrors
, reverse
=True)
156 def get_all_files(self
):
159 for mirror
in self
.get_all():
160 if not mirror
.state
== "UP":
163 for file in mirror
.filelist
:
164 if not file in files
:
169 def get_random(self
, filename
=None):
171 ret
= self
.db
.get("SELECT * FROM mirrors WHERE state = %s \
172 AND mirrors.id IN (SELECT mirror FROM mirror_files \
173 WHERE filename = %s) ORDER BY RANDOM() LIMIT 1", "UP", filename
)
175 ret
= self
.db
.get("SELECT * FROM mirrors WHERE state = %s \
176 ORDER BY RANDOM() LIMIT 1", "UP")
179 return Mirror(self
.backend
, ret
.id, ret
)
181 def file_exists(self
, filename
):
182 ret
= self
.db
.get("SELECT 1 FROM mirror_files \
183 WHERE filename = %s LIMIT 1", filename
)
191 class MirrorSet(Object
):
192 def __init__(self
, backend
, mirrors
):
193 Object
.__init
__(self
, backend
)
195 self
._mirrors
= mirrors
197 def __add__(self
, other
):
200 for mirror
in self
._mirrors
+ other
._mirrors
:
201 if mirror
in mirrors
:
204 mirrors
.append(mirror
)
206 return MirrorSet(self
.backend
, mirrors
)
208 def __sub__(self
, other
):
209 mirrors
= self
._mirrors
[:]
211 for mirror
in other
._mirrors
:
212 if mirror
in mirrors
:
213 mirrors
.remove(mirror
)
215 return MirrorSet(self
.backend
, mirrors
)
218 return iter(self
._mirrors
)
221 return len(self
._mirrors
)
224 return "<MirrorSet %s>" % ", ".join([m
.hostname
for m
in self
._mirrors
])
226 def get_with_file(self
, filename
):
227 with_file
= [m
.mirror
for m
in self
.db
.query("SELECT mirror FROM mirror_files WHERE filename=%s", filename
)]
230 for mirror
in self
._mirrors
:
231 if mirror
.id in with_file
:
232 mirrors
.append(mirror
)
234 return MirrorSet(self
.backend
, mirrors
)
236 def get_random(self
):
238 for mirror
in self
._mirrors
:
239 for i
in range(0, mirror
.priority
):
240 mirrors
.append(mirror
)
242 return random
.choice(mirrors
)
244 def get_for_location(self
, location
):
249 while len(mirrors
) <= 3 and distance
<= 8000:
250 for mirror
in self
._mirrors
:
251 if mirror
in mirrors
:
254 mirror_distance
= mirror
.distance_to(location
)
255 if mirror_distance
is None:
258 if mirror_distance
<= distance
:
259 mirrors
.append(mirror
)
263 return MirrorSet(self
.backend
, mirrors
)
265 def get_with_state(self
, state
):
268 for mirror
in self
._mirrors
:
269 if mirror
.state
== state
:
270 mirrors
.append(mirror
)
272 return MirrorSet(self
.backend
, mirrors
)
275 class Mirror(Object
):
276 def __init__(self
, backend
, id, data
=None):
277 Object
.__init
__(self
, backend
)
284 self
._info
= self
.db
.get("SELECT * FROM mirrors WHERE id = %s", self
.id)
285 self
._info
["url"] = self
.generate_url()
287 self
.__location
= None
288 self
.__country
_name
= None
291 return "<%s %s>" % (self
.__class
__.__name
__, self
.url
)
293 def __cmp__(self
, other
):
294 ret
= cmp(self
.country_code
, other
.country_code
)
297 ret
= cmp(self
.hostname
, other
.hostname
)
301 def generate_url(self
):
302 url
= "%s://%s" % ("https" if self
.supports_https
else "http", self
.hostname
)
303 if not self
.path
.startswith("/"):
305 url
+= "%s" % self
.path
306 if not self
.path
.endswith("/"):
312 return self
._info
.hostname
316 return self
._info
.path
319 def supports_https(self
):
320 return self
._info
.supports_https
324 for addr
in self
.addresses4
:
327 for addr
in self
.addresses6
:
332 return self
._info
.owner
336 if self
.__location
is None:
337 self
.__location
= self
.geoip
.get_location(self
.address
)
339 return self
.__location
344 return self
.location
.latitude
349 return self
.location
.longitude
352 def coordinates(self
):
353 return (self
.latitude
, self
.longitude
)
356 def coordiante_str(self
):
359 for i
in self
.coordinates
:
360 coordinates
.append("%s" % i
)
362 return ",".join(coordinates
)
365 def country_code(self
):
367 return self
.location
.country
370 def country_name(self
):
371 if self
.__country
_name
is None:
372 self
.__country
_name
= self
.geoip
.get_country_name(self
.country_code
)
374 return self
.__country
_name
377 def location_str(self
):
380 if self
._info
.location
:
381 location
.append(self
._info
.location
)
384 location
.append(self
.location
.city
)
385 location
.append(self
.country_name
)
387 return ", ".join([s
for s
in location
if s
])
391 if not hasattr(self
, "__asn"):
392 self
.__asn
= self
.geoip
.get_asn(self
.address
)
398 filelist
= self
.db
.query("SELECT filename FROM mirror_files WHERE mirror=%s ORDER BY filename", self
.id)
399 return [f
.filename
for f
in filelist
]
407 return self
._info
.url
409 def build_url(self
, filename
):
410 return urllib
.parse
.urljoin(self
.url
, filename
)
413 def last_update(self
):
414 return self
._info
.last_update
418 return self
._info
.state
420 def set_state(self
, state
):
421 logging
.info("Setting state of %s to %s" % (self
.hostname
, state
))
423 if self
.state
== state
:
426 self
.db
.execute("UPDATE mirrors SET state = %s WHERE id = %s", state
, self
.id)
428 # Reload changed settings
429 if hasattr(self
, "_info"):
430 self
._info
["state"] = state
434 return self
._info
.enabled
438 return not self
.enabled
441 logging
.info("Running check for mirror %s" % self
.hostname
)
443 self
.db
.execute("UPDATE mirrors SET address = %s WHERE id = %s",
444 self
.address
, self
.id)
446 self
.check_timestamp()
447 self
.check_filelist()
449 def check_state(self
):
450 logging
.debug("Checking state of mirror %s" % self
.id)
453 self
.set_state("DOWN")
456 now
= datetime
.datetime
.utcnow()
458 time_delta
= now
- self
.last_update
459 time_diff
= time_delta
.total_seconds()
461 time_down
= self
.settings
.get_int("mirrors_time_down", 3*24*60*60)
462 if time_diff
>= time_down
:
463 self
.set_state("DOWN")
466 time_outofsync
= self
.settings
.get_int("mirrors_time_outofsync", 6*60*60)
467 if time_diff
>= time_outofsync
:
468 self
.set_state("OUTOFSYNC")
473 def check_timestamp(self
):
474 http
= tornado
.httpclient
.AsyncHTTPClient()
476 http
.fetch(self
.url
+ ".timestamp",
477 headers
={ "Pragma" : "no-cache" },
478 callback
=self
.__check
_timestamp
_response
)
480 def __check_timestamp_response(self
, response
):
482 logging
.debug("Error getting timestamp from %s" % self
.hostname
)
483 self
.set_state("DOWN")
487 timestamp
= int(response
.body
.strip())
491 timestamp
= datetime
.datetime
.utcfromtimestamp(timestamp
)
493 self
.db
.execute("UPDATE mirrors SET last_update = %s WHERE id = %s",
496 # Reload changed settings
497 if hasattr(self
, "_info"):
498 self
._info
["timestamp"] = timestamp
502 logging
.info("Successfully updated timestamp from %s" % self
.hostname
)
504 def check_filelist(self
):
505 # XXX need to remove data from disabled mirrors
509 http
= tornado
.httpclient
.AsyncHTTPClient()
511 http
.fetch(self
.url
+ ".filelist",
512 headers
={ "Pragma" : "no-cache" },
513 callback
=self
.__check
_filelist
_response
)
515 def __check_filelist_response(self
, response
):
517 logging
.debug("Error getting timestamp from %s" % self
.hostname
)
520 files
= self
.filelist
522 for file in response
.body
.splitlines():
523 file = os
.path
.join(self
.prefix
, file)
529 self
.db
.execute("INSERT INTO mirror_files(mirror, filename) VALUES(%s, %s)",
533 self
.db
.execute("DELETE FROM mirror_files WHERE mirror=%s AND filename=%s",
536 logging
.info("Successfully updated mirror filelist from %s" % self
.hostname
)
539 def prefer_for_countries(self
):
540 countries
= self
._info
.get("prefer_for_countries", "")
542 return sorted(countries
.split(", "))
547 def prefer_for_countries_names(self
):
548 countries
= [self
.geoip
.get_country_name(c
.upper()) for c
in self
.prefer_for_countries
]
550 return sorted(countries
)
552 def distance_to(self
, location
, ignore_preference
=False):
558 country_code
= location
.country
.lower()
560 if not ignore_preference
and country_code
in self
.prefer_for_countries
:
563 # http://www.movable-type.co.uk/scripts/latlong.html
565 if self
.latitude
is None:
568 if self
.longitude
is None:
572 delta_lat
= math
.radians(self
.latitude
- location
.latitude
)
573 delta_lon
= math
.radians(self
.longitude
- location
.longitude
)
575 lat1
= math
.radians(self
.latitude
)
576 lat2
= math
.radians(location
.latitude
)
578 a
= math
.sin(delta_lat
/ 2) ** 2
579 a
+= math
.cos(lat1
) * math
.cos(lat2
) * (math
.sin(delta_lon
/ 2) ** 2)
582 b2
= math
.sqrt(1 - a
)
584 c
= 2 * math
.atan2(b1
, b2
)
588 def traffic(self
, since
):
589 # XXX needs to be done better
592 for entry
in self
.db
.query("SELECT filename, filesize FROM files"):
593 files
[entry
.filename
] = entry
.filesize
595 query
= "SELECT COUNT(filename) as count, filename FROM log_download WHERE mirror = %s"
596 query
+= " AND date >= %s GROUP BY filename"
599 for entry
in self
.db
.query(query
, self
.id, since
):
600 if entry
.filename
in files
:
601 traffic
+= entry
.count
* files
[entry
.filename
]
607 return self
._info
.get("priority", 10)
610 def development(self
):
611 return self
._info
.get("mirrorlist_devel", False)
614 def mirrorlist(self
):
615 return self
._info
.get("mirrorlist", False)
619 if not hasattr(self
, "__addresses"):
621 addrinfo
= socket
.getaddrinfo(self
.hostname
, 0, socket
.AF_UNSPEC
, socket
.SOCK_STREAM
)
623 raise Exception("Could not resolve %s" % self
.hostname
)
626 for family
, socktype
, proto
, canonname
, address
in addrinfo
:
627 if family
== socket
.AF_INET
:
628 address
, port
= address
629 elif family
== socket
.AF_INET6
:
630 address
, port
, flowid
, scopeid
= address
631 ret
.append((family
, address
))
633 self
.__addresses
= ret
635 return self
.__addresses
638 def addresses6(self
):
639 return [address
for family
, address
in self
.addresses
if family
== socket
.AF_INET6
]
642 def addresses4(self
):
643 return [address
for family
, address
in self
.addresses
if family
== socket
.AF_INET
]