20201216のNode.jsに関する記事は12件です。

GmailのAPIでのエラー: Metadata scope doesn't allow format FULL

users.messages.getのAPIを使おうとしてのエラーメモです。

https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get

Metadata scope doesn't allow format FULL

というエラーが出ました。

ドキュメントにこれらのパーミッションが必要と書いてあるので許可してたのですが......

https://mail.google.com/
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.metadata
https://www.googleapis.com/auth/gmail.addons.current.message.metadata
https://www.googleapis.com/auth/gmail.addons.current.message.readonly
https://www.googleapis.com/auth/gmail.addons.current.message.action

検証環境

  • Node.js v15.3.0
  • googleapiモジュール v66

試したコード

//省略

gmail.users.messages.get({
    userId: 'me',
    id: `xxxx`,
    // format: 'FULL' 
}, (err, res) => {
   if (err) return console.log('The API returned an error: ' + err);

   console.log(res);
});

//省略

ちなみにallow format FULLとなっていたのは、何も指定しないとデフォルトのフォーマットがFULLになるからという理由です。

  • MINIMAL
  • FULL (デフォ)
  • RAW
  • METADATA

https://developers.google.com/gmail/api/reference/rest/v1/Format

/auth/gmail.metadataのパーミッションを許可しない

Metadata scope doesn't allow format FULL gmailとのことなのでそのままなのですが、APIドキュメントのテスターで試そうとしたら、/auth/gmail.metadataのパーミッションがチェック外れている状態でした。

スクリーンショット 2020-12-16 21.38.10.png

試しにチェックして試すと、テスター上も同様のエラーが発生。

ということでトークンを作成する際に/auth/gmail.metadataを許可しなければOKです。

トークン作成する際のスコープから/auth/gmail.metadataをはずしてトークンを作成しなおしたらいけました。

const SCOPES = [
    `https://mail.google.com/`,
    `https://www.googleapis.com/auth/gmail.modify`,
    `https://www.googleapis.com/auth/gmail.readonly`,
    // `https://www.googleapis.com/auth/gmail.metadata`,
    `https://www.googleapis.com/auth/gmail.addons.current.message.metadata`,
    `https://www.googleapis.com/auth/gmail.addons.current.message.readonly`,
    `https://www.googleapis.com/auth/gmail.addons.current.message.action`
];

まとめ

これはドキュメントミスな気がするけどどうなんでしょう。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SVG + CSS + Node.js + receiptline で電子レシートを発行してみよう

マークダウン言語で紙のレシートや電子レシートを簡単に作れる receiptline。
https://github.com/receiptline/receiptline
https://www.npmjs.com/package/receiptline

今回は receiptline 本来の用途と考えられる、電子レシートの発行です!

レシートを設計する

デザインツール

ReceiptLine Designer を使います。使い方はこの連載記事の初回から。
いますぐ試したい方は、開発元のホームページで公開されているのでこちらへ。
01.png

ロゴの背景を透明に

以前の記事で作成したロゴ画像を再利用します。
02.png

GIMP の「色を透明度に」 (Color to Alpha) 機能で、背景色を白から透明にしました。
03.png

2023年度インボイス制度対応

前回の記事で学習した、簡易適格請求書等の記載事項も追加しておきます。

  • ① 適格請求書発行事業者の氏名又は名称及び登録番号
  • ② 課税資産の譲渡等を行った年月日
  • ③ 課税資産の譲渡等に係る資産又は役務の内容(課税資産の譲渡等が軽減対象資産の譲渡等である場合には、資産の内容及び軽減対象資産の譲渡等である旨)
  • ④ 課税資産の譲渡等の税抜価額又は税込価額を税率ごとに区分して合計した金額
  • ⑤ 税率ごとに区分した消費税額等又は適用税率

04.png

作成したレシートデータ

ReceiptLine
{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC}
柳都市星降町7丁目8番9号
登録番号 T1234567890123

2020年12月16日(水)12:34  #0903
{border:line; width:22}
^領 収 書
{border:space; width:3,*,3,8; text:nowrap}
166003 |2021葱鏡餅 水引 | 3個| ¥1,620*
691004 |洗面器45RPM N025 | 1個| ¥1,210~
-
{width:auto; text:wrap}
小 計 |4点 | ¥2,830~
(税率10%対象 | ¥1,210)
(内消費税等10% | ¥110)
(税率 8%対象 | ¥1,620)
(内消費税等 8% | ¥120)
-
合 計 | ^¥2,830
お預り | ^¥3,000
お釣り | ^¥170
|*印は軽減税率対象商品です
{code:202012160903; option:code128,2,48}

電子レシート発行サーバーを作る

Node.js

Node.js で電子レシートを発行する HTTP サーバーを作ります。
receipt.js の内部構成は、以下のようになっています。

  • ReceiptLine
    • 作成したレシートデータ (固定) を SVG に変換する
  • HTML
    • 作成した SVG を HTML に埋め込む
  • HTTP サーバー
    • 任意の GET リクエストを受けて、作成した HTML を返す
receipt.js
const http = require('http');
const receiptline = require('receiptline');

// ReceiptLine
const text = `{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC}
柳都市星降町7丁目8番9号
登録番号 T1234567890123

2020年12月16日(水)12:34  #0903
{border:line; width:22}
^領 収 書
{border:space; width:3,*,3,8; text:nowrap}
166003 |2021葱鏡餅 水引 | 3個| ¥1,620*
691004 |洗面器45RPM N025 | 1個| ¥1,210~
-
{width:auto; text:wrap}
小 計 |4点 | ¥2,830~
(税率10%対象 | ¥1,210)
(内消費税等10% | ¥110)
(税率 8%対象 | ¥1,620)
(内消費税等 8% | ¥120)
-
合 計 | ^¥2,830
お預り | ^¥3,000
お釣り | ^¥170
|*印は軽減税率対象商品です
{code:202012160903; option:code128,2,48}`;
const svg = receiptline.transform(text, { cpl: 32, encoding: 'cp932', spacing: true });

// HTML
const style = 'float: left; padding: 24px; background: lavender;';
const html = `<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>レシート</title>
    </head>
    <body>
        <div style="${style}">${svg}</div>
    </body>
</html>`;

// HTTP Server
const server = http.createServer((req, res) => {
    switch (req.method) {
        case 'GET':
            res.end(html);
            break;
        default:
            res.end();
            break;
    }
});
server.listen(8080, "127.0.0.1", () => {
    console.log('Server running at http://127.0.0.1:8080/');
});

実行

電子レシート発行サーバーを起動します。

$ node receipt.js

Web ブラウザーで localhost:8080 を開きます。
receiptline ライブラリが動作しない IE11 でも表示 OK。
05.png

作成された SVG データ

レシートデータから生成された SVG データです。
中身はパス、画像、テキスト、Web フォント、フィルター、いろいろ入っています。

