From 9a3e400d0c896e6b43a7e157a7ebe2c085df5a22 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 31 Oct 2024 10:14:00 +0100 Subject: [PATCH 1/2] chore: add user option to upload and ingest ingestData from snapshot --- meteor/client/ui/Settings/SnapshotsView.tsx | 66 +++++++++----- meteor/server/api/snapshot.ts | 97 +++++++++++++++++---- 2 files changed, 126 insertions(+), 37 deletions(-) diff --git a/meteor/client/ui/Settings/SnapshotsView.tsx b/meteor/client/ui/Settings/SnapshotsView.tsx index e8eabe8e3f..5998ec7c8f 100644 --- a/meteor/client/ui/Settings/SnapshotsView.tsx +++ b/meteor/client/ui/Settings/SnapshotsView.tsx @@ -74,7 +74,7 @@ const SnapshotsViewContent = withTranslation()( } } - onUploadFile(e: React.ChangeEvent, restoreDebugData: boolean) { + onUploadFile(e: React.ChangeEvent, restoreVariant?: 'debug' | 'ingest') { const { t } = this.props const file = e.target.files?.[0] @@ -101,7 +101,8 @@ const SnapshotsViewContent = withTranslation()( body: uploadFileContents, headers: { 'content-type': 'application/json', - 'restore-debug-data': restoreDebugData ? '1' : '0', + 'restore-debug-data': restoreVariant === 'debug' ? '1' : '0', + 'ingest-snapshot-data': restoreVariant === 'ingest' ? '1' : '0', }, }) .then(() => { @@ -137,6 +138,7 @@ const SnapshotsViewContent = withTranslation()( reader.readAsText(file) } + restoreStoredSnapshot = (snapshotId: SnapshotId) => { const snapshot = Snapshots.findOne(snapshotId) if (snapshot) { @@ -313,24 +315,48 @@ const SnapshotsViewContent = withTranslation()(

{t('Restore from Snapshot File')}

- this.onUploadFile(e, false)} - key={this.state.uploadFileKey} - > - - {t('Upload Snapshot')} - - this.onUploadFile(e, true)} - key={this.state.uploadFileKey2} - > - - {t('Upload Snapshot (for debugging)')} - +

+ this.onUploadFile(e)} + key={this.state.uploadFileKey} + > + + {t('Upload Snapshot')} + + {t('Upload a snapshot file')} +

+

+ this.onUploadFile(e, 'debug')} + key={this.state.uploadFileKey2} + > + + {t('Upload Snapshot (for debugging)')} + + + {t( + 'Upload a snapshot file (restores additional info not directly related to a Playlist / Rundown, such as Packages, PackageWorkStatuses etc' + )} + +

+

+ this.onUploadFile(e, 'ingest')} + key={this.state.uploadFileKey2} + > + + {t('Ingest from Snapshot')} + + + {t('Reads the ingest (NRCS) data, and pipes it throught the blueprints')} + +

{t('Restore from Stored Snapshots')}

diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 0bbe8510d9..7576d1e6f5 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -489,6 +489,7 @@ async function retreiveSnapshot(snapshotId: SnapshotId, cred0: Credentials): Pro return readSnapshot } + async function restoreFromSnapshot( /** The snapshot data to restore */ snapshot: AnySnapshot, @@ -497,22 +498,13 @@ async function restoreFromSnapshot( ): Promise { // Determine what kind of snapshot - if (!_.isObject(snapshot)) throw new Meteor.Error(500, `Restore input data is not an object`) // First, some special (debugging) cases: - // @ts-expect-error is's not really a snapshot here: - if (snapshot.externalId && snapshot.segments && snapshot.type === 'mos') { + if (snapshotIsAMOSDataDump(snapshot)) { // Special: Not a snapshot, but a datadump of a MOS rundown - const studioId: StudioId = Meteor.settings.manualSnapshotIngestStudioId || 'studio0' - const studioExists = await checkStudioExists(studioId) - if (studioExists) { - await importIngestRundown(studioId, snapshot as unknown as IngestRundown) - return - } - throw new Meteor.Error(500, `No Studio found`) + return ingestFromSnapshot(snapshot) } // Then, continue as if it's a normal snapshot: - if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`) if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) { @@ -525,11 +517,7 @@ async function restoreFromSnapshot( ) } - // TODO: Improve this. This matches the 'old' behaviour - const studios = await Studios.findFetchAsync({}) - const snapshotStudioExists = studios.find((studio) => studio._id === playlistSnapshot.playlist.studioId) - const studioId = snapshotStudioExists ? playlistSnapshot.playlist.studioId : studios[0]?._id - if (!studioId) throw new Meteor.Error(500, `No Studio found`) + const studioId = await getStudioIdFromPlaylistSnapshot(playlistSnapshot) // A snapshot of a rundownPlaylist return restoreFromRundownPlaylistSnapshot(snapshot as RundownPlaylistSnapshot, studioId, restoreDebugData) @@ -540,6 +528,73 @@ async function restoreFromSnapshot( throw new Meteor.Error(402, `Unknown snapshot type "${snapshot.snapshot.type}"`) } } +function snapshotIsAMOSDataDump(snapshot: Record): boolean { + // Special: Is not a snapshot, but a datadump of a MOS rundown + return snapshot.externalId && snapshot.segments && snapshot.type === 'mos' +} +async function getStudioIdFromPlaylistSnapshot(playlistSnapshot: RundownPlaylistSnapshot): Promise { + // TODO: Improve this. This matches the 'old' behaviour + const studios = await Studios.findFetchAsync({}) + const snapshotStudioExists = studios.find((studio) => studio._id === playlistSnapshot.playlist.studioId) + const studioId = snapshotStudioExists ? playlistSnapshot.playlist.studioId : studios[0]?._id + if (!studioId) throw new Meteor.Error(500, `No Studio found`) + return studioId +} +/** Read the ingest data from a snapshot and pipe it into blueprints */ +async function ingestFromSnapshot( + /** The snapshot data to restore */ + snapshot: AnySnapshot +): Promise { + // First, some special (debugging) cases: + if (snapshotIsAMOSDataDump(snapshot)) { + // Special: Not a snapshot, but a datadump of a MOS rundown + const studioId: StudioId = Meteor.settings.manualSnapshotIngestStudioId || 'studio0' + const studioExists = await checkStudioExists(studioId) + if (studioExists) { + return importIngestRundown(studioId, snapshot as unknown as IngestRundown) + } else throw new Meteor.Error(500, `No Studio found`) + } + + // Determine what kind of snapshot + if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`) + if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) { + const playlistSnapshot = snapshot as RundownPlaylistSnapshot + + const studioId = await getStudioIdFromPlaylistSnapshot(playlistSnapshot) + + // Read the ingestData from the snapshot + const ingestData = playlistSnapshot.ingestData + + const rundownData = ingestData.filter((e) => e.type === 'rundown') + const segmentData = ingestData.filter((e) => e.type === 'segment') + const partData = ingestData.filter((e) => e.type === 'part') + + if (rundownData.length === 0) throw new Meteor.Error(402, `No rundowns found in ingestData`) + + for (const seg of segmentData) { + seg.data.parts = partData + .filter((e) => e.segmentId === seg.segmentId) + .map((e) => e.data) + .sort((a, b) => b.rank - a.rank) + } + + for (let i = 0; i < rundownData.length; i++) { + const rundown = rundownData[i] + + const segmentsInRundown = segmentData.filter((e) => e.rundownId === rundown.rundownId) + + const ingestRundown: IngestRundown = rundown.data + ingestRundown.segments = segmentsInRundown.map((s) => s.data).sort((a, b) => b.rank - a.rank) + + await importIngestRundown(studioId, ingestRundown) + } + } else { + throw new Meteor.Error( + 402, + `Unable to ingest a snapshot of type "${snapshot.snapshot.type}", did you mean to restore it?` + ) + } +} async function restoreFromRundownPlaylistSnapshot( snapshot: RundownPlaylistSnapshot, @@ -816,8 +871,16 @@ if (!Settings.enableUserAccounts) { if (!snapshot) throw new Meteor.Error(400, 'Restore Snapshot: Missing request body') const restoreDebugData = ctx.headers['restore-debug-data'] === '1' + const ingestSnapshotData = ctx.headers['ingest-snapshot-data'] === '1' - await restoreFromSnapshot(snapshot, restoreDebugData) + if (typeof snapshot !== 'object' || snapshot === null) + throw new Meteor.Error(500, `Restore input data is not an object`) + + if (ingestSnapshotData) { + await ingestFromSnapshot(snapshot) + } else { + await restoreFromSnapshot(snapshot, restoreDebugData) + } ctx.response.status = 200 ctx.response.body = content From 428e95d1b7e3543247997c8cdcee4d571cd2fcc4 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Thu, 14 Nov 2024 10:14:31 +0100 Subject: [PATCH 2/2] chore: remove handling of legacy mos data import --- meteor/server/api/snapshot.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 7576d1e6f5..713fa45fc2 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -498,12 +498,6 @@ async function restoreFromSnapshot( ): Promise { // Determine what kind of snapshot - // First, some special (debugging) cases: - if (snapshotIsAMOSDataDump(snapshot)) { - // Special: Not a snapshot, but a datadump of a MOS rundown - return ingestFromSnapshot(snapshot) - } - // Then, continue as if it's a normal snapshot: if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`) @@ -528,10 +522,7 @@ async function restoreFromSnapshot( throw new Meteor.Error(402, `Unknown snapshot type "${snapshot.snapshot.type}"`) } } -function snapshotIsAMOSDataDump(snapshot: Record): boolean { - // Special: Is not a snapshot, but a datadump of a MOS rundown - return snapshot.externalId && snapshot.segments && snapshot.type === 'mos' -} + async function getStudioIdFromPlaylistSnapshot(playlistSnapshot: RundownPlaylistSnapshot): Promise { // TODO: Improve this. This matches the 'old' behaviour const studios = await Studios.findFetchAsync({}) @@ -545,16 +536,6 @@ async function ingestFromSnapshot( /** The snapshot data to restore */ snapshot: AnySnapshot ): Promise { - // First, some special (debugging) cases: - if (snapshotIsAMOSDataDump(snapshot)) { - // Special: Not a snapshot, but a datadump of a MOS rundown - const studioId: StudioId = Meteor.settings.manualSnapshotIngestStudioId || 'studio0' - const studioExists = await checkStudioExists(studioId) - if (studioExists) { - return importIngestRundown(studioId, snapshot as unknown as IngestRundown) - } else throw new Meteor.Error(500, `No Studio found`) - } - // Determine what kind of snapshot if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`) if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) {