electronでBox APIを使って定期自動バックアップ – 実装編

前回の記事にて、Box APIに於けるOAuth2.0認証を行ってAccess Token取得までのフローをElectronにて構築しました。今回はこれらのTokenの暗号化保存、対象のフォルダのZIP圧縮、Refresh Tokenの処理、実際にBoxへアップロード、パッケージの作成までを今回実装したいと思います。

Token暗号化ではユーザにパスワードを登録してもらい、keytarで資格情報マネージャに格納、以降Tokenの暗号化や復号化で利用します。

今回追加で利用するモジュール等

  • keytar – npm - 様々な機密情報を資格情報マネージャ等に格納するモジュール
  • Crypto - Node.js - ファイルやデータを暗号化・復号化する為のモジュール
  • archiver - npm - ファイルやフォルダを圧縮する為のモジュール
  • request – npm – Box APIを叩く為に必要なHTTPリクエストモジュール
  • fs-extra - npm - fsモジュールに足りない便利な機能を追加してくれるモジュール
  • date-utils – npm - new Date()を拡張するモジュール
  • electron-packager - 各種OS用に実行ファイル形式のパッケージを作ってくれるモジュール

今回のプログラムでは、予めユーザが指定したパスワードをkeytarで保管。そのパスワードを持ってAccess TokenやRefresh Tokenを暗号化保存、対象のフォルダをZIPで圧縮して、Access Tokenを用いてBoxへREST APIを使ってアップロードの手順になります。

今回のプロジェクトファイルのテンプレートを作りました。

今回のファイルでは以下の手直し等が必要です(PASS:monster-energy)

  1. プロキシのURLやPACファイルのURLなどいくつか手動で入力しなければならない
  2. npm installにてモジュールはインストールされますが、keytarについてはリビルドが必須です
  3. バージョン情報などは修正が必要です(package.json含む)

事前準備

各種モジュールをインストールしておきましょう。requestは既にdepreateになっていますがまだ使えます。いずれ他のモジュールに置き換える必要があるでしょう。今回はrequestモジュールで続行します。

npm install archiver --save
npm install request
npm install fs-extra

CryptoモジュールはNode.js標準装備なので、追加作業は必要ありません。Keytarについてはこちらを参照してください。

パスワードの保存

トークンを暗号化する為のパスワードを資格情報マネージャに登録する為の一連の機能を実装します。設定にパスワード入力欄を設けて、メインプロセス側にはそれを保存するコードを用意します。

setting.html

HTML部分にはパスワード入力欄を追加します。

<li>
    <label for="uid">暗号化パス:</label>
    <input type="password" name="password" placeholder=""  style="width:150px" id="password" required />
</li>

savesetting関数には、入力欄の値を配列に加えるコードを加えます。

//セッティング項目を保存する
function savesetting(){
    //入力値のvalidation
    var validata = "";
    var array = [];

    ・・・・中略・・・・

    //暗号化パス
    validata = document.getElementById("password").value;
    if(validata == ""){
        //エラー表示
        alert("暗号化パスが入っていません");
        return;
    }else{
        array.push(validata);
    }

    //メインプロセスに処理を送る
    ipcRenderer.send('setstore', array);
}

また、起動時に保存済みパスワードを呼び出す為のipcrender.onにもコードの追加が必要です。

//保存済みデータを受け取る
ipcRenderer.on('init',function(event,arg){
  //受け取ったデータをHTMLのボックスに反映する
  var array = arg;

  //配列データを各項目に入れてあげる
  ・・・中略・・・
  
  document.getElementById("password").value = array[5];
});

index.js

HTML側から受け取ったパスワードをまずはkeytarで保存。servicenameは「box_auth」としました。

ipcMain.on('setstore', function( event, args ){
  ・・・・前略・・・

  //サービス名を構築する
  var servicename = "box_auth";

  //パスワードをkeytarで保存
  keytar.setPassword(servicename,"temp",array[5]);

  ・・・・後略・・・
  
});

また、設定ダイアログを起動時にパスワードを呼び出す項目の追加も必要です。

ipcMain.on('async', function( event, args, args2){
  //コマンド名によって処理を開始
  switch(args){
    case "init":

      ・・・・中略・・・・

      //サービス名を設定する
      var servicename = "box_auth";
      var tempid = "temp";
 
      //サービス名で探索して返す
      if(keytar.findPassword(servicename)){
        const secret = keytar.getPassword(servicename,tempid);
        secret.then((result) => {
          array.push(result);
          event.sender.send('init', array);
          return;
        });
      }else{
        array.push("");
        event.sender.send('init', array);
      }

      break;
  }
});

