ブラウザからローカルファイルをオープンしたい
以前に、Node.JSでローカルサーバーを立てて、WebアプリからIMEのコントロールをしました。
今回はIMEの制御とおなじようにWebアプリの課題のひとつである、ローカルファイルやフォルダのオープンを実現したいと思います。
今回も前回同様に、ローカルにアプリのインストールが必要です。前回はをの環境をNode.jsを使って手動で構築しましたが、今回はPC制御用のWebサーバーをElectronでアプリ化しします。アプリの形でを配布できれば、設定作業自体が楽になるだけでなく、Webからダウンロードを通じてエンドユーザーに環境を構築してもらうことも可能で、可用性が上がります。
環境はWindows10を想定していますが、今回利用するElectronはマルチプラットフォームに対応しているので、利用するコマンド等を変えればWindows以外の環境でも実現可能だと思います。
ちなみに、ユーザーの利用環境をGoogle Chromeに絞れるなら、同様の操作ができるプラグインが存在するようなので、そちらを使った方が早いかもしれません。
環境構築
Node.JS環境は構築済みの前提です。まだの場合はリンク先の記事を参照してください。
Electron用のフォルダを作成し、その中にプロジェクト用のフォルダを作成します。筆者の環境ではルートドライブに直接プロジェクトを作成したところ、アプリの生成時に失敗しました。
ここではd:¥electron¥openffという名前でフォルダを作成し、初期化します。Nodeプロジェクトのイニシャルでは、entory pointの部分だけ「main.js」とします。もしデフォルトのまま進めてしまったら、package.jsonファイルの該当箇所を修正します。
あと、author(筆者)とdescription(詳細)はなんでもいいですが、何か記入しておきます。
PS> mkdir d:¥electron¥openff PS> cd d:¥electron¥openff PS> npm init package name: (openff) version: (1.0.0) description: open folder or file from browser request. entry point: (index.js) main.js test command: git repository: keywords: author: nanbu license: (ISC) About to write to D:\electron\openff\package.json: { "name": "openff", "version": "1.0.0", "description": "open folder or file from browser request.", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "nanbu", "license": "ISC" } Is this OK? (yes) ...
Electronを開発領域にインストールします。
PS > npm install --save-dev electron ...
Electronの実行を簡単にするためにpackage.jsonのscriptsのキーに次のエントリーを記述します。
package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ."
}
...
こうすることで、「npm start」とすることでElectronのコードを実行できます。これは、electronコマンドにカレントフォルダ引数に渡して実行するものなので、「npx electron .」とすることでも代替できます。
コード生成
main.jsを次のように作成しました。通常はhtmlファイルを使ってアプリのWindowを作成しますが、今回のプログラムはバックグラウンドで待機するだけなのでWindowは作成しません。
ファイルとフォルダのオープンはWindowsのコマンドプロンプト経由で実現しています。フォルダオープンはexplorer.exeにの引数としてフォルダパスを渡します。ファイルのオープンでは、assocコマンドを使って拡張子からファイルタイプを取得したのち、ftypeコマンドでファイルタイプを指定してOS内のデフォルトのプログラムのパスを取得しています。たいていのプログラムは引数にファイルパスを指定するとそのファイルをオープンしてくれるので、それを前提としてファイルパスをプログラムに渡します。
main.js
// Electron
const electron = require("electron");
// 他ライブラリ
const http = require("http");
const url = require("url");
const path = require("path");
const fs = require("fs");
// ローカルWebサーバーからOSコマンドを実行
const { execSync } = require("child_process");
// Electronのアプリ
const app = electron.app;
// アプリのredyイベント検知でサーバーを起動
// Windowを生成する場合もready以後で行います
app.whenReady().then(function () {
http
.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
//アクセスしてきたURLを取得
var objUrl = url.parse(req.url, true);
var strFilePath = null;
var strCmd = null;
// GETパラメータにファイルまたはフォルダのパスをセットする
if (objUrl.query.path) {
strFilePath = decodeURIComponent(objUrl.query["path"]);
}
switch (objUrl.pathname) {
case "/file":
//ファイルオープン(処理が煩雑なのでサブメソッドへ)
if (strFilePath) {
openFile(strFilePath);
}
break;
case "/folder":
//フォルダオープン
if (strFilePath) {
strCmd = 'start explorer.exe "' + strFilePath + '" ';
console.log(strCmd);
execSync(strCmd);
}
break;
case "/end":
//アプリ終了
console.log("server end");
app.quit();
break;
default:
}
res.writeHead(200, { "Content-Type": "text/plain" });
res.end();
})
.listen(8080, () => console.log("server start"));
});
function openFile(strFilePath) {
// ファイルオープン処理
// strFilePathの文字コードはUTF-8ですがそのままexeSyncに渡すことができます
// (console.logでは文字化けします)
var strExt = path.extname(strFilePath); //拡張子取得
var stdout;
var lines;
var strFound;
var strFileType;
var strAppPathBase;
var strAppPathConv;
var intFound;
// コマンドプロンプトからファイルタイプを取得します
// .txt=txtfile という形で結果が返ります
stdout = execSync("cmd /c assoc " + strExt);
lines = stdout.toString().split("\r\n");
strFound = lines.find(function (value) {
if (value.startsWith(strExt)) {
return true;
} else {
return false;
}
});
if (strFound) {
//ファイルタイプを返す
strFileType = strFound.substring(strExt.length + 1);
} else {
//オープンするアプリを見つけられないので親フォルダをオープンする
openParentFolder(strFilePath);
console.log("not found assoc");
return;
}
// ファイルタイプから、関連付けられたアプリを取得します
// txtfile=%SystemRoot%\system32\NOTEPAD.EXE %1 のような戻り値になります
// パスに空白が含まれる場合は"が存在することもあるようです
stdout = execSync("cmd /c ftype " + strFileType);
lines = stdout.toString().split("\r\n");
strFound = lines.find(function (value) {
if (value.startsWith(strFileType)) {
return true;
} else {
return false;
}
});
if (strFound) {
strAppPathBase = strFound.substring(strFileType.length + 1);
} else {
//オープンするアプリを見つけられないので親フォルダをオープンする
openParentFolder(strFilePath);
console.log("not found ftype");
return;
}
// 結果が"で区切られている場合とそうでない場合がある
// またパラメータ%1の記述があったりするので除去
if (strAppPathBase.startsWith('"')) {
// "がある場合は次の"まで
intFound = strAppPathBase.indexOf('"', 1);
if (3 < intFound) {
strAppPathConv = strAppPathBase.substring(1, intFound);
}
} else {
// "がない場合は次空白まで
intFound = strAppPathBase.indexOf(" ");
if (2 < intFound) {
strAppPathConv = strAppPathBase.substring(0, intFound);
} else {
strAppPathConv = strAppPathBase.trim();
}
}
if (strAppPathConv) {
// 関連付けられたアプリでオープン
if (fs.existsSync(strFilePath)) {
// 関連付けられたアプリでオープン
var strCmd = 'start "' + strAppPathConv + '" "' + strFilePath + '"';
console.log(strCmd);
execSync(strCmd);
} else {
// ファイルが見つからないので親フォルダをオープン
openParentFolder(strFilePath);
console.log("not found file");
}
} else {
openParentFolder(strFilePath);
console.log("not found app");
}
}
function openParentFolder(str) {
//親フォルダをオープン
var strCmd = 'start explorer.exe "' + path.dirname(str) + '"';
console.log(strCmd);
execSync(strCmd);
}
初回のコードを実行時にはWindowsのファイアウォールが反応すると思いますが、許可します。
アプリ化
アプリ化(パッケージ)するには、Electron-forgeを利用します。他にはelectron-packagerというツールもあるようです。
インストール後、セットアップの為にElectron-forgeから直接importコマンドを実行します。
PS> npm install --save-dev @electron-forge/cli ... PS> npx electron-forge import ... PS> npm run make
セットアップにより、「npm run make」が利用できるようになっています。実行するとプロジェクトに「out」フォルダが生成されて、その中に実行ファイルを含んだフォルダが出力されます。
デフォルトでは実行環境と同じアーキテクチャのバイナリが作成されます。今回試してみたところ、outフォルダ以下は次のようなフォルダ構成でした。
- make¥squirrel.windos¥x64フォルダ
この中には、プロジェクト名-1.0.0 Setup.exeと他いくつかのファイルが入っていました。Setup.exeは名前の通りインストーラーです。実行するとプログラムの追加と削除の一覧に該当のアプリが載るようになります。
この時、インストールされるファイル群は筆者の環境だと「c:¥ユーザー¥[ユーザー名]¥AppData¥Local¥[プロジェクト名]」フォルダに配置されました。スタートメニューへの追加はされないようなので、自分でここから実行する必要があります。
- プロジェクト名フォルダ
この中には様々なファイルフォルダが含まれます。そのうちのプロジェクト名.exeを実行するとアプリが動きます。このフォルダを丸ごと動かせば、アプリが他の同様の環境をもったPCで動かせます。
アーキテクチャを変えたい場合は「npx electron-forge make ...」として自分で、パラメータを付与するか、package.jsonのmakeのエントリーに付け加えれば可能です。
-aオプションでアーキテクチャ、-pオプションでプラットフォームの指定ができます。
「npx electron-forge make -h」とすることで、確認ができますが、アーキテクチャで指定できる値は「ia32, x64, armv7l, arm64, mips64el, universal」、プラットフォームで指定できる値は「darwin, mas, win32,linux」となっているようです。
ブラウザ側からの利用方法
先ほどアプリ化したプログラムをスタートアップに登録するなどして利用する際は立ち上げた状態を作っておきます。そして、ブラウザ側からはAJAXでlocalhostの指定のアドレスにオープンしたいファイルやフォルダ名をパラメータをとしてアクセスします。AJAXのいろいろな方法につきましては別途記事もございますので、合わせてお読みいただけると幸いです。
先のコードの仕様では、http://localhost:8080/file?path=...とアクセスした際はファイルオープンモードで、パラメータに指定したファイルの拡張子から、OSに結びつけられたアプリケーションを取得し、取得できたらそのアプリでオープン、できなかったら親フォルダをオープンします。またhttp://localhost:8080/folder?path=...とした時は、そのまま指定したフォルダをオープンします。
sample.html
...
<script>
function openFile(strPath) {
fetch("http://localhost:8080/file?path="+encodeURIComponent(strPath)).then((res) => {
console.log(res.status);
});
}
</script>
...
この頃のGoogle Chromeではブラウザの仕様としてローカルホストにAJAXでアクセスする際には元のページがhttpsでないといけないようです。
また、サンプルコード上ではエラー処理やセキュリティ要件は省略しています。環境によっては、.exeや.batなどの危険なファイルを指定された場合は処理を中断する等の対策も必要になってくると思います。
electron-packager
electron-packagerを用いる方法も書いておきます。
インストールは通常通り行います。開発環境へ保存します。
npxより実行します。AppNameにアプリの名称、pratformにターゲットプラットフォーム、archにアーキテクチャを指定します。
アイコンを変更したい場合は、iconオプションの後に指定します。指定しない場合はElectronのデフォルトアイコンとなります。
こうするととでプロジェクトフォルダに[アプリ名]-[プラットフォーム]-[アーキテクチャ]という名前のフォルダが生成され、その中にプログラム群が入ります。
プロジェクトで利用している設定ファイル等は、先のフォルダの中のresources\appに入りますが、ここに入れたくない場合は、ignoreオプションを使います。
ignoreオプションは=の後、除外したいファイルかフォルダを指定します。複数ある場合は複数回ignoreオプションを指定します。また、=の後//で囲んで正規表現で対象を指定することもできます。