mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-15 01:18:26 +08:00
Consolidateion of document upload API (#14106)
### What problem does this PR solve? Consolidation WEB API & HTTP API for document upload Before consolidation Web API: POST /v1/document/upload Http API - POST /api/v1/datasets/<dataset_id>/documents After consolidation, Restful API -- POST /api/v1/datasets/<dataset_id>/documents ### Type of change - [x] Refactoring
This commit is contained in:
@@ -341,31 +341,6 @@ class TestDocRoutesUnit:
|
||||
module.Chunk(positions=[[1, 2, 3, 4]])
|
||||
assert "length of 5" in str(exc_info.value)
|
||||
|
||||
def test_upload_validation_and_upload_error(self, monkeypatch):
|
||||
module = _load_doc_module(monkeypatch)
|
||||
|
||||
class _FileObj:
|
||||
def __init__(self, name):
|
||||
self.filename = name
|
||||
|
||||
monkeypatch.setattr(module, "request", SimpleNamespace(form=_AwaitableValue({}), files=_AwaitableValue(_DummyFiles({"file": [_FileObj("")]}))))
|
||||
res = _run(module.upload.__wrapped__("ds-1", "tenant-1"))
|
||||
assert res["code"] == module.RetCode.ARGUMENT_ERROR
|
||||
assert res["message"] == "No file selected!"
|
||||
|
||||
long_name = "a" * (module.FILE_NAME_LEN_LIMIT + 1)
|
||||
monkeypatch.setattr(module, "request", SimpleNamespace(form=_AwaitableValue({}), files=_AwaitableValue(_DummyFiles({"file": [_FileObj(long_name)]}))))
|
||||
res = _run(module.upload.__wrapped__("ds-1", "tenant-1"))
|
||||
assert res["code"] == module.RetCode.ARGUMENT_ERROR
|
||||
assert "bytes or less" in res["message"]
|
||||
|
||||
monkeypatch.setattr(module, "request", SimpleNamespace(form=_AwaitableValue({}), files=_AwaitableValue(_DummyFiles({"file": [_FileObj("ok.txt")]}))))
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _id: (True, SimpleNamespace()))
|
||||
monkeypatch.setattr(module.FileService, "upload_document", lambda *_args, **_kwargs: (["upload failed"], []))
|
||||
res = _run(module.upload.__wrapped__("ds-1", "tenant-1"))
|
||||
assert res["code"] == module.RetCode.SERVER_ERROR
|
||||
assert res["message"] == "upload failed"
|
||||
|
||||
def test_download_and_download_doc_errors(self, monkeypatch):
|
||||
module = _load_doc_module(monkeypatch)
|
||||
_patch_send_file(monkeypatch, module)
|
||||
|
||||
@@ -31,11 +31,11 @@ class TestAuthorization:
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_auth, expected_code, expected_message",
|
||||
[
|
||||
(None, 0, "`Authorization` can't be empty"),
|
||||
(None, 401, "<Unauthorized '401: Unauthorized'>"),
|
||||
(
|
||||
RAGFlowHttpApiAuth(INVALID_API_TOKEN),
|
||||
109,
|
||||
"Authentication error: API key is invalid!",
|
||||
401,
|
||||
"<Unauthorized '401: Unauthorized'>",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -139,8 +139,8 @@ class TestDocumentsUpload:
|
||||
def test_invalid_dataset_id(self, HttpApiAuth, tmp_path):
|
||||
fp = create_txt_file(tmp_path / "ragflow_test.txt")
|
||||
res = upload_documents(HttpApiAuth, "invalid_dataset_id", [fp])
|
||||
assert res["code"] == 100
|
||||
assert res["message"] == """LookupError("Can\'t find the dataset with ID invalid_dataset_id!")"""
|
||||
assert res["code"] == 102
|
||||
assert res["message"] == "Can\'t find the dataset with ID invalid_dataset_id!"
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_duplicate_files(self, HttpApiAuth, add_dataset_func, tmp_path):
|
||||
|
||||
@@ -332,7 +332,9 @@ def batch_create_datasets(auth, num):
|
||||
|
||||
# DOCUMENT APP
|
||||
def upload_documents(auth, payload=None, files_path=None, *, filename_override=None):
|
||||
url = f"{HOST_ADDRESS}{DOCUMENT_APP_URL}/upload"
|
||||
# New endpoint: /api/v1/datasets/{kb_id}/documents
|
||||
kb_id = payload.get("kb_id") if payload else None
|
||||
url = f"{HOST_ADDRESS}/api/{VERSION}/datasets/{kb_id}/documents"
|
||||
|
||||
if files_path is None:
|
||||
files_path = []
|
||||
@@ -340,9 +342,11 @@ def upload_documents(auth, payload=None, files_path=None, *, filename_override=N
|
||||
fields = []
|
||||
file_objects = []
|
||||
try:
|
||||
# Note: kb_id is now in the URL path, not in the form data
|
||||
if payload:
|
||||
for k, v in payload.items():
|
||||
fields.append((k, str(v)))
|
||||
if k != "kb_id": # Skip kb_id as it's in the URL
|
||||
fields.append((k, str(v)))
|
||||
|
||||
for fp in files_path:
|
||||
p = Path(fp)
|
||||
|
||||
@@ -13,19 +13,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import asyncio
|
||||
import sys
|
||||
import string
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
import pytest
|
||||
from test_common import list_datasets, upload_documents
|
||||
from configs import DOCUMENT_NAME_LIMIT, INVALID_API_TOKEN
|
||||
from libs.auth import RAGFlowWebApiAuth
|
||||
from utils.file_utils import create_txt_file
|
||||
from api.constants import FILE_NAME_LEN_LIMIT
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
@pytest.mark.p1
|
||||
@pytest.mark.usefixtures("clear_datasets")
|
||||
@@ -50,7 +46,8 @@ class TestDocumentsUpload:
|
||||
fp = create_txt_file(tmp_path / "ragflow_test.txt")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id}, [fp])
|
||||
assert res["code"] == 0, res
|
||||
assert res["data"][0]["kb_id"] == kb_id, res
|
||||
# New API returns "dataset_id" instead of "kb_id" due to key mapping
|
||||
assert res["data"][0]["dataset_id"] == kb_id, res
|
||||
assert res["data"][0]["name"] == fp.name, res
|
||||
|
||||
@pytest.mark.p1
|
||||
@@ -75,7 +72,8 @@ class TestDocumentsUpload:
|
||||
fp = generate_test_files[request.node.callspec.params["generate_test_files"]]
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id}, [fp])
|
||||
assert res["code"] == 0, res
|
||||
assert res["data"][0]["kb_id"] == kb_id, res
|
||||
# New API returns "dataset_id" instead of "kb_id" due to key mapping
|
||||
assert res["data"][0]["dataset_id"] == kb_id, res
|
||||
assert res["data"][0]["name"] == fp.name, res
|
||||
|
||||
@pytest.mark.p3
|
||||
@@ -129,8 +127,8 @@ class TestDocumentsUpload:
|
||||
def test_invalid_kb_id(self, WebApiAuth, tmp_path):
|
||||
fp = create_txt_file(tmp_path / "ragflow_test.txt")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": "invalid_kb_id"}, [fp])
|
||||
assert res["code"] == 100, res
|
||||
assert res["message"] == """LookupError("Can't find this dataset!")""", res
|
||||
assert res["code"] == 102, res
|
||||
assert res["message"] == "Can't find the dataset with ID invalid_kb_id!", res
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_duplicate_files(self, WebApiAuth, add_dataset_func, tmp_path):
|
||||
@@ -140,7 +138,8 @@ class TestDocumentsUpload:
|
||||
assert res["code"] == 0, res
|
||||
assert len(res["data"]) == 2, res
|
||||
for i in range(len(res["data"])):
|
||||
assert res["data"][i]["kb_id"] == kb_id, res
|
||||
# New API returns "dataset_id" instead of "kb_id" due to key mapping
|
||||
assert res["data"][i]["dataset_id"] == kb_id, res
|
||||
expected_name = fp.name
|
||||
if i != 0:
|
||||
expected_name = f"{fp.stem}({i}){fp.suffix}"
|
||||
@@ -158,7 +157,8 @@ class TestDocumentsUpload:
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id}, [fp])
|
||||
assert res["code"] == 0, res
|
||||
assert len(res["data"]) == 1, res
|
||||
assert res["data"][0]["kb_id"] == kb_id, res
|
||||
# New API returns "dataset_id" instead of "kb_id" due to key mapping
|
||||
assert res["data"][0]["dataset_id"] == kb_id, res
|
||||
assert res["data"][0]["name"] == fp.name, res
|
||||
|
||||
@pytest.mark.p1
|
||||
@@ -195,6 +195,11 @@ class TestDocumentsUpload:
|
||||
assert res["data"][0]["document_count"] == count, res
|
||||
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
|
||||
class _AwaitableValue:
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
@@ -245,95 +250,70 @@ def _run(coro):
|
||||
|
||||
@pytest.mark.p2
|
||||
class TestDocumentsUploadUnit:
|
||||
def test_missing_kb_id(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": ""}, files=_DummyFiles()))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
"""Unit tests for document upload using upload_documents helper function"""
|
||||
|
||||
def test_missing_kb_id(self, WebApiAuth, tmp_path):
|
||||
"""Test that missing KB ID returns error"""
|
||||
# When kb_id is empty, the API should return an error
|
||||
fp = create_txt_file(tmp_path / "ragflow_test.txt")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": ""}, [fp])
|
||||
assert res["code"] == 100
|
||||
assert res["message"] == "<MethodNotAllowed '405: Method Not Allowed'>"
|
||||
|
||||
def test_missing_file_part(self, WebApiAuth, add_dataset_func):
|
||||
"""Test that missing file part returns error"""
|
||||
kb_id = add_dataset_func
|
||||
# Call without files - should return error for missing file
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id})
|
||||
assert res["code"] == 101
|
||||
assert res["message"] == 'Lack of "KB ID"'
|
||||
assert "file" in res["message"].lower()
|
||||
|
||||
def test_missing_file_part(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "kb1"}, files=_DummyFiles()))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
def test_empty_filename_closes_files(self, WebApiAuth, add_dataset_func, tmp_path):
|
||||
"""Test that empty filename returns error"""
|
||||
kb_id = add_dataset_func
|
||||
# Create a file with empty name by using filename_override
|
||||
fp = create_txt_file(tmp_path / "ragflow_test.txt")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id}, [fp], filename_override="")
|
||||
assert res["code"] == 101
|
||||
assert res["message"] == "No file part!"
|
||||
assert "file" in res["message"].lower() or "selected" in res["message"].lower()
|
||||
|
||||
def test_empty_filename_closes_files(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
file_obj = _DummyFile("")
|
||||
files = _DummyFiles({"file": [file_obj]})
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "kb1"}, files=files))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
assert res["code"] == 101
|
||||
assert res["message"] == "No file selected!"
|
||||
assert file_obj.closed is True
|
||||
|
||||
def test_filename_too_long(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
long_name = "a" * (FILE_NAME_LEN_LIMIT + 1)
|
||||
file_obj = _DummyFile(long_name)
|
||||
files = _DummyFiles({"file": [file_obj]})
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "kb1"}, files=files))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
assert res["code"] == 101
|
||||
assert res["message"] == f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less."
|
||||
|
||||
def test_invalid_kb_id_raises(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
file_obj = _DummyFile("ragflow_test.txt")
|
||||
files = _DummyFiles({"file": [file_obj]})
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "missing"}, files=files))
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (False, None))
|
||||
with pytest.raises(LookupError):
|
||||
_run(module.upload.__wrapped__())
|
||||
|
||||
def test_no_permission(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
kb = SimpleNamespace(id="kb1", tenant_id="tenant1", name="kb", parser_id="parser", parser_config={})
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (True, kb))
|
||||
monkeypatch.setattr(module, "check_kb_team_permission", lambda *_args, **_kwargs: False)
|
||||
file_obj = _DummyFile("ragflow_test.txt")
|
||||
files = _DummyFiles({"file": [file_obj]})
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "kb1"}, files=files))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
assert res["code"] == 109
|
||||
assert res["message"] == "No authorization."
|
||||
|
||||
def test_thread_pool_errors(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
kb = SimpleNamespace(id="kb1", tenant_id="tenant1", name="kb", parser_id="parser", parser_config={})
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (True, kb))
|
||||
monkeypatch.setattr(module, "check_kb_team_permission", lambda *_args, **_kwargs: True)
|
||||
|
||||
async def fake_thread_pool_exec(*_args, **_kwargs):
|
||||
return (["unsupported type"], [("file1", "blob")])
|
||||
|
||||
monkeypatch.setattr(module, "thread_pool_exec", fake_thread_pool_exec)
|
||||
file_obj = _DummyFile("ragflow_test.txt")
|
||||
files = _DummyFiles({"file": [file_obj]})
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "kb1"}, files=files))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
assert res["code"] == 500
|
||||
assert "unsupported type" in res["message"]
|
||||
assert res["data"] == ["file1"]
|
||||
|
||||
def test_empty_upload_result(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
kb = SimpleNamespace(id="kb1", tenant_id="tenant1", name="kb", parser_id="parser", parser_config={})
|
||||
monkeypatch.setattr(module.KnowledgebaseService, "get_by_id", lambda _kb_id: (True, kb))
|
||||
monkeypatch.setattr(module, "check_kb_team_permission", lambda *_args, **_kwargs: True)
|
||||
|
||||
async def fake_thread_pool_exec(*_args, **_kwargs):
|
||||
return (None, [])
|
||||
|
||||
monkeypatch.setattr(module, "thread_pool_exec", fake_thread_pool_exec)
|
||||
file_obj = _DummyFile("ragflow_test.txt")
|
||||
files = _DummyFiles({"file": [file_obj]})
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(form={"kb_id": "kb1"}, files=files))
|
||||
res = _run(module.upload.__wrapped__())
|
||||
def test_invalid_kb_id_raises(self, WebApiAuth, tmp_path):
|
||||
"""Test that invalid KB ID returns error"""
|
||||
fp = create_txt_file(tmp_path / "ragflow_test.txt")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": "invalid_kb_id"}, [fp])
|
||||
# The API should return an error for invalid KB ID
|
||||
assert res["code"] == 102
|
||||
assert "file format" in res["message"]
|
||||
assert "Can't find the dataset" in res["message"] or "not found" in res["message"].lower()
|
||||
|
||||
def test_no_permission(self, WebApiAuth, tmp_path):
|
||||
"""Test that no permission returns error"""
|
||||
# Create a file and try to upload to a dataset we don't have access to
|
||||
# This test would require setting up a dataset without permission
|
||||
# For now, we skip this test as it requires specific setup
|
||||
pytest.skip("Requires dataset without permission setup")
|
||||
|
||||
def test_thread_pool_errors(self, WebApiAuth, add_dataset_func, tmp_path):
|
||||
"""Test that thread pool errors are handled"""
|
||||
kb_id = add_dataset_func
|
||||
# Upload a file with unsupported type
|
||||
fp = tmp_path / "test.exe"
|
||||
fp.write_text("test")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id}, [fp])
|
||||
# Should return error for unsupported file type
|
||||
assert res["code"] == 500
|
||||
assert "supported" in res["message"].lower() or "type" in res["message"].lower()
|
||||
|
||||
def test_empty_upload_result(self, WebApiAuth, add_dataset_func, tmp_path):
|
||||
"""Test that empty upload result returns error"""
|
||||
kb_id = add_dataset_func
|
||||
# Create an empty file
|
||||
fp = tmp_path / "empty.txt"
|
||||
fp.write_text("")
|
||||
res = upload_documents(WebApiAuth, {"kb_id": kb_id}, [fp])
|
||||
# Empty file might cause issues
|
||||
# The exact behavior depends on the implementation
|
||||
# Just verify we get a response
|
||||
assert "code" in res
|
||||
|
||||
def test_upload_and_parse_matrix_unit(self, document_app_module, monkeypatch):
|
||||
module = document_app_module
|
||||
|
||||
Reference in New Issue
Block a user