Fix API compatibility and add user/role/permission and asset import/export
This commit is contained in:
547
backend/app/api/v1/files.py
Normal file
547
backend/app/api/v1/files.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
文件管理API路由
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File, Form
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.deps import get_sync_db, get_current_user
|
||||
from app.schemas.file_management import (
|
||||
UploadedFileCreate,
|
||||
UploadedFileUpdate,
|
||||
UploadedFileResponse,
|
||||
UploadedFileWithUrl,
|
||||
FileUploadResponse,
|
||||
FileShareCreate,
|
||||
FileShareResponse,
|
||||
FileBatchDelete,
|
||||
FileStatistics,
|
||||
ChunkUploadInit,
|
||||
ChunkUploadInfo,
|
||||
ChunkUploadComplete
|
||||
)
|
||||
from app.crud.file_management import uploaded_file
|
||||
from app.services.file_service import file_service, chunk_upload_manager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/upload", response_model=FileUploadResponse)
|
||||
async def upload_file(
|
||||
file: UploadFile = File(..., description="上传的文件"),
|
||||
remark: Optional[str] = Form(None, description="备注"),
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
上传文件
|
||||
|
||||
- **file**: 上传的文件
|
||||
- **remark**: 备注
|
||||
|
||||
支持的文件类型:
|
||||
- 图片: JPEG, PNG, GIF, BMP, WebP, SVG
|
||||
- 文档: PDF, Word, Excel, PowerPoint, TXT, CSV
|
||||
- 压缩包: ZIP, RAR, 7Z
|
||||
|
||||
文件大小限制:
|
||||
- 图片: 最大10MB
|
||||
- 其他: 最大100MB
|
||||
"""
|
||||
# 上传文件
|
||||
file_obj = await file_service.upload_file(
|
||||
db=db,
|
||||
file=file,
|
||||
uploader_id=current_user.id,
|
||||
remark=remark
|
||||
)
|
||||
|
||||
# 生成访问URL
|
||||
base_url = "http://localhost:8000" # TODO: 从配置读取
|
||||
download_url = f"{base_url}/api/v1/files/{file_obj.id}/download"
|
||||
preview_url = None
|
||||
if file_obj.file_type and file_obj.file_type.startswith('image/'):
|
||||
preview_url = f"{base_url}/api/v1/files/{file_obj.id}/preview"
|
||||
|
||||
return FileUploadResponse(
|
||||
id=file_obj.id,
|
||||
file_name=file_obj.file_name,
|
||||
original_name=file_obj.original_name,
|
||||
file_size=file_obj.file_size,
|
||||
file_type=file_obj.file_type,
|
||||
file_path=file_obj.file_path,
|
||||
download_url=download_url,
|
||||
preview_url=preview_url,
|
||||
message="上传成功"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[UploadedFileResponse])
|
||||
def get_files(
|
||||
skip: int = Query(0, ge=0, description="跳过条数"),
|
||||
limit: int = Query(20, ge=1, le=100, description="返回条数"),
|
||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
||||
file_type: Optional[str] = Query(None, description="文件类型"),
|
||||
uploader_id: Optional[int] = Query(None, description="上传者ID"),
|
||||
start_date: Optional[str] = Query(None, description="开始日期(YYYY-MM-DD)"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期(YYYY-MM-DD)"),
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取文件列表
|
||||
|
||||
- **skip**: 跳过条数
|
||||
- **limit**: 返回条数(最大100)
|
||||
- **keyword**: 搜索关键词(文件名)
|
||||
- **file_type**: 文件类型筛选
|
||||
- **uploader_id**: 上传者ID筛选
|
||||
- **start_date**: 开始日期
|
||||
- **end_date**: 结束日期
|
||||
"""
|
||||
items, total = uploaded_file.get_multi(
|
||||
db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
keyword=keyword,
|
||||
file_type=file_type,
|
||||
uploader_id=uploader_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
# 添加上传者姓名
|
||||
result = []
|
||||
for item in items:
|
||||
item_dict = UploadedFileResponse.from_orm(item).dict()
|
||||
if item.uploader:
|
||||
item_dict['uploader_name'] = item.uploader.real_name
|
||||
result.append(UploadedFileResponse(**item_dict))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=FileStatistics)
|
||||
def get_file_statistics(
|
||||
uploader_id: Optional[int] = Query(None, description="上传者ID筛选"),
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取文件统计信息
|
||||
|
||||
- **uploader_id**: 上传者ID筛选
|
||||
|
||||
返回文件总数、总大小、类型分布等统计信息
|
||||
"""
|
||||
return file_service.get_statistics(db, uploader_id=uploader_id)
|
||||
|
||||
|
||||
@router.get("/{file_id}", response_model=UploadedFileWithUrl)
|
||||
def get_file(
|
||||
file_id: int,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取文件详情
|
||||
|
||||
- **file_id**: 文件ID
|
||||
|
||||
返回文件详情及访问URL
|
||||
"""
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 生成访问URL
|
||||
base_url = "http://localhost:8000"
|
||||
file_dict = UploadedFileWithUrl.from_orm(file_obj).dict()
|
||||
file_dict['download_url'] = f"{base_url}/api/v1/files/{file_id}/download"
|
||||
|
||||
if file_obj.file_type and file_obj.file_type.startswith('image/'):
|
||||
file_dict['preview_url'] = f"{base_url}/api/v1/files/{file_id}/preview"
|
||||
|
||||
if file_obj.share_code:
|
||||
file_dict['share_url'] = f"{base_url}/api/v1/files/share/{file_obj.share_code}"
|
||||
|
||||
return UploadedFileWithUrl(**file_dict)
|
||||
|
||||
|
||||
@router.get("/{file_id}/download")
|
||||
def download_file(
|
||||
file_id: int,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
下载文件
|
||||
|
||||
- **file_id**: 文件ID
|
||||
|
||||
返回文件流
|
||||
"""
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not file_service.file_exists(file_obj):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件已被删除或移动"
|
||||
)
|
||||
|
||||
# 增加下载次数
|
||||
uploaded_file.increment_download_count(db, file_id=file_id)
|
||||
|
||||
# 返回文件
|
||||
file_path = file_service.get_file_path(file_obj)
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
filename=file_obj.original_name,
|
||||
media_type=file_obj.file_type
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{file_id}/preview")
|
||||
def preview_file(
|
||||
file_id: int,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
预览文件
|
||||
|
||||
- **file_id**: 文件ID
|
||||
|
||||
支持图片直接预览,其他文件类型可能需要转换为预览格式
|
||||
"""
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not file_service.file_exists(file_obj):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件已被删除或移动"
|
||||
)
|
||||
|
||||
# 检查文件类型是否支持预览
|
||||
if not file_obj.file_type or not file_obj.file_type.startswith('image/'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该文件类型不支持在线预览"
|
||||
)
|
||||
|
||||
# 返回缩略图(如果存在)
|
||||
if file_obj.thumbnail_path:
|
||||
thumbnail_path = file_obj.thumbnail_path
|
||||
if Path(thumbnail_path).exists():
|
||||
return FileResponse(
|
||||
path=thumbnail_path,
|
||||
media_type="image/jpeg"
|
||||
)
|
||||
|
||||
# 返回原图
|
||||
file_path = file_service.get_file_path(file_obj)
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=file_obj.file_type
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{file_id}", response_model=UploadedFileResponse)
|
||||
def update_file(
|
||||
file_id: int,
|
||||
obj_in: UploadedFileUpdate,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
更新文件信息
|
||||
|
||||
- **file_id**: 文件ID
|
||||
- **remark**: 备注
|
||||
"""
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 检查权限:只有上传者可以更新
|
||||
if file_obj.uploader_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限修改此文件"
|
||||
)
|
||||
|
||||
# 更新文件
|
||||
file_obj = uploaded_file.update(
|
||||
db,
|
||||
db_obj=file_obj,
|
||||
obj_in=obj_in.dict(exclude_unset=True)
|
||||
)
|
||||
|
||||
return UploadedFileResponse.from_orm(file_obj)
|
||||
|
||||
|
||||
@router.delete("/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_file(
|
||||
file_id: int,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
删除文件
|
||||
|
||||
- **file_id**: 文件ID
|
||||
|
||||
软删除文件记录和物理删除文件
|
||||
"""
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 检查权限:只有上传者可以删除
|
||||
if file_obj.uploader_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限删除此文件"
|
||||
)
|
||||
|
||||
# 软删除数据库记录
|
||||
uploaded_file.delete(db, db_obj=file_obj, deleter_id=current_user.id)
|
||||
|
||||
# 从磁盘删除文件
|
||||
file_service.delete_file_from_disk(file_obj)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@router.delete("/batch", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_files_batch(
|
||||
obj_in: FileBatchDelete,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
批量删除文件
|
||||
|
||||
- **file_ids**: 文件ID列表
|
||||
|
||||
批量软删除文件记录和物理删除文件
|
||||
"""
|
||||
# 软删除数据库记录
|
||||
count = uploaded_file.delete_batch(
|
||||
db,
|
||||
file_ids=obj_in.file_ids,
|
||||
deleter_id=current_user.id
|
||||
)
|
||||
|
||||
# 从磁盘删除文件
|
||||
for file_id in obj_in.file_ids:
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if file_obj and file_obj.uploader_id == current_user.id:
|
||||
file_service.delete_file_from_disk(file_obj)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/{file_id}/share", response_model=FileShareResponse)
|
||||
def create_share_link(
|
||||
file_id: int,
|
||||
share_in: FileShareCreate = FileShareCreate(),
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
生成分享链接
|
||||
|
||||
- **file_id**: 文件ID
|
||||
- **expire_days**: 有效期(天),默认7天,最大30天
|
||||
|
||||
生成用于文件分享的临时链接
|
||||
"""
|
||||
file_obj = uploaded_file.get(db, file_id)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件不存在"
|
||||
)
|
||||
|
||||
# 检查权限:只有上传者可以分享
|
||||
if file_obj.uploader_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限分享此文件"
|
||||
)
|
||||
|
||||
# 生成分享链接
|
||||
base_url = "http://localhost:8000"
|
||||
return file_service.generate_share_link(
|
||||
db,
|
||||
file_id=file_id,
|
||||
expire_days=share_in.expire_days,
|
||||
base_url=base_url
|
||||
)
|
||||
|
||||
|
||||
@router.get("/share/{share_code}")
|
||||
def access_shared_file(
|
||||
share_code: str,
|
||||
db: Session = Depends(get_sync_db)
|
||||
):
|
||||
"""
|
||||
访问分享的文件
|
||||
|
||||
- **share_code**: 分享码
|
||||
|
||||
通过分享码访问文件(无需登录)
|
||||
"""
|
||||
file_obj = file_service.get_shared_file(db, share_code)
|
||||
if not file_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="分享链接不存在或已过期"
|
||||
)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not file_service.file_exists(file_obj):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文件已被删除或移动"
|
||||
)
|
||||
|
||||
# 增加下载次数
|
||||
uploaded_file.increment_download_count(db, file_id=file_obj.id)
|
||||
|
||||
# 返回文件
|
||||
file_path = file_service.get_file_path(file_obj)
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
filename=file_obj.original_name,
|
||||
media_type=file_obj.file_type
|
||||
)
|
||||
|
||||
|
||||
# ===== 分片上传 =====
|
||||
|
||||
@router.post("/chunks/init")
|
||||
def init_chunk_upload(
|
||||
obj_in: ChunkUploadInit,
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
初始化分片上传
|
||||
|
||||
- **file_name**: 文件名
|
||||
- **file_size**: 文件大小(字节)
|
||||
- **file_type**: 文件类型
|
||||
- **total_chunks**: 总分片数
|
||||
- **file_hash**: 文件哈希(可选)
|
||||
|
||||
返回上传ID,用于后续上传分片
|
||||
"""
|
||||
upload_id = chunk_upload_manager.init_upload(
|
||||
file_name=obj_in.file_name,
|
||||
file_size=obj_in.file_size,
|
||||
file_type=obj_in.file_type,
|
||||
total_chunks=obj_in.total_chunks,
|
||||
file_hash=obj_in.file_hash
|
||||
)
|
||||
|
||||
return {"upload_id": upload_id, "message": "初始化成功"}
|
||||
|
||||
|
||||
@router.post("/chunks/upload")
|
||||
async def upload_chunk(
|
||||
upload_id: str,
|
||||
chunk_index: int,
|
||||
chunk: UploadFile = File(..., description="分片文件"),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
上传分片
|
||||
|
||||
- **upload_id**: 上传ID
|
||||
- **chunk_index**: 分片索引(从0开始)
|
||||
- **chunk**: 分片文件
|
||||
"""
|
||||
content = await chunk.read()
|
||||
success = chunk_upload_manager.save_chunk(upload_id, chunk_index, content)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="上传会话不存在"
|
||||
)
|
||||
|
||||
return {"message": f"分片 {chunk_index} 上传成功"}
|
||||
|
||||
|
||||
@router.post("/chunks/complete", response_model=FileUploadResponse)
|
||||
def complete_chunk_upload(
|
||||
obj_in: ChunkUploadComplete,
|
||||
db: Session = Depends(get_sync_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
完成分片上传
|
||||
|
||||
- **upload_id**: 上传ID
|
||||
- **file_name**: 文件名
|
||||
- **file_hash**: 文件哈希(可选)
|
||||
|
||||
合并所有分片并创建文件记录
|
||||
"""
|
||||
# 合并分片
|
||||
try:
|
||||
file_obj = chunk_upload_manager.merge_chunks(
|
||||
db=db,
|
||||
upload_id=obj_in.upload_id,
|
||||
uploader_id=current_user.id,
|
||||
file_service=file_service
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"合并分片失败: {str(e)}"
|
||||
)
|
||||
|
||||
# 生成访问URL
|
||||
base_url = "http://localhost:8000"
|
||||
download_url = f"{base_url}/api/v1/files/{file_obj.id}/download"
|
||||
preview_url = None
|
||||
if file_obj.file_type and file_obj.file_type.startswith('image/'):
|
||||
preview_url = f"{base_url}/api/v1/files/{file_obj.id}/preview"
|
||||
|
||||
return FileUploadResponse(
|
||||
id=file_obj.id,
|
||||
file_name=file_obj.file_name,
|
||||
original_name=file_obj.original_name,
|
||||
file_size=file_obj.file_size,
|
||||
file_type=file_obj.file_type,
|
||||
file_path=file_obj.file_path,
|
||||
download_url=download_url,
|
||||
preview_url=preview_url,
|
||||
message="上传成功"
|
||||
)
|
||||
Reference in New Issue
Block a user