VWED_server/services/online_script/script_file_service.py

387 lines
16 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
脚本文件管理服务
提供脚本文件的CRUD操作和文件系统管理
"""
import os
import shutil
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
from sqlalchemy import and_, or_, select
from data.session import get_session, get_async_session
from data.models.script_project import VWEDScriptProject
from data.models.script_file import VWEDScriptFile
from utils.logger import get_logger
from config.settings import settings
logger = get_logger("services.script_file")
class ScriptFileService:
"""脚本文件管理服务"""
def __init__(self):
self.base_path = Path(settings.SCRIPT_SAVE_PATH)
self.base_path.mkdir(parents=True, exist_ok=True)
async def create_project(self, project_name: str, description: str = "",
created_by: str = None) -> Dict[str, Any]:
"""创建新项目"""
try:
# 创建项目路径
project_path = f"projects/{project_name}"
full_project_path = self.base_path / project_path
# 检查项目是否已存在
async with get_async_session() as session:
query = select(VWEDScriptProject).where(
VWEDScriptProject.project_name == project_name
)
result = await session.execute(query)
existing_project = result.scalar_one_or_none()
if existing_project:
return {"success": False, "error": f"项目 {project_name} 已存在"}
# 创建文件系统目录
full_project_path.mkdir(parents=True, exist_ok=True)
# 创建数据库记录
new_project = VWEDScriptProject(
project_name=project_name,
project_path=project_path,
description=description,
created_by=created_by
)
session.add(new_project)
await session.commit()
await session.refresh(new_project)
logger.info(f"项目创建成功: {project_name}")
return {
"success": True,
"project": new_project.to_dict(),
"message": f"项目 {project_name} 创建成功"
}
except Exception as e:
logger.error(f"创建项目失败: {e}", exc_info=True)
return {"success": False, "error": f"创建项目失败: {str(e)}"}
async def get_projects(self, status: str = "active") -> Dict[str, Any]:
"""获取项目列表"""
try:
async with get_async_session() as session:
query = select(VWEDScriptProject)
if status:
query = query.where(VWEDScriptProject.status == status)
result = await session.execute(query)
projects = result.scalars().all()
return {
"success": True,
"projects": [project.to_dict() for project in projects]
}
except Exception as e:
logger.error(f"获取项目列表失败: {e}", exc_info=True)
return {"success": False, "error": f"获取项目列表失败: {str(e)}"}
async def create_file(self, project_id: int, file_name: str, file_path: str,
content: str = "", file_type: str = "python",
created_by: str = None) -> Dict[str, Any]:
"""创建脚本文件"""
try:
async with get_async_session() as session:
# 检查项目是否存在
project = await session.get(VWEDScriptProject, project_id)
if not project:
return {"success": False, "error": f"项目ID {project_id} 不存在"}
# 构建完整文件路径
full_file_path = self.base_path / project.project_path / file_path
# 检查文件名是否已存在(同一项目下不能有重名文件)
name_query = select(VWEDScriptFile).where(
and_(VWEDScriptFile.project_id == project_id,
VWEDScriptFile.file_name == file_name)
)
name_result = await session.execute(name_query)
existing_names = name_result.scalars().all()
if existing_names:
return {"success": False, "error": f"同一项目下已存在名为 '{file_name}' 的文件"}
# 检查文件路径是否已存在
path_query = select(VWEDScriptFile).where(
and_(VWEDScriptFile.project_id == project_id,
VWEDScriptFile.file_path == file_path)
)
path_result = await session.execute(path_query)
existing_paths = path_result.scalars().all()
if existing_paths:
return {"success": False, "error": f"文件路径 '{file_path}' 已存在"}
# 创建目录
full_file_path.parent.mkdir(parents=True, exist_ok=True)
# 写入文件内容
with open(full_file_path, 'w', encoding='utf-8') as f:
f.write(content)
# 检测boot函数
has_boot_function = self._check_boot_function(content)
# 计算文件大小
file_size = full_file_path.stat().st_size
# 创建数据库记录
new_file = VWEDScriptFile(
project_id=project_id,
file_name=file_name,
file_path=full_file_path,
file_type=file_type,
content=content,
size=file_size,
has_boot_function=has_boot_function,
created_by=created_by
)
session.add(new_file)
await session.commit()
await session.refresh(new_file)
logger.info(f"文件创建成功: {full_file_path}")
return {
"success": True,
"file": new_file.to_dict(),
"message": f"文件 {file_name} 创建成功"
}
except Exception as e:
logger.error(f"创建文件失败: {e}", exc_info=True)
return {"success": False, "error": f"创建文件失败: {str(e)}"}
async def update_file_content(self, file_id: int, content: str,
updated_by: str = None) -> Dict[str, Any]:
"""更新文件内容"""
try:
async with get_async_session() as session:
script_file = await session.get(VWEDScriptFile, file_id)
if not script_file:
return {"success": False, "error": f"文件ID {file_id} 不存在"}
# 获取项目信息
project = await session.get(VWEDScriptProject, script_file.project_id)
full_file_path = self.base_path / project.project_path / script_file.file_path
# 备份原文件
backup_path = full_file_path.with_suffix(f".bak.{int(datetime.now().timestamp())}")
if full_file_path.exists():
shutil.copy2(full_file_path, backup_path)
# 写入新内容
with open(full_file_path, 'w', encoding='utf-8') as f:
f.write(content)
# 更新数据库记录
script_file.content = content
script_file.size = full_file_path.stat().st_size
script_file.has_boot_function = self._check_boot_function(content)
script_file.updated_at = datetime.now()
await session.commit()
logger.info(f"文件更新成功: {project.project_name}/{script_file.file_path}")
return {
"success": True,
"file": script_file.to_dict(),
"backup_path": str(backup_path),
"message": "文件内容更新成功"
}
except Exception as e:
logger.error(f"更新文件内容失败: {e}", exc_info=True)
return {"success": False, "error": f"更新文件内容失败: {str(e)}"}
async def get_file_content(self, file_id: int) -> Dict[str, Any]:
"""获取文件内容"""
try:
async with get_async_session() as session:
script_file = await session.get(VWEDScriptFile, file_id)
if not script_file:
return {"success": False, "error": f"文件ID {file_id} 不存在"}
# 从数据库读取内容(优先)
if script_file.content:
return {
"success": True,
"file": script_file.to_dict(),
"content": script_file.content
}
# 从文件系统读取
project = await session.get(VWEDScriptProject, script_file.project_id)
full_file_path = self.base_path / project.project_path / script_file.file_path
if not full_file_path.exists():
return {"success": False, "error": f"物理文件不存在: {script_file.file_path}"}
with open(full_file_path, 'r', encoding='utf-8') as f:
content = f.read()
return {
"success": True,
"file": script_file.to_dict(),
"content": content
}
except Exception as e:
logger.error(f"获取文件内容失败: {e}", exc_info=True)
return {"success": False, "error": f"获取文件内容失败: {str(e)}"}
async def get_project_files(self, project_id: int, include_content: bool = False) -> Dict[str, Any]:
"""获取项目的所有文件"""
try:
async with get_async_session() as session:
project = await session.get(VWEDScriptProject, project_id)
if not project:
return {"success": False, "error": f"项目ID {project_id} 不存在"}
query = select(VWEDScriptFile).where(
VWEDScriptFile.project_id == project_id
)
result = await session.execute(query)
files = result.scalars().all()
file_list = []
for file in files:
file_dict = file.to_dict()
if include_content and file.content:
file_dict['content'] = file.content
file_list.append(file_dict)
return {
"success": True,
"project": project.to_dict(),
"files": file_list
}
except Exception as e:
logger.error(f"获取项目文件列表失败: {e}", exc_info=True)
return {"success": False, "error": f"获取项目文件列表失败: {str(e)}"}
async def delete_file(self, file_id: int) -> Dict[str, Any]:
"""删除文件"""
try:
async with get_async_session() as session:
script_file = await session.get(VWEDScriptFile, file_id)
if not script_file:
return {"success": False, "error": f"文件ID {file_id} 不存在"}
project = await session.get(VWEDScriptProject, script_file.project_id)
full_file_path = self.base_path / project.project_path / script_file.file_path
# 删除物理文件
if full_file_path.exists():
full_file_path.unlink()
# 删除数据库记录
await session.delete(script_file)
await session.commit()
logger.info(f"文件删除成功: {project.project_name}/{script_file.file_path}")
return {
"success": True,
"message": f"文件 {script_file.file_name} 删除成功"
}
except Exception as e:
logger.error(f"删除文件失败: {e}", exc_info=True)
return {"success": False, "error": f"删除文件失败: {str(e)}"}
async def search_files(self, keyword: str, project_id: int = None,
file_type: str = None, has_boot: bool = None) -> Dict[str, Any]:
"""搜索文件"""
try:
async with get_async_session() as session:
query = select(VWEDScriptFile)
# 关键字搜索(文件名或内容)
if keyword:
query = query.where(
or_(VWEDScriptFile.file_name.contains(keyword),
VWEDScriptFile.content.contains(keyword))
)
# 项目筛选
if project_id:
query = query.where(VWEDScriptFile.project_id == project_id)
# 文件类型筛选
if file_type:
query = query.where(VWEDScriptFile.file_type == file_type)
# boot函数筛选
if has_boot is not None:
query = query.where(VWEDScriptFile.has_boot_function == has_boot)
result = await session.execute(query)
files = result.scalars().all()
# 加载项目信息
file_list = []
for file in files:
project = await session.get(VWEDScriptProject, file.project_id)
file_dict = file.to_dict()
file_dict['project_name'] = project.project_name
file_list.append(file_dict)
return {
"success": True,
"files": file_list,
"count": len(file_list)
}
except Exception as e:
logger.error(f"搜索文件失败: {e}", exc_info=True)
return {"success": False, "error": f"搜索文件失败: {str(e)}"}
def _check_boot_function(self, content: str) -> bool:
"""检查是否包含boot函数"""
try:
# 简单检查是否定义了boot函数
return "def boot(" in content or "def boot():" in content
except:
return False
async def validate_script_syntax(self, content: str) -> Dict[str, Any]:
"""验证脚本语法"""
try:
compile(content, '<script>', 'exec')
return {"success": True, "valid": True, "message": "语法检查通过"}
except SyntaxError as e:
return {
"success": True,
"valid": False,
"error": f"语法错误: {e.msg}",
"line": e.lineno,
"offset": e.offset
}
except Exception as e:
return {"success": False, "error": f"语法检查失败: {str(e)}"}
# 全局文件服务实例
_file_service = ScriptFileService()
def get_file_service() -> ScriptFileService:
"""获取文件服务实例"""
return _file_service