0
0
Files
vs-p/out/panels/services/ProjectService.js
2025-12-09 09:38:32 +08:00

730 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectService = void 0;
// src/panels/services/ProjectService.ts
const vscode = __importStar(require("vscode"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const StorageService_1 = require("./StorageService");
/**
* 项目数据管理服务
*/
class ProjectService {
constructor() {
this.projects = [];
this.aircrafts = [];
this.containers = [];
this.configs = [];
this.moduleFolders = [];
this.projectPaths = new Map();
}
/**
* 生成唯一ID
*/
generateUniqueId(prefix, _existingItems = []) {
const randomPart = `${Date.now().toString(36)}${Math.random().toString(36).substring(2, 8)}`;
return `${prefix}${randomPart}`;
}
/**
* [新增] 文件名/文件夹名净化:只允许中文、字母、数字、-、_
*/
sanitizeFileName(name) {
// 允许中文、字母、数字、-、_
let fileName = name.trim().toLowerCase();
// 1. 替换所有空格为下划线
fileName = fileName.replace(/\s+/g, '_');
// 2. 移除所有不被允许的特殊字符(只保留中文、字母、数字、-、_
fileName = fileName.replace(/[^\u4e00-\u9fa5a-z0-9_-]/g, '');
// 3. 确保不为空
if (fileName.length === 0) {
return 'config_file';
}
return fileName;
}
// =============== 项目相关方法 ===============
getProjects() {
return this.projects;
}
getProjectPaths() {
return this.projectPaths;
}
getProjectPath(projectId) {
return this.projectPaths.get(projectId);
}
setProjectPath(projectId, pathStr) {
this.projectPaths.set(projectId, pathStr);
}
async createProject(name) {
const newId = this.generateUniqueId('p', this.projects);
const newProject = {
id: newId,
name: name
};
this.projects.push(newProject);
return newId;
}
updateProjectName(projectId, newName) {
const project = this.projects.find(p => p.id === projectId);
if (project) {
project.name = newName;
return true;
}
return false;
}
deleteProject(projectId) {
const project = this.projects.find(p => p.id === projectId);
if (!project)
return false;
// 先把要删的 id 都算出来,再统一过滤
const relatedAircrafts = this.aircrafts.filter(a => a.projectId === projectId);
const aircraftIds = relatedAircrafts.map(a => a.id);
const relatedContainers = this.containers.filter(c => aircraftIds.includes(c.aircraftId));
const containerIds = relatedContainers.map(c => c.id);
// 真正删除数据
this.projects = this.projects.filter(p => p.id !== projectId);
this.aircrafts = this.aircrafts.filter(a => a.projectId !== projectId);
this.containers = this.containers.filter(c => !aircraftIds.includes(c.aircraftId));
this.configs = this.configs.filter(cfg => !containerIds.includes(cfg.containerId));
this.moduleFolders = this.moduleFolders.filter(folder => !containerIds.includes(folder.containerId));
this.projectPaths.delete(projectId);
return true;
}
// =============== 飞行器相关方法 ===============
getAircraftsByProject(projectId) {
return this.aircrafts.filter(a => a.projectId === projectId);
}
getAircraft(aircraftId) {
return this.aircrafts.find(a => a.id === aircraftId);
}
async createAircraft(name, projectId) {
const newId = this.generateUniqueId('a', this.aircrafts);
const newAircraft = {
id: newId,
name: name,
projectId: projectId
};
this.aircrafts.push(newAircraft);
// 创建飞行器目录
await this.createAircraftDirectory(newAircraft);
return newId;
}
updateAircraftName(aircraftId, newName) {
const aircraft = this.aircrafts.find(a => a.id === aircraftId);
if (aircraft) {
aircraft.name = newName;
return true;
}
return false;
}
deleteAircraft(aircraftId) {
const aircraft = this.aircrafts.find(a => a.id === aircraftId);
if (!aircraft)
return false;
// ⚠️ 先算出要删的 containerIds
const relatedContainers = this.containers.filter(c => c.aircraftId === aircraftId);
const containerIds = relatedContainers.map(c => c.id);
// 删除飞机自身和容器
this.aircrafts = this.aircrafts.filter(a => a.id !== aircraftId);
this.containers = this.containers.filter(c => c.aircraftId !== aircraftId);
// 再删关联的 config 和 moduleFolder
this.configs = this.configs.filter(cfg => !containerIds.includes(cfg.containerId));
this.moduleFolders = this.moduleFolders.filter(folder => !containerIds.includes(folder.containerId));
return true;
}
/**
* 从已存在的磁盘目录导入飞行器
* 不会创建/删除任何文件,只在内存中补充 Aircraft / Container / ModuleFolder 数据
*/
async importAircraftFromExistingFolder(projectId, aircraftName) {
// 已存在同名飞行器直接返回
const existed = this.getAircraftsByProject(projectId).find(a => a.name === aircraftName);
if (existed) {
return existed.id;
}
const newId = this.generateUniqueId('a', this.aircrafts);
const aircraft = {
id: newId,
name: aircraftName,
projectId
};
this.aircrafts.push(aircraft);
const projectPath = this.projectPaths.get(projectId);
if (!projectPath) {
return newId;
}
const aircraftDir = path.join(projectPath, aircraftName);
if (!fs.existsSync(aircraftDir)) {
// 目录不存在就不再解析子目录
return newId;
}
// 每个子目录视为一个容器(排除 .git
const entries = await fs.promises.readdir(aircraftDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
if (entry.name === '.git')
continue;
await this.importContainerFromExistingFolder(newId, entry.name);
}
return newId;
}
// =============== 容器相关方法 ===============
/**
* ⚠️ 这里不再做 .git 过滤:
* - 我们只会通过 createContainer / importContainerFromExistingFolder 创建容器
* - importContainerFromExistingFolder 内部已经排除了 .git
* 所以不会出现名字叫 ".git" 的容器
*/
getContainersByAircraft(aircraftId) {
return this.containers.filter(c => c.aircraftId === aircraftId);
}
getContainer(containerId) {
return this.containers.find(c => c.id === containerId);
}
async createContainer(name, aircraftId) {
const newId = this.generateUniqueId('c', this.containers);
const newContainer = {
id: newId,
name: name,
aircraftId: aircraftId
};
this.containers.push(newContainer);
await this.createContainerDirectory(newContainer);
// UI 手动创建的容器,仍然保留“默认两个配置”的行为
await this.createDefaultConfigs(newContainer);
return newId;
}
updateContainerName(containerId, newName) {
const container = this.containers.find(c => c.id === containerId);
if (container) {
container.name = newName;
return true;
}
return false;
}
deleteContainer(containerId) {
const container = this.containers.find(c => c.id === containerId);
if (!container)
return false;
this.containers = this.containers.filter(c => c.id !== containerId);
this.configs = this.configs.filter(cfg => cfg.containerId !== containerId);
this.moduleFolders = this.moduleFolders.filter(folder => folder.containerId !== containerId);
return true;
}
/**
* 从已存在的磁盘目录导入容器
*
* ✅ 你的最新需求:
* - 不创建“默认两个配置”
* - 自动扫描容器目录下的『子文件夹』
* - 每个子文件夹创建一个 ModuleFoldertype: 'local'
* - 排除 `.git` 子文件夹
*/
async importContainerFromExistingFolder(aircraftId, containerName) {
if (containerName === '.git') {
throw new Error('不能将 .git 导入为容器');
}
const existed = this.getContainersByAircraft(aircraftId).find(c => c.name === containerName);
if (existed) {
return existed.id;
}
const newId = this.generateUniqueId('c', this.containers);
const container = {
id: newId,
name: containerName,
aircraftId
};
this.containers.push(container);
// 🚩 关键逻辑:扫描容器目录下的子文件夹 -> 创建 ModuleFolder
await this.scanContainerModuleFolders(container);
// 不再创建默认两个配置(不调用 createDefaultConfigs
// 也不创建目录(目录是 Git 克隆出来的,本来就存在)
return newId;
}
/**
* 扫描容器目录中的子文件夹(不含 .git将其作为本地模块文件夹记录
*/
async scanContainerModuleFolders(container) {
try {
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft)
return;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return;
const containerDir = path.join(projectPath, aircraft.name, container.name);
if (!fs.existsSync(containerDir)) {
return;
}
const entries = await fs.promises.readdir(containerDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
if (entry.name === '.git')
continue;
const folderId = this.generateUniqueId('local-', this.moduleFolders);
const relativePath = `/${aircraft.projectId}/${aircraft.name}/${container.name}/${entry.name}`;
const moduleFolder = {
id: folderId,
name: entry.name,
type: 'local',
localPath: relativePath,
containerId: container.id
};
this.moduleFolders.push(moduleFolder);
}
}
catch (error) {
console.error('扫描容器目录生成模块文件夹失败:', error);
}
}
// =============== 配置相关方法 ===============
getConfigsByContainer(containerId) {
return this.configs.filter(cfg => cfg.containerId === containerId);
}
getConfig(configId) {
return this.configs.find(c => c.id === configId);
}
async createConfig(name, containerId) {
const newId = this.generateUniqueId('cfg', this.configs);
// 使用新增的 sanitizeFileName 方法来处理文件名
const newFileName = this.sanitizeFileName(name);
const newConfig = {
id: newId,
name: name,
fileName: newFileName,
containerId: containerId
};
this.configs.push(newConfig);
await this.ensureContainerDirectoryExists(containerId);
return newId;
}
/**
* [修改] 同时更新配置显示名称和文件名称
*/
updateConfigName(configId, newName, newFileName) {
const config = this.configs.find(c => c.id === configId);
if (config) {
config.name = newName;
config.fileName = newFileName;
return true;
}
return false;
}
deleteConfig(configId) {
const config = this.configs.find(c => c.id === configId);
if (!config)
return false;
this.configs = this.configs.filter(c => c.id !== configId);
return true;
}
/**
* [新增] 重命名磁盘上的配置文件
*/
async renameConfigFileOnDisk(configId, newFileName) {
const config = this.configs.find(c => c.id === configId);
if (!config)
return false;
const oldFilePath = this.getConfigFilePath(configId);
// 为了计算新路径,临时使用新的文件名
const oldFileName = config.fileName;
config.fileName = newFileName;
const newFilePath = this.getConfigFilePath(configId);
// 恢复旧的文件名,因为 ProjectService.updateConfigName 会在 ConfigPanel 中调用
config.fileName = oldFileName;
if (!oldFilePath || !newFilePath) {
return false;
}
if (!fs.existsSync(oldFilePath)) {
console.warn(`旧配置文件不存在,跳过磁盘重命名: ${oldFilePath}`);
return false;
}
try {
await fs.promises.rename(oldFilePath, newFilePath);
console.log(`✅ 已重命名配置文件: ${oldFilePath} -> ${newFilePath}`);
return true;
}
catch (error) {
console.error(`重命名配置文件失败: ${error}`);
throw error;
}
}
// =============== 模块文件夹相关方法 ===============
getModuleFoldersByContainer(containerId) {
return this.moduleFolders.filter(folder => folder.containerId === containerId);
}
getModuleFolder(folderId) {
return this.moduleFolders.find(f => f.id === folderId);
}
addModuleFolder(folder) {
this.moduleFolders.push(folder);
}
updateModuleFolder(folderId, updates) {
const folderIndex = this.moduleFolders.findIndex(f => f.id === folderId);
if (folderIndex === -1)
return false;
this.moduleFolders[folderIndex] = {
...this.moduleFolders[folderIndex],
...updates
};
return true;
}
deleteModuleFolder(folderId) {
const folder = this.moduleFolders.find(f => f.id === folderId);
if (!folder)
return false;
this.moduleFolders = this.moduleFolders.filter(f => f.id !== folderId);
return true;
}
/**
* [删除] 此方法已被 ConfigPanel 中的 updateModuleFolder 替换,以支持同步磁盘操作。
*/
// renameModuleFolder(folderId: string, newName: string): boolean {
// const folder = this.moduleFolders.find(f => f.id === folderId);
// if (!folder) return false;
//
// const oldName = folder.localPath.split('/').pop() || '';
// folder.localPath = folder.localPath.replace(/\/[^/]+$/, '/' + newName);
// return true;
// }
// =============== 文件系统操作 (新增/修改) ===============
/**
* 获取项目目录重命名所需的旧路径和新路径(新增)
*/
getProjectOldAndNewPaths(projectId, newName) {
const project = this.projects.find(p => p.id === projectId);
const oldPath = this.projectPaths.get(projectId);
if (!project || !oldPath)
return null;
const parentDir = path.dirname(oldPath);
// 新路径使用新的项目名称作为文件夹名
const newPath = path.join(parentDir, newName);
return { oldPath, newPath };
}
/**
* 获取飞行器目录的完整路径
*/
getAircraftDirectoryPath(aircraftId) {
const aircraft = this.getAircraft(aircraftId);
if (!aircraft)
return null;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return null;
return path.join(projectPath, aircraft.name);
}
/**
* 获取飞行器目录重命名所需的旧路径和新路径(新增)
* 注意oldPath使用内存中当前的名称newPath使用将要更新的新名称
*/
getAircraftOldAndNewPaths(aircraftId, newName) {
const aircraft = this.aircrafts.find(a => a.id === aircraftId);
if (!aircraft)
return null;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return null;
const oldPath = path.join(projectPath, aircraft.name);
const newPath = path.join(projectPath, newName);
return { oldPath, newPath };
}
/**
* 获取容器目录的完整路径
*/
getContainerDirectoryPath(containerId) {
const container = this.getContainer(containerId);
if (!container)
return null;
const aircraft = this.getAircraft(container.aircraftId);
if (!aircraft)
return null;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return null;
return path.join(projectPath, aircraft.name, container.name);
}
/**
* 获取容器目录重命名所需的旧路径和新路径(新增)
* 注意oldPath使用内存中当前的名称newPath使用将要更新的新名称
*/
getContainerOldAndNewPaths(containerId, newName) {
const container = this.containers.find(c => c.id === containerId);
if (!container)
return null;
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft)
return null;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return null;
const aircraftDir = path.join(projectPath, aircraft.name);
const oldPath = path.join(aircraftDir, container.name);
const newPath = path.join(aircraftDir, newName);
return { oldPath, newPath };
}
/**
* 递归删除目录
*/
async deleteDirectoryFromDisk(directoryPath) {
if (!directoryPath)
return false;
try {
if (fs.existsSync(directoryPath)) {
await fs.promises.rm(directoryPath, { recursive: true, force: true });
console.log(`✅ 已删除目录: ${directoryPath}`);
return true;
}
return false;
}
catch (error) {
console.error(`删除目录失败: ${error}`);
throw error; // Re-throw to be handled by ConfigPanel
}
}
/**
* 重命名磁盘上的目录(新增)
*/
async renameDirectoryOnDisk(oldPath, newPath) {
if (!oldPath || !newPath)
return false;
try {
if (fs.existsSync(oldPath)) {
await fs.promises.rename(oldPath, newPath);
console.log(`✅ 已重命名目录: ${oldPath} -> ${newPath}`);
return true;
}
console.warn(`目录不存在,跳过重命名: ${oldPath}`);
return false;
}
catch (error) {
console.error(`重命名目录失败: ${error}`);
throw error; // Re-throw to be handled by ConfigPanel
}
}
async createAircraftDirectory(aircraft) {
try {
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath) {
console.warn('未找到项目路径,跳过创建飞行器目录');
return;
}
const aircraftDir = vscode.Uri.joinPath(vscode.Uri.file(projectPath), aircraft.name);
await vscode.workspace.fs.createDirectory(aircraftDir);
console.log(`✅ 创建飞行器目录: ${aircraftDir.fsPath}`);
}
catch (error) {
console.error(`创建飞行器目录失败: ${error}`);
}
}
async createContainerDirectory(container) {
try {
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft) {
console.warn('未找到对应的飞行器,跳过创建容器目录');
return;
}
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath) {
console.warn('未找到项目路径,跳过创建容器目录');
return;
}
const aircraftDir = vscode.Uri.joinPath(vscode.Uri.file(projectPath), aircraft.name);
const containerDir = vscode.Uri.joinPath(aircraftDir, container.name);
await vscode.workspace.fs.createDirectory(aircraftDir);
await vscode.workspace.fs.createDirectory(containerDir);
console.log(`✅ 创建容器目录: ${containerDir.fsPath}`);
}
catch (error) {
console.error(`创建容器目录失败: ${error}`);
}
}
async ensureContainerDirectoryExists(containerId) {
try {
const container = this.containers.find(c => c.id === containerId);
if (!container)
return;
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft)
return;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return;
const aircraftDir = vscode.Uri.joinPath(vscode.Uri.file(projectPath), aircraft.name);
const containerDir = vscode.Uri.joinPath(aircraftDir, container.name);
await vscode.workspace.fs.createDirectory(aircraftDir);
await vscode.workspace.fs.createDirectory(containerDir);
}
catch (error) {
console.error(`确保容器目录存在失败: ${error}`);
}
}
/**
* 仅用于「新建容器」时的默认两个配置
* (导入容器时不会调用)
*/
async createDefaultConfigs(container) {
this.configs.push({
id: this.generateUniqueId('cfg', this.configs),
name: '配置1',
fileName: 'dockerfile',
containerId: container.id
});
this.configs.push({
id: this.generateUniqueId('cfg', this.configs),
name: '配置2',
fileName: 'docker-compose.yml',
containerId: container.id
});
}
// =============== 数据持久化 ===============
async saveCurrentProjectData(projectId) {
try {
const projectPath = this.projectPaths.get(projectId);
if (!projectPath) {
console.warn('未找到项目存储路径,数据将不会保存');
return false;
}
const data = this.getProjectData(projectId);
return await StorageService_1.StorageService.saveProjectData(projectPath, data);
}
catch (error) {
console.error('保存项目数据失败:', error);
return false;
}
}
async loadProjectData(projectPath) {
try {
const data = await StorageService_1.StorageService.loadProjectData(projectPath);
if (!data) {
return null;
}
const projectId = data.projects[0]?.id;
if (!projectId) {
return null;
}
// 先根据旧的 projectId 算出要清理的 aircraft / container / config / moduleFolder
const existingAircrafts = this.aircrafts.filter(a => a.projectId === projectId);
const aircraftIds = existingAircrafts.map(a => a.id);
const existingContainers = this.containers.filter(c => aircraftIds.includes(c.aircraftId));
const containerIds = existingContainers.map(c => c.id);
// 清理旧数据(同一个 projectId 的)
this.projects = this.projects.filter(p => p.id !== projectId);
this.aircrafts = this.aircrafts.filter(a => a.projectId !== projectId);
this.containers = this.containers.filter(c => !aircraftIds.includes(c.aircraftId));
this.configs = this.configs.filter(cfg => !containerIds.includes(cfg.containerId));
this.moduleFolders = this.moduleFolders.filter(folder => !containerIds.includes(folder.containerId));
// ⭐ 载入新数据时顺便过滤掉名字为 ".git" 的容器(避免历史数据带进来)
const cleanedContainers = data.containers.filter(c => c.name !== '.git');
this.projects.push(...data.projects);
this.aircrafts.push(...data.aircrafts);
this.containers.push(...cleanedContainers);
this.configs.push(...data.configs);
this.moduleFolders.push(...data.moduleFolders);
this.projectPaths.set(projectId, projectPath);
return projectId;
}
catch (error) {
console.error('加载项目数据失败:', error);
return null;
}
}
getProjectData(projectId) {
return {
projects: [this.projects.find(p => p.id === projectId)],
aircrafts: this.aircrafts.filter(a => a.projectId === projectId),
containers: this.containers.filter(c => {
const aircraft = this.aircrafts.find(a => a.id === c.aircraftId);
return aircraft && aircraft.projectId === projectId;
}),
configs: this.configs.filter(cfg => {
const container = this.containers.find(c => c.id === cfg.containerId);
if (!container)
return false;
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
return aircraft && aircraft.projectId === projectId;
}),
moduleFolders: this.moduleFolders.filter(folder => {
const container = this.containers.find(c => c.id === folder.containerId);
if (!container)
return false;
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
return aircraft && aircraft.projectId === projectId;
})
};
}
// =============== 工具方法 ===============
getModuleFolderFullPath(folder) {
const container = this.containers.find(c => c.id === folder.containerId);
if (!container)
return null;
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft)
return null;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return null;
const pathParts = folder.localPath.split('/').filter(part => part);
if (pathParts.length < 4)
return null;
// 路径的最后一部分是文件夹的名称
const folderName = pathParts[pathParts.length - 1];
return path.join(projectPath, aircraft.name, container.name, folderName);
}
getConfigFilePath(configId) {
const config = this.configs.find(c => c.id === configId);
if (!config)
return null;
const container = this.containers.find(c => c.id === config.containerId);
if (!container)
return null;
const aircraft = this.aircrafts.find(a => a.id === container.aircraftId);
if (!aircraft)
return null;
const projectPath = this.projectPaths.get(aircraft.projectId);
if (!projectPath)
return null;
return path.join(projectPath, aircraft.name, container.name, config.fileName);
}
async deleteConfigFileFromDisk(configId) {
const filePath = this.getConfigFilePath(configId);
if (!filePath)
return false;
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
console.log(`✅ 已删除配置文件: ${filePath}`);
return true;
}
return false;
}
catch (error) {
console.error(`删除配置文件失败: ${error}`);
return false;
}
}
}
exports.ProjectService = ProjectService;
//# sourceMappingURL=ProjectService.js.map