Skip to content

Commit 9a3e400

Browse files
committed
chore: add user option to upload and ingest ingestData from snapshot
1 parent ea7cece commit 9a3e400

File tree

2 files changed

+126
-37
lines changed

2 files changed

+126
-37
lines changed

meteor/client/ui/Settings/SnapshotsView.tsx

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const SnapshotsViewContent = withTranslation()(
7474
}
7575
}
7676

77-
onUploadFile(e: React.ChangeEvent<HTMLInputElement>, restoreDebugData: boolean) {
77+
onUploadFile(e: React.ChangeEvent<HTMLInputElement>, restoreVariant?: 'debug' | 'ingest') {
7878
const { t } = this.props
7979

8080
const file = e.target.files?.[0]
@@ -101,7 +101,8 @@ const SnapshotsViewContent = withTranslation()(
101101
body: uploadFileContents,
102102
headers: {
103103
'content-type': 'application/json',
104-
'restore-debug-data': restoreDebugData ? '1' : '0',
104+
'restore-debug-data': restoreVariant === 'debug' ? '1' : '0',
105+
'ingest-snapshot-data': restoreVariant === 'ingest' ? '1' : '0',
105106
},
106107
})
107108
.then(() => {
@@ -137,6 +138,7 @@ const SnapshotsViewContent = withTranslation()(
137138

138139
reader.readAsText(file)
139140
}
141+
140142
restoreStoredSnapshot = (snapshotId: SnapshotId) => {
141143
const snapshot = Snapshots.findOne(snapshotId)
142144
if (snapshot) {
@@ -313,24 +315,48 @@ const SnapshotsViewContent = withTranslation()(
313315
</div>
314316
<h2 className="mhn">{t('Restore from Snapshot File')}</h2>
315317
<div className="mdi">
316-
<UploadButton
317-
accept="application/json,.json"
318-
className="btn btn-secondary"
319-
onChange={(e) => this.onUploadFile(e, false)}
320-
key={this.state.uploadFileKey}
321-
>
322-
<FontAwesomeIcon icon={faUpload} />
323-
<span>{t('Upload Snapshot')}</span>
324-
</UploadButton>
325-
<UploadButton
326-
accept="application/json,.json"
327-
className="btn btn-secondary mls"
328-
onChange={(e) => this.onUploadFile(e, true)}
329-
key={this.state.uploadFileKey2}
330-
>
331-
<FontAwesomeIcon icon={faUpload} />
332-
<span>{t('Upload Snapshot (for debugging)')}</span>
333-
</UploadButton>
318+
<p className="mhn">
319+
<UploadButton
320+
accept="application/json,.json"
321+
className="btn btn-secondary"
322+
onChange={(e) => this.onUploadFile(e)}
323+
key={this.state.uploadFileKey}
324+
>
325+
<FontAwesomeIcon icon={faUpload} />
326+
<span>{t('Upload Snapshot')}</span>
327+
</UploadButton>
328+
<span className="text-s vsubtle mls">{t('Upload a snapshot file')}</span>
329+
</p>
330+
<p className="mhn">
331+
<UploadButton
332+
accept="application/json,.json"
333+
className="btn btn-secondary"
334+
onChange={(e) => this.onUploadFile(e, 'debug')}
335+
key={this.state.uploadFileKey2}
336+
>
337+
<FontAwesomeIcon icon={faUpload} />
338+
<span>{t('Upload Snapshot (for debugging)')}</span>
339+
</UploadButton>
340+
<span className="text-s vsubtle mls">
341+
{t(
342+
'Upload a snapshot file (restores additional info not directly related to a Playlist / Rundown, such as Packages, PackageWorkStatuses etc'
343+
)}
344+
</span>
345+
</p>
346+
<p className="mhn">
347+
<UploadButton
348+
accept="application/json,.json"
349+
className="btn btn-secondary"
350+
onChange={(e) => this.onUploadFile(e, 'ingest')}
351+
key={this.state.uploadFileKey2}
352+
>
353+
<FontAwesomeIcon icon={faUpload} />
354+
<span>{t('Ingest from Snapshot')}</span>
355+
</UploadButton>
356+
<span className="text-s vsubtle mls">
357+
{t('Reads the ingest (NRCS) data, and pipes it throught the blueprints')}
358+
</span>
359+
</p>
334360
</div>
335361
<h2 className="mhn">{t('Restore from Stored Snapshots')}</h2>
336362
<div>

meteor/server/api/snapshot.ts

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ async function retreiveSnapshot(snapshotId: SnapshotId, cred0: Credentials): Pro
489489

490490
return readSnapshot
491491
}
492+
492493
async function restoreFromSnapshot(
493494
/** The snapshot data to restore */
494495
snapshot: AnySnapshot,
@@ -497,22 +498,13 @@ async function restoreFromSnapshot(
497498
): Promise<void> {
498499
// Determine what kind of snapshot
499500

500-
if (!_.isObject(snapshot)) throw new Meteor.Error(500, `Restore input data is not an object`)
501501
// First, some special (debugging) cases:
502-
// @ts-expect-error is's not really a snapshot here:
503-
if (snapshot.externalId && snapshot.segments && snapshot.type === 'mos') {
502+
if (snapshotIsAMOSDataDump(snapshot)) {
504503
// Special: Not a snapshot, but a datadump of a MOS rundown
505-
const studioId: StudioId = Meteor.settings.manualSnapshotIngestStudioId || 'studio0'
506-
const studioExists = await checkStudioExists(studioId)
507-
if (studioExists) {
508-
await importIngestRundown(studioId, snapshot as unknown as IngestRundown)
509-
return
510-
}
511-
throw new Meteor.Error(500, `No Studio found`)
504+
return ingestFromSnapshot(snapshot)
512505
}
513506

514507
// Then, continue as if it's a normal snapshot:
515-
516508
if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`)
517509

518510
if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) {
@@ -525,11 +517,7 @@ async function restoreFromSnapshot(
525517
)
526518
}
527519

528-
// TODO: Improve this. This matches the 'old' behaviour
529-
const studios = await Studios.findFetchAsync({})
530-
const snapshotStudioExists = studios.find((studio) => studio._id === playlistSnapshot.playlist.studioId)
531-
const studioId = snapshotStudioExists ? playlistSnapshot.playlist.studioId : studios[0]?._id
532-
if (!studioId) throw new Meteor.Error(500, `No Studio found`)
520+
const studioId = await getStudioIdFromPlaylistSnapshot(playlistSnapshot)
533521

534522
// A snapshot of a rundownPlaylist
535523
return restoreFromRundownPlaylistSnapshot(snapshot as RundownPlaylistSnapshot, studioId, restoreDebugData)
@@ -540,6 +528,73 @@ async function restoreFromSnapshot(
540528
throw new Meteor.Error(402, `Unknown snapshot type "${snapshot.snapshot.type}"`)
541529
}
542530
}
531+
function snapshotIsAMOSDataDump(snapshot: Record<string, any>): boolean {
532+
// Special: Is not a snapshot, but a datadump of a MOS rundown
533+
return snapshot.externalId && snapshot.segments && snapshot.type === 'mos'
534+
}
535+
async function getStudioIdFromPlaylistSnapshot(playlistSnapshot: RundownPlaylistSnapshot): Promise<StudioId> {
536+
// TODO: Improve this. This matches the 'old' behaviour
537+
const studios = await Studios.findFetchAsync({})
538+
const snapshotStudioExists = studios.find((studio) => studio._id === playlistSnapshot.playlist.studioId)
539+
const studioId = snapshotStudioExists ? playlistSnapshot.playlist.studioId : studios[0]?._id
540+
if (!studioId) throw new Meteor.Error(500, `No Studio found`)
541+
return studioId
542+
}
543+
/** Read the ingest data from a snapshot and pipe it into blueprints */
544+
async function ingestFromSnapshot(
545+
/** The snapshot data to restore */
546+
snapshot: AnySnapshot
547+
): Promise<void> {
548+
// First, some special (debugging) cases:
549+
if (snapshotIsAMOSDataDump(snapshot)) {
550+
// Special: Not a snapshot, but a datadump of a MOS rundown
551+
const studioId: StudioId = Meteor.settings.manualSnapshotIngestStudioId || 'studio0'
552+
const studioExists = await checkStudioExists(studioId)
553+
if (studioExists) {
554+
return importIngestRundown(studioId, snapshot as unknown as IngestRundown)
555+
} else throw new Meteor.Error(500, `No Studio found`)
556+
}
557+
558+
// Determine what kind of snapshot
559+
if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`)
560+
if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) {
561+
const playlistSnapshot = snapshot as RundownPlaylistSnapshot
562+
563+
const studioId = await getStudioIdFromPlaylistSnapshot(playlistSnapshot)
564+
565+
// Read the ingestData from the snapshot
566+
const ingestData = playlistSnapshot.ingestData
567+
568+
const rundownData = ingestData.filter((e) => e.type === 'rundown')
569+
const segmentData = ingestData.filter((e) => e.type === 'segment')
570+
const partData = ingestData.filter((e) => e.type === 'part')
571+
572+
if (rundownData.length === 0) throw new Meteor.Error(402, `No rundowns found in ingestData`)
573+
574+
for (const seg of segmentData) {
575+
seg.data.parts = partData
576+
.filter((e) => e.segmentId === seg.segmentId)
577+
.map((e) => e.data)
578+
.sort((a, b) => b.rank - a.rank)
579+
}
580+
581+
for (let i = 0; i < rundownData.length; i++) {
582+
const rundown = rundownData[i]
583+
584+
const segmentsInRundown = segmentData.filter((e) => e.rundownId === rundown.rundownId)
585+
586+
const ingestRundown: IngestRundown = rundown.data
587+
ingestRundown.segments = segmentsInRundown.map((s) => s.data).sort((a, b) => b.rank - a.rank)
588+
589+
await importIngestRundown(studioId, ingestRundown)
590+
}
591+
} else {
592+
throw new Meteor.Error(
593+
402,
594+
`Unable to ingest a snapshot of type "${snapshot.snapshot.type}", did you mean to restore it?`
595+
)
596+
}
597+
}
543598

544599
async function restoreFromRundownPlaylistSnapshot(
545600
snapshot: RundownPlaylistSnapshot,
@@ -816,8 +871,16 @@ if (!Settings.enableUserAccounts) {
816871
if (!snapshot) throw new Meteor.Error(400, 'Restore Snapshot: Missing request body')
817872

818873
const restoreDebugData = ctx.headers['restore-debug-data'] === '1'
874+
const ingestSnapshotData = ctx.headers['ingest-snapshot-data'] === '1'
819875

820-
await restoreFromSnapshot(snapshot, restoreDebugData)
876+
if (typeof snapshot !== 'object' || snapshot === null)
877+
throw new Meteor.Error(500, `Restore input data is not an object`)
878+
879+
if (ingestSnapshotData) {
880+
await ingestFromSnapshot(snapshot)
881+
} else {
882+
await restoreFromSnapshot(snapshot, restoreDebugData)
883+
}
821884

822885
ctx.response.status = 200
823886
ctx.response.body = content

0 commit comments

Comments
 (0)