diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:20 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-27 01:21:20 +0200 |
| commit | 30a36e45686db503fded68e4da5d4c465a06f978 (patch) | |
| tree | 2dbe5e2aaef543155b021f5f4c504fad77b40648 /apps/web/src/project/transfer/migrate.ts | |
| parent | d55d2583a71ae35fa8d0d1bfbec975e67e66aafa (diff) | |
| download | opencut-30a36e45686db503fded68e4da5d4c465a06f978.tar.gz | |
feat: single-file project import/export (.ocp)
fflate zip container: project.json (serialized project incl. schema
version) + media files with a manifest. Export gathers all project
media (missing blobs warn, never abort) after save-flush; import
validates everything before any write, migrates older archives through
the storage migration registry, remaps media ids, and creates a new
project with rollback on failure. Entry points: projects page
import/export, editor header menu, export-project-archive action.
29 tests incl. full round-trip.
Diffstat (limited to 'apps/web/src/project/transfer/migrate.ts')
| -rw-r--r-- | apps/web/src/project/transfer/migrate.ts | 75 |
1 files changed, 75 insertions, 0 deletions
diff --git a/apps/web/src/project/transfer/migrate.ts b/apps/web/src/project/transfer/migrate.ts new file mode 100644 index 00000000..14f59790 --- /dev/null +++ b/apps/web/src/project/transfer/migrate.ts @@ -0,0 +1,75 @@ +import type { StorageMigration } from "@/services/storage/migrations/base"; +import type { ProjectRecord } from "@/services/storage/migrations/transformers/types"; +import { getProjectId } from "@/services/storage/migrations/transformers/utils"; + +/** + * In-memory counterpart to `runStorageMigrations`: applies the same migration + * registry to a single project record (e.g. parsed from an imported project + * archive) without touching IndexedDB. + */ + +export function getRecordVersion({ + project, +}: { + project: ProjectRecord; +}): number { + const versionValue = project.version; + if (typeof versionValue === "number") { + return versionValue; + } + + const scenesValue = project.scenes; + if (Array.isArray(scenesValue) && scenesValue.length > 0) { + return 1; + } + + return 0; +} + +export interface MigratedProjectRecord { + project: ProjectRecord; + fromVersion: number; + toVersion: number; + complete: boolean; +} + +export async function migrateProjectRecord({ + project, + migrations, +}: { + project: ProjectRecord; + migrations: StorageMigration[]; +}): Promise<MigratedProjectRecord> { + const orderedMigrations = [...migrations].sort((a, b) => a.from - b.from); + const fromVersion = getRecordVersion({ project }); + const targetVersion = orderedMigrations.at(-1)?.to ?? fromVersion; + + let currentVersion = fromVersion; + let record = project; + const projectId = getProjectId({ project: record }) ?? "imported-project"; + + for (const migration of orderedMigrations) { + if (migration.from !== currentVersion) { + continue; + } + + const result = await migration.run({ + projectId, + project: record, + }); + + if (result.skipped) { + break; + } + + record = result.project; + currentVersion = migration.to; + } + + return { + project: record, + fromVersion, + toVersion: currentVersion, + complete: currentVersion >= targetVersion, + }; +} |
