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

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

タグ:nodejs

Node.js で MySQL データベースを利用する場合、npm の mysql ライブラリが多く使われると思っています:
mysql - npm


このライブラリを使うと、例えば SELECT 文を実行するのであれば、こんな感じに記述することで実装できます:
var Mysql = require( 'mysql' );

//. データベースへ接続
var mysql = Mysql.createConnection({
  host: '192.168.10.10',
  user: 'username',
  password: 'password',
  database: 'mydb'
});
mysql.connect();

//. SELECT 文実行
mysql.query( 'select * from items where price > 1000', function( error, results, fields ){
  if( error ) throw error;
  results.forEach( function( result ){
    var id = result.id;
       :
       :

    //. 終了
    mysql.end();
  });
});

また INSERT 文やプレースホルダーっぽい機能を使うこともできます:
var Mysql = require( 'mysql' );

//. データベースへ接続
var mysql = Mysql.createConnection({
  host: '192.168.10.10',
  user: 'username',
  password: 'password',
  database: 'mydb'
});
mysql.connect();

//. INSERT 文実行
mysql.query( 'insert into items set ?', { id: 1234, name: 'シャンプー', price: 500 }, function( error, result ){
  if( error ) throw error;
       :
       :

    //. 終了
    mysql.end();
  });
});

詳しくは上記公式ページを参照してください。


さて、MySQL では create table でテーブルを定義する際に blob(バイナリラージオブジェクト)型の列を指定することができます。画像ファイルなどバイナリデータをそのまま格納することができる列が定義できます:
> create table items( id int primary key, name varchar(50), price int, img blob );

定義は上記のように指定すればいいのですが、ではこの blob 列に、特に mysql ライブラリを使ってどのように指定すればデータを格納すればよいか、が今回のテーマです。特にウェブ画面から画像ファイルを指定してアップロードするような場合に、その画像ファイルの内容を具体的にはどのようにして blob 列に格納すればよいか、という内容です。

この要件について、少しググると MySQL の LOAD_FILE() 関数を使う方法が見つかります。この場合、具体的には以下のように記述します(目的の画像ファイルが /tmp/aaa.png に存在すると仮定します):
    :

