まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2021/05

タイトルの通りです。obniz に赤外線人感センサー(HC-SR501)をつなげて人の存在を確認し、なんらかの変化があった場合にその結果をクラウド上の Google スプレッドシートに送信する、という仕組みを作ってみました。必要な設定やコードはこちらからも公開しています:
https://github.com/dotnsf/obniz_sensor_gas


Google スプレッドシートを複数人で共有しておけば人感センサーのセンシング結果を複数の人で共有することができます。今回の例ではそこまで実装していませんが、センシング結果にセンサーの ID を含めるようにすれば、複数のセンサーからの結果を一枚のスプレッドシートにまとめることもでき、またスプレッドシート側でピボットして確認したり、グラフ化などの視覚化も容易にできるようになります。

この仕組みはもともとこちらの動画で紹介されていたものを参考にしています。この動画ではラズベリーパイと HC-SR501 を接続して、ラズベリーパイのコマンドでセンシングを実現しています。また最終的にはそのコマンドを cron に登録する形で永続処理化まで実現されているようでした:
RaspberryPIでIoT簡単入門!クラウド対応人数カウントを作る!

2021052502


これを参考にラズベリーパイを obniz に置き換えて GPIO に接続し、ラズベリーパイのコマンドではなく、obniz のウェブページの JavaScript でセンシングし、その結果を Google スプレッドシートに向けて JavaScript で(jQuery で) POST する、となるように書き換えました。

以下、その準備段階も含めた実現の手順を紹介します(ハードウェアは持っていない場合は購入の必要があります)。

【入手するハードウェア】
obniz 本体
人感センサー(HC-SR501)
ジャンパケーブル(オス-メス)3本

【用意するソフトウェア】
・Google スプレッドシート(Google Drive から作成)


【準備作業】
人感センサーによるセンシングを行うための準備作業は以下の大きく以下3つの作業を行います:
1 スプレッドシート側の準備
2 obniz と HC-SR501 の接続
3 センシングを実行する HTML / Javascript コードの用意


1 スプレッドシート側の準備

まず Google スプレッドシートを1つ新規に作成します:
2021052601


スプレッドシートのメニューから ツール - スクリプトエディタ を選択します:
2021052602


コード画面が表示されるので、デフォルトで用意されているスクリプトをすべて消し、代わりに以下のコード(後述のデータ送信を受けて、シート内に日付時刻と送信データ内容を追加するコード)をコピー&ペーストします:
function doPost(e) {
  SpreadsheetApp.getActive().getActiveSheet().appendRow(
    [new Date(),e.postData.contents]
  );
  
  var output = ContentService.createTextOutput("ok");
  output.setMimeType(ContentService.MimeType.TEXT);
  return output;
}
2021052603



メニューから 公開 - ウェブアプリケーションとして導入 を選択します:
2021052604


プロジェクト名を聞かれたら適当に入力して OK をクリックしてください:
2021052605


続けてデプロイ設定画面になります。バージョンは "New" の "1" 、アプリケーションの実行者は自分のメールアドレスを指定、そしてアクセスできる人として "Anyone, even anonymous" を選択して deploy ボタンをクリックします:
2021052606


アプリケーションの認証が必要なので「許可を確認」をクリックします:
2021052607


ちょっとびっくりするような画面になりますがエラーではないので続けます。詳細を表示して、安全ではないページに移動します:
2021052608


作成したプロジェクトにアクセスの許可を与えるため、「許可」ボタンをクリックします:
2021052601


デプロイが行われ、最後に URL が含まれる画面になります。この URL はこの後必要になるので、メモしておきます。OK を押してダイアログを閉じます:
2021052602


これでスプレッドシートの最低限の準備はできました。必要であればスプレッドシートの名称を変更したり、他の人と共有しておいてください:
2021052603


2 obniz と HC-SR501 の接続

まず以下の作業をする前に obniz の電源を OFF にしてください。以下の作業は obniz の電源が OFF になっている状態で行ってください。

HC-SR501 には3本の接続コネクタピンがあります。接続コネクタ部分が左側にくるように向きを調整した時のピンを上から0、1、2とみなします。また obniz も同様に GPIO が下側にくるように向きを調整した時のコネクタを左から0、1、2、・・・とみなすことにします:
2021052601


この状態でジャンパケーブルを使って、0と0、1と1、2と2をそれぞれ接続します。ここまでできると写真のような状態になります:
D95C6870-EBAC-4D5B-88BB-F8F5F48F9D7F


これで obniz と HC-SR501 が接続できました。この後、obniz の電源を ON にすると HC-SR501 でセンシングできるようになり、センシングした結果を obniz から取得することができるようになりました。


3 センシングを実行する HTML / Javascript コードの用意

最後に実際にセンサーで人の存在を確認し、その結果を取得して、(上述で準備した)スプレッドシートに記録する、というプログラムを作ります。

まず obniz の開発者コンソールにログインします:
https://obniz.io/ja/console


画面左の「リポジトリ」から「新規作成」を選択します:
2021052604


新しいプログラムのダイアログが表示されます。タイプは「WebApp」、アクセスレベルは「公開」を選択し、適当なファイル名を付けて「作成」します:
2021052605


サンプルページの HTML がエディタ内に表示されます:
2021052606


この HTML をすべて消して、以下の内容をコピー&ペーストします:
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://unpkg.com/obniz@3.8.0/obniz.js"></script>
  </head>
  <body>
    <h1>人感センサー</h1>
    <div id="obniz-debug"></div>

    <script>
      var obniz = new Obniz("OBNIZ-ID");
      var sensor = null;
      const url = "https://script.google.com/macros/s/xxxxxxxxxx-xxxxxxxxxx/exec";//GASのウェブアプリケーションのURLを指定してください

      obniz.onconnect = async function () {
        sensor = obniz.wired( "Keyestudio_PIR", { signal:1, vcc:0, gnd:2 } );
        console.log( sensor );
        sensor.onchange = function( v ){
          console.log( v );
          $.post( url, { value: v } )
            .done( function( data ){ console.dir( data ); } );
        };
      };
    </script>
  </body>
</html>
2021052607


12 行目と 14 行目は変更が必要です。12 行目の new Obniz( "OBNIZ-ID" ); となっている OBNIZ-ID の部分は自分が使っている obniz の ID (電源を入れた時に画面に表示される nnnn-nnnn 形式の8桁数字)に置き換えてください。 また 14 行目の url の値は上述の作業1の最後、スプレッドシートに権限を与えた際に取得した URL 文字列に置き換えてください。

なお、17 行目で obniz と人感センサーをソフトウェア的に接続しています。上述の 0-0, 1-1, 2-2 と物理的に繋いだ作業の意味(0 が vcc 、1 がシグナル、2 がアース)を指定しています。

ここまでの変更ができたら準備はすべて完了です。


【センシング実行】
まず obniz と、obniz に接続された HC-SR501 に電源を入れます。obniz にマイクロ USB ケーブルを接続して、WiFi に接続してください。成功すると以下のように obniz ID とその QR コードが表示される画面になります:
2021052601


そして obniz コンソール画面右上の「実行」ボタンをクリックします:
2021052607


すると以下のような画面になります。緑の帯が出ていれば obniz は WiFi を経由してインターネット接続ができていることを意味しています(緑にならず、赤くなっている場合は WiFi 接続に失敗しているなど、インターネットに接続できていないことを意味しているので修正が必要です):
2021052602


