]>
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
15 from .decorators
import *
17 class Downloads(Object
):
20 ret
= self
.db
.get("SELECT COUNT(*) AS total FROM log_download")
26 ret
= self
.db
.get("SELECT COUNT(*) AS today FROM log_download WHERE date::date = NOW()::date")
32 ret
= self
.db
.get("SELECT COUNT(*) AS yesterday FROM log_download WHERE date::date = (NOW() - INTERVAL '1 day')::date")
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")
45 def get_countries(self
, duration
="all"):
46 query
= "SELECT country_code, count(country_code) AS count FROM log_download"
48 if duration
== "today":
49 query
+= " WHERE date::date = NOW()::date"
51 query
+= " GROUP BY country_code ORDER BY count DESC"
53 results
= self
.db
.query(query
)
56 count
= sum([o
.count
for o
in results
])
59 ret
.append((res
.country_code
, res
.count
/ count
))
63 def get_mirror_load(self
, duration
="all"):
64 query
= "SELECT mirror, COUNT(mirror) AS count FROM log_download"
66 if duration
== "today":
67 query
+= " WHERE date::date = NOW()::date"
69 query
+= " GROUP BY mirror ORDER BY count DESC"
71 results
= self
.db
.query(query
)
74 count
= sum([o
.count
for o
in results
])
77 mirror
= self
.mirrors
.get(res
.mirror
)
78 ret
[mirror
.hostname
] = res
.count
/ count
83 class Mirrors(Object
):
84 def _get_mirrors(self
, query
, *args
):
85 res
= self
.db
.query(query
, *args
)
88 yield Mirror(self
.backend
, row
.id, data
=row
)
91 mirrors
= self
._get
_mirrors
("SELECT * FROM mirrors \
92 WHERE enabled IS TRUE ORDER BY hostname")
101 return Mirror(self
.backend
, id)
103 def get_by_hostname(self
, hostname
):
104 ret
= self
.db
.get("SELECT * FROM mirrors WHERE hostname = %s", hostname
)
107 return Mirror(self
.backend
, ret
.id, ret
)
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
)]
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)
120 for mirror_id
in mirror_ids
:
121 mirror
= self
.get(mirror_id
)
122 if not mirror
.state
== "UP":
124 mirrors
.append(mirror
)
128 def get_for_location(self
, location
, max_distance
=4000, filename
=None):
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 \
142 location
.latitude
, location
.longitude
, "UP", filename
, max_distance
)
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 \
150 location
.latitude
, location
.longitude
, "UP", max_distance
)
154 mirror
= Mirror(self
.backend
, row
.id, row
)
155 mirrors
.append(mirror
)
157 return sorted(mirrors
, reverse
=True)
159 def get_all_files(self
):
163 if not mirror
.state
== "UP":
166 for file in mirror
.filelist
:
167 if not file in files
:
172 def get_random(self
, filename
=None):
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
)
178 ret
= self
.db
.get("SELECT * FROM mirrors WHERE state = %s \
179 ORDER BY RANDOM() LIMIT 1", "UP")
182 return Mirror(self
.backend
, ret
.id, ret
)
184 def file_exists(self
, filename
):
185 ret
= self
.db
.get("SELECT 1 FROM mirror_files \
186 WHERE filename = %s LIMIT 1", filename
)
194 class MirrorSet(Object
):
195 def __init__(self
, backend
, mirrors
):
196 Object
.__init
__(self
, backend
)
198 self
._mirrors
= mirrors
200 def __add__(self
, other
):
203 for mirror
in self
._mirrors
+ other
._mirrors
:
204 if mirror
in mirrors
:
207 mirrors
.append(mirror
)
209 return MirrorSet(self
.backend
, mirrors
)
211 def __sub__(self
, other
):
212 mirrors
= self
._mirrors
[:]
214 for mirror
in other
._mirrors
:
215 if mirror
in mirrors
:
216 mirrors
.remove(mirror
)
218 return MirrorSet(self
.backend
, mirrors
)
221 return iter(self
._mirrors
)
224 return len(self
._mirrors
)
227 return "<MirrorSet %s>" % ", ".join([m
.hostname
for m
in self
._mirrors
])
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
)]
233 for mirror
in self
._mirrors
:
234 if mirror
.id in with_file
:
235 mirrors
.append(mirror
)
237 return MirrorSet(self
.backend
, mirrors
)
239 def get_random(self
):
241 for mirror
in self
._mirrors
:
242 for i
in range(0, mirror
.priority
):
243 mirrors
.append(mirror
)
245 return random
.choice(mirrors
)
247 def get_for_location(self
, location
):
252 while len(mirrors
) <= 3 and distance
<= 8000:
253 for mirror
in self
._mirrors
:
254 if mirror
in mirrors
:
257 mirror_distance
= mirror
.distance_to(location
)
258 if mirror_distance
is None:
261 if mirror_distance
<= distance
:
262 mirrors
.append(mirror
)
266 return MirrorSet(self
.backend
, mirrors
)
268 def get_with_state(self
, state
):
271 for mirror
in self
._mirrors
:
272 if mirror
.state
== state
:
273 mirrors
.append(mirror
)
275 return MirrorSet(self
.backend
, mirrors
)
278 class Mirror(Object
):
279 def init(self
, id, data
=None):
287 return "<%s %s>" % (self
.__class
__.__name
__, self
.url
)
289 def __eq__(self
, other
):
290 if isinstance(other
, self
.__class
__):
291 return self
.id == other
.id
293 def __lt__(self
, other
):
294 if isinstance(other
, self
.__class
__):
295 return self
.hostname
< other
.hostname
299 url
= "%s://%s" % ("https" if self
.supports_https
else "http", self
.hostname
)
301 if not self
.path
.startswith("/"):
304 url
+= "%s" % self
.path
306 if not self
.path
.endswith("/"):
313 return self
.data
.hostname
317 return self
.data
.path
320 def supports_https(self
):
321 return self
.data
.supports_https
325 for addr
in self
.addresses4
:
328 for addr
in self
.addresses6
:
333 return self
.data
.owner
337 return self
.geoip
.get_location(self
.address
)
342 return self
.location
.latitude
347 return self
.location
.longitude
350 def country_code(self
):
352 return self
.location
.country
355 def country_name(self
):
356 return self
.geoip
.get_country_name(self
.country_code
)
360 return self
.geoip
.get_asn(self
.address
)
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
]
371 def build_url(self
, filename
):
372 return urllib
.parse
.urljoin(self
.url
, filename
)
375 def last_update(self
):
376 return self
.data
.last_update
380 return self
.data
.state
382 def set_state(self
, state
):
383 logging
.info("Setting state of %s to %s" % (self
.hostname
, state
))
385 if self
.state
== state
:
388 self
.db
.execute("UPDATE mirrors SET state = %s WHERE id = %s", state
, self
.id)
390 # Reload changed settings
391 self
.data
["state"] = state
395 return self
.data
.enabled
399 return not self
.enabled
402 logging
.info("Running check for mirror %s" % self
.hostname
)
404 self
.db
.execute("UPDATE mirrors SET address = %s WHERE id = %s",
405 self
.address
, self
.id)
407 self
.check_timestamp()
408 self
.check_filelist()
410 def check_state(self
):
411 logging
.debug("Checking state of mirror %s" % self
.id)
414 self
.set_state("DOWN")
417 now
= datetime
.datetime
.utcnow()
419 time_delta
= now
- self
.last_update
420 time_diff
= time_delta
.total_seconds()
422 time_down
= self
.settings
.get_int("mirrors_time_down", 3*24*60*60)
423 if time_diff
>= time_down
:
424 self
.set_state("DOWN")
427 time_outofsync
= self
.settings
.get_int("mirrors_time_outofsync", 6*60*60)
428 if time_diff
>= time_outofsync
:
429 self
.set_state("OUTOFSYNC")
434 def check_timestamp(self
):
435 http
= tornado
.httpclient
.AsyncHTTPClient()
437 http
.fetch(self
.url
+ ".timestamp",
438 headers
={ "Pragma" : "no-cache" },
439 callback
=self
.__check
_timestamp
_response
)
441 def __check_timestamp_response(self
, response
):
443 logging
.debug("Error getting timestamp from %s" % self
.hostname
)
444 self
.set_state("DOWN")
448 timestamp
= int(response
.body
.strip())
452 timestamp
= datetime
.datetime
.utcfromtimestamp(timestamp
)
454 self
.db
.execute("UPDATE mirrors SET last_update = %s WHERE id = %s",
457 # Reload changed settings
458 self
.data
["timestamp"] = timestamp
462 logging
.info("Successfully updated timestamp from %s" % self
.hostname
)
464 def check_filelist(self
):
465 # XXX need to remove data from disabled mirrors
469 http
= tornado
.httpclient
.AsyncHTTPClient()
471 http
.fetch(self
.url
+ ".filelist",
472 headers
={ "Pragma" : "no-cache" },
473 callback
=self
.__check
_filelist
_response
)
475 def __check_filelist_response(self
, response
):
477 logging
.debug("Error getting timestamp from %s" % self
.hostname
)
480 files
= self
.filelist
482 for file in response
.body
.splitlines():
483 file = os
.path
.join(self
.prefix
, file)
489 self
.db
.execute("INSERT INTO mirror_files(mirror, filename) VALUES(%s, %s)",
493 self
.db
.execute("DELETE FROM mirror_files WHERE mirror=%s AND filename=%s",
496 logging
.info("Successfully updated mirror filelist from %s" % self
.hostname
)
499 def prefer_for_countries(self
):
500 countries
= self
.data
.get("prefer_for_countries", "")
502 return sorted(countries
.split(", "))
507 def prefer_for_countries_names(self
):
508 countries
= [self
.geoip
.get_country_name(c
.upper()) for c
in self
.prefer_for_countries
]
510 return sorted(countries
)
512 def distance_to(self
, location
, ignore_preference
=False):
518 country_code
= location
.country
.lower()
520 if not ignore_preference
and country_code
in self
.prefer_for_countries
:
523 # http://www.movable-type.co.uk/scripts/latlong.html
525 if self
.latitude
is None:
528 if self
.longitude
is None:
532 delta_lat
= math
.radians(self
.latitude
- location
.latitude
)
533 delta_lon
= math
.radians(self
.longitude
- location
.longitude
)
535 lat1
= math
.radians(self
.latitude
)
536 lat2
= math
.radians(location
.latitude
)
538 a
= math
.sin(delta_lat
/ 2) ** 2
539 a
+= math
.cos(lat1
) * math
.cos(lat2
) * (math
.sin(delta_lon
/ 2) ** 2)
542 b2
= math
.sqrt(1 - a
)
544 c
= 2 * math
.atan2(b1
, b2
)
550 return self
.data
.get("priority", 10)
553 def development(self
):
554 return self
.data
.get("mirrorlist_devel", False)
557 def mirrorlist(self
):
558 return self
.data
.get("mirrorlist", False)
562 if not hasattr(self
, "__addresses"):
564 addrinfo
= socket
.getaddrinfo(self
.hostname
, 0, socket
.AF_UNSPEC
, socket
.SOCK_STREAM
)
566 raise Exception("Could not resolve %s" % self
.hostname
)
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
))
576 self
.__addresses
= ret
578 return self
.__addresses
581 def addresses6(self
):
582 return [address
for family
, address
in self
.addresses
if family
== socket
.AF_INET6
]
585 def addresses4(self
):
586 return [address
for family
, address
in self
.addresses
if family
== socket
.AF_INET
]