図:無事に保存されました

トークン類の暗号化

Access TokenやRefresh Tokenは文字が長すぎる為、keytarなどで資格情報マネージャに格納ができません。そこで、取得したこれらのデータはJSON化し、設定したパスワードを持って、AES256bitで暗号化して保存しておきます。暗号化には、Cryptoモジュールを利用します(標準装備なので別途インストールは不要)。以前別のエントリーでも復号化だけは実際に作っています。

また、生成時に復号化してTokenが切れているかチェックしやすいように、取得時の日付時間および期限の日付時間もJSONに含めて置こうと思います。

暗号化用関数

//暗号・複合モジュール
var crypto = require("crypto");
const keytar = require('keytar');
const algorithm = 'aes-256-cbc';
var iv = Buffer.from(crypto.randomBytes(16));  
var ivstring = iv.toString('hex').slice(0, 16);

//暗号化の為の関数
function encryptAes(json){
  //サービス名を構築する
  var servicename = "box_auth";
  var tempid = "temp";

  //key名で探索して返す
  if(keytar.findPassword(servicename)){
    const secret = keytar.getPassword(servicename,tempid);
    secret.then((result) => {

      //keyは32byteでなければならない
      var key = crypto.createHash('sha256').update(String(result)).digest('base64').substr(0, 32);

      //暗号化
      let cipher = crypto.createCipheriv(algorithm, key, ivstring);
      let encrypted = cipher.update(json);
      encrypted = Buffer.concat([encrypted, cipher.final()]);

      //ファイル書き出し
      fs.writeFileSync("user.json", encrypted);

      //ivの値をkeytarで保存しておく
      //サービス名を構築する
      servicename = "box_iv";
      keytar.setPassword(servicename,"temp",ivstring);

      return true;
    });
  }else{
    return false;
  }
}
  • 暗号化の為のencryptAes関数を用意。取得したTokenデータ(JSON形式)を暗号化して、user.jsonというファイルで保存します。
  • この関数はapp.js側で利用するので、app.js側に記述しています。
  • 今回復号化のルーチンは、index.js側に用意してるので、こちらには記載していません。
  • 暗号化は、資格情報マネージャに登録してあるパスワードを使って暗号化しています(そのためにkeytarを利用)。
  • 暗号化する場合には、encryptAes関数には、JSON.stringifyでJSON文字列に変換してから渡す必要があります。
  • 以前使用していたcrypto.createCipherはセキュリティ上の理由でDeprecatedになってしまったので、こちらのサイトを参考にメソッドを置き換えています。
  • ivの値は16byteで設定し、keytarから取得したパスワードをHASH化しBase64でエンコードした後、32byteで切り出したものを暗号化のkeyとして利用します。
  • 復号化する時の為にランダム生成iv値はkeytarを使って、資格情報マネージャのbox_ivに値を格納しておく

図:暗号化されたTokenデータファイル

復号化用関数

Boxの各種APIを叩く為には、Access Tokenが必要です。しかし、前項でTokenデータは暗号化してありますので、利用時には復号化してあげなければTokenデータを取り出せません。decryptAes関数にてuser.jsonのデータを復号化してコンソールに表示するというものを作ってみました。

var crypto = require("crypto");
const algorithm = 'aes-256-cbc';