この時に人感センサー近くで人が行ったり来たりして、人が存在していたりいなかったりするような状況になると、その変化を人感センサーが感知してスプレッドシートにその変化の様子が送信されます。スプレッドシートは以下のような感じになり、変化を検知する度に一行ずつ増えていきます:
2021052603

↑左が検知の日付時刻、右は value=true が人を感知した時、value=false は検知していた人がいなくなったことを検知したことを意味しています。

なお、このスプレッドシートは手動で情報を消さない限りはずっと情報が「追加」されます。一度リセットしたい場合は該当シート内に書かれた部分を手動で全選択して DELETE してください。


動作確認ができたら、いったん「終了」ボタンを押してセンシングを終了します:
2021052602


【センシングの自動化設定】
とりあえず人感センサーとしての挙動は確認できました。最後にこの仕組を(わざわざウェブページを開いて「実行」ボタンを押さなくても、電源に接続しただけで実行できるように)自動実行する方法を紹介します。

改めて obniz 開発者コンソールに戻り、左側のメニューから「サーバーレスイベント」を選んで「新規作成」します:
2021052601


適当な名前(図では「人感センサー」)を入力し、「トリガー」には obniz Hardware Event を選択後に対象 obniz id を選択して online イベントを、「実行するアプリケーション」にはリポジトリ内の HTML を選択後に先程作成した HTML を、それぞれ選択します。最後に「作成」ボタンをクリックします:
2021052602


これで対象 obniz がオンラインになったタイミングで対象 HTML ページが実行されるので、自動起動に近い処理が実現できました。いったん obniz の電源を OFF にしてから ON にするなどして、再度センシングできているかどうか(スプレッドシートに情報が追加されるかどうか)を確認してください:
2021052603


肝心のセンサー精度などはまだちゃんと測定できていない(苦笑)のと、実際には HC-SR501 についているネジでそのあたりの調整もできるようですがまだしていない(苦笑)ので、実際のところどの程度現実的に役立つものなのか、まだわかっていないところもありますが、(ラズベリーパイや)obniz があるとこんなことも簡単に作れちゃうんですね。単にセンシングするだけでなく、取得した内容をインターネット上に記録して共有するところまでできている点がポイント高いと思っています。




「LINE 手描きスタンプ」「お絵描き共有サービス」「お絵描き Slack」などのお絵描き系(そんな系あるのか?)ウェブアプリを複数開発・運用しています。

これらのウェブアプリで使っている「お絵描きパネル」 UI 部分の作りはほぼ共通です。細かいことを言い出すと i18n 対応の有無とか、履歴呼び出しの有無とか、各アプリ毎に固有の情報を保存したりしなかったりしているので UI 部分含めて全くの共通ではないのですが、でも共通パーツが多いのも事実です。

今後も新しいアプリでこの「お絵描きパネル」を使うことも何度かあるだろう、というわけで、今後のアプリ開発を楽にする目的でパネル部分のモジュール化を地味に続けていました。とりあえず公開してもいいかな、というレベルになったので公開します。

ソースコードはこちらです:
https://github.com/dotnsf/doodlejs

実際に動くサンプルはこちら(スマホか PC のウェブブラウザでアクセスしてください):
https://dotnsf.github.io/doodlejs/

2021052501


実際の UI 含めた挙動は動くサンプルのページで確認してください。2021/05/24 時点では以下のような機能を持っています:
・PC のマウスやトラックボール/Windows タッチパネル/スマホのタッチでの描画に対応
・i18n は無し(画面は日本語のみ)
・線の色、太さを指定して描画する。指定した色を背景色に指定することもできる。
・アンドゥ・リドゥ・リセット可
・「送信」ボタンを押すと、描画内容を画像化し、エンコードした結果を console.log() で書き出す(スマホだと確認できません、ごめんなさい)
・「送信」ボタンを押した時の挙動はカスタマイズ化。実際に描画データをサーバー側へ送ってバックエンドに保存する、といったこともできる(ここまで含めてサンプルコードあり)


↑特に最後のカスタマイズ機能の目処がたったので公開を決断しました。サンプルのページだけでもお絵描きの描画は体験できます。が、実際にこのモジュールを組み込んで作るアプリでは描いた絵を保存する機能も必須だと思っています。一方、Github ページでサンプル公開しているとその部分の実現が難しく、保存するようなカスタマイズができる必要性も考慮した上で試行錯誤した結果、まあまあいい感じのバランスが取れた状態で公開しています。


【使い方】
自分のウェブアプリに組み込んで使う場合は以下の作業を行ってください:

①ソースコードを git clone またはダウンロード

フロントエンドで最低限必要なファイルは doodle.js ファイルと、サンプルページである index.html の2つです。

バックエンド側のカスタマイズも行う場合は、そのサンプルとなっている app.js ファイルも必要です。

②サンプルを元にウェブページを作成

フロントエンドのサンプル UI となる index.html を参考に(あるいはこのまま使って)ウェブページを用意します。同ウェブページには以下の要素が含まれている必要があります(サンプルの index.html にはすべて含まれています):

・お絵描き用のキャンバスを含むことになる <div id="cdiv"></div>
・色選択パーツの <select id="select_color"> ~ </select>
・線の太さ選択パーツの <select id="select_linewidth"> ~ </select>
・背景色変更ボタン <button onClick="setBG();"> ~ </button>
・アンドゥボタン <button onClick="undo();"> ~ </button>
・リドゥボタン <button onClick="redo();"> ~ </button>
・リセットボタン <button onClick="resetCanvas();"> ~ </button>
・送信ボタン <button onClick="sendCanvas();"> ~ </button>

ウェブページが用意できたら、画面ロード後にそれぞれの id 値を指定して以下の JavaScript を実行します(サンプルの index.html には既に含まれています):
$( function(){
  $('#cdiv').doodlejs({
    select_color: 'select_color',
    select_linewidth: 'select_linewidth',
    undo_btn: 'undo_btn',
    redo_btn: 'redo_btn',
    setbg_btn: 'setbg_btn'
  });
});

これで用意されたパーツ要素を使って、指定された <div id="cdiv"></div> 内に連動して動く Canvas が生成されます。

③「送信」ボタンのカスタマイズ

お絵描き後に「送信」ボタンを押した時の挙動をカスタマイズできます。上記サンプルでは単にお絵描き内容を画像化→エンコードして、結果のテキストを console.log() で出力しているだけですが、バックエンドに送信するようなケースを想定してカスタマイズできるようにしています。

送信ボタンをクリックした時の挙動をカスタマイズするには DOODLEJS.prototype.postCanvas() 関数を上書きします。この関数はお絵描き内容を画像化(png 化)したデータを引数に実行されます。プロトタイプでは以下の内容になっているので、その画像をエンコードして console.log() に出力しています:
(カスタマイズ前の処理)
DOODLEJS.prototype.postCanvas = function( png ){
  console.log( 'png', png );
};

