sironekotoroの日記

Perl で楽をしたい

Perl で Google Drive API をつかって特定のファイルを更新する

特定のファイルを更新する

ちょっと前、ファイルを Google Drive にアップロードした時は、同名のファイルがある場合でもアップロードが可能でした。

・・・可能でした、っていうか完全に不意打ちでしたが・・・

sironekotoro.hateblo.jp

しかし、ファイルを更新したい時はどうすれば良いのでしょう?ってわけで、やりました。人生で初めて PATCH メソッド使いました。

URI のパラメータにファイル ID を利用していますが、だいたいはアップロードの時と同じ感じですね。

$target_fileid で指定したファイルが更新されます。metadata の値を変えることで、ファイル名やmimeType を更新することも可能です。

#!/usr/bin/env perl
use strict;
use warnings;

use HTTP::Request::Common;
use JSON qw/encode_json/;
use LWP::UserAgent;
use URI::QueryParam;
use URI;

my $target_fileid = '1dZU7-4d52U2pRWsmMLhMEuh8Cpa0m8sI';

my $GOOGLE_DRIVE_UPLOAD_API
    = "https://www.googleapis.com/upload/drive/v3/files/";
my $ACCESS_TOKEN
    = "";

my $bearer = join ' ', ( 'Bearer', $ACCESS_TOKEN );

my $URI = URI->new( $GOOGLE_DRIVE_UPLOAD_API . $target_fileid );

$URI->query_param( uploadType => 'multipart' );

my $ua  = LWP::UserAgent->new;
my $res = $ua->request(
    PATCH $URI,
    'Content-Type' => 'multipart/form-data',
    Authorization  => $bearer,
    Content        => [

        metadata => [
            undef, undef,
            'Content-Type' => 'application/json;charset=UTF-8',
            'Content'      => encode_json(
                {
                    # name     => 'hogefuga.txt',
                    #  mimeType => 'plain/text',
                    #  parents  => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'],
                    #  id => $target_fileid,
                },
            ),
        ],

        file => ["./hoge.txt"],
    ],
);

print $res->code . "\n";
print $res->content . "\n";

# 200
# {
#  "kind": "drive#file",
#  "id": "19f2RrocH4I3Mdig0LkmNPghJDZnmq35f",
#  "name": "hoge.txt",
#  "mimeType": "plain/text"
# }

Perl で Google Drive API をつかって特定フォルダ配下のファイル一覧を取得する

特定のフォルダ配下にあるファイル一覧を表示する

こちらは GET アクセスなので簡単だろう〜・・・とナメてかかって、割と引っかかりました。

  • url でパラメータを組み立てる時に 2 回 URL エンコードしてしまったというもの。

つまり、「特定のフォルダの中にある」って条件だけじゃなくて「ゴミ箱の中にはない」とかちゃんと指定しようねというお話。まぁ、確かにね。

$folder_id の中にあるファイル・フォルダの ID が表示されます。

#!/usr/bin/env perl
use strict;
use warnings;

use HTTP::Tiny;
use URI::QueryParam;
use URI;

my $ACCESS_TOKEN    = '';

my $folder_id = '10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr';

my $uri = URI->new('https://www.googleapis.com/drive/v3/files');

$uri->query_param( 'q' => "'$folder_id' in parents and trashed = false" );

my $bearer = join ' ', ( 'Bearer', $ACCESS_TOKEN );

my $ht  = HTTP::Tiny->new( default_headers => { Authorization => $bearer } );
my $res = $ht->get($uri);

print $res->{content};

# {
#  "kind": "drive#fileList",
#  "incompleteSearch": false,
#  "files": [
#   {
#    "kind": "drive#file",
#    "id": "1dZU7-4d52U2pRWsmMLhMEuh8Cpa0m8sI",
#    "name": "hogefuga.txt",
#    "mimeType": "text/plain"
#   }
#  ]
# }

query_param で組み立てている検索条件を名前での検索にする時はこんな感じ。クォーテーションの位置とかでわりと試行錯誤しました。

$uri->query_param( 'q' => "name = 'hogefuga.txt'" );

他の検索条件を使いたい人はこちら

developers.google.com

Perl で Google Drive API のアクセストークンを更新する

今日も Perl から Google Drive API v3 をやっていきます。

リフレッシュトークンを使ってアクセストークンを更新する

Google の発行したアクセストークンは 3600 秒、つまり 1 時間で失効します。

失効する都度、アクセストークンを発行しても良いのですが(うちもそうしてた)、さすがに面倒・・・になってきました。

そこで、アクセストークンと一緒に発行されるリフレッシュトークンを利用してアクセストークンを更新します。

その際には以下のものが必要です。

  • クライアントID
  • クライアント シークレット
  • アクセストーク
  • リフレッシュトーク

それらを POST で GoogleAPI になげると、新しいアクセストークンが返ってきます。

これでまだ戦えますね!

注: ACCESS_TOKEN なくても更新できたので、コード更新しております

#!/usr/bin/env perl
use strict;
use warnings;

use Data::Dumper;
use HTTP::Tiny;
use JSON;
use URI;

my $CLIENT_ID = "";
my $CLIENT_SECRET = "";
my $REFRESH_TOKEN = "";

my $URI = URI->new('https://oauth2.googleapis.com/token');

my $ht = HTTP::Tiny->new();

my $response = $ht->request(
    'POST', $URI,

    {   content => encode_json(
            {   client_id     => $CLIENT_ID,
                client_secret => $CLIENT_SECRET,
                grant_type    => 'refresh_token',
                refresh_token => $REFRESH_TOKEN,
            }
        )
    }
);

my $json = decode_json($response->{content});
print Dumper $json;

# "access_token": "hogehogefoofoobarbar",
# "expires_in": 3599,
# "scope": "https://www.googleapis.com/auth/drive",
# "token_type": "Bearer"

Perl で Google Drive API を使ってファイルをアップロードする

Google Drive API を使ってファイルをアップロードしたい!

これが超苦労しましたん・・・

先に blog で書いたとおり、自分の環境(と技能)では、Google Drive を操作するモジュールを使うことができませんでした。

しかし!

Google Drive APIREST API なので、特別モジュールを使わずともいけるはずです。

stackoverflow.com

We have a REST API. You can use any language to implement your own client, you don't have to use one of the client libraries we support.

(DeepLで翻訳)私たちはREST APIを持っています。独自のクライアントを実装するために任意の言語を使用することができます。

実際、一つ前のエントリに書いたファイルリストの取得であれば、https://www.googleapis.com/drive/v3/files?access_token=アクセストークン をブラウザの URL 欄に貼り付けてアクセスすれば表示が可能です。

f:id:sironekotoro:20201107160301p:plain

ってことで、API を生で扱うツールとして curl に目をつけ、利用事例を探しました。そこから Perl の http クライアントに実装していこうという狙いです。

curl を使って Google Drive にファイルをアップロードする

curl というのは、macoslinux 、そして Windows10 にも入っている HTTP クライアントです。

Windows10 でも使えるのはついさっき知りました・・・

ascii.jp

ターミナルやコマンドプロンプトから curl https://www.yahoo.co.jp とかやると応答が返ってくるはずです。

curl での実装を探して見つけたサイトが以下です。割と自然な日本語のタイトルなんですが、日本語に自動翻訳されたサイトみたいです。

www.366service.com

ここのサンプルを用いて Google Drive の任意のフォルダにファイルをアップロードすることができました。

curl -X POST \
 -H "Authorization: Bearer アクセストークン" \
 -F "metadata={ \
              name : 'hoge.txt', \
              mimeType : 'plain/text', \
              parents: ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'] \
              };type=application/json;charset=UTF-8" \
 -F "file=@hoge.txt;type=plain/text" \
 "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"

ここでハマったのは、parents: ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'] \[ ] は削除するものと思い、結果、いつまでも Google Drive の指定したフォルダの中に入らずに困ったという。

いや、先のサンプルで -H "Authorization: Bearer [ACCESS-TOKEN]" \ とあるんだけど、ここは実際には [ ] なしで入力するので、parents のところも [ ] いらないと思ったんですよね・・・

結局、公式のリファレンスでここにはリストを渡す、って記載を発見してやっと気づいたという。公式はいつも大事(1回目)

developers.google.com

parents[] list

Perl を使って Google Drive にファイルをアップロードする

