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 {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"build:obfuscate": "node scripts/build.mjs --production --obfuscate",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"typecheck": "tsc -p tsconfig.eslint.json --noEmit",
|
||||
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/editor/state/projectState.race.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs",
|
||||
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/editor/state/projectState.race.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs",
|
||||
"format": "prettier . --check",
|
||||
"format:write": "prettier . --write",
|
||||
"release:info": "node scripts/print-release-info.mjs",
|
||||
|
||||
Reference in New Issue
Block a user