Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File endpoints: use query parameters for NGINX/Caddy compatibility #6376

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 130 additions & 1 deletion app/user_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def process_full_path(full_path: str) -> FileInfo | str | list[str]:

return web.json_response(results)

def get_user_data_path(request, check_exists = False, param = "file"):
def get_user_data_path(request, check_exists=False, param="file"):
file = request.match_info.get(param, None)
if not file:
return web.Response(status=400)
Expand All @@ -211,6 +211,21 @@ def get_user_data_path(request, check_exists = False, param = "file"):

return path

def get_user_data_path_v1(request, check_exists=False, param="file"):
"""Reads a file-like parameter from the query string."""
file = request.query.get(param)
if not file:
return web.Response(status=400)

path = self.get_request_user_filepath(request, file)
if not path:
return web.Response(status=403)

if check_exists and not os.path.exists(path):
return web.Response(status=404)

return path

@routes.get("/userdata/{file}")
async def getuserdata(request):
path = get_user_data_path(request, check_exists=True)
Expand All @@ -219,6 +234,14 @@ async def getuserdata(request):

return web.FileResponse(path)

@routes.get("/v1/userdata/file")
async def getuserdata_v1(request):
path = get_user_data_path_v1(request, check_exists=True)
if not isinstance(path, str):
return path

return web.FileResponse(path)

@routes.post("/userdata/{file}")
async def post_userdata(request):
"""
Expand Down Expand Up @@ -268,6 +291,53 @@ async def post_userdata(request):

return web.json_response(resp)

@routes.post("/v1/userdata/file")
async def post_userdata_v1(request):
"""
Upload or update a user data file.

This endpoint handles file uploads to a user's data directory, with options for
controlling overwrite behavior and response format.

Query Parameters:
- file: The target file path (URL encoded if necessary).
- overwrite (optional): If "false", prevents overwriting existing files. Defaults to "true".
- full_info (optional): If "true", returns detailed file information (path, size, modified time).
If "false", returns only the relative file path.

Returns:
- 400: If 'file' parameter is missing.
- 403: If the requested path is not allowed.
- 409: If overwrite=false and the file already exists.
- 200: JSON response with either:
- Full file information (if full_info=true)
- Relative file path (if full_info=false)

The request body should contain the raw file content to be written.
"""
path = get_user_data_path_v1(request)
if not isinstance(path, str):
return path

overwrite = request.query.get("overwrite", 'true') != "false"
full_info = request.query.get("full_info", 'false').lower() == "true"

if not overwrite and os.path.exists(path):
return web.Response(status=409, text="File already exists")

body = await request.read()

with open(path, "wb") as f:
f.write(body)

user_path = self.get_request_user_filepath(request, None)
if full_info:
resp = get_file_info(path, user_path)
else:
resp = os.path.relpath(path, user_path)

return web.json_response(resp)

@routes.delete("/userdata/{file}")
async def delete_userdata(request):
path = get_user_data_path(request, check_exists=True)
Expand All @@ -278,6 +348,16 @@ async def delete_userdata(request):

return web.Response(status=204)

@routes.delete("/v1/userdata/file")
async def delete_userdata_v1(request):
path = get_user_data_path_v1(request, check_exists=True)
if not isinstance(path, str):
return path

os.remove(path)

return web.Response(status=204)

@routes.post("/userdata/{file}/move/{dest}")
async def move_userdata(request):
"""
Expand Down Expand Up @@ -328,3 +408,52 @@ async def move_userdata(request):
resp = os.path.relpath(dest, user_path)

return web.json_response(resp)

@routes.post("/v1/userdata/file/move")
async def move_userdata_v1(request):
"""
Move or rename a user data file.

This endpoint handles moving or renaming files within a user's data directory, with options for
controlling overwrite behavior and response format.

Query Parameters:
- source: The source file path (URL encoded if necessary)
- dest: The destination file path (URL encoded if necessary)
- overwrite (optional): If "false", prevents overwriting existing files. Defaults to "true".
- full_info (optional): If "true", returns detailed file information (path, size, modified time).
If "false", returns only the relative file path.

Returns:
- 400: If either 'file' or 'dest' parameter is missing
- 403: If either requested path is not allowed
- 404: If the source file does not exist
- 409: If overwrite=false and the destination file already exists
- 200: JSON response with either:
- Full file information (if full_info=true)
- Relative file path (if full_info=false)
"""
source = get_user_data_path_v1(request, check_exists=True, param="source")
if not isinstance(source, str):
return source

