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:
parent
e35635db4f
commit
55d4e52d86
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue