銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

AsciiDocのテーブルで行のヘッダを指定する方法

f:id:takamints:20190518074249j:plain
photo credit: Pittypomm 27.1.15 - Bicycle via photopin (license)

仕様書書いててAsciiDocのテーブルで行ヘッダ(=特定列をまるごとヘッダ)はどうするの?と疑問に思って調べたことを書いています(列ヘッダ(ヘッダ行)を表現するのは2行目を空行にするか[options="header"]でOKですね)。

以下、AsciiDoc の文書と @asciidoctor/core を使って書き殴ったプレビューツールでお送りしてます。

はてなブログではAsciiDocのシンタックスハイライトがされないみたいですね。

目次

AsciiDocのテーブルで行ヘッダを指定する

テーブルのcols属性で行ヘッダを指定します。該当する列に h を指定すれば、その列すべてがヘッダになります。普通のセルなら d とします。 cols属性は幅指定するためのものと思ってましたが、こういう事もできるのですね。ナルホドね。

[cols="h,d"]
|===
|行のヘッダ |データセル
|行のヘッダ |データセル
|===

列幅も同時に指定する

列幅も同時に指定する場合、hd の前に数値(幅の相対値)を書きます。後ろだと幅指定と解釈されずにテーブルが崩れていました。

[cols="1h,4d"]
|===
|行のヘッダ |データセル
|行のヘッダ |データセル
|===

列幅を指定するなら d は省略できるみたいです。あえて省略する必要もなさそうですが。

[cols="1h,4"]
|===
|行のヘッダ |データセル
|行のヘッダ |データセル
|===

列ヘッダも同時に指定

列のヘッダの指定は、行ヘッダの指定とは独立しているので、通常の書き方と組み合わせるだけです。

つまり、2行目を空行にする。ただそれだけ。

[cols="h,d"]
|===
|行列のヘッダ|列のヘッダ

|行のヘッダ |データセル
|行のヘッダ |データセル
|===

またはoptions属性でheaderを指定する。これも通常のやり方ですね。

[cols="h,d", options="header"]
|===
|行列のヘッダ|列のヘッダ
|行のヘッダ |データセル
|行のヘッダ |データセル
|===

あとがき

AsciiDocは表現の幅が広くてMarkdownよりは複雑ですね。 最近仕様書などをAsciiDocで書くことが多かったのですが、たまにしか書かないので書き方をかなり忘れていて「思い出しフェーズ」が必要でした。 なるべく日常的に書くようにすればよいでしょうけど、そうもいかない。

ググれば出てくるAsciiDocの説明サイトはありがたいのですが、逆に情報量が多くて知りたいところがササッと出てこないのがもどかしかった。 個人的に忘れがちな書式に絞ってチートシートみたいなものを作っておくのも手かもしれない。 もしくはパッと見、どこに行けばよいかがすぐに分かるインデックスとか。

あと使い方に関してだけど、書くことや考えることに集中したい段階ではMarkdownでざっと書き、清書するときにAsciiDocに変換して整えるというのがシームレスにストレスなく実現できれば良いと思います。

いやしかし、シンプルなテーブルに限っては Markdown よりも AsciiDoc のほうがさっと書きやすいですね。

AWSでランダムな画像を返すURLを作りました(Stravaのワークアウトのシェアのため)

タイトル通り「ランダムに画像を返すURL」をAWSで作りました。

Stravaのワークアウト完了時に、IFTTTを使って結果(距離、時間など)をTwitterに流していて、走行経路の地図(GoogleMaps)を添付しているのですが、常に「Image not found」となっており(今思えばセキュリティ設定に関連しているのかもしれない)、アクセスするたびに画像が入れ替わるURLを作ればいいやと、初夏の風吹く4月の土曜日に、朝メシ食わずにAWSでポチポチ作業・・・

↓これです。再読み込みすると別の写真が表示されます(単純にランダムなので同じ写真が連続することもある)。 すみませんAWSの請求書が怖くなったので現在CloudFrontでキャッシュするよう設定しましたー。 たぶんこのページからのリクエストで毎日支払金額が(わずかですけど)上がっていくので固定画像に変更しました。ゴメンナサイ。

