Apply project (#843)

This commit is contained in:
Carlos Valente
2024-03-22 12:52:55 +01:00
committed by GitHub
parent 8c5aaa0901
commit 6178ed1e4e
4 changed files with 80 additions and 70 deletions
+18 -32
View File
@@ -9,15 +9,11 @@ import {
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import fs from 'fs'; import fs from 'fs';
import { join } from 'path';
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { failEmptyObjects } from '../../utils/routerUtils.js'; import { failEmptyObjects } from '../../utils/routerUtils.js';
import { resolveDbPath, resolveProjectsDirectory } from '../../setup/index.js'; import { resolveDbPath, resolveProjectsDirectory } from '../../setup/index.js';
import * as projectService from '../../services/project-service/ProjectService.js'; import * as projectService from '../../services/project-service/ProjectService.js';
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
import { setRundown } from '../../services/rundown-service/RundownService.js';
import { ensureJsonExtension } from '../../utils/fileManagement.js'; import { ensureJsonExtension } from '../../utils/fileManagement.js';
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js'; import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
import { appStateService } from '../../services/app-state-service/AppStateService.js'; import { appStateService } from '../../services/app-state-service/AppStateService.js';
@@ -31,23 +27,10 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
} }
try { try {
const patchDb: Partial<DatabaseModel> = { const { rundown, project, settings, viewSettings, urlPresets, customFields, osc, http } = req.body;
project: req.body?.project, const patchDb: DatabaseModel = { rundown, project, settings, viewSettings, urlPresets, customFields, osc, http };
settings: req.body?.settings,
viewSettings: req.body?.viewSettings,
osc: req.body?.osc,
urlPresets: req.body?.urlPresets,
customFields: req.body?.customFields,
};
const maybeRundown = req.body?.rundown; const newData = await projectService.applyDataModel(patchDb);
await DataProvider.mergeIntoData(patchDb);
if (maybeRundown !== undefined) {
// it is likely cheaper to invalidate cache than to calculate diff
runtimeService.stop();
await setRundown(maybeRundown);
}
const newData = DataProvider.getData();
res.status(200).send(newData); res.status(200).send(newData);
} catch (error) { } catch (error) {
res.status(400).send({ message: String(error) }); res.status(400).send({ message: String(error) });
@@ -93,9 +76,7 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
} }
export async function projectDownload(_req: Request, res: Response) { export async function projectDownload(_req: Request, res: Response) {
const { title } = DataProvider.getProjectData(); const fileTitle = projectService.getProjectTitle();
const fileTitle = title || 'ontime data';
res.download(resolveDbPath, `${fileTitle}.json`, (err) => { res.download(resolveDbPath, `${fileTitle}.json`, (err) => {
if (err) { if (err) {
res.status(500).send({ res.status(500).send({
@@ -116,9 +97,14 @@ export async function postProjectFile(req: Request, res: Response<MessageRespons
try { try {
const options = req.query; const options = req.query;
const filePath = req.file.path; const { filename, path } = req.file;
await projectService.applyProjectFile(filePath, options);
res.status(201).send({ message: 'ok' }); await projectService.handleUploadedFile(path, filename);
await projectService.applyProjectFile(filename, options);
res.status(201).send({
message: `Loaded project ${filename}`,
});
} catch (error) { } catch (error) {
res.status(400).send({ message: `Failed parsing ${error}` }); res.status(400).send({ message: `Failed parsing ${error}` });
} }
@@ -141,15 +127,15 @@ export async function listProjects(_req: Request, res: Response<ProjectFileListR
*/ */
export async function loadProject(req: Request, res: Response<MessageResponse | ErrorResponse>) { export async function loadProject(req: Request, res: Response<MessageResponse | ErrorResponse>) {
try { try {
const filename = req.body.filename; const name = req.body.filename;
const filePath = join(resolveProjectsDirectory, filename); if (!projectService.doesProjectExist(name)) {
if (!fs.existsSync(filePath)) {
return res.status(404).send({ message: 'File not found' }); return res.status(404).send({ message: 'File not found' });
} }
await projectService.applyProjectFile(filePath);
await projectService.applyProjectFile(name);
res.status(201).send({ res.status(201).send({
message: `Loaded project ${filename}`, message: `Loaded project ${name}`,
}); });
} catch (error) { } catch (error) {
res.status(500).send({ message: String(error) }); res.status(500).send({ message: String(error) });
@@ -25,7 +25,7 @@ export class DataProvider {
static async setProjectData(newData: Partial<ProjectData>) { static async setProjectData(newData: Partial<ProjectData>) {
data.project = { ...data.project, ...newData }; data.project = { ...data.project, ...newData };
await this.persist(); this.persist();
return data.project; return data.project;
} }
@@ -35,7 +35,7 @@ export class DataProvider {
static async setCustomFields(newData: CustomFields): Promise<CustomFields> { static async setCustomFields(newData: CustomFields): Promise<CustomFields> {
data.customFields = { ...newData }; data.customFields = { ...newData };
await this.persist(); this.persist();
return data.customFields; return data.customFields;
} }
@@ -45,7 +45,8 @@ export class DataProvider {
static async setRundown(newData: OntimeRundown) { static async setRundown(newData: OntimeRundown) {
data.rundown = [...newData]; data.rundown = [...newData];
await this.persist(); this.persist();
return data.rundown;
} }
static getSettings(): Readonly<Settings> { static getSettings(): Readonly<Settings> {
@@ -54,7 +55,8 @@ export class DataProvider {
static async setSettings(newData: Settings) { static async setSettings(newData: Settings) {
data.settings = { ...newData }; data.settings = { ...newData };
await this.persist(); this.persist();
return data.settings;
} }
static getOsc(): OSCSettings { static getOsc(): OSCSettings {
@@ -71,7 +73,8 @@ export class DataProvider {
static async setUrlPresets(newData: URLPreset[]) { static async setUrlPresets(newData: URLPreset[]) {
data.urlPresets = newData; data.urlPresets = newData;
await this.persist(); this.persist();
return data.urlPresets;
} }
static getViewSettings() { static getViewSettings() {
@@ -80,18 +83,19 @@ export class DataProvider {
static async setViewSettings(newData: ViewSettings) { static async setViewSettings(newData: ViewSettings) {
data.viewSettings = { ...newData }; data.viewSettings = { ...newData };
await this.persist(); this.persist();
return data.viewSettings;
} }
static async setOsc(newData: OSCSettings): Promise<OSCSettings> { static async setOsc(newData: OSCSettings): Promise<OSCSettings> {
data.osc = { ...newData }; data.osc = { ...newData };
await this.persist(); this.persist();
return data.osc; return data.osc;
} }
static async setHttp(newData: HttpSettings): Promise<HttpSettings> { static async setHttp(newData: HttpSettings): Promise<HttpSettings> {
data.http = { ...newData }; data.http = { ...newData };
await this.persist(); this.persist();
return data.http; return data.http;
} }
@@ -116,6 +120,9 @@ export class DataProvider {
data.urlPresets = mergedData.urlPresets; data.urlPresets = mergedData.urlPresets;
data.customFields = mergedData.customFields; data.customFields = mergedData.customFields;
data.rundown = mergedData.rundown; data.rundown = mergedData.rundown;
await this.persist();
this.persist();
return data;
} }
} }
@@ -2,9 +2,9 @@ import { DatabaseModel, GetInfo, ProjectData, ProjectFile, ProjectFileListRespon
import { copyFile, rename, stat, writeFile } from 'fs/promises'; import { copyFile, rename, stat, writeFile } from 'fs/promises';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { basename, join } from 'path'; import { join } from 'path';
import { notifyChanges, setRundown } from '../rundown-service/RundownService.js'; import { initRundown } from '../rundown-service/RundownService.js';
import { DataProvider } from '../../classes/data-provider/DataProvider.js'; import { DataProvider } from '../../classes/data-provider/DataProvider.js';
import { runtimeService } from '../runtime-service/RuntimeService.js'; import { runtimeService } from '../runtime-service/RuntimeService.js';
import { getNetworkInterfaces } from '../../utils/networkInterfaces.js'; import { getNetworkInterfaces } from '../../utils/networkInterfaces.js';
@@ -33,22 +33,31 @@ type Options = {
/** /**
* Handles a file from the upload folder and applies its data * Handles a file from the upload folder and applies its data
*/ */
export async function applyProjectFile(filePath: string, options?: Options) { export async function applyProjectFile(name: string, options?: Options) {
const filePath = join(resolveProjectsDirectory, name);
const data = parseProjectFile(filePath); const data = parseProjectFile(filePath);
// move file to project folder
const filename = basename(filePath);
const newFilePath = join(resolveProjectsDirectory, filename);
await rename(filePath, newFilePath);
// change LowDB to point to new file // change LowDB to point to new file
await switchDb(filename); await switchDb(name);
// apply data model // apply data model
await applyDataModel(data, options); await applyDataModel(data, options);
// persist the project selection // persist the project selection
await appStateService.updateDatabaseConfig(filename); await appStateService.updateDatabaseConfig(name);
}
/**
* Copies a file from upload folder to the projects folder
* @param filePath
* @param name
* @returns
*/
export async function handleUploadedFile(filePath: string, name: string) {
const newFilePath = join(resolveProjectsDirectory, name);
await rename(filePath, newFilePath);
await deleteFile(filePath);
return name;
} }
/** /**
@@ -196,18 +205,27 @@ export function extractPin(value: string | undefined | null, fallback: string |
/** /**
* applies a partial database model * applies a partial database model
*/ */
export async function applyDataModel(data: Partial<DatabaseModel>, options?: Options) { export async function applyDataModel(data: Partial<DatabaseModel>, _options?: Options) {
runtimeService.stop(); runtimeService.stop();
const newRundown = data.rundown || []; // TODO: allow partial project merge from options
const { rundown, ...rest } = data; const { rundown, customFields, ...rest } = data;
if (options?.onlyRundown === 'true') { const newData = await DataProvider.mergeIntoData(rest);
setRundown(newRundown ?? []);
} else { if (rundown != null) {
await DataProvider.mergeIntoData(rest); initRundown(rundown, customFields ?? {});
setRundown(rundown ?? []);
} }
notifyChanges({ timer: true, external: true });
return newData;
}
/**
* Checks whether a project of a given name exists
* @param name
*/
export function doesProjectExist(name: string): boolean {
const projectFilePath = join(resolveProjectsDirectory, name);
return existsSync(projectFilePath);
} }
/** /**
@@ -240,3 +258,11 @@ export const validateProjectFiles = (projectFiles: { filename?: string; newFilen
return errors; return errors;
}; };
/**
* Get current project title or fallback
*/
export function getProjectTitle(): string {
const { title } = DataProvider.getProjectData();
return title || 'ontime data';
}
@@ -210,7 +210,7 @@ function updateRuntimeOnChange() {
/** /**
* Notify services of changes in the rundown * Notify services of changes in the rundown
*/ */
export function notifyChanges(options: { timer?: boolean | string[]; external?: boolean }) { function notifyChanges(options: { timer?: boolean | string[]; external?: boolean }) {
if (options.timer) { if (options.timer) {
const playableEvents = getPlayableEvents(); const playableEvents = getPlayableEvents();
@@ -243,12 +243,3 @@ export async function initRundown(rundown: OntimeRundown, customFields: CustomFi
// notify timer of change // notify timer of change
notifyChanges({ timer: true }); notifyChanges({ timer: true });
} }
/**
* Overrides the rundown with the given
* @param rundown
*/
export async function setRundown(rundown: OntimeRundown) {
await cache.setRundown(rundown);
notifyChanges({ timer: true });
}