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:
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
void test('zipStore: deleteProjectById removes legacy sibling copies by entry.fileName', () => {
|
||||
const src = fs.readFileSync(path.join(here, 'zipStore.ts'), 'utf8');
|
||||
assert.match(src, /async deleteProjectById/);
|
||||
assert.match(src, /for \(const legacyRoot of getLegacyProjectsRootDirs\(\)\)/);
|
||||
assert.match(src, /path\.join\(legacyRoot, entry\.fileName\)/);
|
||||
assert.match(src, /rmWithRetries\(fs\.rm, legacyZipPath, \{ force: true \}\)/);
|
||||
});
|
||||
|
||||
void test('zipStore: legacy migration moves or copy\\+rm so deleted projects are not resurrected', () => {
|
||||
const src = fs.readFileSync(path.join(here, 'zipStore.ts'), 'utf8');
|
||||
assert.match(src, /migrateLegacyProjectZipsIfNeeded/);
|
||||
assert.match(src, /if \(destZips\.has\(name\)\) continue/);
|
||||
assert.match(src, /await fs\.rename\(from, to\)/);
|
||||
assert.match(src, /await fs\.copyFile\(from, to\)/);
|
||||
assert.match(src, /rmWithRetries\(fs\.rm, from, \{ force: true \}\)/);
|
||||
assert.match(src, /не «возрождались»/);
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user