Merge lp:~cjwatson/launchpad/queue-api-fix-urls into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 15607
Proposed branch: lp:~cjwatson/launchpad/queue-api-fix-urls
Merge into: lp:launchpad
Diff against target: 356 lines (+141/-17)
8 files modified
lib/lp/soyuz/browser/configure.zcml (+4/-0)
lib/lp/soyuz/browser/queue.py (+10/-1)
lib/lp/soyuz/configure.zcml (+1/-0)
lib/lp/soyuz/interfaces/queue.py (+17/-0)
lib/lp/soyuz/interfaces/sourcepackagerelease.py (+15/-0)
lib/lp/soyuz/model/queue.py (+29/-9)
lib/lp/soyuz/model/sourcepackagerelease.py (+13/-0)
lib/lp/soyuz/tests/test_packageupload.py (+52/-7)
To merge this branch: bzr merge lp:~cjwatson/launchpad/queue-api-fix-urls
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+113776@code.launchpad.net

Commit message

Fix source, binary, and custom URLs returned by PackageUpload methods.

Description of the change

== Summary ==

Following the deployment of https://code.launchpad.net/~cjwatson/launchpad/queue-api-readonly/+merge/113202, I noticed that "queue fetch" did not work properly: changes files could be retrieved, but everything else resulted in error pages.

== Proposed fix ==

Source and custom files should be served directly from the librarian rather than being proxied through an Archive context. Archive.getFileByName cannot serve source files that do not have an associated SourcePackagePublishingHistory (true for most interesting files in upload queues; they only get an SPPH once they're accepted) and cannot serve custom files at all.

Binary files should be served from a BinaryPackageBuild context, not Archive. This matches how they're served from +build pages (e.g. https://launchpad.net/ubuntu/+source/libmath-tamuanova-perl/1.0.2-1ubuntu1/+build/3630878).

This time, rather than just having the tests check the URLs which obviously wasn't good enough, I arranged for them to actually try to open the files.

== LOC Rationale ==

+27. So far, including this branch, this arc of work has come in at +805. The final branch of the arc will remove the queue tool, all its works, and all its empty promises, currently amounting to -1955. This is therefore very comfortably ahead, even if I find more things I need to fix after this.

== Tests ==

bin/test -vvct TestPackageUploadWebservice

== Demo and Q/A ==

The queue API client is now in lp:ubuntu-archive-tools: use it to fetch source, binary, and custom files, and make sure they come back as real files and not error pages.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/browser/configure.zcml'
2--- lib/lp/soyuz/browser/configure.zcml 2011-12-24 17:49:30 +0000
3+++ lib/lp/soyuz/browser/configure.zcml 2012-07-09 13:05:29 +0000
4@@ -802,6 +802,10 @@
5 path_expression="string:+upload/${id}"
6 attribute_to_parent="distroseries"
7 />
8+ <browser:navigation
9+ module="lp.soyuz.browser.queue"
10+ classes="PackageUploadNavigation"
11+ />
12 <browser:url
13 for="lp.soyuz.interfaces.archivedependency.IArchiveDependency"
14 path_expression="string:+dependency/${dependency/id}"
15
16=== modified file 'lib/lp/soyuz/browser/queue.py'
17--- lib/lp/soyuz/browser/queue.py 2012-07-06 16:17:57 +0000
18+++ lib/lp/soyuz/browser/queue.py 2012-07-09 13:05:29 +0000
19@@ -6,6 +6,7 @@
20 __metaclass__ = type
21
22 __all__ = [
23+ 'PackageUploadNavigation',
24 'QueueItemsView',
25 ]
26
27@@ -26,7 +27,11 @@
28 load_related,
29 )
30 from lp.services.job.model.job import Job
31-from lp.services.webapp import LaunchpadView
32+from lp.services.librarian.browser import FileNavigationMixin
33+from lp.services.webapp import (
34+ GetitemNavigation,
35+ LaunchpadView,
36+ )
37 from lp.services.webapp.authorization import check_permission
38 from lp.services.webapp.batching import BatchNavigator
39 from lp.soyuz.enums import (
40@@ -458,6 +463,10 @@
41 return (priority for priority in PackagePublishingPriority)
42
43
44+class PackageUploadNavigation(GetitemNavigation, FileNavigationMixin):
45+ usedfor = IPackageUpload
46+
47+
48 class CompletePackageUpload:
49 """A decorated `PackageUpload` including sources, builds and packages.
50
51
52=== modified file 'lib/lp/soyuz/configure.zcml'
53--- lib/lp/soyuz/configure.zcml 2012-07-04 13:02:32 +0000
54+++ lib/lp/soyuz/configure.zcml 2012-07-09 13:05:29 +0000
55@@ -168,6 +168,7 @@
56 custom_file_urls
57 customFileUrls
58 getBinaryProperties
59+ getFileByName
60 date_created
61 sourcepackagerelease
62 component_name
63
64=== modified file 'lib/lp/soyuz/interfaces/queue.py'
65--- lib/lp/soyuz/interfaces/queue.py 2012-07-04 17:08:56 +0000
66+++ lib/lp/soyuz/interfaces/queue.py 2012-07-09 13:05:29 +0000
67@@ -301,6 +301,23 @@
68 single binary.
69 """
70
71+ def getFileByName(filename):
72+ """Return the corresponding `ILibraryFileAlias` in this context.
73+
74+ The following file types (and extension) can be looked up in the
75+ PackageUpload context:
76+
77+ * Changes files: '.changes';
78+ * Source files: '.orig.tar.gz', 'tar.gz', '.diff.gz' and '.dsc'.
79+ * Custom files: '.tar.gz'.
80+
81+ :param filename: the exact filename to be looked up.
82+
83+ :raises NotFoundError if no file could be found.
84+
85+ :return the corresponding `ILibraryFileAlias` if the file was found.
86+ """
87+
88 def setNew():
89 """Set queue state to NEW."""
90
91
92=== modified file 'lib/lp/soyuz/interfaces/sourcepackagerelease.py'
93--- lib/lp/soyuz/interfaces/sourcepackagerelease.py 2012-07-05 09:04:09 +0000
94+++ lib/lp/soyuz/interfaces/sourcepackagerelease.py 2012-07-09 13:05:29 +0000
95@@ -170,6 +170,21 @@
96 in this package.
97 """
98
99+ def getFileByName(filename):
100+ """Return the corresponding `ILibraryFileAlias` in this context.
101+
102+ The following file types (and extension) can be looked up in the
103+ SourcePackageRelease context:
104+
105+ * Source files: '.orig.tar.gz', 'tar.gz', '.diff.gz' and '.dsc'.
106+
107+ :param filename: the exact filename to be looked up.
108+
109+ :raises NotFoundError if no file could be found.
110+
111+ :return the corresponding `ILibraryFileAlias` if the file was found.
112+ """
113+
114 def createBuild(distroarchseries, pocket, archive, processor=None,
115 status=None):
116 """Create a build for a given distroarchseries/pocket/archive
117
118=== modified file 'lib/lp/soyuz/model/queue.py'
119--- lib/lp/soyuz/model/queue.py 2012-07-06 16:17:57 +0000
120+++ lib/lp/soyuz/model/queue.py 2012-07-09 13:05:29 +0000
121@@ -259,8 +259,7 @@
122 """See `IPackageUpload`."""
123 if self.contains_source:
124 return [
125- ProxiedLibraryFileAlias(
126- file.libraryfile, self.archive).http_url
127+ ProxiedLibraryFileAlias(file.libraryfile, self).http_url
128 for file in self.sourcepackagerelease.files]
129 else:
130 return []
131@@ -272,7 +271,7 @@
132 def binaryFileUrls(self):
133 """See `IPackageUpload`."""
134 return [
135- ProxiedLibraryFileAlias(file.libraryfile, self.archive).http_url
136+ ProxiedLibraryFileAlias(file.libraryfile, build.build).http_url
137 for build in self.builds
138 for bpr in build.build.binarypackages
139 for file in bpr.files]
140@@ -280,7 +279,7 @@
141 @property
142 def changes_file_url(self):
143 if self.changesfile is not None:
144- return self.changesfile.getURL()
145+ return ProxiedLibraryFileAlias(self.changesfile, self).http_url
146 else:
147 return None
148
149@@ -309,14 +308,12 @@
150 def custom_file_urls(self):
151 """See `IPackageUpload`."""
152 return tuple(
153- file.libraryfilealias.getURL() for file in self.customfiles)
154+ ProxiedLibraryFileAlias(file.libraryfilealias, self).http_url
155+ for file in self.customfiles)
156
157 def customFileUrls(self):
158 """See `IPackageUpload`."""
159- return [
160- ProxiedLibraryFileAlias(
161- file.libraryfilealias, self.archive).http_url
162- for file in self.customfiles]
163+ return self.custom_file_urls
164
165 def getBinaryProperties(self):
166 """See `IPackageUpload`."""
167@@ -329,6 +326,29 @@
168 })
169 return properties
170
171+ def getFileByName(self, filename):
172+ """See `IPackageUpload`."""
173+ if (self.changesfile is not None and
174+ self.changesfile.filename == filename):
175+ return self.changesfile
176+
177+ if self.sourcepackagerelease is not None:
178+ try:
179+ return self.sourcepackagerelease.getFileByName(filename)
180+ except NotFoundError:
181+ pass
182+
183+ custom = Store.of(self).find(
184+ PackageUploadCustom,
185+ PackageUploadCustom.packageupload == self.id,
186+ LibraryFileAlias.id ==
187+ PackageUploadCustom.libraryfilealiasID,
188+ LibraryFileAlias.filename == filename).one()
189+ if custom is not None:
190+ return custom.libraryfilealias
191+
192+ raise NotFoundError(filename)
193+
194 def setNew(self):
195 """See `IPackageUpload`."""
196 if self.status == PackageUploadStatus.NEW:
197
198=== modified file 'lib/lp/soyuz/model/sourcepackagerelease.py'
199--- lib/lp/soyuz/model/sourcepackagerelease.py 2012-07-05 09:06:48 +0000
200+++ lib/lp/soyuz/model/sourcepackagerelease.py 2012-07-09 13:05:29 +0000
201@@ -38,6 +38,7 @@
202 from zope.component import getUtility
203 from zope.interface import implements
204
205+from lp.app.errors import NotFoundError
206 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
207 from lp.archiveuploader.utils import determine_source_file_type
208 from lp.buildmaster.enums import BuildStatus
209@@ -309,6 +310,18 @@
210 filetype=determine_source_file_type(file.filename),
211 libraryfile=file)
212
213+ def getFileByName(self, filename):
214+ """See `ISourcePackageRelease`."""
215+ sprf = Store.of(self).find(
216+ SourcePackageReleaseFile,
217+ SourcePackageReleaseFile.sourcepackagerelease == self.id,
218+ LibraryFileAlias.id == SourcePackageReleaseFile.libraryfileID,
219+ LibraryFileAlias.filename == filename).one()
220+ if sprf:
221+ return sprf.libraryfile
222+ else:
223+ raise NotFoundError(filename)
224+
225 def getPackageSize(self):
226 """See ISourcePackageRelease."""
227 size_query = """
228
229=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
230--- lib/lp/soyuz/tests/test_packageupload.py 2012-07-06 16:17:57 +0000
231+++ lib/lp/soyuz/tests/test_packageupload.py 2012-07-09 13:05:29 +0000
232@@ -7,6 +7,10 @@
233 from email import message_from_string
234 import os
235 import shutil
236+from urllib2 import (
237+ HTTPError,
238+ urlopen,
239+ )
240
241 from lazr.restfulclient.errors import (
242 BadRequest,
243@@ -17,6 +21,8 @@
244 from zope.component import getUtility
245 from zope.security.proxy import removeSecurityProxy
246 from zope.schema import getFields
247+from zope.testbrowser.browser import Browser
248+from zope.testbrowser.testing import PublisherMechanizeBrowser
249
250 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
251 from lp.archiveuploader.tests import datadir
252@@ -889,6 +895,14 @@
253 self.assertEqual(PackageUploadStatus.REJECTED, pu.status)
254
255
256+class NonRedirectingMechanizeBrowser(PublisherMechanizeBrowser):
257+ """A `mechanize.Browser` that does not handle redirects."""
258+
259+ default_features = [
260+ feature for feature in PublisherMechanizeBrowser.default_features
261+ if feature != "_redirect"]
262+
263+
264 class TestPackageUploadWebservice(TestCaseWithFactory):
265 """Test the exposure of queue methods to the web service."""
266
267@@ -958,6 +972,22 @@
268 transaction.commit()
269 return upload, self.load(upload, person)
270
271+ def makeNonRedirectingBrowser(self, person):
272+ # The test browser can only work with the appserver, not the
273+ # librarian, so follow one layer of redirection through the
274+ # appserver and then ask the librarian for the real file.
275+ browser = Browser(mech_browser=NonRedirectingMechanizeBrowser())
276+ browser.handleErrors = False
277+ with person_logged_in(person):
278+ browser.addHeader(
279+ "Authorization", "Basic %s:test" % person.preferredemail.email)
280+ return browser
281+
282+ def assertCanOpenRedirectedUrl(self, browser, url):
283+ redirection = self.assertRaises(HTTPError, browser.open, url)
284+ self.assertEqual(303, redirection.code)
285+ urlopen(redirection.hdrs["Location"]).close()
286+
287 def assertRequiresEdit(self, method_name, **kwargs):
288 """Test that a web service queue method requires launchpad.Edit."""
289 with admin_logged_in():
290@@ -1027,15 +1057,20 @@
291 person = self.makeQueueAdmin([self.universe])
292 upload, ws_upload = self.makeSourcePackageUpload(
293 person, component=self.universe)
294+
295 ws_source_file_urls = ws_upload.sourceFileUrls()
296 self.assertNotEqual(0, len(ws_source_file_urls))
297 with person_logged_in(person):
298 source_file_urls = [
299- ProxiedLibraryFileAlias(
300- file.libraryfile, upload.archive).http_url
301+ ProxiedLibraryFileAlias(file.libraryfile, upload).http_url
302 for file in upload.sourcepackagerelease.files]
303 self.assertContentEqual(source_file_urls, ws_source_file_urls)
304
305+ browser = self.makeNonRedirectingBrowser(person)
306+ for ws_source_file_url in ws_source_file_urls:
307+ self.assertCanOpenRedirectedUrl(browser, ws_source_file_url)
308+ self.assertCanOpenRedirectedUrl(browser, ws_upload.changes_file_url)
309+
310 def test_overrideSource_limited_component_permissions(self):
311 # Overriding between two components requires queue admin of both.
312 person = self.makeQueueAdmin([self.universe])
313@@ -1113,12 +1148,17 @@
314 self.assertNotEqual(0, len(ws_binary_file_urls))
315 with person_logged_in(person):
316 binary_file_urls = [
317- ProxiedLibraryFileAlias(
318- file.libraryfile, upload.archive).http_url
319- for bpr in upload.builds[0].build.binarypackages
320+ ProxiedLibraryFileAlias(file.libraryfile, build.build).http_url
321+ for build in upload.builds
322+ for bpr in build.build.binarypackages
323 for file in bpr.files]
324 self.assertContentEqual(binary_file_urls, ws_binary_file_urls)
325
326+ browser = self.makeNonRedirectingBrowser(person)
327+ for ws_binary_file_url in ws_binary_file_urls:
328+ self.assertCanOpenRedirectedUrl(browser, ws_binary_file_url)
329+ self.assertCanOpenRedirectedUrl(browser, ws_upload.changes_file_url)
330+
331 def test_overrideBinaries_limited_component_permissions(self):
332 # Overriding between two components requires queue admin of both.
333 person = self.makeQueueAdmin([self.universe])
334@@ -1253,15 +1293,20 @@
335 upload, ws_upload = self.makeCustomPackageUpload(
336 person, custom_type=PackageUploadCustomFormat.DEBIAN_INSTALLER,
337 filename="debian-installer-images_1.tar.gz")
338+
339 ws_custom_file_urls = ws_upload.customFileUrls()
340 self.assertNotEqual(0, len(ws_custom_file_urls))
341 with person_logged_in(person):
342 custom_file_urls = [
343- ProxiedLibraryFileAlias(
344- file.libraryfilealias, upload.archive).http_url
345+ ProxiedLibraryFileAlias(file.libraryfilealias, upload).http_url
346 for file in upload.customfiles]
347 self.assertContentEqual(custom_file_urls, ws_custom_file_urls)
348
349+ browser = self.makeNonRedirectingBrowser(person)
350+ for ws_custom_file_url in ws_custom_file_urls:
351+ self.assertCanOpenRedirectedUrl(browser, ws_custom_file_url)
352+ self.assertCanOpenRedirectedUrl(browser, ws_upload.changes_file_url)
353+
354 def test_binary_and_custom_info(self):
355 # API clients can inspect properties of uploads containing both
356 # ordinary binaries and custom upload files.