fix: address CodeRabbit review feedback on import/export

- projects/page.tsx: add aria-label="Import project" to the import
  button so screen readers have a name when the visible label is
  hidden on small screens.
- project-manager.ts: tighten import schema validation — accept only
  formatVersion === SUPPORTED_FORMAT_VERSION (1), reject non-string
  metadata.id/name (previously a numeric value passed the truthy
  check), require non-empty trimmed strings, and reject arrays/null
  payloads. Constant shared with the export path.
- storage/service.ts: await ensureMigrations() at the top of
  importProjectFromJSON so an import can't race with the memoized
  migration run and land in a half-migrated store.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rene Zander 2026-05-20 06:49:34 +00:00
parent e35635db4f
commit 55d4e52d86
3 changed files with 40 additions and 11 deletions

View File

@ -559,6 +559,7 @@ function ImportProjectButton() {
variant="outline"
className="flex px-5 md:px-6"
onClick={() => fileInputRef.current?.click()}
aria-label="Import project"
>
<HugeiconsIcon icon={Upload04Icon} className="size-4" />
<span className="text-sm font-medium hidden md:block">Import</span>

View File

@ -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<string, unknown>;
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<string, unknown>;
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<string, unknown>
| 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",

View File

@ -532,6 +532,9 @@ class StorageService {
}: {
serializedProject: SerializedProject;
}): Promise<void> {
// 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,