Skip to content

Commit ef01346

Browse files
committed
Merge branch 'chore/upload-ingest-snapshot' into release52
2 parents 24aae09 + 428e95d commit ef01346

File tree

2 files changed

+111
-41
lines changed

2 files changed

+111
-41
lines changed

meteor/server/api/snapshot.ts

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ async function retreiveSnapshot(snapshotId: SnapshotId, cred0: Credentials): Pro
482482

483483
return readSnapshot
484484
}
485+
485486
async function restoreFromSnapshot(
486487
/** The snapshot data to restore */
487488
snapshot: AnySnapshot,
@@ -490,22 +491,7 @@ async function restoreFromSnapshot(
490491
): Promise<void> {
491492
// Determine what kind of snapshot
492493

493-
if (!_.isObject(snapshot)) throw new Meteor.Error(500, `Restore input data is not an object`)
494-
// First, some special (debugging) cases:
495-
// @ts-expect-error is's not really a snapshot here:
496-
if (snapshot.externalId && snapshot.segments && snapshot.type === 'mos') {
497-
// Special: Not a snapshot, but a datadump of a MOS rundown
498-
const studioId: StudioId = Meteor.settings.manualSnapshotIngestStudioId || 'studio0'
499-
const studioExists = await checkStudioExists(studioId)
500-
if (studioExists) {
501-
await importIngestRundown(studioId, snapshot as unknown as IngestRundown)
502-
return
503-
}
504-
throw new Meteor.Error(500, `No Studio found`)
505-
}
506-
507494
// Then, continue as if it's a normal snapshot:
508-
509495
if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`)
510496

511497
if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) {
@@ -518,11 +504,7 @@ async function restoreFromSnapshot(
518504
)
519505
}
520506

521-
// TODO: Improve this. This matches the 'old' behaviour
522-
const studios = await Studios.findFetchAsync({})
523-
const snapshotStudioExists = studios.find((studio) => studio._id === playlistSnapshot.playlist.studioId)
524-
const studioId = snapshotStudioExists ? playlistSnapshot.playlist.studioId : studios[0]?._id
525-
if (!studioId) throw new Meteor.Error(500, `No Studio found`)
507+
const studioId = await getStudioIdFromPlaylistSnapshot(playlistSnapshot)
526508

527509
// A snapshot of a rundownPlaylist
528510
return restoreFromRundownPlaylistSnapshot(snapshot as RundownPlaylistSnapshot, studioId, restoreDebugData)
@@ -534,6 +516,60 @@ async function restoreFromSnapshot(
534516
}
535517
}
536518

519+
async function getStudioIdFromPlaylistSnapshot(playlistSnapshot: RundownPlaylistSnapshot): Promise<StudioId> {
520+
// TODO: Improve this. This matches the 'old' behaviour
521+
const studios = await Studios.findFetchAsync({})
522+
const snapshotStudioExists = studios.find((studio) => studio._id === playlistSnapshot.playlist.studioId)
523+
const studioId = snapshotStudioExists ? playlistSnapshot.playlist.studioId : studios[0]?._id
524+
if (!studioId) throw new Meteor.Error(500, `No Studio found`)
525+
return studioId
526+
}
527+
/** Read the ingest data from a snapshot and pipe it into blueprints */
528+
async function ingestFromSnapshot(
529+
/** The snapshot data to restore */
530+
snapshot: AnySnapshot
531+
): Promise<void> {
532+
// Determine what kind of snapshot
533+
if (!snapshot.snapshot) throw new Meteor.Error(500, `Restore input data is not a snapshot (${_.keys(snapshot)})`)
534+
if (snapshot.snapshot.type === SnapshotType.RUNDOWNPLAYLIST) {
535+
const playlistSnapshot = snapshot as RundownPlaylistSnapshot
536+
537+
const studioId = await getStudioIdFromPlaylistSnapshot(playlistSnapshot)
538+
539+
// Read the ingestData from the snapshot
540+
const ingestData = playlistSnapshot.ingestData
541+
542+
const rundownData = ingestData.filter((e) => e.type === 'rundown')
543+
const segmentData = ingestData.filter((e) => e.type === 'segment')
544+
const partData = ingestData.filter((e) => e.type === 'part')
545+
546+
if (rundownData.length === 0) throw new Meteor.Error(402, `No rundowns found in ingestData`)
547+
548+
for (const seg of segmentData) {
549+
seg.data.parts = partData
550+
.filter((e) => e.segmentId === seg.segmentId)
551+
.map((e) => e.data)
552+
.sort((a, b) => b.rank - a.rank)
553+
}
554+
555+
for (let i = 0; i < rundownData.length; i++) {
556+
const rundown = rundownData[i]
557+
558+
const segmentsInRundown = segmentData.filter((e) => e.rundownId === rundown.rundownId)
559+
560+
const ingestRundown: IngestRundown = rundown.data
561+
ingestRundown.segments = segmentsInRundown.map((s) => s.data).sort((a, b) => b.rank - a.rank)
562+
563+
await importIngestRundown(studioId, ingestRundown)
564+
}
565+
} else {
566+
throw new Meteor.Error(
567+
402,
568+
`Unable to ingest a snapshot of type "${snapshot.snapshot.type}", did you mean to restore it?`
569+
)
570+
}
571+
}
572+
537573
async function restoreFromRundownPlaylistSnapshot(
538574
snapshot: RundownPlaylistSnapshot,
539575
studioId: StudioId,
@@ -809,8 +845,16 @@ if (!Settings.enableUserAccounts) {
809845
if (!snapshot) throw new Meteor.Error(400, 'Restore Snapshot: Missing request body')
810846

811847
const restoreDebugData = ctx.headers['restore-debug-data'] === '1'
848+
const ingestSnapshotData = ctx.headers['ingest-snapshot-data'] === '1'
849+
850+
if (typeof snapshot !== 'object' || snapshot === null)
851+
throw new Meteor.Error(500, `Restore input data is not an object`)
812852

813-
await restoreFromSnapshot(snapshot, restoreDebugData)
853+
if (ingestSnapshotData) {
854+
await ingestFromSnapshot(snapshot)
855+
} else {
856+
await restoreFromSnapshot(snapshot, restoreDebugData)
857+
}
814858

815859
ctx.response.status = 200
816860
ctx.response.body = content

packages/webui/src/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>

0 commit comments

Comments
 (0)