From c68bb66de178ab650aa11dc6c23fa0522188cd09 Mon Sep 17 00:00:00 2001 From: b3nw Date: Mon, 25 May 2026 02:44:13 +0000 Subject: [PATCH] fix: Support local file storage uploads in addition to S3 The upload endpoint now detects whether Outline uses local file storage (multipart form POST) or S3 (pre-signed PUT URL) and handles both. --- server.py | 59 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/server.py b/server.py index 1d9beab..7c932d1 100644 --- a/server.py +++ b/server.py @@ -633,31 +633,54 @@ async def upload_endpoint(request: Request) -> JSONResponse: status_code=502, ) - attachment_data = create_response.get("data", {}) - upload_url = attachment_data.get("uploadUrl") + response_data = create_response.get("data", {}) + upload_url = response_data.get("uploadUrl") + form_fields = response_data.get("form", {}) + attachment_data = response_data.get("attachment", {}) if not upload_url: return JSONResponse( {"ok": False, "error": "Outline API did not return an uploadUrl"}, status_code=502, ) - # 3. Stream the request body directly to S3 upload_url - try: - # Pre-signed S3 PUT URLs usually enforce exact headers - headers = { - "Content-Type": content_type, - "Content-Length": str(size), - } + # Resolve relative uploadUrl against the Outline base URL + if upload_url.startswith("/"): + upload_url = f"{OUTLINE_API_URL}{upload_url}" - logger.info(f"Streaming file content to storage (size={size} bytes)") - async with httpx.AsyncClient() as client: - put_response = await client.put( - upload_url, - content=request.stream(), - headers=headers, - timeout=600.0, - ) - put_response.raise_for_status() + is_local_storage = bool(form_fields) + + # 3. Upload the file content to storage + try: + body = await request.body() + + if is_local_storage: + # Local/database file storage: POST multipart form with fields from Outline + logger.info(f"Uploading via local storage endpoint (size={size} bytes)") + files = {"file": (filename, body, content_type)} + async with httpx.AsyncClient() as client: + post_response = await client.post( + upload_url, + data=form_fields, + files=files, + headers={"Authorization": f"Bearer {OUTLINE_API_TOKEN}"}, + timeout=600.0, + ) + post_response.raise_for_status() + else: + # S3-compatible storage: PUT directly to pre-signed URL + headers = { + "Content-Type": content_type, + "Content-Length": str(size), + } + logger.info(f"Streaming file content to S3 storage (size={size} bytes)") + async with httpx.AsyncClient() as client: + put_response = await client.put( + upload_url, + content=body, + headers=headers, + timeout=600.0, + ) + put_response.raise_for_status() logger.info("Upload to storage completed successfully")