Box APIでファイルを分割アップロードする方法
Boxの分割アップロードAPI(chunked upload API)を使い、20MB以上のファイルをアップロードする方法を調べたときのサンプルコードが手元に残っていたので、メモを兼ねて記事に書きます。
経緯
仕事でBox APIを使い、比較的大きなファイルをアップロードする必要があったので、調査した結果を思い出しながらコードに落としました。 以前、Guzzlehttpでフォームを使ってファイルをアップロードをする方法を記事に書きましたが、それの分割アップロード版です。
サンプルコード
GitHubに置いておきました。
https://github.com/flipfrog/box-api-chunked-upload-test
実装上のポイント
Box APIでは、下記の手順でファイルをアップロードします。
- アップロードセッションを作成します
- ファイルを分割して、分割されたチャンク(chunk)毎にBoxのサーバーに送信します
- それぞれのチャンクは、並列に送信できます
- アップロードセッションをコミットします
- アップロードセッションを削除します
チャンクを送信するときの並列度はOSやメモリーの制約内であれば任意の値を取れそうですが、実験ではある程度の値を超えると性能が頭打ちになりました。ネットワークやBox APIサーバーの混み具合にも依存すると思うので状況に合わせて最適値を決める必要がありそうです。今回のサンプルコードでは、並列度5で定数を設定していますが、あくまでも一例です。
また、チャンクドアップロードは、サイズが20MB以上のファイルを対象にしているので、それ未満のファイルを送信しようとするとエラーになります。ファイルサイズによって、前回記事でのフォームを使ったアップロードと今回のチャンクドアップロードを切り替える必要があります。
ハマったポイント
実装時に少しハマったのが、アップロードセッションをコミットするときにPOSTのペイロードに格納するchunkの一覧をソートせずに送信して400系のエラーを返されたことでした。
今回はGuzzlehttpの並列送信機能を利用しました。 並列度の範囲内ですが、各チャンクは並列に処理されるため、それぞれの送信時間にはばらつきが発生します。その結果、単純に返ってきたpromiseの情報を配列に追加するだけでは、チャンク毎のファイル内でのオフセット位置の昇順でなくなってしまいます。 そのため、全てのチャンクを送信した後にコミット情報をチャンクのファイル内オフセットでソートする必要がありました。
サンプルコードで注意するところ
GitHubリポジトリにあるサンプルプログラムtest.phpは、ファイルを新規にアップロードする処理になっています。
既存ファイルを更新する場合は、そのファイルのIDを指定する必要があり、ファイルIDを指定しないと、409エラーが発生します。
このサンプルでは、ファイルIDを取得する処理は含まれていないので、各自でファイルIDを保持したり、仮想的なパスを基にファイルIDを取得する必要があります。
アップロードの動作を確認するだけであれば、下記の2箇所を修正することで、既存ファイルを更新することができます。
サンプルコードの動かしかた
前回のBox APIの記事に使ったコードと同じ構成にしてあります。
composer installを実行します。- Boxのデベロッパーコンソール > アプリを選択 > Configurationタブ > Developer Tokenからトークンを取得して、サンプルコードリポジトリのconfig/box.phpに設定します。
- README.mdにも書いていますが、
config/box.php.exampleをconfig/box.phpにコピーして下さい。
- README.mdにも書いていますが、
今回は20MB超えの大きめのファイルを送信するために、テストファイルをリポジトリに置いていません。その代わりに、C言語で書いた簡単なプログラムで任意のMB数のテストファイルを作成できるようにしてあります。
- こちらもREADME.mdに書いていますが、
makeを実行することにより、テストファイル作成プログラムがコンパイルされた後に、テストファイルをdata/test.datとして作成します。Makefile内の21というMB単位のファイルサイズを変更すれば、任意のサイズのファイルを作成できます。Makefile内の数値を変更する、data/test.datファイルを消す、makeを実行する手順で希望のサイズのファイルを作成できます。
- こちらもREADME.mdに書いていますが、
準備ができたら、
php test.phpで実行します。
コードについて
Box APIの使い方についてフォーカスしたので、特にクラス化などは行っていません。
macOSのDocker Desktopで、No route to hostエラーを回避する方法
とても限定的な環境ですが、仕事で使っているIntel MacBook Pro上のDocker環境で表題のエラーが発生して困っていたのを、おそらく、解消できたのでメモします。
- ハードウェア:13-inch, 2018, Four Thunderbolt 3 Ports
- OS:13.0.1
- Docker Desktop:4.15.0 (93002)
PHPのライブラリであるGuzzlehttpを使って、Box APIにファイルをチャンクに分割してアップロードする、Uploads (Chunked) APIを使い、並列度10以上でファイル転送していると頻繁に下記のエラーが発生しました。
cURL error 7: Failed to connect to api.box.com port 443: No route to host (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://api.box.com/2.0/folders/<folder_id>/items in <ソースのファイル名>
一度これが出ると、Docker Desktopをrestartするまで出続けるという現象が発生していました。
対処方法は、下記のスクリーンショットにある、Preferences > General > Use Virtualization frameworkのチェックを外します。これにより、前述のエラーが発生しなくなりました。

この対処方法は、下記の記事を参考にしました。
https://forums.docker.com/t/192-168-65-1-connect-no-route-to-host/109264/6
Box APIでGuzzlehttpを使ってファイルをアップロードする方法
Box APIを使ってファイルをアップロードする方法を調べたので記事を書きます。
経緯
仕事でBox APIを使っているのですが、ファイルアップロードがうまく行かずに詰まっていました。
帰宅後に調べて解決したので記事に書きます(スッキリしました)。
サンプルコード
GitHubに置いておきました。
https://github.com/flipfrog/box-api-upload-using-guzzlehttp
実装上のポイント
リファレンスにかかれている、cURLでの使い方をGuzzlehttpでどのように実装するかでした。
-F attributes='{"name":"Contract.pdf", "parent":{"id":"11446498"}}' \
-F file=@<FILE_NAME>
POSTMANを使うと、ほぼそのままリクエストパラメータを設定すればアップロードできたのですが、私は直感的にGuzzlehttpを使った実装ができませんでした。Guzzlehttpでは下記のように、multipartフォームを記述します。
'multipart' => [ [ 'name' => 'parent_id', 'contents' => 0, // 0: root folder ], [ 'name' => 'file', 'contents' => \GuzzleHttp\Psr7\Utils::tryFopen('./data/test.txt', 'rb'), 'filename' => 'my_test.txt', ], ],
最初のブロックの、name=parent_idの書き方は、Boxが提供するOpen API specification schemaから、openapi-generatorを使ってPHPのSDKを生成したときの src/Command/Content/File/UploadFile.php ファイルに記載されていた方法を参考にしました。
2つ目のブロックのcontentsに、文字列やファイルが紐付かないstreamを指定したときには、同ブロックのfilenameを指定しないと400エラーになります。
詰まっていた理由
いくつかの理由でうまくいかずに困っていました。
アップロードするサーバーは他のエンドポイントとは別
リファレンス をよく見ていなかった自分が悪いですね。
APIを使ったファイルのアップロードに慣れていなかった
ほぼ、リファレンスに書かれている感じでPOSTMANを使うとファイルをアップロードできますが、Guzzlehttpを使って同じことをやろうとするとうまく行かない。というか、その方法が分からなかった。
下図はPOSTMANでのリクエストパラメータ設定です。

情報が少ない
ネットで検索すると、同じような質問が幾つかありましたが、どれも解決に至っていませんでした。 Box公式のSDKが無く、個人的に作成されたものをGitHubで見たのですが、使うGuzzlehttpのバージョンが4.xと古く、最新の7.xでは参考になりませんでした。
追記
記事を書いたあとに情報を見つけたのですが、multipartブロックを下記のように書けば直感的になるようです。
'multipart' => [ [ 'name' => 'attributes', 'contents' => json_encode([ 'name' => 'my_test.txt', 'parent' => ['id' => 0], ]), ], [ 'name' => 'file', 'contents' => \GuzzleHttp\Psr7\Utils::tryFopen('./data/test.txt', 'rb'), 'filename' => 'my_test.txt', // optional to specify file name on box ], ],
Boxに作成されるファイルのファイル名は、1つ目のブロックのJSON内のname属性が優先して採用されるようです。
出典: https://support.box.com/hc/ja/community/posts/5289184800019-API-upload-did-not-contain-a-file-part
PHP7.2 + xdebug 3.1.5 + PHPStorm(2022.2.3) で、ステップ実行したときにSegmentation faultする件
仕事で、PHP7.2実行環境とxdebug3.1.5の組み合わせで、ブレークポイントで止まったあとにステップ実行すると、Segmentation faultする現象の対応に時間を割いてしまったのでメモ。
- IDEは、PHPStorm(2022.2.3)です。
- VSCodeをインストールして同じことをすると、コアダンプしないで使うことができました。
- PHP7.4では問題なく動作するようなので、PHP7.2との組み合わせ固有の現象のようです。
対処方法は、PHPStormのPreferences... > PHP > Debug にある、Evaluation欄で Enable '__toString' object viewのチェックを外すとコアダンプしなくなりました。

php -vを実行したときの出力は、以下の通りです。
PHP 7.2.24 (cli) (built: Oct 22 2019 08:28:36) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans
with Zend OPcache v7.2.24, Copyright (c) 1999-2018, by Zend Technologies
php.iniのxdebugセクションの抜粋は、以下の通りです。
[xdebug] zend_extension=/usr/lib64/php/modules/xdebug.so xdebug.mode=debug xdebug.start_with_request=yes xdebug.discover_client_host = 0 xdebug.remote_handler = "dbgp" xdebug.client_port=9003 xdebug.client_host=host.docker.internal xdebug.idekey=phpstorm
OSは、AlmaLinux 8をDocker環境で実行しています。
# cat /etc/redhat-release AlmaLinux release 8.6 (Sky Tiger)
gdbを使って実行させたときのバックトレースです。
(gdb) run test.php
Starting program: /usr/bin/php test.php
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x00005583000a56dd in _zval_ptr_dtor ()
(gdb) bt
#0 0x00005583000a56dd in _zval_ptr_dtor ()
#1 0x00007f6e28dd85c0 in xdebug_dbgp_handle_eval (retval=0x7fff3966c850,
context=<optimized out>, args=<optimized out>)
at /var/tmp/xdebug/src/debugger/handler_dbgp.c:1055
#2 0x00007f6e28ddcdc6 in xdebug_dbgp_parse_option (flags=0,
retval=<optimized out>,
line=0x55830245fa10 "eval -i 24 -- KHN0cmluZykoJHNtYXJ0eSk=",
context=0x7f6e28ffdcb8 <xdebug_globals+376>)
at /var/tmp/xdebug/src/debugger/handler_dbgp.c:2230
#3 xdebug_dbgp_cmdloop (context=0x7f6e28ffdcb8 <xdebug_globals+376>, bail=1)
at /var/tmp/xdebug/src/debugger/handler_dbgp.c:2340
#4 0x00007f6e28dddd99 in xdebug_dbgp_breakpoint (
context=0x7f6e28ffdcb8 <xdebug_globals+376>, stack=<optimized out>,
filename=<optimized out>, lineno=23, type=<optimized out>, exception=0x0,
code=0x0, message=0x0, brk_info=0x558302416b60)
at /var/tmp/xdebug/src/debugger/handler_dbgp.c:2701
#5 0x00007f6e28dd3534 in xdebug_debugger_statement_call (
filename=0x7f6e29076030, lineno=23)
at /var/tmp/xdebug/src/debugger/debugger.c:294
#6 0x00005583000a940a in zend_llist_apply_with_argument ()
#7 0x00005583001457dd in ZEND_EXT_STMT_SPEC_HANDLER ()
#8 0x0000558300159a50 in execute_ex ()
#9 0x00007f6e28dbe60a in xdebug_execute_ex (execute_data=0x7f6e2901c030)
--Type <RET> for more, q to quit, c to continue without paging--
r/tmp/xdebug/src/base/base.c:779
#10 0x000055830015d776 in zend_execute ()
#11 0x00005583000b74cb in zend_execute_scripts ()
#12 0x0000558300052a40 in php_execute_script ()
#13 0x000055830015fd5c in do_cli ()
#14 0x00005582fff0d632 in main ()
(gdb)
Rustでfreee会計とAPI連携するためのSDKとサンプル実装の作成
今回の記事
Rustを使って、freee会計のパブリックAPIとの連携用SDKを作ったので記事を書きます。
https://github.com/flipfrog/freee-accounting-sdk-rust
このSDKは、昨年、最初のバージョンを作成し、その後、サンプルの拡充やスキーマをもとにした最新化を続けています。
この記事では、SDKでできることや、サンプルプログラムの動かし方、SDKを実装するときにやったことなどを書きます。
このSDKでできること
Rust言語でfreeeと連携するアプリを作成することができます。
具体的には、freeeアプリストアで作成したアプリの接続情報を使って、API経由でアプリのアクセストークン取得や会計システムと連携することができます。
連携できる内容は、freeeのOAuth2認証と個々のAPI呼び出しです。詳細は、freeeのパブリックAPIの説明を参照して下さい。Web画面でできることの多くをAPIを使って実行することができます。
記事のタイトルの通り、このSDKはRustで実装されており、Rustで書かれたアプリケーションから呼び出されることを想定してます。
このSDKでできないこと
freeeでは、会計、人事労務など、いくつかのプロダクトのパブリックAPIが用意されています。 このSDKは、会計APIのSDKなので、会計API以外にアクセスする機能は提供していません。
サンプルプログラムの動かし方
このリポジトリに含まれるサンプルは、全てmacOS環境で動作確認をしています。 Windowsでは確認していないので、必要に応じてコマンドの使い方などを置き換えて読んで下さい。
最初にやること
このSDKに付属するサンプルプログラムを動かすには、GitHubからSDKのリポジトリをcloneすることから始めます。
% git clone git@github.com:flipfrog/freee-accounting-sdk-rust.git
PCにRustのコンパイル環境がまだ無い場合は、コンパイラをインストールしましょう。 rustupを使えば、コンパイラのバージョン管理も楽になります。
https://www.rust-lang.org/ja/tools/install
次に、APIへのアクセスを試すための開発用テスト事業所を作成します。
freeeアプリストアの開発者ページ上部にある「開発用テスト事業所の作成」リンクから作成画面に入ります。
画面の指示通りに進めていけば、簡単にテスト事業所を作成できます。

つぎは、サンプルプログラムの動かし方を説明します。
コンソールで動くサンプルプログラムの動かし方
最初にコンソールで動作するサンプルの実行方法を説明します。
開発用テスト事業所で、テスト用のアプリを作成します
このサンプルでは、必須入力項目だけ入力してアプリを作成し下書き保存します。公開する必要はありません。
環境変数の設定
作成したアプリの基本情報にある、モバイル・JSアプリ認証用URLをクリップボードにコピーして、ブラウザの別タブなどで開きます。

アプリ連携の開始画面が表示されるので、許可するボタンをクリックします。
すると、アクセストークンが表示されるので、それをクリップボードにコピーして、以下のように環境変数にセットします。
% export RUST_API_EXAMPLE_OAUTH_ACCESS_TOKEN=<コピーしたアクセストークン>
プログラムの実行(事業所IDを一覧させるところまで)
先にcloneしたSDKにあるサンプルコードのディレクトリに入ります。
% cd freee-accounting-sdk-rust/examples/console
cargo run と打ちます。
% cargo run
すると、プログラムが使うライブラリのダウンロードとコンパイルが行われた後に、プログラムが実行されます。
コンパイル時にワーニングが表示されますが、気にしないで下さい。
% cargo run Compiling hashbrown v0.11.2 Compiling log v0.4.14 Compiling futures-task v0.3.19 Compiling tracing v0.1.32 :
最後に、事業所情報の一覧に続けて、エラーが表示されます。 このエラーは、テストする事業所コードが設定されていないために発生します。
Finished dev [unoptimized + debuginfo] target(s) in 21.00s
Running `/Users/flipfrog/github/freee-accounting-sdk-rust/target/debug/openapi-examples`
- company.id: XXXXXXX, company.display_name: YYYYYYYYYYYYYY
- company.id: ZZZZZZZ, company.display_name: 開発用テスト事業所
thread 'main' panicked at '事業所IDの取得に失敗しました: NotPresent', examples/console/src/main.rs:31:70
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
プログラムの実行(環境変数に事業所IDを設定して実行)
事業所一覧の中から、開発用テスト事業所のcompany.id(事業所ID)をコピーし、次の環境変数にセットします。
% export RUST_API_EXAMPLE_COMPANY_ID=ZZZZZZZ(実際は数値)
そして、cargo run を再度実行します。
% cargo run
warning: unused variable: `receipt`
:
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `/Users/flipfrog/github/freee-accounting-sdk-rust/target/debug/openapi-examples`
- company.id: XXXXXXX, company.display_name: YYYYYYYYYYYYYY
- company.id: ZZZZZZZ, company.display_name: 開発用テスト事業所
created: partner.id=99999999, partner.name=Rust API SDKテスト
got: partner.id=99999999, partner.name=Rust API SDKテスト
destroy: deleted the partner.
すると、指定した事業所に取引先を登録したすぐ後に、それを削除する処理が動いたことが分かります。
プログラムの実装内容
このサンプルの処理は、 examples/console/src/main.rs に書かれています。
このソースで参照する、Rust版SDKは examples/console/Cargo.toml で参照するように設定を書いています。
:
[dependencies]
openapi-sdk = { path = "../../sdk" }
:
リポジトリのトップ階層にある、sdkディレクトリをopenapi-sdkとして定義してあり、先のmain.rsからは、下記のようにuseディレクティブを使って参照しています。
use openapi_sdk::apis::partners_api; use openapi_sdk::apis::companies_api; use openapi_sdk::apis::configuration::Configuration; use openapi_sdk::models::PartnerCreateParams;
sdkディレクトリ以下の内容は、openapi-generatorによって自動生成されています。 また、生成されるソースコードの構成は、openapi-generatorが持つRust言語用テンプレートや、APIエンドポイントのスキーマファイルの内容に依存するので、各々のバージョンの組み合わせによって関数や構造体の構成が変更される可能性があります。
このサンプルでは、予め取得したアクセストークンを使いAPIにアクセスします。
後述する「Webサーバーとして動くサンプル」では、ユーザーに、freeeにログインしてもらい、アプリのアクセス範囲の確認後(アプリの認可後)、認証コードを得て、アクセストークンを取得する処理を行います。
main.rsでは、以下の流れで処理を行います。
let oauth_access_token = env::var("RUST_API_EXAMPLE_OAUTH_ACCESS_TOKEN").expect("アクセストークンの取得に失敗しました"); let config = Configuration { base_path: "https://api.freee.co.jp".to_string(), user_agent: None, client: reqwest::Client::new(), basic_auth: None, oauth_access_token: Some(oauth_access_token), bearer_access_token: None, api_key: None };
- アクセストークンに設定されたfreeeユーザーに紐付いた事業所の一覧を取得する
let companies = companies_api::get_companies(&config).await.expect("事業所一覧の取得に失敗しました");
- 上記で取得した事業所情報を標準出力に出力する
for company in &companies.companies { println!("- company.id: {}, company.display_name: {}", company.id, company.display_name.as_ref().unwrap_or(&"".to_string()) ); }
環境変数で指定された事業所IDに、取引先を新規登録する
登録する取引先の情報は、PartnerCreateParams構造体オブジェクトを作成して、partners_api::create_partner関数に渡します
let params = PartnerCreateParams { company_id, name: "Rust API SDKテスト".to_string(), code: None, shortcut1: None, shortcut2: None, org_code: None, country_code: None, long_name: None, name_kana: None, default_title: None, phone: None, contact_name: None, email: None, payer_walletable_id: None, transfer_fee_handling_side: None, address_attributes: None, partner_doc_setting_attributes: None, partner_bank_account_attributes: None, payment_term_attributes: None, invoice_payment_term_attributes: None }; // 取引先を作成する let new_partner = partners_api::create_partner(&config, params).await.expect("取引先の作成に失敗しました"); println!("created: partner.id={}, partner.name={}", new_partner.partner.id, new_partner.partner.name);
上記で登録した取引先の情報を取得する
作成した取引先IDを使って取得します
// 取引先を取得する let partner = partners_api::get_partner(&config, new_partner.partner.id, company_id).await.expect("取引先の取得に失敗しました"); println!("got: partner.id={}, partner.name={}", partner.partner.id, partner.partner.name);
同取り取引先を削除する
作成した取引先IDを使って削除します
// 取引先を削除する partners_api::destroy_partner(&config, new_partner.partner.id, company_id).await.expect("取引先の削除に失敗しました"); println!("destroy: deleted the partner.");
Webサーバーとして動くサンプル
次にWebサーバーとして動作するサンプルの実行方法を説明します。
freeeのアプリストアの開発者画面で、新しいアプリを作成します
こちらも、事業所を開発用テスト事業所に切り替えてから作成するのがよいでしょう。
コマンドラインアプリと同様に必須入力項目だけを入力すればOKです。
ただ、今回は、「コールバックURL」に http://localhost:8080/auth_callback を設定します。
このURLは、freeeでアプリの認可を行った後に、リダイレクトされるURLです。
そのリダイレクト先は、今回動かすWebサーバアプリのページです。

環境変数の設定
アプリストアのアプリ情報にある、クライアントIDとクライアントシークレットを下記のように環境変数に設定します。
% export RUST_API_EXAMPLE_CLIENT_ID=<クライアントID> % export RUST_API_EXAMPLE_CLIENT_SECRET=<クライアントシークレット>
Webサーバーの実行
先にcloneしたSDKにあるサンプルコードのディレクトリに入ります。
% cd freee-accounting-sdk-rust/examples/web
cargo run と打ちます。
% cargo run
すると、プログラムが使うライブラリのダウンロードとコンパイルが行われた後に、プログラムが実行されます。
コンパイル時にワーニングが表示されますが、気にしないで下さい。
コンソールに下記が表示されたら、Webサービスが動いているのでブラウザで http://localhost:8080/ を開いてみて下さい。
開くと、下記のような画面が表示されると思います。

画面の「freeeで認証する」リンクをクリックします。
すると、freeeの認可画面に遷移して、下記のような画面が表示されます。 アプリ名は、自分で入力したものが表示されます。

「許可する」をクリックします。
すると、再びWebサンプル画面に戻って、ログインユーザ名とログインに関連付いている事業所情報が一覧されます。

ログインユーザー名と事業所の一覧情報は、freeeのAPIを使って取得しています。
Webサンプルの説明は以上です。
Webサーバーの実装
このサンプルの処理は、 examples/web/src/main.rs に書かれています。
Webフレームワーク
このサンプルでは、actix-webというフレームワークを使用しています。
このサンプルアプリケーション作成時点でactix-webのシェア率は、トップか2位くらいだったと思います。
シンプルに使うことができ、今回のサンプルを丁度いいサイズで書くことができました(ロジックは200行未満)。 サンプルのソースは、main.rs の中に全てのロジックを含み、テンプレートエンジン用のテンプレートを画面毎に作成しました。
テンプレートエンジンは、askamaテンプレートエンジンを使っています。サンプルアプリ開発時点で、CLionのプラグインでサポートされていたのですが、CLionのバージョンを上げるとプラグインが利用できなくなってしまいました(残念・・・)。
記述は、Laravelで標準のBLADEに似た感じで、個人的にとても書きやすいと思います。
リクエストのルーティング
Webサンプルの全体像を把握するために、ルーティングを確認してどのような画面がサーバーで提供されるかを見てみます。
main.rsファイルの最後には、下記のルーティング定義があります。
// webサービスを開始する HttpServer::new(move || { App::new() .app_data(web::Data::new(AppState { oauth: client.clone(), pool: pool.clone(), })) .wrap(middleware::Logger::default()) .service(web::resource("/").route(web::get().to(index))) .service(web::resource("/login").route(web::get().to(login))) .service(web::resource("/auth_callback").route(web::get().to(auth_callback))) .service(web::resource("/home").route(web::get().to(home))) }) .bind(("127.0.0.1", 8080))? .run() .await
/:Webサンプルに最初にアクセスする画面/login:ログインするリンクをクリックされたときに遷移します/auth_callback:freeeで認可された後にリダイレクトされます/home:/auth_callbackからリダイレクトされる画面
これらのページは、下図のように遷移します。

/loginへの遷移
/loginは、/indexページに表示される「freeeで認証する」リンクからGETで遷移します。 遷移後は、main関数で予め設定されたfreeeの認証ページURLにリダイレクトします。 したがって、/loginはページを持ちません。
このURLは固定値です。
- main関数での定義
let auth_url = AuthUrl::new("https://accounts.secure.freee.co.jp/public_api/authorize".to_string()) .expect("認可画面のURLが正しくありません");
- /login遷移時の処理
// ログインlinkがクリックされたら、freeeの認可画面にリダイレクトする async fn login(app_state: web::Data<AppState>) -> Result<HttpResponse, AWError> { let (auth_url, _csrf_token) = app_state.oauth .authorize_url(CsrfToken::new_random) .url(); Ok(HttpResponse::TemporaryRedirect() .append_header((header::LOCATION, auth_url.to_string())) .finish() ) }
/auth_callbackへの遷移
/loginからfreeeの認可画面に遷移後、ユーザーが「許可する」ボタンをクリックすると、freeeのアプリ設定である「コールバックURL」にリダイレクトされます。このコールバックURLは、先述したとおりアプリ情報作成時に、本webサンプルアプリの /auth_callback に設定してあります。
/auth_callbackは、ユーザーがfreeeの認可画面で「許可する」をクリックした後に、freeeの仕組みにより認可コードをパラメータに含んでリダイレクトされます。/auth_callbackへの遷移後、APIを使ってアクセストークンを取得し、後の処理のためにアクセストークンをDBに保存します。
アクセストークンの保存後、このサンプルでは、/homeページにリダイレクトします。 よって、/auth_callbackも画面を持ちません。
認証コードを/auth_callback遷移時のクエリパラメータから取得し、それを使ってアクセストークンを取得する
let code = params.code.clone(); let code = AuthorizationCode::new(code.to_string()); : // アクセストークンを取得する let token_res = web::block(move|| app_state.oauth.exchange_code(code).request(http_client).unwrap()).await;
アクセストークンをDBに保存する
// アクセストークンとリフレッシュトークンをDBに保存する conn.execute("INSERT INTO token VALUES ('EXAMPLE', ?1, ?2) ON CONFLICT(application_id) do update set refresh_token = ?1, token = ?2;", params![refresh_token, access_token]) .expect("アクセストークンの保存に失敗しました");
リフレッシュトークンも合わせて保存していますが、今回のアプリでは利用していません。
このアプリでは、DBにSQLiteを使用しています。
/home画面
/home画面は、/auth_callbackの処理で保存したアクセストークンを用いて、freeeのAPIにアクセスし、画面に表示する情報を取得します。
// DBからアクセストークンを取得する let conn = app_state.pool.get() .expect("DB接続の取得に失敗しました"); let mut statement = conn.prepare("SELECT token FROM token WHERE application_id = 'EXAMPLE'").unwrap(); let rows = statement.query_map(params![], |row| { let token: String = row.get(0).unwrap(); Ok(token) }) .expect("DBからのアクセストークンの取得に失敗しました"); let token = rows.reduce(|_a, t| t).unwrap().unwrap();
あとは、先に書いたコンソール版のサンプルアプリと同様に、Configuration構造体と共にAPIを呼び出します。
// APIで事業所の一覧を取得する let companies = companies_api::get_companies(&config).await .expect("事業所一覧の取得に失敗しました"); // APIでユーザー情報を取得する let me = users_api::get_users_me(&config, Some(false), Some(false)).await .expect("ユーザ情報の取得に失敗しました"); let last_name = me.user.last_name.unwrap_or("".to_string()); let first_name = me.user.first_name.unwrap_or("".to_string());
SDKを実装するときにやったこと
openapi-generatorの利用
本リポジトリのsdk/srcディレクトリの内容は、openapi-generatorというオープンソースのツールを使っています。
私は、Homebrewを使ってインストールしました。
% brew install openapi-generator
コマンドは、このリポジトリにある update_sdk_src.sh スクリプトで実行できます。
#/bin/bash openapi-generator generate -i https://raw.githubusercontent.com/freee/freee-api-schema/master/v2020_06_15/open-api-3/api-schema.json -g rust -o /tmp/api-rust cp -rp /tmp/api-rust/src sdk/
コマンドラインで参照するスキーマのJSONファイルは、swagger editorを使って表示することもできます。
A Tour of Goの最後の課題
はじめまして
愛知県で働く、ソフトウェアエンジニアのflipfrogといいます。
Go言語を知ろうと思い、A Tour of Goを一通りやってみました。
A Tour of Goをやっただけなので、他にも知らない機能があると思いますが、Go言語はシンプルで言語仕様が頭の中に入りやすかったです。
説明を読みながら課題をこなして、最後までたどり着きました。
最後の課題は、明確に正解が提示されていないので、どのような実装にしたかを記録しておこうと思います。
この課題は、並列処理と排他制御を使った疑似Webクローラーの実装でした。
修正後のコードは、GitHubのリポジトリに保存してあります。
課題で提示された実装からの修正差分は、コミットを参照して下さい。
課題の指示
- ウェブクローラ( web crawler )の並列化
- 同じURLを2度取ってくることなく並列してURLを取ってくる
ベースのコードは提供されているので、この課題ではそれを修正します。
2つめの「同じURLを2度取ってくることなく並列してURLを取ってくる」という指示には、URLを重複して結果に含めないのと、並列で実行するスレッドから重複を管理するデータへのアクセスに排他制御をかけるということが含まれていると考えました。
どのように実装を修正したか
- ウェブクローラ( web crawler )の並列化
Crawl関数は、与えられた1つのurlから再帰的にリソースに含まれる下位のurlを探索します。 下位urlの探索には、再帰呼びをしているので、その処理を別スレッドで実行するように修正しました。
上位のurlから下位のurlを呼び出しますが、上位側では下位urlの探索が終わるまで待ってから制御を上位に戻すようにしました。
+ var wg sync.WaitGroup for _, u := range urls { - Crawl(u, depth-1, fetcher) + wg.Add(1) + go func(u string) { + defer wg.Done() + Crawl(u, depth-1, fetcher, c) + }(u) } + wg.Wait() return }
並列化の制御のために、Crawl関数の呼び出しにクロージャをはさみました。
wg.Add(1)を呼び出したあとにgoroutineを起動、クロージャの処理が終了した時点でwg.Done()が呼び出されるようにしています。 呼び出し元では、全てのスレッドが終了するのをwg.Wait()を使って待ちます。
また、この記事を書いている時点で、main関数でsync.WaitGroupを作成して、Crawl関数の引数渡しで持ち回った方がより並列度が上がるかもしれないと思いました。
- 同じURLを2度取ってくることなく並列してURLを取ってくる
重複しないURLを作成するために、mapを使うことにしました。
更に、排他制御も必要なので、前の課題で作成したSafeCounterを使うことにしました。
SafeCounterオブジェクトへのアドレスを、Crawl関数に渡します。
c := SafeCounter{v: make(map[string]int)}
Crawl("https://golang.org/", 4, fetcher, &c)
Crawl関数では、SafeCounterをロック後、渡されたurlが未登録の場合、そのurlをmapに登録しロックを解除します。 既に登録されている場合は、ロックを解除して制御を呼び元に戻します。 これにより、テストしてセットする処理をアトミックに処理できるようになりました。
c.mu.Lock()
if c.v[url] > 0 {
c.mu.Unlock()
return
}
c.v[url]++
c.mu.Unlock()
全てのスレッドの処理が終わったら、SafeCounterオブジェクトに格納されたurlを表示してmain関数の処理が終わります。
for url, _ := range c.v { fmt.Println("*** ", url) }
以下は、このプログラムをコンソールで実行したときの出力です。
$ ./exercise found: https://golang.org/ "The Go Programming Language" not found: https://golang.org/cmd/ found: https://golang.org/pkg/ "Packages" not found: https://golang.org/cmd/ found: https://golang.org/pkg/os/ "Package os" found: https://golang.org/pkg/fmt/ "Package fmt" *** https://golang.org/ *** https://golang.org/pkg/ *** https://golang.org/pkg/os/ *** https://golang.org/pkg/fmt/ $
***が先頭に付いた行が、収集されたurlの一覧です。
IDEを使うメリット
この課題を実装する過程で、処理結果が想定どおりにならなくて困っていたときがありました。 原因は、ループ変数を、クロージャ内部で使っていたことでした。 そして、ループ変数をクロージャ関数の引数として渡すことで問題を解消しました。
そのとき、たまたまコードをIDEで編集してみようと思い、コードを開いたところ下記のようなワーニングが表示されて気づくことができました。 IDEを使っていなければ、更に余計な時間を使ったと思います。