例えば画像データをサーバーに送る場合は以下のような内容にプロトタイプを上書きします:
DOODLEJS.prototype.postCanvas = function( png ){
  //. バイナリ変換
  var bin = atob( png );
  var buffer = new Uint8Array( bin.length );
  for( var i = 0; i < bin.length; i ++ ){
    buffer[i] = bin.charCodeAt( i );
  }
  var blob = new Blob( [buffer.buffer], {
    type: 'image/png'
  });

  //. フォームにして送信
  console.log( 'Sending data... : ' + blob.size );
  var formData = new FormData();
  formData.append( 'image', blob ); 
  formData.append( 'timestamp', ( new Date() ).getTime() );

  $.ajax({
    type: 'POST',
    url: '/image',
    data: formData,
    contentType: false,
    processData: false,
    success: function( data, dataType ){
      console.log( data );
    },
    error: function( jqXHR, textStatus, errorThrown ){
      console.log( textStatus + ': ' + errorThrown );
    }
  });
};

これで画像データが POST /image という REST API でポストされることになります。後はバックエンド側でこのルーティングを処理するような REST API をバックエンドに用意し、送られてくる画像データを解析するなり、保存するなり、、といった処理を実装することになります(以下は app.js に含まれるサンプルで、特に保存せずに送信された画像の情報を返すだけの内容です):
app.post( '/image', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  if( req.file ){
    var imgpath = req.file.path;
    var imgtype = req.file.mimetype;
    var imgsize = req.file.size;
    //var imgfilename = req.file.filename;
    //var filename = req.file.originalname;

    var timestamp = parseInt( req.body.timestamp );

    var img = fs.readFileSync( imgpath );
    var img64 = new Buffer( img ).toString( 'base64' );
    fs.unlink( imgpath, function( err ){} );

    var params = {
      path: imgpath,
      type: imgtype,
      size: imgsize,
      timestamp: timestamp
    };
    console.log( params );
    var p = JSON.stringify( params, null, 2 );
    res.write( p );
    res.end();
  }else{
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: 'not initialized.' } ) );
    res.end();
  }
});

ウェブ画面内にマウスや指でお絵描きできるパーツ部品を組み込んで、送信後のデータ処理までカスタマイズするのに便利だと思っています。よかったら使ってください。


 

このブログエントリを書くきっかけとなったのはこの記事でした:
2021年5月のWebサーバ利用シェア、「Nginx」が「Apache」を初めて上回る


ウェブサーバー(HTTP サーバー)のシェア争いで、ついに Nginx が Apache を抜いて首位にたった、というものです。この2つの合計だけで全体の 70% 前後あるという「2トップ」ですが、少しずつ差を縮めた結果、5月になってわずかながら Nginx のシェアが Apache を上回ったようでした。

・・・で、その結果は置いといて(苦笑)、自分自身が気になったのは順位では6位に位置づけられている "Node.js" です:



Nginx や Apache は、例えば WordPress や Drupal のような有名な CMS のウェブサーバーとしても使われていたりするし、他にも Ruby on Rails を使う場合のウェブサーバーとして利用されるので、多くのプログラミング言語/コンテンツ管理システムのウェブサーバーとして採用されていることがシェアが高い背景として想像できます。

その観点で見た場合、6位の "Node.js" は異色といえます。なぜならば Node.js はあくまでプログラミング言語であって、これ自体はウェブサーバーではないからです。統計としては「Node.js で作ったウェブサイトのページ」をカウントしているのだと想像できるし、(Node.js がウェブサーバーではない以上)他に分類のしようがないことも理由だと思いますが、いろんな言語で作られたページのウェブサーバーである Nginx や Apache がランクインしている中では「違うカテゴリーの人が混ざってる」感は否めません。

そして最も気になったのは「そもそも、どうやって Node.js で作ったページだとわかったのか?」でした。Nginx や Apache だとわざと 404 エラーを出してそのエラー画面から調べる、という方法もあるのですが、Node.js では(特に明示しない限り)味気ない 404 エラーがでるだけなので調べようがないはずです。どうやって Node.js ウェブページのシェアを数えたのだろう? という興味が湧いて調べてみたのでした。

結論としてはごくシンプルで、404 エラーを出さなくても HTTP レスポンスヘッダの中に答がありました:
2021052501


Node.js でウェブアプリを作る際にほぼ 100% 使われるであろう Express ライブラリを使ってアプリを作って起動し、そこに curl コマンドを HTTP レスポンスヘッダのみ返すようなオプションをつけて実行してリクエストとレスポンスのヘッダ情報も確認しました。するとレスポンスヘッダの中に "X-Powered-By: Express" という項目がありました。なるほど、「この情報が含まれるページは Node.js 製」と判断したんだろうな。。


で、この情報がわかると、同様にウェブサーバーを併用しない開発言語である Python はどうなっているんだろう? と気になりました。ウェブサーバーランキングには入ってないけど、Python で作ったウェブページかどうかも Node.js と同様にして調べることができるのだろうか・・・と。

で、こちらも Flask ライブラリを使ってアプリを起動し、同様に curl コマンドで HTTP レスポンスヘッダを取得するモードで実行してヘッダを確認すると・・・ こちらはレスポンスヘッダの中に "Server: Werkzeug/0.14.1 Python/3.6.7" というわかりやすい項目がありました。Python の場合は「ウェブページを作る際のライブラリはほぼ 100% Flask」とは言い切れない所がありますが、まあでも同様に調べることはできそうですね。
2021052502


(おまけ)ところで、このブログエントリの中でも使っているのですが、「curl コマンドで HTTP レスポンスヘッダのみ調べる」方法を調べました。複数のオプションを組み合わせる形で実現しているのですが、結論としてはこんな感じでした:
$ curl -D - -s -o /dev/null http://localhost:nnnn/

3つのオプションを組みあわせて実現しています:
  • -D - : dump header オプション、標準出力にヘッダを出力する
  • -s : slient オプション、途中経過を出力しない
  • -o /dev/null : output オプション、(ヘッダ以外の)本体を /dev/null へ出力して廃棄


【参照】
https://qiita.com/yousan/items/fcc15e1046939c465ab7

IBM Cloud から提供されている Db2 on Cloud を、2021/05/23 時点では最新となる v4 API を使ってアクセスするまでを実現した Node.js のサンプルアプリケーションを作りました。アクセストークンを取得して SQL を実行し、その結果を取得するまでの一連の流れを紹介します。


Db2 on Cloud は IBM 製リレーショナルデータベース製品である Db2 をマネージドサービスとして IBM Cloud から提供しているサービスです。IBM Cloud のライトアカウントを作成し、フリープランを選択することで、データは 200MB までなどの制約はありますが無料で利用することも可能です:
2021052301


この Db2 は製品としての経緯もあり、専用(ネイティブ)クライアントライブラリをインストールした上で JDBC/ODBC などから利用することが多かったのですが、近年は Node.js 向けのライブラリなども提供されるようになり、各種プログラミング言語からの利用もできるようになっていました。その API の最新版(v4)では REST API 対応が行われ、(ラズベリーパイなど)専用クライアントライブラリが存在しなかった環境からの利用も可能になりました。

というわけで、今回のブログエントリではこの v4 API を使って Db2 on Cloud のインスタンスにアクセスして SQL を実行し、その実行結果を取得するまでの一連の手順をサンプルアプリケーションとそのコードを参照しつつ紹介します。


【サンプルアプリケーション】
以下で紹介するサンプルアプリケーションの(Node.js 向け)ソースコードはこちらで公開しています:
https://github.com/dotnsf/node-db2


Node.js がインストールされていれば Windows でも Mac でも Linux でも動きます。特にこのアプリケーションは v4 API で作っているので、これまでは(クライアントライブラリが提供されていないため)Db2 にアクセスできなかったラズベリーパイなどの環境からでも利用できる点が特徴になっています。なのでラズパイ環境をお持ちだったらぜひラズパイから利用してみていただきたいです。

