diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 3bb849b7..88b0a4e8 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -559,6 +559,7 @@ function ImportProjectButton() { variant="outline" className="flex px-5 md:px-6" onClick={() => fileInputRef.current?.click()} + aria-label="Import project" > Import diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts index 4ea00dc8..dfa29e2c 100644 --- a/apps/web/src/core/managers/project-manager.ts +++ b/apps/web/src/core/managers/project-manager.ts @@ -31,6 +31,8 @@ import { getElementFontFamilies } from "@/lib/timeline/element-utils"; import { getRaisedProjectFpsForImportedMedia } from "@/lib/fps/utils"; import type { MediaAsset } from "@/lib/media/types"; +const SUPPORTED_FORMAT_VERSION = 1; + export interface MigrationState { isMigrating: boolean; fromVersion: number | null; @@ -651,7 +653,7 @@ export class ProjectManager { } const exportData = { - formatVersion: 1, + formatVersion: SUPPORTED_FORMAT_VERSION, exportedAt: new Date().toISOString(), project: serializedProject, }; @@ -701,13 +703,10 @@ export class ProjectManager { return null; } - const data = parsed as Record; - if ( - !data || - typeof data !== "object" || - !("formatVersion" in data) || - !("project" in data) + !parsed || + typeof parsed !== "object" || + Array.isArray(parsed) ) { toast.error("Invalid file format", { description: "This does not appear to be an OpenCut project file", @@ -715,12 +714,38 @@ export class ProjectManager { return null; } - const serializedProject = data.project as SerializedProject; + const data = parsed as Record; + + if (data.formatVersion !== SUPPORTED_FORMAT_VERSION) { + toast.error("Unsupported file version", { + description: `Expected formatVersion ${SUPPORTED_FORMAT_VERSION}, got ${JSON.stringify(data.formatVersion)}`, + }); + return null; + } if ( - !serializedProject?.metadata?.id || - !serializedProject?.metadata?.name || - !Array.isArray(serializedProject?.scenes) + !data.project || + typeof data.project !== "object" || + Array.isArray(data.project) + ) { + toast.error("Invalid project data", { + description: "The project data is incomplete or corrupted", + }); + return null; + } + + const serializedProject = data.project as SerializedProject; + const meta = serializedProject.metadata as unknown as + | Record + | undefined; + + if ( + !meta || + typeof meta.id !== "string" || + meta.id.trim() === "" || + typeof meta.name !== "string" || + meta.name.trim() === "" || + !Array.isArray(serializedProject.scenes) ) { toast.error("Invalid project data", { description: "The project data is incomplete or corrupted", diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts index e29be15b..23983c31 100644 --- a/apps/web/src/services/storage/service.ts +++ b/apps/web/src/services/storage/service.ts @@ -532,6 +532,9 @@ class StorageService { }: { serializedProject: SerializedProject; }): Promise { + // Migrations must complete before any write — otherwise the import races + // with the memoized ensureMigrations() and may end up in a half-migrated store. + await this.ensureMigrations(); await this.projectsAdapter.set( serializedProject.metadata.id, serializedProject,