← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/archive-unambiguous-files-traversals into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/archive-unambiguous-files-traversals into lp:launchpad.

Commit message:
Disambiguate URLs to source package files in the face of filename clashes in imported archives.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/archive-unambiguous-files-traversals/+merge/345118

This gives us a way to cope with fetching the source files for e.g. d3-format 1.0.2-1 vs. 1:1.0.2-1 in the imported Debian archive, which would be helpful to merge-o-matic.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/archive-unambiguous-files-traversals into lp:launchpad.
=== modified file 'lib/lp/services/librarian/browser.py'
--- lib/lp/services/librarian/browser.py	2015-07-09 20:06:17 +0000
+++ lib/lp/services/librarian/browser.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser file for LibraryFileAlias."""
@@ -125,6 +125,13 @@
         self.parent = parent
 
     @property
+    def request(self):
+        request = get_current_browser_request()
+        if WebServiceLayer.providedBy(request):
+            request = IWebBrowserOriginatingRequest(request)
+        return request
+
+    @property
     def http_url(self):
         """Return the webapp URL for the context `LibraryFileAlias`.
 
@@ -137,11 +144,7 @@
         if self.context.deleted:
             return None
 
-        request = get_current_browser_request()
-        if WebServiceLayer.providedBy(request):
-            request = IWebBrowserOriginatingRequest(request)
-
-        parent_url = canonical_url(self.parent, request=request)
+        parent_url = canonical_url(self.parent, request=self.request)
         traversal_url = urlappend(parent_url, '+files')
         url = urlappend(
             traversal_url,

=== added file 'lib/lp/soyuz/adapters/proxiedsourcefiles.py'
--- lib/lp/soyuz/adapters/proxiedsourcefiles.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/adapters/proxiedsourcefiles.py	2018-05-04 22:20:39 +0000
@@ -0,0 +1,37 @@
+# Copyright 2018 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Proxied source files."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+    'ProxiedSourceLibraryFileAlias',
+    ]
+
+from lp.services.librarian.browser import ProxiedLibraryFileAlias
+from lp.services.librarian.client import url_path_quote
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webapp.url import urlappend
+
+
+class ProxiedSourceLibraryFileAlias(ProxiedLibraryFileAlias):
+    """A `ProxiedLibraryFileAlias` variant that traverses via +sourcefiles.
+
+    This can be used to construct unambiguous source file URLs even for
+    imports from upstream archives without robust historical filename
+    uniqueness checks.
+    """
+
+    @property
+    def http_url(self):
+        if self.context.deleted:
+            return None
+
+        url = canonical_url(self.parent.archive, request=self.request)
+        url = urlappend(url, '+sourcefiles')
+        url = urlappend(url, self.parent.source_package_name)
+        url = urlappend(url, self.parent.source_package_version)
+        return urlappend(
+            url, url_path_quote(self.context.filename.encode('utf-8')))

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py	2017-04-22 13:13:22 +0000
+++ lib/lp/soyuz/browser/archive.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for archive."""
@@ -455,6 +455,32 @@
 
         return self.context.getArchiveDependency(archive)
 
+    @stepthrough('+sourcefiles')
+    def traverse_sourcefiles(self, sourcepackagename):
+        """Traverse to a source file in the archive.
+
+        Normally, files in an archive are unique by filename, so the +files
+        traversal is sufficient.  Unfortunately, a gina-imported archive may
+        contain the same filename with different contents due to a
+        combination of epochs and less stringent checks applied by the
+        upstream archive software.  (In practice this only happens for
+        source packages because that's normally all we import using gina.)
+        This provides an unambiguous way to traverse to such files even with
+        this problem.
+
+        The path scheme is::
+
+            +sourcefiles/:sourcename/:sourceversion/:filename
+        """
+        if len(self.request.stepstogo) < 2:
+            return None
+
+        version = self.request.stepstogo.consume()
+        filename = self.request.stepstogo.consume()
+
+        return self.context.getSourceFileByName(
+            sourcepackagename, version, filename)
+
 
 class ArchiveMenuMixin:
 

=== modified file 'lib/lp/soyuz/browser/distributionsourcepackagerelease.py'
--- lib/lp/soyuz/browser/distributionsourcepackagerelease.py	2014-12-18 13:05:10 +0000
+++ lib/lp/soyuz/browser/distributionsourcepackagerelease.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -18,7 +18,6 @@
 from lp.registry.browser.distributionsourcepackage import (
     PublishingHistoryViewMixin,
     )
-from lp.services.librarian.browser import ProxiedLibraryFileAlias
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import (
     canonical_url,
@@ -27,6 +26,7 @@
     stepthrough,
     )
 from lp.services.webapp.breadcrumb import Breadcrumb
+from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias
 from lp.soyuz.browser.build import get_build_by_id_str
 from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
@@ -101,8 +101,8 @@
         """The source package release files as `ProxiedLibraryFileAlias`."""
         last_publication = self._cached_publishing_history[0]
         return [
-            ProxiedLibraryFileAlias(
-                source_file.libraryfile, last_publication.archive)
+            ProxiedSourceLibraryFileAlias(
+                source_file.libraryfile, last_publication)
             for source_file in self.context.files]
 
     @cachedproperty

=== modified file 'lib/lp/soyuz/browser/publishing.py'
--- lib/lp/soyuz/browser/publishing.py	2015-07-09 20:06:17 +0000
+++ lib/lp/soyuz/browser/publishing.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for Soyuz publishing records."""
@@ -30,6 +30,7 @@
     canonical_url,
     LaunchpadView,
     )
