~launchpad-pqm/launchpad/devel

8687.15.17 by Karl Fogel
Add the copyright header block to the rest of the files under lib/lp/.
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
3
4
__metaclass__ = type
5
6
__all__ = [
7
    'ProductReleaseFinder'
8
    ]
9
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
10
from datetime import datetime
11
import mimetypes
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
12
import os
8848.3.4 by Curtis Hovey
Added extract_version function to guarantee that the file's version number is sane and can be used to make a milestone.
13
import re
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
14
import urllib
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
15
import urlparse
16
4187.4.12 by Michael Hudson
fix imports broken by moving code around in cscvs
17
from cscvs.dircompare import path
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
18
import pytz
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
19
from zope.component import getUtility
20
12442.2.2 by j.c.sackett
Moved validators to app, which makes more sense.
21
from lp.app.validators.name import invalid_name_pattern
22
from lp.app.validators.version import sane_version
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
23
from lp.registry.interfaces.product import IProductSet
24
from lp.registry.interfaces.productrelease import UpstreamFileType
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
25
from lp.registry.interfaces.series import SeriesStatus
26
from lp.registry.scripts.productreleasefinder.filter import FilterPattern
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
27
from lp.registry.scripts.productreleasefinder.hose import Hose
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
28
29
8848.3.4 by Curtis Hovey
Added extract_version function to guarantee that the file's version number is sane and can be used to make a milestone.
30
processors = '|'.join([
31
    'all',
32
    'amd64',
33
    'arm',
34
    'armel',
35
    'i386',
36
    'intel',
37
    'hppa',
38
    'hurd-i386',
39
    'ia64',
40
    'm68k',
41
    'mips',
42
    'mipsel',
43
    'powerpc',
44
    's390',
45
    'sparc',
46
    ])
47
flavor_pattern = re.compile(r"""
9772.1.1 by Curtis Hovey
Updated PRF flavor rules to identify the '~' character as the start of packaging
48
    (~                            # Packaging target
49
     |_[a-z][a-z]_[A-Z][A-Z]      # or language version
8848.3.4 by Curtis Hovey
Added extract_version function to guarantee that the file's version number is sane and can be used to make a milestone.
50
     |_(%s)                       # or processor version
51
     |[\.-](win32|OSX)            # or OS version
52
     |\.(deb|noarch|rpm|dmg|exe)  # or packaging version
53
    ).*                           # to the end of the string
54
    """ % processors, re.VERBOSE)
55
56
57
def extract_version(filename):
58
    """Return the release version of the file, or None.
59
60
    Ensure the version is compatible with Launchpad. None is returned
61
    if a version could not be extracted.
62
    """
63
    version = path.split_version(path.name(filename))[1]
64
    if version is None:
65
        return None
66
    # Tarballs pulled from a Debian-style archive often have
67
    # ".orig" appended to the version number.  We don't want this.
68
    if version.endswith('.orig'):
69
        version = version[:-len('.orig')]
70
    # Remove processor and language flavors from the version:
71
    # eg. _de_DE, _all, _i386.
8848.3.6 by Curtis Hovey
Quiet lint.
72
    version = flavor_pattern.sub('', version)
11065.3.1 by Stuart Bishop
Product release finder should ignore unknown file extensions rather than incorporate them into the version number
73
    # Bug #599250. If there is no file extension after extracting
74
    # the version number, we have added an unknown file extension to the
75
    # version. Ignore this dud match.
76
    if filename.endswith(version):
77
        return None
8848.3.4 by Curtis Hovey
Added extract_version function to guarantee that the file's version number is sane and can be used to make a milestone.
78
    # Launchpad requires all versions to be lowercase. They may contain
79
    # letters, numbers, dots, underscores, and hyphens (a-z0-9._-).
80
    version = version.lower()
81
    version = invalid_name_pattern.sub('-', version)
82
    version = version.replace('+', '-')
83
    return version
84
85
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
86
class ProductReleaseFinder:
87
88
    def __init__(self, ztm, log):