SVG
<svg width="384px" height="684px" viewBox="0 0 384 684" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><style type="text/css"><![CDATA[@import url("https://fonts.googleapis.com/css2?family=Kosugi+Maru&display=swap");]]></style><defs><filter id="receiptlineinvert" x="0" y="0" width="100%" height="100%"><feFlood flood-color="#000"/><feComposite in="SourceGraphic" operator="xor"/></filter></defs><g font-family="'Kosugi Maru', 'MS Gothic', 'San Francisco', 'Osaka-Mono', 'Courier New', 'Courier', monospace" fill="#000" font-size="24" dominant-baseline="text-after-edge"><g transform="translate(48,0)"><image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC" x="0" y="0" width="288" height="48"/></g><g transform="translate(0,72)"><text x="54,78,102,126,150,174,198,210,234,258,270,294,306">柳都市星降町7丁目8番9号</text></g><g transform="translate(0,102)"><text x="54,78,102,126,150,162,174,186,198,210,222,234,246,258,270,282,294,306,318">登録番号&#xa0;T1234567890123</text></g><g transform="translate(0,132)"><text x="0">&#xa0;</text></g><g transform="translate(0,162)"><text x="12,24,36,48,60,84,96,108,132,144,156,180,192,216,228,240,252,264,276,288,300,312,324,336,348,360">2020年12月16日(水)12:34&#xa0;&#xa0;#0903</text></g><g transform="translate(48,192)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">╔══════════════════════╗</text></g><g transform="translate(48,216)"><text transform="scale(1,1)" x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;&#xa0;</text><text transform="scale(2,1)" x="24,48,60,84,96">&#xa0;&#xa0;</text></g><g transform="translate(48,240)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">╚══════════════════════╝</text></g><g transform="translate(0,270)"><text x="0,12,24">166</text><text x="48,60,72,84,96,120,144,168,180,204">2021葱鏡餅&#xa0;水引</text><text x="240,252">3個</text><text x="300,312,324,336,348,360,372">¥1,620*</text></g><g transform="translate(0,300)"><text x="0,12,24">691</text><text x="48,72,96,120,132,144,156,168,180,192,204,216">洗面器45RPM&#xa0;N02</text><text x="240,252">1個</text><text x="300,312,324,336,348,360,372">¥1,210&#xa0;</text></g><g transform="translate(0,330)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288,300,312,324,336,348,360,372">════════════════════════════════</text></g><g transform="translate(0,360)"><text x="0,24,48">小 計</text><text x="132,144">4点</text><text x="300,312,324,336,348,360,372">¥2,830&#xa0;</text></g><g transform="translate(0,390)"><text x="0,12,36,60,72,84,96,120">(税率10%対象</text><text x="300,312,324,336,348,360,372">¥1,210)</text></g><g transform="translate(0,420)"><text x="0,12,36,60,84,108,132,144,156">(内消費税等10%</text><text x="324,336,348,360,372">¥110)</text></g><g transform="translate(0,450)"><text x="0,12,36,60,72,84,96,120">(税率&#xa0;8%対象</text><text x="300,312,324,336,348,360,372">¥1,620)</text></g><g transform="translate(0,480)"><text x="0,12,36,60,84,108,132,144,156">(内消費税等&#xa0;8%</text><text x="324,336,348,360,372">¥120)</text></g><g transform="translate(0,510)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288,300,312,324,336,348,360,372">════════════════════════════════</text></g><g transform="translate(0,540)"><text x="0,24,48">合 計</text><text transform="scale(2,1)" x="120,132,144,156,168,180">¥2,830</text></g><g transform="translate(0,570)"><text x="0,24,48">お預り</text><text transform="scale(2,1)" x="120,132,144,156,168,180">¥3,000</text></g><g transform="translate(0,600)"><text x="0,24,48">お釣り</text><text transform="scale(2,1)" x="144,156,168,180">¥170</text></g><g transform="translate(0,630)"><text x="0,12,36,60,84,108,132,156,180,204,228,252,276">*印は軽減税率対象商品です</text></g><g transform="translate(91,636)"><path d="M0,0h4v48h-4zM6,0h2v48h-2zM12,0h6v48h-6zM22,0h4v48h-4zM30,0h2v48h-2zM36,0h6v48h-6zM44,0h4v48h-4zM52,0h2v48h-2zM58,0h6v48h-6zM66,0h2v48h-2zM70,0h4v48h-4zM78,0h6v48h-6zM88,0h2v48h-2zM94,0h6v48h-6zM102,0h4v48h-4zM110,0h4v48h-4zM118,0h2v48h-2zM124,0h2v48h-2zM132,0h2v48h-2zM138,0h2v48h-2zM144,0h4v48h-4zM154,0h4v48h-4zM162,0h2v48h-2zM166,0h6v48h-6zM176,0h4v48h-4zM186,0h6v48h-6zM194,0h2v48h-2zM198,0h4v48h-4z" fill="#000"/></g></g></svg>

電子レシートの背景をカスタマイズする

ソースコードの CSS を変更して、電子レシートの背景をカスタマイズします。

単色

お店のキャンペーンでよく使われるピンクレシートと黄色いレシート。

receipt.js
const style = 'float: left; padding: 24px; background: pink;';

06.png

receipt.js
const style = 'float: left; padding: 24px; background: #ff9;';

07.png

多色

サーマルロール紙にはできないグラデーション。

receipt.js
const style = 'float: left; padding: 24px; background: linear-gradient(skyblue, lightyellow);';

08.png

receipt.js
const style = 'float: left; padding: 24px; background: linear-gradient(lightgreen, snow, beige);';

09.png

透かし

「複写」「COPY」の画像をタイル状に並べます。画像は CSS に埋め込みます。
10.png

画像の Data URI 形式への変換は ReceiptLine Designer を使うと簡単です。
画像をロードして image:data:image/png;base64, に置き換えます。
11.png

receipt.js
const style = 'float: left; padding: 24px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANgAAADYAQMAAACz7r+uAAAABlBMVEXc3tv5+/j5MJA8AAADWklEQVRYw+2YPW7bMBSAqTAwMxTmBVwqR2g3G1AiH8W9gYMsCuLGSg3UW3yBwDlElwLNYMGDlza+QFsoMFqvDAy0NCpENQqRj3yCshfRmyh9En8eH98PCamllucih0+wPjT3MFPQZAh5GbQ5YvQ7tH3E2ATaAWK8Ae0MMXEAXUjEgpZpDjGLzCA0TxGTkelSIeal0tP/XSJGk9Uo1t/N0PJGi0UV478nS9MHYiL3Hw2L0fIuWpnF9t3lRQrGTps2GwQS2Fg5O3H88h765NmxzXx//tXowRcDZw18PIVJc2WrpcG5ME8tZ5cYYQKYoLZGOWkEwJpsZg2429hoYA1+mQPrxATWR9gNy0FvpzMvBcOmj3xu2It1aivfy3zYJ38pma18dUzMTP2pcuy8HwDjt1nDOW+Rxc6u8YEzjG3ayCI9YD/P4wrWYNnaZbAk7m8XLuuxpHjB8uHNO3cufFJ0SrMwv3KZr7d3p6M/U2SSLNPsy8Y9q35GivNIPydr5R4BRQoTpVcJOo+7JWjGt3mMfZE2bb5dILJnGNu+x+4mNiy5wgz+W01LTBhvc11i2qewqF1i2lxZEJXmopVBRR+7zEtZ5U2JB4piJbXksyq3sXOG0EVcB8Faaqmllv9TIGW5KMWHGUSKEksh/S3FAAg5CUpHyX7U1SzGaXMz0NFLEMyEiWwBwTGuxWUl6yw0i0p9vp7LcupUyInFephNZLnG0WFzIrsVjOajdjFMm3C3nuCrUccw381twsWHT4UaRT+8ddjbpfILxuXAydLJ2bdMMyaVW2e92T7qisZLpMu8zU2mZ0dTxNZh2jN5Fard7k7BKGLEWmsJuQbKRI7u+sCEW2CG1+Y/SkTDzSM7CsYLnE4l31gs82Pbctn6o3lIVAgm2qAzurT2Mo3ALDiLPSuZG85k36pXnJoztGuEQPRI5GSjkNq1A0mcaia1WCQPbcPj0mIPst+1D0cEIxytlJMfBqGCMmRx65TGEZdQhkxajur7LAUmhEBlP7AgDKpcQ6iGUZVLOZe/ZBV7uP+BS2PD0vkc+RQa6xddNka+iI6hXirdJkD66y7vX0UDllu6TYB5H+BU3DK0V/jGwKqFT7C7GdiWVX1ZEsZVnrZ8IWK7reYTAWC/joG1PBv5C4viMg7LEHCAAAAAAElFTkSuQmCC);';

12.png

地紋

領収書やチケットでよく見かける地紋です。小さな素片をタイル状に並べます。
13.png

const style = 'float: left; padding: 24px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADAQMAAACplL1tAAAABlBMVEX/pQD////52iT3AAAAFElEQVQI12M4l/+AYf/mCQyFdwoAJNIF3T0xRTsAAAAASUVORK5CYII=);';

14.png

また何か作ったら投稿します。ではまた!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js + NoSQL(Couchbase) アプリ開発 ステップバイステップガイド (1)

はじめに

Node.js + NoSQL(Couchbase) を使ったアプリ開発をステップバイステップで解説していきます。本記事は、シリーズの初回として位置付けられます。

Node.jsコミュニティに向けて

なぜ、NoSQLデータベースを使うのか

NoSQLの中でも、ここで扱うドキュメント指向データベース(ドキュメントストア)は、データとしてJSONドキュメントを格納します。言わずもがなですが、JSONとは「JavaScript Object Notation」の略で、Node.jsアプリ開発のバックエンドデータベースとしてドキュメントストアを利用することは、MEANスタック(MongoDB、Express、AngularJS、Node.js)にもみられように、自然と言えます。

なぜ、Couchbaseを使うのか

NoSQL技術同士の比較については、この記事の目的ではないため、多くは触れません。

