1*14675a02SAndroid Build Coastguard Worker# Copyright 2022 Google LLC 2*14675a02SAndroid Build Coastguard Worker# 3*14675a02SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*14675a02SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*14675a02SAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*14675a02SAndroid Build Coastguard Worker# 7*14675a02SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*14675a02SAndroid Build Coastguard Worker# 9*14675a02SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*14675a02SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*14675a02SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*14675a02SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*14675a02SAndroid Build Coastguard Worker# limitations under the License. 14*14675a02SAndroid Build Coastguard Worker"""Action handlers for file upload and download. 15*14675a02SAndroid Build Coastguard Worker 16*14675a02SAndroid Build Coastguard WorkerIn a production system, download would likely be handled by an external service; 17*14675a02SAndroid Build Coastguard Workerit's important that uploads are not handled separately to help ensure that 18*14675a02SAndroid Build Coastguard Workerunaggregated client data is only held ephemerally. 19*14675a02SAndroid Build Coastguard Worker""" 20*14675a02SAndroid Build Coastguard Worker 21*14675a02SAndroid Build Coastguard Workerimport contextlib 22*14675a02SAndroid Build Coastguard Workerimport http 23*14675a02SAndroid Build Coastguard Workerimport threading 24*14675a02SAndroid Build Coastguard Workerfrom typing import Callable, Iterator, Optional 25*14675a02SAndroid Build Coastguard Workerimport uuid 26*14675a02SAndroid Build Coastguard Worker 27*14675a02SAndroid Build Coastguard Workerfrom fcp.demo import http_actions 28*14675a02SAndroid Build Coastguard Workerfrom fcp.protos.federatedcompute import common_pb2 29*14675a02SAndroid Build Coastguard Worker 30*14675a02SAndroid Build Coastguard Worker 31*14675a02SAndroid Build Coastguard Workerclass DownloadGroup: 32*14675a02SAndroid Build Coastguard Worker """A group of downloadable files.""" 33*14675a02SAndroid Build Coastguard Worker 34*14675a02SAndroid Build Coastguard Worker def __init__(self, prefix: str, add_fn: Callable[[str, bytes, str], None]): 35*14675a02SAndroid Build Coastguard Worker self._prefix = prefix 36*14675a02SAndroid Build Coastguard Worker self._add_fn = add_fn 37*14675a02SAndroid Build Coastguard Worker 38*14675a02SAndroid Build Coastguard Worker @property 39*14675a02SAndroid Build Coastguard Worker def prefix(self) -> str: 40*14675a02SAndroid Build Coastguard Worker """The path prefix for all files in this group.""" 41*14675a02SAndroid Build Coastguard Worker return self._prefix 42*14675a02SAndroid Build Coastguard Worker 43*14675a02SAndroid Build Coastguard Worker def add(self, 44*14675a02SAndroid Build Coastguard Worker name: str, 45*14675a02SAndroid Build Coastguard Worker data: bytes, 46*14675a02SAndroid Build Coastguard Worker content_type: str = 'application/octet-stream') -> str: 47*14675a02SAndroid Build Coastguard Worker """Adds a file to the group. 48*14675a02SAndroid Build Coastguard Worker 49*14675a02SAndroid Build Coastguard Worker Args: 50*14675a02SAndroid Build Coastguard Worker name: The name of the new file. 51*14675a02SAndroid Build Coastguard Worker data: The bytes to make available. 52*14675a02SAndroid Build Coastguard Worker content_type: The content type to include in the response. 53*14675a02SAndroid Build Coastguard Worker 54*14675a02SAndroid Build Coastguard Worker Returns: 55*14675a02SAndroid Build Coastguard Worker The full path to the new file. 56*14675a02SAndroid Build Coastguard Worker 57*14675a02SAndroid Build Coastguard Worker Raises: 58*14675a02SAndroid Build Coastguard Worker KeyError if a file with that name has already been registered. 59*14675a02SAndroid Build Coastguard Worker """ 60*14675a02SAndroid Build Coastguard Worker self._add_fn(name, data, content_type) 61*14675a02SAndroid Build Coastguard Worker return self._prefix + name 62*14675a02SAndroid Build Coastguard Worker 63*14675a02SAndroid Build Coastguard Worker 64*14675a02SAndroid Build Coastguard Workerclass Service: 65*14675a02SAndroid Build Coastguard Worker """Implements a service for uploading and downloading data over HTTP.""" 66*14675a02SAndroid Build Coastguard Worker 67*14675a02SAndroid Build Coastguard Worker def __init__(self, forwarding_info: Callable[[], common_pb2.ForwardingInfo]): 68*14675a02SAndroid Build Coastguard Worker self._forwarding_info = forwarding_info 69*14675a02SAndroid Build Coastguard Worker self._lock = threading.Lock() 70*14675a02SAndroid Build Coastguard Worker self._downloads: dict[str, dict[str, http_actions.HttpResponse]] = {} 71*14675a02SAndroid Build Coastguard Worker self._uploads: dict[str, Optional[bytes]] = {} 72*14675a02SAndroid Build Coastguard Worker 73*14675a02SAndroid Build Coastguard Worker @contextlib.contextmanager 74*14675a02SAndroid Build Coastguard Worker def create_download_group(self) -> Iterator[DownloadGroup]: 75*14675a02SAndroid Build Coastguard Worker """Creates a new group of downloadable files. 76*14675a02SAndroid Build Coastguard Worker 77*14675a02SAndroid Build Coastguard Worker Files can be be added to this group using `DownloadGroup.add`. All files in 78*14675a02SAndroid Build Coastguard Worker the group will be unregistered when the ContextManager goes out of scope. 79*14675a02SAndroid Build Coastguard Worker 80*14675a02SAndroid Build Coastguard Worker Yields: 81*14675a02SAndroid Build Coastguard Worker The download group to which files should be added. 82*14675a02SAndroid Build Coastguard Worker """ 83*14675a02SAndroid Build Coastguard Worker group = str(uuid.uuid4()) 84*14675a02SAndroid Build Coastguard Worker 85*14675a02SAndroid Build Coastguard Worker def add_file(name: str, data: bytes, content_type: str) -> None: 86*14675a02SAndroid Build Coastguard Worker with self._lock: 87*14675a02SAndroid Build Coastguard Worker if name in self._downloads[group]: 88*14675a02SAndroid Build Coastguard Worker raise KeyError(f'{name} already exists') 89*14675a02SAndroid Build Coastguard Worker self._downloads[group][name] = http_actions.HttpResponse( 90*14675a02SAndroid Build Coastguard Worker body=data, 91*14675a02SAndroid Build Coastguard Worker headers={ 92*14675a02SAndroid Build Coastguard Worker 'Content-Length': len(data), 93*14675a02SAndroid Build Coastguard Worker 'Content-Type': content_type, 94*14675a02SAndroid Build Coastguard Worker }) 95*14675a02SAndroid Build Coastguard Worker 96*14675a02SAndroid Build Coastguard Worker with self._lock: 97*14675a02SAndroid Build Coastguard Worker self._downloads[group] = {} 98*14675a02SAndroid Build Coastguard Worker try: 99*14675a02SAndroid Build Coastguard Worker yield DownloadGroup( 100*14675a02SAndroid Build Coastguard Worker f'{self._forwarding_info().target_uri_prefix}data/{group}/', add_file) 101*14675a02SAndroid Build Coastguard Worker finally: 102*14675a02SAndroid Build Coastguard Worker with self._lock: 103*14675a02SAndroid Build Coastguard Worker del self._downloads[group] 104*14675a02SAndroid Build Coastguard Worker 105*14675a02SAndroid Build Coastguard Worker def register_upload(self) -> str: 106*14675a02SAndroid Build Coastguard Worker """Registers a path for single-use upload, returning the resource name.""" 107*14675a02SAndroid Build Coastguard Worker name = str(uuid.uuid4()) 108*14675a02SAndroid Build Coastguard Worker with self._lock: 109*14675a02SAndroid Build Coastguard Worker self._uploads[name] = None 110*14675a02SAndroid Build Coastguard Worker return name 111*14675a02SAndroid Build Coastguard Worker 112*14675a02SAndroid Build Coastguard Worker def finalize_upload(self, name: str) -> Optional[bytes]: 113*14675a02SAndroid Build Coastguard Worker """Returns the data from an upload, if any.""" 114*14675a02SAndroid Build Coastguard Worker with self._lock: 115*14675a02SAndroid Build Coastguard Worker return self._uploads.pop(name) 116*14675a02SAndroid Build Coastguard Worker 117*14675a02SAndroid Build Coastguard Worker @http_actions.http_action(method='GET', pattern='/data/{group}/{name}') 118*14675a02SAndroid Build Coastguard Worker def download(self, body: bytes, group: str, 119*14675a02SAndroid Build Coastguard Worker name: str) -> http_actions.HttpResponse: 120*14675a02SAndroid Build Coastguard Worker """Handles a download request.""" 121*14675a02SAndroid Build Coastguard Worker del body 122*14675a02SAndroid Build Coastguard Worker try: 123*14675a02SAndroid Build Coastguard Worker with self._lock: 124*14675a02SAndroid Build Coastguard Worker return self._downloads[group][name] 125*14675a02SAndroid Build Coastguard Worker except KeyError as e: 126*14675a02SAndroid Build Coastguard Worker raise http_actions.HttpError(http.HTTPStatus.NOT_FOUND) from e 127*14675a02SAndroid Build Coastguard Worker 128*14675a02SAndroid Build Coastguard Worker @http_actions.http_action( 129*14675a02SAndroid Build Coastguard Worker method='POST', pattern='/upload/v1/media/{name}?upload_protocol=raw') 130*14675a02SAndroid Build Coastguard Worker def upload(self, body: bytes, name: str) -> http_actions.HttpResponse: 131*14675a02SAndroid Build Coastguard Worker with self._lock: 132*14675a02SAndroid Build Coastguard Worker if name not in self._uploads or self._uploads[name] is not None: 133*14675a02SAndroid Build Coastguard Worker raise http_actions.HttpError(http.HTTPStatus.UNAUTHORIZED) 134*14675a02SAndroid Build Coastguard Worker self._uploads[name] = body 135*14675a02SAndroid Build Coastguard Worker return http_actions.HttpResponse(b'') 136