From 0eff10fd2190da071f0450cd97becab68a9911c7 Mon Sep 17 00:00:00 2001 From: Sam Pullara Date: Wed, 29 Oct 2025 13:17:56 -0700 Subject: [PATCH 1/3] store json files pretty printed for better source control compatibiility --- app/user_manager.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/user_manager.py b/app/user_manager.py index a2d376c0c..f62901cf7 100644 --- a/app/user_manager.py +++ b/app/user_manager.py @@ -366,8 +366,22 @@ class UserManager(): try: body = await request.read() - with open(path, "wb") as f: - f.write(body) + # Pretty print JSON files for better source control + if path.lower().endswith('.json'): + try: + # Parse JSON and re-serialize with indentation + json_data = json.loads(body.decode('utf-8')) + formatted_json = json.dumps(json_data, indent=2) + with open(path, "w", encoding='utf-8') as f: + f.write(formatted_json) + except (json.JSONDecodeError, UnicodeDecodeError): + # If JSON parsing fails, save as-is + with open(path, "wb") as f: + f.write(body) + else: + # Non-JSON files are saved as-is + with open(path, "wb") as f: + f.write(body) except OSError as e: logging.warning(f"Error saving file '{path}': {e}") return web.Response( From 6d23bfde7f2085dfec0b25c3475df725676f0eae Mon Sep 17 00:00:00 2001 From: Sam Pullara Date: Wed, 29 Oct 2025 13:26:49 -0700 Subject: [PATCH 2/3] add tests for saving json files formatted nicely --- .../prompt_server_test/user_manager_test.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests-unit/prompt_server_test/user_manager_test.py b/tests-unit/prompt_server_test/user_manager_test.py index b939d8e68..9288b20d7 100644 --- a/tests-unit/prompt_server_test/user_manager_test.py +++ b/tests-unit/prompt_server_test/user_manager_test.py @@ -287,3 +287,72 @@ async def test_listuserdata_v2_url_encoded_path(aiohttp_client, app, tmp_path): assert entry["name"] == "file.txt" # Ensure the path is correctly decoded and uses forward slash assert entry["path"] == "my dir/file.txt" + + +async def test_post_userdata_json_pretty_print(aiohttp_client, app, tmp_path): + """Test that JSON files are saved with pretty printing (indentation)""" + import json + + client = await aiohttp_client(app) + + # Create a compact JSON workflow + workflow_data = { + "nodes": [ + {"id": "1", "type": "LoadImage", "inputs": {"image": "test.png"}}, + {"id": "2", "type": "SaveImage", "inputs": {"images": ["1", 0]}} + ], + "metadata": {"version": "1.0", "author": "test"} + } + compact_json = json.dumps(workflow_data).encode('utf-8') + + # Save as JSON file + resp = await client.post("/userdata/workflow.json", data=compact_json) + assert resp.status == 200 + + # Read the saved file and verify it's pretty-printed + with open(tmp_path / "workflow.json", "r", encoding='utf-8') as f: + saved_content = f.read() + + # Verify the file contains indentation (pretty-printed) + assert " " in saved_content # Should have 2-space indentation + assert "\n" in saved_content # Should have newlines + + # Verify the content is still valid JSON and matches original data + saved_data = json.loads(saved_content) + assert saved_data == workflow_data + + # Verify it's actually formatted (not compact) + # Compact JSON would be much shorter + assert len(saved_content) > len(compact_json) + + +async def test_post_userdata_json_invalid_fallback(aiohttp_client, app, tmp_path): + """Test that invalid JSON is saved as-is without error""" + client = await aiohttp_client(app) + + # Create invalid JSON content + invalid_json = b'{"invalid": json content}' + + # Save as JSON file - should not fail + resp = await client.post("/userdata/invalid.json", data=invalid_json) + assert resp.status == 200 + + # Verify file was saved as-is + with open(tmp_path / "invalid.json", "rb") as f: + assert f.read() == invalid_json + + +async def test_post_userdata_non_json_unchanged(aiohttp_client, app, tmp_path): + """Test that non-JSON files are saved unchanged""" + client = await aiohttp_client(app) + + # Create binary content + binary_content = b'\x00\x01\x02\x03\x04\x05' + + # Save as non-JSON file + resp = await client.post("/userdata/test.bin", data=binary_content) + assert resp.status == 200 + + # Verify file was saved exactly as-is + with open(tmp_path / "test.bin", "rb") as f: + assert f.read() == binary_content From 149506beea35a457e713360a3a627dadce241d04 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 00:40:19 +0000 Subject: [PATCH 3/3] Fix file download issue - add attachment disposition type to Content-Disposition headers Files were downloading with filename "view" instead of the actual filename because the Content-Disposition header was missing the disposition type (attachment/inline). Changed from `filename="..."` to `attachment; filename="..."` in all 4 locations in the /view endpoint to ensure proper filename handling by browsers. This fixes downloads for videos, audio, and other file types served through the /view endpoint. --- server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index d059d3dc9..d7cdc16fe 100644 --- a/server.py +++ b/server.py @@ -486,7 +486,7 @@ class PromptServer(): buffer.seek(0) return web.Response(body=buffer.read(), content_type=f'image/{image_format}', - headers={"Content-Disposition": f"filename=\"{filename}\""}) + headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}) if 'channel' not in request.rel_url.query: channel = 'rgba' @@ -506,7 +506,7 @@ class PromptServer(): buffer.seek(0) return web.Response(body=buffer.read(), content_type='image/png', - headers={"Content-Disposition": f"filename=\"{filename}\""}) + headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}) elif channel == 'a': with Image.open(file) as img: @@ -523,7 +523,7 @@ class PromptServer(): alpha_buffer.seek(0) return web.Response(body=alpha_buffer.read(), content_type='image/png', - headers={"Content-Disposition": f"filename=\"{filename}\""}) + headers={"Content-Disposition": f"attachment; filename=\"{filename}\""}) else: # Get content type from mimetype, defaulting to 'application/octet-stream' content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' @@ -535,7 +535,7 @@ class PromptServer(): return web.FileResponse( file, headers={ - "Content-Disposition": f"filename=\"{filename}\"", + "Content-Disposition": f"attachment; filename=\"{filename}\"", "Content-Type": content_type } )