//復号化の為の関数
function decryptAes(callback){
  //ファイルの有無を確認
  let chkfile = fs.existsSync('user.json');

  if(chkfile == true){
    //user.jsonファイルを取り込む
  var json = fs.readFileSync("user.json");
  }else{
    //ダイアログオプション
    var options ={
      type:'error',
      title:"エラー",
      button:['OK'],
      message:'Tokenファイルがありません',
      detail:"user.jsonファイルが見つかりません。Box認証を実行してください。"
    }

    dialog.showMessageBox(null,options);
    return false;
  }
 
  //サービス名を構築する
  var servicename = "box_auth";
  var servicename2 = "box_iv";
  var tempid = "temp";

  //パスワードとivの値用
  var pass = "";
  var iv = "";
 
  //key名で探索して返す
  if(keytar.findPassword(servicename)){
    const secret = keytar.getPassword(servicename,tempid);
    secret.then((result) => {
      //passを格納する
      pass = result;

      //iv値を取得する
      if(keytar.findPassword(servicename2)){
        const secret2 = keytar.getPassword(servicename2,tempid);
        secret2.then((result2) => {
          //iv値を格納する
          iv = result2;

          //復号化
          //keyは32byteでなければならない
          var key = crypto.createHash('sha256').update(String(pass)).digest('base64').substr(0, 32);
          let decipher = crypto.createDecipheriv(algorithm, key, iv);
          let decrypt = decipher.update(json);
          let decrypted = Buffer.concat([decrypt, decipher.final()]);
    
          //復号化したデータを返す(Stringで変換する)
          callback(String(decrypted));
        });
      }else{
        return false;
      }
    });
  }else{
    return false;
  }
}
  • fs.existsSyncにてまず、user.jsonの有無を確認。ファイルが無い場合は認証がされていないので、ダイアログで認証を促します。
  • index.js側にも、cryptoモジュールを読み込ませておきます。
  • 復号化の為に、decryptAes関数を用意。user.jsonを暗号化されたまま、まずは取り込みます。
  • 暗号化時に生成したiv値をkeytarで資格情報マネージャから取り出し復号化で利用します。
  • 復号化したデータをそのまま返しても、返された側で取り出せないので、Stringで文字列に変換します。
  • 復号化は、資格情報マネージャに登録してあるパスワードを使って復号化しています(そのためにkeytarを利用)。
  • decryptAes関数をcallbackにしておかないと、呼び出し元で呼び出されても返り値を受け取れません。
  • 呼び出し元では以下のようなコードで、呼び出してcallbackで受け取ってコンソール表示しています。
//decryptAes関数を呼び出してAccess Tokenを取り出す
decryptAes(function(ret){
  //JSONをParseする
  var json = JSON.parse(ret);
 
  //access_tokenを取り出す
  console.log(json.access_token);
})

図:無事に復号化して、Access Tokenだけ取り出せた

取得したAccess Tokenを暗号化

app.js側に取得したAccess Tokenを実際に作成した関数で暗号化してみます。Box APIのTokenの仕様は

  • Access Tokenは1時間で失効する
  • refresh_tokenは60日で失効する

そのため、60日後には再認証が必要になります。また、日付時刻で失効してるかどうかを確認する必要があるので、取得日時などをJSONファイルに含めてあげるようにします。

passport.use(new BoxStrategy({
    clientID: clientID,
    clientSecret: clientsecret,
    callbackURL: redirecturi
  },
  function(accessToken, refreshToken, profile, done) {
    process.nextTick(function () {
      try{
        //Access Tokenなどを書き出し
        //トークン取得日時
        var tokenday = new Date();

        //アクセストークンリミットの時間を生成(1時間)
        var aclimit = tokenday.setHours(tokenday.getHours() + 1);

        //リフレッシュトークンリミットの日付を生成(60日)
        var rtlimit = tokenday.setDate(tokenday.getDate() + 60);

        //データを組み立て
        var userdata = {};
        userdata.access_token = accessToken;
        userdata.refresh_token = refreshToken;
        userdata.atlimit = aclimit;
        userdata.rtlimit = rtlimit;

        //Access Tokenをテキストに書き出す
        //暗号化
        encryptAes(JSON.stringify(userdata));

        //HTML側へ返す
        return done(null, profile);
      }catch(err){
        return done(null, err);
      }
    });
  }
));

user.jsonの保管場所

Tokenを暗号化したファイルであるuser.jsonですが、autolaunchモジュールを利用した自動起動時にファイルを見失う事があるので、保管場所をドキュメントフォルダ直下にし、user.jsonを参照する場合にはフルパスで指定するようにすると、きちんとファイルを特定してくれます。

app.jsおよびindex.js双方でuser.jsonを参照してる箇所があるので、これを以下のコードを冒頭に追加し、jpath変数を参照するように変更します。

//user.jsonの場所を指定する
var dir_home = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
var docupath = require("path").join(dir_home, "Documents");
var jpath = docupath + "//user.json";

Refresh Tokenを使ったTokenの再取得

取得したAccess Tokenはおよそ1時間で失効します。其のため、継続的に使うには毎回ログインし直さないといけない。これではあまりにも不便です。そこで用意されているのがRefresh Token。これを使って新しいAccess Tokenを自動的に取得して、継続してアプリを使えるようにする仕組みが、この手のアプリケーションでは必須です。

requestモジュールをインストールする

今回使用するのは、requestというモジュール。passport自体にリフレッシュ機能が備わっていないので、これを使う必要があります。インストール自体はとても簡単。

npm i request

これだけ。simple-oauth2passport-oauth2-refreshなどのモジュールもあるのですが、どちらも自分の環境では使えなかったので、自力でrequestモジュールで更新するコードを構築しました。

リフレッシュするコード

特定のモジュールがなくとも、requestモジュールで組み立ててあげれば、Refresh Tokenにてトークンの取得が可能です。Refresh Tokenでリフレッシュすると、新たにAccess TokenとRefresh Tokenが取得出来ます。60日間Refresh Tokenを使ってリフレッシュしないと、再認証が必要になります。

ログアウト時にTokenを廃棄する

app.js側に於いて、ログアウトを実行した際に、user.jsonを廃棄するコードを追加しておきます。

//ログアウト時
app.get('/logout', function(req, res){
  req.session.destroy((err) => {
    if (err) {
      res.send(err)
      return
    }
      //user.jsonファイルを廃棄する
      try {
        fs.unlinkSync('user.json');
        //ダイアログオプション
        var options ={
          type:'info',
          title:"サインアウト",
          button:['OK'],
          message:'トークンは廃棄されました',
          detail: "Access Tokenは廃棄されました。"
        }

        dialog.showMessageBox(null,options);
      } catch (error) {
        //何もしない
      }

      //トップページに移動する
      req.logout();
      res.redirect('/')
    })
});

Access Tokenをリフレッシュする

今回は、プログラム側で自動でToken期限切れを検知して、自動でRefresh Tokenを使ってAccess Tokenを取得し直すようにする為、index.js側にリフレッシュ用のコードを用意します。そのため、index.js側

  • encryptAES関数を追加で記述する(app.js側の関数を参照できない為)
  • encryptAES関数で利用する変数も追記する
  • requestモジュールの呼び出しと社内の場合プロキシ越えの設定を追記

作業が必要です。また、リフレッシュさせる為に、index.js側にrefreshToken関数とrenewToken関数の2つを用意します。

//暗号・複合モジュール
var crypto = require("crypto");
const algorithm = 'aes-256-cbc';
var iv2 = Buffer.from(crypto.randomBytes(16));  
var ivstring = iv2.toString('hex').slice(0, 16);

//HTTPリクエスト用モジュールの読み込み
var request = require('request');

//request用プロキシー設定
var proxy = request.defaults({'proxy':'http://プロキシURL:プロキシポート番号'})

//リフレッシュ用エンドポイント
var ref = "https://api.box.com/oauth2/token";

//Access Tokenをリフレッシュする
function refreshToken(){
  //user.jsonを復号化して各種値を取り出す
  decryptAes(function(ret){
    //JSONデータを受け取る
    var json = JSON.parse(ret);
 
    //tokenリフレッシュの実行
    renewToken(json,function(ret){
      //更新ステータスを取得する
      var status = ret[0];
 
      //ステータスによって処理を分岐
      if(status == true){
        //既存のJSONに値をマージする
        const user = {
                    "access_token": ret[1].access_token,
                    "refresh_token": ret[1].refresh_token,
                    "atlimit": ret[1].atlimit,
                    "rtlimit": ret[1].rtlimit
                };
 
        //Access Tokenをテキストに書き出す
        //暗号化
        encryptAes(JSON.stringify(user));
 
        //ダイアログオプション
        var options ={
          type:'info',
          title:"認証成功",
          button:['OK'],
          message:'トークンリフレッシュ成功',
          detail: "Access Tokenのリフレッシュが成功しました。"
        }

        dialog.showMessageBox(null,options);
 
      }else{
        //ダイアログオプション
        var options ={
          type:'info',
          title:"エラー",
          button:['OK'],
          message:'認証エラーが発生しました',
          detail:ret[1]
        }
 
        dialog.showMessageBox(null,options);
 
      }
    });
  })
}

