addons-server/services/update.py

363 строки
13 KiB
Python
Исходник Обычный вид История

from email.Utils import formatdate
from email.mime.text import MIMEText
import smtplib
import sys
from time import time
import traceback
from urlparse import parse_qsl
2011-02-08 08:25:33 +03:00
import MySQLdb as mysql
import sqlalchemy.pool as pool
2011-02-25 04:44:51 +03:00
2011-02-22 21:32:22 +03:00
import commonware.log
2011-02-25 04:44:51 +03:00
from django.core.management import setup_environ
from django.utils.http import urlencode
2011-02-08 08:25:33 +03:00
import settings_local as settings
2011-02-25 04:44:51 +03:00
setup_environ(settings)
2012-02-01 02:33:11 +04:00
from lib import log_settings_base
2011-09-01 21:43:40 +04:00
# This has to be imported after the settings so statsd knows where to log to.
from statsd import statsd
try:
from compare import version_int
except ImportError:
from apps.versions.compare import version_int
from constants import base
from utils import get_mirror, APP_GUIDS, PLATFORMS, STATUSES_PUBLIC
2011-02-08 08:25:33 +03:00
good_rdf = """<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<RDF:Description about="urn:mozilla:%(type)s:%(guid)s">
<em:updates>
<RDF:Seq>
<RDF:li resource="urn:mozilla:%(type)s:%(guid)s:%(version)s"/>
</RDF:Seq>
</em:updates>
</RDF:Description>
<RDF:Description about="urn:mozilla:%(type)s:%(guid)s:%(version)s">
<em:version>%(version)s</em:version>
<em:targetApplication>
<RDF:Description>
<em:id>%(appguid)s</em:id>
<em:minVersion>%(min)s</em:minVersion>
<em:maxVersion>%(max)s</em:maxVersion>
<em:updateLink>%(url)s</em:updateLink>
%(if_update)s
%(if_hash)s
</RDF:Description>
</em:targetApplication>
</RDF:Description>
</RDF:RDF>"""
bad_rdf = """<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
</RDF:RDF>"""
no_updates_rdf = """<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<RDF:Description about="urn:mozilla:%(type)s:%(guid)s">
<em:updates>
<RDF:Seq>
</RDF:Seq>
</em:updates>
</RDF:Description>
</RDF:RDF>"""
2011-02-22 21:32:22 +03:00
timing_log = commonware.log.getLogger('z.timer')
2011-05-12 03:07:02 +04:00
error_log = commonware.log.getLogger('z.services')
2011-02-22 21:32:22 +03:00
def getconn():
db = settings.SERVICES_DATABASE
return mysql.connect(host=db['HOST'], user=db['USER'],
passwd=db['PASSWORD'], db=db['NAME'])
2011-06-18 00:16:10 +04:00
mypool = pool.QueuePool(getconn, max_overflow=10, pool_size=5, recycle=300)
2011-02-08 08:25:33 +03:00
class Update(object):
def __init__(self, data, compat_mode='strict'):
self.conn, self.cursor = None, None
self.data = data.copy()
2011-02-08 08:25:33 +03:00
self.data['row'] = {}
self.flags = {'use_version': False, 'multiple_status': False}
2011-02-08 08:25:33 +03:00
self.is_beta_version = False
self.version_int = 0
self.compat_mode = compat_mode
2011-02-08 08:25:33 +03:00
def is_valid(self):
# If you accessing this from unit tests, then before calling
# is valid, you can assign your own cursor.
if not self.cursor:
self.conn = mypool.connect()
self.cursor = self.conn.cursor()
2011-02-08 08:25:33 +03:00
data = self.data
2012-01-12 23:36:57 +04:00
# Version can be blank.
data['version'] = data.get('version', '')
for field in ['reqVersion', 'id', 'appID', 'appVersion']:
if field not in data:
2011-02-08 08:25:33 +03:00
return False
data['app_id'] = APP_GUIDS.get(data['appID'])
if not data['app_id']:
2011-02-08 08:25:33 +03:00
return False
sql = """SELECT id, status, addontype_id, guid FROM addons
WHERE guid = %(guid)s AND inactive = 0 LIMIT 1;"""
2011-02-08 08:25:33 +03:00
self.cursor.execute(sql, {'guid': self.data['id']})
result = self.cursor.fetchone()
if result is None:
return False
data['id'], data['addon_status'], data['type'], data['guid'] = result
data['version_int'] = version_int(data['appVersion'])
2011-02-08 08:25:33 +03:00
if 'appOS' in data:
2011-02-08 08:25:33 +03:00
for k, v in PLATFORMS.items():
if k in data['appOS']:
data['appOS'] = v
2011-02-08 08:25:33 +03:00
break
else:
data['appOS'] = None
2011-02-08 08:25:33 +03:00
2012-01-12 23:36:57 +04:00
self.is_beta_version = base.VERSION_BETA.search(data['version'])
2011-02-08 08:25:33 +03:00
return True
def get_beta(self):
data = self.data
data['status'] = base.STATUS_PUBLIC
if data['addon_status'] == base.STATUS_PUBLIC:
# Beta channel looks at the addon name to see if it's beta.
2011-02-08 08:25:33 +03:00
if self.is_beta_version:
# For beta look at the status of the existing files.
2011-02-08 08:25:33 +03:00
sql = """
SELECT versions.id, status
FROM files INNER JOIN versions
ON files.version_id = versions.id
WHERE versions.addon_id = %(id)s
AND versions.version = %(version)s LIMIT 1;"""
self.cursor.execute(sql, data)
2011-02-08 08:25:33 +03:00
result = self.cursor.fetchone()
# Only change the status if there are files.
2011-02-08 08:25:33 +03:00
if result is not None:
status = result[1]
# If it's in Beta or Public, then we should be looking
# for similar. If not, find something public.
if status in (base.STATUS_BETA, base.STATUS_PUBLIC):
data['status'] = status
2011-02-08 08:25:33 +03:00
else:
data.update(STATUSES_PUBLIC)
2011-02-08 08:25:33 +03:00
self.flags['multiple_status'] = True
elif data['addon_status'] in (base.STATUS_LITE,
base.STATUS_LITE_AND_NOMINATED):
data['status'] = base.STATUS_LITE
2011-02-08 08:25:33 +03:00
else:
# Otherwise then we'll keep the update within the current version.
data['status'] = base.STATUS_NULL
self.flags['use_version'] = True
2011-02-08 08:25:33 +03:00
def get_update(self):
self.get_beta()
data = self.data
sql = ["""
SELECT
addons.guid as guid, addons.addontype_id as type,
addons.inactive as disabled_by_user,
applications.guid as appguid, appmin.version as min,
appmax.version as max, files.id as file_id,
files.status as file_status, files.hash,
files.filename, versions.id as version_id,
files.datestatuschanged as datestatuschanged,
files.strict_compatibility as strict_compat,
versions.releasenotes, versions.version as version,
addons.premium_type
FROM versions
INNER JOIN addons
ON addons.id = versions.addon_id AND addons.id = %(id)s
INNER JOIN applications_versions
ON applications_versions.version_id = versions.id
INNER JOIN applications
ON applications_versions.application_id = applications.id
AND applications.id = %(app_id)s
INNER JOIN appversions appmin
ON appmin.id = applications_versions.min
INNER JOIN appversions appmax
ON appmax.id = applications_versions.max
INNER JOIN files
ON files.version_id = versions.id AND (files.platform_id = 1
"""]
2011-02-08 08:25:33 +03:00
if data.get('appOS'):
sql.append(' OR files.platform_id = %(appOS)s')
2011-02-08 08:25:33 +03:00
if self.flags['use_version']:
sql.append(') WHERE files.status > %(status)s AND '
2011-02-08 08:25:33 +03:00
'versions.version = %(version)s ')
else:
if self.flags['multiple_status']:
# Note that getting this properly escaped is a pain.
# Suggestions for improvement welcome.
sql.append(') WHERE files.status in (%(STATUS_PUBLIC)s,'
'%(STATUS_LITE)s,%(STATUS_LITE_AND_NOMINATED)s) ')
2011-02-08 08:25:33 +03:00
else:
sql.append(') WHERE files.status = %(status)s ')
2011-02-08 08:25:33 +03:00
sql.append('AND appmin.version_int <= %(version_int)s ')
2011-02-08 08:25:33 +03:00
if self.compat_mode == 'ignore':
pass # no further SQL modification required.
elif self.compat_mode == 'normal':
# When file has strict_compatibility enabled, or file has binary
# components, default to compatible is disabled.
sql.append("""AND
CASE WHEN files.strict_compatibility = 1 OR
files.binary_components = 1
THEN appmax.version_int >= %(version_int)s ELSE 1 END
""")
# Filter out versions found in compat overrides
sql.append("""AND
NOT versions.id IN (
SELECT version_id FROM incompatible_versions
WHERE app_id=%(app_id)s AND
(min_app_version='0' AND
max_app_version_int >= %(version_int)s) OR
(min_app_version_int <= %(version_int)s AND
max_app_version='*') OR
(min_app_version_int <= %(version_int)s AND
max_app_version_int >= %(version_int)s)) """)
else: # Not defined or 'strict'.
sql.append('AND appmax.version_int >= %(version_int)s ')
sql.append('ORDER BY versions.id DESC LIMIT 1;')
self.cursor.execute(''.join(sql), data)
2011-02-08 08:25:33 +03:00
result = self.cursor.fetchone()
2011-02-08 08:25:33 +03:00
if result:
row = dict(zip([
'guid', 'type', 'disabled_by_user', 'appguid', 'min', 'max',
'file_id', 'file_status', 'hash', 'filename', 'version_id',
'datestatuschanged', 'strict_compat', 'releasenotes',
'version', 'premium_type'],
2011-02-08 08:25:33 +03:00
list(result)))
row['type'] = base.ADDON_SLUGS_UPDATE[row['type']]
if row['premium_type'] == base.ADDON_PREMIUM:
qs = urlencode(dict((k, data.get(k, ''))
for k in base.WATERMARK_KEYS))
row['url'] = (u'%s/downloads/watermarked/%s?%s' %
(settings.SITE_URL, row['file_id'], qs))
else:
row['url'] = get_mirror(self.data['addon_status'],
self.data['id'], row)
data['row'] = row
2011-02-08 08:25:33 +03:00
return True
return False
def get_bad_rdf(self):
return bad_rdf
def get_rdf(self):
if self.is_valid():
if self.get_update():
rdf = self.get_good_rdf()
else:
rdf = self.get_no_updates_rdf()
2011-02-08 08:25:33 +03:00
else:
rdf = self.get_bad_rdf()
self.cursor.close()
if self.conn:
self.conn.close()
return rdf
def get_no_updates_rdf(self):
name = base.ADDON_SLUGS_UPDATE[self.data['type']]
return no_updates_rdf % ({'guid': self.data['guid'], 'type': name})
2011-02-08 08:25:33 +03:00
def get_good_rdf(self):
data = self.data['row']
data['if_hash'] = ''
if data['hash']:
data['if_hash'] = ('<em:updateHash>%s</em:updateHash>' %
data['hash'])
data['if_update'] = ''
if data['releasenotes']:
data['if_update'] = ('<em:updateInfoURL>%s%s%s/%%APP_LOCALE%%/'
'</em:updateInfoURL>' %
(settings.SITE_URL, '/versions/updateInfo/',
data['version_id']))
return good_rdf % data
def format_date(self, secs):
return '%s GMT' % formatdate(time() + secs)[:25]
2011-02-08 08:25:33 +03:00
def get_headers(self, length):
return [('Content-Type', 'text/xml'),
('Cache-Control', 'public, max-age=3600'),
('Last-Modified', self.format_date(0)),
('Expires', self.format_date(3600)),
('Content-Length', str(length))]
def mail_exception(data):
if settings.EMAIL_BACKEND != 'django.core.mail.backends.smtp.EmailBackend':
return
msg = MIMEText('%s\n\n%s' % (
'\n'.join(traceback.format_exception(*sys.exc_info())), data))
msg['Subject'] = '[Update] ERROR at /services/update'
2011-05-11 22:16:03 +04:00
msg['To'] = ','.join([a[1] for a in settings.ADMINS])
msg['From'] = settings.DEFAULT_FROM_EMAIL
conn = smtplib.SMTP(getattr(settings, 'EMAIL_HOST', 'localhost'),
getattr(settings, 'EMAIL_PORT', '25'))
2011-05-11 22:16:03 +04:00
conn.sendmail(settings.DEFAULT_FROM_EMAIL, msg['To'], msg.as_string())
conn.close()
2011-05-12 03:07:02 +04:00
def log_exception(data):
(typ, value, traceback) = sys.exc_info()
error_log.error(u'Type: %s, %s. Query: %s' % (typ, value, data))
2011-02-08 08:25:33 +03:00
def application(environ, start_response):
2011-02-22 21:32:22 +03:00
start = time()
2011-02-08 08:25:33 +03:00
status = '200 OK'
2011-02-22 21:32:22 +03:00
timing = (environ['REQUEST_METHOD'], '%s?%s' %
(environ['SCRIPT_NAME'], environ['QUERY_STRING']))
2011-09-01 21:43:40 +04:00
with statsd.timer('services.update'):
data = dict(parse_qsl(environ['QUERY_STRING']))
compat_mode = data.pop('compatMode', 'strict')
2011-09-01 21:43:40 +04:00
try:
update = Update(data, compat_mode)
2011-09-01 21:43:40 +04:00
output = update.get_rdf()
start_response(status, update.get_headers(len(output)))
except:
timing_log.info('%s "%s" (500) %.2f [ANON]' %
(timing[0], timing[1], time() - start))
#mail_exception(data)
log_exception(data)
raise
timing_log.info('%s "%s" (200) %.2f [ANON]' %
2011-02-22 21:32:22 +03:00
(timing[0], timing[1], time() - start))
2011-03-04 21:38:05 +03:00
return [output]