ElectronでNFSマウント操作UIを作ってみた

# 背景・目的
本ページは、ElectronでNFSマウント操作UIを作成したときのメモになります。
ローカルの仮想環境（UTM + Ubuntu Desktop）を使って、仕組みや動作の検証を進めています。

# 実践
## 開発環境を用意する
1. Ubuntuの[ページ](https://ubuntu.com/download/server)を開きます
1. 「Download」ボタンをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3f8e0462-be91-4617-963c-70403a426d89.png)


1. 前回、[インストール](https://qiita.com/zumax/items/8090f59fc2b779af55be)したUTMを起動します
1. 「新規仮想マシンを作成」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/9a7d7705-8a98-45b4-a70f-929684fbea17.png)

1. 「仮想化」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/66df9d9e-b9c8-4e4b-9d81-51aaf2397706.png)

1. 「Linux」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/ed72bb90-ae61-4a66-8677-e2eabc97d0b8.png)

1. 「選択」をクリックし、ダウンロードした「ubuntu-24.04.2-live-server-amd64.iso」を選択します   
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6d43ca61-b76e-448a-b295-f95a4957ed4a.png)

1. 「続ける」をクリックします
1. 下記を指定して「続ける」をクリックします
    - メモリ:4096MB
    - CPUコア数:2
1. ストレージは、「64GiB」を指定して、「続ける」をクリックします
1. 共有ディレクトリを指定して、「続ける」をクリックします
1. 問題なければ、「保存」をクリックします
1. 仮想マシンが表示されました。「再生」ボタンをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/73f211a4-bc4f-43ab-b1c0-d86810e2939f.png)
1. 「Try or Install Ubuntu Server」 を選びます

1. SSHサーバをインストールします

1. セットアップが進んでいき、「Reboot」します
1. 「[FAILED] Failed unmounting cdrom.mount」が表示されます
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/04a51cd1-72d7-4ce9-81b9-90ea208a2f09.png)
1. CD/DVDの消去を選択します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/26bbf986-a67c-44f4-b692-39f1a0fa0a2f.png)

1. ターミナルでEnterをクリックします
1. しばらくするとログイン画面が表示されるので、設定したusernameとpasswordを設定します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c5101e97-139a-4031-8b9c-fece6f799a96.png)


## NFSサーバのインストール
1. IPアドレスを確認します
    ```bash
    ip a
    ```

1. ローカルPCからSSHします
    ```bash
     ssh <Username>@<UbuntuサーバのIP> -i ~/.ssh/秘密鍵
    ```
1. NFSサーバのインストール
    ```bash
    sudo apt update
    sudo apt install nfs-kernel-server
    ```
1. 共有ディレクトリを作成します
    ```bash
    ~$ ls -l /mnt
    total 0
    ~$ sudo mkdir -p /mnt/share
    ~$ sudo chown nobody:nogroup /mnt/share/
    ~$ ls -l /mnt
    total 4
    drwxr-xr-x 2 nobody nogroup 4096 Apr 20 10:33 share
    ~$ 
    ```
1. ファイルを置きます
    ```bash
    ~$ echo "hello_nfs" | sudo tee /mnt/share/hello.txt
    hello_nfs
    ~$ 
    ```



1. /etc/exports にエクスポート設定を追記します
    ```bash
    ~$ echo "/mnt/share 127.0.0.1(rw,sync,no_subtree_check)" | sudo tee -a /etc/exports
    /mnt/share 127.0.0.1(rw,sync,no_subtree_check)
    ~$ cat /etc/exports 
    # /etc/exports: the access control list for filesystems which may be exported
    #		to NFS clients.  See exports(5).
    #
    # Example for NFSv2 and NFSv3:
    # /srv/homes       hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
    #
    # Example for NFSv4:
    # /srv/nfs4        gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
    # /srv/nfs4/homes  gss/krb5i(rw,sync,no_subtree_check)
    #
    /mnt/share 127.0.0.1(rw,sync,no_subtree_check)
    ~$ 
    ```

1. サーバ再起動します
    ```bash
    ~$ sudo exportfs -a
    ~$ sudo systemctl restart nfs-kernel-server
    $ 
    ```

1. マウントをします
    ```bash
    ~$ sudo mkdir -p /mnt/test_mount
    ~$ sudo mount -t nfs 127.0.0.1:/mnt/share /mnt/test_mount
    ~$ 
    ```

1. 見えました
    ```bash
    ~$ ls -l /mnt/test_mount/
    total 4
    -rw-r--r-- 1 root root 10 Apr 20 10:46 hello.txt
    ~$ cat /mnt/test_mount/hello.txt 
    hello_nfs
    ~$ 
    ```
## Nodeのインストール
1. nvmをインストールします
    ```bash
    ~$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
    ```

1. シェルに反映します
    ```bash
    source ~/.bashrc 
    ```