先にあげたMEANスタックからもわかるように、MongoDBがまずは筆頭に上がる状況を踏まえ、あまり目にする機会のない、日本語によるCouchbaseについての紹介として、興味を持っていただければ幸いです。

一つだけ、Couchbaseに独自の部分として、SQLのJSONドキュメント用の拡張である、Couchbaseのクエリ言語、N1QL(Non 1st-normal-form Query Language: 非第一正規化クエリ言語)の利用について、最後に解説します。

前提

  • Node.js
    • 開発環境がセットアップされている
  • Couchbase Server
    • 開発環境にインストールされている(localhostでアクセス可能)
    • 管理者ユーザが作成されている(ID: Administrator、パスワード: password
    • Bucket(test)が作成されている
    • コマンドラインツールにパスが通っている

動作確認環境

  • Mac OS Catalina Version 10.15.6
    • コマンドラインツールパス:/Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/
  • Node.js
    • node v15.4.0
    • npm 7.0.15
  • Couchbase Server 7.0 BETA

プロジェクト準備

プロジェクト用のディレクトリを作成し、その中で作業を開始します。

はじめに下記を実行します。

$ npm init -y

package.jsonファイルが作成されます。

Couchbase SDKをインストールし、package.jsonに追記します。

$ npm install couchbase --save

package.jsonに以下のような依存関係が追記されます。

  "dependencies": {
    "couchbase": "^3.1.0"
  }

プログラム作成

index.jsファイルを作成し、以下の内容を追加していきます。

Couchbaseへの接続

const couchbase = require("couchbase");

const cluster = new couchbase.Cluster("couchbase://localhost", {
  username: "Administrator",
  password: "password",
});

バケットへの接続

const bucket = cluster.bucket("test");

const collection = bucket.defaultCollection();

サンプルデータ定義


const user = {
  type: "user",
  id: 1,
  name: "田中",
};

ドキュメント登録関数定義

const upsertDocument = async (doc) => {
  try {
    const key = `${doc.type}_${doc.id}`;
    const result = await collection.upsert(key, doc);
    console.log("Upsert: ");
    console.log(result);
  } catch (error) {
    console.error(error);
  }
};

ドキュメント取得関数定義

const getDocumentByKey = async (key) => {
  try {
    const result = await collection.get(key);
    console.log("Get: ");
    console.log(result);
  } catch (error) {
    console.error(error);
  }
};

処理実行部

upsertDocument(user);

getDocumentByKey("user_1");

プログラム実行

$ node index.js

下記のような結果が出力されます。

Upsert: 
{
  cas: CbCas { '0': <Buffer 00 00 05 1b f9 1e 51 16> },
  token: CbMutationToken {
    '0': <Buffer d8 18 d7 a9 dd 24 00 00 03 00 00 00 00 00 00 00 2f 00 00 00 00 00 00 00 74 65 73 74 00 00 00 00 28 a2 03 05 01 00 00 00 40 a2 03 05 01 00 00 00 28 a2 ... 230 more bytes>
  }
}
Get: 
{
  cas: CbCas { '0': <Buffer 00 00 05 1b f9 1e 51 16> },
  content: { type: 'user', id: 1, name: '田中' }
}

Ctrl+Dでプロセスを抜けてください。

実行結果確認

Couchbase Serverのコマンドラインツール(cbq)を使って、登録されたデータを確認します。

$ cbq -u Administrator
 Enter Password: 
 Connected to : http://localhost:8091/. Type Ctrl-D or \QUIT to exit.

クエリ(SELECT * from test;)を実行してみます。

以下のようなエラーになりました。

cbq> SELECT * from test;
{
    "requestID": "00fda8f3-d64e-4d8c-85c1-ad810f921bd2",
    "errors": [
        {
            "code": 4000,
            "msg": "No index available on keyspace test that matches your query. Use CREATE INDEX or CREATE PRIMARY INDEX to create an index, or check that your expected index is online."
        }
    ],
    "status": "fatal",
    "metrics": {
        "elapsedTime": "438.814µs",
        "executionTime": "368.037µs",
        "resultCount": 0,
        "resultSize": 0,
        "serviceLoad": 2,
        "errorCount": 1
    }
}
cbq> 

メッセージ("No index available on keyspace test that matches your query. Use CREATE INDEX or CREATE PRIMARY INDEX to create an index, or check that your expected index is online.")にもある通り、クエリを実行するには、インデックスが作成されている必要があります。

プライマリインデックスを作成します。

cbq> CREATE PRIMARY INDEX test_primary ON test;
{
    "requestID": "2c482905-b794-4092-bb0b-8d8cb76bc535",
    "signature": null,
    "results": [
    ],
    "status": "success",
    "metrics": {
        "elapsedTime": "1.872931032s",
        "executionTime": "1.872853887s",
        "resultCount": 0,
        "resultSize": 0,
        "serviceLoad": 2
    }
}
cbq> 

先ほどのクエリを再度、実行します。

以下のようにクエリの結果が表示されました。

cbq> SELECT * from test;
{
    "requestID": "9b80a8eb-3605-4ff8-84fb-0a435959e4e1",
    "signature": {
        "*": "*"
    },
    "results": [
    {
        "test": {
            "id": 1,
            "name": "田中",
            "type": "user"
        }
    }
    ],
    "status": "success",
    "metrics": {
        "elapsedTime": "78.632291ms",
        "executionTime": "78.571384ms",
        "resultCount": 1,
        "resultSize": 113,
        "serviceLoad": 2
    }
}

結果("results"の値)が幾分冗長なので、以下のようにクエリを微修正して、実行します。

cbq> SELECT test.* from test;
{
    "requestID": "14dae1e2-cf81-45ae-8c55-2f64ad975743",
    "signature": {
        "*": "*"
    },
    "results": [
    {
        "id": 1,
        "name": "田中",
        "type": "user"
    }
    ],
    "status": "success",
    "metrics": {
        "elapsedTime": "1.034558ms",
        "executionTime": "969.805µs",
        "resultCount": 1,
        "resultSize": 73,
        "serviceLoad": 2
    }
}

最後に

今回の記事は以上です。

今回のプログラムでは、キーによるアクセスのみを扱い、クエリでのアクセスについては、コマンドラインツールのみで扱いましたが、クエリをプログラムで実行することも、当然可能です。また、クエリについては、今回紹介したような単純なものではなく、一般にSQLを使った開発で用いられているようなもっと複雑なものが利用可能です。これについては、今後の記事で紹介できればと思っています。

また、Node.jsの本来のあり方を考えると、上記のような一度だけ実行するプログラムを作成・実行することは、直感的ではないかもしれません。サーバ・プログラム化についても、今後の記事で紹介していきたいと考えています。

今回は、非常に単純なプログラムとして作成したことで、データベースへの接続と、データの登録・取得という、本質的な部分のみを見て行くことができたかと思います(Node.js経験者にとっては、それ以外の部分は、よく知られているところかとも思います)。また、MongoDBの経験者が読まれた際に、Couchbaseでの実装について、MongoDBと比べて、どのような違い(と共通点)があるかが分かりやすいものになっていれば幸いです。

最後に、ここで紹介したプログラムを下記に公開していますので、適宜ご参照・ご利用ください。

https://github.com/YoshiyukiKono/couchbase_step-by-step_node_jp

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub ActionsでNodeサーバーでbuildを走らせる

目的:

記事にヘッドレスCMS+SSGを導入することに当たってNodeサーバー構築し、
GitHubActionでpush時に自動でgitから最新ソースをpull後にnodeサーバーでbuildする仕組みを実装

前提:

下記のNodeサーバー構築完了
https://qiita.com/kerry/items/7a414f6fa2ca793c407e

必要な情報をGitHubに登録

下記の必要なサーバーへ接続情報(envに設定する)をGitHubに登録する。
GitHubのSettingsタブの一番下のSecretsメニューを選択して順番に登録する。

      AWS用のACCESS_KEY(secrets.AWS_ACCESS_KEY_ID)
      AWS用のSECRET_ACCESS_KEY(secrets.AWS_SECRET_ACCESS_KEY_ID)
      AWS用のSECURITY_GROUP_ID(secrets.AWS_SECURITY_GROUP_ID_STG)
      SSH用の鍵(secrets.PRIVATE_KEY_STG)
      SSH用のユーザー名(secrets.USER_NAME)
      SSH用のHost(secrets.HOST_NAME_STG)

GitHubActionsのworkflow作成

GitHubのActionsタブに入って新しくTempleteを利用して
.github/workflows/stag.ymlファイルを作成

処理の流れ:

・Github ActionsのイメージのIPを取得
・AWS CLIをインストール
・AWS CLIの設定
・EC2のセキュリティグループにルールを追加
・SSH接続する
・サーバーでGit pull処理
・nodeサーバーでbuild処理
・EC2のセキュリティグループからルールを削除

実装コート:

name: deploy stag

on:
  push:
    branches: [stag]

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/stag'
    steps:
      - uses: actions/checkout@v2

      - name: deploy stag
        env:
          ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
          SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_ID }}
          SECURITY_GROUP: ${{ secrets.AWS_SECURITY_GROUP_ID_STG }}
          PRIVATE_KEY: ${{ secrets.PRIVATE_KEY_STG }}
          USER_NAME: ${{ secrets.USER_NAME }}
          HOST_NAME: ${{ secrets.HOST_NAME_STG }}
        run: |
          IP_ADDRESS=`curl https://api.ipify.org/ -s`

          # AWS CLIインストール
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          sudo ./aws/install
          aws --version

          # AWS CLI設定
          printf "${ACCESS_KEY}\n${SECRET_ACCESS_KEY}\nap-northeast-1\njson\n" | aws configure --profile blog-stag
          aws configure get aws_access_key_id --profile blog-stag

          # セキュリティグループにルールを追加
          aws --profile blog-stag ec2 authorize-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32

          # sshキーをコピー
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/private_key.pem
          echo "$PRIVATE_KEY" > /home/runner/.ssh/private_key.pem
          chmod 600 /home/runner/.ssh/private_key.pem

          # known_hostsに追加
          ssh-keyscan ${HOST_NAME} >> ~/.ssh/known_hosts

          # テスト環境にclone
          #cd ./deploy && git clone git@github.com:test/blog.git /home/ec2-user/deploy

          # SSH接続して、git pullする
          ssh -o StrictHostKeyChecking=no -i /home/runner/.ssh/private_key.pem ${USER_NAME}@${HOST_NAME} '
          source ~/.bash_profile &&
          cd /home/ec2-user/deploy/blog &&
          git checkout stag && git fetch --all &&
          git reset --hard origin/stag &&
          git pull origin stag &&
          chmod +x /home/ec2-user/deploy/deploy_node.sh &&
          sh /home/ec2-user/deploy/deploy_node.sh
          '

          # セキュリティグループからルールを削除
          aws --profile blog-stag ec2 revoke-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32