89
        self.ztm = ztm
90
        self.log = log
91
92
    def findReleases(self):
93
        """Scan for new releases in all products."""
94
        for product_name, filters in self.getFilters():
95
            self.handleProduct(product_name, filters)
96
97
    def getFilters(self):
98
        """Build the list of products and filters.
99
100
        Returns a list of (product_name, filters) for each product in
101
        the database, where the filter keys are series names.
102
        """
103
        todo = []
104
105
        self.ztm.begin()
106
        products = getUtility(IProductSet)
12810.1.1 by Robert Collins
Stop using eager loading in product release finder.
107
        for product in products.get_all_active(eager_load=False):
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
108
            filters = []
109
9760.8.1 by Brad Crittenden
Change the non-English 'serieses' to 'series' throughout our codebase.
110
            for series in product.series:
10977.6.2 by Curtis Hovey
PRF ignores obsolete series.
111
                if (series.status == SeriesStatus.OBSOLETE
112
                    or not series.releasefileglob):
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
113
                    continue
114
115
                filters.append(FilterPattern(series.name,
3691.169.22 by James Henstridge
adjust product-release-finder code to work with single releasefileglob field
116
                                             series.releasefileglob))
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
117
118
            if not len(filters):
119
                continue
120
121
            self.log.info("%s has %d series with information", product.name,
122
                             len(filters))
123
124
            todo.append((product.name, filters))
125
        self.ztm.abort()
126
127
        return todo
128
129
    def handleProduct(self, product_name, filters):
130
        """Scan for tarballs and create ProductReleases for the given product.
131
        """
3691.119.1 by James Henstridge
remove canonical.launchpad.scripts.productreleasrfinder.filter.Cache and all references to it
132
        hose = Hose(filters, log_parent=self.log)
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
133
        for series_name, url in hose:
134
            if series_name is not None:
135
                try:
136
                    self.handleRelease(product_name, series_name, url)
3691.131.4 by James Henstridge
add support for limiting which subdirs get walked in Hose
137
                except (KeyboardInterrupt, SystemExit):
138
                    raise
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
139
                except:
140
                    self.log.exception("Could not successfully process "
141
                                       "URL %s for %s/%s",
142
                                       url, product_name, series_name)
143
            else:
144
                self.log.debug("File in %s found that matched no glob: %s",
145
                               product_name, url)
146
9772.1.3 by Curtis Hovey
Renamed PRF's hasReleaseTarball => hasReleaseFile and added the file_name argument.
147
    def hasReleaseFile(self, product_name, series_name,
9772.1.4 by Curtis Hovey
Updated PRF hasReleaseFile to check for filename, not filetype.
148
                          release_name, filename):
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
149
        """Return True if we have a tarball for the given product release."""
9772.1.4 by Curtis Hovey
Updated PRF hasReleaseFile to check for filename, not filetype.
150
        has_file = False
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
151
        self.ztm.begin()
152
        try:
153
            product = getUtility(IProductSet).getByName(product_name)
154
            if product is not None:
155
                series = product.getSeries(series_name)
156
                if series is not None:
157
                    release = series.getRelease(release_name)
3691.105.2 by James Henstridge
some tests for the ProductReleaseFinder class
158
                    if release is not None:
159
                        for fileinfo in release.files:
9772.1.4 by Curtis Hovey
Updated PRF hasReleaseFile to check for filename, not filetype.
160
                            if filename == fileinfo.libraryfile.filename:
161
                                has_file = True
3691.105.2 by James Henstridge
some tests for the ProductReleaseFinder class
162
                                break
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
163
        finally:
164
            self.ztm.abort()
9772.1.4 by Curtis Hovey
Updated PRF hasReleaseFile to check for filename, not filetype.
165
        return has_file
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
166
167
    def addReleaseTarball(self, product_name, series_name, release_name,
168
                          filename, size, file, content_type):
169
        """Create a ProductRelease (if needed), and attach tarball"""
