From dc4ba4012cd1802583b7e6823e55cf783ca9edf7 Mon Sep 17 00:00:00 2001 From: Ary Date: Thu, 28 Dec 2023 09:23:51 -0300 Subject: [PATCH] feat: Several project files user folder (Steps 3-5) (#664) * feat: steps 3-5 for several project files user folder * feat: common types, better listing function * feat: error response type --- .../src/controllers/ontimeController.ts | 55 ++++++++++++++++++- .../controllers/ontimeController.validate.ts | 14 +++++ apps/server/src/routes/ontimeRouter.ts | 9 +++ .../server/src/utils/getFileListFromFolder.ts | 17 ++++++ .../ontime-controller/BackendResponse.type.ts | 12 ++++ packages/types/src/index.ts | 2 +- 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 apps/server/src/utils/getFileListFromFolder.ts diff --git a/apps/server/src/controllers/ontimeController.ts b/apps/server/src/controllers/ontimeController.ts index 177d1bf5a..325ccbd63 100644 --- a/apps/server/src/controllers/ontimeController.ts +++ b/apps/server/src/controllers/ontimeController.ts @@ -1,16 +1,25 @@ import { LogOrigin } from 'ontime-types'; -import type { Alias, DatabaseModel, GetInfo, HttpSettings, ProjectData } from 'ontime-types'; +import type { + Alias, + DatabaseModel, + GetInfo, + HttpSettings, + ProjectData, + ProjectFileList, + ErrorResponse, +} from 'ontime-types'; import { RequestHandler, Request, Response } from 'express'; import fs from 'fs'; import { networkInterfaces } from 'os'; +import { join } from 'path'; import { fileHandler } from '../utils/parser.js'; import { DataProvider } from '../classes/data-provider/DataProvider.js'; import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js'; import { PlaybackService } from '../services/PlaybackService.js'; import { eventStore } from '../stores/EventStore.js'; -import { isDocker, resolveDbPath, resolveStylesPath } from '../setup.js'; +import { getAppDataPath, isDocker, resolveDbPath, resolveStylesPath } from '../setup.js'; import { oscIntegration } from '../services/integration-service/OscIntegration.js'; import { httpIntegration } from '../services/integration-service/HttpIntegration.js'; import { logger } from '../classes/Logger.js'; @@ -19,6 +28,7 @@ import { deepmerge } from 'ontime-utils'; import { runtimeCacheStore } from '../stores/cachingStore.js'; import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js'; import { integrationService } from '../services/integration-service/IntegrationService.js'; +import { getFileListFromFolder } from '../utils/getFileListFromFolder.js'; // Create controller for GET request to '/ontime/poll' // Returns data for current state @@ -455,3 +465,44 @@ export const postNew: RequestHandler = async (req, res) => { res.status(400).send({ message: error.toString() }); } }; + +/** + * Retrieves and lists all project files from the uploads directory. + * @param req + * @param res + */ +export const listProjects: RequestHandler = (_, res: Response) => { + try { + const uploadsFolderPath = join(getAppDataPath(), 'uploads'); + const fileList = getFileListFromFolder(uploadsFolderPath); + res.status(200).send(fileList); + } catch (error) { + res.status(500).send({ message: error.toString() }); + } +}; + +/** + * Receives a `filename` from the request body and loads the project file from the uploads directory. + * @param req + * @param res + */ +export const loadProject: RequestHandler = async (req, res) => { + try { + const filename = req.body.filename; + + const uploadsFolderPath = join(getAppDataPath(), 'uploads'); + const filePath = join(uploadsFolderPath, filename); + + if (!fs.existsSync(filePath)) { + return res.status(404).send({ message: 'File not found' }); + } + + await parseAndApply(filePath, req, res, {}); + + res.status(200).send({ + message: `Loaded project ${filename}`, + }); + } catch (error) { + res.status(500).send({ message: error.toString() }); + } +}; diff --git a/apps/server/src/controllers/ontimeController.validate.ts b/apps/server/src/controllers/ontimeController.validate.ts index be0d541eb..31da2b0f7 100644 --- a/apps/server/src/controllers/ontimeController.validate.ts +++ b/apps/server/src/controllers/ontimeController.validate.ts @@ -152,3 +152,17 @@ export const validatePatchProjectFile = [ next(); }, ]; + +/** + * @description Validates the filename for loading a project file. + */ +export const validateLoadProjectFile = [ + body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'), + (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + next(); + }, +]; diff --git a/apps/server/src/routes/ontimeRouter.ts b/apps/server/src/routes/ontimeRouter.ts index 080c7a0ef..b7de0aa46 100644 --- a/apps/server/src/routes/ontimeRouter.ts +++ b/apps/server/src/routes/ontimeRouter.ts @@ -21,6 +21,8 @@ import { postViewSettings, previewExcel, postHTTP, + listProjects, + loadProject, } from '../controllers/ontimeController.js'; import { @@ -32,6 +34,7 @@ import { viewValidator, validateHTTP, validateOscSubscription, + validateLoadProjectFile, } from '../controllers/ontimeController.validate.js'; import { projectSanitiser } from '../controllers/projectController.validate.js'; @@ -96,3 +99,9 @@ router.post('/http', validateHTTP, postHTTP); // create route between controller and '/ontime/new' endpoint router.post('/new', projectSanitiser, postNew); + +// create route between controller and '/ontime/projects' endpoint +router.get('/projects', listProjects); + +// create route between controller and '/ontime/load-project' endpoint +router.post('/load-project', validateLoadProjectFile, loadProject); diff --git a/apps/server/src/utils/getFileListFromFolder.ts b/apps/server/src/utils/getFileListFromFolder.ts new file mode 100644 index 000000000..94db83c6f --- /dev/null +++ b/apps/server/src/utils/getFileListFromFolder.ts @@ -0,0 +1,17 @@ +import { ProjectFile } from 'ontime-types'; + +import { readdirSync, statSync } from 'fs'; + +export const getFileListFromFolder = (folderPath: string): Array => { + const files = readdirSync(folderPath); + return files.map((file) => { + const filePath = `${folderPath}/${file}`; + const stats = statSync(filePath); + + return { + filename: file, + createdAt: stats.birthtime.toISOString(), + updatedAt: stats.mtime.toISOString(), + }; + }); +}; diff --git a/packages/types/src/api/ontime-controller/BackendResponse.type.ts b/packages/types/src/api/ontime-controller/BackendResponse.type.ts index 6bfe2fa02..128ed2d13 100644 --- a/packages/types/src/api/ontime-controller/BackendResponse.type.ts +++ b/packages/types/src/api/ontime-controller/BackendResponse.type.ts @@ -12,3 +12,15 @@ export interface GetInfo { osc: OSCSettings; cssOverride: string; } + +export type ProjectFile = { + filename: string; + createdAt: string; + updatedAt: string; +}; + +export type ProjectFileList = Array; + +export type ErrorResponse = { + message: string; +} \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d0b5ef704..8ad819d1b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -38,7 +38,7 @@ export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './def export type { HttpSettings, HttpSubscription, HttpSubscriptionOptions } from './definitions/core/HttpSettings.type.js'; // SERVER RESPONSES -export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js'; +export type { NetworkInterface, GetInfo, ProjectFileList, ProjectFile, ErrorResponse } from './api/ontime-controller/BackendResponse.type.js'; export type { GetRundownCached } from './api/rundown-controller/BackendResponse.type.js'; // SERVER RUNTIME