f:id:takamints:20190415215350j:plain

当初、API Gateway、Lambda Function、S3 Bucket で簡単にできると思っていたのですが、ブラウザのIMGタグで表示させようとするとリクエストヘッダにAcceptで受け入れ可能なMIME Typeを指定しておかなければならなくて、そのためには CloudFront を使わなければならないらしい。なるほど勉強になりました。

目次

Lambda Function

Lambda Functionは、S3のバケットの特定のフォルダー以下から画像ファイルをリストして、その中からランダムに選んだ画像を返します。 バケット名とフォルダの名前は環境変数で設定しています。

const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const bucket = process.env["BUCKET"];
const keyPrefix = process.env["KEYPREFIX"];// `strava-tweet-photos`

exports.handler = async (event) => {

    //S3の画像をリスト
    const listResult = await S3ListObjects({
        Bucket: bucket,
        Prefix: keyPrefix
    });

    //フォルダ以外のキーの配列に変換
    const keys = listResult.Contents
        .map(item => item.Key)
        .filter(key => (key !== keyPrefix));

    //ランダムに一つ選ぶ
    const index = randomInt(keys.length);
    const key = keys[index];

    //画像を取得
    const image = await S3GetObject({
        Bucket: bucket,
        Key: key
    });

    //HTTPレスポンスを返す
    const response = {
        statusCode: 200,
        headers: {
            'Content-Type': image.ContentType
        },
        body: image.Body.toString('base64'),
        isBase64Encoded: true,
    };
    return response;
};

const S3ListObjects = params =>
    new Promise( (resolve, reject) =>
        S3.listObjects(params,
            (err, data) => (err ?
                reject(err) :
                resolve(data))));

const S3GetObject = params =>
    new Promise( (resolve, reject) =>
        S3.getObject(params,
            (err, data) => (err ?
                reject(err) :
                resolve(data))));

const randomInt = max =>
    Math.floor(
        Math.random() * Math.floor(max));

API Gateway REST API

適当なリソースを作ってGETメソッドを作り、「統合リクエスト」で「Lambda統合」を選択して、上のLambdaを指定します。 Lambdaの戻り値をそのままメソッドレスポンスへ渡すので、「統合リクエスト」の「Lambdaプロキシ統合」にチェックを入れます。 ブラウザで表示するだけなら、CORSは対応しなくて良いですね。

CloudFront Distribution

上のREST APIをデプロイし、そのエンドポイント(例: xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com)を Origin に指定してCloudFrontディストリビューションを作ります。 Origin Path にはAPIをデプロイしたステージ名を指定(例:/production)して、Origin Protocol PolicyHTTPS Only とします(API GatewayREST APIなので)。

今後の話

画像のサイズを指定できたり、画像のEXIFを読み取って、アクセスした日に近い画像を返すとか、いろんな機能拡張が思い浮かびます。 ちょっとずつ楽しみながら実装していきたいなーと思いました。

参考リンク

qiita.com

qiita.com

AWS S3 の putObject API でメタデータを設定する

f:id:takamints:20190415215350j:plain

AWSによるサーバーレスアーキテクチャ
Peter Sbarski
翔泳社
売り上げランキング: 157,672

AWS S3 の putObject APIバケットにファイルをアップロードするときには、メタデータをきちんと指定しておきましょうという話。 例えば、メタデータを設定しておいた既存ファイルに上書きアップロードする場合でも、putObjectでメタデータを指定していなければすべて失われてしまいます。

Content-Type については、AWS-CLIからアップロードする場合は適切なMIME Type を設定してくれるのですが、APIでは自分で指定しなければなりません。でなけりゃすべて application/octet-stream に設定されて、WEBで公開している場合は、すべてがバイナリファイルの扱いになり、HTMLファイルだってなんでもかんでもダウンロードされてしまいます。