//リフレッシュトークンを使って新しいTokenを取得する
function renewToken(token,callback){
  //リフレッシュトークンの日付時刻
  var dt = new Date(token.rtlimit);
 
  //現在の日付時刻
  var dt2 = new Date();

  //取得したRefresh Tokenの期限日と現在日付を比較する
  if(dt >= dt2){
    var clientid = String(store.get("clientid"));
    var clisec = String(store.get("secret"));
    var reftoken = "refresh_token";

    //POST通信オプションの設定
    var options = {
      uri: ref,
      headers: {
        "Content-type": "application/json",
      },
      json: {
        client_id: clientid,
        client_secret: clisec,
        refresh_token: token.refresh_token,
        grant_type: reftoken
      }
    };

    //requestにて新しいトークンを取得する
    proxy.post(options, function (error, response, body) {
      //ステータスコードを取得する
      var status = response.statusCode;  
      
      //無事に取得できたら
      if (!error && status == 200) {
        //データのパース
        var json = body;
 
        //トークン取得日時
        var tokenday = new Date();
 
        //アクセストークンリミットの時間を生成(1時間)
        var atlimit = tokenday.setHours(tokenday.getHours() + 1);
 
        //リフレッシュトークンリミットの日付を生成(60日)
        var rtlimit = tokenday.setDate(tokenday.getDate() + 60);
 
        //トークンデータを返す
        var newToken = {};
        newToken.atlimit = atlimit;
        newToken.rtlimit = rtlimit;
        newToken.access_token = json.access_token;
        newToken.refresh_token = json.refresh_token;
        callback([true,newToken]);
 
      }else{
        //エラー処理
        callback([false,error]);
        return;
      }
    });
  }else{
    //リフレッシュトークン切れなので、再認証が必要
    callback([false,"Refresh Tokenの期限切れ。再認証が必要です。"]);
  }
}
  • index.js側でrequestモジュールを社内プロキシの後ろで使う場合には、request.defaultsにてプロキシを直接指定し、proxy.postでHTTPリクエストを投げるようにします。
  • descriptAESでuser.jsonを復号化し、そのままrenewTokenへわたしてあげます
  • Box APIをrequestモジュールでrefresh要求する場合、公式ドキュメント通りのやり方を行うと400エラーになります。
  • リクエストヘッダーはapplication/x-www-form-urlencodedではなく、application/jsonを指定しましょう。
  • optionsを組み立てたら、proxy.postでHTTPリクエストを投げます。
  • ステータスコード200が帰ってきたら、bodyの中身はJSONなので、取り出しencryptAES関数で再びuser.jsonを再生成し、ivの値をkeytarで格納してあげます。
  • Token失効の条件判定を行ってこの関数を呼び出せば自動で、失効時にはrenewTokenが実行されてBox APIが実行される仕組みです。

フォルダをZIPで圧縮

archiverモジュールを利用して圧縮を行いますが、fsモジュールも併用する必要があるので、事前に読み込みが必要です。

//ファイル圧縮関係
var archiver = require('archiver');
var fs = require('fs');
require('date-utils');

//ZIP形式でフォルダを圧縮
function zipman(callback){
    //デスクトップのパスを取得
    var dir_home = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
    var deskpath = require("path").join(dir_home, "Desktop");

    //日付でファイル名を生成
    var today = new Date();
    var filename = deskpath + "//backup_" + today.toFormat("YYYYMMDDHH24MISS") + ".zip";

    //archiverの準備
    var archive = archiver.create('zip', {
      zlib: { level: 9 } //圧縮レベルの指定
    });

    var output = fs.createWriteStream(filename);
    archive.pipe(output);

    //圧縮対象のフォルダの指定
    var zipfolder = store.get("folpath");
    archive.directory(zipfolder, false);

    //圧縮完了後
    output.on("close", function () {
      //ZIPファイルパスをcallback
      callback(filename);
    });

    //エラー発生時
    archive.on('error', function(err){
      throw err;
    });

    //ZIP圧縮を行う
    archive.finalize();
}
  • date-utilsにてnew Date()を拡張し、日付の処理をしやすくしています。
  • 今回はデスクトップにbackupという名前に日付でzipのファイル名をつけています。
  • あらかじめelectron-storeにて設定しておいた圧縮対象フォルダをarchive.directoryにて圧縮を指定しています。
  • output.onにて圧縮完了を待って、呼び出し元へcallbackでZIPファイルのパスを返しています。
  • archive.finalizeにて圧縮を実行します。

ただし、この状態の場合、「誰かが指定のフォルダ内のファイルを開いてる場合(特にExcel)」には、圧縮時にエラーが発生します。Excelなどが作成する一時ファイルが圧縮時のエラーの原因です。これを回避するには、対象のフォルダを一旦、デスクトップに作った一時フォルダ内にまるごとコピーし、それを圧縮。完了後に一時フォルダごと削除する処理が必要です。フォルダまるごとコピーや削除では、fs-extraを利用します。

