launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20338
Re: [Merge] lp:~cjwatson/launchpad/snap-store-client into lp:launchpad
Diff comments:
>
> === added file 'lib/lp/snappy/model/snapstoreclient.py'
> --- lib/lp/snappy/model/snapstoreclient.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/snappy/model/snapstoreclient.py 2016-05-03 19:57:26 +0000
> @@ -0,0 +1,143 @@
> +# Copyright 2016 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Communication with the snap store."""
> +
> +from __future__ import absolute_import, print_function, unicode_literals
> +
> +__metaclass__ = type
> +__all__ = [
> + 'SnapStoreClient',
> + ]
> +
> +try:
> + from urllib.parse import quote_plus
> +except ImportError:
> + from urllib import quote_plus
> +
> +from lazr.restful.utils import get_current_browser_request
> +import requests
> +from requests_toolbelt import MultipartEncoder
> +from zope.interface import implementer
> +
> +from lp.services.config import config
> +from lp.services.timeline.requesttimeline import get_request_timeline
> +from lp.services.timeout import urlfetch
> +from lp.services.webapp.url import urlappend
> +from lp.snappy.interfaces.snapstoreclient import (
> + BadRequestPackageUploadResponse,
> + BadUploadResponse,
> + ISnapStoreClient,
> + )
> +
> +
> +class LibraryFileAliasWrapper:
> + """A `LibraryFileAlias` wrapper usable with a `MultipartEncoder`."""
> +
> + def __init__(self, lfa):
> + self.lfa = lfa
> + self.position = 0
> +
> + @property
> + def len(self):
> + return self.lfa.content.filesize - self.position
> +
> + def read(self, length=-1):
> + chunksize = None if length == -1 else length
> + data = self.lfa.read(chunksize=chunksize)
> + if chunksize is None:
> + self.position = self.lfa.content.filesize
> + else:
> + self.position += length
> + return data
> +
> +
> +class MacaroonAuth(requests.auth.AuthBase):
> + """Attaches macaroon authentication to a given Request object."""
> +
> + def __init__(self, tokens):
> + self.tokens = tokens
> +
> + def __call__(self, r):
> + r.headers["Authorization"] = (
> + "Macaroon " +
> + ", ".join('%s="%s"' % (k, v) for k, v in self.tokens.items()))
> + return r
> +
> +
> +@implementer(ISnapStoreClient)
> +class SnapStoreClient:
> + """A client for the API provided by the snap store."""
> +
> + def requestPackageUpload(self, snap_series, snap_name):
> + assert config.snappy.store_url is not None
> + request_url = urlappend(
> + config.snappy.store_url, "api/2.0/acl/package_upload/")
> + request = get_current_browser_request()
> + timeline_action = get_request_timeline(request).start(
> + "request-snap-upload-macaroon",
> + "%s/%s" % (snap_series.name, snap_name), allow_nested=True)
> + try:
> + response = urlfetch(
> + request_url, method="POST",
> + json={"name": snap_name, "series": snap_series.name})
> + response_data = response.json()
> + if "macaroon" not in response_data:
> + raise BadRequestPackageUploadResponse(response.text)
> + return response_data["macaroon"]
> + except requests.HTTPError as e:
> + raise BadRequestPackageUploadResponse(e.response.text)
> + finally:
> + timeline_action.finish()
> +
> + def _uploadFile(self, lfa, lfc):
> + """Upload a single file."""
> + assert config.snappy.store_upload_url is not None
> + unscanned_upload_url = urlappend(
> + config.snappy.store_upload_url, "unscanned-upload/")
> + lfa.open()
> + try:
> + lfa_wrapper = LibraryFileAliasWrapper(lfa)
> + encoder = MultipartEncoder(
> + fields={
> + "binary": (
> + "filename", lfa_wrapper, "application/octet-stream"),
> + })
> + try:
> + response = urlfetch(
> + unscanned_upload_url, method="POST", data=encoder,
> + headers={"Content-Type": encoder.content_type})
I'm not sure this is possible. This bit is going to execute in a job context, and as far as I know jobs (and script-like things in general) don't have enough of a persistent request context to attach timeline information to. Bug 623199 is related, but the fix for that bug didn't get as far as timelines.
Suggestions welcome, but I plan to defer this for now and just leave XXX comments.
> + response_data = response.json()
> + if not response_data.get("successful", False):
> + raise BadUploadResponse(response.text)
> + return {"upload_id": response_data["upload_id"]}
> + except requests.HTTPError as e:
> + raise BadUploadResponse(e.response.text)
> + finally:
> + lfa.close()
> +
> + def _uploadApp(self, snap, upload_data):
> + """Create a new store upload based on the uploaded file."""
> + assert config.snappy.store_url is not None
> + assert snap.store_name is not None
> + upload_url = urlappend(
> + config.snappy.store_url,
> + "dev/api/snap-upload/%s/" % quote_plus(snap.store_name))
> + data = {
> + "updown_id": upload_data["upload_id"],
> + "series": snap.store_series.name,
> + }
> + # XXX cjwatson 2016-04-20: handle refresh
> + try:
> + assert snap.store_tokens is not None
> + urlfetch(
> + upload_url, method="POST", data=data,
> + auth=MacaroonAuth(snap.store_tokens))
> + except requests.HTTPError as e:
> + raise BadUploadResponse(e.response.text)
> +
> + def upload(self, snapbuild):
> + """See `ISnapStoreClient`."""
> + for _, lfa, lfc in snapbuild.getFiles():
> + upload_data = self._uploadFile(lfa, lfc)
> + self._uploadApp(snapbuild.snap, upload_data)
--
https://code.launchpad.net/~cjwatson/launchpad/snap-store-client/+merge/293668
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
References