登場してから久しいShotgrid REST APIですがほとんどの場合Pythonからの(JavascriptやC#もありそう)アクセスになるかと思います。
事実うちでもPythonからのアクセスです。
しかし最近、USDのAssetResolverを筆頭にC++から扱いたい場合が出てきそうだったのでどのようにアクセスすればいいのか調べてみることにしました。
Shotgrid REST API
説明不要かと思いますがShotgridへアクセスする手段の一つであるREST APIです。
数年くらい前に登場しそれまでPythonを用いるしかなかったShotgridへのアクセスの新たな手段となりました。
うちの部署ではPython API版からこちらへ徐々にこちらへシフトしています。(といっても自分が作ったツールを改修しているだけだったりしますw)
使用ライブラリ
libcurl
言わずと知れたCURLのC言語ライブラリです。
Shotgrid REST APIの公式リファレンスにcurl
コマンドを使ったサンプルが乗っていてかつライセンスが緩かったので採用しました。
nlohmann json
Shotgrid REST APIを扱うにあたってjsonをパース、ダンプする必要があるため評価が高いnlohmann jsonを使うことにしました。
nlohmann jsonとはヘッダーオンリーで扱えるModern C++用のjsonライブラリです。
環境構築
今回、環境構築が一番大変でした...
クライアント側のOSとしてWindowsとLinuxが使用されることが想定されるのでCMakeを用いてプロジェクトを作っていきます。
まず、gitのsub moduleで管理することを想定してプロジェクトディレクトリにexternal
ディレクトリ作成しCURLとnlohmann jsonをgit clone
します。
project
├─CMakeLists.txt
│
└─external
├─curl
└─json
CMakeLists.txt
にそれぞれのライブラリをサブディレクトリとして登録し、CURLのビルドオプションを記述していきます。
今回はlibcurlを静的リンクしたかったのでそのためのオプションも追加しました。
cmake_minimum_required(VERSION 3.20)
# C++バージョンの設定
set(CMAKE_CXX_STANDARD 20)
# ユニコードの設定
add_definitions(-DUNICODE -D_UNICODE)
# CURLのSSL設定オプション
set(CURL_USE_OPENSSL TRUE CACHE BOOL "")
mark_as_advanced(CURL_USE_OPENSSL)
# CURLのライブラリ設定オプション
set(BUILD_SHARED_LIBS FALSE CACHE BOOL "")
set(BUILD_STATIC_LIBS TRUE CACHE BOOL "")
mark_as_advanced(BUILD_SHARED_LIBS)
mark_as_advanced(BUILD_STATIC_LIBS)
# CURLのユニコード設定オプション
set(ENABLE_UNICODE TRUE CACHE BOOL "")
mark_as_advanced(ENABLE_UNICODE)
# サブディレクトリ追加
add_subdirectory(external/curl)
add_subdirectory(external/json)
project(SGRestAPI)
環境によってはこれで問題ないですがOpenSSLがインストールされていない場合エラーが出て止まってしまうため以下のように修正しました。
project
├─CMakeLists.txt
│
├─external
│ ├─curl
│ └─json
└─other
└─curl-CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
# C++バージョンの設定
set(CMAKE_CXX_STANDARD 20)
# ユニコードの設定
add_definitions(-DUNICODE -D_UNICODE)
include(FetchContent)
FetchContent_Declare(
libressl
URL "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-3.8.2.tar.gz")
FetchContent_MakeAvailable(libressl)
list(APPEND CMAKE_PREFIX_PATH ${libressl_BINARY_DIR})
find_package(LibreSSL REQUIRED)
file(COPY_FILE "other/curl-CMakeLists.txt" "external/curl/CMakeLists.txt")
# CURLのSSL設定オプション
set(CURL_USE_OPENSSL TRUE CACHE BOOL "")
mark_as_advanced(CURL_USE_OPENSSL)
# CURLのライブラリ設定オプション
set(BUILD_SHARED_LIBS FALSE CACHE BOOL "")
set(BUILD_STATIC_LIBS TRUE CACHE BOOL "")
mark_as_advanced(BUILD_SHARED_LIBS)
mark_as_advanced(BUILD_STATIC_LIBS)
# CURLのユニコード設定オプション
set(ENABLE_UNICODE TRUE CACHE BOOL "")
mark_as_advanced(ENABLE_UNICODE)
# サブディレクトリ追加
add_subdirectory(external/curl)
add_subdirectory(external/json)
# libcurlのビルドにsslのビルドを依存させる
add_dependencies(libcurl_object ssl)
project(SGRestAPI)
add_executable(SGRestAPI main.cpp)
target_include_directories(SGRestAPI
PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(SGRestAPI
PUBLIC CURL::libcurl
nlohmann_json::nlohmann_json)
if(CURL_USE_OPENSSL)
find_package(LibreSSL REQUIRED)
set(SSL_ENABLED ON)
set(USE_OPENSSL ON)
# Depend on OpenSSL via imported targets if supported by the running
# version of CMake. This allows our dependents to get our dependencies
# transitively.
if(NOT CMAKE_VERSION VERSION_LESS 3.4)
list(APPEND CURL_LIBS LibreSSL::SSL LibreSSL::Crypto)
else()
list(APPEND CURL_LIBS ${LIBRESSL_LIBRARIES})
include_directories(${LIBRESSL_INCLUDE_DIR})
endif()
if(CURL_DEFAULT_SSL_BACKEND AND CURL_DEFAULT_SSL_BACKEND STREQUAL "openssl")
set(valid_default_ssl_backend TRUE)
endif()
set(CMAKE_REQUIRED_INCLUDES ${LIBRESSL_INCLUDE_DIR})
if(NOT DEFINED HAVE_BORINGSSL)
check_symbol_exists(OPENSSL_IS_BORINGSSL "openssl/base.h" HAVE_BORINGSSL)
endif()
if(NOT DEFINED HAVE_AWSLC)
check_symbol_exists(OPENSSL_IS_AWSLC "openssl/base.h" HAVE_AWSLC)
endif()
endif()
何をしているかというと、curlのサブディレクトリを追加する前にCMakeに対応しているOpenSSL派生であるLibreSSLをFetchContent
したあとLibreSSLをパッケージとして利用できるように修正したcurlのCMakeLists.txt
を上書きしています。
さらにビルドの依存関係が正しく来なかったのでadd_dependencies
を使いlibcurlのビルドにsslのビルドを依存させました。
他にもっといい方法があると思うのですが自分のCMakeの知識が乏しくこのような力業で対処しました。
スマートな方法をご存知の方がいらっしゃればコメントで教えてくださると助かります。
アクセストークンを取得
先ほども述べましたがShotgrid REST APIの公式リファレンスにcurl
コマンドを用いたサンプルが乗っているのでこちらを参考に進めていきます。
当初、自分でcurl
コマンドをC++コードに書き換えようと思っていたのですがせっかくならこのコマンドをそのままコンバートできないかと思い調べたところ--libcurl
というオプションを見つけました。
こちらのオプション、名前の通りこのオプションとファイルパスを指定するとそちらに実行したコマンドの内容をlibcurl
を用いたコードに変えてくれるという代物で今回はこちらをがっつり使わせていただくことにしました。
お恥ずかしながらWindowsコマンドを一切書けないのでPythonを使ってバッチファイルの代わりにしています。
以下のコードは作成したShotgrid APIのスクリプト名とスクリプトキーからShotgrid REST APIのアクセストークンを取得しています。
import json
import subprocess
sg_url = '...'
sg_script_name = '...'
sg_script_key = '...'
curl_bin_dir = r'...'
out_cpp_file = r'...'
curl_command = f'{curl_bin_dir}/curl '
curl_command += f'-X POST '
curl_command += f'{sg_url}/api/v1/auth/access_token '
curl_command += f'-H "Content-Type: application/x-www-form-urlencoded" '
curl_command += f'-H "Accept: application/json" '
curl_command += f'-d "client_id={sg_script_name}&client_secret={sg_script_key}&grant_type=client_credentials" '
curl_command += f'--libcurl {out_cpp_file} '
res = subprocess.check_output(curl_command)
こちらを実行するとout_cpp_file
に指定したファイルに以下のコードが出力されます。
/********* Sample code generated by the curl command line tool **********
* All curl_easy_setopt() options are documented at:
* https://curl.se/libcurl/c/curl_easy_setopt.html
************************************************************************/
#include <curl/curl.h>
int main(int argc, char *argv[])
{
CURLcode ret;
CURL *hnd;
struct curl_slist *slist1;
slist1 = NULL;
slist1 = curl_slist_append(slist1, "Content-Type: application/x-www-form-urlencoded");
slist1 = curl_slist_append(slist1, "Accept: application/json");
hnd = curl_easy_init();
curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
curl_easy_setopt(hnd, CURLOPT_URL, ".../api/v1/auth/access_token");
curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, "client_id=...&client_secret=...&grant_type=client_credentials");
curl_easy_setopt(hnd, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)98);
curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/8.5.0");
curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
curl_easy_setopt(hnd, CURLOPT_CAINFO, "...");
curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "POST");
curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);
/* Here is a list of options the curl code used that cannot get generated
as source easily. You may choose to either not use them or implement
them yourself.
CURLOPT_WRITEDATA was set to an object pointer
CURLOPT_INTERLEAVEDATA was set to an object pointer
CURLOPT_WRITEFUNCTION was set to a function pointer
CURLOPT_READDATA was set to an object pointer
CURLOPT_READFUNCTION was set to a function pointer
CURLOPT_SEEKDATA was set to an object pointer
CURLOPT_SEEKFUNCTION was set to a function pointer
CURLOPT_ERRORBUFFER was set to an object pointer
CURLOPT_STDERR was set to an object pointer
CURLOPT_HEADERFUNCTION was set to a function pointer
CURLOPT_HEADERDATA was set to an object pointer
*/
ret = curl_easy_perform(hnd);
curl_easy_cleanup(hnd);
hnd = NULL;
curl_slist_free_all(slist1);
slist1 = NULL;
return (int)ret;
}
/**** End of sample code ****/
こちらをもとに取得したデータをjsonにパースするコードを書きました。
#include <format>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
// Shotgrid API関連の定数
static constexpr std::string_view SG_URL{"..."};
static constexpr std::string_view SG_SCRIPT_NAME{"..."};
static constexpr std::string_view SG_SCRIPT_KEY{"..."};
using json = nlohmann::json;
// 文字列リテラルを使用するため
using namespace std::literals::string_literals;
using namespace std::string_view_literals;
// CURL構造体のスマートポインタ
auto curl_deleter = [](CURL* _curl_ptr) { curl_easy_cleanup(_curl_ptr); };
using curl_ptr = std::unique_ptr<CURL, decltype(curl_deleter)>;
// stringstreamに出力するコールバック
auto writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t
{
if (!ptr)
{
return 0;
}
auto real_size = size * nmemb;
auto* ss = reinterpret_cast<std::stringstream*>(userdata);
ss->write(ptr, static_cast<std::streamsize>(real_size));
return real_size;
}
// アクセストークンを取得する関数
auto sgRestGetAcessToken(const std::string_view& sg_url, const std::string_view& sg_script_name,
const std::string_view& sg_script_key, json* sg_auth_json) -> bool
{
// CURL関連
curl_ptr curl(curl_easy_init(), curl_deleter);
CURLcode ret{};
curl_slist* slist{nullptr};
// HTTPレスポンスコード
int http_code{0};
// 出力データ
std::stringstream write_data{};
// ポストファイル
std::string post_file{std::format("client_id={}&client_secret={}&grant_type=client_credentials"sv, sg_script_name, sg_script_key)};
// ヘッダーを設定
slist = curl_slist_append(slist, "Content-Type: application/x-www-form-urlencoded");
slist = curl_slist_append(slist, "Accept: application/json");
// オプションを設定
curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, "POST");
curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, 102400L);
curl_easy_setopt(curl.get(), CURLOPT_URL, std::format("{}/api/v1/auth/access_token"sv, sg_url).c_str());
curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, post_file.c_str());
curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(post_file.size()));
curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 50L);
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl.get(), CURLOPT_FTP_SKIP_PASV_IP, 1L);
curl_easy_setopt(curl.get(), CURLOPT_TCP_KEEPALIVE, 1L);
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &writeCallback);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, static_cast<void*>(&write_data));
// 実行
ret = curl_easy_perform(curl.get());
// レスポンスコード取得
curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);
//
curl_slist_free_all(slist);
slist = nullptr;
// レスポンスコード200以外はエラー
if ((ret != CURLE_OK) || (http_code != 200))
{
return false;
}
// 出力データをjsonにパース
*sg_auth_json = json::parse(write_data);
return true;
}
auto main() -> int
{
// 初期化
curl_global_init(CURL_GLOBAL_ALL);
// アクセストークンjson
json auth_json{};
if (!sgRestGetAcessToken(SG_URL, SG_SCRIPT_NAME, SG_SCRIPT_KEY, &auth_json))
{
curl_global_cleanup();
return 1;
}
auto token_type = auth_json["token_type"].get<std::string>();
auto access_token = auth_json["access_token"].get<std::string>();
// アクセストークンを表示
std::cout << std::format("token_type : {}, access_token: {}"sv, token_type, access_token) << std::endl;
// クリーンアップ
curl_global_cleanup();
return 0;
}
レコードを検索
アクセストークンを取得するコードができたので次にレコードを検索するコードを作成します。
先ほどと同様にPythonでコマンドを実行します。
import json
import subprocess
sg_url = '...'
sg_script_name = '...'
sg_script_key = '...'
curl_bin_dir = r'...'
out_cpp_file = r'...'
...
...
res = subprocess.check_output(curl_command)
auth_json = json.loads(res)
filters = {
"filters": [["name", "contains", "..."]],
"fields": ["name"]
}
curl_command = f'{curl_bin_dir}/curl '
curl_command += '-X POST '
curl_command += f'{sg_url}/api/v1/entity/projects/_search '
curl_command += f'-H "Content-Type: application/vnd+shotgun.api3_array+json" '
curl_command += f'-H "Accept: application/json" '
curl_command += f'-H "Authorization: {auth_json["token_type"]} {auth_json["access_token"]}" '
curl_command += f'-d "{json.dumps(filters)}" '
curl_command += f'--libcurl {out_cpp_file} '
res = subprocess.check_output(curl_command)
このコードはエラーが返りますが本題には関係ないので無視します。
また出力されたC++ファイルは省略します。
#include <format>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
// Shotgrid API関連の定数
static constexpr std::string_view SG_URL{"..."};
static constexpr std::string_view SG_SCRIPT_NAME{"..."};
static constexpr std::string_view SG_SCRIPT_KEY{"..."};
using json = nlohmann::json;
// 文字列リテラルを使用するため
using namespace std::literals::string_literals;
using namespace std::string_view_literals;
// CURL構造体のスマートポインタ
auto curl_deleter = [](CURL* _curl_ptr) { curl_easy_cleanup(_curl_ptr); };
using curl_ptr = std::unique_ptr<CURL, decltype(curl_deleter)>;
auto writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t;
auto sgRestGetAcessToken(const std::string_view& sg_url, const std::string_view& sg_script_name,
const std::string_view& sg_script_key, json* sg_auth_json) -> bool;
auto sgRestSearchRecord(const std::string_view& sg_url, const std::string_view& sg_entity,
const std::string_view& sg_token_type, const std::string_view& sg_access_token, const json& sg_filters_json, json* sg_result_json) -> bool
{
// CURL関連
curl_ptr curl(curl_easy_init(), curl_deleter);
CURLcode ret{};
curl_slist* slist{nullptr};
// HTTPレスポンスコード
int http_code{0};
// フィルターデータおよび出力データ
std::stringstream filters_data{}, write_data{};
filters_data << sg_filters_json;
// ポストファイル
std::string post_file{filters_data.str()};
// ヘッダーを設定
// TODO:Hashタイプのフィルターに対応
slist = curl_slist_append(slist, "Content-Type: application/vnd+shotgun.api3_array+json");
slist = curl_slist_append(slist, "Accept: application/json");
slist = curl_slist_append(slist, std::format("Authorization: {} {}"sv, sg_token_type, sg_access_token).c_str());
// オプションを設定
curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, "POST");
curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, 102400L);
curl_easy_setopt(curl.get(), CURLOPT_URL, std::format("{}/api/v1/entity/{}/_search"sv, sg_url, sg_entity).c_str());
curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, post_file.c_str());
curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(post_file.size()));
curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 50L);
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl.get(), CURLOPT_FTP_SKIP_PASV_IP, 1L);
curl_easy_setopt(curl.get(), CURLOPT_TCP_KEEPALIVE, 1L);
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &writeCallback);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, static_cast<void*>(&write_data));
// 実行
ret = curl_easy_perform(curl.get());
// レスポンスコード取得
curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);
//
curl_slist_free_all(slist);
slist = nullptr;
// レスポンスコード200以外はエラー
if ((ret != CURLE_OK) || (http_code != 200))
{
return false;
}
// 出力データをjsonにパース
*sg_result_json = json::parse(write_data);
return true;
}
auto main() -> int
{
// 初期化
curl_global_init(CURL_GLOBAL_ALL);
// アクセストークン、検索出力、フィルターjson
json auth_json{}, result_json{}, filters_json{};
if (!sgRestGetAcessToken(SG_URL, SG_SCRIPT_NAME, SG_SCRIPT_KEY, &auth_json))
{
curl_global_cleanup();
return 1;
}
auto token_type = auth_json["token_type"].get<std::string>();
auto access_token = auth_json["access_token"].get<std::string>();
// アクセストークンを表示
std::cout << std::format("token_type : {}, access_token: {}"sv, token_type, access_token) << std::endl;
// フィルターを設定
filters_json["filters"] = {{"name", "contains", "..."}};
filters_json["fields"] = {"name"};
// フィルター内容を表示
std::cout << filters_json << std::endl;
if (!sgRestSearchRecord(SG_URL, "projects", token_type, access_token, filters_json, &result_json))
{
curl_global_cleanup();
return 1;
}
// 取得結果を表示
std::cout << result_json << std::endl;
// クリーンアップ
curl_global_cleanup();
return 0;
}
上記コードはHashタイプのフィルターに対応していないためもし使用される場合はご注意ください。
終わりに
今後はUSDのAssetResolverに組み込む等を試すつもりです。
libcurlまわりのコードが少しわずらわしいのでC++ライクに書けるラッパーを用意しようかと思います。