var fsExtra = require('fs-extra')

//ZIP形式でフォルダを圧縮
function zipman(callback){
    //デスクトップのパスを取得
    var dir_home = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
    var deskpath = require("path").join(dir_home, "Desktop");
    var zipfolder = store.get("folpath") + "//";

    //日付でファイル名を生成
    var today = new Date();
    var filename = deskpath + "//backup_" + today.toFormat("YYYYMMDDHH24MISS") + ".zip";

    if (fs.existsSync(zipfolder)) {
      //フォルダがあるので問題ない
    } else {
      //アップロード失敗
      var options ={
        type:'info',
        title:"失敗",
        button:['OK'],
        message:'圧縮失敗',
        detail:'コピー元フォルダが存在しません'
      }

      //フォルダが無いのでエラー処理
      //表示する
      dialog.showMessageBox(null,options);
      return false;
    }

    //デスクトップに一時フォルダを作成
    var tempfolder = deskpath + "//tempzip_backup//";
    if (!fs.existsSync(tempfolder)) {
      fs.mkdirSync(tempfolder);
    }

    //一時フォルダにfolpathのフォルダをコピー
    fsExtra.copySync(zipfolder, tempfolder, {
      clobber: true, 
      dereference: false, 
      filter: function (element) {
          return true;
          //or
          return false;
      }, 
        preserveTimestamps: false 
    });

    //archiverの準備
    var archive = archiver.create('zip', {
      zlib: { level: 9 } //圧縮レベルの指定
    });

    var output = fs.createWriteStream(filename);
    archive.pipe(output);

    //圧縮対象のフォルダの指定
    archive.directory(tempfolder, false);

    //圧縮完了後
    output.on("close", function () {
      //一時ディレクトリを削除
      fsExtra.remove(tempfolder, function (err) {

      });

      //ZIPファイルパスをcallback
      callback(filename);
    });

    //エラー発生時
    archive.on('error', function(err){
      //アップロード失敗
      var options ={
        type:'info',
        title:"失敗",
        button:['OK'],
        message:'圧縮失敗',
        detail:'フォルダの圧縮に失敗しました。' + err
      }

      //表示する
      dialog.showMessageBox(null,options);
    });

    //ZIP圧縮を行う
    archive.finalize();
}
  • fs.existsSyncにて、デスクトップの一時フォルダの有無を確認後に、一時フォルダを作成します。
  • fsExtra.copySyncにて、指定のフォルダをまるごと、一時フォルダにコピーします
  • fsExtra.removeにて、指定のフォルダをまるごと削除します(ZIP圧縮完了後)。この処理はファイル数によっては時間が掛かるので、非同期で処理をします

ZIPファイルをBoxへアップロード

いよいよ最後の関門である、Boxへのファイルのアップロードですが、こちらもリファレンス通りに行うと400エラーでアップロードが出来ません。Invalid API request pathというエラーがでます。

過去に作ったVBAでBox APIを叩くコードを参考にrequestモジュールだけでアップロードができるように、またアップロード時にAccess Tokenが期限切れかどうかをチェックする関数を用意し、継続的にアップロードができるようにします。

Access Tokenが期限切れかチェックする関数