1. 最新のLTSバージョンのNode.jsをインストールします
    ```bash
    ~$ nvm install --lts
    Installing latest LTS version.
    Downloading and installing node v22.14.0...
    Downloading https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz...
    ###################################################################################################################################################################################################### 100.0%
    Computing checksum with sha256sum
    Checksums matched!
    Now using node v22.14.0 (npm v10.9.2)
    Creating default alias: default -> lts/* (-> v22.14.0)
    ~$ 
    ```
1. インストールを確認します
    ```bash
    ~$ node -v
    v22.14.0
    ~$ npm -v
    10.9.2
    ~$ 
    ```

## Ubuntu Desktopのインストール
1. Ubuntu desktopをインストールします
    ```bash
    sudo apt install -y ubuntu-desktop
    ```

## GUIライブラリの一括インストール
1. `apt update`を実行します
    ```bash
    $ sudo apt update
    [sudo] password for XXX: 
    Hit:1 http://security.ubuntu.com/ubuntu noble-security InRelease                           
    Hit:2 http://jp.archive.ubuntu.com/ubuntu noble InRelease                                  
    Get:3 http://jp.archive.ubuntu.com/ubuntu noble-updates InRelease [126 kB]
    Hit:4 http://jp.archive.ubuntu.com/ubuntu noble-backports InRelease
    Fetched 126 kB in 3s (47.9 kB/s)
    Reading package lists... Done
    Building dependency tree... Done
    Reading state information... Done
    55 packages can be upgraded. Run 'apt list --upgradable' to see them.
    $ 
    ```
1. ライブラリをインストールします
    ```bash
    sudo apt install -y \
      libatk1.0-0t64 \
      libgtk-3-0t64 \
      libxss1 \
      libasound2t64 \
      libnss3 \
      libx11-xcb1 \
      libxcomposite1 \
      libxdamage1 \
      libxrandr2 \
      libgbm1 \
      libpango-1.0-0 \
      libcairo2
    
    ```


## Electron UIの構築
### プロジェクトの作成
1. 専用ディレクトリを作成します
    ```bash
    ~$ mkdir nfs-ui && cd nfs-ui
    ```
1. 初期化します
    ```bash
    ~/nfs-ui$ npm init -y
    Wrote to /home/XXXX/nfs-ui/package.json:
    
    {
      "name": "nfs-ui",
      "version": "1.0.0",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "description": ""
    }
    
    
    
    ~/nfs-ui$ 
    ```

1. electronをインストールします
    ```bash
    ~/nfs-ui$ npm install electron --save-dev
    npm warn deprecated boolean@3.2.0: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
    
    added 70 packages, and audited 71 packages in 26s
    
    17 packages are looking for funding
      run `npm fund` for details
    
    found 0 vulnerabilities
    ~/nfs-ui$ 
    ```

### main.jsの作成

1. main.jsを作成します
    ```js
    const { app, BrowserWindow, ipcMain } = require('electron');
    const { exec } = require('child_process');
    
    function createWindow () {
      const win = new BrowserWindow({
        width: 400,
        height: 300,
        webPreferences: {
          nodeIntegration: true,
          contextIsolation: false
        }
      });
    
      win.loadFile('index.html');
    }
    
    app.whenReady().then(createWindow);
    
    // mount処理
    ipcMain.handle('mount-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec('sudo mount -t nfs 127.0.0.1:/mnt/share /mnt/test_mount', (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve('mounted');
        });
      });
    });
    
    // umount処理
    ipcMain.handle('umount-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec('sudo umount /mnt/test_mount', (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve('unmounted');
        });
      });
    });
    ```
### index.htmlの作成

1. index.htmlを作成します
    ```html
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>NFS Control</title>
    </head>
    <body>
      <h1>NFS操作</h1>
      <button onclick="mount()">マウント</button>
      <button onclick="umount()">アンマウント</button>
      <p id="status"></p>
      <script src="renderer.js"></script>
    </body>
    </html>
    
    ```
### renderer.jsの作成

1. renderer.js を作成します
    ```js
    const { ipcRenderer } = require('electron');
    
    async function mount() {
      try {
        await ipcRenderer.invoke('mount-nfs');
        document.getElementById('status').innerText = 'マウント成功';
      } catch (e) {
        document.getElementById('status').innerText = 'マウント失敗: ' + e;
      }
    }
    
    async function umount() {
      try {
        await ipcRenderer.invoke('umount-nfs');
        document.getElementById('status').innerText = 'アンマウント成功';
      } catch (e) {
        document.getElementById('status').innerText = 'アンマウント失敗: ' + e;
      }
    }
    ```

