refactor: simplify setup of asset paths

This commit is contained in:
Carlos Valente
2024-10-13 13:53:55 +02:00
committed by Carlos Valente
parent e46948772e
commit 5950551da2
17 changed files with 177 additions and 144 deletions
@@ -3,8 +3,8 @@ import { getErrorMessage, obfuscate } from 'ontime-utils';
import type { Request, Response } from 'express';
import { isDocker } from '../../externals.js';
import { failEmptyObjects } from '../../utils/routerUtils.js';
import { isDocker } from '../../setup/index.js';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { extractPin } from './settings.utils.js';
+9 -17
View File
@@ -9,15 +9,8 @@ import serverTiming from 'server-timing';
// import utils
import { resolve } from 'path';
import {
srcDirectory,
environment,
isProduction,
resolveExternalsDirectory,
resolveStylesDirectory,
resolvedPath,
resolvePublicDirectoy,
} from './setup/index.js';
import { publicDir, srcDir } from './setup/index.js';
import { environment, isProduction } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
@@ -56,8 +49,8 @@ consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
const canLog = isProduction;
if (!canLog) {
console.log(`Ontime running in ${environment} environment`);
console.log(`Ontime source directory at ${srcDirectory} `);
console.log(`Ontime public directory at ${resolvePublicDirectoy} `);
console.log(`Ontime source directory at ${srcDir.root} `);
console.log(`Ontime public directory at ${publicDir.root} `);
}
// Create express APP
@@ -82,17 +75,16 @@ app.use(express.json({ limit: '1mb' }));
app.use('/data', appRouter); // router for application data
app.use('/api', integrationRouter); // router for integrations
// serve static - css
app.use('/external/styles', express.static(resolveStylesDirectory));
app.use('/external/', express.static(resolveExternalsDirectory));
// serve static external files
app.use('/external/', express.static(publicDir.externalDir));
// if the user reaches to the root, we show a 404
app.use('/external', (req, res) => {
res.status(404).send(`${req.originalUrl} not found`);
});
// serve static - react, in dev/test mode we fetch the React app from module
const reactAppPath = resolvedPath();
app.use(
expressStaticGzip(reactAppPath, {
expressStaticGzip(srcDir.clientDir, {
enableBrotli: true,
orderPreference: ['br'],
// when we build the client all the react subfiles will get a hashed name we can the immutable tag
@@ -103,7 +95,7 @@ app.use(
);
app.get('*', (_req, res) => {
res.sendFile(resolve(reactAppPath, 'index.html'));
res.sendFile(resolve(srcDir.clientDir, 'index.html'));
});
// Implement catch all
+1 -1
View File
@@ -2,9 +2,9 @@ import { Log, LogLevel } from 'ontime-types';
import { generateId, millisToString } from 'ontime-utils';
import { clock } from '../services/Clock.js';
import { isProduction } from '../setup/index.js';
import { socket } from '../adapters/WebsocketAdapter.js';
import { consoleSubdued, consoleError } from '../utils/console.js';
import { isProduction } from '../externals.js';
class Logger {
private queue: Log[];
@@ -13,11 +13,11 @@ import {
import type { Low } from 'lowdb';
import { JSONFilePreset } from 'lowdb/node';
import { isTest } from '../../setup/index.js';
import { isPath } from '../../utils/fileManagement.js';
import { shouldCrashDev } from '../../utils/development.js';
import { isTest } from '../../externals.js';
import { safeMerge } from './DataProvider.utils.js';
import { shouldCrashDev } from '../../utils/development.js';
type ReadonlyPromise<T> = Promise<Readonly<T>>;
+12
View File
@@ -0,0 +1,12 @@
/**
* This file contains a list of constants that may need to be resolved at runtime
*/
// =================================================
// resolve running environment
const env = process.env.NODE_ENV || 'production';
export const isTest = Boolean(process.env.IS_TEST);
export const environment = isTest ? 'test' : env;
export const isDocker = env === 'docker';
export const isProduction = isDocker || (env === 'production' && !isTest);
+3 -2
View File
@@ -1,9 +1,10 @@
import { MaybeNumber, MaybeString, Playback } from 'ontime-types';
import { JSONFile } from 'lowdb/node';
import { resolveRestoreFile } from '../setup/index.js';
import { deepEqual } from 'fast-equals';
import { publicFiles } from '../setup/index.js';
export type RestorePoint = {
playback: Playback;
selectedEventId: MaybeString;
@@ -147,4 +148,4 @@ export class RestoreService {
}
}
export const restoreService = new RestoreService(resolveRestoreFile);
export const restoreService = new RestoreService(publicFiles.restoreFile);
@@ -1,7 +1,8 @@
import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';
import { appStatePath, isTest } from '../../setup/index.js';
import { publicFiles } from '../../setup/index.js';
import { isTest } from '../../externals.js';
import { isPath } from '../../utils/fileManagement.js';
import { shouldCrashDev } from '../../utils/development.js';
@@ -9,7 +10,7 @@ interface AppState {
lastLoadedProject?: string;
}
const adapter = new JSONFile<AppState>(appStatePath);
const adapter = new JSONFile<AppState>(publicFiles.appState);
const config = new Low<AppState>(adapter, {});
export async function isLastLoadedProject(projectName: string): Promise<boolean> {
@@ -5,7 +5,7 @@ import { copyFile, rename } from 'fs/promises';
import { logger } from '../../classes/Logger.js';
import { getNetworkInterfaces } from '../../utils/networkInterfaces.js';
import { resolveCorruptDirectory, resolveProjectsDirectory, resolveStylesPath } from '../../setup/index.js';
import { publicDir, publicFiles } from '../../setup/index.js';
import {
appendToName,
ensureDirectory,
@@ -47,8 +47,8 @@ init();
* Ensure services has its dependencies initialized
*/
function init() {
ensureDirectory(resolveProjectsDirectory);
ensureDirectory(resolveCorruptDirectory);
ensureDirectory(publicDir.projectsDir);
ensureDirectory(publicDir.corruptDir);
}
export async function getCurrentProject() {
@@ -63,7 +63,7 @@ export async function getCurrentProject() {
* to be composed in the loading functions
*/
async function loadDemoProject(): Promise<string> {
const pathToNewFile = generateUniqueFileName(resolveProjectsDirectory, config.demoProject);
const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, config.demoProject);
await initPersistence(getPathToProject(pathToNewFile), demoDb);
const newName = getFileNameFromPath(pathToNewFile);
await setLastLoadedProject(newName);
@@ -75,7 +75,7 @@ async function loadDemoProject(): Promise<string> {
* to be composed in the loading functions
*/
async function loadNewProject(): Promise<string> {
const pathToNewFile = generateUniqueFileName(resolveProjectsDirectory, config.newProject);
const pathToNewFile = generateUniqueFileName(publicDir.projectsDir, config.newProject);
await initPersistence(getPathToProject(pathToNewFile), dbModel);
const newName = getFileNameFromPath(pathToNewFile);
await setLastLoadedProject(newName);
@@ -273,7 +273,7 @@ export async function createProject(filename: string, projectData: ProjectData)
},
};
const uniqueFileName = generateUniqueFileName(resolveProjectsDirectory, filename);
const uniqueFileName = generateUniqueFileName(publicDir.projectsDir, filename);
const newFile = getPathToProject(uniqueFileName);
// change LowDB to point to new file
@@ -316,14 +316,13 @@ export async function getInfo(): Promise<GetInfo> {
// get nif and inject localhost
const ni = getNetworkInterfaces();
ni.unshift({ name: 'localhost', address: '127.0.0.1' });
const cssOverride = resolveStylesPath;
return {
networkInterfaces: ni,
version,
serverPort,
osc,
cssOverride,
cssOverride: publicFiles.cssOverride,
};
}
@@ -4,7 +4,7 @@ import { existsSync } from 'fs';
import { copyFile, readFile, rename, stat } from 'fs/promises';
import { extname, join } from 'path';
import { resolveCorruptDirectory, resolveProjectsDirectory } from '../../setup/index.js';
import { publicDir } from '../../setup/index.js';
import { getFilesFromFolder, removeFileExtension } from '../../utils/fileManagement.js';
/**
@@ -13,7 +13,7 @@ import { getFilesFromFolder, removeFileExtension } from '../../utils/fileManagem
* @param name
*/
export async function handleUploaded(filePath: string, name: string) {
const newFilePath = join(resolveProjectsDirectory, name);
const newFilePath = join(publicDir.projectsDir, name);
await rename(filePath, newFilePath);
}
@@ -29,12 +29,12 @@ export async function handleUploaded(filePath: string, name: string) {
* @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 allFiles = await getFilesFromFolder(publicDir.projectsDir);
const filteredFiles = filterProjectFiles(allFiles);
const projectFiles: ProjectFile[] = [];
for (const file of filteredFiles) {
const filePath = join(resolveProjectsDirectory, file);
const filePath = join(publicDir.projectsDir, file);
const stats = await stat(filePath);
projectFiles.push({
@@ -62,14 +62,14 @@ export function doesProjectExist(name: string): MaybeString {
* Returns the absolute path to a project file
*/
export function getPathToProject(name: string): string {
return join(resolveProjectsDirectory, name);
return join(publicDir.projectsDir, name);
}
/**
* Makes a copy of a given project to the corrupted directory
*/
export async function copyCorruptFile(filePath: string, name: string): Promise<void> {
const newPath = join(resolveCorruptDirectory, name);
const newPath = join(publicDir.corruptDir, name);
return copyFile(filePath, newPath);
}
@@ -77,7 +77,7 @@ export async function copyCorruptFile(filePath: string, name: string): Promise<v
* Moves a file permanently to the corrupted directory
*/
export async function moveCorruptFile(filePath: string, name: string): Promise<void> {
const newPath = join(resolveCorruptDirectory, name);
const newPath = join(publicDir.corruptDir, name);
return rename(filePath, newPath);
}
@@ -11,7 +11,7 @@ import { sheets, sheets_v4 } from '@googleapis/sheets';
import { Credentials, OAuth2Client } from 'google-auth-library';
import got from 'got';
import { resolveSheetsDirectory } from '../../setup/index.js';
import { publicDir } from '../../setup/index.js';
import { ensureDirectory } from '../../utils/fileManagement.js';
import { cellRequestFromEvent, type ClientSecret, getA1Notation, validateClientSecret } from './sheetUtils.js';
import { parseExcel } from '../../utils/parser.js';
@@ -57,7 +57,7 @@ function reset() {
*/
export function init() {
reset();
ensureDirectory(resolveSheetsDirectory);
ensureDirectory(publicDir.sheetsDir);
}
/**
+83 -75
View File
@@ -1,18 +1,26 @@
/**
* This file handles resolving paths for the server resources
* There are two main directories
* - 1. the installation directory, exposed by __dirname
* - 2. the public directory, exposed by getAppDataPath()
*/
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { config } from './config.js';
import { ensureDirectory } from '../utils/fileManagement.js';
// =================================================
// resolve public path
import { isProduction } from '../externals.js';
/**
* @description Returns public path depending on OS
* Returns public path depending on OS
* This is the correct path for the app running in production mode
*/
export function getAppDataPath(): string {
// handle docker
/**
* If we are running in docker, the ONTIME_DATA environment variable
* allows moving the public directory to a user defined location
*/
if (process.env.ONTIME_DATA) {
return join(process.env.ONTIME_DATA);
}
@@ -33,86 +41,86 @@ export function getAppDataPath(): string {
}
}
// =================================================
// resolve running environment
const env = process.env.NODE_ENV || 'production';
/**
* 1. Paths relative to the installation (or source in development)
* ------------------------------------------------------------------
*/
export const isTest = Boolean(process.env.IS_TEST);
export const environment = isTest ? 'test' : env;
export const isDocker = env === 'docker';
export const isProduction = isDocker || (env === 'production' && !isTest);
// =================================================
// Resolve directory paths
// resolve file URL in both CJS and ESM (build and dev)
/** resolve file URL in both CJS and ESM (build and dev) */
if (import.meta.url) {
globalThis.__dirname = fileURLToPath(import.meta.url);
}
// path to server src folder
const currentDir = dirname(__dirname);
// locally we are in src/setup, in the production build, this is a single file at src
export const srcDirectory = isProduction ? currentDir : join(currentDir, '../');
// TODO: simplify logic
// resolve path to client
const productionPath = join(srcDirectory, 'client/');
const devPath = join(srcDirectory, '../../client/build/');
export const resolvedPath = (): string => {
if (isTest) {
return devPath;
}
if (isProduction) {
return productionPath;
}
return devPath;
/**
* path to server src folder
* when running in dev, this file is located in src/setup, so we go one level up
* */
const srcDirectory = isProduction ? currentDir : join(currentDir, '../');
export const srcDir = {
root: srcDirectory,
/** Path to the react app */
clientDir: isProduction ? join(srcDirectory, 'client/') : join(srcDirectory, '../../client/build/'),
/** Path to the demo app */
demoDir: join(srcDirectory, '/external/demo/'),
} as const;
export const srcFiles = {
/** Path to bundled CSS */
cssOverride: join(srcDir.root, '/external/styles/', config.styles.filename),
};
// resolve public directory
export const resolvePublicDirectoy = getAppDataPath();
ensureDirectory(resolvePublicDirectoy);
/**
* 2. Paths relative to the user public directory
* ------------------------------------------------
*/
export const externalsStartDirectory = isProduction ? resolvePublicDirectoy : join(srcDirectory, 'external');
// TODO: we only need one when they are all in the same folder
export const resolveExternalsDirectory = join(isProduction ? resolvePublicDirectoy : srcDirectory, 'external');
/** Resolve root to public directory */
const resolvePublicDirectory = getAppDataPath();
// Ensure directory tree is created
ensureDirectory(resolvePublicDirectory);
// project files
export const appStatePath = join(resolvePublicDirectoy, config.appState);
export const uploadsFolderPath = join(resolvePublicDirectoy, config.uploads);
/**
* Path to external
* This is unique in the way that we use the src directory in development
* For simplicity we still bundle this in the public directory object
*/
const externalsStartDirectory = isProduction ? resolvePublicDirectory : join(srcDirectory, 'external');
// path to public styles
export const resolveStylesDirectory = join(externalsStartDirectory, config.styles.directory);
export const resolveStylesPath = join(resolveStylesDirectory, config.styles.filename);
export const publicDir = {
root: resolvePublicDirectory,
/** path to sheets folder */
sheetsDir: join(resolvePublicDirectory, config.sheets.directory),
/** path to crash reports folder */
crashDir: join(resolvePublicDirectory, config.crash),
/** path to projects folder */
projectsDir: join(resolvePublicDirectory, config.projects),
/** path to corrupt folder */
corruptDir: join(resolvePublicDirectory, config.corrupt),
/** path to uploads folder */
uploadsDir: join(resolvePublicDirectory, config.uploads),
/** path to external folder */
externalDir: externalsStartDirectory,
/** path to demo project folder */
demoDir: join(
externalsStartDirectory,
isProduction ? '/external/' : '', // move to external folder in production
config.demo.directory,
),
/** path to external styles override */
stylesDir: join(externalsStartDirectory, config.styles.directory),
} as const;
export const pathToStartStyles = join(srcDirectory, '/external/styles/', config.styles.filename);
// path to public demo
export const resolveDemoDirectory = join(
externalsStartDirectory,
isProduction ? '/external/' : '', // move to external folder in production
config.demo.directory,
);
export const resolveDemoPath = config.demo.filename.map((file) => {
return join(resolveDemoDirectory, file);
});
// path to demo project
export const pathToStartDemo = config.demo.filename.map((file) => {
return join(srcDirectory, '/external/demo/', file);
});
// path to restore file
export const resolveRestoreFile = join(resolvePublicDirectoy, config.restoreFile);
// path to sheets folder
export const resolveSheetsDirectory = join(resolvePublicDirectoy, config.sheets.directory);
// path to crash reports
export const resolveCrashReportDirectory = join(resolvePublicDirectoy, config.crash);
// path to projects
export const resolveProjectsDirectory = join(resolvePublicDirectoy, config.projects);
// path to corrupt files
export const resolveCorruptDirectory = join(resolvePublicDirectoy, config.corrupt);
/**
* Resolve path to specific files
*/
export const publicFiles = {
/** path to app state file */
appState: join(publicDir.root, config.appState),
/** path to restore file */
restoreFile: join(publicDir.root, config.restoreFile),
/** path to CSS override file */
cssOverride: join(publicDir.stylesDir, config.styles.filename),
};
+6 -10
View File
@@ -1,20 +1,16 @@
import { copyFile } from 'fs/promises';
import { pathToStartDemo, resolveDemoDirectory, resolveDemoPath } from './index.js';
import { ensureDirectory } from '../utils/fileManagement.js';
import { copyDirectory, ensureDirectory } from '../utils/fileManagement.js';
import { publicDir, srcDir } from './index.js';
/**
* @description ensures directories exist and populates demo folder
*/
export const populateDemo = () => {
ensureDirectory(resolveDemoDirectory);
ensureDirectory(publicDir.demoDir);
// even if demo exist we want to use startup demo
try {
Promise.all(
resolveDemoPath.map((to, index) => {
const from = pathToStartDemo[index];
return copyFile(from, to);
}),
);
copyDirectory(srcDir.demoDir, publicDir.demoDir);
} catch (_) {
/* we do not handle this */
}
+8 -5
View File
@@ -1,16 +1,19 @@
import { copyFileSync, existsSync } from 'fs';
import { pathToStartStyles, resolveStylesDirectory, resolveStylesPath } from './index.js';
import { ensureDirectory } from '../utils/fileManagement.js';
import { publicDir, publicFiles, srcFiles } from './index.js';
/**
* @description ensures directories exist and populates stylesheet
* ensures directories exist and populates stylesheet
*/
export const populateStyles = () => {
ensureDirectory(resolveStylesDirectory);
ensureDirectory(publicDir.stylesDir);
// if styles doesn't exist we want to use startup stylesheet
if (!existsSync(resolveStylesPath)) {
if (!existsSync(publicFiles.cssOverride)) {
try {
copyFileSync(pathToStartStyles, resolveStylesPath);
// copy the startup stylesheet to the public directory
copyFileSync(srcFiles.cssOverride, publicFiles.cssOverride);
} catch (_) {
/* we do not handle this */
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { isProduction } from '../setup/index.js';
import { isProduction } from '../externals.js';
import { consoleError } from '../utils/console.js';
/**
+22 -2
View File
@@ -1,5 +1,5 @@
import { existsSync, mkdirSync } from 'fs';
import { readdir } from 'fs/promises';
import { readdir, copyFile } from 'fs/promises';
import { basename, extname, join, parse } from 'path';
/**
@@ -80,8 +80,28 @@ export function getFileNameFromPath(filePath: string): string {
}
/**
* Utility naivly checks for paths on whether it includes directories
* Utility naively checks for paths on whether it includes directories
*/
export function isPath(filePath: string): boolean {
return filePath !== basename(filePath);
}
/**
* Recursively copies a directory and its contents.
* @param {string} src - The source directory.
* @param {string} dest - The destination directory.
*/
export async function copyDirectory(src: string, dest: string) {
const entries = await readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isDirectory()) {
await copyDirectory(srcPath, destPath);
} else {
await copyFile(srcPath, destPath);
}
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import { join } from 'path';
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
import { get } from '../services/rundown-service/rundownCache.js';
import { getState } from '../stores/runtimeState.js';
import { resolveCrashReportDirectory } from '../setup/index.js';
import { publicDir } from '../setup/index.js';
import { ensureDirectory } from './fileManagement.js';
/**
@@ -13,8 +13,8 @@ import { ensureDirectory } from './fileManagement.js';
* @param content
*/
function writeToFile(fileName: string, content: object) {
const path = join(resolveCrashReportDirectory, fileName);
ensureDirectory(resolveCrashReportDirectory);
const path = join(publicDir.crashDir, fileName);
ensureDirectory(publicDir.crashDir);
try {
const textContent = JSON.stringify(content, null, 2);
+7 -6
View File
@@ -3,8 +3,9 @@ import path from 'path';
import fs from 'fs';
import { rm } from 'fs/promises';
import { getAppDataPath, publicDir } from '../setup/index.js';
import { ensureDirectory } from './fileManagement.js';
import { getAppDataPath, uploadsFolderPath } from '../setup/index.js';
function generateNewFileName(filePath: string, callback: (newName: string) => void) {
const baseName = path.basename(filePath, path.extname(filePath));
@@ -36,19 +37,19 @@ export const storage = multer.diskStorage({
throw new Error('Could not resolve public folder for platform');
}
ensureDirectory(uploadsFolderPath);
ensureDirectory(publicDir.uploadsDir);
const filePath = path.join(uploadsFolderPath, file.originalname);
const filePath = path.join(publicDir.uploadsDir, file.originalname);
// Check if file already exists
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
// File does not exist, can safely proceed to this destination
cb(null, uploadsFolderPath);
cb(null, publicDir.uploadsDir);
} else {
generateNewFileName(filePath, (newName) => {
file.originalname = newName;
cb(null, uploadsFolderPath);
cb(null, publicDir.uploadsDir);
});
}
});
@@ -63,7 +64,7 @@ export const storage = multer.diskStorage({
*/
export async function clearUploadfolder() {
try {
await rm(uploadsFolderPath, { recursive: true });
await rm(publicDir.uploadsDir, { recursive: true });
} catch (_) {
// we dont care that there was no folder
}