上記の実行しているシェルの中身
sh deploy_node.sh
```

!/bin/sh

nodenv local 14.15.1
npm ci
npm run build
```

今回はサーバー上に静的htmlを吐き出したがったので、pull後に直接nodeサーバー内でbuildする仕組みを実装したが、下記のようにS3に保存することも可能

S3にアップ

上記の情報以外に下記の情報が追加で必要

      AWS用のS3_BUCKET名(secrets.AWS_S3_BUCKET)

実装コート:

name: deploy stag

on:
  push:
    branches: [stag]

jobs:
  deploy:
    name: git pull origin
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/stag'
    steps:
      - uses: actions/checkout@v2

      - name: deploy stag
        env:
          ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
          SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_ID }}
          SECURITY_GROUP: ${{ secrets.AWS_SECURITY_GROUP_ID_STG }}
          PRIVATE_KEY: ${{ secrets.PRIVATE_KEY_STG }}
          USER_NAME: ${{ secrets.USER_NAME }}
          HOST_NAME: ${{ secrets.HOST_NAME_STG }}
        run: |
          IP_ADDRESS=`curl https://api.ipify.org/ -s`

          # AWS CLIインストール
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          sudo ./aws/install
          aws --version

          # AWS CLI設定
          printf "${ACCESS_KEY}\n${SECRET_ACCESS_KEY}\nap-northeast-1\njson\n" | aws configure --profile blog-stag
          aws configure get aws_access_key_id --profile blog-stag

          # セキュリティグループにルールを追加
          aws --profile blog-stag ec2 authorize-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32

          # sshキーをコピー
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/private_key.pem
          echo "$PRIVATE_KEY" > /home/runner/.ssh/private_key.pem
          chmod 600 /home/runner/.ssh/private_key.pem

          # known_hostsに追加
          ssh-keyscan ${HOST_NAME} >> ~/.ssh/known_hosts

          # テスト環境にclone
          #cd ./deploy && git clone git@github.com:com:test/blog.git /home/ec2-user/deploy

          # SSH接続して、git pullする
          ssh -o StrictHostKeyChecking=no -i /home/runner/.ssh/private_key.pem ${USER_NAME}@${HOST_NAME} '
          cd /home/ec2-user/deploy/blog &&
          git checkout stg && git fetch --all &&
          git reset --hard origin/stg &&
          git pull origin stg
          '

          # セキュリティグループからルールを削除
          aws --profile blog-stag ec2 revoke-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32

  build:
    name: build and deploy to s3
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/stag'
    strategy:
      matrix:
        node-version: [14.15.1]
    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

      - name: s3 sync
        uses: jakejarvis/s3-sync-action@master
        with:
          args: --acl public-read --follow-symlinks --delete
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_ID }}
          AWS_REGION: 'ap-northeast-1'
          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
          SOURCE_DIR: 'out'

今回ハマったところ

nodenvコマンドのところで下記のエラーが発生。

bash: line 5: nodenv: command not found

直接サーバーでは問題なくbuildできたのに、、
ssh接続後下記を追加で解決

source ~/.bash_profile

https://superuser.com/questions/564926/profile-is-not-loaded-when-using-ssh-ubuntu

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BOXのダウンロードの流れとSDK無しでダウンロードする方法

BOXからファイルをダウンロードする時にSDKを使わないと少し戸惑うのでメモ。

大まかな処理の流れ

利用するAPIの説明はこれです。
https://ja.developer.box.com/reference/get-files-id-content/

このAPIを叩くと、レスポンスが302で帰って来る時は、レスポンスヘッダーでLocationが戻されます。
このLocationには、https://dl.boxcloud.com/d/1/b1!V1LhT0hCN.... のようなURLが入ってきます。
このLocationから実際にファイルをダウンロードします。

レスポンスは2パターンあって、ステータスコードが302の場合は、ヘッダーにLocation202の場合は、ヘッダーにRetry-Afterが戻され、クライアントがファイルをダウンロードできるようになるまでの秒数を示します。

ためしても202がもどされなかったので、202が発生して、Retry-Afterが戻される時、curlとかaxiosではどういう挙動になるのか不明です。
ここに、curl 7.66.0からRetry-Afterに従うようなことがかいてあるけど、どうなんだろう。試してないので動くかわかんないです。

SDKにはこのあたりの処理が間違いなくちゃんと入ってます。
オートメーションとか連携に使うなら、アクセストークンの取り回しなどもあるので、SDKで(特にJWT認証で)使うのがいいでしょうね。

APIをcurlで実行

ダウンロード用URLの取得

まず、Locationをフォローしないかたちで(-Lを付けずに)以下のように実行してみます。

curl -i -X GET "https://api.box.com/2.0/files/690849498295/content" -H "Authorization: Bearer XXXXXXXXXX"

以下のようなレスポンスが帰ってきます。

HTTP/1.1 302 Found
Date: Wed, 16 Dec 2020 02:28:43 GMT
Transfer-Encoding: chunked
Strict-Transport-Security: max-age=31536000
Cache-Control: no-cache, no-store
BOX-REQUEST-ID: 17d4c6d940827c69342f2705eb8df260c
Location: https://dl.boxcloud.com/d/1/b1!V1LhT0hCNCBXXWBMjANGzmCxSWNA0BsyXMMYGOAdG7ZSlBs4FzPrGPjDASb06VFsGc (めちゃくちゃ長いので省略) jLux9WOM./download
Connection: close

ふむふむ、Locationが入ってます。

ダウンロード用URLまで続けて実行

curlでは、-L を付けて実行すると、上のレスポンスヘッダーのLocationに入ってきたURLに自動的に移動してくれるので、ヘッダーからLocationを取り出すような操作は不要です。

curl -i -X GET "https://api.box.com/2.0/files/690849498295/content" -H "Authorization: Bearer XXXXXXXXXX" -L

以下が帰ってきます。

HTTP/1.1 200 OK
Date: Wed, 16 Dec 2020 06:32:08 GMT
Content-Type: image/png
Content-Length: 86195
Connection: keep-alive
Accept-Ranges: bytes
Cache-Control: private
X-Envoy-Upstream-Service-Time: 116
Content-Disposition: attachment;filename="baseball_girl.png";filename*=UTF-8''baseball_girl.png
X-Robots-Tag: noindex, nofollow
Encryption_Policy_Id: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000