curl でできるなら、Perl でもできる!あとは移植していくだけや!と意気揚々といつもの use HTTP::Tiny をタイプしますが、ここで手が止まります。

Perl を使ってファイルアップロード?

HTTP::Tiny でファイルのアップロードどうやるんだ?

GET, POST 共に使ったことありますが、はて、ファイルアップロードはしたことない・・・

ググります。

stackoverflow.com

Not easily. HTTP::Tiny contains no support for the multipart/form-data content type required by file uploads. (That's one of the reasons it's called "Tiny".)

(DeepLで翻訳)簡単にはできません。HTTP::Tiny は、ファイルのアップロードに必要な multipart/form-data コンテンツタイプをサポートしていません。(これが "Tiny" と呼ばれる理由の 1 つです)。

いや、curl でアクセスするときに multipart とか書いてあるんやけど、それ無いのん・・・この時点で心が折れかかるんですが、大丈夫、我々には Furl, LWP::Simple, LWP::UserAgent がある!

そして、どうやら POST するときは HTTP::Request::Common でリクエストメッセージを作るのが良い、という事を知ります。

qz.tsugumi.org

ただし "multipart/form-data" を送る場合は、post() メソッドを使ってリクエストを行うよりも、HTTP::Request::Common オブジェクトを直接扱って LWP::UserAgent へ request() させた方がいいかも知れない。boundary だのファイル名だの若干複雑なので……。

これは多分、先の Stack overflow で回答者が最後に書いていた

Correctly populating $multipart_form_data is left as an exercise for the reader. :-)

(DeepLで翻訳)multipart_form_dataを正しく入力することは、読者のための練習として残されています。)

ここを解決するのが、この HTTP::Request::Common なのかなぁ?と思っています。

煮詰まって助けてもらう

なんとか、LWP::UserAgent での実装にたどり着いたものの、動かない。

煮詰まったので、 Perl入学式の公開チャンネルに質問したのでした。

Slack

そこに上げた、動かないコードがこちら。

use HTTP::Request::Common;
use LWP::UserAgent;
my $ua  = LWP::UserAgent->new;
my $res = $ua->request(
    POST
        'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
    Authorization =>
        'Bearer アクセストークン',
    Content_Type => 'multipart/form-data',
    Content      => [
        metadata => encode_json(
            {   name     => 'hoge.txt',
                mimeType => 'plain/text',
                parents  => ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'],
            }
        ),
        file => ["./hoge.txt"],
    ]
);
say $res->code;  # 400
say $res->content;
# {
#  "error": {
#   "errors": [
#    {
#     "domain": "global",
#     "reason": "badContent",
#     "message": "Unsupported content with type: application/octet-stream"
#    }
#   ],
#   "code": 400,
#   "message": "Unsupported content with type: application/octet-stream"
#  }
# }

ベテランの Perl Monger 達に助けてもらいました。本当にありがとうございます。

id:anatofuz , id:karupanerura, id:shoichikaji

動かなかった原因

これは curl と LWP::UserAgent の Dump をとって分かったのですが、metadata 部のリクエストがうまく組み立てられていませんでした。あと typo も。

ちなみに、LWP の Dumper はこんな感じで入れられます。(slack で教えてもらった)

metacpan.org

$ua->add_handler("request_send",  sub { shift->dump; return });
$ua->add_handler("response_done", sub { shift->dump; return });

以下は LWP::UserAgent の Dump の抜粋です。

--xYzZY\r
Content-Disposition: form-data; name="metadata"\r
\r
{"mimeType":"plain/text","name":"hoge.txt","parents":["10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr"]}\r
--xYzZY\r
Content-Disposition: form-data; name="file"; filename="plain/text"\r
Content-Type: text/plain\r
\r
hoge
\r

metadata のところに Content-Type: がありません。json のデータを送る旨明示しなければならないのですが、それがありません。

対して、file のところには Content-Type: text/plain\r があります。

つまり、metadata のところのデータ構造の構築に失敗していました。

で、どうやってこの metadata のところに curl と同じ application/json;charset=UTF-8 を入れるんだ?となりました。

どう書けば良いかはちゃんと HTTP::Request::Common 公式にありました。公式大事(2回目)

