Electronで画像をサーバに送信して保存する
今の組織で、フリーアドレスの推進のアシストアプリとして、かねてより作ってきた「座席表アプリ」について、新機能を都度都度追加しています。前回の「組織図」をElectronに移植し、今回は「顔写真を変更する」機能を搭載を考案。顔写真はサーバ側にあり、写真のファイル名は社員番号.jpgといった形で置いてある。
一般ユーザは自分のIDでログインし自分の写真のみ変更可とし、管理者は全員の写真に対して変更可にする必要性もある。ローカルの画像をElectronからサーバ側(Node.js)に送って、既存のファイルを置換しつつ、MySQLサーバのpictフラグを-1にするよう(写真未登録時のみ)にコードを追加する必要があるので、記録の為に下記に残します。
目次
今回使用するファイルやライブラリ
ExpressでAPIを作って画像ファイルの受け口を作るのは、前回の記事でも実装しています。続編的にはこちらの記事の続編です。
事前準備
サーバー側の準備
サーバー側の準備は至ってシンプルです。
- 画像保存用のフォルダを作成しておく(Node.jsから書き込めるパーミッションも設定しておく)
- npm init -yでプロジェクトを作成しておく
- 使用するモジュール(今回は、expressモジュールのみ)をインストールしておく
- index.jsを作成しコードを記述する
モジュールをインストールするコマンドは以下の通りです。
npm i express --save
クライアント側の準備
keytarモジュールのインストールは非常に大変なので、こちらを参照してインストールしてください。それ以外で「今回必要な分のモジュール」をインストールしておきましょう。
npm i promise-mysql --save npm i electron-store npm i request npm i file-type
保存先フォルダの準備
サーバー側の画像を保存するフォルダを用意します。今回は後に出てくるsavepath変数に格納するimgフォルダに保存することにします。問題はこの保存先が例えば、どこか別の場所(/var/www以下のサブディレクトリなど)に配置したい場合、そのまま指定すると、permission deniedとなり、保存が出来ません。
これは対象のフォルダについて、アクセス権限が無いが為に起きる現象で、以下の手順で対象のフォルダのアクセス権限を変更してみましょう。
- ターミナルからgroupコマンドで、自分自身の所属グループを調べる(今回はadmに所属していたのでこれを利用します)
- 次に、sudo nautilusにてファイラを起動、対象のフォルダまで移動し、右クリック⇒プロパティを開く
- 所有者は自分自身のアカウントにしておく。そして、グループはadmを指定。いずれも作成と削除を許可する
- 改めてアップロードを実施し、画像がアップできていればOK
図:結構な嵌りポイントです
ソースコード
クライアント側
クライアントであるelectron側では、ファイルの選択やBase64エンコード、MySQLのデータの読み書きなど様々な処理を担当しています。今回のコードは必要な箇所だけに絞っていますので、注意してください。
レンダラープロセス側
<script> //テストダイアログを開く function choiceopen(){ //メインプロセスに引数を送信 ipcRenderer.send('baseopen'); } //seatデータを取得してHTMLへ反映する ipcRenderer.on('uploaded',function(event,arg){ //メッセージを取得 alert(arg); return; }) </script> <body> <button onClick='choiceopen()' id="base64man" class="action" title='テスト'>🍄</button> </body>
- メインプロセス側のファイル選択ダイアログの呼び出しと、アップロード結果を受け取るipcRendererだけが今回は行う仕事です。
メインプロセス側
'use strict'; //標準モジュールの宣言 const electron = require('electron'); const { app, dialog } = require('electron'); //Node.js側とHTML側で通信をするモジュール const ipcMain = require('electron').ipcMain; //追加モジュールの宣言 const keytar = require('keytar'); const mysql = require('promise-mysql'); const Store = require('electron-store'); const store = new Store(); const filetype = require('file-type'); //HTTPリクエスト用モジュールの読み込み var request = require('request'); var picturl = "http://サーバのアドレス:3700/api/v1/pictchange"; //ファイル選択ダイアログと選択後の処理 ipcMain.on('baseopen', function( event, args ){ //ファイル選択ダイアログを開く dialog.showOpenDialog(null,{ properties: ['openFile'], title: '顔写真を選択', filters: [ {name: '画像ファイル', extensions: ['jpg', 'png', 'gif']} ] }, function(files) { //ファイルへのパスが配列で返却される if(files){ //ファイルのパスを取得する var pictpath = files[0] //ファイルの拡張子を取得する var tempfile = fs.readFileSync(pictpath); var tempinfo = filetype(tempfile); var extname = tempinfo.ext; var mime = tempinfo.mime; //base64エンコードする fs.readFile(pictpath, 'base64', function(err, data) { if (err) throw err; //base64文字列を取得する var content = data.toString('base64'); var base64man = "data:" + mime + ";base64," + content; //MySQL側の対象ユーザのpictフラグを取得しつつ画像をアップする chkpictFlg([base64man,extname],function (ret){ switch(ret.status){ case "NOSET": //接続設定がないため繋がなかった場合の処理 mainWindow.webContents.send('message', "DB接続設定がありませんよ"); break; case "ERR": //エラーが発生した場合の処理 mainWindow.webContents.send('message', ret.msg); break; case "OK": //カウントデータを取得する event.sender.send('uploaded', ret.msg); break; } }); }); } }) }); //pictフラグをチェックしbase64データをサーバ側にPOSTする function chkpictFlg(args,callback){ var connection; var retman = {}; var pass = []; var result = "" var pictflg = ""; var base64man = args[0]; var extname = args[1]; //サービス名を構築する var servicename = "zaseki_" + store.get("id"); //接続設定があるかないか判定 if(store.get("id") == "undefined" || store.get("id") == null){ //エラーでコールバックさせる retman.status = "NOSET"; callback(retman); return; } var secret = keytar.getPassword(servicename,store.get("id")); secret.then((result) => { //パスワードを取得する pass = result; var uid = store.get("id"); //MySQLに接続してデータを取得する //createConnectionでは接続が時々切れる mysql.createConnection({ host: store.get("server"), port: 3306, user: uid, password: pass, database: store.get("dbname") }).then(function(conn){ //コネクション取得 connection = conn; //該当のIDの存在をチェック var result = connection.query('select * from userid where userid = "' + uid + '";'); return result; }).then(function(rows){ //pictflgを取得する pictflg = rows[0].pict; //送信オプション var options = { url:picturl, method:"POST", form:{ "uid":uid, "img":base64man, "ext":extname, }, json: true } //リクエスト送信 request(options, function (error, response, body) { if (!error && response.statusCode == 200) { //無事送信できた場合はMySQLサーバのpictflgを-1にする if(body.status == "OK"){ //MySQLのpictフラグを-1にする var result = connection.query('update userid set pict = ? where userid = ?;', [-1,uid], (err,result)=> { //エラーが発生した場合 if (err) { console.log("接続エラー"); retman.status = "ERR"; retman.msg = error; connection.end(); callback(retman); return; } } ); //無事にファイル生成が完了したので、通知する retman.status = "OK"; retman.msg = "画像のアップロードが完了しました。"; callback(retman); connection.end(); return; }else{ //エラーが発生したので通知する retman.status = "ERR"; retman.msg = "画像の生成に失敗しました"; callback(retman); connection.end(); return; } }else{ //エラー処理 console.log("送信エラー"); retman.status = "ERR"; retman.msg = error; connection.end(); callback(retman); return; } }); }).catch(function(error){ if (connection && connection.end) connection.end(); //logs out the error retman.status = "ERR"; retman.error = "接続エラーですよ。パスワードが違うとかサーバアドレス間違ってるとか、ありませんか?"; callback(retman); return; }); }); }
- picturl変数にはサーバー側のPOSTで用意しておいたAPIのURLを入れる(ポート番号も含めて)
- showOpenDialogにて、画像のファイル形式のみを選択可能なようにfilter設定しています。
- filetypeモジュールにて、ファイルの拡張子およびMIMETYPEを取得しています。
- 変数base64manにbase64エンコードしたデータ含めて、変換と組み立てを行っています。
- chkpictFlg関数にて一度MySQLへログインさせて、pictフラグの取得をさせることで認証としています。
- requestモジュールでサーバー側のNode.jsで作成したAPIへPOST通信を送っています。この時、json:trueとしておくと、返り値をJSONパースした状態で取得可能です。
- リクエストを送って、サーバステータスコードの200が返ってきたら無事にリクエストは成功
- なおかつ、アップロード完了の返り値をサーバ側から受け取ったら、MySQLサーバの対象者のpictフラグを-1にしてあげるようにしています。
- chkpictFlgからのcallbackを拾って、無事に完了すればevent.sender.sendにてレンダラプロセスのIPCである「uploaded」へ最終的に返しています(ここで処理が完了。場合によっては、ここから更にレンダラプロセス側で画像のリロードなどを入れておくと良い)
サーバー側
サーバー側の主な処理は、
- APIで受け取ったBase64データをデコード
- 指定のフォルダに社員番号と拡張子を持ってして、ファイルを上書き生成
のみです。非常にシンプルな作業だけを担当させています。
'use strict' // expressフレームワーク const express = require('express'); const app = express(); const bodyParser = require('body-parser'); var fs = require('fs'); //bodyParserを初期化 // urlencodedとjsonは別々に初期化する app.use(bodyParser.urlencoded({ extended: true, limit:'50mb' //アップできる画像サイズ上限を指定(デフォルト100kb) })); app.use(bodyParser.json()); //3700番ポートでリクエスト待機 app.listen(3700, () => console.log('Listening on port 3700')) //POSTでbase64データを受信する app.post('/api/v1/pictchange', (req, res) =>{ //base64データを取得してデコードする var para = req.body; var retman = {}; var base64 = para.img; var uid = para.uid; var ext = para.ext; //ファイル名を構築する var filename = uid + "." + ext; //保存先フォルダパス var savepath = "./img/"; //base64デコード var fileData = base64.replace(/^data:\w+\/\w+;base64,/, '') var decode = Buffer.from(fileData, 'base64'); //ファイルを上書きで書き出し fs.writeFile(savepath + filename, decode, function(err) { //エラーが発生した場合 if(err){ //エラーを返す retman.status = "ERR"; retman.error = err; res.json(retman) return; }else{ //成功した通知 retman.status = "OK"; res.json(retman) return; } }); });
- bodyParserのlimitを50MBに設定していますが、デフォルトは100KBです。画像ですので、適切なファイルサイズを指定しないと「request entity too large」のエラーが発生します。
- 今回は3700番ポートで/api/v1/pictchangeというURLにてPOST用のAPIを待機させてます。
- POSTで受信するので、app.postで受信したデータを冒頭で分解しています。
- savepathですが、このnode.jsのサーバがきちんと書き込めるパーミッションである必要があります。(今回は、このindex.jsがあるフォルダにimgフォルダを作っておきました)
- デコードはBuffer.fromで行います。new Bufferは現在非推奨ですので注意してください。
- fs.writeFileにてデコードしたデータをファイルに戻し、指定のフォルダに生成します。
- res.jsonにてクライアント側にレスポンスを返しています。
関連リンク
- Electron APIデモから学ぶ実装テクニック ― ネイティブUIと通信
- [Node.js]Basse64エンコード、Base64デコードしてみた
- Canvas(ブラウザ)からBase64変換した画像をNode.jsのサーバーに送って画像保存する #linebootawards
- Electron 画像をドラッグ&ドロップするだけでリサイズするアプリをつくる
- JavaScript(Node.js)を使った画像ファイルのBase64エンコード
- Express4でエラー「request entity too large」が発生する
- Node.js スクリプトからの HTTP 通信時にプロキシを通すには request が楽チン
- permission deniedで悩まされたので勉強してみた