fix(project): prevent deleted projects from resurrecting

- Migrate legacy project zips by moving instead of copying
- Remove legacy zip copies on project delete
- Add contract test for legacy migration/delete behavior

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-22 15:29:22 +08:00
parent ffce066842
commit f823a7c05f
3 changed files with 53 additions and 2 deletions
+27 -1
View File
@@ -100,7 +100,21 @@ export class ZipProjectStore {
try {
const st = await fs.stat(from);
if (!st.isFile()) continue;
await fs.copyFile(from, to);
// Переносим (а не копируем), чтобы:
// - не было дублей между разными appName
// - удалённые пользователем проекты не «возрождались» при следующем ensureRoots()
try {
await fs.rename(from, to);
} catch {
await fs.copyFile(from, to);
try {
await rmWithRetries(fs.rm, from, { force: true });
} catch {
// best effort: если zip уже скопирован в dest, миграцию считаем успешной;
// legacy-копия может остаться (например из-за lock/AV), но удаление проекта
// затем чистит legacy по fileName.
}
}
destZips.add(name);
} catch {
/* ignore */
@@ -795,6 +809,18 @@ export class ZipProjectStore {
await rmWithRetries(fs.rm, zipPath, { force: true });
await rmWithRetries(fs.rm, cacheDir, { recursive: true, force: true });
// Если проект подтянулся миграцией из legacy userData (другое имя приложения),
// то после удаления из текущей папки он может снова появиться при следующем ensureRoots().
// Поэтому удаляем и legacy-копии архива.
for (const legacyRoot of getLegacyProjectsRootDirs()) {
const legacyZipPath = path.join(legacyRoot, entry.fileName);
try {
await rmWithRetries(fs.rm, legacyZipPath, { force: true });
} catch {
/* ignore */
}
}
}
private randomId(): string {