+from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias
 from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
 from lp.soyuz.interfaces.packagediff import IPackageDiff
@@ -270,8 +271,7 @@
     def published_source_and_binary_files(self):
         """Return list of dictionaries representing published files."""
         files = sorted(
-            (ProxiedLibraryFileAlias(lfa, self.context.archive)
-             for lfa in self.context.getSourceAndBinaryLibraryFiles()),
+            self.context.getSourceAndBinaryLibraryFiles(),
             key=attrgetter('filename'))
         result = []
         urls = set()
@@ -285,14 +285,17 @@
             urls.add(url)
 
             custom_dict = {}
-            custom_dict["url"] = url
             custom_dict["filename"] = library_file.filename
             custom_dict["filesize"] = library_file.content.filesize
             if (library_file.filename.endswith('.deb') or
                 library_file.filename.endswith('.udeb')):
                 custom_dict['class'] = 'binary'
+                custom_dict["url"] = ProxiedLibraryFileAlias(
+                    library_file, self.context.archive).http_url
             else:
                 custom_dict['class'] = 'source'
+                custom_dict["url"] = ProxiedSourceLibraryFileAlias(
+                    library_file, self.context).http_url
 
             result.append(custom_dict)
 

=== modified file 'lib/lp/soyuz/browser/tests/distributionsourcepackagerelease-views.txt'
--- lib/lp/soyuz/browser/tests/distributionsourcepackagerelease-views.txt	2014-11-27 22:13:36 +0000
+++ lib/lp/soyuz/browser/tests/distributionsourcepackagerelease-views.txt	2018-05-04 22:20:39 +0000
@@ -24,14 +24,14 @@
     u'testing-dspr 1.0 source package in ubuntutest'
 
 The 'files' property returns a list of files included in the source
-upload encapsulated as `ProxiedLibraryFileAlias` objects. Their
+upload encapsulated as `ProxiedSourceLibraryFileAlias` objects. Their
 'http_url' points to the LP proxied url which normalizes the path
 tofiles allowing them to be downloaded using `dget`.
 
     >>> for source_file in dspr_view.files:
     ...     print source_file.filename, source_file.http_url
     testing-dspr_1.0.dsc
-    http://.../ubuntutest/+archive/primary/+files/testing-dspr_1.0.dsc
+    http://.../ubuntutest/+archive/primary/+sourcefiles/testing-dspr/1.0/testing-dspr_1.0.dsc
 
 The 'sponsor' property indicates whether the upload was 'sponsored' or
 not. When the upload was signed by someone else than the source

=== modified file 'lib/lp/soyuz/browser/tests/publishing-views.txt'
--- lib/lp/soyuz/browser/tests/publishing-views.txt	2014-07-24 09:37:03 +0000
+++ lib/lp/soyuz/browser/tests/publishing-views.txt	2018-05-04 22:20:39 +0000
@@ -63,7 +63,7 @@
     >>> view = create_initialized_view(alsa_pub, "+listing-archive-detailed")
 
     >>> view.published_source_and_binary_files