//Access Tokenが期限切れかどうかチェックする関数
function chkexpireToken(callback){

  //user.jsonを復号化して各種値を取り出す
  decryptAes(function(ret){
    //JSONデータを受け取る
    var json = JSON.parse(ret);

    //リフレッシュトークンの日付時刻
    var dt = new Date(json.atlimit);   //アクセストークンリミット
    var dt3 = new Date(json.rtlimit);  //リフレッシュトークンリミット
  
    //現在の日付時刻
    var dt2 = new Date();

    //expireまでの日数・時間(分)チェック
    let rtsabun = dt3.getTime() - dt2.getTime();
    var diffTime = dt.getTime() - dt2.getTime();
    var diffday = Math.floor(rtsabun / (1000 * 60 * 60 * 24) ); 
    var diffSec = Math.floor(diffTime / (1000 * 60) );  //分の差を取得

    if(diffday <= 1){
      //60日リミットまで1日以下なのでTokenをリフレッシュする
      renewToken(json,function(retman){
        if(retman[0] == false){          
          //Tokenリフレッシュ失敗
          //コールバックする
          callback(false)
        }else{
          //既存のJSONに値をマージする
          let user = {
            "access_token": retman[1].access_token,
            "refresh_token": retman[1].refresh_token,
            "atlimit": retman[1].atlimit,
            "rtlimit": retman[1].rtlimit
          };

          //Access Tokenをテキストに書き出す
          //暗号化
          encryptAes(JSON.stringify(user));

          //コールバックする
          callback(retman[1].access_token)
        }
      });
    }else{
      //Access Tokenの期限切れチェック
      console.log(diffSec + "min");
      if(diffSec <= 10){
          //10分以下の場合、リフレッシュする
          renewToken(json,function(retman){
            if(retman[0] == false){ 
            //Tokenリフレッシュ失敗
            //コールバックする
            callback(false)
          }else{
            //既存のJSONに値をマージする
            let user = {
              "access_token": retman[1].access_token,
              "refresh_token": retman[1].refresh_token,
              "atlimit": retman[1].atlimit,
              "rtlimit": retman[1].rtlimit
            };

            //Access Tokenをテキストに書き出す
            //暗号化
            encryptAes(JSON.stringify(user));

            //コールバックする
            callback(retman[1].access_token)
          }
        });
      }else{
        //コールバックする
        callback(json.access_token)
      }
    }
  });
}
  • 60日のRefresh Token切れのチェックおよび1時間のAccess Token切れをチェックし、前者の場合1日以下の場合にはリフレッシュを実行し、後者の場合10分を下回ってる場合にはリフレッシュを実行します。
  • それぞれ、renewToken関数を利用してるので、access_tokenが取得できたらcallbackし、そうでない場合falseを返すようにしています。
  • プログラム側から自動で更新作業を行うので、前項のrenewToken関数内のダイアログを表示するコードは削除しています。

ファイルをアップロードする

ファイルのアップロードは、嵌りポイントがいくつかありました。それらをクリアしつつ、Access Tokenの期限をチェックしつつAPIへZIPファイルを送りつけます。これに前回のコードのCronで自動で処理を行う部分で呼び出してあげれば、起動している間は自動で対象のフォルダを無限にBoxへバックアップし続けてくれます。

※Box APIのアップロードは50MBの制限があります。50MB以上のファイルの場合には分割アップロードのAPIを利用する必要があります。

//アップロード用エンドポイント
var upref = "https://upload.box.com/api/2.0/files/content";

//Box APIでアップロード
function boxupload(zippath,callback){
  //Tokenのexpireチェックする
  chkexpireToken(function(ret){
    if(ret==false){
      //ダイアログオプション
      var options ={
        type:'info',
        title:"エラー",
        button:['OK'],
        message:'認証エラーが発生しました',
        detail:"Refresh Tokenの期限切れ。再認証が必要です。"
      }

      dialog.showMessageBox(null,options);

      //処理を中止
      return;
    }else{
      //Access Tokenを取得
      let accessToken = ret;

      //BoxのフォルダIDを取得
      let folderid = store.get("boxid");

      //リクエストを作成
      let header = {
        'Authorization': 'Bearer ' + accessToken,
        "Content-type": "multipart/form-data",
      }

      var r = proxy.post({
          url: upref,
          headers: header,
        }, function (err, response, body) {
          //ステータスコードを取得
          let statuscode = response.statusCode;

          if(statuscode == 201 || statuscode == 200){
            //アップロード成功
            callback(true)
          }else{
            //エラーログを書き出し
            fs.writeFileSync("error.json", JSON.stringify(response));
            callback(false)
          }
        });
      
      //ファイルパスからファイル名だけ取り出す
      let filename = path.basename(zippath);

      //ファイルのアップロード
      var form = r.form();
      form.append('folder_id', folderid);
      form.append('filename', fs.createReadStream(zippath), {filename: filename});
    }
  });
}
  • upload用のエンドポイントは、公式サイトにあるURLだと404になります。https://upload.box.com/api/2.0/files/contentですのでご注意
  • renewToken関数にあったエラーダイアログの表示は、こちらのboxupload関数の側に配置しました。
  • chkexpireToken関数でTokenの期限切れチェックを行い、利用可能なAccess Tokenを取得してproxy.post(request)で投げています。
  • 無事に成功すると、statuscodeに201が返ってくるので、関数呼び出し元へtrueをcallbackします。
  • エラー時にはresponseの内容をerror.jsonに書き出します。
  • requestのコードを組み立てたら、それをもってform.appendでパラメータを送りリクエストを実行しています。
  • VBAの時のような面倒なコードはなく、スッキリとしたコードでバイナリデータを送り込むことが可能です。
  • ファイルのフルパスからファイル名path.basenameで切り出して、アップロード時のファイル名としています。

