sironekotoroの日記

Perl で楽をしたい

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. これを毎日定時実行

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

Perl から Google Drive にファイルを「上書きっぽい感じ」でアップロードする

ここまで( 7 時間くらい)のあらすじ

  1. 前回の Google Drive の続きをやろう。Google Drive の特定のフォルダの配下にあるファイルを上書きアップロードしよう。

    • Google Drive は同名ファイルをフォルダ内に作成できる。
    • このため、同じファイルを続けてアップロードしても上書きにならず、同名ファイルが増えて行く。
    • f:id:sironekotoro:20201101161845p:plain
  2. 前回も使った Net::Google::Drive だと、ファイルリスト取得時にクエリーを差し挟めず、「特定のフォルダの配下にあるファイルのみ」という条件で取れない

    • 全ファイル、または名前で検索して、そのファイルの親ID(フォルダID)と照合することで特定は可能。
    • ただし、検索で引っかかったファイル数だけAPIを発行することになり遅い
  3. 他にある Perl から Google Drive を扱うモジュールを試すものの、うまくいかなくて泣く

    • Net::Google::Drive::Simple(0.18):Crypt::SSLeay のインストールがどうやってもダメ

    • WWW::Google::Drive(0.05):Google のサービスアカウント作って new してもエラーは出ないものの、SYNOPSIS 通りにメソッド呼んでも空の応答しか返ってこない

    • Google::RestApi(0.4):どうやって Google の認証情報渡せばいいかわからない〜、YAML で渡す時の key とか

  4. 諦めて Net::Google::Drive 使うか・・・うわぁ!uploadFile メソッドで file_id 指定できない!上書きできない!

    1. 名前で検索して、特定の親IDを持つファイルをリストアップ
    2. そのファイルを削除
    3. 新しいファイルをアップロード

    というのをやってみたコードが以下です。まぁ、動きだけ見れば上書きっぽい。

    終わった後だと上書きと見分けがつかない・・・いや、 file_id 変わってるか・・・

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

use feature qw/say/;
use Data::Dumper;
binmode STDIN,  ":utf8";
binmode STDOUT, ":utf8";

use Carp;

use Net::Google::Drive;

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

my $disk = Net::Google::Drive->new(
    -client_id     => $CLIENT_ID,
    -client_secret => $CLIENT_SECRET,
    -access_token  => $ACCESS_TOKEN,
    -refresh_token => $REFRESH_TOKEN,
);

# 更新(削除->アップロード)対象のファイル名
my $write_up_filename = 'test.txt';

# アップロード先のフォルダID
my $folder_id = '12ZOW6qR0BxDeksOuCwnWA85w4KgTh5ey';

my $files = $disk->searchFileByName( -filename => $write_up_filename, )
    or croak "File '$write_up_filename' not found";

# 特定のフォルダIDを親にしている key があり
# 特定のフォルダIDを親にもち
# ゴミ箱の中にないファイルのみを対象にする
my @metadatas = ();
for my $file ( @{$files} ) {
    my $metadata = $disk->getFileMetadata( -file_id => $file->{id}, );

    if (   exists $metadata->{parents}[0]{id}
        && $metadata->{parents}[0]{id} eq $folder_id
        && $metadata->{labels}->{trashed} == 0 )
    {
        push @metadatas, $metadata;
    }
}

# ファイル削除
for my $metadata (@metadatas) {
    $disk->deleteFile( -file_id => $metadata->{id} );
}

# ファイルアップロード
# -dest_file :アップロードしたいファイル(のパス)
# -parents   :Google Drive上のフォルダの下に格納するかを指定
my $res = $disk->uploadFile(
    -source_file => $write_up_filename,
    -parents     => [$folder_id]
    ,    # 配列リファレンスの中に指定すること
);

なお、アップロードはファイルのみ。フォルダをアップロードすることはできませんでした。

ううーん。まぁ、場合によっては使えるか・・・

