feat: implement ZIP file management for project auditing
- Added functionality to automatically load saved ZIP files from IndexedDB when creating audit tasks. - Introduced loading indicators and user feedback for ZIP file operations in the CreateTaskDialog component. - Enhanced project detail page to handle ZIP file audits, including error handling and user notifications. - Implemented methods for saving and deleting ZIP files in IndexedDB to streamline project management.
This commit is contained in:
parent
281ab2c9e7
commit
86e3892d45
|
|
@ -24,6 +24,7 @@ import { toast } from "sonner";
|
|||
import TerminalProgressDialog from "./TerminalProgressDialog";
|
||||
import { runRepositoryAudit } from "@/features/projects/services/repoScan";
|
||||
import { scanZipFile, validateZipFile } from "@/features/projects/services/repoZipScan";
|
||||
import { loadZipFile } from "@/shared/utils/zipStorage";
|
||||
|
||||
interface CreateTaskDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -40,6 +41,8 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
|||
const [showTerminalDialog, setShowTerminalDialog] = useState(false);
|
||||
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
|
||||
const [zipFile, setZipFile] = useState<File | null>(null);
|
||||
const [loadingZipFile, setLoadingZipFile] = useState(false);
|
||||
const [hasLoadedZip, setHasLoadedZip] = useState(false);
|
||||
|
||||
const [taskForm, setTaskForm] = useState<CreateAuditTaskForm>({
|
||||
project_id: "",
|
||||
|
|
@ -72,9 +75,40 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
|||
if (preselectedProjectId) {
|
||||
setTaskForm(prev => ({ ...prev, project_id: preselectedProjectId }));
|
||||
}
|
||||
// 重置ZIP文件状态
|
||||
setZipFile(null);
|
||||
setHasLoadedZip(false);
|
||||
}
|
||||
}, [open, preselectedProjectId]);
|
||||
|
||||
// 当项目ID变化时,尝试自动加载保存的ZIP文件
|
||||
useEffect(() => {
|
||||
const autoLoadZipFile = async () => {
|
||||
if (!taskForm.project_id || hasLoadedZip) return;
|
||||
|
||||
const project = projects.find(p => p.id === taskForm.project_id);
|
||||
if (!project || project.repository_type !== 'other') return;
|
||||
|
||||
try {
|
||||
setLoadingZipFile(true);
|
||||
const savedFile = await loadZipFile(taskForm.project_id);
|
||||
|
||||
if (savedFile) {
|
||||
setZipFile(savedFile);
|
||||
setHasLoadedZip(true);
|
||||
console.log('✓ 已自动加载保存的ZIP文件:', savedFile.name);
|
||||
toast.success(`已加载保存的ZIP文件: ${savedFile.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动加载ZIP文件失败:', error);
|
||||
} finally {
|
||||
setLoadingZipFile(false);
|
||||
}
|
||||
};
|
||||
|
||||
autoLoadZipFile();
|
||||
}, [taskForm.project_id, projects, hasLoadedZip]);
|
||||
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
|
@ -338,61 +372,86 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
|||
<Card className="bg-amber-50 border-amber-200">
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-900 text-sm">ZIP项目需要上传文件</p>
|
||||
<p className="text-xs text-amber-700 mt-1">
|
||||
该项目是通过ZIP上传创建的,请重新上传ZIP文件进行扫描
|
||||
</p>
|
||||
{loadingZipFile ? (
|
||||
<div className="flex items-center space-x-3 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
||||
<p className="text-sm text-blue-800">正在加载保存的ZIP文件...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="zipFile">上传ZIP文件</Label>
|
||||
<Input
|
||||
id="zipFile"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
console.log('📁 选择的文件:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
sizeMB: (file.size / 1024 / 1024).toFixed(2)
|
||||
});
|
||||
|
||||
const validation = validateZipFile(file);
|
||||
if (!validation.valid) {
|
||||
toast.error(validation.error || "文件无效");
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
setZipFile(file);
|
||||
|
||||
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
||||
const sizeKB = (file.size / 1024).toFixed(2);
|
||||
const sizeText = file.size >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`;
|
||||
|
||||
toast.success(`已选择文件: ${file.name} (${sizeText})`);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
{zipFile && (
|
||||
<p className="text-xs text-green-600">
|
||||
✓ 已选择: {zipFile.name} (
|
||||
{zipFile.size >= 1024 * 1024
|
||||
? `${(zipFile.size / 1024 / 1024).toFixed(2)} MB`
|
||||
: zipFile.size >= 1024
|
||||
? `${(zipFile.size / 1024).toFixed(2)} KB`
|
||||
: `${zipFile.size} B`
|
||||
})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : zipFile ? (
|
||||
<div className="flex items-start space-x-3 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<Info className="w-5 h-5 text-green-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-green-900 text-sm">已准备就绪</p>
|
||||
<p className="text-xs text-green-700 mt-1">
|
||||
使用保存的ZIP文件: {zipFile.name} (
|
||||
{zipFile.size >= 1024 * 1024
|
||||
? `${(zipFile.size / 1024 / 1024).toFixed(2)} MB`
|
||||
: zipFile.size >= 1024
|
||||
? `${(zipFile.size / 1024).toFixed(2)} KB`
|
||||
: `${zipFile.size} B`
|
||||
})
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setZipFile(null);
|
||||
setHasLoadedZip(false);
|
||||
}}
|
||||
>
|
||||
更换文件
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-900 text-sm">需要上传ZIP文件</p>
|
||||
<p className="text-xs text-amber-700 mt-1">
|
||||
未找到保存的ZIP文件,请上传文件进行扫描
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="zipFile">上传ZIP文件</Label>
|
||||
<Input
|
||||
id="zipFile"
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
console.log('📁 选择的文件:', {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
sizeMB: (file.size / 1024 / 1024).toFixed(2)
|
||||
});
|
||||
|
||||
const validation = validateZipFile(file);
|
||||
if (!validation.valid) {
|
||||
toast.error(validation.error || "文件无效");
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
setZipFile(file);
|
||||
setHasLoadedZip(true);
|
||||
|
||||
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
||||
const sizeKB = (file.size / 1024).toFixed(2);
|
||||
const sizeText = file.size >= 1024 * 1024 ? `${sizeMB} MB` : `${sizeKB} KB`;
|
||||
|
||||
toast.success(`已选择文件: ${file.name} (${sizeText})`);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export function SystemConfig() {
|
|||
const parsedConfig = JSON.parse(savedConfig);
|
||||
setConfig(parsedConfig);
|
||||
setConfigSource('runtime');
|
||||
toast.success("已加载运行时配置");
|
||||
console.log('已加载运行时配置');
|
||||
} else {
|
||||
// 使用构建时配置
|
||||
loadFromEnv();
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ import {
|
|||
FileText
|
||||
} from "lucide-react";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { runRepositoryAudit } from "@/features/projects/services";
|
||||
import { runRepositoryAudit, scanZipFile } from "@/features/projects/services";
|
||||
import type { Project, AuditTask, CreateProjectForm } from "@/shared/types";
|
||||
import { loadZipFile } from "@/shared/utils/zipStorage";
|
||||
import { toast } from "sonner";
|
||||
import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
|
||||
import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog";
|
||||
|
|
@ -81,34 +82,76 @@ export default function ProjectDetail() {
|
|||
|
||||
const handleRunAudit = async () => {
|
||||
if (!project || !id) return;
|
||||
if (!project.repository_url || project.repository_type !== 'github') {
|
||||
toast.error('请在项目中配置 GitHub 仓库地址');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setScanning(true);
|
||||
console.log('开始启动审计任务...');
|
||||
const taskId = await runRepositoryAudit({
|
||||
projectId: id,
|
||||
repoUrl: project.repository_url,
|
||||
branch: project.default_branch || 'main',
|
||||
githubToken: undefined,
|
||||
createdBy: undefined
|
||||
});
|
||||
|
||||
console.log('审计任务创建成功,taskId:', taskId);
|
||||
|
||||
// 显示终端进度窗口
|
||||
setCurrentTaskId(taskId);
|
||||
setShowTerminalDialog(true);
|
||||
|
||||
// 重新加载项目数据
|
||||
loadProjectData();
|
||||
} catch (e: any) {
|
||||
console.error('启动审计失败:', e);
|
||||
toast.error(e?.message || '启动审计失败');
|
||||
} finally {
|
||||
setScanning(false);
|
||||
|
||||
// 如果是GitHub项目且有仓库地址,直接启动审计
|
||||
if (project.repository_type === 'github' && project.repository_url) {
|
||||
try {
|
||||
setScanning(true);
|
||||
console.log('开始启动审计任务...');
|
||||
const taskId = await runRepositoryAudit({
|
||||
projectId: id,
|
||||
repoUrl: project.repository_url,
|
||||
branch: project.default_branch || 'main',
|
||||
githubToken: undefined,
|
||||
createdBy: undefined
|
||||
});
|
||||
|
||||
console.log('审计任务创建成功,taskId:', taskId);
|
||||
|
||||
// 显示终端进度窗口
|
||||
setCurrentTaskId(taskId);
|
||||
setShowTerminalDialog(true);
|
||||
|
||||
// 重新加载项目数据
|
||||
loadProjectData();
|
||||
} catch (e: any) {
|
||||
console.error('启动审计失败:', e);
|
||||
toast.error(e?.message || '启动审计失败');
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
} else {
|
||||
// 对于ZIP项目,尝试从IndexedDB加载保存的文件
|
||||
try {
|
||||
setScanning(true);
|
||||
const file = await loadZipFile(id);
|
||||
|
||||
if (file) {
|
||||
console.log('找到保存的ZIP文件,开始启动审计...');
|
||||
try {
|
||||
// 启动ZIP文件审计
|
||||
const taskId = await scanZipFile({
|
||||
projectId: id,
|
||||
zipFile: file,
|
||||
excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
|
||||
createdBy: 'local-user'
|
||||
});
|
||||
|
||||
console.log('审计任务创建成功,taskId:', taskId);
|
||||
|
||||
// 显示终端进度窗口
|
||||
setCurrentTaskId(taskId);
|
||||
setShowTerminalDialog(true);
|
||||
|
||||
// 重新加载项目数据
|
||||
loadProjectData();
|
||||
} catch (e: any) {
|
||||
console.error('启动审计失败:', e);
|
||||
toast.error(e?.message || '启动审计失败');
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
} else {
|
||||
setScanning(false);
|
||||
toast.error('未找到保存的ZIP文件,请通过"新建任务"上传');
|
||||
setShowCreateTaskDialog(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动审计失败:', error);
|
||||
setScanning(false);
|
||||
toast.error('读取ZIP文件失败,请通过"新建任务"重新上传');
|
||||
setShowCreateTaskDialog(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ import {
|
|||
CheckCircle
|
||||
} from "lucide-react";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { scanZipFile, validateZipFile } from "@/features/projects/services";
|
||||
import { validateZipFile } from "@/features/projects/services";
|
||||
import type { Project, CreateProjectForm } from "@/shared/types";
|
||||
import { saveZipFile } from "@/shared/utils/zipStorage";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
|
||||
import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog";
|
||||
|
||||
export default function Projects() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
|
@ -46,8 +46,6 @@ export default function Projects() {
|
|||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [showTerminalDialog, setShowTerminalDialog] = useState(false);
|
||||
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [projectToDelete, setProjectToDelete] = useState<Project | null>(null);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
|
@ -144,30 +142,29 @@ export default function Projects() {
|
|||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
// 模拟上传进度
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress(prev => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(progressInterval);
|
||||
return 100;
|
||||
}
|
||||
return prev + 20;
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// 创建项目
|
||||
const project = await api.createProject({
|
||||
...createForm,
|
||||
repository_type: "other"
|
||||
} as any);
|
||||
|
||||
// 模拟上传进度
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress(prev => {
|
||||
if (prev >= 90) {
|
||||
clearInterval(progressInterval);
|
||||
return 90;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
// 扫描ZIP文件
|
||||
const taskId = await scanZipFile({
|
||||
projectId: project.id,
|
||||
zipFile: file,
|
||||
excludePatterns: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
|
||||
createdBy: 'local-user' // 使用默认本地用户ID
|
||||
});
|
||||
// 保存ZIP文件到IndexedDB(使用项目ID作为key)
|
||||
try {
|
||||
await saveZipFile(project.id, file);
|
||||
} catch (error) {
|
||||
console.error('保存ZIP文件失败:', error);
|
||||
}
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(100);
|
||||
|
|
@ -177,9 +174,10 @@ export default function Projects() {
|
|||
resetCreateForm();
|
||||
loadProjects();
|
||||
|
||||
// 显示终端进度窗口
|
||||
setCurrentTaskId(taskId);
|
||||
setShowTerminalDialog(true);
|
||||
toast.success(`项目 "${project.name}" 已创建`, {
|
||||
description: 'ZIP文件已保存,您可以启动代码审计',
|
||||
duration: 4000
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Upload failed:', error);
|
||||
|
|
@ -520,8 +518,8 @@ export default function Projects() {
|
|||
<ul className="space-y-1 text-xs">
|
||||
<li>• 请确保 ZIP 文件包含完整的项目代码</li>
|
||||
<li>• 系统会自动排除 node_modules、.git 等目录</li>
|
||||
<li>• 上传后将立即开始代码分析</li>
|
||||
<li>• 分析完成后可在任务详情页查看结果</li>
|
||||
<li>• ZIP 文件会保存,只需上传一次</li>
|
||||
<li>• 创建后可在项目详情页启动多次审计</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -766,14 +764,6 @@ export default function Projects() {
|
|||
preselectedProjectId={selectedProjectForTask}
|
||||
/>
|
||||
|
||||
{/* 终端进度对话框 */}
|
||||
<TerminalProgressDialog
|
||||
open={showTerminalDialog}
|
||||
onOpenChange={setShowTerminalDialog}
|
||||
taskId={currentTaskId}
|
||||
taskType="zip"
|
||||
/>
|
||||
|
||||
{/* 编辑项目对话框 */}
|
||||
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { api } from "@/shared/config/database";
|
||||
import type { Project } from "@/shared/types";
|
||||
import { toast } from "sonner";
|
||||
import { deleteZipFile } from "@/shared/utils/zipStorage";
|
||||
|
||||
export default function RecycleBin() {
|
||||
const [deletedProjects, setDeletedProjects] = useState<Project[]>([]);
|
||||
|
|
@ -73,7 +74,16 @@ export default function RecycleBin() {
|
|||
if (!selectedProject) return;
|
||||
|
||||
try {
|
||||
// 删除项目数据
|
||||
await api.permanentlyDeleteProject(selectedProject.id);
|
||||
|
||||
// 删除保存的ZIP文件(如果有)
|
||||
try {
|
||||
await deleteZipFile(selectedProject.id);
|
||||
} catch (error) {
|
||||
console.error('删除ZIP文件失败:', error);
|
||||
}
|
||||
|
||||
toast.success(`项目 "${selectedProject.name}" 已永久删除`);
|
||||
setShowPermanentDeleteDialog(false);
|
||||
setSelectedProject(null);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
/**
|
||||
* ZIP文件存储工具
|
||||
* 用于管理保存在IndexedDB中的ZIP文件
|
||||
*/
|
||||
|
||||
const DB_NAME = 'xcodereviewer_files';
|
||||
const STORE_NAME = 'zipFiles';
|
||||
|
||||
/**
|
||||
* 保存ZIP文件到IndexedDB
|
||||
*/
|
||||
export async function saveZipFile(projectId: string, file: File): Promise<void> {
|
||||
// 检查浏览器是否支持IndexedDB
|
||||
if (!window.indexedDB) {
|
||||
throw new Error('您的浏览器不支持IndexedDB,无法保存ZIP文件');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 不指定版本号,让IndexedDB使用当前最新版本
|
||||
const dbRequest = indexedDB.open(DB_NAME);
|
||||
|
||||
dbRequest.onupgradeneeded = (event) => {
|
||||
try {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建对象存储失败:', error);
|
||||
reject(new Error('创建存储结构失败,请检查浏览器设置'));
|
||||
}
|
||||
};
|
||||
|
||||
dbRequest.onsuccess = async (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// 检查对象存储是否存在,如果不存在则需要升级数据库
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.close();
|
||||
// 增加版本号以触发onupgradeneeded
|
||||
const upgradeRequest = indexedDB.open(DB_NAME, db.version + 1);
|
||||
|
||||
upgradeRequest.onupgradeneeded = (event) => {
|
||||
try {
|
||||
const upgradeDb = (event.target as IDBOpenDBRequest).result;
|
||||
if (!upgradeDb.objectStoreNames.contains(STORE_NAME)) {
|
||||
upgradeDb.createObjectStore(STORE_NAME);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('升级数据库时创建对象存储失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
upgradeRequest.onsuccess = async (event) => {
|
||||
const upgradeDb = (event.target as IDBOpenDBRequest).result;
|
||||
await performSave(upgradeDb, file, projectId, resolve, reject);
|
||||
};
|
||||
|
||||
upgradeRequest.onerror = (event) => {
|
||||
const error = (event.target as IDBOpenDBRequest).error;
|
||||
console.error('升级数据库失败:', error);
|
||||
reject(new Error(`升级数据库失败: ${error?.message || '未知错误'}`));
|
||||
};
|
||||
} else {
|
||||
await performSave(db, file, projectId, resolve, reject);
|
||||
}
|
||||
};
|
||||
|
||||
dbRequest.onerror = (event) => {
|
||||
const error = (event.target as IDBOpenDBRequest).error;
|
||||
console.error('打开IndexedDB失败:', error);
|
||||
const errorMsg = error?.message || '未知错误';
|
||||
reject(new Error(`无法打开本地存储,可能是隐私模式或存储权限问题: ${errorMsg}`));
|
||||
};
|
||||
|
||||
dbRequest.onblocked = () => {
|
||||
console.warn('数据库被阻塞,可能有其他标签页正在使用');
|
||||
reject(new Error('数据库被占用,请关闭其他标签页后重试'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function performSave(
|
||||
db: IDBDatabase,
|
||||
file: File,
|
||||
projectId: string,
|
||||
resolve: () => void,
|
||||
reject: (error: Error) => void
|
||||
) {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
const putRequest = store.put({
|
||||
buffer: arrayBuffer,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
uploadedAt: new Date().toISOString()
|
||||
}, projectId);
|
||||
|
||||
putRequest.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('写入数据失败:', error);
|
||||
reject(new Error(`保存ZIP文件失败: ${error?.message || '未知错误'}`));
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
console.log(`ZIP文件已保存到项目 ${projectId} (${(file.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
db.close();
|
||||
resolve();
|
||||
};
|
||||
|
||||
transaction.onerror = (event) => {
|
||||
const error = (event.target as IDBTransaction).error;
|
||||
console.error('事务失败:', error);
|
||||
reject(new Error(`保存事务失败: ${error?.message || '未知错误'}`));
|
||||
};
|
||||
|
||||
transaction.onabort = () => {
|
||||
console.error('事务被中止');
|
||||
reject(new Error('保存操作被中止'));
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('保存ZIP文件时发生异常:', error);
|
||||
reject(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从IndexedDB加载ZIP文件
|
||||
*/
|
||||
export async function loadZipFile(projectId: string): Promise<File | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 不指定版本号,让IndexedDB使用当前最新版本
|
||||
const dbRequest = indexedDB.open(DB_NAME);
|
||||
|
||||
dbRequest.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
};
|
||||
|
||||
dbRequest.onsuccess = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.close();
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const getRequest = store.get(projectId);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
const savedFile = getRequest.result;
|
||||
|
||||
if (savedFile && savedFile.buffer) {
|
||||
const blob = new Blob([savedFile.buffer], { type: 'application/zip' });
|
||||
const file = new File([blob], savedFile.fileName, { type: 'application/zip' });
|
||||
resolve(file);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
getRequest.onerror = () => {
|
||||
reject(new Error('读取ZIP文件失败'));
|
||||
};
|
||||
};
|
||||
|
||||
dbRequest.onerror = () => {
|
||||
// 数据库打开失败,可能是首次使用,返回null而不是报错
|
||||
console.warn('打开ZIP文件数据库失败,可能是首次使用');
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除ZIP文件
|
||||
*/
|
||||
export async function deleteZipFile(projectId: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 不指定版本号,让IndexedDB使用当前最新版本
|
||||
const dbRequest = indexedDB.open(DB_NAME);
|
||||
|
||||
dbRequest.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
};
|
||||
|
||||
dbRequest.onsuccess = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.close();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const deleteRequest = store.delete(projectId);
|
||||
|
||||
deleteRequest.onsuccess = () => {
|
||||
console.log(`已删除项目 ${projectId} 的ZIP文件`);
|
||||
resolve();
|
||||
};
|
||||
|
||||
deleteRequest.onerror = () => {
|
||||
reject(new Error('删除ZIP文件失败'));
|
||||
};
|
||||
};
|
||||
|
||||
dbRequest.onerror = () => {
|
||||
// 数据库打开失败,可能文件不存在,直接resolve
|
||||
console.warn('打开ZIP文件数据库失败,跳过删除操作');
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在ZIP文件
|
||||
*/
|
||||
export async function hasZipFile(projectId: string): Promise<boolean> {
|
||||
try {
|
||||
const file = await loadZipFile(projectId);
|
||||
return file !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue