Electron open file/directory in specific application - node.js

I'm building a sort of File explorer / Finder using Electron.
I want to open some file types with a specific application.
I've tried the approach from this answer:
Open external file with Electron
import { spawn } from 'child_process'
spawn('/path/to/app/superApp.app', ['/path/to/file'])
But when I do that, I get a EACCES error as follows.
Is this the right approach? If yes, how can I fix the issue? If not, what is the right approach?

You can open a file or folder through shell commands from the electron module. The commands work on both main and renderer process.
const {shell} = require('electron') // deconstructing assignment
shell.showItemInFolder('filepath') // Show the given file in a file manager. If possible, select the file.
shell.openPath('folderpath') // Open the given file in the desktop's default manner.
More info on https://github.com/electron/electron/blob/master/docs/api/shell.md

While the accepted answer does say how to open the folder in file explorer, it doesn't answer the question on how to open the folder WITH an external program like VSCode. It can be done like so:
import { spawn, SpawnOptions } from "child_process";
import { pathExists } from "fs-extra";
const editorPath = "C:/Program Files/Microsoft VS Code/Code.exe";
export const launchExternalEditor = async (
folderPath: string
): Promise<void> => {
const exists = await pathExists(editorPath);
if (!exists) {
console.error("editor not found");
}
const opts: SpawnOptions = {
// Make sure the editor processes are detached from the Desktop app.
// Otherwise, some editors (like Notepad++) will be killed when the
// Desktop app is closed.
detached: true,
};
spawn(editorPath, [folderPath], opts);
};
This will launch the folder in VSCode as it's root directory. Code is taken from this repository.
Note: this may require some tinkering depending on what program you are trying to use, but so far it worked properly with: VSCode, Visual Studio 2019, Intellij IDEA, NetBeans.

For display native system dialogs for opening and saving files, alerting, etc. you can use dialog module from electron package.
const electron = require('electron');
var filePath = __dirname;
console.log(electron.dialog.showOpenDialog)({
properties:['openFile'],
filters:[
{name:'Log', extentions:['csv', 'log']}
]
});
A very prompt explanation is provided at Electron Docs.

Related

Interacting with the FileSystem using TypeScript Path Aliases

I'm working in a rather large monorepo, so I set up some path aliases in my tsconfig.json:
"paths": {
"#app/specialLib/*": ["libs/specialLib/src/lib/*"],
"#app/specialLib": ["libs/specialLib/src/index.ts"],
}
so that I can simplify my import statements, both in my apps, and in other places inside the library:
import { Foo } from "#app/specialLib"
import { SubItem } from "#app/specialLib/nested/library"
In one of my library files, I need to work with the file system, using writeFileSync:
// the path to the log file
const itemLog = "#app/specialLib/item-log.json";
// remove the necessary item from the log
let loggedItems: LogItem[] = require(itemLog);
loggedItems = loggedItems.filter(({ id }) => {
id != this.id;
});
// write the log back to the file system
writeFileSync(resolve(itemLog), JSON.stringify(loggedItems));
Now the compiler can resolve the require just fine, but when I try to run my script, I get Error: ENOENT: no such file or directory, open 'C:\Users\Chris\development\Big.Monorepo\#app\specialLib\item-log.json', because of course, there is no such folder on my machine. I've tried just using the variable itself, as well as wrapping it inside of path.resolve(), with the same effects.
So my question is this: is there a way to read and write to the file system using Node's built-in functionality in conjunction with TypeScript path aliases?

How to read contents of a directory in linux on nodejs?

