ファイルはコピーできたが、ディレクトリはコピーできなかった。原因不明。

ブツ

インストール&実行

NAME='Electron.fs.cp.File.20220829162539'
git clone https://github.com/ytyaru/$NAME
cd $NAME
npm install
npm start
  1. インストール&実行してアプリ終了する
  2. 以下ファイルが作成される(ファイルコピー)
    • dst/src/lib/toastify/1.11.2/min.js
    • dst/src/lib/toastify/1.11.2/min.css

src/lib/toastify/1.11.2/配下にある同ファイルをdst/配下にコピーした。

fs.cp

Node.js APIであるfs.cpを使えばファイルやディレクトリをコピーできるっぽいので使ってみた。

v16.7.0から追加されたらしいので注意。私の環境はv16.15.1だったのでセーフ。

$ node --version
v16.15.1

こんな基本的なコマンドも割と最近できたみたい。Node.jsって結構昔からあったような気がするけど。wikipediaによると2009年が初版らしい。なのに最近までfs.cpコマンドがなかったのが不思議。まあいいか。藪蛇になる前に進めよう。

コード抜粋(成功)

main.js

const fs = require('fs')
ipcMain.handle('cp', async(event, src, dst) => {
    fs.cp(src, dst, ()=>{})
})
位置 名前 意味
1 src コピー元のパス
2 dst コピー先のパス
3 callback コールバック関数

コールバック関数は一体何に使うのか不明。引数さえ何も受け取れないみたいだし。なので空っぽのメソッドをセットした。

コピー先に同名ファイルがあってもエラーにならずスルーされるらしい。fs.cp(src, dest[, options], callback)のうちoptions引数にerrorOnExist:trueプロパティを付与すれば、エラー発生するようだ。

optionsも任意で付与できるようにするなら以下。

ipcMain.handle('cp', async(event, src, dst, options=null) => {
    if (options) { fs.cp(src, dst, options, ()=>{}) }
    else { fs.cp(src, dst, ()=>{}) }
})

optionsはオブジェクトであり、以下のようなプロパティを許容するようだ。

optionsプロパティ名 デフォルト値 概要
dereference boolean false シンボリックリンクを逆参照する
errorOnExist boolean false コピー先が既存ならエラー
filter function - コピーされたファイル/ディレクトリをフィルタリングする。trueの項目をコピーしfalseを無視する
force boolean true 既存のファイルまたはディレクトリを上書き
preserveTimestamps boolean false タイムスタンプ保持
recursive boolean false ディレクトリ再帰的コピー
verbatimSymlinks boolean false trueならシンボリックリンクのパス解決をスキップする

recursiveは是非ほしい。

あと、recursive:trueでなくとも、出力先ディレクトリの階層が深ければ必要な分だけ作成してくれるらしい。fs.mkdirSyncなら第二引数に{recursive:true}が必要だった。けれどfs.cpなら不要であり自動作成してくれるようだ。そうした違いもあるので若干混乱しそう。

ディレクトリコピーの場合はglobも使えないらしいので、もうふつうにLinuxのcpコマンドを使いたいなぁと思ってしまう。でもOS間差異を吸収してもらうために仕方なくfs.cpを使う。

というか、なぜかディレクトリはコピーできなかった。原因不明。

preload.js

const {remote,contextBridge,ipcRenderer} =  require('electron');
contextBridge.exposeInMainWorld('myApi', {
    cp:async(src, dst, options=null)=>await ipcRenderer.invoke('cp', src, dst, options=null),
})

renderer.js

const maker = new SiteMaker(setting)
await maker.make()

renderer.js

class SiteMaker {
    constructor(setting) {
        this.setting = setting
    }
    async make() { // 初回にリモートリポジトリを作成するとき一緒に作成する
        await this.#make(`src/lib/toastify/1.11.2/min.js`)
        await this.#make(`src/lib/toastify/1.11.2/min.css`)
    }
    async #make(path) { await window.myApi.cp(path, `dst/${this.setting.github.repo}/${path}`, {'recursive':true, 'preserveTimestamps':true}) }
}

もし存在しないパスをsrcに渡すと以下のようなエラーが出る。

[Error: ENOENT: no such file or directory, lstat 'lib/toastify/1.11.2/min.css'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'lstat',
  path: 'lib/toastify/1.11.2/min.css'
}

コピー対象がディレクトリではなくファイル単体であっても'recursive':trueがあっても問題ないらしい。なら、もうこれは何も考えずにつけておけばいいかな。

そう思って以下のようにディレクトリをコピーしようとしたら、できなかった。エラーも表示されず、コピーもされない状態。なぜ? ディレクトリの場合は'recursive':trueが必須なのかな? 原因不明。

await this.#make(`memo/`)

コールバック関数は必須

main.js

以下のようにfs.cpのコールバック関数をセットしなかったらエラーになった。

ipcMain.handle('cp', async(event, src, dst) => {
    fs.cp(src, dst)
})

アプリの開発者ツールのコンソールでは以下エラー。

Uncaught (in promise) Error: Error invoking remote method 'cp': TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined

端末では以下エラー。

Error occurred in handler for 'cp': TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
    at makeCallback (node:fs:186:3)
    at Object.cp (node:fs:2834:14)
    at /tmp/work/Electron.MyLog.20220829121957/src/js/main.js:98:8
    at node:electron/js2c/browser_init:189:579
    at EventEmitter.<anonymous> (node:electron/js2c/browser_init:161:11327)
    at EventEmitter.emit (node:events:527:28) {
  code: 'ERR_INVALID_CALLBACK'
}

コールバック関数の使い方がわからないが、それでも必須らしい。fs.cp実行後に必ず実行される関数ということなのだろうけど。それで何をしろと? せめて成否などの結果を引数で受け取れたなら、ログ出力などで使えるかなと思うが。

仮に使い方がわかったとしても、IPC通信のシリアライズ制約により関数を引数として受け取れない。なのでコールバック関数は引数として受け取れない。

結局、fs.cpは最低でも以下の3つ引数が必要だとわかった。

const fs = require('fs')
ipcMain.handle('cp', async(event, src, dst) => {
    fs.cp(src, dst, ()=>{})
})