Refa: files /file API to RESTFul style (#13741)

### What problem does this PR solve?

Files /file API to RESTFul style.

### Type of change

- [x] Documentation Update
- [x] Refactoring

---------

Co-authored-by: writinwaters <cai.keith@gmail.com>
Co-authored-by: Liu An <asiro@qq.com>
This commit is contained in:
Yongteng Lei
2026-03-24 19:24:41 +08:00
committed by GitHub
parent 10a36d6443
commit 3d10e2075c
23 changed files with 2118 additions and 3553 deletions

View File

@@ -101,6 +101,20 @@ def document_app_module(monkeypatch):
deepdoc_html_module.RAGFlowHtmlParser = _StubHtmlParser
monkeypatch.setitem(sys.modules, "deepdoc.parser.html_parser", deepdoc_html_module)
deepdoc_mineru_module = ModuleType("deepdoc.parser.mineru_parser")
class _StubMinerUParser:
pass
deepdoc_mineru_module.MinerUParser = _StubMinerUParser
monkeypatch.setitem(sys.modules, "deepdoc.parser.mineru_parser", deepdoc_mineru_module)
deepdoc_paddleocr_module = ModuleType("deepdoc.parser.paddleocr_parser")
class _StubPaddleOCRParser:
pass
deepdoc_paddleocr_module.PaddleOCRParser = _StubPaddleOCRParser
monkeypatch.setitem(sys.modules, "deepdoc.parser.paddleocr_parser", deepdoc_paddleocr_module)
monkeypatch.setitem(sys.modules, "xgboost", ModuleType("xgboost"))
stub_apps = ModuleType("api.apps")

View File

@@ -1,5 +1,5 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -225,9 +225,10 @@ class _DummyFile:
class _DummyRequest:
def __init__(self, form=None, files=None):
def __init__(self, form=None, files=None, args=None):
self._form = form or {}
self._files = files or _DummyFiles()
self.args = args or {}
@property
def form(self):

View File

@@ -0,0 +1,139 @@
#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
from pathlib import Path
import importlib.util
import sys
from types import ModuleType
import pytest
class _AwaitableValue:
def __init__(self, value):
self._value = value
def __await__(self):
async def _co():
return self._value
return _co().__await__()
class _DummyFiles(dict):
def getlist(self, key):
value = self.get(key, [])
if isinstance(value, list):
return value
return [value]
class _DummyFile:
def __init__(self, filename):
self.filename = filename
class _DummyRequest:
def __init__(self, *, files=None, args=None):
self._files = files or _DummyFiles()
self.args = args or {}
@property
def files(self):
return _AwaitableValue(self._files)
def _run(coro):
return asyncio.run(coro)
def _load_document_app_module(monkeypatch):
repo_root = Path(__file__).resolve().parents[4]
common_mod = ModuleType("common")
common_mod.bulk_upload_documents = lambda *_args, **_kwargs: []
common_mod.delete_document = lambda *_args, **_kwargs: None
common_mod.list_documents = lambda *_args, **_kwargs: {"data": {"docs": []}}
monkeypatch.setitem(sys.modules, "common", common_mod)
module_path = repo_root / "test" / "testcases" / "test_web_api" / "test_document_app" / "conftest.py"
spec = importlib.util.spec_from_file_location("test_document_app_unit_conftest", module_path)
module = importlib.util.module_from_spec(spec)
sys.modules["test_document_app_unit_conftest"] = module
spec.loader.exec_module(module)
return module.document_app_module.__wrapped__(monkeypatch)
@pytest.mark.p2
def test_upload_info_rejects_mixed_inputs(monkeypatch):
module = _load_document_app_module(monkeypatch)
files = _DummyFiles({"file": [_DummyFile("a.txt")]})
monkeypatch.setattr(module, "request", _DummyRequest(files=files, args={"url": "https://example.com/a.txt"}))
res = _run(module.upload_info())
assert res["code"] == module.RetCode.BAD_REQUEST
assert "not both" in res["message"]
@pytest.mark.p2
def test_upload_info_requires_file_or_url(monkeypatch):
module = _load_document_app_module(monkeypatch)
monkeypatch.setattr(module, "request", _DummyRequest(files=_DummyFiles()))
res = _run(module.upload_info())
assert res["code"] == module.RetCode.BAD_REQUEST
assert "Missing input" in res["message"]
@pytest.mark.p2
def test_upload_info_supports_url_single_and_multiple_files(monkeypatch):
module = _load_document_app_module(monkeypatch)
captured = []
def fake_upload_info(user_id, file_obj, url=None):
captured.append((user_id, getattr(file_obj, "filename", None), url))
if url is not None:
return {"kind": "url", "value": url}
return {"kind": "file", "value": file_obj.filename}
monkeypatch.setattr(module.FileService, "upload_info", fake_upload_info)
monkeypatch.setattr(module, "request", _DummyRequest(files=_DummyFiles(), args={"url": "https://example.com/a.txt"}))
res = _run(module.upload_info())
assert res["code"] == 0
assert res["data"] == {"kind": "url", "value": "https://example.com/a.txt"}
monkeypatch.setattr(module, "request", _DummyRequest(files=_DummyFiles({"file": _DummyFile("single.txt")})))
res = _run(module.upload_info())
assert res["code"] == 0
assert res["data"] == {"kind": "file", "value": "single.txt"}
monkeypatch.setattr(
module,
"request",
_DummyRequest(files=_DummyFiles({"file": [_DummyFile("a.txt"), _DummyFile("b.txt")]})),
)
res = _run(module.upload_info())
assert res["code"] == 0
assert res["data"] == [
{"kind": "file", "value": "a.txt"},
{"kind": "file", "value": "b.txt"},
]
assert captured == [
("user-1", None, "https://example.com/a.txt"),
("user-1", "single.txt", None),
("user-1", "a.txt", None),
("user-1", "b.txt", None),
]

View File

@@ -143,6 +143,10 @@ def _load_file2document_module(monkeypatch):
def get_by_id(_file_id):
return True, _DummyFile(_file_id, _FileType.DOC.value)
@staticmethod
def get_parser(_file_type, _file_name, parser_id):
return parser_id
file_service_mod.FileService = _StubFileService
monkeypatch.setitem(sys.modules, "api.db.services.file_service", file_service_mod)
services_pkg.file_service = file_service_mod
@@ -284,7 +288,14 @@ def test_convert_branch_matrix_unit(monkeypatch):
"get_by_id",
lambda _file_id: (True, _DummyFile("inner-1", module.FileType.DOC.value, name="inner.txt", location="inner.loc", size=2)),
)
monkeypatch.setattr(module.DocumentService, "insert", lambda _payload: SimpleNamespace(id="doc-new"))
inserted = {}
def _insert(payload):
inserted.update(payload)
return SimpleNamespace(id="doc-new")
monkeypatch.setattr(module.DocumentService, "insert", _insert)
monkeypatch.setattr(module.FileService, "get_parser", lambda _ft, _name, _parser_id: "picked-parser")
monkeypatch.setattr(
module.File2DocumentService,
"insert",
@@ -293,6 +304,8 @@ def test_convert_branch_matrix_unit(monkeypatch):
res = _run(module.convert())
assert res["code"] == 0
assert res["data"] == [{"file_id": "inner-1", "document_id": "doc-new"}]
assert inserted["parser_id"] == "picked-parser"
assert inserted["pipeline_id"] == "p1"
req_state["file_ids"] = ["f1"]
monkeypatch.setattr(