-    [{'url': u'http://launchpad.dev/ubuntutest/+archive/primary/+files/alsa-utils-test_666.dsc',
+    [{'url': u'http://launchpad.dev/ubuntutest/+archive/primary/+sourcefiles/alsa-utils-test/666/alsa-utils-test_666.dsc',
      'class': 'source',
      'filesize': 28,
      'filename': u'alsa-utils-test_666.dsc'}]
@@ -81,11 +81,11 @@
     ...     iceweasel_source_pub, "+listing-archive-detailed")
 
     >>> ppa_source_view.published_source_and_binary_files
-    [{'url': u'http://launchpad.dev/~cprov/+archive/ubuntu/ppa/+files/firefox_0.9.2.orig.tar.gz',
+    [{'url': u'http://launchpad.dev/~cprov/+archive/ubuntu/ppa/+sourcefiles/iceweasel/1.0/firefox_0.9.2.orig.tar.gz',
       'class': 'source',
       'filesize': 9922560,
       'filename': u'firefox_0.9.2.orig.tar.gz'},
-     {'url': u'http://launchpad.dev/~cprov/+archive/ubuntu/ppa/+files/iceweasel-1.0.dsc',
+     {'url': u'http://launchpad.dev/~cprov/+archive/ubuntu/ppa/+sourcefiles/iceweasel/1.0/iceweasel-1.0.dsc',
       'class': 'source',
       'filesize': 123,
       'filename': u'iceweasel-1.0.dsc'},

=== modified file 'lib/lp/soyuz/browser/tests/test_publishing_webservice.py'
--- lib/lp/soyuz/browser/tests/test_publishing_webservice.py	2018-02-01 18:44:21 +0000
+++ lib/lp/soyuz/browser/tests/test_publishing_webservice.py	2018-05-04 22:20:39 +0000
@@ -9,6 +9,7 @@
 
 from lp.services.librarian.browser import ProxiedLibraryFileAlias
 from lp.services.webapp.interfaces import OAuthPermission
+from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias
 from lp.testing import (
     api_url,
     login_person,
@@ -47,8 +48,7 @@
         with person_logged_in(person):
             sprf = spph.sourcepackagerelease.files[0]
             expected_urls = [
-                ProxiedLibraryFileAlias(
-                    sprf.libraryfile, spph.archive).http_url]
+                ProxiedSourceLibraryFileAlias(sprf.libraryfile, spph).http_url]
         self.assertEqual(expected_urls, urls)
 
     def test_sourceFileUrls_include_meta(self):
@@ -75,8 +75,8 @@
         info = response.jsonBody()
         with person_logged_in(person):
             expected_info = [{
-                "url": ProxiedLibraryFileAlias(
-                    sprf.libraryfile, spph.archive).http_url,
+                "url": ProxiedSourceLibraryFileAlias(
+                    sprf.libraryfile, spph).http_url,
                 "size": sprf.libraryfile.content.filesize,
                 "sha256": sprf.libraryfile.content.sha256,
                 } for sprf in spph.sourcepackagerelease.files]

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2017-09-28 14:06:20 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Archive interfaces."""
@@ -936,6 +936,21 @@
         :return the corresponding `ILibraryFileAlias` is the file was found.
         """
 
+    def getSourceFileByName(name, version, filename):
+        """Return the `ILibraryFileAlias` for a source name/version/filename.
+
+        This can be used to avoid ambiguities with `getFileByName` in
+        imported archives, where the upstream archive software may not
+        always have had robust historical filename uniqueness checks.
+
+        :param name: The name of the source package.
+        :param version: The version of the source package.
+        :param filename: The exact filename to look up.
+
+        :raises NotFoundError: if no matching file could be found.
+        :return: the corresponding `ILibraryFileAlias`.
+        """
+
     def getBinaryPackageRelease(name, version, archtag):
         """Find the specified `IBinaryPackageRelease` in the archive.
 

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2018-01-30 16:19:15 +0000
+++ lib/lp/soyuz/model/archive.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Database class for table Archive."""
@@ -1688,6 +1688,29 @@
 
         return archive_file
 
+    def getSourceFileByName(self, name, version, filename):
+        """See `IArchive`."""
+        result = IStore(LibraryFileAlias).find(
+            LibraryFileAlias,
+            SourcePackagePublishingHistory.archive == self,
+            SourcePackagePublishingHistory.sourcepackagereleaseID ==
+                SourcePackageRelease.id,
+            SourcePackageRelease.sourcepackagename == SourcePackageName.id,
+            SourcePackageName.name == name,
+            SourcePackageRelease.version == version,
+            SourcePackageRelease.id ==
+                SourcePackageReleaseFile.sourcepackagereleaseID,
+            SourcePackageReleaseFile.libraryfileID == LibraryFileAlias.id,
+            LibraryFileAlias.filename == filename,
+            LibraryFileAlias.content != None)
+        result = result.config(distinct=True).order_by(LibraryFileAlias.id)
+        # Unlike `getFileByName`, we are guaranteed at most one match even
+        # for files in imported archives.
+        archive_file = result.one()
+        if archive_file is None:
+            raise NotFoundError(filename)
+        return archive_file
+
     def getBinaryPackageRelease(self, name, version, archtag):
         """See `IArchive`."""
         from lp.soyuz.model.distroarchseries import DistroArchSeries

=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py	2017-06-02 21:46:50 +0000
+++ lib/lp/soyuz/model/publishing.py	2018-05-04 22:20:39 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -76,6 +76,7 @@
     ScriptRequest,
     )
 from lp.services.worlddata.model.country import Country
+from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias
 from lp.soyuz.enums import (
     BinaryPackageFormat,
     PackagePublishingPriority,
@@ -142,6 +143,12 @@
     return [ProxiedLibraryFileAlias(file, parent).http_url for file in files]
 
 
+def proxied_source_urls(files, parent):
+    """Return the files passed through `ProxiedSourceLibraryFileAlias`."""
+    return [
+        ProxiedSourceLibraryFileAlias(file, parent).http_url for file in files]
+
+
 class ArchivePublisherBase:
     """Base class for `IArchivePublisher`."""
 
@@ -548,8 +555,8 @@
             SourcePackageReleaseFile.sourcepackagerelease ==
                 SourcePackageRelease.id,
             SourcePackageRelease.id == self.sourcepackagereleaseID)
-        source_urls = proxied_urls(
-            [source for source, _ in sources], self.archive)
+        source_urls = proxied_source_urls(
+            [source for source, _ in sources], self)
         if include_meta:
             meta = [
                 (content.filesize, content.sha256) for _, content in sources]

=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-files.txt'
--- lib/lp/soyuz/stories/ppa/xx-ppa-files.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/soyuz/stories/ppa/xx-ppa-files.txt	2018-05-04 22:20:39 +0000
@@ -90,18 +90,19 @@
 
     >>> ppa_links = [
     ...     ('(changes file)',
-    ...         another_test_source.sourcepackagerelease.upload_changesfile),
+    ...         another_test_source.sourcepackagerelease.upload_changesfile,
+    ...         None, None),
     ...     ]
 
     >>> ppa_1_0_links = [
-    ...     ('test-pkg_1.0.dsc', dsc_file),
-    ...     ('test-pkg_1.0.tar.gz', tar_gz),
-    ...     ('test-bin_1.0_all.deb', deb_file),
+    ...     ('test-pkg_1.0.dsc', dsc_file, 'test-pkg', '1.0'),
+    ...     ('test-pkg_1.0.tar.gz', tar_gz, 'test-pkg', '1.0'),
+    ...     ('test-bin_1.0_all.deb', deb_file, None, None),
     ...     ]
 
     >>> ppa_1_1_links = [
-    ...     ('test-pkg_1.1.dsc', another_dsc_file),
-    ...     ('1.0 to 1.1', package_diff.diff_content),
+    ...     ('test-pkg_1.1.dsc', another_dsc_file, 'test-pkg', '1.1'),
+    ...     ('1.0 to 1.1', package_diff.diff_content, None, None),
     ...     ]
 
 Links to files accessible via +files/ proxy in the Build page.
@@ -109,13 +110,14 @@
     >>> build_id = build.id
 
     >>> builds_links = [
-    ...     ('see the log', build.log),
+    ...     ('see the log', build.log, None, None),
     ...     ]
 
     >>> build_links = [
-    ...     ('test-bin_1.0_i386.changes', build.upload_changesfile),
-    ...     ('buildlog', build.log),
-    ...     ('uploadlog', build.upload_log),
+    ...     ('test-bin_1.0_i386.changes', build.upload_changesfile,
+    ...      None, None),
+    ...     ('buildlog', build.log, None, None),
+    ...     ('uploadlog', build.upload_log, None, None),
     ...     ]
 
     >>> logout()
@@ -124,15 +126,20 @@
 
     >>> from mechanize import LinkNotFoundError
     >>> def check_urls(browser, links, base_url):
-    ...     for link, libraryfile in links:
+    ...     for link, libraryfile, source_name, source_version in links:
     ...         try:
     ...             found_url = browser.getLink(link).url
     ...         except LinkNotFoundError:
     ...             print '%s: NOT FOUND' % libraryfile.filename
     ...             continue
     ...         found_url = found_url.replace('%7E', '~')
-    ...         expected_url = '/'.join(
-    ...             (base_url, '+files', libraryfile.filename))
+    ...         if source_name is not None:
+    ...             expected_url = '/'.join(
+    ...                 (base_url, '+sourcefiles', source_name,
+    ...                  source_version, libraryfile.filename))
+    ...         else:
+    ...             expected_url = '/'.join(
+    ...                 (base_url, '+files', libraryfile.filename))
     ...         if found_url == expected_url:
     ...             print '%s: OK' % libraryfile.filename
     ...         else:
@@ -184,7 +191,7 @@
     ...     'http://launchpad.dev/~no-priv/+archive/ubuntu/p3a/+packages')
 
 Source and binary files, in the expandable-row area, are served via
-the PPA '+files' traversal.
+the PPA '+sourcefiles' and '+files' traversals.
 
     >>> expander_id = find_tags_by_class(
     ...     no_priv_browser.contents, 'expander')[1]['id']

=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt'
--- lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt	2015-04-09 05:16:37 +0000
+++ lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt	2018-05-04 22:20:39 +0000
@@ -383,7 +383,7 @@
     >>> expander_url = foo_browser.getLink(id=expander_id).url
     >>> anon_browser.open(expander_url)
     >>> print anon_browser.getLink("orig").url
-    http://.../+files/foo.orig.tar.gz
+    http://.../+sourcefiles/.../foo.orig.tar.gz
 
 The uploader name is linkified to that user's home page:
 

=== modified file 'lib/lp/soyuz/stories/soyuz/xx-distributionsourcepackagerelease-pages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-distributionsourcepackagerelease-pages.txt	2016-03-30 10:01:57 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-distributionsourcepackagerelease-pages.txt	2018-05-04 22:20:39 +0000
@@ -201,7 +201,7 @@
     View changes file
 
     >>> print anon_browser.getLink('testing-dspr_1.0.dsc').url
-    http://.../ubuntutest/+archive/primary/+files/testing-dspr_1.0.dsc
+    http://.../ubuntutest/+archive/primary/+sourcefiles/testing-dspr/1.0/testing-dspr_1.0.dsc
 
 The 'Downloads' section also lists and link to package diffs when they
 are available.

=== modified file 'lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt'
--- lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt	2016-03-30 10:01:57 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt	2018-05-04 22:20:39 +0000
@@ -90,7 +90,7 @@
   firefox_0.9.2.orig.tar.gz 9.5 MiB ...
 
   >>> print browser.getLink("firefox_0.9.2.orig.tar.gz").url
-  http://launchpad.dev/ubuntu/+archive/primary/+files/firefox_0.9.2.orig.tar.gz
+  http://launchpad.dev/ubuntu/+archive/primary/+sourcefiles/mozilla-firefox/0.9/firefox_0.9.2.orig.tar.gz
 
 This page also provides links to the binary packages generated by this
 source in a specfic architecture:
@@ -285,7 +285,7 @@
   firefox_0.9.2.orig.tar.gz 9.5 MiB ...
 
   >>> print browser.getLink("firefox_0.9.2.orig.tar.gz").url
-  http://launchpad.dev/ubuntu/+archive/primary/+files/firefox_0.9.2.orig.tar.gz
+  http://launchpad.dev/ubuntu/+archive/primary/+sourcefiles/mozilla-firefox/0.9/firefox_0.9.2.orig.tar.gz
 
 If we go to the same page for alsa-utils, the changelog has text that is
 linkified.
@@ -353,11 +353,11 @@
     commercialpackage_1.0-1.dsc 567 bytes ...
 
     >>> print browser.getLink("commercialpackage_1.0.orig.tar.gz").url
-    http://launchpad.dev/ubuntu/+archive/partner/+files/commercialpackage_1.0.orig.tar.gz
+    http://launchpad.dev/ubuntu/+archive/partner/+sourcefiles/commercialpackage/1.0-1/commercialpackage_1.0.orig.tar.gz
     >>> print browser.getLink("commercialpackage_1.0-1.diff.gz").url
-    http://launchpad.dev/ubuntu/+archive/partner/+files/commercialpackage_1.0-1.diff.gz
+    http://launchpad.dev/ubuntu/+archive/partner/+sourcefiles/commercialpackage/1.0-1/commercialpackage_1.0-1.diff.gz
     >>> print browser.getLink("commercialpackage_1.0-1.dsc").url
-    http://launchpad.dev/ubuntu/+archive/partner/+files/commercialpackage_1.0-1.dsc
+    http://launchpad.dev/ubuntu/+archive/partner/+sourcefiles/commercialpackage/1.0-1/commercialpackage_1.0-1.dsc
 
 This page also provides links to the binary packages generated by this
 source in a specfic architecture:
@@ -433,11 +433,11 @@
     commercialpackage_1.0-1.dsc 567 bytes ...
 
     >>> print browser.getLink("commercialpackage_1.0.orig.tar.gz").url
-    http://launchpad.dev/ubuntu/+archive/partner/+files/commercialpackage_1.0.orig.tar.gz
+    http://launchpad.dev/ubuntu/+archive/partner/+sourcefiles/commercialpackage/1.0-1/commercialpackage_1.0.orig.tar.gz
     >>> print browser.getLink("commercialpackage_1.0-1.diff.gz").url
-    http://launchpad.dev/ubuntu/+archive/partner/+files/commercialpackage_1.0-1.diff.gz
+    http://launchpad.dev/ubuntu/+archive/partner/+sourcefiles/commercialpackage/1.0-1/commercialpackage_1.0-1.diff.gz
     >>> print browser.getLink("commercialpackage_1.0-1.dsc").url
-    http://launchpad.dev/ubuntu/+archive/partner/+files/commercialpackage_1.0-1.dsc
+    http://launchpad.dev/ubuntu/+archive/partner/+sourcefiles/commercialpackage/1.0-1/commercialpackage_1.0-1.dsc
 
 
 Tracing copied sources

=== modified file 'lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt'
--- lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt	2016-03-02 15:52:33 +0000
+++ lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt	2018-05-04 22:20:39 +0000
@@ -375,11 +375,11 @@
     ...     source_urls = webservice.named_get(
     ...         pub_link, 'sourceFileUrls').jsonBody()
     ...     print source_urls
-    [u'http://.../~cprov/+archive/ubuntu/ppa/+files/foobar-1.0.dsc']
-    [u'http://.../~cprov/+archive/ubuntu/ppa/+files/firefox_0.9.2.orig.tar.gz',
-     u'http://.../~cprov/+archive/ubuntu/ppa/+files/iceweasel-1.0.dsc']
+    [u'http://.../~cprov/+archive/ubuntu/ppa/+sourcefiles/cdrkit/1.0/foobar-1.0.dsc']
+    [u'http://.../~cprov/+archive/ubuntu/ppa/+sourcefiles/iceweasel/1.0/firefox_0.9.2.orig.tar.gz',
+     u'http://.../~cprov/+archive/ubuntu/ppa/+sourcefiles/iceweasel/1.0/iceweasel-1.0.dsc']
     []
-    [u'http://.../~cprov/+archive/ubuntu/ppa/+files/testwebservice_666.dsc']
+    [u'http://.../~cprov/+archive/ubuntu/ppa/+sourcefiles/testwebservice/666/testwebservice_666.dsc']
 
 binaryFileUrls() is similar:
 

=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py	2018-02-14 11:13:47 +0000
+++ lib/lp/soyuz/tests/test_archive.py	2018-05-04 22:20:39 +0000
@@ -2425,6 +2425,89 @@
             self.archive.getFileByName(pu.changesfile.filename))
 
 
+class TestGetSourceFileByName(TestCaseWithFactory):
+    """Tests for Archive.getSourceFileByName."""
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestGetSourceFileByName, self).setUp()
+        self.archive = self.factory.makeArchive()
+
+    def test_source_file_is_found(self):
+        # A file from a published source package can be retrieved.
+        pub = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive)
+        dsc = self.factory.makeLibraryFileAlias(filename='foo_1.0.dsc')
+        self.assertRaises(
+            NotFoundError, self.archive.getSourceFileByName,
+            pub.source_package_name, pub.source_package_version, dsc.filename)
+        pub.sourcepackagerelease.addFile(dsc)
+        self.assertEqual(
+            dsc, self.archive.getSourceFileByName(
+                pub.source_package_name, pub.source_package_version,
+                dsc.filename))
+
+    def test_nonexistent_source_file_is_not_found(self):
+        # Something that looks like a source file but isn't is not
+        # found.
+        pub = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive)
+        self.assertRaises(
+            NotFoundError, self.archive.getSourceFileByName,
+            pub.source_package_name, pub.source_package_version,
+            'foo_1.0.dsc')
+
+    def test_nonexistent_source_package_version_is_not_found(self):
+        # The source package version must match exactly.
+        pub = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive)
+        pub2 = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive, sourcepackagename=pub.source_package_name)
+        dsc = self.factory.makeLibraryFileAlias(filename='foo_1.0.dsc')
+        pub2.sourcepackagerelease.addFile(dsc)
+        self.assertRaises(
+            NotFoundError, self.archive.getSourceFileByName,
+            pub.source_package_name, pub.source_package_version,
+            'foo_1.0.dsc')
+
+    def test_nonexistent_source_package_name_is_not_found(self):
+        # The source package name must match exactly.
+        pub = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive)
+        pub2 = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive)
+        dsc = self.factory.makeLibraryFileAlias(filename='foo_1.0.dsc')
+        pub2.sourcepackagerelease.addFile(dsc)
+        self.assertRaises(
+            NotFoundError, self.archive.getSourceFileByName,
+            pub.source_package_name, pub.source_package_version,
+            'foo_1.0.dsc')
+
+    def test_epoch_stripping_collision(self):
+        # Even if the archive contains two source packages with identical
+        # names and versions apart from epochs which have the same filenames
+        # with different contents (the worst case), getSourceFileByName
+        # returns the correct files.
+        pub = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive, version='1.0-1')
+        dsc = self.factory.makeLibraryFileAlias(filename='foo_1.0.dsc')
+        pub.sourcepackagerelease.addFile(dsc)
+        pub2 = self.factory.makeSourcePackagePublishingHistory(
+            archive=self.archive, sourcepackagename=pub.source_package_name,
+            version='1:1.0-1')
+        dsc2 = self.factory.makeLibraryFileAlias(filename='foo_1.0.dsc')
+        pub2.sourcepackagerelease.addFile(dsc2)
+        self.assertEqual(
+            dsc, self.archive.getSourceFileByName(
+                pub.source_package_name, pub.source_package_version,
+                dsc.filename))
+        self.assertEqual(
+            dsc2, self.archive.getSourceFileByName(
+                pub2.source_package_name, pub2.source_package_version,
+                dsc2.filename))
+
+
 class TestGetPublishedSources(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer

=== modified file 'lib/lp/soyuz/tests/test_publishing_models.py'
--- lib/lp/soyuz/tests/test_publishing_models.py	2018-02-02 03:14:35 +0000
+++ lib/lp/soyuz/tests/test_publishing_models.py	2018-05-04 22:20:39 +0000
@@ -14,6 +14,7 @@
 from lp.services.database.constants import UTC_NOW
 from lp.services.librarian.browser import ProxiedLibraryFileAlias
 from lp.services.webapp.publisher import canonical_url
+from lp.soyuz.adapters.proxiedsourcefiles import ProxiedSourceLibraryFileAlias
 from lp.soyuz.enums import (
     BinaryPackageFileType,
     BinaryPackageFormat,
@@ -159,8 +160,7 @@
 
     def getURLsForSPPH(self, spph, include_meta=False):
         spr = spph.sourcepackagerelease
-        archive = spph.archive
-        urls = [ProxiedLibraryFileAlias(f.libraryfile, archive).http_url
+        urls = [ProxiedSourceLibraryFileAlias(f.libraryfile, spph).http_url
             for f in spr.files]
 
         if include_meta:


Follow ups