3691.131.6 by James Henstridge
fixes from BjornT's review
170
        # Get the series.
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
171
        self.ztm.begin()
172
        try:
173
            product = getUtility(IProductSet).getByName(product_name)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
174
            # XXX: This might match a milestone on a product series that was
175
            # not intended, since product release used to have unique
176
            # names per product series, but are now dependent on the milestone
177
            # name which is unique per product. The series_name method
178
            # parameter can be removed.
179
            milestone = product.getMilestone(release_name)
180
            if milestone is None:
181
                series = product.getSeries(series_name)
182
                milestone = series.newMilestone(release_name)
183
                # Normally, a milestone is deactived when that version is
184
                # released. This is only safe to do in an automated script
185
                # if we are not using a pre-existing milestone.
186
                milestone.active = False
187
            release = milestone.product_release
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
188
            if release is None:
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
189
                release = milestone.createProductRelease(
190
                    owner=product.owner, datereleased=datetime.now(pytz.UTC))
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
191
                self.log.info("Created new release %s for %s/%s",
192
                              release_name, product_name, series_name)
6935.4.13 by Edwin Grubbs
Fixed tests.
193
            release.addReleaseFile(
6935.7.7 by Brad Crittenden
Add IProductReleaseFile.delete
194
                filename, file, content_type, uploader=product.owner)
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
195
            self.ztm.commit()
196
        except:
197
            self.ztm.abort()
198
            raise
199
8848.3.4 by Curtis Hovey
Added extract_version function to guarantee that the file's version number is sane and can be used to make a milestone.
200
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
201
    def handleRelease(self, product_name, series_name, url):
202
        """If the given URL looks like a release tarball, download it
203
        and create a corresponding ProductRelease."""
204
        filename = urlparse.urlsplit(url)[2]
205
        slash = filename.rfind("/")
206
        if slash != -1:
207
            filename = filename[slash+1:]
208
        self.log.debug("Filename portion is %s", filename)
209
8848.3.4 by Curtis Hovey
Added extract_version function to guarantee that the file's version number is sane and can be used to make a milestone.
210
        version = extract_version(filename)
3691.314.3 by Diogo Matsubara
Fixes 79563 (Product release finder script crashes if it can't parse the product version in the given URL.)
211
        if version is None:
8848.3.7 by Curtis Hovey
Reduce product-release-finder warings to info because there is nothing that we can do to fix them.
212
            self.log.info("Unable to parse version from %s", url)
3691.314.3 by Diogo Matsubara
Fixes 79563 (Product release finder script crashes if it can't parse the product version in the given URL.)
213
            return
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
214
        self.log.debug("Version is %s", version)
215
        if not sane_version(version):
216
            self.log.error("Version number '%s' for '%s' is not sane",
217
                           version, url)
4195.1.1 by Brad Crittenden
Implement upload and management of files associated with a product release.
218
            return
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
219
9772.1.3 by Curtis Hovey
Renamed PRF's hasReleaseTarball => hasReleaseFile and added the file_name argument.
220
        if self.hasReleaseFile(product_name, series_name, version, filename):
3691.105.1 by James Henstridge
move body of product-release-finder to a module, and make it check for existing releases and attached tarballs
221
            self.log.debug("Already have a tarball for release %s", version)
222
            return
223
224
        mimetype, encoding = mimetypes.guess_type(url)
225
        self.log.debug("Mime type is %s", mimetype)
226
        if mimetype is None:
227
            mimetype = 'application/octet-stream'
228
229
        self.log.info("Downloading %s", url)
230
        try:
231
            local, headers = urllib.urlretrieve(url)
232
            stat = os.stat(local)
233
        except IOError:
234
            self.log.error("Download of %s failed", url)
235
            raise
236
        except OSError:
237
            self.log.error("Unable to stat downloaded file")
238
            raise
239
240
        fp = open(local, 'r')
241
        os.unlink(local)
242
        self.addReleaseTarball(product_name, series_name, version,
243
                               filename, stat.st_size, fp, mimetype)