|
|
|
import hashlib
|
|
import uuid
|
|
from werkzeug.utils import secure_filename
|
|
import os
|
|
from app import db
|
|
from app.models.customer import Customer
|
|
from app.models.translate import Translate
|
|
from app.utils.response import APIResponse
|
|
from pathlib import Path
|
|
from flask_restful import Resource
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
from flask import request, current_app
|
|
from datetime import datetime
|
|
|
|
|
|
class FileUploadResource1(Resource):
|
|
@jwt_required()
|
|
def post(self):
|
|
"""文件上传接口"""
|
|
|
|
if 'file' not in request.files:
|
|
return APIResponse.error('未选择文件', 400)
|
|
file = request.files['file']
|
|
|
|
|
|
if file.filename == '':
|
|
return APIResponse.error('无效文件名', 400)
|
|
|
|
|
|
if not self.allowed_file(file.filename):
|
|
return APIResponse.error(
|
|
f"仅支持以下格式:{', '.join(current_app.config['ALLOWED_EXTENSIONS'])}", 400)
|
|
|
|
|
|
if not self.validate_file_size(file.stream):
|
|
return APIResponse.error(
|
|
f"文件大小超过{current_app.config['MAX_FILE_SIZE'] // (1024 * 1024)}MB限制", 400)
|
|
|
|
|
|
user_id = get_jwt_identity()
|
|
customer = Customer.query.get(user_id)
|
|
file_size = request.content_length
|
|
|
|
|
|
if customer.storage + file_size > current_app.config['MAX_USER_STORAGE']:
|
|
return APIResponse.error('存储空间不足', 403)
|
|
|
|
try:
|
|
|
|
save_dir = self.get_upload_dir()
|
|
filename = file.filename
|
|
save_path = os.path.join(save_dir, filename)
|
|
|
|
|
|
if not self.is_safe_path(save_dir, save_path):
|
|
return APIResponse.error('文件名包含非法字符', 400)
|
|
|
|
|
|
file.save(save_path)
|
|
|
|
customer.storage += file_size
|
|
db.session.commit()
|
|
|
|
file_uuid = str(uuid.uuid4())
|
|
|
|
file_md5 = self.calculate_md5(save_path)
|
|
|
|
|
|
translate_record = Translate(
|
|
translate_no=f"TRANS{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
|
uuid=file_uuid,
|
|
customer_id=user_id,
|
|
origin_filename=filename,
|
|
origin_filepath=os.path.abspath(save_path),
|
|
target_filepath='',
|
|
status='none',
|
|
origin_filesize=file_size,
|
|
md5=file_md5,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
db.session.add(translate_record)
|
|
db.session.commit()
|
|
|
|
|
|
return APIResponse.success({
|
|
'filename': filename,
|
|
'uuid': file_uuid,
|
|
'translate_id': translate_record.id,
|
|
'save_path': os.path.abspath(save_path)
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"文件上传失败:{str(e)}")
|
|
return APIResponse.error('文件上传失败', 500)
|
|
|
|
@staticmethod
|
|
def allowed_file(filename):
|
|
|
|
ALLOWED_EXTENSIONS = {'docx', 'xlsx', 'pptx', 'txt', 'md', 'csv', 'xls', 'doc'}
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
@staticmethod
|
|
def validate_file_size(file_stream):
|
|
"""验证文件大小是否超过限制"""
|
|
MAX_FILE_SIZE = 10 * 1024 * 1024
|
|
file_stream.seek(0, os.SEEK_END)
|
|
file_size = file_stream.tell()
|
|
file_stream.seek(0)
|
|
return file_size <= MAX_FILE_SIZE
|
|
|
|
@staticmethod
|
|
def get_upload_dir():
|
|
"""获取按日期分类的上传目录"""
|
|
|
|
base_dir = Path(current_app.config['UPLOAD_BASE_DIR'])
|
|
upload_dir = base_dir / 'uploads' / datetime.now().strftime('%Y-%m-%d')
|
|
|
|
|
|
if not upload_dir.exists():
|
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
return str(upload_dir)
|
|
|
|
@staticmethod
|
|
def calculate_md5(file_path):
|
|
"""计算文件的 MD5 值"""
|
|
hash_md5 = hashlib.md5()
|
|
with open(file_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
hash_md5.update(chunk)
|
|
return hash_md5.hexdigest()
|
|
|
|
@staticmethod
|
|
def is_safe_path(base_dir, file_path):
|
|
"""检查文件路径是否安全,防止路径遍历攻击"""
|
|
base_dir = Path(base_dir).resolve()
|
|
file_path = Path(file_path).resolve()
|
|
return file_path.is_relative_to(base_dir)
|
|
|
|
|
|
|
|
class FileUploadResource(Resource):
|
|
@jwt_required()
|
|
def post(self):
|
|
"""文件上传接口"""
|
|
|
|
if 'file' not in request.files:
|
|
return APIResponse.error('未选择文件', 400)
|
|
file = request.files['file']
|
|
|
|
|
|
if file.filename == '':
|
|
return APIResponse.error('无效文件名', 400)
|
|
|
|
|
|
if not self.allowed_file(file.filename):
|
|
return APIResponse.error(
|
|
f"仅支持以下格式:{', '.join(current_app.config['ALLOWED_EXTENSIONS'])}", 400)
|
|
|
|
|
|
if not self.validate_file_size(file.stream):
|
|
return APIResponse.error(
|
|
f"文件大小超过{current_app.config['MAX_FILE_SIZE'] // (1024 * 1024)}MB限制", 400)
|
|
|
|
|
|
user_id = get_jwt_identity()
|
|
customer = Customer.query.get(user_id)
|
|
file_size = request.content_length
|
|
|
|
|
|
if customer.storage + file_size > current_app.config['MAX_USER_STORAGE']:
|
|
return APIResponse.error('存储空间不足', 403)
|
|
|
|
try:
|
|
|
|
save_dir = Path(self.get_upload_dir())
|
|
filename = file.filename
|
|
save_path = save_dir / filename
|
|
|
|
|
|
if not self.is_safe_path(save_dir, save_path):
|
|
return APIResponse.error('文件名包含非法字符', 400)
|
|
|
|
|
|
file.save(save_path)
|
|
|
|
customer.storage += file_size
|
|
db.session.commit()
|
|
|
|
file_uuid = str(uuid.uuid4())
|
|
|
|
file_md5 = self.calculate_md5(save_path)
|
|
|
|
|
|
translate_record = Translate(
|
|
translate_no=f"TRANS{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
|
uuid=file_uuid,
|
|
customer_id=user_id,
|
|
origin_filename=filename,
|
|
origin_filepath=str(save_path.resolve()),
|
|
target_filepath='',
|
|
status='none',
|
|
origin_filesize=file_size,
|
|
md5=file_md5,
|
|
created_at=datetime.utcnow()
|
|
)
|
|
db.session.add(translate_record)
|
|
db.session.commit()
|
|
|
|
|
|
return APIResponse.success({
|
|
'filename': filename,
|
|
'uuid': file_uuid,
|
|
'translate_id': translate_record.id,
|
|
'save_path': str(save_path.resolve())
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"文件上传失败:{str(e)}")
|
|
return APIResponse.error('文件上传失败', 500)
|
|
|
|
@staticmethod
|
|
def allowed_file(filename):
|
|
"""验证文件类型是否允许"""
|
|
ALLOWED_EXTENSIONS = {'docx', 'xlsx', 'pptx', 'txt', 'md', 'csv', 'xls', 'doc'}
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
@staticmethod
|
|
def validate_file_size(file_stream):
|
|
"""验证文件大小是否超过限制"""
|
|
MAX_FILE_SIZE = 10 * 1024 * 1024
|
|
file_stream.seek(0, os.SEEK_END)
|
|
file_size = file_stream.tell()
|
|
file_stream.seek(0)
|
|
return file_size <= MAX_FILE_SIZE
|
|
|
|
@staticmethod
|
|
def get_upload_dir():
|
|
"""获取按日期分类的上传目录"""
|
|
|
|
base_dir = Path(current_app.config['UPLOAD_BASE_DIR'])
|
|
upload_dir = base_dir / 'uploads' / datetime.now().strftime('%Y-%m-%d')
|
|
|
|
|
|
if not upload_dir.exists():
|
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
return str(upload_dir)
|
|
|
|
@staticmethod
|
|
def calculate_md5(file_path):
|
|
"""计算文件的 MD5 值"""
|
|
hash_md5 = hashlib.md5()
|
|
with open(file_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
hash_md5.update(chunk)
|
|
return hash_md5.hexdigest()
|
|
|
|
@staticmethod
|
|
def is_safe_path(base_dir, file_path):
|
|
"""检查文件路径是否安全,防止路径遍历攻击"""
|
|
base_dir = Path(base_dir).resolve()
|
|
file_path = Path(file_path).resolve()
|
|
return file_path.is_relative_to(base_dir)
|
|
|
|
|
|
|
|
class FileDeleteResource1(Resource):
|
|
@jwt_required()
|
|
def post(self):
|
|
"""文件删除接口[^1]"""
|
|
data = request.form
|
|
if 'uuid' not in data:
|
|
return APIResponse.error('缺少必要参数', 400)
|
|
|
|
try:
|
|
|
|
translate_record = Translate.query.filter_by(uuid=data['uuid']).first()
|
|
if not translate_record:
|
|
return APIResponse.error('文件记录不存在', 404)
|
|
|
|
|
|
file_path = translate_record.origin_filepath
|
|
|
|
|
|
if os.path.exists(file_path):
|
|
os.remove(file_path)
|
|
else:
|
|
current_app.logger.warning(f"文件不存在:{file_path}")
|
|
|
|
|
|
db.session.delete(translate_record)
|
|
db.session.commit()
|
|
|
|
return APIResponse.success(message='文件删除成功')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"文件删除失败:{str(e)}")
|
|
return APIResponse.error('文件删除失败', 500)
|
|
|
|
|
|
|
|
class FileDeleteResource(Resource):
|
|
@jwt_required()
|
|
def post(self):
|
|
"""文件删除接口"""
|
|
data = request.form
|
|
if 'uuid' not in data:
|
|
return APIResponse.error('缺少必要参数', 400)
|
|
|
|
try:
|
|
|
|
translate_record = Translate.query.filter_by(uuid=data['uuid']).first()
|
|
if not translate_record:
|
|
return APIResponse.error('文件记录不存在', 404)
|
|
|
|
|
|
file_path = Path(translate_record.origin_filepath)
|
|
|
|
|
|
if file_path.exists():
|
|
file_path.unlink()
|
|
else:
|
|
current_app.logger.warning(f"文件不存在:{file_path}")
|
|
|
|
|
|
db.session.delete(translate_record)
|
|
db.session.commit()
|
|
|
|
return APIResponse.success(message='文件删除成功')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
current_app.logger.error(f"文件删除失败:{str(e)}")
|
|
return APIResponse.error('文件删除失败', 500)
|
|
|