xref: /aosp_15_r20/external/federated-compute/fcp/demo/media.py (revision 14675a029014e728ec732f129a32e299b2da0601)
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