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

@@ -1,5 +1,5 @@
#
# Copyright 2024 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.
@@ -765,7 +765,6 @@ async def download_attachment(attachment_id):
@login_required
@validate_request("doc_id")
async def change_parser():
req = await get_request_json()
if not DocumentService.accessible(req["doc_id"], current_user.id):
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
@@ -983,10 +982,34 @@ async def set_meta():
@manager.route("/upload_info", methods=["POST"]) # noqa: F821
@login_required
async def upload_info():
files = await request.files
file = files["file"] if files and files.get("file") else None
file_objs = files.getlist("file") if files and files.get("file") else []
url = request.args.get("url")
if file_objs and url:
return get_json_result(
data=False,
message="Provide either multipart file(s) or ?url=..., not both.",
code=RetCode.BAD_REQUEST,
)
if not file_objs and not url:
return get_json_result(
data=False,
message="Missing input: provide multipart file(s) or url",
code=RetCode.BAD_REQUEST,
)
try:
return get_json_result(data=FileService.upload_info(current_user.id, file, request.args.get("url")))
if url and not file_objs:
return get_json_result(data=FileService.upload_info(current_user.id, None, url))
if len(file_objs) == 1:
return get_json_result(data=FileService.upload_info(current_user.id, file_objs[0], None))
results = [FileService.upload_info(current_user.id, f, None) for f in file_objs]
return get_json_result(data=results)
except Exception as e:
return server_error_response(e)

View File

