📁

Electronでファイル入出力【Electron × Next.js】

2021/11/25に公開

概要

  • Electronはブラウザ標準のinput file / APIでdownloadが使えないので、ファイル入出力に関してはElectronの機能と連携する必要がある
  • アプリであることから、ファイルデータ自体を送信する必要はないため、ファイル入力は『ファイルを選択してパスを送るだけ』にしている。バックエンドは現状はAPI側は普通にファイル送信を実施してるが、他の方法(ファイルのコピー)もある気がする。

前提

  • 環境はフロントエンドNext.jsのバックエンドPythonのElectron。環境構築は下記2つを組み合わせて作っています。

https://zenn.dev/erukiti/articles/933fc127f751aef45b4f
https://techacademy.jp/magazine/50133

  • ファイル構成はこんな感じです
- backend (Python)
 - index.py
- electron-src (Electron)
  - index.ts
- renderer (Next.js)
  - pages
     - index.ts
  • 実際に使ったリポジトリはこちら

https://github.com/thetalemon/movie2music/tree/develop

ファイル選択・送信

  • ブラウザの<input type="file"> は使えないので、electronのdialog.showOpenDialogをNext.jsからIPC通信で呼び出す。
  • IPC通信でファイルパスを受け取り、API通信でファイルパスを送る
renderer/pages/index.ts
import Layout from '../components/Layout'

const IndexPage = () => {
  const [filePath, setFilePath] = useState('')

  const onClickSetFile = async () => {
    setFilePath(await global.ipcRenderer.invoke('getFilePath'))
  }
  
  const onClickDoProcess = async () => {
    let formData = new FormData();
    formData.append("file", filePath);
    apiClient
      .post("/processDo", formData)
  }

  return (
    <Layout title="Electron File Select Sample">
      <button onClick={onClickSetFile}>set file</button>
      <button onClick={onClickDoProcess}>do processing</button>
      <p>
        {filePath}
      </p>
    </Layout>
  )
}
electron-src/index.ts
import {ipcMain, dialog } from 'electron';

ipcMain.handle('getFilePath', async (_event: IpcMainInvokeEvent) => {
  const fileName = await dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    title: 'Select a text file',
    defaultPath: '.',
  });

  return fileName.filePaths[0];
});

ダウンロード

  • ブラウザの『特定のURLにアクセスするとダウンロードが開始される』が使えないので、electron-dlを利用。これをIPC通信で呼び出し、その中でAPIを呼び出す。
renderer/pages/index.ts
import Layout from '../components/Layout'

const IndexPage = () => {
  const onClickGetFile = async () => {
    global.ipcRenderer.send('download', 'sample.txt')
  }

  return (
    <Layout title="Electron File Select Sample">
      <button onClick={onClickGetFile}>get file</button>
    </Layout>
  )
}
electron-src/index.ts
import { download } from 'electron-dl';

ipcMain.on('download', async (_event: IpcMainEvent, filename: string) => {
  await download(
    mainWindow,
    `http://127.0.0.1:5000/process/download?ID=${filename}`,
    {
      directory: app.getPath('desktop'),
      filename: filename,
      saveAs: true,
    }
  );
});

おわり

  • ブラウザを離れてわかるありがたさ
  • IPC通信はローカルアプリで利用されるプロセス間の通信のことらしい。今回はElectronがメインプロセス、Next.jsはレンダラープロセスになっていて、レンダラープロセスからメインプロセスを呼び出してる感じ。
    • ブラウザでも内部的には子プロセスからメインプロセスを呼び出してファイルを選ぶやつを出現させたりしているんじゃないかなという予想。ブラウザにおける『OS依存』ってそういうところからできそう。

Discussion