|
2 | 2 | API for asynchronous job running mechanisms can use to fetch or put files related to running and queued jobs. |
3 | 3 | """ |
4 | 4 |
|
| 5 | +import asyncio |
5 | 6 | import logging |
6 | 7 | import os |
7 | 8 | import re |
|
19 | 20 | Path, |
20 | 21 | Query, |
21 | 22 | Request, |
| 23 | + Response, |
22 | 24 | UploadFile, |
23 | 25 | ) |
24 | 26 | from fastapi.params import Depends |
@@ -220,6 +222,63 @@ def index( |
220 | 222 |
|
221 | 223 | return GalaxyFileResponse(path) |
222 | 224 |
|
| 225 | + # The ARC remote job runner (`lib.galaxy.jobs.runners.pulsar.PulsarARCJobRunner`) expects a `PUT` endpoint to stage |
| 226 | + # out result files back to Galaxy. |
| 227 | + @router.put( |
| 228 | + "/api/jobs/{job_id}/files", |
| 229 | + summary="Populate an output file.", |
| 230 | + responses={ |
| 231 | + 201: {"description": "A new file has been created."}, |
| 232 | + 204: {"description": "An existing file has been replaced."}, |
| 233 | + 400: {"description": "Bad request."}, |
| 234 | + }, |
| 235 | + ) |
| 236 | + def populate( |
| 237 | + self, |
| 238 | + job_id: Annotated[str, Path(description="Encoded id string of the job.")], |
| 239 | + path: Annotated[str, Query(description="Path to file to create/replace.")], |
| 240 | + job_key: Annotated[ |
| 241 | + str, |
| 242 | + Query( |
| 243 | + description=( |
| 244 | + "A key used to authenticate this request as acting on behalf of a job runner for the specified job." |
| 245 | + ), |
| 246 | + ), |
| 247 | + ], |
| 248 | + trans: SessionRequestContext = DependsOnTrans, |
| 249 | + ): |
| 250 | + path = unquote(path) |
| 251 | + |
| 252 | + job = self.__authorize_job_access(trans, job_id, path=path, job_key=job_key) |
| 253 | + self.__check_job_can_write_to_path(trans, job, path) |
| 254 | + |
| 255 | + destination_file_exists = os.path.exists(path) |
| 256 | + |
| 257 | + # FastAPI can only read the file contents from the request body in an async context. To write the file without |
| 258 | + # using an async endpoint, the async code that reads the file from the body and writes it to disk will have to |
| 259 | + # run within the sync endpoint. Since the code that writes the data to disk is blocking |
| 260 | + # `destination_file.write(chunk)`, it has to run on its own event loop within the thread spawned to answer the |
| 261 | + # request to the sync endpoint. |
| 262 | + async def write(): |
| 263 | + with open(path, "wb") as destination_file: |
| 264 | + async for chunk in trans.request.stream(): |
| 265 | + destination_file.write(chunk) |
| 266 | + |
| 267 | + target_dir = os.path.dirname(path) |
| 268 | + util.safe_makedirs(target_dir) |
| 269 | + event_loop = asyncio.new_event_loop() |
| 270 | + try: |
| 271 | + asyncio.set_event_loop(event_loop) |
| 272 | + event_loop.run_until_complete(write()) |
| 273 | + finally: |
| 274 | + event_loop.close() |
| 275 | + |
| 276 | + return ( |
| 277 | + Response(status_code=201, headers={"Location": str(trans.request.url)}) |
| 278 | + if not destination_file_exists |
| 279 | + else Response(status_code=204) |
| 280 | + ) |
| 281 | + |
223 | 282 | @router.post( |
224 | 283 | "/api/jobs/{job_id}/files", |
225 | 284 | summary="Populate an output file.", |
|
0 commit comments