図:無事にZIPをアップロード出来た

Cronで自動アップと後処理

前項までのコードだと、どんどんデスクトップに圧縮されたZIPファイルが溜まり続けてしまいます。また、Cronでこのboxuploadを呼び出すコードがまだ装備されていません。この部分を完成させて完了となります。まずは、ZIP作成とアップロードをまとめて行う関数を用意してまとめておきます。

//ファイルの圧縮とアップロード
function uploadfolder(callback){
  //ZIPで圧縮
  zipman(function(ret){
    //ZIPファイルのフルパスを取得する
    var fullpath = ret;

    //ファイル名を受け取ったら次の作業へ
    boxupload(fullpath,function(ret2){
      //ret2の内容で処理を分岐
      if(ret2==false){
        //エラー発生
        //ダイアログオプション
        var options ={
          type:'error',
          title:"エラー",
          button:['OK'],
          message:'アップロードができませんでした',
          detail:"Boxに対象のファイルをアップロードできませんでした"
        }

        dialog.showMessageBox(null,options);

        //zipファイルを削除する
        fs.unlinkSync(fullpath);

        //callbackする
        callback(false);
      }else{
        //無事にアップロード成功
        console.log("Upload Successfully!!")

        //zipファイルを削除する
        fs.unlinkSync(fullpath);

        //callbackする
        callback(true);
      }
    });
  })
}
  • fs.unlinkSyncにてアップロード処理後にZIPファイルを自動削除させています。
  • zipman関数の結果をboxupload関数に渡しています。boxupload内ではrenewTokenや実際にファイルをアップロードする処理がなされています。
  • callbackの引数で呼び出し元でダイアログなどを表示すると良いでしょう

そして、Cronjobの設定部分は以下のように書き直します。

//cronJob設定を行う
function setCronJob(timeval){
  if(jobs == "" || jobs == undefined){
    //既存のジョブ設定がないので、何もしない
  }else{
    //既存のジョブ設定を削除する
    jobs.destroy();
  }
 
  //crontimeを設定する(毎時
  var cronTime = "0 0 */" + Number(timeval) + " * * *";
 
  //引数の分を元に、分単位トリガーを設置する
  jobs = CronJob.schedule(cronTime, () => {
    uploadfolder(function(ret){
      console.log(ret);
    });
  });
 
  console.log(timeval + "hour CronJob Setting Up");
}
  • CronJob.scheduleにて、uploadfolderを呼び出しています。callbackの値であるretで処理を追加すると尚良いでしょう
  • ただし、自動処理の時にはエラー時以外メッセージダイアログなどを出すのは適切ではないので、ログ出力程度に留めておくと良いでしょう。

パッケージを作る

現在はelectron-builderを使うのが主流になっているのですが、今回はelectron-packagerを使うことにします。まずはインストールします。

npm install electron-packager -g

今回プロジェクトはboxmanというフォルダ内に構築しています。このプロジェクトからexeを作成するのがこのパッケージャの役割ですが、色々とオプションがついており、コマンドラインから指定してパッケージングします。boxmanフォルダがあるフォルダまで移動し(boxmanフォルダには入らず)、以下のコマンドを実行すると、electron 5.0で64bit用のWindows向けパッケージが作成されます。exeのアイコンも指定しています。

electron-packager boxman --platform=win32 --arch=x64 --electronVersion=5.0.0 --icon=boxman/img/naruto.ico

実行すると、electronが別途ダウンロードされ、ビルドが始まります(この時プロキシに阻まれることがあるので、前回の記事を参照しプロキシ越えできるようにしておく必要がある)。完了すると、boxman-win32-x64というフォルダに色々パッケージとexeが入っています。

resources\app\の中に自分の作ったファイル群がいます。しかし今回問題が。。

node_modulesの中身の一部が取り込まれていない・・・よってexeを実行しても動かない。という事で、プロジェクト側にあるnode_modulesフォルダまるごと上書きしてみたところ無事に起動。electron-builderを使えというお告げなのかもしれません。配布用のインストーラはEXEPressInno Setupで別途作ると良いでしょう。

毎回ビルドをするたびにコマンドライン打つのは面倒なので、BATファイル作って置くと捗ります。

図:boxman.exeが本体です

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)