mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 16:12:19 +08:00
fix: Add fallback to bundled images for profile pictures (#10758)
* add profile picture nullable validation * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * add tests profile pic * [autofix.ci] apply automated fixes * ruff fixes * add validation to images read file compnent * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * fix ruff * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9b9e353842
commit
677f87f99b
@ -134,8 +134,8 @@ async def download_profile_picture(
|
||||
):
|
||||
"""Download profile picture from local filesystem.
|
||||
|
||||
Profile pictures are always stored locally in config_dir/profile_pictures/,
|
||||
regardless of the storage_type setting (local, s3, etc.).
|
||||
Profile pictures are first looked up in config_dir/profile_pictures/,
|
||||
then fallback to the package's bundled profile_pictures directory.
|
||||
"""
|
||||
try:
|
||||
extension = file_name.split(".")[-1]
|
||||
@ -143,8 +143,15 @@ async def download_profile_picture(
|
||||
config_path = Path(config_dir) # type: ignore[arg-type]
|
||||
file_path = config_path / "profile_pictures" / folder_name / file_name
|
||||
|
||||
# Fallback to package bundled profile pictures if not found in config_dir
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Profile picture {folder_name}/{file_name} not found")
|
||||
from langflow.initial_setup import setup
|
||||
|
||||
package_path = Path(setup.__file__).parent / "profile_pictures" / folder_name / file_name
|
||||
if package_path.exists():
|
||||
file_path = package_path
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Profile picture {folder_name}/{file_name} not found")
|
||||
|
||||
content_type = build_content_type_from_extension(extension)
|
||||
# Read file directly from local filesystem using async file operations
|
||||
@ -163,8 +170,8 @@ async def list_profile_pictures(
|
||||
):
|
||||
"""List profile pictures from local filesystem.
|
||||
|
||||
Profile pictures are always stored locally in config_dir/profile_pictures/,
|
||||
regardless of the storage_type setting (local, s3, etc.).
|
||||
Profile pictures are first looked up in config_dir/profile_pictures/,
|
||||
then fallback to the package's bundled profile_pictures directory.
|
||||
"""
|
||||
try:
|
||||
config_dir = settings_service.settings.config_dir
|
||||
@ -173,9 +180,19 @@ async def list_profile_pictures(
|
||||
people_path = config_path / "profile_pictures" / "People"
|
||||
space_path = config_path / "profile_pictures" / "Space"
|
||||
|
||||
# List files directly from local filesystem - bundled with the container
|
||||
# List files directly from local filesystem
|
||||
people = [f.name for f in people_path.iterdir() if f.is_file()] if people_path.exists() else []
|
||||
space = [f.name for f in space_path.iterdir() if f.is_file()] if space_path.exists() else []
|
||||
|
||||
# Fallback to package bundled profile pictures if config_dir is empty
|
||||
if not people and not space:
|
||||
from langflow.initial_setup import setup
|
||||
|
||||
package_base = Path(setup.__file__).parent / "profile_pictures"
|
||||
people_path = package_base / "People"
|
||||
space_path = package_base / "Space"
|
||||
people = [f.name for f in people_path.iterdir() if f.is_file()] if people_path.exists() else []
|
||||
space = [f.name for f in space_path.iterdir() if f.is_file()] if space_path.exists() else []
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@ -487,3 +487,257 @@ async def test_profile_pictures_different_file_types(setup_profile_pictures, fil
|
||||
assert response.status_code == 200
|
||||
# All profile pictures should be SVGs
|
||||
assert "image/svg+xml" in response.headers["content-type"]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for package fallback functionality (PR #10758)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def empty_config_dir(monkeypatch):
|
||||
"""Fixture that sets up an empty config directory without profile pictures.
|
||||
|
||||
This simulates the scenario where copy_profile_pictures() was never called,
|
||||
so the endpoints should fallback to the package bundled profile pictures.
|
||||
"""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
config_path = Path(temp_dir)
|
||||
|
||||
# Create an empty profile_pictures directory (no files inside)
|
||||
(config_path / "profile_pictures").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Override the config_dir setting BEFORE app initialization
|
||||
monkeypatch.setenv("LANGFLOW_CONFIG_DIR", str(config_path))
|
||||
|
||||
yield config_path
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
async def test_download_profile_picture_fallback_to_package(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test that profile pictures fallback to package when not in config_dir.
|
||||
|
||||
This tests the core fix from PR #10758 - when profile pictures don't exist
|
||||
in config_dir, they should be served from the package's bundled directory.
|
||||
"""
|
||||
# The 046-rocket.svg should be found in the package's bundled directory
|
||||
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
|
||||
assert response.status_code == 200, (
|
||||
f"Expected 200, got {response.status_code}. Fallback to package profile pictures should work."
|
||||
)
|
||||
|
||||
# Verify content type
|
||||
assert "image/svg+xml" in response.headers["content-type"]
|
||||
|
||||
# Verify SVG content
|
||||
content = response.content
|
||||
assert b"<svg" in content
|
||||
assert b"</svg>" in content
|
||||
|
||||
|
||||
async def test_list_profile_pictures_fallback_to_package(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test that list endpoint fallbacks to package when config_dir is empty.
|
||||
|
||||
This tests the list fallback from PR #10758 - when config_dir has no
|
||||
profile pictures, it should list from the package's bundled directory.
|
||||
"""
|
||||
response = await files_client.get("api/v1/files/profile_pictures/list")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "files" in data
|
||||
files = data["files"]
|
||||
|
||||
# Should have files from the package
|
||||
assert len(files) > 0, "Should have profile pictures from package fallback"
|
||||
assert any(f.startswith("Space/") for f in files), "Should have Space profile pictures"
|
||||
assert any(f.startswith("People/") for f in files), "Should have People profile pictures"
|
||||
|
||||
# The bundled rocket should be available
|
||||
assert "Space/046-rocket.svg" in files
|
||||
|
||||
|
||||
async def test_download_profile_picture_not_found_in_both_locations(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test 404 when profile picture doesn't exist in config_dir OR package.
|
||||
|
||||
This ensures the fallback logic correctly returns 404 when the file
|
||||
is not found in either location.
|
||||
"""
|
||||
response = await files_client.get("api/v1/files/profile_pictures/Space/nonexistent-file-xyz.svg")
|
||||
assert response.status_code == 404
|
||||
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
assert "Space/nonexistent-file-xyz.svg" in data["detail"]
|
||||
|
||||
|
||||
async def test_download_profile_picture_invalid_folder(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test 404 when using an invalid folder name.
|
||||
|
||||
Only 'People' and 'Space' folders should exist in the package.
|
||||
"""
|
||||
response = await files_client.get("api/v1/files/profile_pictures/InvalidFolder/file.svg")
|
||||
assert response.status_code == 404
|
||||
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
|
||||
|
||||
async def test_download_profile_picture_config_dir_takes_precedence(setup_profile_pictures, files_client):
|
||||
"""Test that config_dir profile pictures take precedence over package.
|
||||
|
||||
When a file exists in both config_dir and package, the config_dir
|
||||
version should be served.
|
||||
"""
|
||||
config_path = setup_profile_pictures
|
||||
|
||||
# Create a custom rocket SVG in config_dir with identifiable content
|
||||
custom_svg = b'<svg xmlns="http://www.w3.org/2000/svg"><text>CUSTOM_CONFIG_DIR_VERSION</text></svg>'
|
||||
space_dir = config_path / "profile_pictures" / "Space"
|
||||
space_dir.mkdir(parents=True, exist_ok=True)
|
||||
(space_dir / "046-rocket.svg").write_bytes(custom_svg)
|
||||
|
||||
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Should get the config_dir version, not the package version
|
||||
content = response.content
|
||||
assert b"CUSTOM_CONFIG_DIR_VERSION" in content
|
||||
|
||||
|
||||
async def test_list_profile_pictures_config_dir_takes_precedence(setup_profile_pictures, files_client):
|
||||
"""Test that config_dir listing takes precedence over package.
|
||||
|
||||
When config_dir has profile pictures, they should be listed instead
|
||||
of the package's bundled ones.
|
||||
"""
|
||||
config_path = setup_profile_pictures
|
||||
|
||||
# Ensure we have a file in config_dir
|
||||
space_dir = config_path / "profile_pictures" / "Space"
|
||||
space_dir.mkdir(parents=True, exist_ok=True)
|
||||
(space_dir / "custom-test-file.svg").write_bytes(b"<svg></svg>")
|
||||
|
||||
response = await files_client.get("api/v1/files/profile_pictures/list")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
files = data["files"]
|
||||
|
||||
# Should include our custom file from config_dir
|
||||
assert "Space/custom-test-file.svg" in files
|
||||
|
||||
|
||||
async def test_download_profile_picture_path_traversal_attempt(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test that path traversal attacks are prevented.
|
||||
|
||||
Attempting to access files outside the profile_pictures directory
|
||||
using '../' should not work.
|
||||
"""
|
||||
# Try path traversal to access parent directories
|
||||
response = await files_client.get("api/v1/files/profile_pictures/../../../etc/passwd")
|
||||
# Should either be 404 (file not found) or the path should be sanitized
|
||||
assert response.status_code in [404, 500]
|
||||
|
||||
|
||||
async def test_download_profile_picture_special_characters_in_filename(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test handling of special characters in filename.
|
||||
|
||||
Filenames with spaces or special characters should be handled properly.
|
||||
"""
|
||||
# Test with URL-encoded space (the real file has a space: "042-space shuttle.svg")
|
||||
response = await files_client.get("api/v1/files/profile_pictures/Space/042-space%20shuttle.svg")
|
||||
# Should work if the file exists with that name, or 404 if not
|
||||
assert response.status_code in [200, 404]
|
||||
|
||||
|
||||
async def test_list_profile_pictures_empty_response_format(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test that the list response format is correct even with fallback.
|
||||
|
||||
The response should always have the correct format: {"files": [...]}
|
||||
"""
|
||||
response = await files_client.get("api/v1/files/profile_pictures/list")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Verify response structure
|
||||
assert isinstance(data, dict)
|
||||
assert "files" in data
|
||||
assert isinstance(data["files"], list)
|
||||
|
||||
# Verify file path format (should be "Folder/filename")
|
||||
for file_path in data["files"]:
|
||||
assert "/" in file_path, f"File path should contain '/': {file_path}"
|
||||
folder, filename = file_path.split("/", 1)
|
||||
assert folder in ["People", "Space"], f"Invalid folder: {folder}"
|
||||
assert len(filename) > 0, "Filename should not be empty"
|
||||
|
||||
|
||||
async def test_download_profile_picture_content_is_valid_svg(empty_config_dir, files_client): # noqa: ARG001
|
||||
"""Test that downloaded profile pictures are valid SVG files.
|
||||
|
||||
This ensures the fallback serves actual SVG content, not corrupted data.
|
||||
"""
|
||||
# Download the rocket from package fallback
|
||||
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
|
||||
assert response.status_code == 200
|
||||
|
||||
content = response.content
|
||||
|
||||
# Verify it's valid XML/SVG
|
||||
assert content.startswith((b"<", b"<?xml")), "Should start with XML/SVG tag"
|
||||
assert b"<svg" in content.lower(), "Should contain svg tag"
|
||||
assert b"</svg>" in content.lower(), "Should have closing svg tag"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def partial_config_dir(monkeypatch):
|
||||
"""Fixture that sets up a config directory with only People folder (no Space).
|
||||
|
||||
This tests the edge case where config_dir is partially populated.
|
||||
"""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
config_path = Path(temp_dir)
|
||||
|
||||
# Create only People directory with a file
|
||||
people_dir = config_path / "profile_pictures" / "People"
|
||||
people_dir.mkdir(parents=True, exist_ok=True)
|
||||
(people_dir / "test-person.svg").write_bytes(b"<svg><circle/></svg>")
|
||||
|
||||
# Note: Space directory is NOT created intentionally
|
||||
|
||||
# Override the config_dir setting BEFORE app initialization
|
||||
monkeypatch.setenv("LANGFLOW_CONFIG_DIR", str(config_path))
|
||||
|
||||
yield config_path
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
async def test_profile_pictures_fallback_with_partial_config_dir(partial_config_dir, files_client): # noqa: ARG001
|
||||
"""Test fallback when config_dir has only People folder but not Space.
|
||||
|
||||
This is an edge case where config_dir is partially populated.
|
||||
"""
|
||||
# For list: since we have at least one file in People, it should NOT fallback completely
|
||||
# The current implementation only falls back if BOTH people AND space are empty
|
||||
response = await files_client.get("api/v1/files/profile_pictures/list")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
files = data["files"]
|
||||
|
||||
# Should have our People file from config_dir
|
||||
assert "People/test-person.svg" in files
|
||||
|
||||
# For download: Space files should still work via fallback to package
|
||||
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
|
||||
assert response.status_code == 200, "Space files should fallback to package"
|
||||
|
||||
Reference in New Issue
Block a user