追記

Net::Google::Drive::Simple なんですが、GitHub に meta::cpan よりマイナーバージョンが1上がったやつ(0.19)を発見。

これは勝てる!って思ったけどやっぱ Crypt::SSLeay への依存は変わらずでインストールできませんでした。

しょんぼり。

github.com

Lenovo 製 M75q-1 Tiny の USB Recovery Creator で大変だった(追記あり)

まとめ:Lenovo 製 M75q-1 Tiny の USB Recovery Creator でメディア作成に失敗し続ける人へ

  • 購入した Lenovo 以外の、他の Windows 機で USB Recovery Creator を動かしてメディアを作ってみたら成功した。

  • BUFFALO 製 USB メモリでは USB からのブートに失敗。Trancend 製 USB メモリからはブートが成功。換装した SSDリカバリすることができた。

n = 1 、つまり自分の経験のみですが参考になれば。

Lenovo M75q-1 Tiny 買いました

比較対象はMacBook Pro の13インチです。なぜ大事なそれを書き忘れるかな・・・

現在利用している Windows マシンは富士通製の FMV-ESPRIMO D582 という機種です。

www.fmworld.net

2016年に中古で買ったものです。なんでこれかというと、当時の会社で使っていたPCと同じ機種だったからです。

会社PCが壊れても、筐体交換でHDD載せ替えだけで動くだろう、と。

今思うと、会社への忠誠心が高めですね。

2.5 インチのマウンターを追加し、SSD を積んでいます。

速度は申し分ないのですが、元が 32bit Windows7 だったため、そろそろ動かないソフトも出てきました。

M75q-1 Tiny

週末になると、価格ドットコムから専用モデルのページへのリンクが出てきます。そこから最小構成にダストカバー、VESAマウントなどを揃えて買いました。

f:id:sironekotoro:20201031155906p:plain

一部ネット界隈で安い!と噂のモデルです。到着した時にはその小ささに驚きました。

最小構成にしたのは、初期搭載メモリとSSDを換装して利用しようと思ったからです。

https://amzn.to/3kKCXaIamzn.to

https://amzn.to/34Jx5ZSamzn.to

メモリの換装はうまく行きました。というか、とても簡単でした。

問題は SSD の換装に必要なリカバリツールの作成、USB Recovery Creator でした。

USB Recovery Creatorリカバリーメディアを作るまでの長い旅

ダウンロードが 99% で止まる

SSD を載せ替えるためには、USB Recovery Creator なるツールを利用して、USB メモリをリカバリーメディアにする必要があります。

support.lenovo.com

この時点で推奨とされる 16GB 以上の容量をもつ USB メモリがなかったため、追加で BUFFALO の 32GB の USB メモリ(RUF3-K32GA-BK/N)を注文しました。

USB メモリ到着後、家の低速回線で 1 時間かけてダウンロード・・・するも、99 %でダウンロードの進捗が止まります。

これはちゃんと対処法があります。

エラー「メタファイルが見つかりません」または 自分のダウンロードが2時間以上も 99%で停止しています。 これは、通常「.rmf」ファイルが正しくダウンロードされなかったことを意味します。ダウンロードされた場所から拡張子名に「.rmf」を含むファイルを探してください。 1. 拡張子名が「.rmf.prg」の場合「.rmf」に変更してください。それからツールを実行してダウンロードを続行します。 2. 拡張子名が「.rmf」の場合、それを削除してください。それからツールを起動してダウンロードを続行してください。

わかってるんなら自動で削除してくれ・・・って思うものの、できない理由もあるのでしょう。

USB メモリが使えなくなる

さて、この対処(「拡張子名が「.rmf.prg」の場合「.rmf」に変更」)をすると、ダウンロードが完了します。

USB メモリを差し込んで、いざ作成・・・!

「書き込みに失敗しました」

