mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
refactor: project service boundaries
This commit is contained in:
committed by
Carlos Valente
parent
20d9df2501
commit
ad69c0ff80
@@ -16,8 +16,9 @@ import { failEmptyObjects } from '../../utils/routerUtils.js';
|
|||||||
import { resolveDbDirectory, resolveProjectsDirectory } from '../../setup/index.js';
|
import { resolveDbDirectory, resolveProjectsDirectory } from '../../setup/index.js';
|
||||||
|
|
||||||
import * as projectService from '../../services/project-service/ProjectService.js';
|
import * as projectService from '../../services/project-service/ProjectService.js';
|
||||||
|
import { doesProjectExist, upload, validateProjectFiles } from '../../services/project-service/projectServiceUtils.js';
|
||||||
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
|
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
|
||||||
import { appStateService } from '../../services/app-state-service/AppStateService.js';
|
import { appStateProvider } from '../../services/app-state-service/AppStateService.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 { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
@@ -54,7 +55,7 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
|||||||
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const filename = generateUniqueFileName(resolveProjectsDirectory, req.body.filename);
|
const filename = generateUniqueFileName(resolveProjectsDirectory, req.body.filename);
|
||||||
const errors = projectService.validateProjectFiles({ newFilename: filename });
|
const errors = validateProjectFiles({ newFilename: filename });
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return res.status(409).send({ message: 'Project with title already exists' });
|
return res.status(409).send({ message: 'Project with title already exists' });
|
||||||
@@ -113,7 +114,8 @@ export async function postProjectFile(req: Request, res: Response<MessageRespons
|
|||||||
const options = req.query;
|
const options = req.query;
|
||||||
const { filename, path } = req.file;
|
const { filename, path } = req.file;
|
||||||
|
|
||||||
await projectService.handleUploadedFile(path, filename);
|
// TODO: controller shouldnt consume this directly
|
||||||
|
await upload(path, filename);
|
||||||
await projectService.applyProjectFile(filename, options);
|
await projectService.applyProjectFile(filename, options);
|
||||||
|
|
||||||
const oscSettings = await DataProvider.getOsc();
|
const oscSettings = await DataProvider.getOsc();
|
||||||
@@ -150,7 +152,7 @@ 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 name = req.body.filename;
|
const name = req.body.filename;
|
||||||
if (!projectService.doesProjectExist(name)) {
|
if (!doesProjectExist(name)) {
|
||||||
return res.status(404).send({ message: 'File not found' });
|
return res.status(404).send({ message: 'File not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +188,7 @@ export async function duplicateProjectFile(req: Request, res: Response<MessageRe
|
|||||||
const { filename } = req.params;
|
const { filename } = req.params;
|
||||||
const { newFilename } = req.body;
|
const { newFilename } = req.body;
|
||||||
|
|
||||||
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
const errors = validateProjectFiles({ filename, newFilename });
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
@@ -218,7 +220,7 @@ export async function renameProjectFile(req: Request, res: Response<MessageRespo
|
|||||||
const { filename: newFilename } = req.body;
|
const { filename: newFilename } = req.body;
|
||||||
const { filename } = req.params;
|
const { filename } = req.params;
|
||||||
|
|
||||||
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
const errors = validateProjectFiles({ filename, newFilename });
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
@@ -250,13 +252,13 @@ export async function deleteProjectFile(req: Request, res: Response<MessageRespo
|
|||||||
try {
|
try {
|
||||||
const { filename } = req.params;
|
const { filename } = req.params;
|
||||||
|
|
||||||
const { lastLoadedProject } = await appStateService.get();
|
const lastLoadedProject = await appStateProvider.getLastLoadedProject();
|
||||||
|
|
||||||
if (lastLoadedProject === filename) {
|
if (lastLoadedProject === filename) {
|
||||||
return res.status(403).send({ message: 'Cannot delete currently loaded project' });
|
return res.status(403).send({ message: 'Cannot delete currently loaded project' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = projectService.validateProjectFiles({ filename });
|
const errors = validateProjectFiles({ filename });
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { Request, Response } from 'express';
|
|||||||
|
|
||||||
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
import { extractPin } from '../../services/project-service/ProjectService.js';
|
|
||||||
import { isDocker } from '../../setup/index.js';
|
import { isDocker } from '../../setup/index.js';
|
||||||
|
|
||||||
|
import { extractPin } from './settings.utils.js';
|
||||||
|
|
||||||
export async function getSettings(_req: Request, res: Response<Settings>) {
|
export async function getSettings(_req: Request, res: Response<Settings>) {
|
||||||
const settings = DataProvider.getSettings();
|
const settings = DataProvider.getSettings();
|
||||||
const obfuscatedSettings = { ...settings };
|
const obfuscatedSettings = { ...settings };
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Business logic for resolving a string
|
||||||
|
*/
|
||||||
|
export function extractPin(value: string | undefined | null, fallback: string | null): string | null {
|
||||||
|
if (value === null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (value.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ interface Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service manages Ontime's runtime memory between boots
|
* Manages Ontime's runtime memory between boots
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AppStateService {
|
class AppState {
|
||||||
private config: Low<Config>;
|
private config: Low<Config>;
|
||||||
private pathToFile: string;
|
private pathToFile: string;
|
||||||
private didInit = false;
|
private didInit = false;
|
||||||
@@ -28,7 +28,7 @@ class AppStateService {
|
|||||||
this.didInit = true;
|
this.didInit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(): Promise<Config> {
|
private async get(): Promise<Config> {
|
||||||
if (!this.didInit) {
|
if (!this.didInit) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,11 @@ class AppStateService {
|
|||||||
return this.config.data;
|
return this.config.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLastLoadedProject(): Promise<string> {
|
||||||
|
const data = await this.get();
|
||||||
|
return data.lastLoadedProject;
|
||||||
|
}
|
||||||
|
|
||||||
async updateDatabaseConfig(filename: string): Promise<void> {
|
async updateDatabaseConfig(filename: string): Promise<void> {
|
||||||
if (isTest) return;
|
if (isTest) return;
|
||||||
|
|
||||||
@@ -48,4 +53,4 @@ class AppStateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appStateService = new AppStateService(appStatePath);
|
export const appStateProvider = new AppState(appStatePath);
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { DatabaseModel, GetInfo, ProjectData, ProjectFile, ProjectFileListResponse } from 'ontime-types';
|
import { DatabaseModel, GetInfo, ProjectData, ProjectFileListResponse } from 'ontime-types';
|
||||||
|
|
||||||
import { copyFile, rename, stat, writeFile } from 'fs/promises';
|
import { copyFile, rename } from 'fs/promises';
|
||||||
import { existsSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { initRundown } 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';
|
||||||
import { resolveProjectsDirectory, resolveStylesPath } from '../../setup/index.js';
|
import { resolveProjectsDirectory, resolveStylesPath } from '../../setup/index.js';
|
||||||
import { filterProjectFiles, parseProjectFile } from './projectFileUtils.js';
|
import { parseProjectFile } from './projectFileUtils.js';
|
||||||
import { appStateService } from '../app-state-service/AppStateService.js';
|
import { appStateProvider } from '../app-state-service/AppStateService.js';
|
||||||
import { ensureDirectory, getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
|
import { ensureDirectory, removeFileExtension } from '../../utils/fileManagement.js';
|
||||||
import { dbModel } from '../../models/dataModel.js';
|
import { dbModel } from '../../models/dataModel.js';
|
||||||
import { deleteFile } from '../../utils/parserUtils.js';
|
import { deleteFile } from '../../utils/parserUtils.js';
|
||||||
import { switchDb } from '../../setup/loadDb.js';
|
import { switchDb } from '../../setup/loadDb.js';
|
||||||
|
import { getPathToProject, getProjectFiles } from './projectServiceUtils.js';
|
||||||
|
|
||||||
// init dependencies
|
// init dependencies
|
||||||
init();
|
init();
|
||||||
@@ -34,7 +33,7 @@ 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(name: string, options?: Options) {
|
export async function applyProjectFile(name: string, options?: Options) {
|
||||||
const filePath = join(resolveProjectsDirectory, name);
|
const filePath = getPathToProject(name);
|
||||||
const data = parseProjectFile(filePath);
|
const data = parseProjectFile(filePath);
|
||||||
|
|
||||||
// change LowDB to point to new file
|
// change LowDB to point to new file
|
||||||
@@ -44,49 +43,7 @@ export async function applyProjectFile(name: string, options?: Options) {
|
|||||||
await applyDataModel(data, options);
|
await applyDataModel(data, options);
|
||||||
|
|
||||||
// persist the project selection
|
// persist the project selection
|
||||||
await appStateService.updateDatabaseConfig(name);
|
await appStateProvider.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously retrieves and returns an array of project files from the 'uploads' folder.
|
|
||||||
* Each file in the 'uploads' folder is checked, and only those with a '.json' extension are processed.
|
|
||||||
* For each qualifying file, its metadata is retrieved, including filename, creation time, and last modification time.
|
|
||||||
*
|
|
||||||
* @returns {Promise<Array<ProjectFile>>} A promise that resolves to an array of ProjectFile objects,
|
|
||||||
* each representing a file in the 'uploads' folder with its metadata.
|
|
||||||
* The metadata includes the filename, creation or overwriting time (updatedAt)
|
|
||||||
*
|
|
||||||
* @throws {Error} Throws an error if there is an issue in reading the directory or fetching file statistics.
|
|
||||||
*/
|
|
||||||
export async function getProjectFiles(): Promise<ProjectFile[]> {
|
|
||||||
const allFiles = await getFilesFromFolder(resolveProjectsDirectory);
|
|
||||||
const filteredFiles = filterProjectFiles(allFiles);
|
|
||||||
|
|
||||||
const projectFiles: ProjectFile[] = [];
|
|
||||||
for (const file of filteredFiles) {
|
|
||||||
const filePath = join(resolveProjectsDirectory, file);
|
|
||||||
const stats = await stat(filePath);
|
|
||||||
|
|
||||||
projectFiles.push({
|
|
||||||
filename: removeFileExtension(file),
|
|
||||||
updatedAt: stats.mtime.toISOString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return projectFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,12 +51,11 @@ export async function getProjectFiles(): Promise<ProjectFile[]> {
|
|||||||
*/
|
*/
|
||||||
export async function getProjectList(): Promise<ProjectFileListResponse> {
|
export async function getProjectList(): Promise<ProjectFileListResponse> {
|
||||||
const files = await getProjectFiles();
|
const files = await getProjectFiles();
|
||||||
const appState = await appStateService.get();
|
const lastLoadedProject = await appStateProvider.getLastLoadedProject();
|
||||||
const lastLoadedProject = removeFileExtension(appState.lastLoadedProject);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files,
|
files,
|
||||||
lastLoadedProject,
|
lastLoadedProject: removeFileExtension(lastLoadedProject),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +63,8 @@ export async function getProjectList(): Promise<ProjectFileListResponse> {
|
|||||||
* Duplicates an existing project file
|
* Duplicates an existing project file
|
||||||
*/
|
*/
|
||||||
export async function duplicateProjectFile(existingProjectFile: string, newProjectFile: string) {
|
export async function duplicateProjectFile(existingProjectFile: string, newProjectFile: string) {
|
||||||
const projectFilePath = join(resolveProjectsDirectory, existingProjectFile);
|
const projectFilePath = getPathToProject(existingProjectFile);
|
||||||
const duplicateProjectFilePath = join(resolveProjectsDirectory, newProjectFile);
|
const duplicateProjectFilePath = getPathToProject(newProjectFile);
|
||||||
|
|
||||||
return copyFile(projectFilePath, duplicateProjectFilePath);
|
return copyFile(projectFilePath, duplicateProjectFilePath);
|
||||||
}
|
}
|
||||||
@@ -117,16 +73,16 @@ export async function duplicateProjectFile(existingProjectFile: string, newProje
|
|||||||
* Renames an existing project file
|
* Renames an existing project file
|
||||||
*/
|
*/
|
||||||
export async function renameProjectFile(existingProjectFile: string, newName: string) {
|
export async function renameProjectFile(existingProjectFile: string, newName: string) {
|
||||||
const projectFilePath = join(resolveProjectsDirectory, existingProjectFile);
|
const projectFilePath = getPathToProject(existingProjectFile);
|
||||||
const newProjectFilePath = join(resolveProjectsDirectory, newName);
|
const newProjectFilePath = getPathToProject(newName);
|
||||||
|
|
||||||
await rename(projectFilePath, newProjectFilePath);
|
await rename(projectFilePath, newProjectFilePath);
|
||||||
|
|
||||||
// Update the last loaded project config if current loaded project is the one being renamed
|
// Update the last loaded project config if current loaded project is the one being renamed
|
||||||
const { lastLoadedProject } = await appStateService.get();
|
const lastLoadedProject = await appStateProvider.getLastLoadedProject();
|
||||||
|
|
||||||
if (lastLoadedProject === existingProjectFile) {
|
if (lastLoadedProject === existingProjectFile) {
|
||||||
await appStateService.updateDatabaseConfig(newName);
|
await appStateProvider.updateDatabaseConfig(newName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +99,8 @@ export async function createProjectFile(filename: string, projectData: ProjectDa
|
|||||||
};
|
};
|
||||||
|
|
||||||
// create new file
|
// create new file
|
||||||
const newFile = join(resolveProjectsDirectory, filename);
|
|
||||||
await writeFile(newFile, JSON.stringify(data));
|
await writeFile(newFile, JSON.stringify(data));
|
||||||
|
const newFile = getPathToProject(filename);
|
||||||
|
|
||||||
// change LowDB to point to new file
|
// change LowDB to point to new file
|
||||||
await switchDb(filename);
|
await switchDb(filename);
|
||||||
@@ -152,14 +108,15 @@ export async function createProjectFile(filename: string, projectData: ProjectDa
|
|||||||
// apply its data
|
// apply its data
|
||||||
await applyDataModel(data);
|
await applyDataModel(data);
|
||||||
|
|
||||||
appStateService.updateDatabaseConfig(filename);
|
// update app state to point to new value
|
||||||
|
appStateProvider.updateDatabaseConfig(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a project file
|
* Deletes a project file
|
||||||
*/
|
*/
|
||||||
export async function deleteProjectFile(filename: string) {
|
export async function deleteProjectFile(filename: string) {
|
||||||
const projectFilePath = join(resolveProjectsDirectory, filename);
|
const projectFilePath = getPathToProject(filename);
|
||||||
await deleteFile(projectFilePath);
|
await deleteFile(projectFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,22 +141,6 @@ export async function getInfo(): Promise<GetInfo> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Business logic for resolving a string
|
|
||||||
*/
|
|
||||||
export function extractPin(value: string | undefined | null, fallback: string | null): string | null {
|
|
||||||
if (value === null) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof value === 'undefined') {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
if (value.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* applies a partial database model
|
* applies a partial database model
|
||||||
*/
|
*/
|
||||||
@@ -216,51 +157,3 @@ export async function applyDataModel(data: Partial<DatabaseModel>, _options?: Op
|
|||||||
|
|
||||||
return newData;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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: string[] = [];
|
|
||||||
|
|
||||||
if (projectFiles.filename) {
|
|
||||||
const projectFilePath = join(resolveProjectsDirectory, projectFiles.filename);
|
|
||||||
|
|
||||||
if (!existsSync(projectFilePath)) {
|
|
||||||
errors.push('Project file does not exist');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectFiles.newFilename) {
|
|
||||||
const projectFilePath = join(resolveProjectsDirectory, projectFiles.newFilename);
|
|
||||||
|
|
||||||
if (existsSync(projectFilePath)) {
|
|
||||||
errors.push('New project file already exists');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current project title or fallback
|
|
||||||
*/
|
|
||||||
export function getProjectTitle(): string {
|
|
||||||
const { title } = DataProvider.getProjectData();
|
|
||||||
return title || 'ontime data';
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { expect, vi } from 'vitest';
|
import { expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { getProjectFiles } from '../ProjectService.js';
|
import { getProjectFiles } from '../projectServiceUtils.js';
|
||||||
|
|
||||||
vi.mock('fs/promises', () => {
|
vi.mock('fs/promises', () => {
|
||||||
const mockFiles = ['file1.json', 'file2.json', 'file3.json', 'document.txt', 'image.png'];
|
const mockFiles = ['file1.json', 'file2.json', 'file3.json', 'document.txt', 'image.png'];
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { ProjectFile } from 'ontime-types';
|
||||||
|
|
||||||
|
import { stat } from 'fs/promises';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { resolveProjectsDirectory } from '../../setup/index.js';
|
||||||
|
import { filterProjectFiles } from './projectFileUtils.js';
|
||||||
|
import { getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
|
||||||
|
import { moveUploadedFile } from '../../utils/upload.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the upload of a new project file
|
||||||
|
* @param filePath
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function upload(filePath: string, name: string) {
|
||||||
|
const newFilePath = join(resolveProjectsDirectory, name);
|
||||||
|
await moveUploadedFile(filePath, newFilePath);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously retrieves and returns an array of project files from the 'uploads' folder.
|
||||||
|
* Each file in the 'uploads' folder is checked, and only those with a '.json' extension are processed.
|
||||||
|
* For each qualifying file, its metadata is retrieved, including filename, creation time, and last modification time.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Array<ProjectFile>>} A promise that resolves to an array of ProjectFile objects,
|
||||||
|
* each representing a file in the 'uploads' folder with its metadata.
|
||||||
|
* The metadata includes the filename, creation or overwriting time (updatedAt)
|
||||||
|
*
|
||||||
|
* @throws {Error} Throws an error if there is an issue in reading the directory or fetching file statistics.
|
||||||
|
*/
|
||||||
|
export async function getProjectFiles(): Promise<ProjectFile[]> {
|
||||||
|
const allFiles = await getFilesFromFolder(resolveProjectsDirectory);
|
||||||
|
const filteredFiles = filterProjectFiles(allFiles);
|
||||||
|
|
||||||
|
const projectFiles: ProjectFile[] = [];
|
||||||
|
for (const file of filteredFiles) {
|
||||||
|
const filePath = join(resolveProjectsDirectory, file);
|
||||||
|
const stats = await stat(filePath);
|
||||||
|
|
||||||
|
projectFiles.push({
|
||||||
|
filename: removeFileExtension(file),
|
||||||
|
updatedAt: stats.mtime.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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: string[] = [];
|
||||||
|
|
||||||
|
// current project must exist
|
||||||
|
if (projectFiles.filename) {
|
||||||
|
if (!doesProjectExist(projectFiles.filename)) {
|
||||||
|
errors.push('Project file does not exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// new project must NOT exist
|
||||||
|
if (projectFiles.newFilename) {
|
||||||
|
if (doesProjectExist(projectFiles.newFilename)) {
|
||||||
|
errors.push('New project file already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute path to a project file
|
||||||
|
*/
|
||||||
|
export function getPathToProject(name: string): string {
|
||||||
|
return join(resolveProjectsDirectory, name);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { pathToStartDb, resolveDbDirectory, resolveDbName } from './index.js';
|
|||||||
import { parseProjectFile } from '../services/project-service/projectFileUtils.js';
|
import { parseProjectFile } from '../services/project-service/projectFileUtils.js';
|
||||||
import { parseJson } from '../utils/parser.js';
|
import { parseJson } from '../utils/parser.js';
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
import { getErrorMessage } from 'ontime-utils';
|
||||||
import { appStateService } from '../services/app-state-service/AppStateService.js';
|
import { appStateProvider } from '../services/app-state-service/AppStateService.js';
|
||||||
import { consoleError } from '../utils/console.js';
|
import { consoleError } from '../utils/console.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +58,7 @@ async function loadDb(directory: string, filename: string) {
|
|||||||
const maybeProjectFile = parseProjectFile(dbInDisk);
|
const maybeProjectFile = parseProjectFile(dbInDisk);
|
||||||
const result = parseJson(maybeProjectFile);
|
const result = parseJson(maybeProjectFile);
|
||||||
|
|
||||||
await appStateService.updateDatabaseConfig(filename);
|
await appStateProvider.updateDatabaseConfig(filename);
|
||||||
|
|
||||||
newData = result.data;
|
newData = result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { rm } from 'fs/promises';
|
import { rename, rm } from 'fs/promises';
|
||||||
|
|
||||||
import { ensureDirectory } from './fileManagement.js';
|
import { ensureDirectory } from './fileManagement.js';
|
||||||
import { getAppDataPath, uploadsFolderPath } from '../setup/index.js';
|
import { getAppDataPath, uploadsFolderPath } from '../setup/index.js';
|
||||||
|
import { deleteFile } from './parserUtils.js';
|
||||||
|
|
||||||
function generateNewFileName(filePath: string, callback: (newName: string) => void) {
|
function generateNewFileName(filePath: string, callback: (newName: string) => void) {
|
||||||
const baseName = path.basename(filePath, path.extname(filePath));
|
const baseName = path.basename(filePath, path.extname(filePath));
|
||||||
@@ -68,3 +69,14 @@ export async function clearUploadfolder() {
|
|||||||
// we dont care that there was no folder
|
// we dont care that there was no folder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file from upload folder to a destination
|
||||||
|
* @param filePath
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function moveUploadedFile(fromUpload: string, toDestination: string) {
|
||||||
|
await rename(fromUpload, toDestination);
|
||||||
|
await deleteFile(fromUpload);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user