feat: Unified File Management API (#6100)
* feat: FIrst pass at file management API * [autofix.ci] apply automated fixes * Add delete and edit endpoints * [autofix.ci] apply automated fixes * Add file size and duplicate name handling * Ensure the File model has a unique name * Ensure count is before extension * [autofix.ci] apply automated fixes * Add the correct path to the return * Added function to handle list of paths in File component * [autofix.ci] apply automated fixes * Update input_mixin.py * Refactor to a v2 endpoint * Add unit tests * Update test_files.py * Update frontend.ts * [autofix.ci] apply automated fixes * Remove extension from name * Cast the string type for like * Update files.py * Update base.py * Update base.py --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
This commit is contained in:
parent
cc3417bec7
commit
28e07be870
18 changed files with 635 additions and 15 deletions
0
src/backend/tests/unit/api/v2/__init__.py
Normal file
0
src/backend/tests/unit/api/v2/__init__.py
Normal file
209
src/backend/tests/unit/api/v2/test_files.py
Normal file
209
src/backend/tests/unit/api/v2/test_files.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import asyncio
|
||||
import tempfile
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
# we need to import tmpdir
|
||||
import anyio
|
||||
import pytest
|
||||
from asgi_lifespan import LifespanManager
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from langflow.main import create_app
|
||||
from langflow.services.auth.utils import get_password_hash
|
||||
from langflow.services.database.models.api_key.model import ApiKey
|
||||
from langflow.services.database.models.user.model import User, UserRead
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.deps import get_db_service
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlmodel import select
|
||||
|
||||
from tests.conftest import _delete_transactions_and_vertex_builds
|
||||
|
||||
|
||||
@pytest.fixture(name="files_created_api_key")
|
||||
async def files_created_api_key(files_client, files_active_user): # noqa: ARG001
|
||||
hashed = get_password_hash("random_key")
|
||||
api_key = ApiKey(
|
||||
name="files_created_api_key",
|
||||
user_id=files_active_user.id,
|
||||
api_key="random_key",
|
||||
hashed_api_key=hashed,
|
||||
)
|
||||
db_manager = get_db_service()
|
||||
async with session_getter(db_manager) as session:
|
||||
stmt = select(ApiKey).where(ApiKey.api_key == api_key.api_key)
|
||||
if existing_api_key := (await session.exec(stmt)).first():
|
||||
yield existing_api_key
|
||||
return
|
||||
session.add(api_key)
|
||||
await session.commit()
|
||||
await session.refresh(api_key)
|
||||
yield api_key
|
||||
# Clean up
|
||||
await session.delete(api_key)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@pytest.fixture(name="files_active_user")
|
||||
async def files_active_user(files_client): # noqa: ARG001
|
||||
db_manager = get_db_service()
|
||||
async with db_manager.with_session() as session:
|
||||
user = User(
|
||||
username="files_active_user",
|
||||
password=get_password_hash("testpassword"),
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
)
|
||||
stmt = select(User).where(User.username == user.username)
|
||||
if active_user := (await session.exec(stmt)).first():
|
||||
user = active_user
|
||||
else:
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
user = UserRead.model_validate(user, from_attributes=True)
|
||||
yield user
|
||||
# Clean up
|
||||
# Now cleanup transactions, vertex_build
|
||||
async with db_manager.with_session() as session:
|
||||
user = await session.get(User, user.id, options=[selectinload(User.flows)])
|
||||
await _delete_transactions_and_vertex_builds(session, user.flows)
|
||||
await session.delete(user)
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def max_file_size_upload_fixture(monkeypatch):
|
||||
monkeypatch.setenv("LANGFLOW_MAX_FILE_SIZE_UPLOAD", "1")
|
||||
yield
|
||||
monkeypatch.undo()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def max_file_size_upload_10mb_fixture(monkeypatch):
|
||||
monkeypatch.setenv("LANGFLOW_MAX_FILE_SIZE_UPLOAD", "10")
|
||||
yield
|
||||
monkeypatch.undo()
|
||||
|
||||
|
||||
@pytest.fixture(name="files_client")
|
||||
async def files_client_fixture(
|
||||
monkeypatch,
|
||||
request,
|
||||
):
|
||||
# Set the database url to a test database
|
||||
if "noclient" in request.keywords:
|
||||
yield
|
||||
else:
|
||||
|
||||
def init_app():
|
||||
db_dir = tempfile.mkdtemp()
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
from langflow.services.manager import service_manager
|
||||
|
||||
service_manager.factories.clear()
|
||||
service_manager.services.clear() # Clear the services cache
|
||||
app = create_app()
|
||||
return app, db_path
|
||||
|
||||
app, db_path = await asyncio.to_thread(init_app)
|
||||
|
||||
async with (
|
||||
LifespanManager(app, startup_timeout=None, shutdown_timeout=None) as manager,
|
||||
AsyncClient(transport=ASGITransport(app=manager.app), base_url="http://testserver/") as client,
|
||||
):
|
||||
yield client
|
||||
# app.dependency_overrides.clear()
|
||||
monkeypatch.undo()
|
||||
# clear the temp db
|
||||
with suppress(FileNotFoundError):
|
||||
await anyio.Path(db_path).unlink()
|
||||
|
||||
|
||||
async def test_upload_file(files_client, files_created_api_key):
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("test.txt", b"test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201, f"Expected 201, got {response.status_code}: {response.json()}"
|
||||
|
||||
response_json = response.json()
|
||||
assert "id" in response_json
|
||||
|
||||
|
||||
async def test_download_file(files_client, files_created_api_key):
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
# First upload a file
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("test.txt", b"test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
upload_response = response.json()
|
||||
|
||||
# Then try to download it
|
||||
response = await files_client.get(f"api/v2/files/{upload_response['id']}", headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"test content"
|
||||
|
||||
|
||||
async def test_list_files(files_client, files_created_api_key):
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
# First upload a file
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("test.txt", b"test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Then list the files
|
||||
response = await files_client.get("api/v2/files", headers=headers)
|
||||
assert response.status_code == 200
|
||||
files = response.json()
|
||||
assert len(files) == 1
|
||||
|
||||
|
||||
async def test_delete_file(files_client, files_created_api_key):
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("test.txt", b"test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
upload_response = response.json()
|
||||
|
||||
response = await files_client.delete(f"api/v2/files/{upload_response['id']}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "File deleted successfully"}
|
||||
|
||||
|
||||
async def test_edit_file(files_client, files_created_api_key):
|
||||
headers = {"x-api-key": files_created_api_key.api_key}
|
||||
|
||||
# First upload a file
|
||||
response = await files_client.post(
|
||||
"api/v2/files",
|
||||
files={"file": ("test.txt", b"test content")},
|
||||
headers=headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
upload_response = response.json()
|
||||
|
||||
# Then list the files
|
||||
response = await files_client.put(f"api/v2/files/{upload_response['id']}?name=potato.txt", headers=headers)
|
||||
assert response.status_code == 200
|
||||
file = response.json()
|
||||
assert file["name"] == "potato.txt"
|
||||
Loading…
Add table
Add a link
Reference in a new issue