サンプルアプリを利用するには、まず IBM Cloud 側の準備が必要です。大まかに以下2段階の準備を行います:
(1)IBM Cloud にログインして Db2 on Cloud サービスインスタンスを作成
(2)作成したインスタンスの接続情報を取得


まず(1)を行います。IBM Cloud に(必要であればアカウントを作成して)ログインし、Db2 サービスインスタンスを作成します:
2021052302


この際に "Lite" プランを選択しておくとデータ量 200MB 上限や、同時接続数 15 などの制約がありますが、無料で利用することができます:
2021052303


(1)のインスタンスを作成したら(2)の準備を行います。作成したインスタンスの「サービス資格情報」タブを選択し、「新規資格情報」ボタンで「サービス資格情報」を1つ追加して、その中身を確認します:
2021052304


以下のような JSON フォーマットの情報を確認することができます:
{
  "db": "BLUDB",
  "dsn": "DATABASE=BLUDB;HOSTNAME=dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net;PORT=50000;PROTOCOL=TCPIP;UID=username;PWD=password;",
  "host": "dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "hostname": "dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "https_url": "https://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net",
  "jdbcurl": "jdbc:db2://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50000/BLUDB",
  "parameters": {
    "role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager"
  },
  "password": "password",
  "port": 50000,
  "ssldsn": "DATABASE=BLUDB;HOSTNAME=dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net;PORT=50001;PROTOCOL=TCPIP;UID=username;PWD=password;Security=SSL;",
  "ssljdbcurl": "jdbc:db2://dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50001/BLUDB:sslConnection=true;",
  "uri": "db2://username:password@dashdb-xxx-xxxxx-xxxxxx.services.dal.bluemix.net:50000/BLUDB",
  "username": "username"
}

ここで必要になるのは以下の3つの値です:

username
password
hostname


加えて deployment id を取得します。この ID はサービス固有の ID で、すべての REST API 実行時に必要な値です。取得方法はサービスを表示しているブラウザ画面の URL を確認します:
2021052305


おそらくこのようなフォーマットの URL 文字列になっています:
https://cloud.ibm.com/services/dashdb-for-transactions/crn%3Av1%3Abluemix%3Apublic%3Adashdb-for-transactions%3Aus-south%3Aa%2Fxxxxxxxxxxxx%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%3A%3A?paneId=manage

この中の "crn" で始まる文字列(? が含まれる場合はその手前まで)を URL デコードした値が deployment id です。URL デコードといってもパターンは2つに決まっていて、"%3A" を ":" に、"%2F" を "/" に変換するだけです。例えば該当の Db2 on Cloud サービスを表示している時の URL 内文字列がこの内容だった場合、
https://cloud.ibm.com/services/dashdb-for-transactions/crn%3Av1%3Abluemix%3Apublic%3Adashdb-for-transactions%3Aus-south%3Aa%2Fxxxxxxxxxxxx%3Axxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx%3A%3A?paneId=manage

deployment id は crn で始まる青字部分を URL デコードした値ということになり、具体的には以下の値となります:
crn:3v1:bluemix:public:dashdb-for-transactions:us-south:a/xxxxxxxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx::

以上、ここまでの作業で Db2 on Cloud のサービスインスタンスを作成し、その username, password, hostname, deployment id の4つの情報を取得することができました。この4つの値が取得できれば、実際にサンプルアプリケーションを動かすことができます。


上述のリポジトリ URL から git clone やダウンロード&展開するなどしてソースコード一式を取得します。そして settings.js をテキストエディタで開いて、先程取得した4つの値を入力して保存します:
2021052306


これでソースコード側に必要な変更も完了です。では実際に Node.js で動かしてみましょう:
$ cd node-db2

$ npm install

$ node app

ここまでのコマンドを実行すると、サンプルのウェブアプリケーションは 8080 番ポートで待ち受ける形で起動します。ウェブブラウザ(別 PC からでも可)からサンプルの HTTP リクエストを送信して、Db2 on Cloud で SQL を実行し、その結果を取得します:
http://(上記の $ node app を実行したマシンの IP アドレス):8080/ping

なお、この GET /ping リクエストは Db2 on Cloud インスタンスに対して以下の SQL を実行した結果のシステム情報を取得するように作られたものです:
select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;

実際にウェブブラウザでこの URL にアクセスするとこのような結果になります。上述の SQL を実行した結果が JSON で返ってきました。仮にアプリケーションをラズベリーパイ上で動かしていても同様の結果が取得できているはずです。これまでは Db2 のクライアントライブラリが導入できる環境からでないと実行できなかった SQL を、v4 API では(サーバー上のもの以外には)クライアントライブラリを使わない環境でも実行して結果を取得することができる、ということがわかります:
2021052307


【ソースコード解説】
ではソースコードを見ながら v4 API の解説をします。上述のオペレーションで使っているファイルは変数設定以外は app.js のみなので、全容を確認したい場合はこのファイルを参照ください。

上述で行ったのは1つの SQL を実行して、その結果を取得する、というごく普通のオペレーションですが、v4 API では大きく3つのパートに分かれて処理されています((2)と(3)が別れて処理されている点に注目):

(1)アクセストークン取得
(2)SQL 実行
(3)SQL 結果取得


まず(1)について。これまでの Db2 API では認証時にユーザー名とパスワードがあればログインして SQL を実行することができましたが、v4 API では(今どきらしく)まずアクセストークンを取得して、アクセストークンを付与しながら SQL を実行する必要があります。というわけでアクセストークンの取得手順を紹介します。

具体的には以下のようなコードを実行しているのですが、settings.js に設定した username と password を https://(hostname)/dbapi/v4/auth/tokens に POST しています。またその際の HTTP リクエストヘッダ内で deployment id の値を送付しています(HTTP リクエストヘッダに deployment id を含める、というのは以下すべての REST API で共通です)。成功した場合、返されるオブジェクトの token キーにアクセストークンが付与されて返ってくるので、この値を保存しています(実際のアプリではセッション等に保存しておくべきです):
var access_token = null;
var option = {
  url: 'https://' + settings.hostname + '/dbapi/v4/auth/tokens',
  json: { userid: settings.username, password: settings.password },
  headers: { 'content-type': 'application/json', 'x-deployment-id': settings.deployment_id },
  method: 'POST'
};
request( option, async function( err, res0, body ){
  if( err ){
    console.log( { err } );
  }else{
    if( body && body.token ){
      access_token = body.token;
      console.log( { access_token } );
    }
  }
});


