@open-audio-stack/core
Version:
Open-source audio plugin management software
371 lines (370 loc) • 12.8 kB
JavaScript
import AdmZip from 'adm-zip';
import { execFileSync, execSync, spawn } from 'child_process';
import { createReadStream, chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'fs';
import { createHash } from 'crypto';
import { unpack } from '7zip-min';
import stream from 'stream/promises';
import { globSync } from 'glob';
import { moveSync } from 'fs-extra/esm';
import os from 'os';
import * as tar from 'tar';
import path, { dirname } from 'path';
import yaml from 'js-yaml';
import { ZodIssueCode } from 'zod';
import { SystemType } from '../types/SystemType.js';
import { fileURLToPath } from 'url';
import sudoPrompt from '@vscode/sudo-prompt';
import { getSystem } from './utilsLocal.js';
import { log } from './utils.js';
export async function archiveExtract(filePath, dirPath) {
log('⎋', dirPath);
const ext = path.extname(filePath).trim().toLowerCase();
if (ext === '.zip') {
const zip = new AdmZip(filePath);
return zip.extractAllTo(dirPath);
}
else if (ext === '.tar' || ext === '.gz' || ext === '.tgz') {
return await tar.extract({
file: filePath,
cwd: dirPath,
});
}
else if (ext === '.7z') {
return unpack(filePath, dirPath, err => {
if (err)
throw new Error(`7z extraction failed: ${err.message}`);
});
}
}
export function dirApp(dirName = 'open-audio-stack') {
if (getSystem() === SystemType.Win)
return process.env.APPDATA || path.join(os.homedir(), dirName);
else if (getSystem() === SystemType.Mac)
return path.join(os.homedir(), 'Library', 'Preferences', dirName);
return path.join(os.homedir(), '.local', 'share', dirName);
}
export function dirContains(parentDir, childDir) {
return path.normalize(childDir).startsWith(path.normalize(parentDir));
}
export function dirCreate(dir) {
if (!dirExists(dir)) {
log('+', dir);
mkdirSync(dir, { recursive: true });
return dir;
}
return false;
}
export function dirDelete(dir) {
if (dirExists(dir)) {
log('-', dir);
return rmSync(dir, { recursive: true });
}
return false;
}
export function dirEmpty(dir) {
const files = readdirSync(dir);
return files.length === 0 || (files.length === 1 && files[0] === '.DS_Store');
}
export function dirExists(dir) {
return existsSync(dir);
}
export function dirIs(dir) {
return statSync(dir).isDirectory();
}
export function dirMove(dir, dirNew) {
if (dirExists(dir)) {
log('-', dir);
log('+', dirNew);
return moveSync(dir, dirNew, { overwrite: true });
}
return false;
}
export function dirOpen(dir) {
let command = '';
if (process.env.CI)
return Buffer.from('');
if (getSystem() === SystemType.Win)
command = 'start ""';
else if (getSystem() === SystemType.Mac)
command = 'open';
else
command = 'xdg-open';
log('⎋', `${command} "${dir}"`);
return execSync(`${command} "${dir}"`);
}
export function dirPackage(pkg) {
const parts = pkg.slug.split('/');
parts.push(pkg.version);
return path.join(...parts);
}
export function dirPlugins() {
if (getSystem() === SystemType.Win)
return path.join('Program Files', 'Common Files');
else if (getSystem() === SystemType.Mac)
return path.join(os.homedir(), 'Library', 'Audio', 'Plug-ins');
return path.join('usr', 'local', 'lib');
}
export function dirPresets() {
if (getSystem() === SystemType.Win)
return path.join(os.homedir(), 'Documents', 'VST3 Presets');
else if (getSystem() === SystemType.Mac)
return path.join(os.homedir(), 'Library', 'Audio', 'Presets');
return path.join(os.homedir(), '.vst3', 'presets');
}
export function dirProjects() {
// Windows throws permissions errors if you scan hidden folders
// Therefore set to a more specific path than Documents
if (getSystem() === SystemType.Win)
return path.join(os.homedir(), 'Documents', 'Audio');
else if (getSystem() === SystemType.Mac)
return path.join(os.homedir(), 'Documents', 'Audio');
return path.join(os.homedir(), 'Documents', 'Audio');
}
export function dirRead(dir, options) {
log('⌕', dir);
// Glob now expects forward slashes on Windows
// Convert backslashes from path.join() to forwardslashes
if (getSystem() === SystemType.Win) {
dir = dir.replace(/\\/g, '/');
}
// Ignore Mac files in Contents folders
// Filter out any paths not starting with the base directory
// This is to prevent issues with symlinks.
const baseDir = dir.includes('*') ? dir.split('*')[0] : dir;
const allPaths = globSync(dir, {
ignore: [`${baseDir}/**/*.{app,component,lv2,vst,vst3}/**/*`],
realpath: true,
...options,
});
// Glob input paths use forward slashes.
// Glob output paths are system-specific.
const baseDirCrossPlatform = baseDir.split('/').join(path.sep);
return allPaths.filter(p => p.startsWith(baseDirCrossPlatform));
}
export function dirRename(dir, dirNew) {
if (dirExists(dir)) {
return moveSync(dir, dirNew, { overwrite: true });
}
return false;
}
export function fileCreate(filePath, data) {
log('+', filePath);
return writeFileSync(filePath, data);
}
export function fileCreateJson(filePath, data) {
return fileCreate(filePath, JSON.stringify(data, null, 2));
}
export function fileCreateYaml(filePath, data) {
return fileCreate(filePath, yaml.dump(data));
}
export function fileDate(filePath) {
return statSync(filePath).mtime;
}
export function fileDelete(filePath) {
if (fileExists(filePath)) {
log('-', filePath);
return unlinkSync(filePath);
}
return false;
}
export function fileExec(filePath) {
return chmodSync(filePath, '755');
}
export function fileExists(filePath) {
return existsSync(filePath);
}
export async function fileHash(filePath, algorithm = 'sha256') {
log('⎋', filePath);
const input = createReadStream(filePath);
const hash = createHash(algorithm);
await stream.pipeline(input, hash);
return hash.digest('hex');
}
export function fileInstall(filePath) {
if (process.env.CI)
return Buffer.from('');
const ext = path.extname(filePath).toLowerCase();
let command = null;
switch (ext) {
case '.dmg':
command = `hdiutil attach -nobrowse "${filePath}" && sudo installer -pkg "$(find /Volumes -name '*.pkg' -maxdepth 2 | head -n 1)" -target / && hdiutil detach "$(dirname "$(find /Volumes -name '*.pkg' -maxdepth 2 | head -n 1)")"`;
break;
case '.pkg':
command = `sudo installer -pkg "${filePath}" -target /`;
break;
case '.deb':
command = `sudo dpkg -i "${filePath}" || sudo apt-get install -f -y`;
break;
case '.rpm':
command = `sudo rpm -i --nodigest --nofiledigest --nosignature --force "${filePath}" || sudo dnf install -y "${filePath}" || sudo yum install -y "${filePath}"`;
break;
case '.exe':
command = `start /wait "" "${filePath}" /quiet /norestart`;
break;
case '.msi':
command = `msiexec /i "${filePath}" /quiet /norestart`;
break;
default:
throw new Error(`Unsupported file format: ${ext}`);
}
log('⎋', command);
return execSync(command, { stdio: 'inherit' });
}
export function fileMove(filePath, newPath) {
if (fileExists(filePath)) {
log('-', filePath);
log('+', newPath);
return moveSync(filePath, newPath, { overwrite: true });
}
return false;
}
export function filesMove(dirSource, dirTarget, dirSub, formatDir) {
// Read files from source directory, ignoring Mac Contents files.
const files = dirRead(`${dirSource}/**/*.*`);
const filesMoved = [];
// For each file, move to correct folder based on type
files.forEach((fileSource) => {
const fileExt = path.extname(fileSource).slice(1).toLowerCase();
const fileExtTarget = formatDir[fileExt];
// If this is not a supported file format, then ignore.
if (fileExtTarget === undefined)
return log(`${fileSource} - ${fileExt} not mapped to a installation folder, skipping.`);
const fileTarget = path.join(dirTarget, fileExtTarget, dirSub, path.basename(fileSource));
if (fileExists(fileTarget))
return log(`${fileSource} - ${fileTarget} already exists, skipping.`);
dirCreate(path.dirname(fileTarget));
fileMove(fileSource, fileTarget);
filesMoved.push(fileTarget);
});
return filesMoved;
}
export function fileOpen(filePath) {
let command = '';
if (process.env.CI)
return Buffer.from('');
if (getSystem() === SystemType.Win)
command = 'start ""';
else if (getSystem() === SystemType.Mac)
command = 'open';
else
command = 'xdg-open';
log('⎋', `${command} "${filePath}"`);
return execSync(`${command} "${filePath}"`);
}
export function fileRead(filePath) {
log('⎋', filePath);
return readFileSync(filePath, 'utf8');
}
export function fileReadJson(filePath) {
if (fileExists(filePath)) {
log('⎋', filePath);
return JSON.parse(readFileSync(filePath, 'utf8').toString());
}
return false;
}
export function fileReadString(filePath) {
log('⎋', filePath);
return readFileSync(filePath, 'utf8').toString();
}
export function fileReadYaml(filePath) {
const file = fileReadString(filePath);
return yaml.load(file);
}
export function fileSize(filePath) {
return statSync(filePath).size;
}
export function isAdmin() {
if (process.platform === 'win32') {
try {
execFileSync('net', ['session'], { stdio: 'ignore' });
return true;
}
catch {
return false;
}
}
else {
return process && process.getuid ? process.getuid() === 0 : false;
}
}
export async function fileValidateMetadata(filePath, fileMetadata) {
const errors = [];
const hash = await fileHash(filePath);
if (fileMetadata.sha256 !== hash) {
errors.push({
code: ZodIssueCode.invalid_type,
expected: fileMetadata.sha256,
message: 'Required',
path: ['sha256'],
received: hash,
});
}
if (fileMetadata.size !== fileSize(filePath)) {
errors.push({
code: ZodIssueCode.invalid_type,
expected: String(fileMetadata.size),
message: 'Required',
path: ['size'],
received: String(fileSize(filePath)),
});
}
return errors;
}
export function getPlatform() {
if (getSystem() === SystemType.Win)
return SystemType.Win;
else if (getSystem() === SystemType.Mac)
return SystemType.Mac;
return SystemType.Linux;
}
export function runCliAsAdmin(args) {
return new Promise((resolve, reject) => {
const filename = fileURLToPath(import.meta.url).replace('src/', 'build/');
const dirPathClean = dirname(filename).replace('app.asar', 'app.asar.unpacked');
const script = path.join(dirPathClean, 'admin.js');
// Temp file for logging
const logFile = path.join(dirPathClean, 'admin.log');
writeFileSync(logFile, ''); // Clear previous logs
log(`Running as admin: node "${script}" ${args}`);
// Tail the log file in real-time
const tail = spawn('tail', ['-f', logFile]);
const grep = spawn('grep', ['.']);
tail.stdout.pipe(grep.stdin);
grep.stdout.on('data', data => {
log(data.toString());
});
// Run the script with sudoPrompt
sudoPrompt.exec(`node "${script}" ${args} >> "${logFile}" 2>&1`, { name: 'Open Audio Stack' }, error => {
tail.kill();
if (error) {
reject(error);
}
else {
resolve();
}
});
});
}
export function zipCreate(filesPath, zipPath) {
if (fileExists(zipPath)) {
unlinkSync(zipPath);
}
const zip = new AdmZip();
const pathList = dirRead(filesPath);
pathList.forEach(pathItem => {
log('⎋', pathItem);
try {
if (dirIs(pathItem)) {
zip.addLocalFolder(pathItem, path.basename(pathItem));
}
else {
zip.addLocalFile(pathItem);
}
}
catch (error) {
log(error);
}
});
log('+', zipPath);
return zip.writeZip(zipPath);
}