perldoc.jp

複数の値を持つフォームフィールドは、フィールド名を繰り返すか、 配列リファレンスを渡すことで指定できます。

POST メソッドはRFC1867 で示された Form-based File Upload のために使われる multipart/form-data コンテントもサポートします。 リクエストヘッダの一つとして 'form-data' のコンテントタイプを 指定することにより、このコンテントフォーマットを利用することが出来ます。 もし $form_ref の中の値の1つが配列リファレンスであれば、それは以下の解釈で ファイル部分の指定であるように扱われます:

[ $file, $filename, Header => Value... ]

[ undef, $filename, Header => Value,..., Content => $content ]

配列での先頭の値 ($file) はオープンするファイルの名前です。 このファイルは読みこまれ、その内容がリクエストに入れられます。 もしファイルをオープンできなければルーチンは croak します。 コンテントを直接 Content ヘッダで指定したければ $file の値を undef に してください。 $filename はリクエストで報告されるファイル名です。 この値が未定義であれば、$file の基本名が使われます。 $file の値を提供したとき、ファイル名の送信をよくせいしたいなら、 $filename に空文字列を指定することができます。

なるほど。

動かないコードだと metadata に対する値は一つだけでした。

[ $file, $filename, Header => Value... ]$file しか入っていない状態です。

