初次提交

This commit is contained in:
2025-10-14 20:05:29 +08:00
commit 6e4e48fdd2
673 changed files with 437006 additions and 0 deletions

View File

View File

View File

View File

@@ -0,0 +1,217 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from app.core.database import get_db
from app.core.security import verify_token
from app.core.token_blacklist import token_blacklist
from app.services.user_service import UserService
from app.schemas.auth import (
UserRegister, UserLogin, UserResponse, LoginResponse,
TokenResponse, TokenRefresh, ApiResponse
)
from app.dependencies.auth import get_current_user_response
from app.models.user import User
from app.exceptions.auth import UsernameAlreadyExistsException
router = APIRouter()
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(request: Request, user_data: UserRegister, db: Session = Depends(get_db)):
"""用户注册"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] INFO: === 注册接口 ===")
print(f"[{timestamp}] INFO: 用户名: {user_data.username}")
print(f"[{timestamp}] INFO: 邮箱: {user_data.email}")
print(f"[{timestamp}] INFO: 密码长度: {len(user_data.password)}字符")
print(f"[{timestamp}] INFO: 确认密码长度: {len(user_data.confirm_password)}字符")
print(f"[{timestamp}] DEBUG: Starting registration process...")
try:
user_service = UserService(db)
# 创建用户
print("[DEBUG] Creating user...")
user = user_service.create_user(user_data)
print(f"[DEBUG] User created successfully with ID: {user.id}")
# 创建令牌
print("[DEBUG] Creating tokens...")
tokens = user_service.create_user_tokens(user)
print("[DEBUG] Tokens created successfully")
# 转换为响应格式
print("[DEBUG] Converting to response format...")
user_response = user_service.to_user_response(user)
print("[DEBUG] Response conversion successful")
response_data = {
"user": user_response.dict(),
"tokens": tokens
}
print("[DEBUG] Response data created successfully")
return ApiResponse(
success=True,
message="注册成功",
data=response_data
)
except UsernameAlreadyExistsException as e:
print(f"[DEBUG] UsernameAlreadyExistsException caught: {e}")
raise e
except HTTPException as e:
print(f"[DEBUG] HTTPException caught: {e}")
raise e
except Exception as e:
# 打印异常信息以便调试
import traceback
print(f"[ERROR] Unexpected error in register: {e}")
print(f"[ERROR] Exception type: {type(e)}")
traceback.print_exc()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "REGISTRATION_FAILED",
"message": f"注册过程中发生错误: {str(e)}"
}
)
@router.post("/login", response_model=ApiResponse)
async def login(request: Request, login_data: UserLogin, db: Session = Depends(get_db)):
"""用户登录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] INFO: === 登录接口 ===")
print(f"[{timestamp}] INFO: 用户名: {login_data.username}")
print(f"[{timestamp}] INFO: 密码长度: {len(login_data.password)}字符")
print(f"[{timestamp}] DEBUG: Starting authentication process...")
try:
user_service = UserService(db)
# 验证用户
user = user_service.authenticate_user(login_data)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"code": "INVALID_CREDENTIALS",
"message": "用户名或密码错误"
}
)
# 创建令牌
tokens = user_service.create_user_tokens(user)
# 转换为响应格式
user_response = user_service.to_user_response(user)
return ApiResponse(
success=True,
message="登录成功",
data={
"user": user_response.dict(),
"tokens": tokens
}
)
except HTTPException as e:
raise e
except Exception as e:
print("用户登录的异常:",e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "LOGIN_FAILED",
"message": "登录过程中发生错误"
}
)
@router.post("/refresh", response_model=ApiResponse)
async def refresh_token(token_data: TokenRefresh, db: Session = Depends(get_db)):
"""刷新访问令牌"""
try:
# 验证刷新令牌
payload = verify_token(token_data.refresh_token, "refresh")
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"code": "INVALID_REFRESH_TOKEN",
"message": "无效的刷新令牌"
}
)
# 获取用户ID
user_id = int(payload.get("sub"))
user_service = UserService(db)
user = user_service.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"code": "USER_NOT_FOUND",
"message": "用户不存在"
}
)
# 创建新的访问令牌
tokens = user_service.create_user_tokens(user)
return ApiResponse(
success=True,
message="令牌刷新成功",
data={
"tokens": tokens
}
)
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "TOKEN_REFRESH_FAILED",
"message": "令牌刷新过程中发生错误"
}
)
@router.get("/me", response_model=ApiResponse)
async def get_current_user_info(current_user: UserResponse = Depends(get_current_user_response)):
"""获取当前用户信息"""
return ApiResponse(
success=True,
message="获取用户信息成功",
data={
"user": current_user.dict()
}
)
@router.post("/logout", response_model=ApiResponse)
async def logout(
request: Request,
current_user: UserResponse = Depends(get_current_user_response)
):
"""用户登出"""
try:
# 从请求头中获取Authorization令牌
authorization = request.headers.get("Authorization")
if authorization and authorization.startswith("Bearer "):
token = authorization.split(" ")[1]
# 将令牌加入黑名单
token_blacklist.add_token(token)
return ApiResponse(
success=True,
message="登出成功",
data={}
)
except Exception as e:
# 即使添加令牌到黑名单失败,也返回成功,因为登出操作主要目的是让客户端删除令牌
return ApiResponse(
success=True,
message="登出成功",
data={}
)