Warning: Binary output can mess up your terminal. Use "--output -" to tell 
Warning: curl to output it to your terminal anyway, or consider "--output 
Warning: <FILE>" to save to a file.

-Lをつけると、一気にファイルを含んだレスポンスが帰ってきます。
--outputつけてファイルに出力しろって警告してますね。
ファイルに出力するんだったら、-iで詳細情報だすのは不要です。

ファイルに保存

-i を外し、-o/--output で、出力先のファイルを指定すると、ファイルを直接保存可能になります。
APIに対して1発でファイルをダウンロードするコマンドは以下のようになります。

curl -X GET "https://api.box.com/2.0/files/690849498295/content" -H "Authorization: Bearer XXXXXXXXXX" -L -o sample.png

これで一気にファイルがダウンロードできました。

Node.js(Axios)からの実行

Node.jsから実行してみるとこんな感じです。
axiosをつかっていますが、デフォルトで1つめのレスポンスに含まれるLocationをフォローしてくれますね。
ここではresponseTypestreamを指定して、そのままWriteStreamに書き出してます。

const axios = require("axios");
const fs = require("fs");
const accessToken = "XXXXXXXXXX";

axios
  .get("https://api.box.com/2.0/files/690849498295/content", {
    headers: { Authorization: `Bearer ${accessToken}` },
    responseType: "stream",
  })
  .then((result) => {
    const output = fs.createWriteStream("sample.png");
    result.data.pipe(output);
  })
  .catch((error) => {
    console.log(error.message);
  });

Box node SDKを使う

ちなみに、Box Node SDKをつかうとこんな感じです。
わざわざアクセストークンを貼るなどせず、JWT認証をつかって認証周りの制御もやってます。
自動化するならBox公式SDKをつかうのが一番らくでしょうね。

const boxSDK = require("box-node-sdk");
const config = require("./config.json");
const fs = require("fs");
const sdk = boxSDK.getPreconfiguredInstance(config);
const client = sdk.getAppAuthClient("enterprise");

client.files.getReadStream("690849498295", null, function (error, stream) {
  if (error) {
    return; // handle error
  }
  const output = fs.createWriteStream("./sample.png");
  stream.pipe(output);
});

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jest のテストがメモリ不足で落ちまくっていたので、調査方法を調べてみた

TL;DR

簡易的にメモリの利用状況を知りたいのであれば次のオプションで jest 実行

実行例:

$ node --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage
PASS  test/a.e2e-spec.ts (10.43 s, 160 MB heap size)
PASS  test/b.e2e-spec.ts (18.028 s, 197 MB heap size)
PASS  test/c.e2e-spec.ts (5.313 s, 215 MB heap size)
...

ちなみにこの実行例は実際にメモリリークが発生してるテストなので、リークしていなければ基本的にここの値は大きく変わらないはず

Google Chrome の inspect 機能を使いたい場合は次のオプションで jest 実行

実行例:

$ node --inspect-brk ./node_modules/.bin/jest --runInBand
Debugger listening on ws://127.0.0.1:9229/8ede11f0-7073-49bc-8e17-c16e661197d2
For help, see: https://nodejs.org/en/docs/inspector
# Chrome の chrome://inspect を開いて `inspect` ボタンをクリックするとを押すと進む
Debugger attached.

# Chrome の inspect ツールから breakpoint からの開始ボタンをクリックすると進む
RUNS  test/operations/event.e2e-spec.ts
...

ちなみに「これで最低限作業できる」ってオプションなので、「他のオプションを組み合わせたらもっと有用なデータが取れる」みたいなことはあるかもしれない。
もしご存知の方がいればコメントいただけると幸い。

オプションたち

--runInBand

Jestを実行したプロセスをそのままテスト実行プロセスとして利用し、全てのテストを直列で実行するデバッグ用のオプション。

--maxWorkers=1 と似ているが子プロセスを生成しないという点が違う。はず。
内部の実装を見ていないのでもしかすると同じなのかもしれないけど。

あと --runInBand--maxWorkers を同時に指定すると「バカなの?」と聞かれてコマンドが終了するので「わぁ、同時に指定しちゃった!どっちだ!」みたいなことにはならない。

--logHeapUsage

それぞれのテストでのメモリ使用量を出力してくれるオプション。
Jest の --runInBand オプションと node (v8) の --expose-gc オプションの設定が必要とのこと。

付けると時間情報の部分にメモリの利用量も表示されるようになる。

例:

# オプション無し
PASS  test/a.e2e-spec.ts (10.43 s)

# オプション有り
PASS  test/a.e2e-spec.ts (10.43 s, 160 MB heap size)

「メモリ利用量」と連呼しているけど、あくまでも「ヒープメモリ」であることには注意。
ただデバッグしてる時はそこまで気にする事でも無い。

--expose-gc

Node.js のオプションかと思ったら v8 のオプションらしい。
Web 上でのオプションのドキュメントは見当たらなかったのでオプションのありかだけ。

$ node --v8-options | grep expose-gc
  --expose-gc (expose gc extension)
  --expose-gc-as (expose gc extension under the specified name)

要は GC の情報を外部のアプリケーションからも触れるようにしてあげることでメモリの利用状況をより細かに取得できるようにしているんだと理解している。
--logHeapUsage を取りたいなら必要だと言われているから付ける。以上。

--inspect-brk

こちらは Node.js のオプション。
デバッグ用の inspector というサービスを立ち上げると同時に、ユーザーのアプリケーション実行前に Break してくれる。 --inspect というオプションを使うとデバッガーを接続した瞬間にアプリケーションコードが実行される。基本的に自分が手動で実行する分には --inspect-brk で良いと思う。

ToDo

あとで chrome を使った inspect 周りを書く

参考文献

https://dev.to/pustovalov_p/reducing-jest-memory-usage-1ina
「こうすれば Jest のメモリ利用状況把握できるよ」ってのを教えてくれた記事
ただ、結論が「--logHeapUsage するとその度に gc が走るからそれでパフォーマンス改善したよ。」だったので対処法は別途調べ中。

https://www.alexkras.com/simple-guide-to-finding-a-javascript-memory-leak-in-node-js/
Node のメモリリークのデバッグ方法を書いている記事
細かいメモリリークへの対応が書いてあるけど、「自分のコードにメモリの stats 出すコードを埋め込んで調査する」が基本スタイルなので、今回の jest のメモリ利用状況把握とは若干噛み合わない部分も。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nodemailerでWindowsパスを使って添付ファイルを指定する。

CSVでメールの情報を受け取ってnodemailerで送信するスクリプトを書いていた。
CSVには添付ファイルの情報がwindows形式パスで入ってくる。

C:\hoge\hoge.jpg

↓以下のコードで送信できると思って一生懸命実行しまくっていた

let message = {
  from: config.FromAddress,
  to: targetMail.to,
  subject: targetMail.subject,
  text: targetMail.body,
  attachments: [
    {
      filename: 'hoge.jpg',
      path: 'C:\\hoge.jpg'
    },
  ]
};

送れない。。。
結果、これで送ることが出来た

let message = {
  from: config.FromAddress,
  to: targetMail.to,
  subject: targetMail.subject,
  text: targetMail.body,
  attachments: [
    {
      filename: 'hoge.jpg',
      content: fs.createReadStream('C:\\hoge.jpg')
    },
  ]
};

パスではなくストリームを渡す

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヘッドレスCMS+SSGを導入することに当たってNodeサーバー構築

目的:

担当する記事サイト改修でSSG/ヘッドレスCMSを導入する

問題:

・Vercelの利用を考えましたが、ドメイン変更が必要。
・SSG/ヘッドレスCMSを導入したいけど、ドメイン変更したくないというディレクターの要望。

検討

・とりあえず既存サーバー内にNodeサーバー構築し、いろいろ試すことに

nodenvとは

・nodeのバージョンを管理するためのコマンド
・プラグイン(nodenv-build)を入れることで、nodeのインストールが可能

nodenvのダウンロード

GitHubからnodenvのソースをcloneする

git clone https://github.com/nodenv/nodenv.git ~/.nodenv

nodenvのビルド

nodenvをビルドして実行可能な状態にする

cd ~/.nodenv && src/configure && make -C src

pathを通す

echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(nodenv init -)"' >> ~/.bash_profile