I am creating an application on nodejs that has to read contents of a folder in the install location. The installer creates the directory 'cert' at the install location.
My code is:
const dircert = './cert'
files = fs.readdirSync(dircert)
if (!files.length) {
***some code***
} else {
files.forEach(file => {
if (path.extname(file) == '.key') {
pathkey = path.resolve(dircert, file)
}
if (path.extname(file) == '.crt') {
pathcert = path.resolve(dircert, file)
}
})
This runs fine in windows but not in Linux. The installer is not able to read the contents. What do I need to change to ensure that it works in both? I am pretty new to linux.
The application is getting installed in ProgramFiles in windows but in /opt/AppName in linux. Please suggest.
Editing to add: So this cert folder is getting created under /opt/AppName/ in linux and under c:/ProgramFiles/company/App Name/ in Windows. While this code runs fine on windows, on linux this code tries to look for cert at root. How do I make sure it looks for the folder at the installed location which is /opt/AppName, and it should work on both the platforms.
It's unclear from your question how the files are being installed and how the code is being executed. Both of these matter in Linux. You can use the __dirname node variable with path.resolve() to get the directory of the current module. If cert is below the current module, you can use the following code to resolve the location:
const path = require('path');
const dircert = path.resolve(__dirname, 'cert');
If you're using ES Modules:
import path from 'path';
const dircert = path.resolve('cert').

nodejs writeFile only working with admin permssions

I currently learning electron and I would like to write a config file in the same dir than the exe file. This is my code:
var save = fs.readFileSync(__dirname+"/config.json")
save = {
storein : savegame
}
fs.writeFile("./config.json", JSON.stringify(save), function(error){
//other stuff
})
But if I run this without administrator permissions, its not working, only if I run the .exe as an admin. If I use electron app it works too

Creating a Visual Studio Code integrated terminal with a custom Node script as the shell

I'm working on a Visual Studio Code extension in which I hope to create a terminal which gives access to a custom shell. I have a Node.js script (.js file) which implements this shell. Now, I'm trying to use Code's createTerminal method from my extension to launch a terminal that uses my Node.js script as its shell.
I can't directly set the shellPath to be my js file, because I have no guarantee that the user has Node.js installed, that the system will run such files with Node.js, nor what version of Node is installed. I need to be able to point at a universally-understood binary which can handle the file. Roughly speaking, I want to do this:
let terminal = vscode.window.createTerminal({
name: "My terminal",
shellPath: 'node', // Use the Node.js executable as the target
shellArgs: [path.join(__dirname, 'my-shell-wrapper.js')] // Tell Node to run my shell
});
terminal.show();
How can I accomplish this? Is there an executable that ships with Code that I can point to which runs Node scripts? Or is there another mechanism which I'm missing?
VSCode now (as of the past year or so) has the option of specifying custom behaviors for terminals aside from just the underlying shell executable. createTerminal has an overload which takes ExtensionTerminalOptions; this has a pty property which allows you to specify custom handlers for reading from and writing to the terminal. From their Pseudoterminal documentation:
const writeEmitter = new vscode.EventEmitter<string>();
const pty: vscode.Pseudoterminal = {
onDidWrite: writeEmitter.event,
open: () => {},
close: () => {},
handleInput: data => writeEmitter.fire(data === '\r' ? '\r\n' : data)
};
vscode.window.createTerminal({ name: 'Local echo', pty });
This can be used to implement arbitrary terminal interactions.

How can I bundle a precompiled binary with electron

I am trying to include a precompiled binary with an electron app. I began with electron quick start app and modified my renderer.js file to include this code that is triggered when a file is dropped on the body:
spawn = require('child_process').spawn,
ffmpeg = spawn('node_modules/.bin/ffmpeg', ['-i', clips[0], '-an', '-q:v', '1', '-vcodec', 'libx264', '-y', '-pix_fmt', 'yuv420p', '-vf', 'setsar=1,scale=trunc(iw/2)*2:trunc(ih/2)*2,crop=in_w:in_h-50:0:50', '/tmp/out21321.mp4']);
ffmpeg.stdout.on('data', data => {
console.log(`stdout: ${data}`);
});
ffmpeg.stderr.on('data', data => {
console.log(`stderr: ${data}`);
});
I have placed my precompiled ffmpeg binary in node_modules/.bin/. Everything works great in the dev panel, but when I use electron-packager to set up the app, it throws a spawn error ENOENT to the console when triggered. I did find a very similar question on SO, but the question doesn't seem to be definitively answered. The npm page on electron-packager does show that they can be bundled, but I cannot find any documentation on how to do so.
The problem is that electron-builder or electron-packager will bundle your dependency into the asar file. It seems that if the dependency has a binary into node_modules/.bin it is smart enough to not package it.
This is the documentation for asar packaging for electron-builder on that topic. It says
Node modules, that must be unpacked, will be detected automatically
I understand that it is related to existing binaries in node_modules/.bin.
If the module you are using is not automatically unpacked you can disable asar archiving completely or explicitly tell electron-builder to not pack certain files. You do so in your package.json file like this:
"build": {
"asarUnpack": [
"**/app/node_modules/some-module/*"
],
For your particular case
I ran into the same issue with ffmpeg and this is what I've done:
Use ffmpeg-static. This package bundles statically compiled ffmpeg binaries for Windows, Mac and Linux. It also provides a way to get the full path of the binary for the OS you are running: require('ffmpeg-static').path
This will work fine in development, but we still need to troubleshoot the distribution problem.
Tell electron-builder to not pack the ffmpeg-static module:
"build": {
"asarUnpack": [
"**/app/node_modules/ffmpeg-static/*"
],
Now we need to slightly change the code to get the right path to ffmpeg with this code: require('ffmpeg-static').path.replace('app.asar', 'app.asar.unpacked') (if we are in development the replace() won't replace anything which is fine).
If you are using webpack (or other javascript bundler)
I ran into the issue that require('ffmpeg-static').path was returning a relative path in the renderer process. But the issue seemed to be that webpack changes the way the module is required and that prevents ffmpeg-static to provide a full path. In the Dev Tools the require('ffmpeg-static').path was working fine when run manually, but when doing the same in the bundled code I was always getting a relative path. So this is what I did.
In the main process add this before opening the BrowserWindow: global.ffmpegpath = require('ffmpeg-static').path.replace('app.asar', 'app.asar.unpacked'). The code that runs in the main process is not bundled by webpack so I always get a full path with this code.
In the renderer process pick the value this way: require('electron').remote.getGlobal('ffmpegpath')
I know I'm a bit late but just wanted to mention ffbinaries npm package I created a while ago exactly for this purpose.
It'll allow you to download ffmpeg/ffplay/ffserver/ffprobe binaries to specified location either during application boot (so you don't need to bundle it with your application) or in a CI setup. It can autodetect platform, you can also specify it manually.
If anyone happens to need an answer to this question: I do have a solution to this, but I have no idea if this is considered best practice. I couldn't find any good documentation for including 3rd party precompiled binaries, so I just fiddled with it until it finally worked. Here's what I did (starting with the electron quick start, node.js v6):
From the app directory I ran the following commands to include the ffmpeg binary as a module:
mkdir node_modules/ffmpeg
cp /usr/local/bin/ffmpeg node_modules/ffmpeg/
ln -s ../ffmpeg/ffmpeg node_modules/.bin/ffmpeg
(replace /usr/local/bin/ffmpeg with your current binary path, download it from here) Placing the link allowed electron-packager to include the binary I saved to node_modules/ffmpeg/.
Then to get the bundled app path I installed the npm package app-root-dir by running the following command:
npm i -S app-root-dir
Since I could then get the app path, I just appended the subfolder for my binary and spawned from there. This is the code that I placed in renderer.js:.
var appRootDir = require('app-root-dir').get();
var ffmpegpath=appRootDir+'/node_modules/ffmpeg/ffmpeg';
console.log(ffmpegpath);
const
spawn = require( 'child_process' ).spawn,
ffmpeg = spawn( ffmpegpath, ['-i',clips_input[0]]); //add whatever switches you need here
ffmpeg.stdout.on( 'data', data => {
console.log( `stdout: ${data}` );
});
ffmpeg.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
});
This is how I would do it:
Taking cues from tsuriga's answer, here is my code:
Note: replace or add OS path accordingly.
Create a directory ./resources/mac/bin
Place you binaries inside this folder
Create file ./app/binaries.js and paste the following code:
'use strict';
import path from 'path';
import { remote } from 'electron';
import getPlatform from './get-platform';
const IS_PROD = process.env.NODE_ENV === 'production';
const root = process.cwd();
const { isPackaged, getAppPath } = remote.app;
const binariesPath =
IS_PROD && isPackaged
? path.join(path.dirname(getAppPath()), '..', './Resources', './bin')
: path.join(root, './resources', getPlatform(), './bin');
export const execPath = path.resolve(path.join(binariesPath, './exec-file-name'));
Create file ./app/get-platform.js and paste the following code:
'use strict';
import { platform } from 'os';
export default () => {
switch (platform()) {
case 'aix':
case 'freebsd':
case 'linux':
case 'openbsd':
case 'android':
return 'linux';
case 'darwin':
case 'sunos':
return 'mac';
case 'win32':
return 'win';
}
};
Add the following code inside the ./package.json file:
"build": {
....
"extraFiles": [
{
"from": "resources/mac/bin",
"to": "Resources/bin",
"filter": [
"**/*"
]
}
],
....
},
import binary file path as:
import { execPath } from './binaries';
#your program code:
var command = spawn(execPath, arg, {});
Why this is better?
Most of the answers require an additional package called app-root-dir
The original answer doesn't handle the (env=production) build or the pre-packed versions properly. He/she has only taken care of development and post-packaged versions.

Resources