View File

@@ -0,0 +1,382 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from typing import Optional, List
from datetime import datetime
from app.core.database import get_db
from app.services.file_service import FileService
from app.schemas.file import (
FileUploadRequest, FileUpdateRequest, FileSearchRequest,
FileResponse, FileListResponse, FileInfo, StorageInfo, ApiResponse,
UploadResponse, DeleteResponse, FileListRequest, FileIdRequest, StorageInfoRequest
)
from app.models.user import User
from app.dependencies.auth import get_current_user_from_headers
from app.exceptions.file import (
FileTooLargeException, StorageQuotaExceededException,
FileAlreadyExistsException, FileNotFoundException,
InvalidFileTypeException, FileUploadException, FileDeleteException
)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [MODULE] Files module loaded successfully")
router = APIRouter()
@router.post("/upload", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
async def upload_file(
file: UploadFile = File(...),
description: Optional[str] = Form(None),
tags: Optional[str] = Form(None),
is_public: bool = Form(False),
current_user: User = Depends(get_current_user_from_headers),
db: Session = Depends(get_db)
):
"""上传文件"""
try:
file_service = FileService(db)
# 创建上传请求对象
upload_request = FileUploadRequest(
description=description,
tags=tags,
is_public=is_public
)
# 上传文件
db_file = file_service.upload_file(file, current_user, upload_request)
# 转换为响应格式
file_response = FileResponse(
id=db_file.id,
user_id=db_file.user_id,
filename=db_file.filename,
original_filename=db_file.original_filename,
file_size=db_file.file_size,
mime_type=db_file.mime_type,
file_hash=db_file.file_hash,
is_public=db_file.is_public,
download_count=db_file.download_count,
description=db_file.description,
tags=db_file.tags,
created_at=db_file.created_at,
updated_at=db_file.updated_at,
last_accessed_at=db_file.last_accessed_at
)
return ApiResponse(
success=True,
message="文件上传成功",
data={
"file": file_response.model_dump()
}
)
except (FileTooLargeException, StorageQuotaExceededException,
FileAlreadyExistsException, InvalidFileTypeException) as e:
raise e
except Exception as e:
raise FileUploadException(str(e))
@router.get("/list", response_model=ApiResponse)
def get_user_files(
page: int = Query(1, ge=1),
size: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_user_from_headers),
db: Session = Depends(get_db)
):
"""获取用户文件列表"""
try:
file_service = FileService(db)
file_list = file_service.get_user_files(current_user.id, page, size)
return ApiResponse(
success=True,
message="获取文件列表成功",
data={
"files": [FileResponse(
id=file.id,
user_id=file.user_id,
filename=file.filename,
original_filename=file.original_filename,
file_size=file.file_size,
mime_type=file.mime_type,
file_hash=file.file_hash,
is_public=file.is_public,
download_count=file.download_count,
description=file.description,
tags=file.tags,
created_at=file.created_at,
updated_at=file.updated_at,
last_accessed_at=file.last_accessed_at
).model_dump() for file in file_list.files],
"pagination": {
"total": file_list.total,
"page": file_list.page,
"size": file_list.size,
"pages": file_list.pages
}
}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "GET_FILES_FAILED",
"message": "获取文件列表失败"
}
)
@router.post("/info", response_model=ApiResponse)
def get_file_info(
request: FileIdRequest,
db: Session = Depends(get_db)
):
"""获取文件详细信息"""
try:
file_service = FileService(db)
file_info = file_service.get_file_info(request.file_id, request.user_id)
return ApiResponse(
success=True,
message="获取文件信息成功",
data=file_info.model_dump()
)
except FileNotFoundException as e:
raise e
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "GET_FILE_INFO_FAILED",
"message": "获取文件信息失败"
}
)
@router.post("/update", response_model=ApiResponse)
def update_file(
file_id_request: FileIdRequest,
update_request: FileUpdateRequest,
db: Session = Depends(get_db)
):
"""更新文件信息"""
try:
file_service = FileService(db)
db_file = file_service.update_file(file_id_request.file_id, file_id_request.user_id, update_request)
file_response = FileResponse(
id=db_file.id,
user_id=db_file.user_id,
filename=db_file.filename,
original_filename=db_file.original_filename,
file_size=db_file.file_size,
mime_type=db_file.mime_type,
file_hash=db_file.file_hash,
is_public=db_file.is_public,
download_count=db_file.download_count,
description=db_file.description,
tags=db_file.tags,
created_at=db_file.created_at,
updated_at=db_file.updated_at,
last_accessed_at=db_file.last_accessed_at
)
return ApiResponse(
success=True,
message="文件信息更新成功",
data={
"file": file_response.model_dump()
}
)
except FileNotFoundException as e:
raise e
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "UPDATE_FILE_FAILED",
"message": "更新文件信息失败"
}
)
@router.post("/delete", response_model=ApiResponse)
def delete_file(
request: FileIdRequest,
db: Session = Depends(get_db)
):
"""删除文件"""
try:
file_service = FileService(db)
success = file_service.delete_file(request.file_id, request.user_id)
if success:
return ApiResponse(
success=True,
message="文件删除成功",
data={}
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "DELETE_FILE_FAILED",
"message": "文件删除失败"
}
)
except FileNotFoundException as e:
raise e
except Exception as e:
raise FileDeleteException(str(e))
@router.post("/download")
def download_file(
request: FileIdRequest,
db: Session = Depends(get_db)
):
"""下载文件"""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [IN] Processing download request: file_id={request.file_id}, user_id={request.user_id}")
try:
file_service = FileService(db)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] DEBUG: File service created")
db_file = file_service.get_file_by_id(request.file_id, request.user_id)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] DEBUG: File found in database: {db_file is not None}")
if not db_file:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ERROR: File not found in database")
raise FileNotFoundException()
# 增加下载次数
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] DEBUG: Incrementing download count")
file_service.increment_download_count(request.file_id)
# 确保使用绝对路径
import os
absolute_path = os.path.abspath(db_file.file_path)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [PATH] File path: {absolute_path}")
# 验证文件存在
if not os.path.exists(absolute_path):
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ERROR: File not found on disk: {absolute_path}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"code": "FILE_NOT_FOUND_ON_DISK",
"message": "upload文件夹与数据库不匹配"
}
)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [OK] File exists on disk")
# 对于文本文件,直接读取内容返回
if db_file.mime_type and db_file.mime_type.startswith('text/'):
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [TEXT] Processing text file")
with open(absolute_path, 'r', encoding='utf-8') as f:
content = f.read()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [READ] Read {len(content)} characters from file")
from fastapi.responses import Response
import urllib.parse
# 对文件名进行URL编码以支持中文
encoded_filename = urllib.parse.quote(db_file.original_filename.encode('utf-8'))
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [SEND] Sending file: {db_file.original_filename}")
return Response(
content=content,
media_type=db_file.mime_type,
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
}
)
else:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] INFO: [BINARY] Processing binary file with FileResponse")
# 对于二进制文件使用FileResponse
return FileResponse(
path=absolute_path,
filename=db_file.original_filename,
media_type=db_file.mime_type
)
except FileNotFoundException as e:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ERROR: FileNotFoundException: {e}")
raise e
except Exception as e:
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ERROR: Download error: {e}")
import traceback
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ERROR: {traceback.format_exc()}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "DOWNLOAD_FILE_FAILED",
"message": "文件下载失败"
}
)
@router.post("/test-download")
def test_download_file():
"""测试下载功能 - 直接返回5KB文件内容"""
print("[TEST_DOWNLOAD] Test endpoint called", flush=True)
return {"message": "Test endpoint is working"}
@router.post("/simple-download")
def simple_download_file():
"""简单下载测试"""
try:
print("[SIMPLE_DOWNLOAD] Simple download endpoint called", flush=True)
# 直接读取我们创建的文件
file_path = "uploads/verified_5kb_download_test.txt"
import os
if not os.path.exists(file_path):
print(f"[SIMPLE_DOWNLOAD] File not found: {file_path}", flush=True)
return {"error": "File not found"}
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
print(f"[SIMPLE_DOWNLOAD] Read {len(content)} characters", flush=True)
from fastapi.responses import Response
return Response(
content=content,
media_type="text/plain",
headers={"Content-Disposition": "attachment; filename=verified_5kb.txt"}
)
except Exception as e:
print(f"[SIMPLE_DOWNLOAD] Exception: {e}", flush=True)
import traceback
traceback.print_exc()
return {"error": str(e)}
@router.post("/storage/info", response_model=ApiResponse)
def get_storage_info(
request: StorageInfoRequest,
db: Session = Depends(get_db)
):
"""获取用户存储信息"""
try:
file_service = FileService(db)
storage_info = file_service.get_storage_info(request.user_id)
return ApiResponse(
success=True,
message="获取存储信息成功",
data=storage_info.model_dump()
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"code": "GET_STORAGE_INFO_FAILED",
"message": "获取存储信息失败"
}
)