GET /ping にリクエストがあると、このアクセストークンを使って(2)SQL を実行します。app.js では以下のように実行しています:
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  if( access_token ){
    var sql0 = 'select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;';
    var option0 = {
      url: 'https://' + settings.hostname + '/dbapi/v4/sql_jobs',
      json: { commands: sql0, limit: 10, separator: ';', stop_on_error: 'no' },
      headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + access_token, 'x-deployment-id': settings.deployment_id },
      method: 'POST'
    };
    request( option0, async function( err0, res0, body0 ){
      if( err0 ){
        console.log( { err0 } );
        res.status( 400 );
        res.write( JSON.stringify( { error: err0 }, null, 2 ) );
        res.end();
      }else{
        if( body0 && body0.id ){
            :
            :

GET /ping リクエストへのハンドラとして、まず "select TABNAME, TABSCHEMA, OWNER from syscat.tables fetch first 5 rows only;" という SQL を https://(hostname)/dbapi/v4/sql_jobs に POST して実行しています。この処理が成功すると、返されるオブジェクトに id というキー値が含まれています。

そして(3)では、実行した SQL の結果を取得します。その際には(2)で取得した id キー値を指定して GET https://(hostname)/dbapi/v4/sql_jobs/(id) を実行して、その結果が先程のブラウザ画面のようなフォーマットで取得されていたのでした:
            :
            :
        if( body0 && body0.id ){
          console.log( { body0 } );
          var option1 = {
            url: 'https://' + settings.hostname + '/dbapi/v4/sql_jobs/' + body0.id,
            headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + access_token, 'x-deployment-id': settings.deployment_id },
            method: 'GET'
          };
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              res.status( 400 );
              res.write( JSON.stringify( { error: err1 }, null, 2 ) );
              res.end();
            }else{
              //. body1 は string
              body1 = JSON.parse( body1 );
              console.log( { body1 } );
              res.write( JSON.stringify( body1, null, 2 ) );
              res.end();
            }
          });
        }else{
          res.status( 400 );
          res.write( JSON.stringify( { error: body0 }, null, 2 ) );
          res.end();
        }
      }
    });
  }else{
    res.status( 400 );
    res.write( JSON.stringify( { error: 'no access_token' }, null, 2 ) );
    res.end();
  }
});


NoSQL とは違って、トランザクションを含む(時間のかかる)処理を実行する可能性もあるため、その間で処理をブロックしないようにこのような仕様で API が用意されているのだと思います。上の例では SQL の実行と結果の取得を続けて行って実行結果を返していますが、(途中に取得できる id をいったんクライアントに返すことで)これら2つの処理を別の REST API で分けて実行させることも可能になります。シングルスレッドの Node.js での実装を考慮すると、こちらのほうがより正しいといえるかもしれません。


【良い点・悪い点】
なんといっても「ラズベリーパイなどの、Db2 クライアントライブラリが提供されていないシステムから Db2 サーバーに接続してクエリーを実行できる」ようになったことが大きな改善点であると感じました。また時間のかかる処理を実行する際にも、まとめて実行して、その間の他のリクエストをブロックすることなく、後から結果を確認できるようになっている点もクラウド時代らしいといえます。

唯一残念な点は「プレースホルダー型の SQL に未対応」な点です。SQL を比較的安全に実行する上でよく使われるプレースホルダー型の SQL("SELECT * from xxxxx where id = ?" で、パラメーターの id を分けて送って実行する SQL)には現時点ではこの REST API では未対応のようで、現時点では開発者が気をつけて実装するか、(クライアントライブラリが提供されているシステムから)従来の専用ライブラリを使って実行する必要がありそうでした。


【参考】
IBM Db2 on Cloud REST API


このブログエントリは以下の続きです:
Github API を使う(1)
Github API を使う(2)

過去2つのエントリで Github API の基本的な内容として OAuth による認証を行ってアクセストークンを取得し、そのアクセストークンを使って(ログインしたユーザーの権限で)「自分自身の情報を取得する」、「特定リポジトリの特定ブランチに含まれるファイル一覧を取得する」という2つの Github API の実装内容をサンプルソースコードと合わせて紹介しました。

シリーズ最終回の今回はこれまでの内容に加えて以下の機能を Github API で実装して、このシリーズ当初の目的であった「Github ベースのファイルサーバー」っぽいものを作ってみます:
・ブランチを作成する/マージする
・ブランチにファイルをコミットする


実際に作ってみたものを先に紹介しておきます。ソースコードはこちらから参照してください:
https://github.com/dotnsf/githubapi

2021051901


これまでのサンプルと同様に git clone などでソースコード取得後、 settings.js に OAuth アプリケーションの client_id と client_secret 、コールバック URL、API で操作する対象のリポジトリを指定します(この時に作って登録した OAuth アプリケーションのものを流用しても構いません)。またこれも同様ですが、アプリケーション登録時に使ったユーザーとは別のユーザーでアクセスする場合は collaborators に追加しておく必要があります。

加えて、このアプリケーションでは各ユーザー毎にブランチを(ログイン時に自動的に)作成してもらい、そのブランチで作業した内容を特定の別ブランチにマージできるような機能があり、そのマージ先のブランチを target_branch_name 変数に指定する必要があります(デフォルト状態では "__all__")。この初期設定を最初に済ませておいてください。

ここで早速動作確認、、、する前に対象となるリポジトリがアプリケーションによってどのように変化していくかを確認しておきます。今回 settings.js の repo_name で指定されたリポジトリはこちらで、main ブランチの中に README.md ファイルが1つだけ格納されている状態です:
2021051905


また main 以外にはブランチは存在していません:
2021051906


ではこのリポジトリがどのように変わっていくのかを確認するため、改めてアプリケーションを実行します:
$ npm install

$ node app

起動したらウェブブラウザで http://localhost:8080/ にアクセスして、前回までと同様に画面右上の login ボタンから Gihub ユーザー ID でログインします:
2021051902


OAuth ログインに成功すると以下のような画面になります:
2021051903


画面右上にログインしたユーザーのアバター画像、画面内には files, merge, push という3つのボタンと、ファイル選択フィールドが表示されているはずです。各ボタンやフィールドの説明をする前に、この(ログイン直後の)時点でリポジトリ側に変化が加えられているので説明しておきます。

対象リポジトリをリロードして、改めてブランチを確認します。すると直前までは main 1つだけだったブランチに2つのブランチ("3183150" と "__all__")が追加されていることがわかります:
2021051907


このアプリケーションではユーザーがログインすると同時に Gihub ユーザー ID と同じ名前のブランチと、settings.js の target_branch_name 変数で指定したブランチの2つが指定リポジトリ内に作成されます(作成済みの場合は失敗するだけなので変化ありません)。この時点で指定リポジトリにはもともと存在していた main ブランチに加えて、ログインしたユーザーのユーザー ID 名と同じブランチ(以降、「個人ブランチ」と呼びます)と、(今回は "__all__" が指定されているので)__all__ ブランチが作成されています。つまり "318350" とはログインした私の Github ユーザー ID ということです。ちなみにユーザー ID を確認するにはログイン後にアバター画像の上をマウスでホバーした時に表示されます:
2021051904
(↑この例だと 318350)


別の(collaborators として権限を与えられた)ユーザーがログインすると、そのユーザーの個人ブランチも作成されます。なお新たに作成される個人ブランチは「その時点での main ブランチの内容をコピー」した状態で作成されます。

改めてボタン類の説明をします。このうち files ボタンは前回紹介したものと同様ですが、main ブランチではなく個人ブランチ内のファイル一覧が表示されます。ログイン直後は main ブランチをコピーした状態なので、もとの main ブランチに含まれていたファイルが一覧(この例では README.md)表示されます。ファイルの(個人ブランチからの)削除もここから行うことができます:
2021051908


まずこの状態に変化を加えるため、ファイルを1つ追加してみます。「ファイルを選択」と書かれた部分をクリックしてローカルシステムからファイルを1つ選択し、push ボタンをクリックします:
2021052001


すると選択したファイルがアップロードされ、個人ブランチに追加されます。アップロード後に files ボタンをクリックすると、アップロードしたファイルが追加されて一覧表示されることがわかります:
2021052002