バケットをWebで公開設定していたり、CloudFrontから配信しているならば、ブラウザキャッシュを禁止したいておきたい場合がありますが、ファイルを上書きするたびに、その設定は失われます(AWS-CLIでは、--cache-control オプションに、no-cache を指定すればOKです)。

まあ、とにかくAPIでputObjectする場合には必要なメタデータを毎回設定するべきなんですね。

ということで、以下のコードは、APIでS3にファイルをアップロードする際、同時にメタデータを設定するNode.jsのコードです(AWSへの接続はAWS-CLIのプロファイル(~/.aws/ 以下の configcredentials)が正しくセットアップされている前提です)。 ここでは putObject のパラメータで、最低限設定しておきたい Content-Type と、おそらく必要になるであろう Cache-Control を指定しています。

"use strict";
const AWS = require("aws-sdk");
const mime = require("mime-types"); //拡張子からMIME Type
const fs = require("promise-fs"); //地獄に落ちないfsモジュール
const { promisify } = require("es6-promisify"); //callbackの非同期をPromise化
const s3 = new AWS.S3();
const promised = {
    s3: {
        putObject: promisify(s3.putObject.bind(s3))
    }
};

/**
 * ContentTypeとCacheControlを設定してS3のバケットへファイルを
 * アップロードする。
 * @async
 * @param {string} bucket バケット名
 * @param {string} key アップロード先のキー(バケット内のパス)
 * @param {string} pathname ローカルファイルのパス名
 * @returns {Promise<undefined>} アップロード完了で解決するPromise
 */
const uploadS3Bucket = async (bucket, key, pathname) => {
    try {
        const body = await fs.readFile(pathname);
        const contentType = mime.lookup(pathname);
        const params = {
            Body: contentType.match(/^text\//) ? body.toString() : body,
            Bucket: bucket,
            Key: key,
            CacheControl: "no-cache",
            ContentType: contentType,
        };
        console.log(`Uploading: ${pathname}`);
        console.log(`  [ContentType: ${params.ContentType}]`);
        console.log(`  ==> s3:${'//'}${params.Bucket}/${params.Key}`);
        await promised.s3.putObject(params);
    } catch(err) {
        console.warn(err.message);
    }
};

Content-Type の特定は npm mime-type が使えます

ファイル名(拡張子)からMIME Typeを得るために、 mime-types というnpmモジュールを使ってみました。別の npm mime-dbに依存しているのですが、どちらも週に1500万回ほどダウンロードされていますから実用上の問題はないでしょう。

Cache-Control も設定したい

Webサイトとして公開している場合、Cache-Controlを設定しておかないとブラウザキャッシュが有効になり、AWS側から制御しにくいので、ワタシはたいてい no-cache に設定してます。 CloudFrontのディストリビューションから公開している場合、24時間程度のキャッシュが効いていますので、S3から外へ出ていく容量(と課金)をあまり気にする必要はないはずです。 CloudFrontでInvalidationを作成してキャッシュを無効化しても、ブラウザキャッシュが効いているとページが更新されませんし。

参考:S3のオブジェクトのメタデータ

下表は S3 Bucket のファイルに設定可能なメタデータです。 メタデータ列は S3 のWEBコンソールで表示される名称で、パラメータキーはputObjectのパラメータで指定する場合のキー名称です。

メタデータ パラメータキー 詳細
Cache-Control CacheControl ブラウザキャッシュの指定 (☞MDN)
Content-Disposition ContentDisposition ファイルの扱い方法を指定 (☞MDN)
Content-Encoding ContentEncoding 圧縮アルゴリズムを指定 (☞MDN)
Content-Language ContentLanguage 閲覧者の言語を指定 (☞MDN)
Content-Type ContentType ファイルのMIME Typeを指定 (☞MDN)
Website-Redirect-Location WebsiteRedirectLocation リダイレクト先 (☞DevelopersIO)
x-amz-meta-<key> Metadata.<key> ユーザー定義メタデータ (☞AWS)
有効期限 Expires 削除される日時(☞AWS)

※ ユーザー定義メタデータ以外は、APIであらかじめ定義されているシステムメタデータです。

リンク