しかも、モジュールは file が来ることを想定してるのに、渡しているのは(JSON化した)文字列です。なるほど動かん。

        metadata => encode_json(
            {   name     => 'hoge.txt',
                mimeType => 'plain/text',
                parents  => ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'],
            }

これを直すとこうです。

  • [ undef, $filename, Header => Value,..., Content => $content ] の形式にする

  • コンテントを直接 Content ヘッダで指定したければ $file の値を undef に してください に従う

  • JSONの文字列は Content キーの値に移動

適用した配列リファレンスを metadata の値に渡しています。

        metadata => [
            undef, # ファイル使わないので undef
            undef, # ファイルからデータを取得しないので undef
            'Content-Type' => 'application/json;charset=UTF-8',
            'Content' => encode_json( # jsonのデータは Content キーの下に
                {   name     => 'hoge.txt',
                    mimeType => 'plain/text',
                    parents  => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'],
                },
            ),
        ],

PerlGoogle Drive API を使ってファイルをアップロードする(タイトル回収)

ということで、やっと PerlGoogle Drive にファイルをあげることができました。

#!/usr/bin/env perl
use strict;
use warnings;

binmode STDOUT, ":utf8";

use HTTP::Request::Common;
use JSON qw/encode_json/;
use LWP::UserAgent;

my $GOOGLE_DRIVE_UPLOAD_API
    = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";

my $ua = LWP::UserAgent->new;
my $res = $ua->request(
    POST $GOOGLE_DRIVE_UPLOAD_API,
    'Content-Type' => 'multipart/form-data',
    Authorization =>
        'Bearer アクセストークン',
    Content => [

        metadata => [
            undef,
            undef,
            'Content-Type' => 'application/json;charset=UTF-8',
            'Content' => encode_json(
                {   name     => 'hoge.txt',
                    mimeType => 'plain/text',
                    parents  => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'],
                },
            ),
        ],

        file => ["./hoge.txt"],
    ],
);

print  $res->code . "\n";
print  $res->content . "\n";

# 200
# {
#  "kind": "drive#file",
#  "id": "19f2RrocH4I3Mdig0LkmNPghJDZnmq35f",
#  "name": "hoge.txt",
#  "mimeType": "plain/text"
# }

おまけ HTTP::Tiny::Multipart

むやみやたらとググる中、コアモジュール民(自分)が大好きな HTTP::Tiny の拡張がありました。HTTP::Tiny::Multipart です。

metacpan.org

これを使って Google Drive へのアップロードを書いてみました。こちらも動きました。

#!/usr/bin/env perl
use strict;
use warnings;

binmode STDOUT, ":utf8";

use File::Slurp qw/read_file/;
use HTTP::Tiny;
use HTTP::Tiny::Multipart;
use JSON;

my $GOOGLE_DRIVE_UPLOAD_API
    = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
my $ACCESS_TOKEN
    = "";
my $bearer = join " ", ( 'Bearer', $ACCESS_TOKEN );

my $content = read_file('./hoge.txt');

my $http = HTTP::Tiny->new( default_headers => { Authorization => $bearer } );

my $response = $http->post_multipart(
    $GOOGLE_DRIVE_UPLOAD_API,
    {   metadata => {
            content => encode_json(
                {   name     => 'hoge.txt',
                    mimeType => 'plain/text',
                    parents  => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'],
                },
            ),
            content_type => 'application/json; charset=utf-8',
        },

        file => {
            filename     => './hoge.txt',
            content      => $content,
            content_type => 'plain/text',
        },
    }
);
print $response->{content} . "\n";

# {
#  "kind": "drive#file",
#  "id": "1UInkENiv0GApyfhJWyxgNaplw512Mmw2",
#  "name": "hoge.txt",
#  "mimeType": "plain/text"
# }

後始末

f:id:sironekotoro:20201108104903p:plain

Perl で Google Drive API を使ってファイル一覧を取得する

さて、Google Cloud Platform で利用する ACCESS TOKEN と REFRESH TOKEN を手にしたからには、次の段階へ向かわねばなりません。

そもそも、なんか OAuth いじるの楽しくなって色々やってしまいましたが、これは目的と手段が逆転するいつものパターンです。

楽しいんですよねー

Google Drive に上がっているファイル一覧を取得する

GET でパラメータを並べていくだけなので、簡単にできました。

注意するところとしては、一回の応答で取得できるのは100件のみ。100件以上ファイルがある場合には応答に nextPageToken が含まれるので、次の API 組み立ての際に pageToken を加えてアクセスって感じです。

これで連続取得ができます。

大量にファイルがあると返ってこないので、5 回取得したら(つまり 500件)ループを抜けるようにしてます。

#!/usr/bin/env perl
use strict;
use warnings;

binmode STDOUT, ":utf8";

use HTTP::Tiny;
use JSON;
use URI::Escape;
use URI;

my $GOOGLE_DRIVE_API = "https://www.googleapis.com/drive/v3/files";
my $ACCESS_TOKEN       = "";
my $count_limit = 5;

# 全てのファイルを取得する
my $uri = URI->new($GOOGLE_DRIVE_API);

$uri->query_form( access_token => $ACCESS_TOKEN );

files($uri);

sub files {
    my $uri   = shift;
    my $count = 0;
    my $ht    = HTTP::Tiny->new();
    while ( $count < $count_limit ) {
        my $contents = decode_json( $ht->get($uri)->{content} );

        $uri->query_form(
            access_token => $ACCESS_TOKEN,
            pageToken    => $contents->{nextPageToken},
        );

        for my $content ( @{ $contents->{files} } ) {

            print "=" x 20 . "\n";
            printf( "%-8s: %s\n", "id",       $content->{id} );
            printf( "%-8s: %s\n", "name",     $content->{name} );
            printf( "%-8s: %s\n", "mimeType", $content->{mimeType} );
            printf( "%-8s: %s\n", "kind",     $content->{kind} );
            print "=" x 20 . "\n";
        }

        $count++;
        last if !$contents->{nextPageToken};

        # 最終ページには nextPageToken キーが無い
    }
}
====================
id      : 13uf_8fJph3J3raPee0Sg5rsgb-w5MIUBLVdhDhD8xSE
name    : チェックリスト
mimeType: application/vnd.google-apps.document
kind    : drive#file
====================
====================
id      : 1f9ZuDCOSGUujhSSXvv_kruzI7qbkKq9FQXf2YuNn_Io
name    : メモ
mimeType: application/vnd.google-apps.document
kind    : drive#file
====================
...

Mojolicious::Lite で Google Drive API のアクセストークンを表示する

そろそろ前に進まないとと思いつつ

ここしばらく、Google の提供する API を扱うためのことやってきたんですが、やりつつ一つの疑念がありました。

途中で Web にアクセスして URL を貼り付けるところ、そのまま HTTP の GET で良いのでは?

いちいちブラウザに貼り付けなくても良さそうな・・・?

というか、最初からブラウザで動かしたら?

それって、 Perl 入学式 でやった Mojolicious::Lite で実装できそう・・・と言うことで実装してみました。

下準備としては、Google Compute Platform の API 設定の OAuth 同意画面で、アプリケーションのホームページを http://localhost:3000 にしておくことです。

言わずと知れた Mojolicious を morbo で起動した時の URL とデフォルトポートですね。

f:id:sironekotoro:20201104192003p:plain

出来上がったもの

苦労したところ

Google の認証画面に遷移するところまでは割とすんなり行きました。

問題はそっから ACCESS_TOKEN と REFRESH_TOKEN つくるところで詰まりました。

結局、 Net::Google::OAuth のソースを参考にしつつ作ることに・・・

おかげで、メールアドレスやリダイレクトのURLをURLエスケープするとか、HTTP::Tiny の POST つかって JSON で送るとかを読み取ることができました。

コード

Google Drive API での利用を想定して SCOPE のところがそうなってます。

http://localhost:3000 -> (POST) -> Google の認証画面 -> (GET) -> http://localhost:3000/code という感じで遷移します。

URL の組み立てのところが雑だなぁ・・・と思うんですが、まぁそこはそれってことで。

#!/usr/bin/env perl
use Mojolicious::Lite;
use HTTP::Tiny;
use JSON;
use URI::Escape;
use URI;

my ( $CLIENT_ID, $CLIENT_SECRET, );

get '/' => sub {
    my $c = shift;

    $c->render( template => 'index' );
};

get '/code' => sub {
    my $c = shift;

    my $code = $c->param('code');

    # Exchange code to token
    my $param = {
        'client_id'     => $CLIENT_ID,
        'client_secret' => $CLIENT_SECRET,
        'redirect_uri'  => 'http://localhost:3000/code/',
        'code'          => $code,
        'grant_type'    => 'authorization_code',
        'access_type'   => 'offline',
    };

    my $json = encode_json($param);

    my $ht       = HTTP::Tiny->new();
    my $response = $ht->request(
        'POST',
        'https://accounts.google.com/o/oauth2/token',
        {
            headers =>
                { 'Content-Type' => 'application/json; charset=utf-8', },
            content => $json,
        },
    );

    $json = decode_json( $response->{content} );

    $c->stash(
        access_token  => $json->{access_token},
        refresh_token => $json->{refresh_token}
    );

    $c->render( template => 'code' );
};

post '/post' =>
    sub {
    my $c = shift;
    $CLIENT_ID     = $c->param('CLIENT_ID');
    $CLIENT_SECRET = $c->param('CLIENT_SECRET');
    my $MAIL  = $c->param('MAIL');
    my $SCOPE = $c->param('SCOPE');

    my $redirect_url = uri_escape('http://localhost:3000/code/');
    my $login_hint   = uri_escape($MAIL);
    my $scope        = uri_escape($SCOPE);

    # url組み立て
    my $url
        = "https://accounts.google.com/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$redirect_url&scope=$scope&response_type=code&approval_prompt=force&access_type=offline";

    $c->redirect_to($url);

    };

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'Welcome';
<h1>Welcome to the Mojolicious real-time web framework!</h1>

<form action="/post" method="post">
    <ul>
        <li>CLIENT_ID: <input name="CLIENT_ID" size=80 type="text" value=""></li>
        <li>CLIENT_SECRET: <input name="CLIENT_SECRET" size=80 type="text" value=""></li>
        <li>MAIL: <input name="MAIL" size=20 type="text" value=""></li>
        <li>SCOPE: <input name="SCOPE" size=40 type="text" value="https://www.googleapis.com/auth/drive"></li>
    </ul>
    <input type="submit" value="POSTで投稿する">
</form>


@@ code.html.ep
% layout 'default';
% title 'Welcome';
<h1>Welcome to the Mojolicious real-time web framework!</h1>

<ul>
    <% if ( stash('access_token') )  {%>
        <li>ACCESS TOKEN:<%= $access_token %></li>
    <% } %></textbox>

    <% if ( stash('refresh_token') )  {%>
        <li>REFRESH TOKEN:<%= $refresh_token %></li>
    <% } %>
</ul>

<a href="http://localhost:3000">トップページ</a>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head><title><%= title %></title></head>
  <body><%= content %></body>
</html>

CircleCI で Perl のテストをローカルでやってみた

環境構築から

この本を参考にしました。

gihyo.jp

Perl の例は掲載されていないですが、他の言語での例は豊富です。

なので、そっから応用していけるのではないか、していきたい!という気持ち。

$ brew install circleci

バージョン確認

$ brew info circleci
circleci: stable 0.1.11393 (bottled)
(以下略)

あと、Docker for Mac も必要になります。

いつもの Hello, World! の下準備

作業用のディレクトリに CircleCI 用のフォルダを作る

$ mkdir -p .circleci/

設定を記述する YAML ファイルを用意する

$ touch .circleci/config.yml

エディタで YAML ファイルを編集する

実行時に必要になる Perl の入った Docker イメージは好きなのを使ってください。Perl が入っていれば ok です、多分。

ってことで、去年くらいに作った自作の Perl 入りの Docker イメージ sironekotoro/alpine-perl を使います。

version: 2.1

jobs:
  build:
    docker:
      - image: sironekotoro/alpine-perl
    steps:
      - run: perl -e 'print "Hello, World!\n"';

workflows:
  version: 2
  workflow:
    jobs:
      - build

出来上がったファイルが文法上正しいかを確認する

$ circleci config validate
Config file at .circleci/config.yml is valid.

大丈夫だったら実行します。

$ circleci local execute

ログが流れ・・・そして最後にコマンドが実行されて Success! となって終わりです。

(省略)
perl -e 'print "Hello, World!\n"';
Hello, World!
Success!

ちなみに、2020年11月1日より Docker Hub では pull の回数に制限が入ります。念のため、Docker Hub の認証情報を追加しておきます。

support.circleci.com

version: 2.1

references:
  docker_hub_authentication: &docker_hub_authentication
    auth:
      username: DockerHubでのユーザー名
      password: DockerHubでのパスワード
jobs:
  build:
    docker:
      - image: sironekotoro/alpine-perl
        <<: *docker_hub_authentication
    steps:
      - run: perl -e 'print "Hello, World!\n"';

workflows:
  version: 2
  workflow:
    jobs:
      - build

circleci 上で、Perl の コマンドが実行できることがわかりました。素晴らしい!

Hello World! が動くってことは prove -l も動くっしょ

prove -lPerl のテスト時に使われるコマンドですが、もちろん動きます。

実際のモジュールで試してみます。これも自作の雑モジュールです。こういうとき助かるー!

$ git clone git@github.com:sironekotoro/Acme-MetaVar.git
$ cd Acme-MetaVar
$ mkdir -p .circleci/
$ touch touch .circleci/config.yml

そしてお好みのエディタで .circleci/config.yml 編集します。

先の例とは、 steps セクションが異なっています。

version: 2.1

references:
  docker_hub_authentication: &docker_hub_authentication
    auth:
      username: DockerHubでのユーザー名
      password: DockerHubでのパスワード
jobs:
  build:
    docker:
      - image: sironekotoro/alpine-perl
        <<: *docker_hub_authentication
    steps:
      - checkout
      - run: prove -l;

workflows:
  version: 2
  workflow:
    jobs:
      - build

編集が終わったらローカルで実行です。

$ circleci local execute
(中略)
====>> prove -l;
prove -l;
t/00_compile.t .. ok
t/01_hoge.t ..... ok
t/02_fuga.t ..... ok
All tests successful.
Files=3, Tests=3,  0 wallclock secs ( 0.04 usr  0.02 sys +  0.29 cusr  0.08 csys =  0.43 CPU)
Result: PASS
Success!

という感じで、CircleCI に入門したのでした。

なぜ CircleCI やろうと?

Perl のモジュールオーサリングツール Minilla で モジュールを作ると CI ツールである Travis への連携が簡単にできます。

そのテスト結果が出るのが思ったより楽しかったので、他の CI ツールはどうなんだろう?という好奇心から。

最終的にはこういうのをやりたいんですよねー

  1. GitHub 上にあるリポジトリ から持ってくる

  2. Makefile とかで色々やる

  3. 色々やった後の結果で、 diff があるようだったら自動で P/R 作成

  4. これを毎日定時実行

本を読むとできそうなのですが、うちが作れるかどうかはまた別の話・・・