リトライしようとすると、ダウンロードしたファイルの一部が消えるのか、「***がありません」のエラーが出ます。

しかも、この書き込みに失敗した後はUSBメモリのフォーマット等ができなくなります。

手元にある私物の MacBook Air でディスクユーティリティを利用し、 USB メモリを exFAT 形式でフォーマットすることで復活します。

で、USB の差込口を変えつつ試しますがダメです。その都度、MacBook Air で USB メモリを初期化します。

ダウンロードしたファイルが良くないのかも、ってことで再度ダウンロードするも状況変わらず。

この時点で窓から投げ捨てたくなってきますが、ぐっと我慢します。

USB メモリではなく、 USB 接続の HDD にメディアを作れないだろうか?

ダメでした。USB メモリじゃないって弾かれます。

Lenovo に頼らず、Windows 純正のリカバリツールは?

USB メモリに書き込みはできるのですが、USB メモリから起動しませんでした。

USB メモリを交換する

USB メモリから起動しないことで、USB メモリの初期不良を疑い、色々と調べます。

Web で検索していると、 Lenovo と BUFFALO の相性が悪い、的な話を見つけました。

ここで追加投資するのー?と思うものの、800 円くらいで直るなら・・・ということで Transcend の USB メモリを買います。

https://amzn.to/2HI18bHamzn.to

しかし、結果は変わりませんでした。

別な PC で USB Recovery Creator を動かしてみる

この時点で、本体の USB コントローラや差込口の初期不良を疑っていました。

M75q を投げ捨てるのにちょうど良い溶鉱炉を探す途中、この USB Recovery CreatorLenovo 製品でしか動かないのだろうか?と疑問に思いました。

このツールは、 Lenovo 製 PC が動かなくなった時に工場出荷時状態に戻すためにも使えるものです。

Lenovo 製 PC が動かない時のためのツールだから、他の Windows マシンでも動くのでは?

幸い、会計ソフトを利用するために、MacParallels というソフトを入れて Windows10 が起動するようにしています。

その Windows に USB Recovery Creator をインストールして稼働したところ・・・ダウンロードが99%で止まることもなく、BUFFALO 、Transcend どちらの USB メモリにも書き込みが成功しました。

・・・えー、 Lenovo のツールが Lenovo 製マシンの上で動かなくて、Mac の上に仮想化して載ってる Windows マシンでまともに動くってどういう・・・

いざ SSD 換装 して リカバリメディアからブート

ここでやっと SSD を換装します。やや蓋とぶつかる感がありますが、そこはギュッと押し込みます。

まずは、BUFFALO の USB キーを差し込んで USB メモリからの起動を試みます。起動順の設定変更は起動直後から F12 キーです。

・・・はいだめー、認識しません。BUFFALO め・・・

続いて Transcend の USB キーで試したところ、無事 USB からのブートが成功。リカバリを完了させることができました。

長かった

なお、Windows が起動するまでで精神力を使い果たした感があるので、移行はこれからです。

まぁ、ボチボチやっていきますー

追記

みんな大変だったんだね・・・

追記2

リカバリ完了後、HandBreak で動画エンコードして早い早いと遊んでいたのですが、HDD も追加してやっとハードウェアの追加が完了しました。

HDD は容量 2TB 、厚さ 7mm のものを購入しました。装着してみた感想ですが、HDD は薄い方が良さそうですね。

https://amzn.to/37g3aIKamzn.to

これでやっと旧PCからの移行を進められそうです。

追記3

部屋の模様替えに伴い、VESA マウントキットを装着して M75q-1 Tiny をモニタ裏に装着しました。

なかなか良い感じです。

なお、マウントするための枠をモニタ裏にネジで装着し、そこにスライドさせる形で M75q-1 Tiny をスライドさせる感じです。

しっかり固定というわけではないので、モニタを縦横でグルングルン回転させる方は要注意。

f:id:sironekotoro:20210131110815j:plain