mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 23:49:15 +00:00
Keep several project files in user folder - Steps 5A-E (#684)
This commit is contained in:
@@ -13,13 +13,21 @@ import { RequestHandler, Request, Response } from 'express';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { networkInterfaces } from 'os';
|
import { networkInterfaces } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { copyFile, rename, writeFile } from 'fs/promises';
|
||||||
|
|
||||||
import { fileHandler } from '../utils/parser.js';
|
import { fileHandler } from '../utils/parser.js';
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { PlaybackService } from '../services/PlaybackService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { getAppDataPath, isDocker, lastLoadedProjectConfigPath, resolveDbPath, resolveStylesPath } from '../setup.js';
|
import {
|
||||||
|
getAppDataPath,
|
||||||
|
isDocker,
|
||||||
|
lastLoadedProjectConfigPath,
|
||||||
|
resolveDbPath,
|
||||||
|
resolveStylesPath,
|
||||||
|
uploadsFolderPath,
|
||||||
|
} from '../setup.js';
|
||||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||||
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
@@ -29,6 +37,10 @@ import { runtimeCacheStore } from '../stores/cachingStore.js';
|
|||||||
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
||||||
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
||||||
import { getProjectFiles } from '../utils/getFileListFromFolder.js';
|
import { getProjectFiles } from '../utils/getFileListFromFolder.js';
|
||||||
|
import { configService } from '../services/ConfigService.js';
|
||||||
|
import { deleteFile } from '../utils/parserUtils.js';
|
||||||
|
import { validateProjectFiles } from './ontimeController.validate.js';
|
||||||
|
import { dbModel } from '../models/dataModel.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/poll'
|
// Create controller for GET request to '/ontime/poll'
|
||||||
// Returns data for current state
|
// Returns data for current state
|
||||||
@@ -511,3 +523,148 @@ export const loadProject: RequestHandler = async (req, res) => {
|
|||||||
res.status(500).send({ message: error.toString() });
|
res.status(500).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates a project file.
|
||||||
|
* Receives the original project filename (`filename`) from the request parameters
|
||||||
|
* and the filename for the duplicate (`newFilename`) from the request body.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request parameters and `newFilename` in the request body.
|
||||||
|
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful duplication,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export const duplicateProjectFile: RequestHandler = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filename } = req.params;
|
||||||
|
const { newFilename } = req.body;
|
||||||
|
|
||||||
|
const projectFilePath = join(uploadsFolderPath, filename);
|
||||||
|
const duplicateProjectFilePath = join(uploadsFolderPath, newFilename);
|
||||||
|
|
||||||
|
const errors = validateProjectFiles({ filename, newFilename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
await copyFile(projectFilePath, duplicateProjectFilePath);
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
message: `Duplicated project ${filename} to ${newFilename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a project file.
|
||||||
|
* Receives the current filename (`filename`) from the request parameters
|
||||||
|
* and the new filename (`newFilename`) from the request body.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request parameters and `newFilename` in the request body.
|
||||||
|
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful renaming,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export const renameProjectFile: RequestHandler = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { newFilename } = req.body;
|
||||||
|
const { filename } = req.params;
|
||||||
|
|
||||||
|
const projectFilePath = join(uploadsFolderPath, filename);
|
||||||
|
const newProjectFilePath = join(uploadsFolderPath, newFilename);
|
||||||
|
|
||||||
|
const errors = validateProjectFiles({ filename, newFilename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the file
|
||||||
|
await rename(projectFilePath, newProjectFilePath);
|
||||||
|
|
||||||
|
// Update the last loaded project config if current loaded project is the one being renamed
|
||||||
|
const { lastLoadedProject } = await configService.getConfig();
|
||||||
|
|
||||||
|
if (lastLoadedProject === filename) {
|
||||||
|
await configService.updateDatabaseConfig(newFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
message: `Renamed project ${filename} to ${newFilename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new project file.
|
||||||
|
* Receives the project filename (`filename`) from the request body.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request body.
|
||||||
|
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful creation,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export const createProjectFile: RequestHandler = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filename } = req.body;
|
||||||
|
|
||||||
|
const projectFilePath = join(uploadsFolderPath, filename);
|
||||||
|
|
||||||
|
const errors = validateProjectFiles({ newFilename: filename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(projectFilePath, JSON.stringify(dbModel));
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
message: `Created project ${filename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an existing project file.
|
||||||
|
* Receives the project filename (`filename`) from the request parameters.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request parameters.
|
||||||
|
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful deletion,
|
||||||
|
* a 403 status if attempting to delete the currently loaded project,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export const deleteProjectFile: RequestHandler = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filename } = req.params;
|
||||||
|
|
||||||
|
const { lastLoadedProject } = await configService.getConfig();
|
||||||
|
|
||||||
|
if (lastLoadedProject === filename) {
|
||||||
|
return res.status(403).send({ message: 'Cannot delete currently loaded project' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectFilePath = join(uploadsFolderPath, filename);
|
||||||
|
|
||||||
|
const errors = validateProjectFiles({ filename: filename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteFile(projectFilePath);
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
message: `Deleted project ${filename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { body, check, validationResult } from 'express-validator';
|
import { body, check, validationResult } from 'express-validator';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
validateHttpSubscriptionObject,
|
validateHttpSubscriptionObject,
|
||||||
validateOscSubscriptionObject,
|
validateOscSubscriptionObject,
|
||||||
validateOscSubscriptionCycle,
|
validateOscSubscriptionCycle,
|
||||||
} from '../utils/parserFunctions.js';
|
} from '../utils/parserFunctions.js';
|
||||||
|
import { uploadsFolderPath } from '../setup.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Validates object for POST /ontime/views
|
* @description Validates object for POST /ontime/views
|
||||||
@@ -166,3 +170,99 @@ export const validateLoadProjectFile = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the filenames for duplicating a project.
|
||||||
|
*/
|
||||||
|
export const validateProjectDuplicate = [
|
||||||
|
body('newFilename')
|
||||||
|
.exists()
|
||||||
|
.withMessage('New project filename is required')
|
||||||
|
.isString()
|
||||||
|
.withMessage('New project filename must be a string')
|
||||||
|
.isLength({ min: 1, max: 255 })
|
||||||
|
.withMessage('New project filename must be between 1 and 255 characters'),
|
||||||
|
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the filenames for renaming a project.
|
||||||
|
*/
|
||||||
|
export const validateProjectRename = [
|
||||||
|
body('newFilename')
|
||||||
|
.exists()
|
||||||
|
.withMessage('Duplicate project filename is required')
|
||||||
|
.isString()
|
||||||
|
.withMessage('Duplicate project filename must be a string')
|
||||||
|
.isLength({ min: 1, max: 255 })
|
||||||
|
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
||||||
|
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the filename for creating a project file.
|
||||||
|
*/
|
||||||
|
export const validateProjectCreate = [
|
||||||
|
body('filename')
|
||||||
|
.exists()
|
||||||
|
.withMessage('Filename is required')
|
||||||
|
.isString()
|
||||||
|
.withMessage('Filename must be a string')
|
||||||
|
.isLength({ min: 1, max: 255 })
|
||||||
|
.withMessage('Filename must be between 1 and 255 characters'),
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the existence of project files.
|
||||||
|
* @param {object} projectFiles
|
||||||
|
* @param {string} projectFiles.projectFilename
|
||||||
|
* @param {string} projectFiles.newFilename
|
||||||
|
*
|
||||||
|
* @returns {Promise<Array<string>>} Array of errors
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const validateProjectFiles = (projectFiles: { filename?: string; newFilename?: string }): Array<string> => {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (projectFiles.filename) {
|
||||||
|
const projectFilePath = join(uploadsFolderPath, projectFiles.filename);
|
||||||
|
|
||||||
|
if (!existsSync(projectFilePath)) {
|
||||||
|
errors.push('Project file does not exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectFiles.newFilename) {
|
||||||
|
const projectFilePath = join(uploadsFolderPath, projectFiles.newFilename);
|
||||||
|
|
||||||
|
if (existsSync(projectFilePath)) {
|
||||||
|
errors.push('New project file already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ import {
|
|||||||
postViewSettings,
|
postViewSettings,
|
||||||
previewExcel,
|
previewExcel,
|
||||||
postHTTP,
|
postHTTP,
|
||||||
|
duplicateProjectFile,
|
||||||
listProjects,
|
listProjects,
|
||||||
loadProject,
|
loadProject,
|
||||||
|
renameProjectFile,
|
||||||
|
createProjectFile,
|
||||||
|
deleteProjectFile,
|
||||||
} from '../controllers/ontimeController.js';
|
} from '../controllers/ontimeController.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -34,9 +38,13 @@ import {
|
|||||||
viewValidator,
|
viewValidator,
|
||||||
validateHTTP,
|
validateHTTP,
|
||||||
validateOscSubscription,
|
validateOscSubscription,
|
||||||
|
validateProjectDuplicate,
|
||||||
validateLoadProjectFile,
|
validateLoadProjectFile,
|
||||||
|
validateProjectRename,
|
||||||
|
validateProjectCreate,
|
||||||
} from '../controllers/ontimeController.validate.js';
|
} from '../controllers/ontimeController.validate.js';
|
||||||
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
||||||
|
import { sanitizeProjectFilename } from '../utils/sanitizeProjectFilename.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
@@ -105,3 +113,15 @@ router.get('/projects', listProjects);
|
|||||||
|
|
||||||
// create route between controller and '/ontime/load-project' endpoint
|
// create route between controller and '/ontime/load-project' endpoint
|
||||||
router.post('/load-project', validateLoadProjectFile, loadProject);
|
router.post('/load-project', validateLoadProjectFile, loadProject);
|
||||||
|
|
||||||
|
// create route between controller and '/ontime/project/:filename/duplicate' endpoint
|
||||||
|
router.post('/project/:filename/duplicate', validateProjectDuplicate, sanitizeProjectFilename, duplicateProjectFile);
|
||||||
|
|
||||||
|
// create route between controller and '/ontime/project/:filename/rename' endpoint
|
||||||
|
router.put('/project/:filename/rename', validateProjectRename, sanitizeProjectFilename, renameProjectFile);
|
||||||
|
|
||||||
|
// create route between controller and '/ontime/project' endpoint
|
||||||
|
router.post('/project', validateProjectCreate, sanitizeProjectFilename, createProjectFile);
|
||||||
|
|
||||||
|
// create route between controller and '/ontime/project/:filename' endpoint
|
||||||
|
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const testDbStartDirectory = isTest ? '../' : getAppDataPath();
|
|||||||
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
|
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
|
||||||
|
|
||||||
export const lastLoadedProjectConfigPath = join(getAppDataPath(), 'config.json');
|
export const lastLoadedProjectConfigPath = join(getAppDataPath(), 'config.json');
|
||||||
|
export const uploadsFolderPath = join(getAppDataPath(), 'uploads');
|
||||||
|
|
||||||
let lastLoadedProject;
|
let lastLoadedProject;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { ensureJsonExtension } from '../ensureJsonExtension.js';
|
||||||
|
|
||||||
|
describe('ensureJsonExtension', () => {
|
||||||
|
it('should add .json to a filename without an extension', () => {
|
||||||
|
const filename = 'testfile';
|
||||||
|
const result = ensureJsonExtension(filename);
|
||||||
|
expect(result).toBe('testfile.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add .json to a filename that already has .json', () => {
|
||||||
|
const filename = 'testfile.json';
|
||||||
|
const result = ensureJsonExtension(filename);
|
||||||
|
expect(result).toBe('testfile.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add .json to a filename with a different extension', () => {
|
||||||
|
const filename = 'testfile.txt';
|
||||||
|
const result = ensureJsonExtension(filename);
|
||||||
|
expect(result).toBe('testfile.txt.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle filenames with multiple dots', () => {
|
||||||
|
const filename = 'my.test.file';
|
||||||
|
const result = ensureJsonExtension(filename);
|
||||||
|
expect(result).toBe('my.test.file.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const ensureJsonExtension = (filename: string) => {
|
||||||
|
if (!filename) return filename;
|
||||||
|
|
||||||
|
return filename.includes('.json') ? filename : `${filename}.json`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ensureJsonExtension } from '../utils/ensureJsonExtension.js';
|
||||||
|
|
||||||
|
export const sanitizeProjectFilename = (req, res, next) => {
|
||||||
|
const { filename, newFilename } = req.body;
|
||||||
|
const { filename: projectName } = req.params;
|
||||||
|
|
||||||
|
req.body.filename = ensureJsonExtension(filename);
|
||||||
|
req.body.newFilename = ensureJsonExtension(newFilename);
|
||||||
|
req.params.filename = ensureJsonExtension(projectName);
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user