dest = get_user_data_path_v1(request, check_exists=False, param="dest")
if not isinstance(dest, str):
return dest

overwrite = request.query.get("overwrite", 'true') != "false"
full_info = request.query.get('full_info', 'false').lower() == "true"

if not overwrite and os.path.exists(dest):
return web.Response(status=409, text="File already exists")

logging.info(f"moving '{source}' -> '{dest}'")
shutil.move(source, dest)

user_path = self.get_request_user_filepath(request, None)
if full_info:
resp = get_file_info(dest, user_path)
else:
resp = os.path.relpath(dest, user_path)

return web.json_response(resp)
14 changes: 7 additions & 7 deletions tests-unit/prompt_server_test/user_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async def test_listuserdata_normalized_separator(aiohttp_client, app, tmp_path):
async def test_post_userdata_new_file(aiohttp_client, app, tmp_path):
client = await aiohttp_client(app)
content = b"test content"
resp = await client.post("/userdata/test.txt", data=content)
resp = await client.post("/v1/userdata/file?file=test.txt", data=content)

assert resp.status == 200
assert await resp.text() == '"test.txt"'
Expand All @@ -138,7 +138,7 @@ async def test_post_userdata_overwrite_existing(aiohttp_client, app, tmp_path):

client = await aiohttp_client(app)
new_content = b"updated content"
resp = await client.post("/userdata/test.txt", data=new_content)
resp = await client.post("/v1/userdata/file?file=test.txt", data=new_content)

assert resp.status == 200
assert await resp.text() == '"test.txt"'
Expand All @@ -154,7 +154,7 @@ async def test_post_userdata_no_overwrite(aiohttp_client, app, tmp_path):
f.write("initial content")

client = await aiohttp_client(app)
resp = await client.post("/userdata/test.txt?overwrite=false", data=b"new content")
resp = await client.post("/v1/userdata/file?file=test.txt&overwrite=false", data=b"new content")

assert resp.status == 409

Expand All @@ -166,7 +166,7 @@ async def test_post_userdata_no_overwrite(aiohttp_client, app, tmp_path):
async def test_post_userdata_full_info(aiohttp_client, app, tmp_path):
client = await aiohttp_client(app)
content = b"test content"
resp = await client.post("/userdata/test.txt?full_info=true", data=content)
resp = await client.post("/v1/userdata/file?file=test.txt&full_info=true", data=content)

assert resp.status == 200
result = await resp.json()
Expand All @@ -181,7 +181,7 @@ async def test_move_userdata(aiohttp_client, app, tmp_path):
f.write("test content")

client = await aiohttp_client(app)
resp = await client.post("/userdata/source.txt/move/dest.txt")
resp = await client.post("/v1/userdata/file/move?source=source.txt&dest=dest.txt")

assert resp.status == 200
assert await resp.text() == '"dest.txt"'
Expand All @@ -200,7 +200,7 @@ async def test_move_userdata_no_overwrite(aiohttp_client, app, tmp_path):
f.write("destination content")

client = await aiohttp_client(app)
resp = await client.post("/userdata/source.txt/move/dest.txt?overwrite=false")
resp = await client.post("/v1/userdata/file/move?source=source.txt&dest=dest.txt&overwrite=false")

assert resp.status == 409

Expand All @@ -217,7 +217,7 @@ async def test_move_userdata_full_info(aiohttp_client, app, tmp_path):
f.write("test content")

client = await aiohttp_client(app)
resp = await client.post("/userdata/source.txt/move/dest.txt?full_info=true")
resp = await client.post("/v1/userdata/file/move?source=source.txt&dest=dest.txt&full_info=true")

assert resp.status == 200
result = await resp.json()
Expand Down
Loading