mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-20 06:31:02 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
364
api/apps/restful_apis/file_api.py
Normal file
364
api/apps/restful_apis/file_api.py
Normal 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")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
397
api/apps/services/file_api_service.py
Normal file
397
api/apps/services/file_api_service.py
Normal 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
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user