Initial
This commit is contained in:
35
package-lock.json
generated
35
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"monaco-editor": "^0.53.0",
|
"monaco-editor": "^0.53.0",
|
||||||
"multer": "^2.2.0",
|
"multer": "^2.2.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"path": "^0.12.7",
|
||||||
"tailwindcss": "^4.3.1",
|
"tailwindcss": "^4.3.1",
|
||||||
"vite": "^8.1.0",
|
"vite": "^8.1.0",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
@@ -2584,6 +2585,16 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path": {
|
||||||
|
"version": "0.12.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||||
|
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"process": "^0.11.1",
|
||||||
|
"util": "^0.10.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "8.4.2",
|
"version": "8.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
@@ -2706,6 +2717,15 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -3319,12 +3339,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||||
|
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/util/node_modules/inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"monaco-editor": "^0.53.0",
|
"monaco-editor": "^0.53.0",
|
||||||
"multer": "^2.2.0",
|
"multer": "^2.2.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"path": "^0.12.7",
|
||||||
"tailwindcss": "^4.3.1",
|
"tailwindcss": "^4.3.1",
|
||||||
"vite": "^8.1.0",
|
"vite": "^8.1.0",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import path from 'path';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
if (fs.existsSync('dist/index.html')) {
|
if (fs.existsSync(path.join('dist', 'index.html'))) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = spawnSync(process.execPath, ['node_modules/vite/bin/vite.js', 'build'], { stdio: 'inherit' });
|
const result = spawnSync(process.execPath, [path.join('node_modules', 'vite', 'bin', 'vite.js'), 'build'], { stdio: 'inherit' });
|
||||||
|
|
||||||
process.exit(result.status ?? 1);
|
process.exit(result.status ?? 1);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
const mode = process.argv[2] || 'development';
|
const mode = process.argv[2] || 'development';
|
||||||
const watch = process.argv.includes('--watch');
|
const watch = process.argv.includes('--watch');
|
||||||
const args = watch
|
const args = watch
|
||||||
? ['node_modules/nodemon/bin/nodemon.js', 'server/index.js']
|
? [path.join('node_modules', 'nodemon', 'bin', 'nodemon.js'), path.join('server', 'index.js')]
|
||||||
: ['server/index.js'];
|
: [path.join('server', 'index.js')];
|
||||||
|
|
||||||
const child = spawn(process.execPath, args, {
|
const child = spawn(process.execPath, args, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
const child = spawn(process.execPath, ['node_modules/vite/bin/vite.js', 'preview', '--host', '0.0.0.0', '--port', '3000', '--strictPort'], {
|
const child = spawn(process.execPath, [path.join('node_modules', 'vite', 'bin', 'vite.js'), 'preview', '--host', '0.0.0.0', '--port', '3000', '--strictPort'], {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'node:path';
|
import { path } from './utils/pathUtils.js';
|
||||||
|
|
||||||
dotenv.config({ quiet: true });
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
updatePsadtProfile
|
updatePsadtProfile
|
||||||
} from '../models/psadtModel.js';
|
} from '../models/psadtModel.js';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { createChangeRequest, findApprovedRequest, findPendingRequest, markRequestExecuted } from '../models/changeRequestModel.js';
|
import { createChangeRequest, findApprovedRequest, findPendingRequest, markRequestExecuted } from '../models/changeRequestModel.js';
|
||||||
import { buildIntuneWin, builderAvailability } from '../services/intuneWinBuilder.js';
|
import { buildIntuneWin, builderAvailability } from '../services/intuneWinBuilder.js';
|
||||||
@@ -30,6 +29,7 @@ import { parseIntunewin } from '../services/intunewinParser.js';
|
|||||||
import { createAssetFromUpload, getAssetFile } from '../models/assetModel.js';
|
import { createAssetFromUpload, getAssetFile } from '../models/assetModel.js';
|
||||||
import { validatePsadtScript, validationRuleCatalog } from '../services/psadtValidator.js';
|
import { validatePsadtScript, validationRuleCatalog } from '../services/psadtValidator.js';
|
||||||
import { planPsadtMigration } from '../services/psadtMigrationService.js';
|
import { planPsadtMigration } from '../services/psadtMigrationService.js';
|
||||||
|
import { basenameAny, path } from '../utils/pathUtils.js';
|
||||||
|
|
||||||
export function catalog(req, res) {
|
export function catalog(req, res) {
|
||||||
res.json(getPsadtCatalog());
|
res.json(getPsadtCatalog());
|
||||||
@@ -353,9 +353,10 @@ export async function intuneBuild(req, res) {
|
|||||||
try {
|
try {
|
||||||
const { outputFile } = await buildIntuneWin({ sourceFolder, setupFile, outputDir });
|
const { outputFile } = await buildIntuneWin({ sourceFolder, setupFile, outputDir });
|
||||||
const stats = fs.statSync(outputFile);
|
const stats = fs.statSync(outputFile);
|
||||||
|
const outputName = basenameAny(outputFile, 'package.intunewin');
|
||||||
const asset = createAssetFromUpload(
|
const asset = createAssetFromUpload(
|
||||||
{ path: outputFile, originalname: path.basename(outputFile), mimetype: 'application/octet-stream', size: stats.size },
|
{ path: outputFile, originalname: outputName, mimetype: 'application/octet-stream', size: stats.size },
|
||||||
{ name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: path.basename(outputFile) },
|
{ name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: outputName },
|
||||||
req.user.id
|
req.user.id
|
||||||
);
|
);
|
||||||
const updated = updateIntuneDeployment(req.params.id, { ...deployment, intunewinFile: asset.originalName }, req.user.id);
|
const updated = updateIntuneDeployment(req.params.id, { ...deployment, intunewinFile: asset.originalName }, req.user.id);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import { path } from './utils/pathUtils.js';
|
||||||
import { DatabaseSync } from 'node:sqlite';
|
import { DatabaseSync } from 'node:sqlite';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
import { path, sanitizeFileName } from '../utils/pathUtils.js';
|
||||||
|
|
||||||
const incomingDir = path.join(config.fileLockerDir, '_incoming');
|
const incomingDir = path.resolve(config.fileLockerDir, '_incoming');
|
||||||
fs.mkdirSync(incomingDir, { recursive: true });
|
fs.mkdirSync(incomingDir, { recursive: true });
|
||||||
|
|
||||||
function sanitizeFileName(name) {
|
|
||||||
return String(name || 'asset.bin')
|
|
||||||
.replace(/[/\\?%*:|"<>]/g, '-')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.slice(0, 180) || 'asset.bin';
|
|
||||||
}
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination(req, file, callback) {
|
destination(req, file, callback) {
|
||||||
fs.mkdirSync(incomingDir, { recursive: true });
|
fs.mkdirSync(incomingDir, { recursive: true });
|
||||||
|
|||||||
@@ -1,30 +1,13 @@
|
|||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { db, id, now, visibleClause } from '../db.js';
|
import { db, id, now, visibleClause } from '../db.js';
|
||||||
import { camelAsset, camelAssetFolder, camelAssetLink } from './mappers.js';
|
import { camelAsset, camelAssetFolder, camelAssetLink } from './mappers.js';
|
||||||
|
import { extnameAny, isPathInside, normalizePackagePath, path, sanitizeFileName } from '../utils/pathUtils.js';
|
||||||
function sanitizeFileName(name) {
|
|
||||||
return String(name || 'asset.bin')
|
|
||||||
.replace(/[/\\?%*:|"<>]/g, '-')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
.slice(0, 180) || 'asset.bin';
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePackagePath(value, fallbackName = '') {
|
|
||||||
const clean = String(value || fallbackName || '')
|
|
||||||
.replace(/\\/g, '/')
|
|
||||||
.split('/')
|
|
||||||
.filter((part) => part && part !== '.' && part !== '..')
|
|
||||||
.join('/');
|
|
||||||
return clean.slice(0, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferKind(mimeType = '', fileName = '') {
|
function inferKind(mimeType = '', fileName = '') {
|
||||||
const mime = mimeType.toLowerCase();
|
const mime = mimeType.toLowerCase();
|
||||||
const ext = path.extname(fileName).toLowerCase();
|
const ext = extnameAny(fileName);
|
||||||
if (mime.startsWith('image/')) return 'image';
|
if (mime.startsWith('image/')) return 'image';
|
||||||
if (mime.includes('zip') || ['.zip', '.intunewin', '.7z', '.rar'].includes(ext)) return 'archive';
|
if (mime.includes('zip') || ['.zip', '.intunewin', '.7z', '.rar'].includes(ext)) return 'archive';
|
||||||
if (['.msi', '.msp', '.exe'].includes(ext)) return 'installer';
|
if (['.msi', '.msp', '.exe'].includes(ext)) return 'installer';
|
||||||
@@ -162,8 +145,7 @@ export function getAssetFile(assetId, userId) {
|
|||||||
const row = selectAssetById(assetId, userId);
|
const row = selectAssetById(assetId, userId);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
const resolved = path.resolve(row.storage_path);
|
const resolved = path.resolve(row.storage_path);
|
||||||
const fileLockerRoot = path.resolve(config.fileLockerDir);
|
if (!isPathInside(config.fileLockerDir, resolved)) throw new Error('Asset file path is outside the configured FileLocker directory');
|
||||||
if (!resolved.startsWith(fileLockerRoot)) throw new Error('Asset file path is outside the configured FileLocker directory');
|
|
||||||
return { asset: camelAsset(row), filePath: resolved };
|
return { asset: camelAsset(row), filePath: resolved };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
import { nameWithoutExtensionAny, path } from '../utils/pathUtils.js';
|
||||||
|
|
||||||
// Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft
|
// Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft
|
||||||
// Win32 Content Prep Tool (IntuneWinAppUtil.exe) on Windows, or an optional
|
// Win32 Content Prep Tool (IntuneWinAppUtil.exe) on Windows, or an optional
|
||||||
@@ -45,7 +45,7 @@ export function buildPackagerInvocation({ toolPath, sourceFolder, setupFile, out
|
|||||||
|
|
||||||
// IntuneWinAppUtil names the output after the setup file's base name.
|
// IntuneWinAppUtil names the output after the setup file's base name.
|
||||||
export function expectedOutputFile(outputDir, setupFile) {
|
export function expectedOutputFile(outputDir, setupFile) {
|
||||||
return path.join(outputDir, `${path.parse(setupFile).name}.intunewin`);
|
return path.join(outputDir, `${nameWithoutExtensionAny(setupFile, 'package')}.intunewin`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildIntuneWin({ sourceFolder, setupFile, outputDir }) {
|
export function buildIntuneWin({ sourceFolder, setupFile, outputDir }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import { path } from '../utils/pathUtils.js';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { db, id, now } from '../db.js';
|
import { db, id, now } from '../db.js';
|
||||||
import { decryptSecret } from './cryptoStore.js';
|
import { decryptSecret } from './cryptoStore.js';
|
||||||
@@ -9,6 +8,7 @@ import { logger } from './logger.js';
|
|||||||
import { listExecutableAssets } from '../models/assetModel.js';
|
import { listExecutableAssets } from '../models/assetModel.js';
|
||||||
import { listExecutableCustomVariables } from '../models/variableModel.js';
|
import { listExecutableCustomVariables } from '../models/variableModel.js';
|
||||||
import { getBooleanSetting } from '../models/settingsModel.js';
|
import { getBooleanSetting } from '../models/settingsModel.js';
|
||||||
|
import { path } from '../utils/pathUtils.js';
|
||||||
|
|
||||||
// The execution kill-switch can be set at boot via ALLOW_SCRIPT_EXECUTION and
|
// The execution kill-switch can be set at boot via ALLOW_SCRIPT_EXECUTION and
|
||||||
// flipped at runtime via the Config screen (allow_script_execution). The DB
|
// flipped at runtime via the Config screen (allow_script_execution). The DB
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { load } from './setup.mjs';
|
import { load } from './setup.mjs';
|
||||||
|
|
||||||
const { buildPackagerInvocation, expectedOutputFile, resolveToolPath, builderAvailability } = await load('../services/intuneWinBuilder.js');
|
const { buildPackagerInvocation, expectedOutputFile, resolveToolPath, builderAvailability } = await load('../services/intuneWinBuilder.js');
|
||||||
|
const { isPathInside, normalizePackagePath, sanitizeFileName } = await load('../utils/pathUtils.js');
|
||||||
|
|
||||||
test('bundled invocation uses IntuneWinAppUtil flags', () => {
|
test('bundled invocation uses IntuneWinAppUtil flags', () => {
|
||||||
const inv = buildPackagerInvocation({ toolPath: 'C:/tools/IntuneWinAppUtil.exe', sourceFolder: 'C:/src', setupFile: 'setup.exe', outputDir: 'C:/out' });
|
const inv = buildPackagerInvocation({ toolPath: 'C:/tools/IntuneWinAppUtil.exe', sourceFolder: 'C:/src', setupFile: 'setup.exe', outputDir: 'C:/out' });
|
||||||
@@ -24,6 +25,18 @@ test('external command template substitutes placeholders', () => {
|
|||||||
test('expectedOutputFile names after the setup base name', () => {
|
test('expectedOutputFile names after the setup base name', () => {
|
||||||
assert.ok(expectedOutputFile('/out', 'ContosoVpnSetup.exe').endsWith('ContosoVpnSetup.intunewin'));
|
assert.ok(expectedOutputFile('/out', 'ContosoVpnSetup.exe').endsWith('ContosoVpnSetup.intunewin'));
|
||||||
assert.ok(expectedOutputFile('/out', 'app.msi').endsWith('app.intunewin'));
|
assert.ok(expectedOutputFile('/out', 'app.msi').endsWith('app.intunewin'));
|
||||||
|
assert.ok(expectedOutputFile('/out', 'Files\\Vendor Setup.exe').endsWith('Vendor Setup.intunewin'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path utilities normalize Windows-style upload and package paths', () => {
|
||||||
|
assert.equal(sanitizeFileName('C:\\temp\\Contoso VPN?.msi'), 'Contoso VPN-.msi');
|
||||||
|
assert.equal(normalizePackagePath('Files\\Installers\\..\\setup.exe'), 'Files/Installers/setup.exe');
|
||||||
|
assert.equal(normalizePackagePath('../SupportFiles/config.json'), 'SupportFiles/config.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path containment rejects sibling directories with the same prefix', () => {
|
||||||
|
assert.equal(isPathInside('/tmp/FileLocker', '/tmp/FileLocker/assets/a.bin'), true);
|
||||||
|
assert.equal(isPathInside('/tmp/FileLocker', '/tmp/FileLocker2/assets/a.bin'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolveToolPath defaults into the tools directory', () => {
|
test('resolveToolPath defaults into the tools directory', () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// loaded, then migrates + seeds a fresh schema. Import this first, then use
|
// loaded, then migrates + seeds a fresh schema. Import this first, then use
|
||||||
// `load()` to dynamically import the modules under test.
|
// `load()` to dynamically import the modules under test.
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
|
|||||||
40
server/utils/pathUtils.js
Normal file
40
server/utils/pathUtils.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export { path };
|
||||||
|
|
||||||
|
export function basenameAny(value, fallback = '') {
|
||||||
|
const normalized = String(value || fallback || '').replace(/\\/g, '/');
|
||||||
|
return path.posix.basename(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extnameAny(value) {
|
||||||
|
return path.extname(basenameAny(value)).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nameWithoutExtensionAny(value, fallback = '') {
|
||||||
|
return path.parse(basenameAny(value, fallback)).name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeFileName(name, fallback = 'asset.bin') {
|
||||||
|
return basenameAny(name, fallback)
|
||||||
|
.replace(/[/\\?%*:|"<>]/g, '-')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.slice(0, 180) || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePackagePath(value, fallbackName = '') {
|
||||||
|
const clean = String(value || fallbackName || '')
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.split('/')
|
||||||
|
.filter((part) => part && part !== '.' && part !== '..')
|
||||||
|
.join('/');
|
||||||
|
return clean.slice(0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPathInside(parentDir, candidatePath) {
|
||||||
|
const parent = path.resolve(parentDir);
|
||||||
|
const candidate = path.resolve(candidatePath);
|
||||||
|
const relative = path.relative(parent, candidate);
|
||||||
|
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user