今回のサンプルアプリでは DELETE ボタンでブランチから削除、またファイル名部分のクリックでファイルダウンロードまでが実装されています:
2021052003


またこの時点でリモートリポジトリの該当ブランチを確認すると、アップロードしたファイルが含まれていることを確認できます:
2021052004


ではこの個人ブランチに加えた変更(ファイル追加)を(settings.js で指定した)別ブランチにマージします。マージするには画面内の merge ボタンをクリックするだけです(画面に { "result": true } と表示されれば成功です):
2021052005


改めてリモートリポジトリの __all__ ブランチ(settings.js の target_branch_name 変数で指定したブランチ)を確認すると、このアクションによって個人ブランチでの変更がマージされているはずです。つまりマージ処理も Github API で実装できていることが確認できます:
2021052006


可能であれば(リポジトリに collaborators として招待した)別の Github ユーザーで同じ手順を実行してみてください。まずはログインして、別のファイルを push します:
2021052007


この時点で(正確には最初のログイン直後の時点で)このユーザーの個人ブランチが作られ、同ブランチにファイルが push された状態になっています:
2021052008

2021052009


ここでこのユーザーも merge を行うと、__all__ ブランチには元のユーザーが追加したファイルと、このユーザーが追加したファイル両方が含まれる状態になります(マージ元である個人ブランチには変化はありません):
2021052010


というわけで、ログインユーザー毎にブランチを作ってファイルを追加/削除し、特定ブランチにマージする機能が Github API で実装できました。当初「Github API を使ってファイルサーバーを作る」ことを目的としていたのですが、ここまで実現できたことで技術的に最低限必要な機能は実装できそうだと思っています。


【Github API による実装内容】
次にこのサンプルアプリケーションのソースコードを解説して、このアプリケーションの機能を Github API でどのように実装しているのかを紹介します。改めてソースコードはこちらです:
https://github.com/dotnsf/githubapi

また、以下の説明の大半は api/github.js ファイル内で実装している内容です。実際のソースコードを参照する場合はこのファイルを見て確認してください。

すべてのコードを説明するのはさすがにちと大変そうなので、「ブランチを作成」する処理と「ファイルをブランチに追加」するという2つの処理内容をソースコード含めて紹介します。


「ブランチ作成」
まずは「ブランチを作る」部分(正確には「main ブランチをコピーして新しいブランチを作る」部分)です。サンプルアプリケーションではログイン時に個人ブランチとマージ対象ブランチを作っています(いずれも作成成功するのは初回のみで、2回目以降は "already exists" というエラーになりますが無視します)。

OAuth 処理の一環で GET /api/callback を処理してログインが成功すると、InitMyBranch() 関数を実行します。この InitMyBranch() 関数は次のようになっています:
async function InitMyBranch( access_token ){
  return new Promise( async function( resolve, reject ){
    if( access_token ){
      var option = {
        url: 'https://api.github.com/user',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      request( option, async function( err, res0, body ){
        if( err ){
          console.log( { err } );
          resolve( false );
        }else{
          body = JSON.parse( body );
          //. body = { login: 'dotnsf', id: XXXXXX, avatar_url: 'xxx', name: 'きむらけい', email: 'xxx@xxx', created_at: 'XX', updated_at: 'XX', ... }
          loggedIns[access_token].user = body;

          //. https://qiita.com/nysalor/items/68d2463bcd0bb24cf69b

          //. main ブランチの SHA 取得
          var option1 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/main',
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          console.log( { option1 } );
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              resolve( false );
            }else{
              body1 = JSON.parse( body1 );
              console.log( { body1 } );  //. body1 = { message: 'Git Repository is empty.', documentation_url 'xxx' }  ->  あらかじめリポジトリの main ブランチに README.md などを登録しておくことで回避
              var sha1 = body1.object.sha;
  
              //. 個人ブランチ作成
              var data2 = {
                ref: 'refs/heads/' + body.id,
                sha: sha1
              };
              var option2 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                json: data2,
                method: 'POST'
              };
              request( option2, async function( err2, res2, body2 ){
                if( err2 ){
                  console.log( { err2 } );
                  resolve( false );
                }else{
                  console.log( { body2 } );  //. { message: 'Reference already exists', .. }
                  //body2 = JSON.parse( body2 );
                  //console.log( { body2 } );

                  //. 作成したブランチの SHA 取得(?)
                  var option3 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + body.id,
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    method: 'GET'
                  };
                  console.log( { option3 } );
                  request( option3, async function( err3, res3, body3 ){
                    var obj = false;
                    if( err3 ){
                      console.log( { err3 } );
                      //resolve( false );
                    }else{
                      body3 = JSON.parse( body3 );
                      console.log( { body3 } );  //. 権限がないユーザーだと { message: 'Not Found', documentation_url: '' }
                      if( body3.message ){
                        //resolve( false );
                      }else{
                        var sha3 = body3.object.sha;
                        //req.session.oauth.sha = sha3;
                        body.sha = sha3;

                        //. ターゲットブランチの生成結果に関係なく、この値を返す
                        obj = JSON.parse( JSON.stringify( body ) );
                        //resolve( body );
                      }
                    }

                    //. ターゲットブランチ作成
                    var data4 = {
                      ref: 'refs/heads/' + settings.target_branch_name,
                      sha: sha1
                    };
                    var option4 = {
                      url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                      headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                      json: data4,
                      method: 'POST'
                    };
                    request( option4, async function( err4, res4, body4 ){
                      if( err4 ){
                        console.log( { err4 } );
                      }else{
                        console.log( { body4 } );  //. { message: 'Reference already exists', .. }
                      }
                      resolve( obj );
                    });
                  });
                }
              });
            }
          });
        }
      });
    }else{
      resolve( false );
    }
  });
}

InitMyBranch 関数は OAuth 処理で取得したアクセストークンを引数に渡されて実行します。この関数内ではまず(1)個人ブランチを作成 して、次に(2)マージ対象ブランチを作成 します。処理内容は似ていますが、この順に説明します。

