mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-06 02:07:49 +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:
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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user