mysql.query( 'insert into items set ?', { id: 1234, name: 'シャンプー', price: 500, img: LOAD_FILE('/tmp/aaa.png') }, function( error, result ){
: :

この方法はローカル MySQL サーバーに対しては有効に利用できます。LOAD_FILE() 関数はサーバー側のファイルシステムに対して実行されます。なので上記命令を実行してデータを格納する MySQL サーバーのファイルシステムに /tmp/aaa/png というファイルが存在していれば正しく動きます(命令を実行するクライアント側のファイルシステムにあっても動きません)。

しかし一般的なウェブシステムではウェブサーバーとデータベースサーバーは分離しています。そのようなケースでは(ウェブサーバーはデータベースクライアントになるので)ユーザーがウェブでアップロードしたファイルはウェブサーバーに一時格納されるだけで、データベースサーバーへは送られません。そこでウェブサーバー上で LOAD_FILE を実行してもデータは格納できないことになります。

では改めて、 LOAD_FILE() を使わずに blob データをどのように MySQL に格納するか、その方法がこちらです:
var fs = require( 'fs' );
    :
var img_content = fs.readFileSync( '/tmp/aaa.png' );
mysql.query( 'insert into items set ?', { id: 1234, name: 'シャンプー', price: 500, img: img_content }, function( error, result ){
    :
    :

ファイルシステムライブラリである fs をロードし、readFileSync 関数でローカルの(ウェブサーバー上の)バイナリファイルを読み込み、その結果をプレースホルダーに指定するだけです。

ちなみに取り出すときはこんな感じ:
    :
mysql.query( 'select * from items where id = 1234', function( error, results, fields ){
  if( error ) throw error;
  var img_content = results[0].img;
    :
    :

パフォーマンス等の観点からバイナリラージオブジェクトを MySQL などのデータベースに格納するべきか?という問題はあると思いますが、S3 ストレージなどの外部に格納する場合と比べて「データベースのバックアップ/リストアでオブジェクトごとバックアップ/リストアされる」というメリットはあります。用途に応じては使う価値があると思っています。



某アプリを Slack 対応する経緯で Slack API の中の、特に認証/認可を司る OAuth API を使う機会があったので自己まとめです。

もともとやりたかったのはウェブアプリに Slack アカウントでログインして、そのログインした人の権限でチャネル一覧を取得し(※)、ウェブアプリから指定したチャネルにメッセージを書き込む、ということでした。この中の※部分までを Node.js + Express + EJS で実現したコードを Github に公開しています(後述)。


実際に試してみるにはまず Slack に対象アプリケーションを登録する必要があります。 https://api.slack.com/apps を開いてログインし、"Create New App" ボタンをクリックしてウェブアプリを登録します:
2019052401


登録するアプリの名前と、対象ワークスペースを指定します(つまり同じアプリを複数のワークスペースで使いたい場合は、アプリを複数登録する必要があります)。以下では名前は "Slack OAuth Sample"、ワークスペースは "dotnsf" を指定しています(ワークスペースはログインしたユーザーが利用可能なワークスペース一覧から選択します)。最後に "Create App" ボタンをクリックして作成します:
2019052402


すると指定したアプリケーションの API 設定画面に切り替わります。画面左上に入力したアプリ名がデフォルトアイコンと一緒に表示されていて、"Basic Information" メニューが選択されていることを確認します:
2019052403


この画面を下スクロールすると App Credentials という項目があります。この中の Client IDClient Secret の値を後で使うので、どこかにコピーしておくか、いつでもこの画面を開ける状態にしておきましょう。なお Client ID の値は画面内に表示されていますが、Client Secret の値は初期状態では非表示になっています。"Show" ボタンをクリックして内容を表示し、その表示された値をあとで使うことに注意してください。またこれらの値は他の人には教えないように、自分で管理する必要があります:
2019052404


次に画面左のメニューから "OAuth & Permissions" を選び、少し下にスクロールすると Redirect URLs という項目があります。ここにウェブアプリケーションを動かす際のコールバック URL を登録しておく必要があります。"Add New Redirect URL" ボタンをクリックします:
2019052405


すると Redirect URL を追加する画面になるので、http(s)://サーバー名/slack/callback と入力します。この値は開発時には開発時用のサーバー名とポート番号、本番環境では本番環境用のサーバー名を指定する必要があります。下図では開発時向けに localhost の 6010 番ポートで動かす想定で http://localhost:6010/slack/callback と指定しています。ここの値は実際の環境に合わせて適宜変更してください。入力し終わったら "Add" ボタンをクリックして、その後 "Save URLs" ボタンをクリックします:
2019052406


画面上部に "Success!" というメッセージが表示されればリダイレクト URL の設定は完了です。正しい Redirect URLs が登録されたことを確認します:
2019052407


続けて、このアプリで利用する Slack 機能のスコープを指定します。実は OAuth 認証だけであればここの設定は不要なのですが、今回のデモアプリでは OAuth 認証後にログインユーザーが参照できるチャネルの一覧を取得して表示する、という機能が含まれています。また実際のアプリケーションではそのアプリケーションで実装する機能によって、ここでスコープを追加する必要があります:
2019052408


今回はログインユーザーが利用できるチャネル一覧を取得するため、"channels:read" スコープを追加します。また他に必要なスコープがあればここで追加します。最後に "Save Changes" ボタンをクリックして変更を反映します:
2019052409


これで Slack API 側の設定は完了しました。

では改めて Github からアプリケーションを取得します。Node.js がインストール済みの実行サーバー上で以下の URL を指定して git clone するか、ソースコードをダウンロード&展開してください:
 https://github.com/dotnsf/slack-oauth

2019052410


ソースコード内の settings.js をテキストエディタで開き、exports.slack_client_id の値と exports.slack_client_secret の値を上記で確認した client_id と client_secret の値に(コピー&ペーストなどで)変更して、保存してください。

なお、このサンプルアプリケーションでは以下のリクエスト API(?)が用意されていて、これらを明示的&内部的に使って動きます:
リクエスト API用途
GET /ユーザーがアクセスする唯一のページ。アクセス時に認証情報がセッションに含まれているとチャネル一覧が表示される。認証情報がセッションに含まれていない場合は認証前とみなして「ログイン」ボタンを表示する
GET /slack/loginユーザーページで「ログイン」ボタンをクリックした時に実行される。Slack の OAuth 認証ページにリダイレクトされる
GET /slack/callbackSlack の OAuth 認証ページで Authorize された時のリダイレクトページ。この URL が Slack OAuth 設定時に指定されている必要がある。アクセストークンを暗号化してセッションに保存し、GET / へリダイレクトされる
POST /slack/logoutログアウト(セッション情報を削除)する
GET /channelsチャネル一覧を取得する。認証後にユーザーページが表示されると内部的にこの REST API が AJAX 実行されて、画面にチャネル一覧が表示される。


では実際に起動してみます。起動サーバーにログインし、ソースコードのあるフォルダに移動した状態で、以下を実行します:
$ npm install
$ node app

そしてウェブブラウザで起動中のアプリケーションにアクセスします。以下の例では localhost:6010 でアプリケーションが起動されている想定になっているので、http://localhost:6010/ にアクセスします。上記の GET / が実行され、ログイン前のシンプルなページが表示されます。ここで "Login" を選択します:
2019052411


GET /slack/login が実行され、ブラウザは Slack API の OAuth 認証ページにリダイレクトされます。アプリケーションが利用する scope が表示され、このまま認証処理を許可するかどうかを聞かれます。許可する場合は "Authorize" を選択します:
2019052412


Authorize を選択すると認証と認可が完了し、そのアクセストークンが GET /slack/callback へ渡されます。そこでアクセストークンを暗号化してセッションに含めます。この状態であらためてトップページ(GET /)が表示され、ログイン処理が住んでいるのでログイン後の画面が表示されます。AJAX で GET /channels が実行され、ログインユーザーが参照することのできるチャネルの一覧が表示されれば成功です:
2019052413


以上、Slack API の OAuth を使ってウェブアプリケーションから Slack の認証を行い、認証ユーザーの権限で Slack API を外部から実行する、というアプリケーションのサンプルを作って実行するまでの手順紹介でした。


 

わけわからないタイトルになってしまいました。これは
  • ウェブブラウザの JavaScript を使わずに、HTML と CSS でマウスの軌跡を追跡する方法が発見された
  • 実際に確認できるサンプルが Go 言語で実装されていたので、JavaScript に移植してみた
ということです。

まず、元ネタはこれです:
Researcher Finds CSS-Only Method to Track Mouse Movements

spyware


セキュリティの研究者である Davy Wybiral 氏の以下のデモ動画付きツイートを紹介する形で伝えられていました。このデモでは JavaScript が無効にされた Tor ブラウザが使われていますが、確かに左画面でのマウスの動きが右画面で確認できています。左の画面をユーザーが使っていて、右画面ではそこでのマウスの動きがほぼリアルタイムに再現されています:




もともとウェブページにおいては JavaScript を利用することでマウスの動きを検知することができるようになっています。移動したり、クリックしたり、クリックが開放されたり、といったタイミングや、そのイベントが発生したときのマウス位置を知ること自体はこれまでも可能でした。

ただし、それには JavaScript が有効になっている、という条件があります。多くのウェブブラウザにおいて、JavaScript ははじめから有効になっているもので、あえて無効になるよう設定しない限りは有効なままです。また最近のウェブページも JavaScript が有効になっている前提で作られているものが多いので、JavaScript が有効であることが特別に危険ということはないと思っています。 一方で JavaScript を使ったイタズラページが存在していることも事実で、あえて JavaScript を無効にしてから利用する、というシーンが(それによって思い通りに動かない、ということはあるかもしれませんが)ないわけではありません。


・・・という中での今回のニュースです。安全性を高める目的でウェブブラウザの JavaScript を無効にしていても、マウスの動きがウェブページ提供側に知られてしまう可能性(というか方法)があった、というものでした。上述の Davy 氏が Go 言語で作成したサンプルも公開されています(みるとわかりますが、このシンプルなコードだけで実現できるという明瞭さ!):
https://gist.github.com/wybiral/c8f46fdf1fc558d631b55de3a0267771


で、自分がこれを参考にしてサーバーサイド JavaScript である、Node.js 向けに移植したサンプルを作ってみた、というものでした。なのでサブジェクトは正確には
 『クライアント JavaScriptを使わずにCSSでマウスを追跡する方法』をサーバーサイド JavaScript で実装してみた
という感じになりますかね。

ちなみに移植したコードはこちら:
https://github.com/dotnsf/noscript-tracking.js


で、(ブラウザ側の)JavaScript を使わずにどうやってマウスの座標を調べるのか、という話です。具体的には CSS の :hover 疑似クラスと、その background 属性に画像を指定することで、特定のエリアに入ったことを知らせるリクエストをサーバー側に発生させる、という方法によって実現しています。

上記移植コードを見ると、2つのページが定義されています。1つはコンテキストルート( "/" )へのリクエストがあった際に表示される index ページで、そのテンプレートは views/index.ejs です。もう1つは "/watch" へのリクエストがあった際に表示される watch ページ(テンプレートは views/watch.ejs)です。index ページには JavaScript は一切使われていないのですが、このページの上でマウスを動かすと、その軌跡がほぼリアルタイムに watch ページから確認できるようになる、という内容のサンプルです:
2019051501
(↑左が index ページ、右が watch ページ。index ページ上でマウスを動かす様子が watch ページ上に表示されている)


では JavaScript を使わずに、どうやって index ページから watch ページへマウスの軌跡を知らせているかを説明します。今回ブラウザの JavaScript は使いませんが、サーバーサイドのロジックは必要になります。要はスタンドアロンでどうにかできる、というものではないということです。

まず index ページについて、index ページにアクセスすると画面には格子状(今回の例では 50x50)のブロックが表示されます。個々の格子は <p> タグで構成されています。格子は見えないスタイルにすることも可能ですが、今回は視覚的にわかりやすいようにあえて枠線を表示することで見やすくしています。

そしてスタイルシートを使って、個々の格子(<p>)に :hover 疑似クラスと、その時に背景画像が設定されるよう指定します(つまり各格子の上にマウスが来ると、その格子に背景画像が表示されるように設定します)。

これが CSS だけでマウス軌跡を追跡する方法の肝になります。つまり「ある格子の上をマウスが通過した時に、画像を表示するようなリクエストがサーバー側に送られ」ます。そしてサーバー側はそのようなリクエストに対してエラー処理を行い(つまり画像は表示されない=何も変わらない)、画像を表示する変わりにリクエスト内容を記録します。これによってどの格子の上をマウスが通過したのか、という情報をサーバー側に溜めることが可能になります:
2019051603


そしてもう1つの watch ページ側では上記の処理によってサーバー側に記録されたマウスの軌跡情報ごと取得し、index ページと同様の格子ブロックを描画します。ただしその際にマウスが通過した格子だけには背景色が設定され、マウスが通過していない格子と視覚的に違いがわかるようにしています:
2019051604


この2つのページを横に並べて表示し、index ページ(下図左)上でマウスを動かすと、その情報が watch ページ(下図右)上で確認できる、ということが実現できています。なるほどね~。
2019051501


※僕のコードでは watch ページをリロードする機能までは実装していないので、F5 キーや Ctl+R などで watch ページを定期的に更新する必要がありますが、リアルタイムでサーバー側には記録されている、ということがわかると思います。



令和最初のブログエントリです。


Node.js アプリの中で git を使う方法を調べてみました。git で Node.js のソースコードを管理する、のではなく、アプリケーションの中で特定の git リポジトリに対して clone したり、pull したり、add して commit して push したり、、、といった操作を Node.js アプリ側から行う方法、という意味です。

この操作を実現するためのライブラリとして simple-git というものを使ってみました。npm を使って以下のコマンドを実行することで導入できます:
$ npm install simple-git

2019050201


そして、以下のコードで扱う git リポジトリは https://github.com/dotnsf/simple-git-sample.git であると仮定します。異なるリポジトリで実験する場合は( fork してコピーを作るなどして)適宜読み替えてください。またローカル側のワーキングフォルダ(git で管理するフォルダ)は ./work/ であると仮定します。


【初期化】
まず、このライブラリを使って Node.js アプリを作る際の初期化方法を紹介します(実はここが一番ややこしい所だったりします)。ここは「実行時に git clone して初期化」するのか、「実行時に git pull して初期化」するのかによって手順が異なります。

実行時に git clone して初期化する場合は以下のようにします:
//. git-clone.js
var git = require( 'simple-git' );

var git_url = 'https://github.com/dotnsf/simple-git-sample.git';
var local_folder = 'work';


//. clone
git().clone( git_url, local_folder );

このコードを node コマンドで実行すると、work/ というフォルダが新規に作成され、https://github.com/dotnsf/simple-git-sample.git のクローンがそのフォルダの中に作成されます。

一方、実行時に git pull して初期化する場合は以下のようにします:
//. git-pull.js
var git = require( 'simple-git' );

var git_url = 'https://github.com/dotnsf/simple-git-sample.git';
var local_folder = 'work';


//. pull
git( local_folder ).pull();

このコードを node コマンドで実行すると、既にワーキングフォルダとして存在している ./work/ フォルダの中に最新のリポジトリ状態が git pull されます。

ここでややこしいのが「どちらを使って初期化するべきか?」です。既にワーキングフォルダ ./work/ が存在している状態で git clone を実行するとエラーになってしまうし、一方ワーキングフォルダが存在していない状態では git pull してもエラーとなってしまうからです。というわけで、以下のように実行すべきだと思いました:
//. git-init.js
var git = require( 'simple-git' );
var fs = require( 'fs' );
var path = require( 'path' );

var git_url = 'https://github.com/dotnsf/simple-git-sample.git';
var local_folder = 'work';

//. フォルダの存在確認
var dirname = path.dirname( './' + local_folder );
fs.access( dirname, fs.constants.R_OK | fs.constants.W_OK, ( err ) => {
  if( err ){
    //. clone
    git().clone( git_url, local_folder );
  }else{
    //. pull
    git( local_folder ).pull();
  }
});

ワーキングフォルダが存在しているかどうかを確認し、存在していなかった場合は git clone を、存在していた場合は(git clone 済みと解釈して)git pull を、それぞれ実行して初期化しています。


【ファイル追加】
次にこの(初期化済みの)ライブラリを使って、リポジトリにファイルを追加する方法を紹介します。厳密にいうと「(git pull して、)git add して、git commit して、git push する」までの一連の方法を紹介します。

といっても、実は結構簡単でこんな感じで一連の処理を実現できます:
//. git-push.js
var git = require( 'simple-git' );
var fs = require( 'fs' );
var path = require( 'path' );

var git_url = 'https://github.com/dotnsf/simple-git-sample.git';
var local_folder = 'work';

//. pull, add, commit, and push
git( local_folder ).pull()
  .add( 'README.md' )
  .commit( 'README.md updated.' )
  .push( [ '-u', 'origin', 'master' ] );

add() のパラメータに追加したいファイル名、commit() のパラメータにはコミットメッセージ、そして push のパラメータにはオプションを指定します(この例だと $ git push -u origin master を実行しているのと同じ処理をしています)。


実際にはブランチ切ったり、マージが必要になったりすることもあるので、全ての git 処理を全自動でというのはなかなか難しい所もあると思いますが、一連の決まった処理を(例外処理無しで)実現するにはシンプルで便利なライブラリだと思いました。


「ウェブ魚拓」というサービスをご存知でしょうか?ネット上のウェブサイト(URL)を独自にキャッシュして保存するサービスのことです。ネット上のウェブサイトがなくなってしまったり、内容が変わったりしても、その保存したタイミングでのウェブサイトを後からでも参照できるように残す、というサービスです。

このウェブ魚拓もどきのサービスを Node.js 向けに作って、ソースコードを公開しました:
https://github.com/dotnsf/urlrec

2019020801


まず、この公開したサービスではあらゆる全てのサイトでキャッシュを残せるわけではありません(いわゆる「対策済みサイト」に対しては、この機能は無効です)。残せるサイトの場合は独自フォーマット(本当は MHTML にしたかったんですが、色々難しそうだったので、画像だけインライン展開するフォーマットで HTML と画像を記録できるようにしました)でキャッシュを記録します。つまり取得時点でのテキスト内容と画像をキャッシュして記録します。

また記録先として IBM CloudIBM Cloudant サービスが必要です。必要に応じて IBM Cloud のアカウントを作成し、IBM Cloudant サービスインスタンスを作成しておいてください:
2019020802


ソースコードを入手する前に、IBM Cloudant インスタンスを利用するための接続情報を確認しておきます。作成したサービスインスタンスの「サービス資格情報」メニューから「資格情報の表示」を選択(見当たらない場合は1つ新規に作成してから選択)すると JSON フォーマットの接続情報が表示されます。この中の username と password の値を後で使うことになるので、このページを開いたままにしておくなどしてコピー&ペーストできるようにしておきます:
2019020803


では改めてソースコードを入手します。https://github.com/dotnsf/urlrec から git clone するか、下図のように zip をダウンロード&展開してソースコードを手元にコピーします:
2019020804


ソースコードが入手できたら、settings.js ファイルをテキストエディタで開きます。そして exports.db_username の値を先程確認した IBM Cloudant の接続情報の username の値に、exports.db_password の値を同じく password の値に、それぞれ(コピー&ペースト等で)書き直して保存します:
exports.db_username = '(username の値)';
exports.db_password = '(password の値)';
exports.db_name = 'urlrec';
: :

Node.js が導入されていればローカル環境で動かすことも可能です。その場合は以下のコマンドを実行します:
$ npm install  (依存ライブラリを導入)

$ node app  (実行)

server stating on XXXX ...

成功すると上記のように "server starting on XXXX ..." と表示されます。この XXXX は数字になっているはずで、ウェブ魚拓サービスはこのポート番号で待ち受けている、ということを示しています。


では実際に使ってみましょう。先程確認した XXXX を使って、ウェブブラウザから http://(サーバー名 または IP アドレス):XXXX/ にアクセスします。以下のような画面が表示されれば成功です:
2019020805


画面上部の "URL" にキャッシュしたいページの URL を入力して "CHECK" ボタンを押すとプレビューが表示されます(このプレビューが表示されないページは非対応だと思ってください)。下の例では試しに自分のブログの1ページを指定してみました:
2019020801


そしてプレビュー画面で(必要に応じてテキストコメントを入力した上で) "Record" をクリックするとキャッシュがシステムに保存され、先程の画面内に一覧表示されます:
2019020802


この画面で一覧内の一番左の列(赤枠部分)をクリックすると、別ウィンドウが開いてキャッシュされた内容を確認することができる、というものです(元の URL の記事が削除されていても見れるようになります):
2019020803


UI的にもまだまだシンプルすぎる所があったり、元のページのスタイルの再現性など、まだまだ改良の余地はあると思っていますが、基本機能は実装できていると思ってます。MIT でソースコードを公開しているので、役に立つ場があればご自由に使っていただきたいです。

そろそろプロ野球のシーズン開幕前順位予想とかが始まるので、それらを片っ端から記録しておこうかなあ、と邪な考えを持っていたりします。 σ(^^;


個人的な野望としてはこれを元に更に改良してブロックチェーンと組み合わせて真偽証明付きにして・・・って感じかなあ。

このページのトップヘ