pathを通してコマンドを叩けるようになる

シェルを再起動する

exec $SHELL -l

ログインシェルを再起動して新しい設定ファイルを読ませる

nodenvバージョンの確認

nodenv --version

プラグインのインストール

プラグイン用のディレクトリ作成

mkdir -p "$(nodenv root)"/plugins

プラグインのインストール(clone)

git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-build

nodenvアップデート用プラグインのインストール(clone)

git clone https://github.com/nodenv/nodenv-update.git "$(nodenv root)"/plugins/nodenv-update

nodenv updateとコマンドを叩くことで、nodenvとそのプラグインを自動的にアップデート可能

nodeのインストール

インストール可能nodeバージョンの確認

nodenv install --list

nodeのインストール(14.15.1は現時点でのLTSバージョン)

nodenv install 14.15.1

rehash実行

nodenv rehash

・nodenvからnodeやグローバルなnpmパッケージを見えるようにするため
・新しいnodeのバージョンを入れたり、npm install -gなどを行ったときに実行する必要がある

nodeバージョンの確認

nodenv versions
* 14.15.1 (set by /home/ec2-user/***_deploy/blog/.node-version)

nodeバージョンの指定

プロジェクトで使うnodeのバージョンを指定
プロジェクトのディレクトリでのみ使うnodeのバージョンを指定する

cd /home/ec2-user/xxx_deploy/blog
nodenv local 14.15.1

プロジェクトのディレクトリ (/home/ec2-user/xxx_deploy/xxx_blog)内に.node-versionという名前の不可視ファイルが生成され、その中にそのディレクトリで使うnodeのバージョンが記述

ちなみにグローバルで使う場合のnodeバージョン指定

nodenv global 14.15.1

グローバルとローカル両方指定した場合、ローカルで指定した方が優先らしい。

build

npm ci(少し時間かかる)
npm run build

Compiled successfully、Export successfulメッセージでbuildが完了し、プロジェクトのディレクトリにout(デフォルト)というフォルダが出来て色々吐き出される。
outフォルダ内の記事のhtmlファイルを開くと問題なく記事が表示されたのでbuildが正常に行われたと判断。

スクリーンショット 2020-12-16 13.43.43.png

その他nodeコマンド

nodeアンインストール

nodenv uninstall 14.15.1

nodenvのアップデート

nodenv update

nodenvのアンインストール

~/.nodenvのディレクトリを消すだけ

rm -rf $(nodenv root)

サーバーで直接コマンド実行でbuildが正常に行われたので次はGitHubActtionでpush時に自動でgitから最新ソースをpull後にnodeサーバーでbuildする仕組みを下記のように実装。
https://qiita.com/kerry/items/5568232516b8ee323d4b

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

特定のパッケージを使用しているローカルリポジトリを探す

はじめに

この記事は、grepコマンドによる依存パッケージの調査方法についてのメモです。

GitHubのDependabotなど、依存パッケージの脆弱性警告ツールが普及しました。しかしその警告に従い、複数のリポジトリをまたいで依存パッケージを調査するのは骨の折れる作業です。

こうした状況で便利なコマンドがgrepです。grepは多くの環境に移植されていますので、使い方を一度憶えてしまえば役立つ資産となります。

grep

grepは正規表現によりファイルを検索するUNIXコマンドです。

man page of grep

このコマンドを利用すれば、ファイルを横断した本文検索ができます。

使用例

% grep -lr <pattern> --include="package.json" --exclude-dir=node_modules ./*

このコマンドの内容を書き下すと、以下のようになります。

カレントディレクトリ./*以下で、ファイル名package.json<pattern>を含む行を探す。ただしディレクトリnode_modulesは除外する。

使用しているオプションは以下の通りです。

  • -r : ディレクトリを再帰処理(デフォルトではディレクトリ直下のファイルを検索する)
  • -l : ファイル名のみを表示(デフォルトでは該当する行も同時に表示する)

他にも検索パターンやオプションを変更すれば、grepは様々な問題の解決に役立ちます。皆様の問題に合わせてオプションを変更してみてください。

以上、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

秘伝のServerlessFrameworkをアップデートする

はじめに

こんにちは。本年度からSREになりました蔭山です。
今回はサービスローンチ時より使い続けているServerlessFrameworkを(作業当時の)1系最新バージョンだった1.83.0へバージョンアップ対応を行いました。
そこでどのようにアップデートを行ったのか、実際の取り組み内容を元にご紹介します。

当時の状況

私達のチームが運営しているサービスはAWSのLambdaをメイン基板としたシステムとなっています。サービスローンチ時より、アプリケーション側のリリース管理フレームワークとしてServerlessFrameworkを利用してきました。
ServerlessFrameworkを導入してからバージョンは1.36.2(2019/01/21リリース) で固定し、サービスを成長させてきました。

発生した問題

このような状態でプロダクトを成長・運用してきていましたが、月日が経つと以下のような問題も発生してきました。

  • クラウドベンダー(AWS)でのアップデートに追従できない
  • ServerlessFrameworkの新機能に追従できない
  • npmパッケージの脆弱性に対応できない

クラウドベンダー(AWS)でのアップデートに追従できない

Lambdaのようなサーバーレスなサービスは近年注目されていることもあり、クラウドベンダーでのアップデートは頻繁に実施されています。そういったアップデートでの新機能は有効なものが多く、プロダクトに取り込みたいものが数多くありました。
ですがServerlessFramework側はバージョン固定しているため基本的には対応できておらず、採用を見送ることも多々ありました。

ServerlessFrameworkの新機能に追従できない

もちろんですがServerlessFrameworkも日々バージョンアップが進められています。そういったアップデートでは記述の簡略化など開発体験の向上につながるものを多くあります。
そういった新機能はもちろん利用できないためServerlessFrameworkのテンプレート上にリソース定義を書く、独自のServerless Pluginsを作成しフックする、別のCloudFormationテンプレート上に起こすなどの冗長的な対応が発生していました。

npmパッケージの脆弱性に対応できない

アプリケーションはPythonでできているためnode.js製のパッケージが本番環境に乗ることはありませんが、ServerlessFrameworkのバージョンを固定してることで依存しているパッケージの脆弱性に対応できていない状況でした。
アプリケーションのコードはGithubで管理しているため脆弱性が公表されるたびにDependabotがアップデート対応のPRを生成してくれますが、対応を取り込むことによってServerlessFrameworkが動かなくなる懸念が拭えず取り込みを断念してきました。

このような問題を解決するために、今回ServerlessFrameworkおよび利用しているServerless Pluginsのバージョンアップ対応を推し進めることにしました。

やったこと

上記の問題を解消するために、以下の対応を行いました。

  • ServerlessFrameworkのバージョン検討
  • ローカル環境・CI環境でのnode.jsのバージョンアップ
  • package.json、package-lock.json更新
  • 動作確認

ServerlessFrameworkのバージョン検討

アップデートを検討した段階で、Serveless Frameworkは2系がすでにリリースされていました。そのため、1系のままで最新版にするか、メジャーバージョンまで上げるかの検討を行いました。
当時の結論としては2系へのメジャーバージョンアップは行わず、1系の最新バージョンまでアップデートすることとしました。
主な理由としては以下となります。

  • 当時2系がリリースされたばかりで潜在の不具合がある可能性があった
  • 1系の古いバージョンからのアップデートであるため2系への変更時のbreaking changeやその他の影響が読めない
  • 2系へ上げることのメリットが上記の懸念に比べると大きくなかった

ローカル環境・CI環境でのnode.jsのバージョンアップ

リリース時よりローカル環境・CI環境もnode.jsのバージョンを古いもの(8.11.1)で固定していた状態でしたので、まずはnode.jsのバージョンを14.10.0までアップデートを行いました。
ローカル環境は開発者の好みでさまざまなnode.jsのバージョン管理ツールを利用している状況だったため、アップデート方法に関しては開発者各自で行ってもらいました。
CI環境については独自にDocker Imageを準備していましたので、そちらのImageで対応を行いました。

package.json、package-lock.json更新

package.jsonの更新はnpm-check-updatesを利用し、ServerlessFrameworkのバージョンアップを実施しました。
lockファイルに関してはpackage.json更新後に npm install を行って更新を行いました。

動作確認

実際に開発環境にリリースし、以下の観点で動作確認を行いました。

  • ServerlessFrameworkで生成されるCloudFormationスタックのリソースで消えたリソースはないか
  • CloudFormationスタックで不審な変更はないか
  • アプリケーションのパッケージング内容に差はないか
  • 実際にアプリケーションを動かし、動作に問題ないか

問題

動作確認が無事完了し、実際に本番リリースすることができました。ですが、実際に本番へリリースしてみると以下の問題が発生してしまいました。

一時的にAWSリソースへの権限がなくなり、アクセスできなくなる

リリース直後に一時的に一部の処理でDynamoDBやS3、SQSといったAWSリソースへのアクセス権限がなくなり、エラーとなる事象が発生しました。
急遽切り戻して確認を行っていくと、LambdaのIAMロールのインラインポリシーの名前で命名規則が修正された改修があり、こちらが原因でAWS内部で一時的に権限が確認できなかったと仮説を立てました。
更に調査を進めて行くと発生率としては0.5%程度、発生期間は10分ほどであったため、リリース時のみの瞬断として対応を行うことなくリリースを続行しました。

まとめ

以上、今回対応したServerlessFrameworkのバージョンアップ対応について紹介しました。
リリース時には問題はあったものの、その後は安定稼働を続けており冒頭で挙げた問題点も解消されています。

ですが、今回は1系の最新バージョンにバージョンアップする方針としたため、まだ最新版に追いつけていないのが現状となります。
こちらに関しても引き続き対応を進めていく所存です。

それでは、快適なServerlessライフを!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAssemblyをNode-REDで使う (後編)

前編の内容

前編では、Node-REDでWebAssemblyを使う方法として、functionノードにWebAssemblyバイナリを埋め込む方法と、独自ノード内でWebAssemblyを呼び出す方法の前準備までを説明しました。

後編では、前編で作成したWebAssemblyコンパイル済みのPNGトリミングルーチンを使って独自ノードを作っていきます。

(承前) 第二段階: PNG画像のトリミングをする独自ノードの作成

Node-REDノードの作成

Node-RED Nodeの作り方のドキュメントを参考にしながら、モジュールを作っていきます。

まずnpmモジュールの雛形を作ります。

% mkdir node-red-contrib-png-crop
% cd node-red-contrib-png-crop
% npm init -y
...

%

次に、package.jsonを更新します。

package.json

  "name": "node-red-contrib-png-crop",
  "version": "1.0.0",
  "description": "Cropping PNG image",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "node-red": {
    "nodes": {
      "png-crop": "png-crop.js"
    }
  },
  "dependencies": {
  }
}

ランタイム側のロジックはpng-crop.jsとして記述します。

png-crop.js
const pngcropwasm = require('./pngcrop');

module.exports =  (RED) => {
    function PngCropNode(config) {
        RED.nodes.createNode(this,config);
        const node = this;
        node.on('input', (msg, send, done) => {
            if (Buffer.isBuffer(msg.payload.img)) {
                const x = msg.payload.x || 0;
                const y = msg.payload.y || 0;
                const width = msg.payload.width || 100;
                const height = msg.payload.height || 100;
                const cropped_img = pngcropwasm.croppng(msg.payload.img, x, y, width, height);
                msg.payload.img = cropped_img;
                send(msg);
                done();
            } else {
                done('no image');
            }
        });
    };
    RED.nodes.registerType('png-crop', PngCropNode);
}

冒頭のrequire()でWebAssemblyバイナリにコンパイルされたPNGトリミングライブラリをモジュールとして読み込んでいます。そして、Node-REDのメッセージハンドラで、ペイロードに含まれているイメージファイルを引数としてトリミング関数を呼び出し、結果をメッセージとして送信しています。

あとは、エディタ側の記述です。とくに設定インタフェースはないので、最低限の記述のみしてあります。

png-crop.html
<script type="text/javascript">
    RED.nodes.registerType('png-crop', {
        category: 'function',
        color: '#F3B567',
        defaults: {
            name: {value: ""}
        },
        inputs: 1,
        outputs: 1,
        icon: "font-awesome/fa-crop",
        label: function() { return this.name ||"png-crop";}
    });
</script>

<script type="text/html" data-template-name="png-crop">
    <div class="form-row">
        <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
        <input type="text" id="node-input-name" placeholder="Name">
    </div>
</script>

<script type="text/html" data-help-name="png-crop">
    <p>A simple node that crops the image</p>
</script>

独自ノードのインストールとフローの作成

これをインストールして、フローを作ってみましょう。

% cd ~/.node-red/
% npm install ....../node-red-contrib-png-crop
...
% cd ...../node-red
% npm start

functionカテゴリに"png crop"というノードができています。
png cropノード

試しにWeb上のPNG画像をトリミングしてダッシュボードに表示するフローを作ってみます。

Web上のファイルのトリミング

[{"id":"924181c4.4467c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"fc70fd9.aa3a5","type":"inject","z":"924181c4.4467c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["aef153f1.97613"]]},{"id":"aef153f1.97613","type":"http request","z":"924181c4.4467c","name":"Ferris the crab","method":"GET","ret":"bin","paytoqs":"ignore","url":"https://rustacean.net/assets/rustacean-flat-happy.png","tls":"","persist":false,"proxy":"","authType":"","x":340,"y":100,"wires":[["b33daa13.f05df8"]]},{"id":"b33daa13.f05df8","type":"change","z":"924181c4.4467c","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload.img","tot":"msg"},{"t":"set","p":"payload.x","pt":"msg","to":"480","tot":"num"},{"t":"set","p":"payload.y","pt":"msg","to":"350","tot":"str"},{"t":"set","p":"payload.height","pt":"msg","to":"300","tot":"str"},{"t":"set","p":"payload.width","pt":"msg","to":"300","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":100,"wires":[["1f7259b8.b4e1e6"]]},{"id":"7497a53c.595bdc","type":"function","z":"924181c4.4467c","name":"Base64 encode","func":"const cropped = Buffer.from(msg.payload.img);\nmsg.payload = cropped.toString('base64');\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":180,"wires":[["6dd7310.e04afd"]]},{"id":"6dd7310.e04afd","type":"ui_template","z":"924181c4.4467c","group":"c744f26f.11e5f","name":"","order":0,"width":0,"height":0,"format":"<div style=\"height: 300px; width: 300px\">\n<img src=\"data:image/png;base64,{{msg.payload}}\"\n alt='cropped image'\n />\n </div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":520,"y":180,"wires":[[]]},{"id":"1f7259b8.b4e1e6","type":"png-crop","z":"924181c4.4467c","name":"","x":160,"y":180,"wires":[["7497a53c.595bdc"]]},{"id":"c744f26f.11e5f","type":"ui_group","z":"","name":"Default","tab":"22fdfd7c.238b92","order":1,"disp":true,"width":"6","collapse":false},{"id":"22fdfd7c.238b92","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

RustマスコットのFerris the Crabの画像を持ってきて、顔の部分を切り出してui_templateダッシュボードに表示するフローになっています。

結果のダッシュボードがこちらです。
ダッシュボード

速度は?

気になるのが変換速度になります。
5K画像を100回トリミングする簡単なスクリプトを書いて、速度を調べました。

pure JS版(png-crop, pngjs利用)
const pngcrop = require('png-crop');

for (let i=0; i<100; i++) {
    pngcrop.crop('./test.png', './testout.png',
        {width: 1000, height: 1000, top: 10, left: 10},
        (err) => { if (err) throw err; });
}
Rust+WebAssembly+JS版
const pngcrop = require('./pngcrop');
const fs = require('fs');

for (let i = 0; i < 100; i++) {
    const input = fs.readFileSync('./test.png', {flag:'r'});
    const cropped = pngcrop.croppng(input, 10,10,1000,1000);
    fs.writeFileSync('./testout.png',cropped);
}

結果は下記の通りです。プログラムの作りが違うので直接は比べられませんが、WebAssemblyベースのほうが約1.4倍高速という結果になっています。もちろん、ネイティブコードで実装すればさらに高速化できますが、ここではNode.jsで完結するところに利点を見出しています。

所要時間 pure JS比
pure JS版 98秒 1
Rust+Wasm+JS版 68秒 0.69

今回はwasm-bindgenで生成したファイルを手でコピーしてnpmモジュールに追加していますが、wasm-packを使うとnpmモジュールの作成まで自動的に行えます。Node-REDのノードモジュールとして使うときには、wasm-bindgenの生成物をそのままコピーで十分かと思います。

最後に

やや無理やりな使い方ではありますが、RustとWebAssemblyの組み合わせでNode-REDノードが記述できることを示しました。

WebAssemblyによって、多様な言語で記述したプログラムがブラウザ上やNode.js上で安全に実行できるようになりました。また、WebAssembly System Interface(WASI)によってWebAssemblyはサンドボックスを備えた軽量な実行環境としても使われるようになってきています。今後は、エッジコンピューティングなどより広い応用範囲で使われる技術になっていくと期待しています。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAssemblyをNode-REDで使う (前編)

はじめに

Node-REDの独自ノードを作るには、一般にはJavaScriptとHTMLを使います。高速化のためにNode.jsのnative addonを使ってランタイム側にC++などで書いたコードをつかうこともできますが、環境に応じて再コンパイルする必要がでてきます。

そこで、WebAssemblyを使ってみることを考えてみます。WebAssemblyは

  • JavaScriptより高速な処理が期待できる
  • Node.js(8以上)が動けばどこでも動く
  • C,C++,Rustなどの言語のコンパイル対象になっている

という特長があります。

ここでは、Rustで書いたコードからWebAssemblyのバイナリにコンパイルし、それをNode-REDから活用する方法を説明します。

環境整備

ここでは下記の環境を使いました。

  • macOS Catalina version 10.15.7
  • Node-RED v1.2.6
  • Node.js v14.15.1
  • Rustc 1.48.0 (target=wasm32-unknown-unknown)
  • wasm-bindgen 0.2.69

各ツールのインストール方法はそれぞれのツールのWebページの説明をご覧ください。

第一段階: Node-REDから呼び出せることを確認する

最初から独自ノードを作っていくのも大変なので、まずは動くことを確認するためにfunctionノードをつかって無理やり使ってみましょう。

Rustのライブラリを作る

Cargoコマンドで、プロジェクトの雛形を作りましょう。この時点ではとくにWebAssemblyを意識することはありません。

% cargo new --lib hellowasm
     Created library `hellowasm` package
%

C形式の動的ライブラリを生成してWebAssemblyにコンパイルできるようにするためにCargo.tomlを編集します。最後の2行は、最適化のための設定です。

Cargo.toml
[package]
name = "hellowasm"
version = "0.1.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[profile.release]
lto = true

プログラム本体は、src/lib.rsに書きます。ここでは単純な浮動小数点の掛け算をします。なお、WebAssembly自体には32,64bitの整数、浮動小数点しか型がありません。

src/lib.rs
#[no_mangle]
pub extern "C" fn mul_f64_f64(x: f64, y: f64) -> f64 {
    x * y
}

WebAssemblyにコンパイルする

それでは、これをWebAssemblyにコンパイルしましょう。

% cargo build --release --target wasm32-unknown-unknown
   Compiling hellowasm v0.1.0 (.../hellowasm)
    Finished release [optimized] target(s) in 0.70s

WebAssemblyのバイナリがtarget/wasm32-unknown-unknown/release/hellowasm.wasmに生成されています。

% ls -l target/wasm32-unknown-unknown/release/hellowasm.wasm
-rwxr-xr-x  2 ktoumura  staff  243 Dec  3 16:03 target/wasm32-unknown-unknown/release/hellowasm.wasm

243byteと小さいですね。なお、最適化の設定(link time optimization)を行わないと1513648byteのファイルになります。

functionノードでwasmをロードする

今回は小さいファイルですので、Base64エンコードをして直接functionノードから読み込ませてしまいましょう。

% base64 target/wasm32-unknown-unknown/release/hellowasm.wasm
AGFzbQEAAAABBwFgAnx8AXwDAgEABAUBcAEBAQUDAQAQBhkDfwFBgIDAAAt/AEGAgMAAC38AQYCAwAALBzMEBm1lbW9yeQIAC211bF9mNjRfZjY0AAAKX19kYXRhX2VuZAMBC19faGVhcF9iYXNlAwIKCQEHACAAIAGiCwAPDi5kZWJ1Z19hcmFuZ2VzABUEbmFtZQEOAQALbXVsX2Y2NF9mNjQATQlwcm9kdWNlcnMCCGxhbmd1YWdlAQRSdXN0AAxwcm9jZXNzZWQtYnkBBXJ1c3RjHTEuNDguMCAoN2VhYzg4YWJiIDIwMjAtMTEtMTYp
%

これを使って、functionノードのsetupタブでWebAssemblyのロードを行います。Setup時にロードすることで、メッセージを受けるたびにWebAssemblyのロードが行われるコストを削減します。

const wasmcode = "AGFzbQ...(略)...";
WebAssembly.instantiate(Buffer.from(wasmcode, 'base64'),{})
    .then(result => {
        if (context.get("mul_f64_f64") === undefined) {
            context.set("mul_f64_f64", result.instance.exports.mul_f64_f64)
        }
    });

このような形で関数定義を取り込み、ノードのコンテキストにセットしておきます。本体のコードはFunctionタブにセットしましょう。

const mul_f64_f64 = context.get("mul_f64_f64");
msg.payload = mul_f64_f64(msg.payload, 2.5);
return msg;

それではデプロイして実行してみましょう。
WebAssembly in function node
$12345.6 \times 2.5=30864$が計算できてますね。

第二段階: PNG画像のトリミングをする独自ノードの作成

さて、もう一歩実用的な使い方に進みましょう。次は、独自ノードの中でWebAssemblyを使うことにします。ここでは「PNG画像をうけとって、それをトリミングした画像を出力する」ノードを作ります。

前節の説明ではRust-WebAssembly-JavaScriptの間のつなぎを全て手作業で実施しました。このままですと文字列を渡すのも一苦労です。このような作業を補助してくれるツールとしてwasm-bindgenがあります。

RustでPNGのトリミングを記述

前節と同様に、Rustのプロジェクトを作りましょう。

% cargo new --lib pngcrop
     Created library `pngcrop` package
%

今回はwasm-bindgenを使うためにCargo.tomlを下記のように変更します。

Cargo.toml
[package]
name = "pngcrop"
version = "0.1.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
image = "0.23.12"
wasm-bindgen = "0.2"

[profile.release]
lto = true

dependenciesに画像を扱うためのimageクレートと、先ほど述べたwasm-bindgenを加えています。

RustでのPNG画像のトリミングのコード(src/lib.rs)は下記になります。まだRustに慣れていないので不自然なコードになっているかもしれません...

src/lib.rs
use image::*;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn croppng(buf: &[u8], x: u32, y: u32, width: u32, height: u32) -> Vec<u8> {
    let mut img = load_from_memory(buf).unwrap().to_rgb8();
    let cropped = imageops::crop(&mut img, x, y, width, height).to_image().into_raw();
    let mut data = Vec::new();
    let encoder = codecs::png::PngEncoder::new(&mut data);
    encoder.encode(&cropped, width, height, ColorType::Rgb8).unwrap();
    data
}

croppng()関数は、PNG形式のバイナリのスライスとトリミングのパラメータを受け取って、トリンミング済みのPNG形式のバイナリを返します。#[wasm_bindgen]というアトリビュートをつけることで、後に置かれた関数がバインディング生成の対象であることを示します。

このライブラリをビルドし、wasm-bindgenコマンドでNode.js用のバインディングを生成します。

% cargo build --release --target wasm32-unknown-unknown
   Compiling autocfg v1.0.1
...
   Compiling wasm-bindgen-macro v0.2.69
   Compiling image v0.23.12
   Compiling pngcrop v0.1.0 (.../pngcrop)
    Finished release [optimized] target(s) in 44.06s
% wasm-bindgen target/wasm32-unknown-unknown/release/pngcrop.wasm --target nodejs --out-dir . --no-typescript

% ls -l

total 1640
-rw-r--r--  1 ktoumura  staff   11555 Dec  3 17:57 Cargo.lock
-rw-r--r--  1 ktoumura  staff     178 Dec  3 18:10 Cargo.toml
-rw-r--r--  1 ktoumura  staff    1945 Dec  3 19:07 pngcrop.js
-rw-r--r--  1 ktoumura  staff  815413 Dec  3 19:07 pngcrop_bg.wasm
drwxr-xr-x  3 ktoumura  staff      96 Dec  3 16:52 src
drwxr-xr-x  6 ktoumura  staff     192 Dec  3 18:55 target
%

pngcrop_bg.wasmがコンパイルされたWebAssemblyバイナリ、pngcrop.jsがWebAssemblyへのインタフェース用のJavascriptプログラムになります。Javascriptのプログラム側からは、pngcrop.jsrequire()して使います。

後編では、この2つのファイルを使って、Node-REDの独自ノードに仕立てていきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む