|
|
| 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)
|
|
|