まず(1)個人ブランチの作成です。個人ブランチはユーザー個人の Github ユーザー ID 名のブランチを作ることになるので、まずは実行ユーザーの Github ユーザー ID を取得する必要があります。そのため以前のブログエントリでも説明した個人ユーザー情報を取得する API を実行します:
            :
            :
      var option = {
        url: 'https://api.github.com/user',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        method: 'GET'
      };
      request( option, async function( err, res0, body ){
        if( err ){
          console.log( { err } );
          resolve( false );
        }else{
          body = JSON.parse( body );
            :
            :

これで Github API を取得すれば、作成する個人ブランチの名称が決まったことになります。次にその名称のブランチを作成するのですが、git のブランチは作成元ブランチを指定する必要があります(CLI でも同様ですが、ブランチ作成元になるブランチを指定する必要がある、という意味です)。今回は main ブランチを元に個人ブランチを作るので、まずは main ブランチの SHA 情報を取得しておく必要があります。というわけで、これも以前のブログエントリで説明した方法で main ブランチの情報を取得する API を実行します:
            :
            :
          //. main ブランチの SHA 取得
          var option1 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/main',
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          console.log( { option1 } );
          request( option1, async function( err1, res1, body1 ){
            if( err1 ){
              console.log( { err1 } );
              resolve( false );
            }else{
              body1 = JSON.parse( body1 );
              console.log( { body1 } );
              var sha1 = body1.object.sha;

            :
            :

これで個人ブランチを作成するために必要な情報が揃いました。あらためて main ブランチの SHA を指定して作成元を明示した上で、新しいブランチを Github ユーザー ID 名で作成します:
            :
            :
              //. 個人ブランチ作成
              var data2 = {
                ref: 'refs/heads/' + body.id,
                sha: sha1
              };
              var option2 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                json: data2,
                method: 'POST'
              };
              request( option2, async function( err2, res2, body2 ){
                if( err2 ){
                  console.log( { err2 } );
                  resolve( false );
                }else{
                  console.log( { body2 } );  //. { message: 'Reference already exists', .. }

            :
            :

余談ですが、既に個人ブランチが作成済みであった場合、この API 実行は失敗するのですが、REST API の HTTP ステータスとしては 200 が、作成メッセージとして "Reference already exists" が返ります(上のコードの赤字部分)。一般的にはこういう場合は 400 番代のエラーステータスコードが返ってくることが多いと思っていますが、この点は注意が必要です(処理的にはエラー扱いしなくてよい、こちらの方が楽ですけど・・)。

これで個人ブランチが作成された状態になりました。続いて同様にマージ対象ブランチを作成します。これも main ブランチを元に新しいブランチを作る形になるため、main ブランチの SHA を指定して、settings.js 内の target_branch_name 変数で定義した新しいブランチを作成します:
            :
            :
                    //. ターゲットブランチ作成
                    var data4 = {
                      ref: 'refs/heads/' + settings.target_branch_name,
                      sha: sha1
                    };
                    var option4 = {
                      url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs',
                      headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi', 'Accept': 'application/vnd.github.v3+json' },
                      json: data4,
                      method: 'POST'
                    };
                    request( option4, async function( err4, res4, body4 ){
                      if( err4 ){
                        console.log( { err4 } );
                      }else{
                        console.log( { body4 } );  //. { message: 'Reference already exists', .. }
                      }
                      resolve( obj );
                    });
            :
            :


「ファイルを追加」
次は「ブランチにファイルを追加する」部分です。処理としてはブラウザからファイルをアップロードして、そのアップロードしたファイルを API でブランチ(の tree)に追加する、という流れになります。tree については前回のブログエントリを参照してください。

まずはファイルアップロード処理の部分から見てみます。ブラウザ画面からファイルを選択して push ボタンを押すと、POST /api/file という API が実行されます。その内容がこちらで、アップロードされたファイル(のパス)を取り出し、アクセストークンなどと一緒に PushToMyBranch() 関数を実行しています:
router.post( '/file', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  if( req.session && req.session.oauth && req.session.oauth.token && req.session.oauth.id, req.session.oauth.sha ){
    //. https://qiita.com/ngs/items/34e51186a485c705ffdb
    var filepath = req.file.path;
    var filetype = req.file.mimetype;
    //var filesize = req.file.size;
    var ext = filetype.split( "/" )[1];
    var filename = req.file.filename;
    var originalfilename = req.file.originalname;

    var r = await PushToMyBranch( req.session.oauth.token, req.session.oauth.id, req.session.oauth.sha, filepath, filetype, originalfilename );
    console.log( { r } );
    if( r ){
      //. 追加したブランチの最新 SHA を取得
      r = await InitMyBranch( req.session.oauth.token );
      console.log( { r } );
      if( r ){
        req.session.oauth.sha = r.sha;
      }
    }

    //res.write( JSON.stringify( { result: r }, null, 2 ) );
    //res.end();
    res.redirect( '/' );
  }else{
    //res.status( 400 );
    //res.write( JSON.stringify( { error: 'no access_token' }, null, 2 ) );
    //res.end();
    res.redirect( '/' );
  }
});

PushToMyBranch() 関数内の処理がこちらです。
async function PushToMyBranch( access_token, id, sha, filepath, filetype, originalfilename ){
  return new Promise( async function( resolve, reject ){
    if( access_token && id && sha ){
      var data1 = {};
      if( filetype.startsWith( 'text' ) ){
        //. text
        var text = fs.readFileSync( filepath, 'utf8' );
        data1 = {
          content: text,
          encoding: 'utf-8'
        };
      }else{
        //. binary
        var bin = fs.readFileSync( filepath );
        data1 = {
          content: new Buffer( bin ).toString( 'base64' ),
          encoding: 'base64'
        };
      }
      console.log( { data1 } );
  
      //. BLOB 作成
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/blobs',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        json: data1,
        method: 'POST'
      };
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          fs.unlink( filepath, function( e ){} );
          resolve( false );
        }else{
          //body1 = JSON.parse( body1 );
          //. body1 = { url: 'XXXXX', sha: 'XXXXXX' }
          console.log( { body1 } );
          var sha1 = body1.sha;

          //. ここで Tree を新規に作成するのではなく、既存の最新 Tree を取得して追加する
          //. 最後に InitMyBranch() を実行するなりして、セッション内 sha の更新が必要?
          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha,
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          request( option2, async function( err2, res2, body2 ){
            if( err2 ){
              console.log( { err2 } );
              resolve( false );
            }else{
              body2 = JSON.parse( body2 );  //. body2 = { commit: {}, url: '', author: {}, files: [], .. }
              console.log( { body2 } );

              //. tree
              var option3 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees/' + body2.sha,
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                method: 'GET'
              };
              request( option3, async function( err3, res3, body3 ){
                if( err3 ){
                  console.log( { err3 } );
                  resolve( false );
                }else{
                  body3 = JSON.parse( body3 );
                  console.log( { body3 } ); //. body3.tree = [ { path: "README.md", size: 130, url: "", .. }, .. ]
    
                  //. Tree 追加
                  var data4 = { tree: [] };
                  data4.tree = JSON.parse( JSON.stringify( body3.tree ) );
                  data4.tree.push( { path: originalfilename, mode: '100644', type: 'blob', sha: sha1 } );

                  var option4 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees',
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    json: data4,
                    method: 'POST'
                  };
                  request( option4, async function( err4, res4, body4 ){
                    if( err4 ){
                      console.log( { err4 } );
                      fs.unlink( filepath, function( e ){} );
                      resolve( false );
                    }else{
                      //body4 = JSON.parse( body4 );
                      console.log( { body4 } );
                      var sha4 = body4.sha;

                      //. 現在の Commit の SHA を取得
                      var option5 = {
                        url: 'https://api.github.com/repos/' + settings.repo_name + '/branches/' + id,
                        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                        method: 'GET'
                      };
                      request( option5, async function( err5, res5, body5 ){
                        if( err5 ){
                          console.log( { err5 } );
                          fs.unlink( filepath, function( e ){} );
                          resolve( false );
                        }else{
                          body5 = JSON.parse( body5 );
                          console.log( { body5 } );
                          var sha5 = body5.commit.sha;

                          //. Commit を作成
                          var ts = ( new Date() ).getTime();
                          var data6 = {
                            message: '' + ts,
                            tree: sha4,
                            parents: [ sha5 ]
                          };
                          var option6 = {
                            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/commits',
                            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                            json: data6,
                            method: 'POST'
                          };
                          request( option6, async function( err6, res6, body6 ){
                            if( err6 ){
                              console.log( { err6 } );
                              fs.unlink( filepath, function( e ){} );
                              resolve( false );
                            }else{
                              //body6 = JSON.parse( body6 );
                              console.log( { body6 } );
                              var sha6 = body6.sha;
          
                              //. リファレンスを更新
                              var data7 = {
                                force: false,
                                sha: sha6
                              };
                              var option7 = {
                                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + id,
                                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                                json: data7,
                                method: 'PATCH'
                              };
                              request( option7, async function( err7, res7, body7 ){
                                if( err7 ){
                                  console.log( { err7 } );
                                  fs.unlink( filepath, function( e ){} );
                                  resolve( false );
                                }else{
                                  //body7 = JSON.parse( body7 );
                                  console.log( { body7 } );
                                  fs.unlink( filepath, function( e ){} );

                                  var sha7 = body7.object.sha;

                                  resolve( { sha: sha7 } );
                                }
                              });
                            }
                          });
                        }
                      });
                    }
                  });
                }
              });
            }
          });
        }
      });
    }else{
      if( filepath ){
        fs.unlink( filepath, function( e ){} );
      }
      resolve( false );
    }
  });
}

では PushToMyBranch() 関数内の処理を順に説明します。この関数内ではまず(1)ファイルの blob を作成 した後に(2)ブランチ内の現在のファイルツリーを取得 します。そして(3)ファイルの blob をツリーに追加 してから(4)ファイルをコミット し、最後に(5)リファレンス SHA を更新 する、という一連の流れを処理しています。では上述のコードを少しずつ見ながら、この流れを説明します。

まず(1)ファイルの blob を作成する部分です。ここはアップロードされたファイルのタイプを参照して、テキストファイルだったら UTF-8 エンコードで、それ以外だったら Base64 エンコードして中身を取り出した上で blob オブジェクトを作成し、その SHA を取り出します:
            :
            :
      var data1 = {};
      if( filetype.startsWith( 'text' ) ){
        //. text
        var text = fs.readFileSync( filepath, 'utf8' );
        data1 = {
          content: text,
          encoding: 'utf-8'
        };
      }else{
        //. binary
        var bin = fs.readFileSync( filepath );
        data1 = {
          content: new Buffer( bin ).toString( 'base64' ),
          encoding: 'base64'
        };
      }
      console.log( { data1 } );
  
      //. BLOB 作成
      var option1 = {
        url: 'https://api.github.com/repos/' + settings.repo_name + '/git/blobs',
        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
        json: data1,
        method: 'POST'
      };
      request( option1, async function( err1, res1, body1 ){
        if( err1 ){
          console.log( { err1 } );
          fs.unlink( filepath, function( e ){} );
          resolve( false );
        }else{
          //body1 = JSON.parse( body1 );
          //. body1 = { url: 'XXXXX', sha: 'XXXXXX' }
          console.log( { body1 } );
          var sha1 = body1.sha;
            :
            :

(2)この blob をブランチに追加するのですが、そのためにブランチの現在のファイルツリーを取得しておきます:
            :
            :
          //. インスペクト
          var option2 = {
            url: 'https://api.github.com/repos/' + settings.repo_name + '/commits/' + sha,
            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
            method: 'GET'
          };
          request( option2, async function( err2, res2, body2 ){
            if( err2 ){
              console.log( { err2 } );
              resolve( false );
            }else{
              body2 = JSON.parse( body2 );  //. body2 = { commit: {}, url: '', author: {}, files: [], .. }
              console.log( { body2 } );

              //. tree
              var option3 = {
                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees/' + body2.sha,
                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                method: 'GET'
              };
              request( option3, async function( err3, res3, body3 ){
                if( err3 ){
                  console.log( { err3 } );
                  resolve( false );
                }else{
                  body3 = JSON.parse( body3 );
                  console.log( { body3 } ); //. body3.tree = [ { path: "README.md", size: 130, url: "", .. }, .. ]
            :
            :

そして(3)ファイルの blob をツリーに追加します:
            :
            :
                  //. Tree 追加
                  var data4 = { tree: [] };
                  data4.tree = JSON.parse( JSON.stringify( body3.tree ) );
                  data4.tree.push( { path: originalfilename, mode: '100644', type: 'blob', sha: sha1 } );

                  var option4 = {
                    url: 'https://api.github.com/repos/' + settings.repo_name + '/git/trees',
                    headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                    json: data4,
                    method: 'POST'
                  };
                  request( option4, async function( err4, res4, body4 ){
                    if( err4 ){
                      console.log( { err4 } );
                      fs.unlink( filepath, function( e ){} );
                      resolve( false );
                    }else{
                      //body4 = JSON.parse( body4 );
                      console.log( { body4 } );
                      var sha4 = body4.sha;

            :
            :

そして(4)この状態をコミットします:
            :
            :
                      //. 現在の Commit の SHA を取得
                      var option5 = {
                        url: 'https://api.github.com/repos/' + settings.repo_name + '/branches/' + id,
                        headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                        method: 'GET'
                      };
                      request( option5, async function( err5, res5, body5 ){
                        if( err5 ){
                          console.log( { err5 } );
                          fs.unlink( filepath, function( e ){} );
                          resolve( false );
                        }else{
                          body5 = JSON.parse( body5 );
                          console.log( { body5 } );
                          var sha5 = body5.commit.sha;

                          //. Commit を作成
                          var ts = ( new Date() ).getTime();
                          var data6 = {
                            message: '' + ts,
                            tree: sha4,
                            parents: [ sha5 ]
                          };
                          var option6 = {
                            url: 'https://api.github.com/repos/' + settings.repo_name + '/git/commits',
                            headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                            json: data6,
                            method: 'POST'
                          };
                          request( option6, async function( err6, res6, body6 ){
                            if( err6 ){
                              console.log( { err6 } );
                              fs.unlink( filepath, function( e ){} );
                              resolve( false );
                            }else{
                              //body6 = JSON.parse( body6 );
                              console.log( { body6 } );
                              var sha6 = body6.sha;

            :
            :

最後にこの後の処理に備えて(5)リファレンス SHA を更新することで一連のアップロード処理は完了します:
            :
            :
                              //. リファレンスを更新
                              var data7 = {
                                force: false,
                                sha: sha6
                              };
                              var option7 = {
                                url: 'https://api.github.com/repos/' + settings.repo_name + '/git/refs/heads/' + id,
                                headers: { 'Authorization': 'token ' + access_token, 'User-Agent': 'githubapi' },
                                json: data7,
                                method: 'PATCH'
                              };
                              request( option7, async function( err7, res7, body7 ){
                                if( err7 ){
                                  console.log( { err7 } );
                                  fs.unlink( filepath, function( e ){} );
                                  resolve( false );
                                }else{
                                  //body7 = JSON.parse( body7 );
                                  console.log( { body7 } );
                                  fs.unlink( filepath, function( e ){} );

                                  var sha7 = body7.object.sha;

                                  resolve( { sha: sha7 } );
                                }
                              });

            :
            :

これ以外にもブランチからファイルを削除するなど、他に実装している内容もありますが、なんとなく API を実行する際の共通の流れ(変更する場合は直前の状態の SHA を取得し、その値を指定して変更の POST を実行)がわかるのではないかと思います。

また上のソースコードの最後の Github API 実行部分をよく見ると、新しい状態の SHA を取得するための REST API のメソッドが珍しい PATCH になっていることがわかります。メソッドの種類として存在は知っていましたが、実際に使ったのは初めてのような気がします。それくらい珍しい REST API でした。



このページのトップヘ