|||||||||||||||||||||

なんぶ電子

- 更新: 

ブラウザからローカルファイルをオープンしたい

Electron

以前に、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内のデフォルトのプログラムのパスを取得しています。たいていのプログラムは引数にファイルパスを指定するとそのファイルをオープンしてくれるので、それを前提としてファイルパスをプログラムに渡します。

githubでもソースを公開しています。

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と、「out¥プロジェクト名-win32-64」フォルダにプロジェクト名.exeファイルができました。プロジェクト名-1.0.0 Setup.exeは、インストーラーで実行すると起動するとともにプログラムの追加と削除の一覧に載るようになります。プロジェクト名.exeの方は通常の実行ファイルとなります。

アーキテクチャを変えたい場合は「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などの危険なファイルを指定された場合は処理を中断する等の対策も必要になってくると思います。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告