View File

@@ -0,0 +1,81 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.config import settings
import redis
import time
router = APIRouter()
@router.get("/health")
async def health_check():
"""基础健康检查"""
return {
"success": True,
"data": {
"status": "healthy",
"service": "cloud-drive-api",
"environment": settings.ENVIRONMENT,
"timestamp": int(time.time())
},
"message": "API服务运行正常"
}
@router.get("/ready")
async def readiness_check(db: Session = Depends(get_db)):
"""就绪检查 - 检查数据库和Redis连接"""
checks = {}
# 检查数据库连接
try:
db.execute("SELECT 1")
checks["database"] = {
"status": "healthy",
"message": "数据库连接正常"
}
except Exception as e:
checks["database"] = {
"status": "unhealthy",
"message": f"数据库连接失败: {str(e)}"
}
raise HTTPException(status_code=503, detail="数据库连接失败")
# 检查Redis连接
try:
r = redis.from_url(settings.REDIS_URL)
r.ping()
checks["redis"] = {
"status": "healthy",
"message": "Redis连接正常"
}
except Exception as e:
checks["redis"] = {
"status": "unhealthy",
"message": f"Redis连接失败: {str(e)}"
}
raise HTTPException(status_code=503, detail="Redis连接失败")
# 检查文件存储
try:
import os
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
checks["storage"] = {
"status": "healthy",
"message": "文件存储正常"
}
except Exception as e:
checks["storage"] = {
"status": "unhealthy",
"message": f"文件存储失败: {str(e)}"
}
raise HTTPException(status_code=503, detail="文件存储失败")
return {
"success": True,
"data": {
"status": "ready",
"checks": checks,
"timestamp": int(time.time())
},
"message": "所有服务已就绪"
}