Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: sitescripts/extensions/bin/updateDownloadLinks.py

Issue 5723465818570752: Issue 520 - Generate PAD files for download portals when updating download links (Closed)
Patch Set: Created May 26, 2014, 10 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
1 # coding: utf-8 1 # coding: utf-8
2 2
3 # This file is part of the Adblock Plus web scripts, 3 # This file is part of the Adblock Plus web scripts,
4 # Copyright (C) 2006-2014 Eyeo GmbH 4 # Copyright (C) 2006-2014 Eyeo GmbH
5 # 5 #
6 # Adblock Plus is free software: you can redistribute it and/or modify 6 # Adblock Plus is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License version 3 as 7 # it under the terms of the GNU General Public License version 3 as
8 # published by the Free Software Foundation. 8 # published by the Free Software Foundation.
9 # 9 #
10 # Adblock Plus is distributed in the hope that it will be useful, 10 # Adblock Plus is distributed in the hope that it will be useful,
(...skipping 11 matching lines...) Expand all
22 This script generates a list of extensions and saves these with download links 22 This script generates a list of extensions and saves these with download links
23 and version information 23 and version information
24 """ 24 """
25 25
26 import sys, os, re, urllib, urllib2, urlparse, subprocess, time 26 import sys, os, re, urllib, urllib2, urlparse, subprocess, time
27 import xml.dom.minidom as dom 27 import xml.dom.minidom as dom
28 from ConfigParser import SafeConfigParser 28 from ConfigParser import SafeConfigParser
29 from StringIO import StringIO 29 from StringIO import StringIO
30 from sitescripts.utils import get_config, get_template 30 from sitescripts.utils import get_config, get_template
31 from sitescripts.extensions.utils import compareVersions, Configuration, getSafa riCertificateID 31 from sitescripts.extensions.utils import compareVersions, Configuration, getSafa riCertificateID
32 from sitescripts.extensions.android import get_min_sdk_version
33 from sitescripts.extensions.pad import PadFile
32 from buildtools.packagerGecko import KNOWN_APPS 34 from buildtools.packagerGecko import KNOWN_APPS
33 35
34 def urlencode(value): 36 def urlencode(value):
35 return urllib.quote(value.encode('utf-8'), '') 37 return urllib.quote(value.encode('utf-8'), '')
36 38
37 def urlopen(url, attempts=3): 39 def urlopen(url, attempts=3):
38 """ 40 """
39 Tries to open a particular URL, retries on failure. 41 Tries to open a particular URL, retries on failure.
40 """ 42 """
41 for i in range(attempts): 43 for i in range(attempts):
(...skipping 17 matching lines...) Expand all
59 versionTag = versionTags[0] if len(versionTags) > 0 else None 61 versionTag = versionTags[0] if len(versionTags) > 0 else None
60 if linkTag and versionTag and linkTag.firstChild and versionTag.firstChild: 62 if linkTag and versionTag and linkTag.firstChild and versionTag.firstChild:
61 return (linkTag.firstChild.data, versionTag.firstChild.data) 63 return (linkTag.firstChild.data, versionTag.firstChild.data)
62 else: 64 else:
63 return (None, None) 65 return (None, None)
64 66
65 def getGoogleDownloadLink(galleryID): 67 def getGoogleDownloadLink(galleryID):
66 """ 68 """
67 gets download link for a Chrome add-on from the Chrome Gallery site 69 gets download link for a Chrome add-on from the Chrome Gallery site
68 """ 70 """
69 param = 'id=%s&uc' % urlencode(galleryID) 71 galleryID = urlencode(galleryID)
70 url = 'https://clients2.google.com/service/update2/crx?x=%s' % urlencode(param ) 72
71 contents = urlopen(url).read() 73 response = urlopen('https://clients2.google.com/service/update2/crx?x=' + urle ncode('id=%s&uc' % galleryID))
Wladimir Palant 2014/05/26 10:54:57 a) Why use an overly long line instead of a tempor
Sebastian Noack 2014/05/26 12:04:53 Agreed.
Wladimir Palant 2014/05/26 13:26:53 What we have here is a URL template, substitution
Sebastian Noack 2014/05/27 12:01:23 Done.
72 document = dom.parseString(contents) 74 document = dom.parse(response)
73 updateTags = document.getElementsByTagName('updatecheck') 75 updateTags = document.getElementsByTagName('updatecheck')
74 updateTag = updateTags[0] if len(updateTags) > 0 else None 76 version = updateTags and updateTags[0].getAttribute('version')
75 if updateTag and updateTag.hasAttribute('codebase') and updateTag.hasAttribute ('version'): 77
76 return (updateTag.getAttribute('codebase'), updateTag.getAttribute('version' )) 78 if not version:
77 else:
78 return (None, None) 79 return (None, None)
79 80
81 request = urllib2.Request('https://chrome.google.com/webstore/detail/_/' + gal leryID)
82 request.get_method = lambda : 'HEAD'
83 url = urllib2.urlopen(request).geturl()
84
85 return (url, version)
86
80 def getOperaDownloadLink(galleryID): 87 def getOperaDownloadLink(galleryID):
81 """ 88 """
82 gets download link for an Opera add-on from the Opera Addons site 89 gets download link for an Opera add-on from the Opera Addons site
83 """ 90 """
84 class HeadRequest(urllib2.Request): 91 galleryID = urlencode(galleryID)
85 def get_method(self):
86 return "HEAD"
87 92
88 url = 'https://addons.opera.com/extensions/download/%s/' % urlencode(galleryID ) 93 request = urllib2.Request('https://addons.opera.com/extensions/download/%s/' % galleryID)
89 response = urllib2.urlopen(HeadRequest(url)) 94 request.get_method = lambda : 'HEAD'
90 content_disposition = response.info().dict.get('content-disposition', None) 95 response = urllib2.urlopen(request)
91 if content_disposition != None: 96
92 match = re.search(r'filename=\S+-([\d.]+)-\d+\.oex$', content_disposition) 97 content_disposition = response.info().getheader('Content-Disposition')
93 else: 98 if content_disposition:
94 match = None; 99 match = re.search(r'filename=\S+-([\d.]+)-\d+\.crx$', content_disposition)
95 if match: 100 if match:
96 return (url, match.group(1)) 101 return ('https://addons.opera.com/extensions/details/%s/' % galleryID , ma tch.group(1))
97 else: 102
98 return (None, None) 103 return (None, None)
99 104
100 def getLocalLink(repo): 105 def getLocalLink(repo):
101 """ 106 """
102 gets the link for the newest download of an add-on in the local downloads 107 gets the link for the newest download of an add-on in the local downloads
103 repository 108 repository
104 """ 109 """
105 url = repo.downloadsURL
106
107 highestURL = None 110 highestURL = None
108 highestVersion = None 111 highestVersion = None
109 112
110 if repo.type in ('gecko', 'chrome', 'opera', 'safari'): 113 for filename, version in repo.getDownloads():
111 prefix = readRawMetadata(repo).get('general', 'basename') 114 if not highestVersion or compareVersions(version, highestVersion) > 0:
112 else: 115 highestURL = urlparse.urljoin(repo.downloadsURL, filename)
113 prefix = os.path.basename(repo.repository) 116 highestVersion = version
114 prefix += '-'
115 suffix = repo.packageSuffix
116 117
117 # go through the downloads repository looking for downloads matching this exte nsion
118 command = ['hg', 'locate', '-R', repo.downloadsRepo, '-r', 'default']
119 result = subprocess.check_output(command)
120 for fileName in result.splitlines():
121 if fileName.startswith(prefix) and fileName.endswith(suffix):
122 version = fileName[len(prefix):len(fileName) - len(suffix)]
123 if highestVersion == None or compareVersions(version, highestVersion) > 0:
124 highestURL = urlparse.urljoin(url, fileName)
125 highestVersion = version
126 return (highestURL, highestVersion) 118 return (highestURL, highestVersion)
127 119
128 def getDownloadLink(repo): 120 def getDownloadLink(repo):
129 """ 121 """
130 gets the download link to the most current version of an extension 122 gets the download link to the most current version of an extension
131 """ 123 """
132 galleryURL = None 124 # you can't easily install extensions from third-party sources on Chrome
133 galleryVersion = None 125 # and Opera. So always get the link for the version on the Web Store.
134 if repo.type == "gecko" and repo.galleryID: 126 if repo.type in ("chrome", "opera") and repo.galleryID:
Wladimir Palant 2014/05/26 10:54:57 Isn't the repo.type check redundant here? You chec
Sebastian Noack 2014/05/26 12:04:53 You are right.
127 if repo.type == "chrome":
128 return getGoogleDownloadLink(repo.galleryID)
129
130 if repo.type == "opera":
131 return getOperaDownloadLink(repo.galleryID)
132
133 (localURL, localVersion) = getLocalLink(repo)
134
135 # get a link to Firefox Add-Ons, if the latest version has been published ther e
136 if repo.type == 'gecko' and repo.galleryID:
135 (galleryURL, galleryVersion) = getMozillaDownloadLink(repo.galleryID) 137 (galleryURL, galleryVersion) = getMozillaDownloadLink(repo.galleryID)
136 elif repo.type == "chrome" and repo.galleryID: 138 if not localVersion or (galleryVersion and
137 (galleryURL, galleryVersion) = getGoogleDownloadLink(repo.galleryID) 139 compareVersions(galleryVersion, localVersion) >= 0):
138 elif repo.type == "opera" and repo.galleryID: 140 return (galleryURL, galleryVersion)
139 (galleryURL, galleryVersion) = getOperaDownloadLink(repo.galleryID)
140 141
141 (downloadsURL, downloadsVersion) = getLocalLink(repo) 142 return (localURL, localVersion)
142 if galleryVersion == None or (downloadsVersion != None and
143 compareVersions(galleryVersion, downloadsVersion ) < 0):
144 return (downloadsURL, downloadsVersion)
145 else:
146 return (galleryURL, galleryVersion)
147 143
148 def getQRCode(text): 144 def getQRCode(text):
149 try: 145 try:
150 import qrcode 146 import qrcode
151 import base64 147 import base64
152 import Image # required by qrcode but not formally a dependency 148 import Image # required by qrcode but not formally a dependency
153 except: 149 except:
154 return None 150 return None
155 151
156 data = StringIO() 152 data = StringIO()
(...skipping 11 matching lines...) Expand all
168 continue 164 continue
169 if not result.has_section(repo.repositoryName): 165 if not result.has_section(repo.repositoryName):
170 result.add_section(repo.repositoryName) 166 result.add_section(repo.repositoryName)
171 result.set(repo.repositoryName, "downloadURL", downloadURL) 167 result.set(repo.repositoryName, "downloadURL", downloadURL)
172 result.set(repo.repositoryName, "version", version) 168 result.set(repo.repositoryName, "version", version)
173 169
174 qrcode = getQRCode(downloadURL) 170 qrcode = getQRCode(downloadURL)
175 if qrcode != None: 171 if qrcode != None:
176 result.set(repo.repositoryName, "qrcode", qrcode) 172 result.set(repo.repositoryName, "qrcode", qrcode)
177 173
178 def readRawMetadata(repo, version='tip'):
179 files = subprocess.check_output(['hg', '-R', repo.repository, 'locate', '-r', version]).splitlines()
180 genericFilename = 'metadata'
181 filename = '%s.%s' % (genericFilename, repo.type)
182
183 # Fall back to platform-independent metadata file
184 if filename not in files:
185 filename = genericFilename
186
187 command = ['hg', '-R', repo.repository, 'cat', '-r', version, os.path.join(rep o.repository, filename)]
188 result = subprocess.check_output(command)
189
190 parser = SafeConfigParser()
191 parser.readfp(StringIO(result))
192
193 return parser
194
195 def readMetadata(repo, version): 174 def readMetadata(repo, version):
196 """ 175 """
197 reads extension ID and compatibility information from metadata file in the 176 reads extension ID and compatibility information from metadata file in the
198 extension's repository 177 extension's repository
199 """ 178 """
200 if repo.type == 'android': 179 if repo.type == 'android':
201 command = ['hg', '-R', repo.repository, 'id', '-r', version, '-n'] 180 command = ['hg', '-R', repo.repository, 'id', '-r', version, '-n']
202 result = subprocess.check_output(command) 181 result = subprocess.check_output(command)
203 revision = re.sub(r'\D', '', result) 182 revision = re.sub(r'\D', '', result)
204 183
205 command = ['hg', '-R', repo.repository, 'cat', '-r', version, os.path.join(r epo.repository, 'AndroidManifest.xml')]
206 result = subprocess.check_output(command)
207 manifest = dom.parseString(result)
208 usesSdk = manifest.getElementsByTagName('uses-sdk')[0]
209
210 return { 184 return {
211 'revision': revision, 185 'revision': revision,
212 'minSdkVersion': usesSdk.attributes["android:minSdkVersion"].value, 186 'minSdkVersion': get_min_sdk_version(repo, version),
213 } 187 }
214 elif repo.type == 'safari': 188 elif repo.type == 'safari':
215 metadata = readRawMetadata(repo, version) 189 metadata = repo.readMetadata(version)
216 return { 190 return {
217 'certificateID': getSafariCertificateID(repo.keyFile), 191 'certificateID': getSafariCertificateID(repo.keyFile),
218 'version': version, 192 'version': version,
219 'shortVersion': version, 193 'shortVersion': version,
220 'basename': metadata.get('general', 'basename'), 194 'basename': metadata.get('general', 'basename'),
221 } 195 }
222 elif repo.type == 'gecko': 196 elif repo.type == 'gecko':
223 metadata = readRawMetadata(repo, version) 197 metadata = repo.readMetadata(version)
224 result = { 198 result = {
225 'extensionID': metadata.get('general', 'id'), 199 'extensionID': metadata.get('general', 'id'),
226 'version': version, 200 'version': version,
227 'compat': [] 201 'compat': []
228 } 202 }
229 for key, value in KNOWN_APPS.iteritems(): 203 for key, value in KNOWN_APPS.iteritems():
230 if metadata.has_option('compat', key): 204 if metadata.has_option('compat', key):
231 minVersion, maxVersion = metadata.get('compat', key).split('/') 205 minVersion, maxVersion = metadata.get('compat', key).split('/')
232 result['compat'].append({'id': value, 'minVersion': minVersion, 'maxVers ion': maxVersion}) 206 result['compat'].append({'id': value, 'minVersion': minVersion, 'maxVers ion': maxVersion})
233 return result 207 return result
(...skipping 16 matching lines...) Expand all
250 extensions[repo.type].append(data) 224 extensions[repo.type].append(data)
251 225
252 if len(extensions['android']) > 1: 226 if len(extensions['android']) > 1:
253 print >>sys.stderr, 'Warning: more than one Android app defined, update mani fest only works for one' 227 print >>sys.stderr, 'Warning: more than one Android app defined, update mani fest only works for one'
254 228
255 for repoType in extensions.iterkeys(): 229 for repoType in extensions.iterkeys():
256 manifestPath = get_config().get('extensions', '%sUpdateManifestPath' % repoT ype) 230 manifestPath = get_config().get('extensions', '%sUpdateManifestPath' % repoT ype)
257 template = get_template(get_config().get('extensions', '%sUpdateManifest' % repoType)) 231 template = get_template(get_config().get('extensions', '%sUpdateManifest' % repoType))
258 template.stream({'extensions': extensions[repoType]}).dump(manifestPath) 232 template.stream({'extensions': extensions[repoType]}).dump(manifestPath)
259 233
234 def writePadFile(links):
235 for repo in Configuration.getRepositoryConfigurations():
236 if repo.padTemplate and links.has_section(repo.repositoryName):
237 PadFile.forRepository(repo, links.get(repo.repositoryName, 'version'),
238 links.get(repo.repositoryName, 'downloadURL')) .write()
239
260 def updateLinks(): 240 def updateLinks():
261 """ 241 """
262 writes the current extension download links to a file 242 writes the current extension download links to a file
263 """ 243 """
264 244
265 # Now get download links and save them to file 245 # Now get download links and save them to file
266 result = SafeConfigParser() 246 result = SafeConfigParser()
267 getDownloadLinks(result) 247 getDownloadLinks(result)
268 file = open(get_config().get('extensions', 'downloadLinksFile'), 'wb') 248 file = open(get_config().get('extensions', 'downloadLinksFile'), 'wb')
269 result.write(file) 249 result.write(file)
270 file.close() 250 file.close()
271 251
272 writeUpdateManifest(result) 252 writeUpdateManifest(result)
253 writePadFile(result)
273 254
274 if __name__ == "__main__": 255 if __name__ == "__main__":
275 updateLinks() 256 updateLinks()
OLDNEW

Powered by Google App Engine
This is Rietveld