### package.jsonの修正
1. package.jsonに、`"start": "electron ."`、`main:main.js`を追加します
    ```json
    ~/nfs-ui$ cat package.json 
    {
      "name": "nfs-ui",
      "version": "1.0.0",
      "main": "main.js",
      "scripts": {
        "start": "electron .",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "description": "",
      "devDependencies": {
        "electron": "^35.2.0"
      }
    }
    ~/nfs-ui$ 
    ```

###  chrome-sandbox に権限を設定
Electronは内部的にChrome（Chromium）を使っており、Linuxで特権昇格（setuid）機能によるサンドボックス保護を追加います。

1. 権限を設定します
    ```bash
    ~/nfs-ui$ sudo chown root:root ./node_modules/electron/dist/chrome-sandbox
    ~/nfs-ui$ sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
    ```

### 起動
1. `npm start`で起動します
    ```bash
    npm start
    ```
1. 画面が起動されました
1. マウントをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d4120fd7-1e32-4ce6-9c67-0554d6c68288.png)

1. アンマウントをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/72fef258-41d8-4ef3-a7ff-b27cd8d0c39c.png)


## ファイルの一覧を確認するように変更する
### main.jsを修正
1. 下記のように修正する
    ```js
    const { app, BrowserWindow, ipcMain } = require('electron');
    const { exec } = require('child_process');
    const fs = require('fs');
    const path = require('path');
    
    function createWindow () {
      const win = new BrowserWindow({
        width: 600,
        height: 400,
        webPreferences: {
          nodeIntegration: true,
          contextIsolation: false, // 今回は簡略化のためfalse
        }
      });
    
      win.loadFile('index.html');
    }
    
    app.whenReady().then(createWindow);
    
    const mountPoint = '/mnt/test_mount';
    const nfsTarget = '127.0.0.1:/mnt/share';
    
    // ✅ mountしてファイル一覧を返す
    ipcMain.handle('mount-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec(`sudo mount -t nfs ${nfsTarget} ${mountPoint}`, (error, stdout, stderr) => {
          if (error) return reject(stderr);
    
          try {
            const files = fs.readdirSync(mountPoint);
            resolve(files);
          } catch (err) {
            reject('マウントは成功しましたが、ファイル一覧取得に失敗しました');
          }
        });
      });
    });
    
    // ✅ umountのみ
    ipcMain.handle('umount-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec(`sudo umount ${mountPoint}`, (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve('アンマウント完了');
        });
      });
    });
    ```

### index.html
1. 下記のように修正します
    ```html
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>NFS UI</title>
    </head>
    <body>
      <h2>NFS マウントテスト</h2>
      <button id="mount">📂 Mount</button>
      <button id="umount">📤 Unmount</button>
      <ul id="file-list"></ul>
    
      <script src="renderer.js"></script>
    </body>
    </html>
    ```

### render.js
1. 下記のように修正します
    ```js
    const { ipcRenderer } = require('electron');
    
    document.getElementById('mount').onclick = async () => {
      try {
        const files = await ipcRenderer.invoke('mount-nfs');
        const ul = document.getElementById('file-list');
        ul.innerHTML = ''; // 初期化
        files.forEach(f => {
          const li = document.createElement('li');
          li.textContent = f;
          ul.appendChild(li);
        });
      } catch (err) {
        alert('マウントに失敗: ' + err);
      }
    };
    
    document.getElementById('umount').onclick = async () => {
      try {
        const result = await ipcRenderer.invoke('umount-nfs');
        alert(result);
        document.getElementById('file-list').innerHTML = '';
      } catch (err) {
        alert('アンマウントに失敗: ' + err);
      }
    };
    ```

### 起動
1. `npm start`で起動します
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/aa3e9432-ca9b-4e56-b89e-e9a1ac171010.png)

1. Mountをクリックします。ファイル名が見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/9e843701-b72e-4e1a-81ac-5f5c44faa94c.png)

1. Unmountをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/baa375fd-a5c2-465b-b773-8e8597c0b117.png)

1. 消えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/29fc0c51-3b06-40ea-9028-4d71195a68ce.png)

### ファイルを追加
1. ファイルを追加します
    ```bash
    $ sudo touch /mnt/share/hello2.txt
    [sudo] password for XXXX: 
    $ ls -l /mnt/share/
    total 4
    -rw-r--r-- 1 root root  0 Apr 20 12:40 hello2.txt
    -rw-r--r-- 1 root root 10 Apr 20 10:46 hello.txt
    $ 
    ```
1. Mountをクリックすると、追加し他ファイルが見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b128e733-fe84-4528-be49-f0c3fba50142.png)


# 考察
今回、ElectronでNFSマウントを行うUIを作成してみました。次回以降はコピーや削除なども試してみます。


# 参考
https://qiita.com/zumax/items/8090f59fc2b779af55be

https://qiita.com/zumax/items/2f016c4738bd02c8af05

https://qiita.com/zumax/items/52b8db51bc4017889c41