@@ -1,5 +1,5 @@
#
# Copyright 2024 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.
@@ -77,7 +77,7 @@ async def convert():
doc = DocumentService.insert({
"id": get_uuid(),
"kb_id": kb.id,
"parser_id": kb.parser_id,
"parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
"pipeline_id": kb.pipeline_id,
"parser_config": kb.parser_config,
"created_by": current_user.id,

View File

@@ -1,464 +1,464 @@
# #
# # Copyright 2024 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 logging
# import os
# import pathlib
# import re
# from quart import request, make_response
# from api.apps import login_required, current_user
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
# from api.common.check_team_permission import check_file_team_permission
# from api.db.services.document_service import DocumentService
# from api.db.services.file2document_service import File2DocumentService
# from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
# from common.misc_utils import get_uuid, thread_pool_exec
# from common.constants import RetCode, FileSource
# from api.db import FileType
# from api.db.services import duplicate_name
# from api.db.services.file_service import FileService
# from api.utils.api_utils import get_json_result, get_request_json
# from api.utils.file_utils import filename_type
# from api.utils.web_utils import CONTENT_TYPE_MAP, apply_safe_file_response_headers
# from common import settings
#
# 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
# @manager.route('/upload', methods=['POST']) # noqa: F821
# @login_required
# # @validate_request("parent_id")
# async def upload():
# form = await request.form
# pf_id = form.get("parent_id")
#
# http://www.apache.org/licenses/LICENSE-2.0
# if not pf_id:
# root_folder = FileService.get_root_folder(current_user.id)
# pf_id = root_folder["id"]
#
# 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
# files = await request.files
# if 'file' not in files:
# return get_json_result(
# data=False, message='No file part!', code=RetCode.ARGUMENT_ERROR)
# file_objs = files.getlist('file')
#
import logging
import os
import pathlib
import re
from quart import request, make_response
from api.apps import login_required, current_user
from api.common.check_team_permission import check_file_team_permission
from api.db.services.document_service import DocumentService
from api.db.services.file2document_service import File2DocumentService
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
from common.misc_utils import get_uuid, thread_pool_exec
from common.constants import RetCode, FileSource
from api.db import FileType
from api.db.services import duplicate_name
from api.db.services.file_service import FileService
from api.utils.api_utils import get_json_result, get_request_json
from api.utils.file_utils import filename_type
from api.utils.web_utils import CONTENT_TYPE_MAP, apply_safe_file_response_headers
from common import settings
@manager.route('/upload', methods=['POST']) # noqa: F821
@login_required
# @validate_request("parent_id")
async def upload():
form = await request.form
pf_id = form.get("parent_id")
if not pf_id:
root_folder = FileService.get_root_folder(current_user.id)
pf_id = root_folder["id"]
files = await request.files
if 'file' not in files:
return get_json_result(
data=False, message='No file part!', code=RetCode.ARGUMENT_ERROR)
file_objs = files.getlist('file')
for file_obj in file_objs:
if file_obj.filename == '':
return get_json_result(
data=False, message='No file selected!', code=RetCode.ARGUMENT_ERROR)
file_res = []
try:
e, pf_folder = FileService.get_by_id(pf_id)
if not e:
return get_data_error_result( message="Can't find this folder!")
async def _handle_single_file(file_obj):
MAX_FILE_NUM_PER_USER: int = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
if 0 < MAX_FILE_NUM_PER_USER <= await thread_pool_exec(DocumentService.get_doc_count, current_user.id):
return get_data_error_result( message="Exceed the maximum file number of a free user!")
# split file name path
if not file_obj.filename:
file_obj_names = [pf_folder.name, file_obj.filename]
else:
full_path = '/' + file_obj.filename
file_obj_names = full_path.split('/')
file_len = len(file_obj_names)
# get folder
file_id_list = await thread_pool_exec(FileService.get_id_list_by_id, pf_id, file_obj_names, 1, [pf_id])
len_id_list = len(file_id_list)
# create folder
if file_len != len_id_list:
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 1])
if not e:
return get_data_error_result(message="Folder not found!")
last_folder = await thread_pool_exec(FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names,
len_id_list)
else:
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 2])
if not e:
return get_data_error_result(message="Folder not found!")
last_folder = await thread_pool_exec(FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names,
len_id_list)
# file type
filetype = filename_type(file_obj_names[file_len - 1])
location = file_obj_names[file_len - 1]
while await thread_pool_exec(settings.STORAGE_IMPL.obj_exist, last_folder.id, location):
location += "_"
blob = await thread_pool_exec(file_obj.read)
filename = await thread_pool_exec(
duplicate_name,
FileService.query,
name=file_obj_names[file_len - 1],
parent_id=last_folder.id)
await thread_pool_exec(settings.STORAGE_IMPL.put, last_folder.id, location, blob)
file_data = {
"id": get_uuid(),
"parent_id": last_folder.id,
"tenant_id": current_user.id,
"created_by": current_user.id,
"type": filetype,
"name": filename,
"location": location,
"size": len(blob),
}
inserted = await thread_pool_exec(FileService.insert, file_data)
return inserted.to_json()
for file_obj in file_objs:
res = await _handle_single_file(file_obj)
file_res.append(res)
return get_json_result(data=file_res)
except Exception as e:
return server_error_response(e)
@manager.route('/create', methods=['POST']) # noqa: F821
@login_required
@validate_request("name")
async def create():
req = await get_request_json()
pf_id = req.get("parent_id")
input_file_type = req.get("type")
if not pf_id:
root_folder = FileService.get_root_folder(current_user.id)
pf_id = root_folder["id"]
try:
if not FileService.is_parent_folder_exist(pf_id):
return get_json_result(
data=False, message="Parent Folder Doesn't Exist!", code=RetCode.OPERATING_ERROR)
if FileService.query(name=req["name"], parent_id=pf_id):
return get_data_error_result(
message="Duplicated folder name in the same folder.")
if input_file_type == FileType.FOLDER.value:
file_type = FileType.FOLDER.value
else:
file_type = FileType.VIRTUAL.value
file = FileService.insert({
"id": get_uuid(),
"parent_id": pf_id,
"tenant_id": current_user.id,
"created_by": current_user.id,
"name": req["name"],
"location": "",
"size": 0,
"type": file_type
})
return get_json_result(data=file.to_json())
except Exception as e:
return server_error_response(e)
@manager.route('/list', methods=['GET']) # noqa: F821
@login_required
def list_files():
pf_id = request.args.get("parent_id")
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 1))
items_per_page = int(request.args.get("page_size", 15))
orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True)
if not pf_id:
root_folder = FileService.get_root_folder(current_user.id)
pf_id = root_folder["id"]
FileService.init_knowledgebase_docs(pf_id, current_user.id)
try:
e, file = FileService.get_by_id(pf_id)
if not e:
return get_data_error_result(message="Folder not found!")
files, total = FileService.get_by_pf_id(
current_user.id, pf_id, page_number, items_per_page, orderby, desc, keywords)
parent_folder = FileService.get_parent_folder(pf_id)
if not parent_folder:
return get_json_result(message="File not found!")
return get_json_result(data={"total": total, "files": files, "parent_folder": parent_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/root_folder', methods=['GET']) # noqa: F821
@login_required
def get_root_folder():
try:
root_folder = FileService.get_root_folder(current_user.id)
return get_json_result(data={"root_folder": root_folder})
except Exception as e:
return server_error_response(e)
@manager.route('/parent_folder', methods=['GET']) # noqa: F821
@login_required
def get_parent_folder():
file_id = request.args.get("file_id")
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(message="Folder not found!")
parent_folder = FileService.get_parent_folder(file_id)
return get_json_result(data={"parent_folder": parent_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/all_parent_folder', methods=['GET']) # noqa: F821
@login_required
def get_all_parent_folders():
file_id = request.args.get("file_id")
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(message="Folder not found!")
parent_folders = FileService.get_all_parent_folders(file_id)
parent_folders_res = []
for parent_folder in parent_folders:
parent_folders_res.append(parent_folder.to_json())
return get_json_result(data={"parent_folders": parent_folders_res})
except Exception as e:
return server_error_response(e)
@manager.route("/rm", methods=["POST"]) # noqa: F821
@login_required
@validate_request("file_ids")
async def rm():
req = await get_request_json()
file_ids = req["file_ids"]
uid = current_user.id
try:
def _delete_single_file(file):
try:
if file.location:
settings.STORAGE_IMPL.rm(file.parent_id, file.location)
except Exception as e:
logging.exception(f"Fail to remove object: {file.parent_id}/{file.location}, error: {e}")
informs = File2DocumentService.get_by_file_id(file.id)
for inform in informs:
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if e and doc:
tenant_id = DocumentService.get_tenant_id(doc_id)
if tenant_id:
DocumentService.remove_document(doc, tenant_id)
File2DocumentService.delete_by_file_id(file.id)
FileService.delete(file)
def _delete_folder_recursive(folder, tenant_id):
sub_files = FileService.list_all_files_by_parent_id(folder.id)
for sub_file in sub_files:
if sub_file.type == FileType.FOLDER.value:
_delete_folder_recursive(sub_file, tenant_id)
else:
_delete_single_file(sub_file)
FileService.delete(folder)
def _rm_sync():
for file_id in file_ids:
e, file = FileService.get_by_id(file_id)
if not e or not file:
return get_data_error_result(message="File or Folder not found!")
if not file.tenant_id:
return get_data_error_result(message="Tenant not found!")
if not check_file_team_permission(file, uid):
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
if file.source_type == FileSource.KNOWLEDGEBASE:
continue
if file.type == FileType.FOLDER.value:
_delete_folder_recursive(file, uid)
continue
_delete_single_file(file)
return get_json_result(data=True)
return await thread_pool_exec(_rm_sync)
except Exception as e:
return server_error_response(e)
@manager.route('/rename', methods=['POST']) # noqa: F821
@login_required
@validate_request("file_id", "name")
async def rename():
req = await get_request_json()
try:
e, file = FileService.get_by_id(req["file_id"])
if not e:
return get_data_error_result(message="File not found!")
if not check_file_team_permission(file, current_user.id):
return get_json_result(data=False, message='No authorization.', code=RetCode.AUTHENTICATION_ERROR)
if file.type != FileType.FOLDER.value \
and pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
file.name.lower()).suffix:
return get_json_result(
data=False,
message="The extension of file can't be changed",
code=RetCode.ARGUMENT_ERROR)
for file in FileService.query(name=req["name"], pf_id=file.parent_id):
if file.name == req["name"]:
return get_data_error_result(
message="Duplicated file name in the same folder.")
if not FileService.update_by_id(
req["file_id"], {"name": req["name"]}):
return get_data_error_result(
message="Database error (File rename)!")
informs = File2DocumentService.get_by_file_id(req["file_id"])
if informs:
if not DocumentService.update_by_id(
informs[0].document_id, {"name": req["name"]}):
return get_data_error_result(
message="Database error (Document rename)!")
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@manager.route('/get/<file_id>', methods=['GET']) # noqa: F821
@login_required
async def get(file_id):
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_data_error_result(message="Document not found!")
if not check_file_team_permission(file, current_user.id):
return get_json_result(data=False, message='No authorization.', code=RetCode.AUTHENTICATION_ERROR)
blob = await thread_pool_exec(settings.STORAGE_IMPL.get, file.parent_id, file.location)
if not blob:
b, n = File2DocumentService.get_storage_address(file_id=file_id)
blob = await thread_pool_exec(settings.STORAGE_IMPL.get, b, n)
response = await make_response(blob)
ext = re.search(r"\.([^.]+)$", file.name.lower())
ext = ext.group(1) if ext else None
content_type = None
if ext:
fallback_prefix = "image" if file.type == FileType.VISUAL.value else "application"
content_type = CONTENT_TYPE_MAP.get(ext, f"{fallback_prefix}/{ext}")
apply_safe_file_response_headers(response, content_type, ext)
return response
except Exception as e:
return server_error_response(e)
@manager.route("/mv", methods=["POST"]) # noqa: F821
@login_required
@validate_request("src_file_ids", "dest_file_id")
async def move():
req = await get_request_json()
try:
file_ids = req["src_file_ids"]
dest_parent_id = req["dest_file_id"]
ok, dest_folder = FileService.get_by_id(dest_parent_id)
if not ok or not dest_folder:
return get_data_error_result(message="Parent folder not found!")
files = FileService.get_by_ids(file_ids)
if not files:
return get_data_error_result(message="Source files not found!")
files_dict = {f.id: f for f in files}
for file_id in file_ids:
file = files_dict.get(file_id)
if not file:
return get_data_error_result(message="File or folder not found!")
if not file.tenant_id:
return get_data_error_result(message="Tenant not found!")
if not check_file_team_permission(file, current_user.id):
return get_json_result(
data=False,
message="No authorization.",
code=RetCode.AUTHENTICATION_ERROR,
)
def _move_entry_recursive(source_file_entry, dest_folder):
if source_file_entry.type == FileType.FOLDER.value:
existing_folder = FileService.query(name=source_file_entry.name, parent_id=dest_folder.id)
if existing_folder:
new_folder = existing_folder[0]
else:
new_folder = FileService.insert(
{
"id": get_uuid(),
"parent_id": dest_folder.id,
"tenant_id": source_file_entry.tenant_id,
"created_by": current_user.id,
"name": source_file_entry.name,
"location": "",
"size": 0,
"type": FileType.FOLDER.value,
}
)
sub_files = FileService.list_all_files_by_parent_id(source_file_entry.id)
for sub_file in sub_files:
_move_entry_recursive(sub_file, new_folder)
FileService.delete_by_id(source_file_entry.id)
return
old_parent_id = source_file_entry.parent_id
old_location = source_file_entry.location
filename = source_file_entry.name
new_location = filename
while settings.STORAGE_IMPL.obj_exist(dest_folder.id, new_location):
new_location += "_"
try:
settings.STORAGE_IMPL.move(old_parent_id, old_location, dest_folder.id, new_location)
except Exception as storage_err:
raise RuntimeError(f"Move file failed at storage layer: {str(storage_err)}")
FileService.update_by_id(
source_file_entry.id,
{
"parent_id": dest_folder.id,
"location": new_location,
},
)
def _move_sync():
for file in files:
_move_entry_recursive(file, dest_folder)
return get_json_result(data=True)
return await thread_pool_exec(_move_sync)
except Exception as e:
return server_error_response(e)
# for file_obj in file_objs:
# if file_obj.filename == '':
# return get_json_result(
# data=False, message='No file selected!', code=RetCode.ARGUMENT_ERROR)
# file_res = []
# try:
# e, pf_folder = FileService.get_by_id(pf_id)
# if not e:
# return get_data_error_result( message="Can't find this folder!")
#
# async def _handle_single_file(file_obj):
# MAX_FILE_NUM_PER_USER: int = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
# if 0 < MAX_FILE_NUM_PER_USER <= await thread_pool_exec(DocumentService.get_doc_count, current_user.id):
# return get_data_error_result( message="Exceed the maximum file number of a free user!")
#
# # split file name path
# if not file_obj.filename:
# file_obj_names = [pf_folder.name, file_obj.filename]
# else:
# full_path = '/' + file_obj.filename
# file_obj_names = full_path.split('/')
# file_len = len(file_obj_names)
#
# # get folder
# file_id_list = await thread_pool_exec(FileService.get_id_list_by_id, pf_id, file_obj_names, 1, [pf_id])
# len_id_list = len(file_id_list)
#
# # create folder
# if file_len != len_id_list:
# e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 1])
# if not e:
# return get_data_error_result(message="Folder not found!")
# last_folder = await thread_pool_exec(FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names,
# len_id_list)
# else:
# e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 2])
# if not e:
# return get_data_error_result(message="Folder not found!")
# last_folder = await thread_pool_exec(FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names,
# len_id_list)
#
# # file type
# filetype = filename_type(file_obj_names[file_len - 1])
# location = file_obj_names[file_len - 1]
# while await thread_pool_exec(settings.STORAGE_IMPL.obj_exist, last_folder.id, location):
# location += "_"
# blob = await thread_pool_exec(file_obj.read)
# filename = await thread_pool_exec(
# duplicate_name,
# FileService.query,
# name=file_obj_names[file_len - 1],
# parent_id=last_folder.id)
# await thread_pool_exec(settings.STORAGE_IMPL.put, last_folder.id, location, blob)
# file_data = {
# "id": get_uuid(),
# "parent_id": last_folder.id,
# "tenant_id": current_user.id,
# "created_by": current_user.id,
# "type": filetype,
# "name": filename,
# "location": location,
# "size": len(blob),
# }
# inserted = await thread_pool_exec(FileService.insert, file_data)
# return inserted.to_json()
#
# for file_obj in file_objs:
# res = await _handle_single_file(file_obj)
# file_res.append(res)
#
# return get_json_result(data=file_res)
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/create', methods=['POST']) # noqa: F821
# @login_required
# @validate_request("name")
# async def create():
# req = await get_request_json()
# pf_id = req.get("parent_id")
# input_file_type = req.get("type")
# if not pf_id:
# root_folder = FileService.get_root_folder(current_user.id)
# pf_id = root_folder["id"]
#
# try:
# if not FileService.is_parent_folder_exist(pf_id):
# return get_json_result(
# data=False, message="Parent Folder Doesn't Exist!", code=RetCode.OPERATING_ERROR)
# if FileService.query(name=req["name"], parent_id=pf_id):
# return get_data_error_result(
# message="Duplicated folder name in the same folder.")
#
# if input_file_type == FileType.FOLDER.value:
# file_type = FileType.FOLDER.value
# else:
# file_type = FileType.VIRTUAL.value
#
# file = FileService.insert({
# "id": get_uuid(),
# "parent_id": pf_id,
# "tenant_id": current_user.id,
# "created_by": current_user.id,
# "name": req["name"],
# "location": "",
# "size": 0,
# "type": file_type
# })
#
# return get_json_result(data=file.to_json())
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/list', methods=['GET']) # noqa: F821
# @login_required
# def list_files():
# pf_id = request.args.get("parent_id")
#
# keywords = request.args.get("keywords", "")
#
# page_number = int(request.args.get("page", 1))
# items_per_page = int(request.args.get("page_size", 15))
# orderby = request.args.get("orderby", "create_time")
# desc = request.args.get("desc", True)
# if not pf_id:
# root_folder = FileService.get_root_folder(current_user.id)
# pf_id = root_folder["id"]
# FileService.init_knowledgebase_docs(pf_id, current_user.id)
# try:
# e, file = FileService.get_by_id(pf_id)
# if not e:
# return get_data_error_result(message="Folder not found!")
#
# files, total = FileService.get_by_pf_id(
# current_user.id, pf_id, page_number, items_per_page, orderby, desc, keywords)
#
# parent_folder = FileService.get_parent_folder(pf_id)
# if not parent_folder:
# return get_json_result(message="File not found!")
#
# return get_json_result(data={"total": total, "files": files, "parent_folder": parent_folder.to_json()})
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/root_folder', methods=['GET']) # noqa: F821
# @login_required
# def get_root_folder():
# try:
# root_folder = FileService.get_root_folder(current_user.id)
# return get_json_result(data={"root_folder": root_folder})
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/parent_folder', methods=['GET']) # noqa: F821
# @login_required
# def get_parent_folder():
# file_id = request.args.get("file_id")
# try:
# e, file = FileService.get_by_id(file_id)
# if not e:
# return get_data_error_result(message="Folder not found!")
#
# parent_folder = FileService.get_parent_folder(file_id)
# return get_json_result(data={"parent_folder": parent_folder.to_json()})
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/all_parent_folder', methods=['GET']) # noqa: F821
# @login_required
# def get_all_parent_folders():
# file_id = request.args.get("file_id")
# try:
# e, file = FileService.get_by_id(file_id)
# if not e:
# return get_data_error_result(message="Folder not found!")
#
# parent_folders = FileService.get_all_parent_folders(file_id)
# parent_folders_res = []
# for parent_folder in parent_folders:
# parent_folders_res.append(parent_folder.to_json())
# return get_json_result(data={"parent_folders": parent_folders_res})
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route("/rm", methods=["POST"]) # noqa: F821
# @login_required
# @validate_request("file_ids")
# async def rm():
# req = await get_request_json()
# file_ids = req["file_ids"]
# uid = current_user.id
#
# try:
# def _delete_single_file(file):
# try:
# if file.location:
# settings.STORAGE_IMPL.rm(file.parent_id, file.location)
# except Exception as e:
# logging.exception(f"Fail to remove object: {file.parent_id}/{file.location}, error: {e}")
#
# informs = File2DocumentService.get_by_file_id(file.id)
# for inform in informs:
# doc_id = inform.document_id
# e, doc = DocumentService.get_by_id(doc_id)
# if e and doc:
# tenant_id = DocumentService.get_tenant_id(doc_id)
# if tenant_id:
# DocumentService.remove_document(doc, tenant_id)
# File2DocumentService.delete_by_file_id(file.id)
#
# FileService.delete(file)
#
# def _delete_folder_recursive(folder, tenant_id):
# sub_files = FileService.list_all_files_by_parent_id(folder.id)
# for sub_file in sub_files:
# if sub_file.type == FileType.FOLDER.value:
# _delete_folder_recursive(sub_file, tenant_id)
# else:
# _delete_single_file(sub_file)
#
# FileService.delete(folder)
#
# def _rm_sync():
# for file_id in file_ids:
# e, file = FileService.get_by_id(file_id)
# if not e or not file:
# return get_data_error_result(message="File or Folder not found!")
# if not file.tenant_id:
# return get_data_error_result(message="Tenant not found!")
# if not check_file_team_permission(file, uid):
# return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
#
# if file.source_type == FileSource.KNOWLEDGEBASE:
# continue
#
# if file.type == FileType.FOLDER.value:
# _delete_folder_recursive(file, uid)
# continue
#
# _delete_single_file(file)
#
# return get_json_result(data=True)
#
# return await thread_pool_exec(_rm_sync)
#
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/rename', methods=['POST']) # noqa: F821
# @login_required
# @validate_request("file_id", "name")
# async def rename():
# req = await get_request_json()
# try:
# e, file = FileService.get_by_id(req["file_id"])
# if not e:
# return get_data_error_result(message="File not found!")
# if not check_file_team_permission(file, current_user.id):
# return get_json_result(data=False, message='No authorization.', code=RetCode.AUTHENTICATION_ERROR)
# if file.type != FileType.FOLDER.value \
# and pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
# file.name.lower()).suffix:
# return get_json_result(
# data=False,
# message="The extension of file can't be changed",
# code=RetCode.ARGUMENT_ERROR)
# for file in FileService.query(name=req["name"], pf_id=file.parent_id):
# if file.name == req["name"]:
# return get_data_error_result(
# message="Duplicated file name in the same folder.")
#
# if not FileService.update_by_id(
# req["file_id"], {"name": req["name"]}):
# return get_data_error_result(
# message="Database error (File rename)!")
#
# informs = File2DocumentService.get_by_file_id(req["file_id"])
# if informs:
# if not DocumentService.update_by_id(
# informs[0].document_id, {"name": req["name"]}):
# return get_data_error_result(
# message="Database error (Document rename)!")
#
# return get_json_result(data=True)
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route('/get/<file_id>', methods=['GET']) # noqa: F821
# @login_required
# async def get(file_id):
# try:
# e, file = FileService.get_by_id(file_id)
# if not e:
# return get_data_error_result(message="Document not found!")
# if not check_file_team_permission(file, current_user.id):
# return get_json_result(data=False, message='No authorization.', code=RetCode.AUTHENTICATION_ERROR)
#
# blob = await thread_pool_exec(settings.STORAGE_IMPL.get, file.parent_id, file.location)
# if not blob:
# b, n = File2DocumentService.get_storage_address(file_id=file_id)
# blob = await thread_pool_exec(settings.STORAGE_IMPL.get, b, n)
#
# response = await make_response(blob)
# ext = re.search(r"\.([^.]+)$", file.name.lower())
# ext = ext.group(1) if ext else None
# content_type = None
# if ext:
# fallback_prefix = "image" if file.type == FileType.VISUAL.value else "application"
# content_type = CONTENT_TYPE_MAP.get(ext, f"{fallback_prefix}/{ext}")
# apply_safe_file_response_headers(response, content_type, ext)
# return response
# except Exception as e:
# return server_error_response(e)
#
#
# @manager.route("/mv", methods=["POST"]) # noqa: F821
# @login_required
# @validate_request("src_file_ids", "dest_file_id")
# async def move():
# req = await get_request_json()
# try:
# file_ids = req["src_file_ids"]
# dest_parent_id = req["dest_file_id"]
#
# ok, dest_folder = FileService.get_by_id(dest_parent_id)
# if not ok or not dest_folder:
# return get_data_error_result(message="Parent folder not found!")
#
# files = FileService.get_by_ids(file_ids)
# if not files:
# return get_data_error_result(message="Source files not found!")
#
# files_dict = {f.id: f for f in files}
#
# for file_id in file_ids:
# file = files_dict.get(file_id)
# if not file:
# return get_data_error_result(message="File or folder not found!")
# if not file.tenant_id:
# return get_data_error_result(message="Tenant not found!")
# if not check_file_team_permission(file, current_user.id):
# return get_json_result(
# data=False,
# message="No authorization.",
# code=RetCode.AUTHENTICATION_ERROR,
# )
#
# def _move_entry_recursive(source_file_entry, dest_folder):
# if source_file_entry.type == FileType.FOLDER.value:
# existing_folder = FileService.query(name=source_file_entry.name, parent_id=dest_folder.id)
# if existing_folder:
# new_folder = existing_folder[0]
# else:
# new_folder = FileService.insert(
# {
# "id": get_uuid(),
# "parent_id": dest_folder.id,
# "tenant_id": source_file_entry.tenant_id,
# "created_by": current_user.id,
# "name": source_file_entry.name,
# "location": "",
# "size": 0,
# "type": FileType.FOLDER.value,
# }
# )
#
# sub_files = FileService.list_all_files_by_parent_id(source_file_entry.id)
# for sub_file in sub_files:
# _move_entry_recursive(sub_file, new_folder)
#
# FileService.delete_by_id(source_file_entry.id)
# return
#
# old_parent_id = source_file_entry.parent_id
# old_location = source_file_entry.location
# filename = source_file_entry.name
#
# new_location = filename
# while settings.STORAGE_IMPL.obj_exist(dest_folder.id, new_location):
# new_location += "_"
#
# try:
# settings.STORAGE_IMPL.move(old_parent_id, old_location, dest_folder.id, new_location)
# except Exception as storage_err:
# raise RuntimeError(f"Move file failed at storage layer: {str(storage_err)}")
#
# FileService.update_by_id(
# source_file_entry.id,
# {
# "parent_id": dest_folder.id,
# "location": new_location,
# },
# )
#
# def _move_sync():
# for file in files:
# _move_entry_recursive(file, dest_folder)
# return get_json_result(data=True)
#
# return await thread_pool_exec(_move_sync)
#
# except Exception as e:
# return server_error_response(e)

View File

@@ -0,0 +1,364 @@
#
# 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 logging
import re
from quart import request, make_response
from api.apps import login_required
from api.db import FileType
from api.db.services.file2document_service import File2DocumentService
from api.utils.api_utils import (
add_tenant_id_to_kwargs,
get_error_argument_result,
get_error_data_result,
get_result,
)
from api.utils.validation_utils import (
CreateFolderReq,
DeleteFileReq,
ListFileReq,
MoveFileReq,
validate_and_parse_json_request,
validate_and_parse_request_args,
)
from api.utils.web_utils import CONTENT_TYPE_MAP, apply_safe_file_response_headers
from common import settings
from common.misc_utils import thread_pool_exec
from api.apps.services import file_api_service
@manager.route("/files", methods=["POST"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
async def create_or_upload(tenant_id: str = None):
"""
Upload files or create a folder.
---
tags:
- Files
security:
- ApiKeyAuth: []
parameters:
- in: header
name: Authorization
type: string
required: true
description: Bearer token for authentication.
responses:
200:
description: Successful operation.
"""
content_type = request.content_type or ""
try:
if "multipart/form-data" in content_type:
form = await request.form
pf_id = form.get("parent_id")
files = await request.files
if 'file' not in files:
return get_error_argument_result("No file part!")
file_objs = files.getlist('file')
for file_obj in file_objs:
if file_obj.filename == '':
return get_error_argument_result("No file selected!")
success, result = await file_api_service.upload_file(tenant_id, pf_id, file_objs)
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
else:
req, err = await validate_and_parse_json_request(request, CreateFolderReq)
if err is not None:
return get_error_argument_result(err)
success, result = await file_api_service.create_folder(
tenant_id, req["name"], req.get("parent_id"), req.get("type")
)
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")
@manager.route("/files", methods=["GET"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
def list_files(tenant_id: str = None):
"""
List files under a folder.
---
tags:
- Files
security:
- ApiKeyAuth: []
parameters:
- in: query
name: parent_id
type: string
description: Folder ID to list files from.
- in: query
name: keywords
type: string
description: Search keyword filter.
- in: query
name: page
type: integer
default: 1
- in: query
name: page_size
type: integer
default: 15
- in: query
name: orderby
type: string
default: "create_time"
- in: query
name: desc
type: boolean
default: true
responses:
200:
description: Successful operation.
"""
args, err = validate_and_parse_request_args(request, ListFileReq)
if err is not None:
return get_error_argument_result(err)
try:
success, result = file_api_service.list_files(tenant_id, args)
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")
@manager.route("/files", methods=["DELETE"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
async def delete(tenant_id: str = None):
"""
Delete files.
---
tags:
- Files
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
required: true
schema:
type: object
required:
- ids
properties:
ids:
type: array
items:
type: string
description: List of file IDs to delete.
responses:
200:
description: Successful operation.
"""
req, err = await validate_and_parse_json_request(request, DeleteFileReq)
if err is not None:
return get_error_argument_result(err)
try:
success, result = await file_api_service.delete_files(tenant_id, req["ids"])
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")
@manager.route("/files/move", methods=["POST"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
async def move(tenant_id: str = None):
"""
Move and/or rename files. Follows Linux mv semantics:
at least one of dest_file_id or new_name must be provided.
- dest_file_id only: move files to a new folder (names unchanged).
- new_name only: rename a single file in place (no storage operation).
- both: move and rename simultaneously.
---
tags:
- Files
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
required: true
schema:
type: object
required:
- src_file_ids
properties:
src_file_ids:
type: array
items:
type: string
description: List of source file IDs. Required.
dest_file_id:
type: string
description: Destination folder ID. Optional; omit to rename in place.
new_name:
type: string
description: New file name. Optional; only valid for a single source file.
responses:
200:
description: Successful operation.
"""
req, err = await validate_and_parse_json_request(request, MoveFileReq)
if err is not None:
return get_error_argument_result(err)
try:
success, result = await file_api_service.move_files(
tenant_id, req["src_file_ids"], req.get("dest_file_id"), req.get("new_name")
)
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")
@manager.route("/files/<file_id>", methods=["GET"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
async def download(tenant_id: str = None, file_id: str = None):
"""
Download a file.
---
tags:
- Files
security:
- ApiKeyAuth: []
produces:
- application/octet-stream
parameters:
- in: path
name: file_id
type: string
required: true
description: File ID to download.
responses:
200:
description: File stream.
"""
try:
success, result = file_api_service.get_file_content(tenant_id, file_id)
if not success:
return get_error_data_result(message=result)
file = result
blob = await thread_pool_exec(settings.STORAGE_IMPL.get, file.parent_id, file.location)
if not blob:
b, n = File2DocumentService.get_storage_address(file_id=file_id)
blob = await thread_pool_exec(settings.STORAGE_IMPL.get, b, n)
response = await make_response(blob)
ext = re.search(r"\.([^.]+)$", file.name.lower())
ext = ext.group(1) if ext else None
content_type = None
if ext:
fallback_prefix = "image" if file.type == FileType.VISUAL.value else "application"
content_type = CONTENT_TYPE_MAP.get(ext, f"{fallback_prefix}/{ext}")
apply_safe_file_response_headers(response, content_type, ext)
return response
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")
@manager.route("/files/<file_id>/parent", methods=["GET"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
def parent_folder(tenant_id: str = None, file_id: str = None):
"""
Get parent folder of a file.
---
tags:
- Files
security:
- ApiKeyAuth: []
parameters:
- in: path
name: file_id
type: string
required: true
responses:
200:
description: Parent folder information.
"""
try:
success, result = file_api_service.get_parent_folder(file_id)
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")
@manager.route("/files/<file_id>/ancestors", methods=["GET"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
def ancestors(tenant_id: str = None, file_id: str = None):
"""
Get all ancestor folders of a file.
---
tags:
- Files
security:
- ApiKeyAuth: []
parameters:
- in: path
name: file_id
type: string
required: true
responses:
200:
description: List of ancestor folders.
"""
try:
success, result = file_api_service.get_all_parent_folders(file_id)
if success:
return get_result(data=result)
else:
return get_error_data_result(message=result)
except Exception as e:
logging.exception(e)
return get_error_data_result(message="Internal server error")

View File

@@ -1,5 +1,5 @@
#
# Copyright 2024 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.
@@ -21,34 +21,33 @@ import re
from io import BytesIO
import xxhash
from quart import request, send_file
from peewee import OperationalError
from pydantic import BaseModel, Field, validator
from quart import request, send_file
from api.constants import FILE_NAME_LEN_LIMIT
from api.db import FileType
from api.db.db_models import APIToken, File, Task
from api.db.services.document_service import DocumentService
from api.db.joint_services.tenant_model_service import get_model_config_by_id, get_model_config_by_type_and_name, get_tenant_default_model_by_type
from api.db.services.doc_metadata_service import DocMetadataService
from api.db.services.document_service import DocumentService
from api.db.services.file2document_service import File2DocumentService
from api.db.services.file_service import FileService
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.llm_service import LLMBundle
from api.db.services.task_service import TaskService, cancel_all_task_of, queue_tasks
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.task_service import TaskService, queue_tasks, cancel_all_task_of
from api.db.joint_services.tenant_model_service import get_model_config_by_id, get_tenant_default_model_by_type, get_model_config_by_type_and_name
from common.metadata_utils import meta_filter, convert_conditions
from api.utils.api_utils import check_duplicate_ids, construct_json_result, get_error_data_result, get_parser_config, get_result, server_error_response, token_required, \
get_request_json
from api.utils.api_utils import check_duplicate_ids, construct_json_result, get_error_data_result, get_parser_config, get_request_json, get_result, server_error_response, token_required
from api.utils.image_utils import store_chunk_image
from common import settings
from common.constants import FileSource, LLMType, ParserType, RetCode, TaskStatus
from common.metadata_utils import convert_conditions, meta_filter
from common.misc_utils import thread_pool_exec
from common.string_utils import remove_redundant_spaces
from rag.app.qa import beAdoc, rmPrefix
from rag.app.tag import label_question
from rag.nlp import rag_tokenizer, search
from rag.prompts.generator import cross_languages, keyword_extraction
from common.string_utils import remove_redundant_spaces
from common.misc_utils import thread_pool_exec
from common.constants import RetCode, LLMType, ParserType, TaskStatus, FileSource
from common import settings
from api.utils.image_utils import store_chunk_image
MAXIMUM_OF_UPLOADING_FILES = 256
@@ -162,7 +161,7 @@ async def upload(dataset_id, tenant_id):
"""
e, kb = KnowledgebaseService.get_by_id(dataset_id)
if not e:
raise LookupError(f"Can't find the dataset with ID {dataset_id}!")
return server_error_response(LookupError(f"Can't find the dataset with ID {dataset_id}!"))
err, files = FileService.upload_document(kb, file_objs, tenant_id, parent_path=form.get("parent_path"))
if err:
return get_result(message="\n".join(err), code=RetCode.SERVER_ERROR)
@@ -263,6 +262,8 @@ async def update_doc(tenant_id, dataset_id, document_id):
return get_error_data_result(message="Failed to update metadata")
if "name" in req and req["name"] != doc.name:
if not isinstance(req["name"], str):
return server_error_response(AttributeError(f"'{type(req['name']).__name__}' object has no attribute 'encode'"))
if len(req["name"].encode("utf-8")) > FILE_NAME_LEN_LIMIT:
return get_result(
message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.",
@@ -426,12 +427,12 @@ async def download(tenant_id, dataset_id, document_id):
async def download_doc(document_id):
token = request.headers.get("Authorization").split()
if len(token) != 2:
return get_error_data_result(message='Authorization is not valid!')
return get_error_data_result(message="Authorization is not valid!")
token = token[1]
objs = APIToken.query(beta=token)
if not objs:
return get_error_data_result(message='Authentication error: API key is invalid!"')
if not document_id:
return get_error_data_result(message="Specify document_id please.")
doc = DocumentService.query(id=document_id)
@@ -565,28 +566,28 @@ def list_docs(dataset_id, tenant_id):
description: Processing status.
"""
if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
return get_error_data_result(message=f"You don't own the dataset {dataset_id}. ")
return get_error_data_result(message=f"You don't own the dataset {dataset_id}. ")
q = request.args
document_id = q.get("id")
name = q.get("name")
name = q.get("name")
if document_id and not DocumentService.query(id=document_id, kb_id=dataset_id):
return get_error_data_result(message=f"You don't own the document {document_id}.")
if name and not DocumentService.query(name=name, kb_id=dataset_id):
return get_error_data_result(message=f"You don't own the document {name}.")
page = int(q.get("page", 1))
page_size = int(q.get("page_size", 30))
orderby = q.get("orderby", "create_time")
desc = str(q.get("desc", "true")).strip().lower() != "false"
keywords = q.get("keywords", "")
page = int(q.get("page", 1))
page_size = int(q.get("page_size", 30))
orderby = q.get("orderby", "create_time")
desc = str(q.get("desc", "true")).strip().lower() != "false"
keywords = q.get("keywords", "")
# filters - align with OpenAPI parameter names
suffix = q.getlist("suffix")
run_status = q.getlist("run")
create_time_from = int(q.get("create_time_from", 0))
create_time_to = int(q.get("create_time_to", 0))
suffix = q.getlist("suffix")
run_status = q.getlist("run")
create_time_from = int(q.get("create_time_from", 0))
create_time_to = int(q.get("create_time_to", 0))
metadata_condition_raw = q.get("metadata_condition")
metadata_condition = {}
if metadata_condition_raw:
@@ -608,17 +609,11 @@ def list_docs(dataset_id, tenant_id):
if metadata_condition.get("conditions") and not doc_ids_filter:
return get_result(data={"total": 0, "docs": []})
docs, total = DocumentService.get_list(
dataset_id, page, page_size, orderby, desc, keywords, document_id, name, suffix, run_status_converted, doc_ids_filter
)
docs, total = DocumentService.get_list(dataset_id, page, page_size, orderby, desc, keywords, document_id, name, suffix, run_status_converted, doc_ids_filter)
# time range filter (0 means no bound)
if create_time_from or create_time_to:
docs = [
d for d in docs
if (create_time_from == 0 or d.get("create_time", 0) >= create_time_from)
and (create_time_to == 0 or d.get("create_time", 0) <= create_time_to)
]
docs = [d for d in docs if (create_time_from == 0 or d.get("create_time", 0) >= create_time_from) and (create_time_to == 0 or d.get("create_time", 0) <= create_time_to)]
# rename keys + map run status back to text for output
key_mapping = {
@@ -682,7 +677,7 @@ async def metadata_batch_update(dataset_id, tenant_id):
for d in deletes:
if not isinstance(d, dict) or not d.get("key"):
return get_error_data_result(message="Each delete requires key.")
if document_ids:
kb_doc_ids = KnowledgebaseService.list_documents_by_ids([dataset_id])
target_doc_ids = set(kb_doc_ids)
@@ -702,6 +697,7 @@ async def metadata_batch_update(dataset_id, tenant_id):
updated = DocMetadataService.batch_update_metadata(dataset_id, target_doc_ids, updates, deletes)
return get_result(data={"updated": updated, "matched_docs": len(target_doc_ids)})
@manager.route("/datasets/<dataset_id>/documents", methods=["DELETE"]) # noqa: F821
@token_required
async def delete(tenant_id, dataset_id):
@@ -957,7 +953,7 @@ async def stop_parsing(tenant_id, dataset_id):
doc = DocumentService.query(id=id, kb_id=dataset_id)
if not doc:
return get_error_data_result(message=f"You don't own the document {id}.")
if doc[0].run != TaskStatus.RUNNING.value :
if doc[0].run != TaskStatus.RUNNING.value:
return construct_json_result(
code=RetCode.DATA_ERROR,
message=DOC_STOP_PARSING_INVALID_STATE_MESSAGE,
@@ -1259,6 +1255,7 @@ async def add_chunk(tenant_id, dataset_id, document_id):
if "tag_feas" in req:
d["tag_feas"] = req["tag_feas"]
import base64
image_base64 = req.get("image_base64", None)
if image_base64:
d["img_id"] = "{}-{}".format(dataset_id, chunk_id)
@@ -1702,8 +1699,8 @@ async def retrieval_test(tenant_id):
toc_enhance = req.get("toc_enhance", False)
langs = req.get("cross_languages", [])
if not isinstance(doc_ids, list):
return get_error_data_result("`documents` should be a list")
if doc_ids:
return get_error_data_result("`documents` should be a list")
if doc_ids:
doc_ids_list = KnowledgebaseService.list_documents_by_ids(kb_ids)
for doc_id in doc_ids:
if doc_id not in doc_ids_list:

View File

@@ -1,835 +0,0 @@
#
# Copyright 2025 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 pathlib
import re
from quart import request, make_response
from pathlib import Path
from api.db.services.document_service import DocumentService
from api.db.services.file2document_service import File2DocumentService
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.utils.api_utils import get_json_result, get_request_json, server_error_response, token_required
from common.misc_utils import get_uuid, thread_pool_exec
from api.db import FileType
from api.db.services import duplicate_name
from api.db.services.file_service import FileService
from api.utils.file_utils import filename_type
from api.utils.web_utils import CONTENT_TYPE_MAP, apply_safe_file_response_headers
from common import settings
from common.constants import RetCode
@manager.route('/file/upload', methods=['POST']) # noqa: F821
@token_required
async def upload(tenant_id):
"""
Upload a file to the system.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: formData
name: file
type: file
required: true
description: The file to upload
- in: formData
name: parent_id
type: string
description: Parent folder ID where the file will be uploaded. Optional.
responses:
200:
description: Successfully uploaded the file.
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
description: File ID
name:
type: string
description: File name
size:
type: integer
description: File size in bytes
type:
type: string
description: File type (e.g., document, folder)
"""
form = await request.form
files = await request.files
pf_id = form.get("parent_id")
if not pf_id:
root_folder = FileService.get_root_folder(tenant_id)
pf_id = root_folder["id"]
if 'file' not in files:
return get_json_result(data=False, message='No file part!', code=RetCode.BAD_REQUEST)
file_objs = files.getlist('file')
for file_obj in file_objs:
if file_obj.filename == '':
return get_json_result(data=False, message='No selected file!', code=RetCode.BAD_REQUEST)
file_res = []
try:
e, pf_folder = FileService.get_by_id(pf_id)
if not e:
return get_json_result(data=False, message="Can't find this folder!", code=RetCode.NOT_FOUND)
for file_obj in file_objs:
# Handle file path
full_path = '/' + file_obj.filename
file_obj_names = full_path.split('/')
file_len = len(file_obj_names)
# Get folder path ID
file_id_list = FileService.get_id_list_by_id(pf_id, file_obj_names, 1, [pf_id])
len_id_list = len(file_id_list)
# Crete file folder
if file_len != len_id_list:
e, file = FileService.get_by_id(file_id_list[len_id_list - 1])
if not e:
return get_json_result(data=False, message="Folder not found!", code=RetCode.NOT_FOUND)
last_folder = FileService.create_folder(file, file_id_list[len_id_list - 1], file_obj_names,
len_id_list)
else:
e, file = FileService.get_by_id(file_id_list[len_id_list - 2])
if not e:
return get_json_result(data=False, message="Folder not found!", code=RetCode.NOT_FOUND)
last_folder = FileService.create_folder(file, file_id_list[len_id_list - 2], file_obj_names,
len_id_list)
filetype = filename_type(file_obj_names[file_len - 1])
location = file_obj_names[file_len - 1]
while settings.STORAGE_IMPL.obj_exist(last_folder.id, location):
location += "_"
blob = file_obj.read()
filename = duplicate_name(FileService.query, name=file_obj_names[file_len - 1], parent_id=last_folder.id)
file = {
"id": get_uuid(),
"parent_id": last_folder.id,
"tenant_id": tenant_id,
"created_by": tenant_id,
"type": filetype,
"name": filename,
"location": location,
"size": len(blob),
}
file = FileService.insert(file)
settings.STORAGE_IMPL.put(last_folder.id, location, blob)
file_res.append(file.to_json())
return get_json_result(data=file_res)
except Exception as e:
return server_error_response(e)
@manager.route("/file/upload_info", methods=["POST"]) # noqa: F821
@token_required
async def upload_info(tenant_id):
"""
Upload runtime file metadata for SDK chat completions.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: formData
name: file
type: file
required: false
description: File(s) to upload as runtime attachments.
- in: query
name: url
type: string
required: false
description: Optional URL to fetch and convert into a runtime attachment.
responses:
200:
description: Runtime attachment descriptor(s) for the `files` field in completions requests.
"""
files = await request.files
file_objs = files.getlist("file") if files and files.get("file") else []
url = request.args.get("url")
if file_objs and url:
return get_json_result(
data=False,
message="Provide either multipart file(s) or ?url=..., not both.",
code=RetCode.BAD_REQUEST,
)
if not file_objs and not url:
return get_json_result(
data=False,
message="Missing input: provide multipart file(s) or url",
code=RetCode.BAD_REQUEST,
)
try:
if url and not file_objs:
return get_json_result(data=FileService.upload_info(tenant_id, None, url))
if len(file_objs) == 1:
return get_json_result(data=FileService.upload_info(tenant_id, file_objs[0], None))
results = [FileService.upload_info(tenant_id, f) for f in file_objs]
return get_json_result(data=results)
except Exception as e:
return server_error_response(e)
@manager.route('/file/create', methods=['POST']) # noqa: F821
@token_required
async def create(tenant_id):
"""
Create a new file or folder.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
description: File creation parameters
required: true
schema:
type: object
properties:
name:
type: string
description: Name of the file/folder
parent_id:
type: string
description: Parent folder ID. Optional.
type:
type: string
enum: ["FOLDER", "VIRTUAL"]
description: Type of the file
responses:
200:
description: File created successfully.
schema:
type: object
properties:
data:
type: object
properties:
id:
type: string
name:
type: string
type:
type: string
"""
req = await get_request_json()
pf_id = req.get("parent_id")
input_file_type = req.get("type")
if not pf_id:
root_folder = FileService.get_root_folder(tenant_id)
pf_id = root_folder["id"]
try:
if not FileService.is_parent_folder_exist(pf_id):
return get_json_result(data=False, message="Parent Folder Doesn't Exist!", code=RetCode.BAD_REQUEST)
if FileService.query(name=req["name"], parent_id=pf_id):
return get_json_result(data=False, message="Duplicated folder name in the same folder.",
code=RetCode.CONFLICT)
if input_file_type == FileType.FOLDER.value:
file_type = FileType.FOLDER.value
else:
file_type = FileType.VIRTUAL.value
file = FileService.insert({
"id": get_uuid(),
"parent_id": pf_id,
"tenant_id": tenant_id,
"created_by": tenant_id,
"name": req["name"],
"location": "",
"size": 0,
"type": file_type
})
return get_json_result(data=file.to_json())
except Exception as e:
return server_error_response(e)
@manager.route('/file/list', methods=['GET']) # noqa: F821
@token_required
async def list_files(tenant_id):
"""
List files under a specific folder.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: query
name: parent_id
type: string
description: Folder ID to list files from
- in: query
name: keywords
type: string
description: Search keyword filter
- in: query
name: page
type: integer
default: 1
description: Page number
- in: query
name: page_size
type: integer
default: 15
description: Number of results per page
- in: query
name: orderby
type: string
default: "create_time"
description: Sort by field
- in: query
name: desc
type: boolean
default: true
description: Descending order
responses:
200:
description: Successfully retrieved file list.
schema:
type: object
properties:
total:
type: integer
files:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
type:
type: string
size:
type: integer
create_time:
type: string
format: date-time
"""
pf_id = request.args.get("parent_id")
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 1))
items_per_page = int(request.args.get("page_size", 15))
orderby = request.args.get("orderby", "create_time")
desc = request.args.get("desc", True)
if not pf_id:
root_folder = FileService.get_root_folder(tenant_id)
pf_id = root_folder["id"]
FileService.init_knowledgebase_docs(pf_id, tenant_id)
try:
e, file = FileService.get_by_id(pf_id)
if not e:
return get_json_result(message="Folder not found!", code=RetCode.NOT_FOUND)
files, total = FileService.get_by_pf_id(tenant_id, pf_id, page_number, items_per_page, orderby, desc, keywords)
parent_folder = FileService.get_parent_folder(pf_id)
if not parent_folder:
return get_json_result(message="File not found!", code=RetCode.NOT_FOUND)
return get_json_result(data={"total": total, "files": files, "parent_folder": parent_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/file/root_folder', methods=['GET']) # noqa: F821
@token_required
async def get_root_folder(tenant_id):
"""
Get user's root folder.
---
tags:
- File
security:
- ApiKeyAuth: []
responses:
200:
description: Root folder information
schema:
type: object
properties:
data:
type: object
properties:
root_folder:
type: object
properties:
id:
type: string
name:
type: string
type:
type: string
"""
try:
root_folder = FileService.get_root_folder(tenant_id)
return get_json_result(data={"root_folder": root_folder})
except Exception as e:
return server_error_response(e)
@manager.route('/file/parent_folder', methods=['GET']) # noqa: F821
@token_required
async def get_parent_folder():
"""
Get parent folder info of a file.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: query
name: file_id
type: string
required: true
description: Target file ID
responses:
200:
description: Parent folder information
schema:
type: object
properties:
data:
type: object
properties:
parent_folder:
type: object
properties:
id:
type: string
name:
type: string
"""
file_id = request.args.get("file_id")
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_json_result(message="Folder not found!", code=RetCode.NOT_FOUND)
parent_folder = FileService.get_parent_folder(file_id)
return get_json_result(data={"parent_folder": parent_folder.to_json()})
except Exception as e:
return server_error_response(e)
@manager.route('/file/all_parent_folder', methods=['GET']) # noqa: F821
@token_required
async def get_all_parent_folders(tenant_id):
"""
Get all parent folders of a file.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: query
name: file_id
type: string
required: true
description: Target file ID
responses:
200:
description: All parent folders of the file
schema:
type: object
properties:
data:
type: object
properties:
parent_folders:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
"""
file_id = request.args.get("file_id")
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_json_result(message="Folder not found!", code=RetCode.NOT_FOUND)
parent_folders = FileService.get_all_parent_folders(file_id)
parent_folders_res = [folder.to_json() for folder in parent_folders]
return get_json_result(data={"parent_folders": parent_folders_res})
except Exception as e:
return server_error_response(e)
@manager.route('/file/rm', methods=['POST']) # noqa: F821
@token_required
async def rm(tenant_id):
"""
Delete one or multiple files/folders.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
description: Files to delete
required: true
schema:
type: object
properties:
file_ids:
type: array
items:
type: string
description: List of file IDs to delete
responses:
200:
description: Successfully deleted files
schema:
type: object
properties:
data:
type: boolean
example: true
"""
req = await get_request_json()
file_ids = req["file_ids"]
try:
for file_id in file_ids:
e, file = FileService.get_by_id(file_id)
if not e:
return get_json_result(message="File or Folder not found!", code=RetCode.NOT_FOUND)
if not file.tenant_id:
return get_json_result(message="Tenant not found!", code=RetCode.NOT_FOUND)
if file.type == FileType.FOLDER.value:
file_id_list = FileService.get_all_innermost_file_ids(file_id, [])
for inner_file_id in file_id_list:
e, file = FileService.get_by_id(inner_file_id)
if not e:
return get_json_result(message="File not found!", code=RetCode.NOT_FOUND)
settings.STORAGE_IMPL.rm(file.parent_id, file.location)
FileService.delete_folder_by_pf_id(tenant_id, file_id)
else:
settings.STORAGE_IMPL.rm(file.parent_id, file.location)
if not FileService.delete(file):
return get_json_result(message="Database error (File removal)!", code=RetCode.SERVER_ERROR)
informs = File2DocumentService.get_by_file_id(file_id)
for inform in informs:
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if not e:
return get_json_result(message="Document not found!", code=RetCode.NOT_FOUND)
tenant_id = DocumentService.get_tenant_id(doc_id)
if not tenant_id:
return get_json_result(message="Tenant not found!", code=RetCode.NOT_FOUND)
if not DocumentService.remove_document(doc, tenant_id):
return get_json_result(message="Database error (Document removal)!", code=RetCode.SERVER_ERROR)
File2DocumentService.delete_by_file_id(file_id)
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@manager.route('/file/rename', methods=['POST']) # noqa: F821
@token_required
async def rename(tenant_id):
"""
Rename a file.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
description: Rename file
required: true
schema:
type: object
properties:
file_id:
type: string
description: Target file ID
name:
type: string
description: New name for the file
responses:
200:
description: File renamed successfully
schema:
type: object
properties:
data:
type: boolean
example: true
"""
req = await get_request_json()
try:
e, file = FileService.get_by_id(req["file_id"])
if not e:
return get_json_result(message="File not found!", code=RetCode.NOT_FOUND)
if file.type != FileType.FOLDER.value and pathlib.Path(req["name"].lower()).suffix != pathlib.Path(
file.name.lower()).suffix:
return get_json_result(data=False, message="The extension of file can't be changed",
code=RetCode.BAD_REQUEST)
for existing_file in FileService.query(name=req["name"], pf_id=file.parent_id):
if existing_file.name == req["name"]:
return get_json_result(data=False, message="Duplicated file name in the same folder.",
code=RetCode.CONFLICT)
if not FileService.update_by_id(req["file_id"], {"name": req["name"]}):
return get_json_result(message="Database error (File rename)!", code=RetCode.SERVER_ERROR)
informs = File2DocumentService.get_by_file_id(req["file_id"])
if informs:
if not DocumentService.update_by_id(informs[0].document_id, {"name": req["name"]}):
return get_json_result(message="Database error (Document rename)!", code=RetCode.SERVER_ERROR)
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@manager.route('/file/get/<file_id>', methods=['GET']) # noqa: F821
@token_required
async def get(tenant_id, file_id):
"""
Download a file.
---
tags:
- File
security:
- ApiKeyAuth: []
produces:
- application/octet-stream
parameters:
- in: path
name: file_id
type: string
required: true
description: File ID to download
responses:
200:
description: File stream
schema:
type: file
RetCode.NOT_FOUND:
description: File not found
"""
try:
e, file = FileService.get_by_id(file_id)
if not e:
return get_json_result(message="Document not found!", code=RetCode.NOT_FOUND)
blob = settings.STORAGE_IMPL.get(file.parent_id, file.location)
if not blob:
b, n = File2DocumentService.get_storage_address(file_id=file_id)
blob = settings.STORAGE_IMPL.get(b, n)
response = await make_response(blob)
ext = re.search(r"\.([^.]+)$", file.name)
extension = ext.group(1).lower() if ext else None
content_type = None
if extension:
fallback_prefix = "image" if file.type == FileType.VISUAL.value else "application"
content_type = CONTENT_TYPE_MAP.get(extension, f"{fallback_prefix}/{extension}")
apply_safe_file_response_headers(response, content_type, extension)
return response
except Exception as e:
return server_error_response(e)
@manager.route("/file/download/<attachment_id>", methods=["GET"]) # noqa: F821
@token_required
async def download_attachment(tenant_id, attachment_id):
try:
ext = request.args.get("ext", "markdown")
data = await thread_pool_exec(settings.STORAGE_IMPL.get, tenant_id, attachment_id)
response = await make_response(data)
content_type = CONTENT_TYPE_MAP.get(ext, f"application/{ext}")
apply_safe_file_response_headers(response, content_type, ext)
return response
except Exception as e:
return server_error_response(e)
@manager.route('/file/mv', methods=['POST']) # noqa: F821
@token_required
async def move(tenant_id):
"""
Move one or multiple files to another folder.
---
tags:
- File
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
description: Move operation
required: true
schema:
type: object
properties:
src_file_ids:
type: array
items:
type: string
description: Source file IDs
dest_file_id:
type: string
description: Destination folder ID
responses:
200:
description: Files moved successfully
schema:
type: object
properties:
data:
type: boolean
example: true
"""
req = await get_request_json()
try:
file_ids = req["src_file_ids"]
parent_id = req["dest_file_id"]
files = FileService.get_by_ids(file_ids)
files_dict = {f.id: f for f in files}
for file_id in file_ids:
file = files_dict[file_id]
if not file:
return get_json_result(message="File or Folder not found!", code=RetCode.NOT_FOUND)
if not file.tenant_id:
return get_json_result(message="Tenant not found!", code=RetCode.NOT_FOUND)
fe, _ = FileService.get_by_id(parent_id)
if not fe:
return get_json_result(message="Parent Folder not found!", code=RetCode.NOT_FOUND)
FileService.move_file(file_ids, parent_id)
return get_json_result(data=True)
except Exception as e:
return server_error_response(e)
@manager.route('/file/convert', methods=['POST']) # noqa: F821
@token_required
async def convert(tenant_id):
req = await get_request_json()
kb_ids = req["kb_ids"]
file_ids = req["file_ids"]
file2documents = []
try:
files = FileService.get_by_ids(file_ids)
files_set = dict({file.id: file for file in files})
for file_id in file_ids:
file = files_set[file_id]
if not file:
return get_json_result(message="File not found!", code=RetCode.NOT_FOUND)
file_ids_list = [file_id]
if file.type == FileType.FOLDER.value:
file_ids_list = FileService.get_all_innermost_file_ids(file_id, [])
for id in file_ids_list:
informs = File2DocumentService.get_by_file_id(id)
# delete
for inform in informs:
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if not e:
return get_json_result(message="Document not found!", code=RetCode.NOT_FOUND)
tenant_id = DocumentService.get_tenant_id(doc_id)
if not tenant_id:
return get_json_result(message="Tenant not found!", code=RetCode.NOT_FOUND)
if not DocumentService.remove_document(doc, tenant_id):
return get_json_result(
message="Database error (Document removal)!", code=RetCode.NOT_FOUND)
File2DocumentService.delete_by_file_id(id)
# insert
for kb_id in kb_ids:
e, kb = KnowledgebaseService.get_by_id(kb_id)
if not e:
return get_json_result(
message="Can't find this dataset!", code=RetCode.NOT_FOUND)
e, file = FileService.get_by_id(id)
if not e:
return get_json_result(
message="Can't find this file!", code=RetCode.NOT_FOUND)
doc = DocumentService.insert({
"id": get_uuid(),
"kb_id": kb.id,
"parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
"parser_config": kb.parser_config,
"created_by": tenant_id,
"type": file.type,
"name": file.name,
"suffix": Path(file.name).suffix.lstrip("."),
"location": file.location,
"size": file.size
})
file2document = File2DocumentService.insert({
"id": get_uuid(),
"file_id": id,
"document_id": doc.id,
})
file2documents.append(file2document.to_json())
return get_json_result(data=file2documents)
except Exception as e:
return server_error_response(e)

View File

@@ -0,0 +1,397 @@
#
# 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 logging
import os
import pathlib
from api.common.check_team_permission import check_file_team_permission
from api.db import FileType
from api.db.services import duplicate_name
from api.db.services.document_service import DocumentService
from api.db.services.file2document_service import File2DocumentService
from api.db.services.file_service import FileService
from api.utils.file_utils import filename_type
from common import settings
from common.constants import FileSource
from common.misc_utils import get_uuid, thread_pool_exec
async def upload_file(tenant_id: str, pf_id: str, file_objs: list):
"""
Upload files to a folder.
:param tenant_id: tenant ID
:param pf_id: parent folder ID
:param file_objs: list of file objects from request
:return: (success, result_list) or (success, error_message)
"""
if not pf_id:
root_folder = FileService.get_root_folder(tenant_id)
pf_id = root_folder["id"]
e, pf_folder = FileService.get_by_id(pf_id)
if not e:
return False, "Can't find this folder!"
file_res = []
for file_obj in file_objs:
MAX_FILE_NUM_PER_USER = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
if 0 < MAX_FILE_NUM_PER_USER <= await thread_pool_exec(DocumentService.get_doc_count, tenant_id):
return False, "Exceed the maximum file number of a free user!"
if not file_obj.filename:
file_obj_names = [pf_folder.name, file_obj.filename]
else:
full_path = '/' + file_obj.filename
file_obj_names = full_path.split('/')
file_len = len(file_obj_names)
file_id_list = await thread_pool_exec(FileService.get_id_list_by_id, pf_id, file_obj_names, 1, [pf_id])
len_id_list = len(file_id_list)
if file_len != len_id_list:
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 1])
if not e:
return False, "Folder not found!"
last_folder = await thread_pool_exec(
FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names, len_id_list
)
else:
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 2])
if not e:
return False, "Folder not found!"
last_folder = await thread_pool_exec(
FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names, len_id_list
)
filetype = filename_type(file_obj_names[file_len - 1])
location = file_obj_names[file_len - 1]
while await thread_pool_exec(settings.STORAGE_IMPL.obj_exist, last_folder.id, location):
location += "_"
blob = await thread_pool_exec(file_obj.read)
filename = await thread_pool_exec(
duplicate_name, FileService.query, name=file_obj_names[file_len - 1], parent_id=last_folder.id
)
await thread_pool_exec(settings.STORAGE_IMPL.put, last_folder.id, location, blob)
file_data = {
"id": get_uuid(),
"parent_id": last_folder.id,
"tenant_id": tenant_id,
"created_by": tenant_id,
"type": filetype,
"name": filename,
"location": location,
"size": len(blob),
}
inserted = await thread_pool_exec(FileService.insert, file_data)
file_res.append(inserted.to_json())
return True, file_res
async def create_folder(tenant_id: str, name: str, pf_id: str = None, file_type: str = None):
"""
Create a new folder or virtual file.
:param tenant_id: tenant ID
:param name: folder name
:param pf_id: parent folder ID
:param file_type: file type (folder or virtual)
:return: (success, result) or (success, error_message)
"""
if not pf_id:
root_folder = FileService.get_root_folder(tenant_id)
pf_id = root_folder["id"]
if not FileService.is_parent_folder_exist(pf_id):
return False, "Parent Folder Doesn't Exist!"
if FileService.query(name=name, parent_id=pf_id):
return False, "Duplicated folder name in the same folder."
if file_type == FileType.FOLDER.value:
ft = FileType.FOLDER.value
else:
ft = FileType.VIRTUAL.value
file = FileService.insert({
"id": get_uuid(),
"parent_id": pf_id,
"tenant_id": tenant_id,
"created_by": tenant_id,
"name": name,
"location": "",
"size": 0,
"type": ft,
})
return True, file.to_json()
def list_files(tenant_id: str, args: dict):
"""
List files under a folder.
:param tenant_id: tenant ID
:param args: query arguments (parent_id, keywords, page, page_size, orderby, desc)
:return: (success, result) or (success, error_message)
"""
pf_id = args.get("parent_id")
keywords = args.get("keywords", "")
page_number = int(args.get("page", 1))
items_per_page = int(args.get("page_size", 15))
orderby = args.get("orderby", "create_time")
desc = args.get("desc", True)
if not pf_id:
root_folder = FileService.get_root_folder(tenant_id)
pf_id = root_folder["id"]
FileService.init_knowledgebase_docs(pf_id, tenant_id)
e, file = FileService.get_by_id(pf_id)
if not e:
return False, "Folder not found!"
files, total = FileService.get_by_pf_id(tenant_id, pf_id, page_number, items_per_page, orderby, desc, keywords)
parent_folder = FileService.get_parent_folder(pf_id)
if not parent_folder:
return False, "File not found!"
return True, {"total": total, "files": files, "parent_folder": parent_folder.to_json()}
def get_parent_folder(file_id: str):
"""
Get parent folder of a file.
:param file_id: file ID
:return: (success, result) or (success, error_message)
"""
e, file = FileService.get_by_id(file_id)
if not e:
return False, "Folder not found!"
parent_folder = FileService.get_parent_folder(file_id)
return True, {"parent_folder": parent_folder.to_json()}
def get_all_parent_folders(file_id: str):
"""
Get all ancestor folders of a file.
:param file_id: file ID
:return: (success, result) or (success, error_message)
"""
e, file = FileService.get_by_id(file_id)
if not e:
return False, "Folder not found!"
parent_folders = FileService.get_all_parent_folders(file_id)
return True, {"parent_folders": [pf.to_json() for pf in parent_folders]}
async def delete_files(uid: str, file_ids: list):
"""
Delete files/folders with team permission check and recursive deletion.
:param uid: user ID
:param file_ids: list of file IDs to delete
:return: (success, result) or (success, error_message)
"""
def _delete_single_file(file):
try:
if file.location:
settings.STORAGE_IMPL.rm(file.parent_id, file.location)
except Exception as e:
logging.exception(f"Fail to remove object: {file.parent_id}/{file.location}, error: {e}")
informs = File2DocumentService.get_by_file_id(file.id)
for inform in informs:
doc_id = inform.document_id
e, doc = DocumentService.get_by_id(doc_id)
if e and doc:
tenant_id = DocumentService.get_tenant_id(doc_id)
if tenant_id:
DocumentService.remove_document(doc, tenant_id)
File2DocumentService.delete_by_file_id(file.id)
FileService.delete(file)
def _delete_folder_recursive(folder, tenant_id):
sub_files = FileService.list_all_files_by_parent_id(folder.id)
for sub_file in sub_files:
if sub_file.type == FileType.FOLDER.value:
_delete_folder_recursive(sub_file, tenant_id)
else:
_delete_single_file(sub_file)
FileService.delete(folder)
def _rm_sync():
for file_id in file_ids:
e, file = FileService.get_by_id(file_id)
if not e or not file:
return False, "File or Folder not found!"
if not file.tenant_id:
return False, "Tenant not found!"
if not check_file_team_permission(file, uid):
return False, "No authorization."
if file.source_type == FileSource.KNOWLEDGEBASE:
continue
if file.type == FileType.FOLDER.value:
_delete_folder_recursive(file, uid)
continue
_delete_single_file(file)
return True, True
return await thread_pool_exec(_rm_sync)
async def move_files(uid: str, src_file_ids: list, dest_file_id: str = None, new_name: str = None):
"""
Move and/or rename files. Follows Linux mv semantics:
- new_name only: rename in place (no storage operation)
- dest_file_id only: move to new folder (keep names)
- both: move and rename simultaneously
:param uid: user ID
:param src_file_ids: list of source file IDs
:param dest_file_id: destination folder ID (optional)
:param new_name: new name for the file (optional, single file only)
:return: (success, result) or (success, error_message)
"""
files = FileService.get_by_ids(src_file_ids)
if not files:
return False, "Source files not found!"
files_dict = {f.id: f for f in files}
for file_id in src_file_ids:
file = files_dict.get(file_id)
if not file:
return False, "File or folder not found!"
if not file.tenant_id:
return False, "Tenant not found!"
if not check_file_team_permission(file, uid):
return False, "No authorization."
dest_folder = None
if dest_file_id:
ok, dest_folder = FileService.get_by_id(dest_file_id)
if not ok or not dest_folder:
return False, "Parent folder not found!"
if new_name:
file = files_dict[src_file_ids[0]]
if file.type != FileType.FOLDER.value and \
pathlib.Path(new_name.lower()).suffix != pathlib.Path(file.name.lower()).suffix:
return False, "The extension of file can't be changed"
target_parent_id = dest_folder.id if dest_folder else file.parent_id
for f in FileService.query(name=new_name, parent_id=target_parent_id):
if f.name == new_name:
return False, "Duplicated file name in the same folder."
def _move_entry_recursive(source_file_entry, dest_folder_entry, override_name=None):
effective_name = override_name or source_file_entry.name
if source_file_entry.type == FileType.FOLDER.value:
existing_folder = FileService.query(name=effective_name, parent_id=dest_folder_entry.id)
if existing_folder:
new_folder = existing_folder[0]
else:
new_folder = FileService.insert({
"id": get_uuid(),
"parent_id": dest_folder_entry.id,
"tenant_id": source_file_entry.tenant_id,
"created_by": source_file_entry.tenant_id,
"name": effective_name,
"location": "",
"size": 0,
"type": FileType.FOLDER.value,
})
sub_files = FileService.list_all_files_by_parent_id(source_file_entry.id)
for sub_file in sub_files:
_move_entry_recursive(sub_file, new_folder)
FileService.delete_by_id(source_file_entry.id)
return
# Non-folder file
need_storage_move = dest_folder_entry.id != source_file_entry.parent_id
updates = {}
if need_storage_move:
new_location = effective_name
while settings.STORAGE_IMPL.obj_exist(dest_folder_entry.id, new_location):
new_location += "_"
try:
settings.STORAGE_IMPL.move(
source_file_entry.parent_id, source_file_entry.location,
dest_folder_entry.id, new_location,
)
except Exception as storage_err:
raise RuntimeError(f"Move file failed at storage layer: {str(storage_err)}")
updates["parent_id"] = dest_folder_entry.id
updates["location"] = new_location
if override_name:
updates["name"] = override_name
if updates:
FileService.update_by_id(source_file_entry.id, updates)
if override_name:
informs = File2DocumentService.get_by_file_id(source_file_entry.id)
if informs:
if not DocumentService.update_by_id(informs[0].document_id, {"name": override_name}):
raise RuntimeError("Database error (Document rename)!")
def _move_or_rename_sync():
if dest_folder:
for file in files:
_move_entry_recursive(file, dest_folder, override_name=new_name)
else:
# Pure rename: no storage operation needed
file = files[0]
if not FileService.update_by_id(file.id, {"name": new_name}):
return False, "Database error (File rename)!"
informs = File2DocumentService.get_by_file_id(file.id)
if informs:
if not DocumentService.update_by_id(informs[0].document_id, {"name": new_name}):
return False, "Database error (Document rename)!"
return True, True
return await thread_pool_exec(_move_or_rename_sync)
def get_file_content(uid: str, file_id: str):
"""
Get file content and metadata for download.
:param uid: user ID
:param file_id: file ID
:return: (success, (blob, file_obj)) or (success, error_message)
"""
e, file = FileService.get_by_id(file_id)
if not e:
return False, "Document not found!"
if not check_file_team_permission(file, uid):
return False, "No authorization."
return True, file

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.
@@ -781,3 +781,40 @@ class BaseListReq(BaseModel):
class ListDatasetReq(BaseListReq):
include_parsing_status: Annotated[bool, Field(default=False)]
ext: Annotated[dict, Field(default={})]
# ---- File Management Request Models ----
class CreateFolderReq(Base):
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=255), Field(...)]
parent_id: Annotated[str | None, Field(default=None)]
type: Annotated[str | None, Field(default=None)]
class DeleteFileReq(Base):
ids: Annotated[list[str], Field(min_length=1)]
class MoveFileReq(Base):
src_file_ids: Annotated[list[str], Field(min_length=1)]
dest_file_id: Annotated[str | None, Field(default=None)]
new_name: Annotated[str | None, StringConstraints(strip_whitespace=True, min_length=1, max_length=255), Field(default=None)]
@model_validator(mode='after')
def check_operation(self):
if not self.dest_file_id and not self.new_name:
raise ValueError("At least one of dest_file_id or new_name must be provided")
if self.new_name and len(self.src_file_ids) > 1:
raise ValueError("new_name can only be used with a single file")
return self
class ListFileReq(BaseModel):
model_config = ConfigDict(extra="forbid")
parent_id: Annotated[str | None, Field(default=None)]
keywords: Annotated[str, Field(default="")]
page: Annotated[int, Field(default=1, ge=1)]
page_size: Annotated[int, Field(default=15, ge=1, le=100)]
orderby: Annotated[str, Field(default="create_time")]
desc: Annotated[bool, Field(default=True)]