CloudDoc: R2JUCEクラウド連携サンプルアプリケーション
English follows Japanese
JUCEでGoogle DriveとOneDriveの差異を吸収したファイル読み書きモジュールを作ったんだけど、もしかしたらこれJUCEじゃなくても有意義なんじゃないかと思い公開。
GitHubにあります。この中のexample/CloudDocです。
https://github.com/masanaohayashi/R2JUCE
CloudDocは、JUCEフレームワークとカスタムモジュールR2JUCEを使用して、クラウドストレージ(Google Drive、OneDrive)およびローカルストレージとの間でファイルを操作するサンプルアプリケーションです。このアプリケーションは、R2JUCEが提供するクラウド連携機能を実演します。
主な機能
- クラウドサービス選択: Google Drive、Microsoft OneDrive、およびローカルストレージ間で切り替えてファイル操作を行うことができます。
- OAuth 2.0デバイス認証フロー: Google DriveおよびOneDriveへの初回アクセス時には、OAuth 2.0デバイス認証フローを介したユーザー認証が必要です。このプロセスはアプリケーション内で完結し、ユーザーは表示されるURLにアクセスし、コードを入力することで認証を完了できます。この認証方式は、通常、TV用アプリなど、キーボード入力が困難なデバイスで用いられます。そのため、Raspberry Piなどのデバイスで動作するCloudDocアプリケーションを認証する際にも、スマートフォンやMac/PCを使用して認証を行うことが可能です。
- ファイル操作: 画面内のテキストエディタに表示されたテキストデータをクラウドまたはローカルに保存したり、読み込んだりすることができます。
- ドラッグ&ドロップによるファイルアップロード: アプリケーションの指定されたエリアにファイルをドラッグ&ドロップすることで、その内容を読み込み、現在選択されているクラウドサービスにアップロードできます。この機能は主に画像や音声などのファイルに対応しています。フォルダのアップロードはまだ実装してないです。
- 設定の永続化: 最後に選択したクラウドサービス、ファイルパス、ファイル名がアプリケーションの終了時に保存され、次回起動時に自動的に復元されます。
- カスタムUIコンポーネント: 進捗表示付きアラートや、ソフトウェアキーボード対応(Raspberry Pi向け)のテキストエディタなど、R2JUCEが提供するカスタムUIコンポーネントを使用しています。
はじめに
このプロジェクトをビルドして実行するには、JUCEフレームワークの基本的な知識と、C++開発環境がセットアップされている必要があります。
Google Drive / OneDrive 認証情報の取得
Google DriveおよびOneDriveと連携するためには、ご自身のクラウドプラットフォームでアプリケーションを登録し、クライアントIDとクライアントシークレットを取得する必要があります。これらの情報は機密性が高いため、公開リポジトリにコミットしないように注意してください。
Google Driveの場合
- Google Cloud Consoleにアクセスします。
- 左上の「プロジェクトの選択」をクリックし、新規プロジェクトを作成するか、既存のプロジェクトを選択します。
- 左側の「APIとサービス」>「ライブラリ」から「Google Drive API」を有効にします。
- 「APIとサービス」>「認証情報」に移動します。
- 「認証情報を作成」をクリックし、「OAuth クライアント ID」を選択します。
- アプリケーションの種類として「デスクトップ アプリケーション」を選択し、クライアントを作成します。
- 作成されたクライアントの「クライアント ID」と「クライアント シークレット」をメモします。
Microsoft OneDriveの場合
- Azure Portalにアクセスし、サインインします。
- 「Azure Active Directory」>「アプリの登録」に移動します。
- 「新規登録」をクリックし、以下の情報を入力します。
-
名前:
CloudDoc Desktop Client
(任意の名前) - サポートされているアカウントの種類: 「個人用 Microsoft アカウントのみ」を選択します。
- リダイレクト URI: ここは空欄のまま進めてください。デバイス認証フローではリダイレクトURIは使用しません。
-
名前:
- アプリの登録後、左メニューの「証明書とシークレット」に移動し、「新しいクライアント シークレット」を作成します。表示される値(シークレット)を必ずすぐにメモしてください。この値は一度しか表示されず、後から確認することはできません。メモを忘れた場合は、シークレットを削除して再作成する必要があります。
- 左メニューの「API のアクセス許可」に移動し、「アクセス許可の追加」をクリックします。
- 「Microsoft Graph」を選択し、「委任されたアクセス許可」タブで以下のアクセス許可を追加します。
Files.ReadWrite
offline_access
- 追加後、「管理者の同意を与えます」をクリックします。
認証情報の設定
取得したクライアントIDとクライアントシークレットは、Source/Credentials_template.h
ファイルを Source/Credentials.h
にリネームし、そのファイルに定義されている以下のマクロを置き換えることで設定します。
警告: Credentials.h
ファイルは機密情報を含むため、バージョン管理システム(Gitなど)にコミットしないでください。
ビルドと実行
- Projucerでプロジェクトファイル(
.jucer
)を開きます。 - 適切なエクスポートターゲット(Visual Studio, Xcode, Makefileなど)を選択し、IDEプロジェクトを生成します。
- 生成されたプロジェクトをIDE(Visual Studio, Xcodeなど)で開き、ビルドして実行します。
アプリケーションが起動したら、ドロップダウンメニューからクラウドサービスを選択し、ファイル操作を試すことができます。
CloudDoc アプリケーションの Google Drive および OneDrive API を使ったファイル読み書きの詳細
CloudDoc アプリケーションは、R2CloudManager
クラスを介して抽象化された R2CloudStorageProvider
インターフェースを利用し、具体的なクラウドサービスプロバイダ(R2GoogleDriveProvider
、R2OneDriveProvider
)がそれぞれの API と連携しています。
認証方式 (OAuth 2.0 Device Authorization Flow)
Google Drive と OneDrive の両方で、OAuth 2.0 Device Authorization Flow が使用されています。これは、Web ブラウザを持たないデバイス(例: Raspberry Pi)から認証を行う場合に適したフローです。
1. デバイスコードのリクエスト
- アプリは、選択されたサービス(Google DriveまたはOneDrive)に応じて、それぞれの認証エンドポイントにデバイスコードをリクエストします。この処理は R2CloudAuthComponent::requestDeviceCode() メソッドで行われます。
- Google Drive: https://oauth2.googleapis.com/device/code
client_id
とscope
(https://www.googleapis.com/auth/drive.file) を POST データとして送信します。
// R2CloudAuthComponent::requestDeviceCode()
if (serviceType == ServiceType::GoogleDrive)
{
if (googleClientId.isEmpty())
{
showError(TRANS("Google credentials not set"));
return;
}
endpoint = "https://oauth2.googleapis.com/device/code";
postData = "client_id=" + juce::URL::addEscapeChars(googleClientId, false);
postData += "&scope=" + juce::URL::addEscapeChars("https://www.googleapis.com/auth/drive.file", false);
}
- OneDrive
https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode
client_id と scope (Files.ReadWrite offline_access) を POST データとして送信します。
// R2CloudAuthComponent::requestDeviceCode()
else if (serviceType == ServiceType::OneDrive)
{
if (oneDriveClientId.isEmpty())
{
showError(TRANS("OneDrive credentials not set"));
return;
}
endpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
postData = "client_id=" + juce::URL::addEscapeChars(oneDriveClientId, false);
postData += "&scope=" + juce::URL::addEscapeChars("Files.ReadWrite offline_access", false);
}
-
makeHttpRequest
関数が別スレッドでHTTPリクエストを実行し、結果を非同期でコールバックに返します。 -
成功すると、
device_code
、user_code
、verification_url
、およびinterval
(ポーリング間隔) が返されます。これらはparseDeviceCodeResponse()
メソッドでパースされます。
2. ユーザーへの指示
アプリは user_code と verification_url/verification_uri を UI (R2CloudAuthComponent) に表示し、ユーザーにブラウザでその URL にアクセスし、user_code を入力して認証を完了するよう指示します。この UI 更新は updateUI() メソッドで行われます。
// R2CloudAuthComponent::updateUI()
labelCodeDisplay->setText (userCode, juce::dontSendNotification);
buttonURL->setButtonText(verificationUrl);
buttonURL->setTooltip (verificationUrl);
buttonURL->setURL (verificationUrl);
buttonURL->setEnabled(true);
buttonCopyUrl->setEnabled (true);
buttonCopyCode->setEnabled (true);
3. アクセストークンのポーリング
- アプリは、返された
interval
に基づいて定期的にアクセストークンエンドポイントをポーリングします。このポーリングには、juce::Timer
が使用されます。
R2CloudAuthComponent
クラスは juce::Timer
を継承しており、startTimer(interval * 1000)
を呼び出すことで、指定された interval
(秒) ごとに timerCallback()
メソッドが実行されます。
-
timerCallback()
の中でpollForAccessToken()
メソッドが呼び出され、アクセストークンリクエストが実行されます。
Google Drive: https://oauth2.googleapis.com/token
-
client_id
,client_secret
,device_code
,grant_type=urn:ietf:params:oauth:grant-type:device_code
を POST データとして送信します。
// R2CloudAuthComponent::pollForAccessToken()
if (serviceType == ServiceType::GoogleDrive)
{
endpoint = "https://oauth2.googleapis.com/token";
postData = "client_id=" + juce::URL::addEscapeChars(googleClientId, false);
postData += "&client_secret=" + juce::URL::addEscapeChars(googleClientSecret, false);
postData += "&device_code=" + juce::URL::addEscapeChars(deviceCode, false);
postData += "&grant_type=urn:ietf:params:oauth:grant-type:device_code";
}
OneDrive: https://login.microsoftonline.com/consumers/oauth2/v2.0/token
-
client_id
,client_secret
,device_code
,grant_type=urn:ietf:params:oauth:grant-type:device_code
を POST データとして送信します。
// R2CloudAuthComponent::pollForAccessToken()
else if (serviceType == ServiceType::OneDrive)
{
endpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
postData = "client_id=" + juce::URL::addEscapeChars(oneDriveClientId, false);
postData += "&client_secret=" + juce::URL::addEscapeChars(oneDriveClientSecret, false);
postData += "&device_code=" + juce::URL::addEscapeChars(deviceCode, false);
postData += "&grant_type=urn:ietf:params:oauth:grant-type:device_code";
}
ポーリング結果: parseTokenResponse()
メソッドで JSON レスポンスをパースします。
-
authorization_pending
: ユーザーがまだ認証を完了していない場合、再度ポーリングを継続します。 -
slow_down
: ポーリングが速すぎる場合、interval
を増やしてポーリングを継続します。 -
access_token
が含まれる場合: 認証成功。access_token
とrefresh_token
を取得し、保存します。 - その他のエラー: 認証失敗。
4. トークンの永続化
取得した access_token
と refresh_token
は、各プロバイダの getTokenFile()
メソッドで指定されるファイルに JSON 形式で保存されます。これにより、次回の起動時に再認証なしで利用できます。
R2GoogleDriveProvider::saveTokens()
:
// R2GoogleDriveProvider::saveTokens()
juce::File R2GoogleDriveProvider::getTokenFile() const
{
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("R2JUCE_CloudApp")
.getChildFile("google_drive_tokens.json");
}
void R2GoogleDriveProvider::saveTokens()
{
auto tokenFile = getTokenFile();
if (!tokenFile.getParentDirectory().exists())
tokenFile.getParentDirectory().createDirectory();
juce::DynamicObject::Ptr tokenData = new juce::DynamicObject();
tokenData->setProperty("access_token", accessToken);
tokenData->setProperty("refresh_token", refreshToken);
tokenData->setProperty("token_expiry", tokenExpiry.toMilliseconds());
tokenFile.replaceWithText(juce::JSON::toString(juce::var(tokenData.get()), true));
}
R2OneDriveProvider::saveTokens()
も同様のロジックです。
トークンのリフレッシュ
アクセストークンは有効期限があるため、有効期限が切れる前に refresh_token を使用して新しいアクセストークンを取得します。
1. ファイル操作などの API リクエストを行う前に、isTokenValid()
で現在のアクセストークンが有効かを確認します。
// R2GoogleDriveProvider::isTokenValid()
bool R2GoogleDriveProvider::isTokenValid() const { return accessToken.isNotEmpty() && juce::Time::getCurrentTime() < tokenExpiry; }
2. 有効でない場合、refreshAccessToken()
が呼び出されます。
3. refresh_token
と grant_type=refresh_token
を指定して、各サービスのトークンエンドポイントにリクエストを送信します。
// R2GoogleDriveProvider::refreshAccessToken()
juce::String postData = "client_id=" + juce::URL::addEscapeChars(clientId, false)
+ "&client_secret=" + juce::URL::addEscapeChars(clientSecret, false)
+ "&refresh_token=" + juce::URL::addEscapeChars(refreshToken, false)
+ "&grant_type=refresh_token";
juce::URL url("https://oauth2.googleapis.com/token");
url = url.withPOSTData(postData);
4. 成功すると新しい access_token
が返され、tokenExpiry
が更新されます。新しい refresh_token
は初回認証時のみ返されるため、それ以外の場合は既存のものを維持します。
ファイルの読み書きロジック
ファイルの読み書きは、R2CloudManager
の saveFile
と loadFile
メソッドが抽象化されたインターフェースを提供し、内部で現在のプロバイダ(Google Drive または OneDrive)の対応するメソッドを呼び出すことで実現されます。
ファイル書き込み (Save File)
R2CloudManager::saveFile(const juce::String& filePath, const juce::MemoryBlock& data, ...)
を呼び出すと、以下の順序で処理が行われます。
1. プロバイダの取得と認証チェック
現在選択されているクラウドサービスのプロバイダを取得し、認証済みかを確認します。未認証であればコールバックでエラーを返します。
// R2CloudManager::saveFile()
auto provider = getCurrentProvider();
if (!provider) { if (callback) callback(false, "No provider"); return; }
if (needsAuthentication()) { if (callback) callback(false, "Authentication required"); return; }
provider->uploadFileByPath(filePath, data, callback);
2. パスの解析とフォルダの作成
-
filePath
がパスを含む場合(例:folder/subfolder/document.txt
)、R2GoogleDriveProvider::uploadFile
またはR2OneDriveProvider::uploadFile
が内部的にcreateFolderPath
を呼び出し、必要なフォルダが存在しない場合に再帰的に作成します。 -
createFolderPath
は、指定されたパスの各部分について、既存のフォルダを検索し、なければ新しいフォルダを作成します。 -
Google Drive:
files
API のGET
リクエストでフォルダを検索し、POST
リクエストでmimeType = 'application/vnd.google-apps.folder'
を指定してフォルダを作成します。
// R2GoogleDriveProvider::createFolderPath() (一部)
juce::String query = "'" + parentFolderId + "' in parents and name = '" + folderName + "' and mimeType = 'application/vnd.google-apps.folder' and trashed = false";
juce::String endpoint = "https://www.googleapis.com/drive/v3/files?q=" + juce::URL::addEscapeChars(query, false) + "&fields=files(id)";
// ... (GET リクエスト) ...
// POST リクエストでフォルダ作成
juce::DynamicObject::Ptr newFolderMeta = new juce::DynamicObject();
newFolderMeta->setProperty("name", folderName);
newFolderMeta->setProperty("mimeType", "application/vnd.google-apps.folder");
newFolderMeta->setProperty("parents", juce::var(juce::StringArray(parentFolderId)));
// ...
makeAPIRequest("https://www.googleapis.com/drive/v3/files", "POST", createHeaders, metadata, [=](bool createSuccess, const juce::String& createResponse) { /* ... */ });
- OneDrive:
children
エンドポイントに対するPOST
リクエストで@microsoft.graph.conflictBehavior = 'rename'
とfolder
プロパティを指定してフォルダを作成します。
// R2OneDriveProvider::createFolderInPath() (一部、createFolderPathから呼び出し)
juce::String postData = "{\"name\": \"" + folderName + "\", \"folder\": {}, \"@microsoft.graph.conflictBehavior\": \"rename\"}";
juce::String endpoint = "https://graph.microsoft.com/v1.0/me/drive/items/" + parentFolderId + "/children";
makeAPIRequest(endpoint, "POST", headers, postData, [=](bool success, const juce::String& response) { /* ... */ });
3. 既存ファイルのチェック
ターゲットフォルダ内に同じ名前のファイルが存在するかを確認します。
- Google Drive:
files
API のGET
リクエストでファイル名と親フォルダID、mimeType != 'application/vnd.google-apps.folder'
を条件に検索します。
// R2GoogleDriveProvider::uploadToFolder() (一部)
juce::String query = "'" + targetFolderId + "' in parents and name = '" + fileName + "' and mimeType != 'application/vnd.google-apps.folder' and trashed = false";
juce::String endpoint = "https://www.googleapis.com/drive/v3/files?q=" + juce::URL::addEscapeChars(query, false) + "&fields=files(id)";
makeAPIRequest(endpoint, "GET", {}, "", [=](bool success, const juce::String& response) { /* ... */ });
- OneDrive:
children
エンドポイントに対するGET
リクエストでファイル名を指定して検索します。
// R2OneDriveProvider::uploadFile() (一部)
juce::String queryUrl = "https://graph.microsoft.com/v1.0/me/drive/items/" + folderId + "/children?$filter=name eq '" + juce::URL::addEscapeChars(fileName, true) + "'";
makeAPIRequest(queryUrl, "GET", {}, "", [=](bool success, const juce::String& response) { /* ... */ });
4. ファイルのアップロード/更新
ファイルが存在する場合: 既存ファイルを更新します。
- Google Drive:
https://www.googleapis.com/upload/drive/v3/files/{fileId}?uploadType=media
に対してPATCH
リクエストでファイル内容を送信します。
// R2GoogleDriveProvider::updateExistingFile()
juce::String endpoint = "https://www.googleapis.com/upload/drive/v3/files/" + fileId + "?uploadType=media";
juce::StringPairArray headers;
headers.set("Content-Type", "application/octet-stream");
makeAPIRequest(endpoint, "PATCH", headers, data, [callback](bool success, const juce::String& response) { /* ... */ });
- OneDrive:
https://graph.microsoft.com/v1.0/me/drive/items/{itemId}/content
に対してPUT
リクエストでファイル内容を送信します。
// R2OneDriveProvider::uploadFileToExisting()
juce::String endpoint = "https://graph.microsoft.com/v1.0/me/drive/items/" + fileId + "/content";
juce::StringPairArray headers;
headers.set("Content-Type", "application/octet-stream");
makeAPIRequest(endpoint, "PUT", headers, data, callback);
ファイルが存在しない場合: 新規ファイルをアップロードします。
-
Google Drive:
https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart
に対してPOST
リクエストでマルチパート形式(メタデータとファイル内容)を送信します。
// R2GoogleDriveProvider::uploadNewFile()
juce::String endpoint = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
juce::StringPairArray headers;
headers.set("Content-Type", "multipart/related; boundary=" + boundary);
makeAPIRequest(endpoint, "POST", headers, multipartBody, [callback](bool success, const juce::String& response) { /* ... */ });
-
OneDrive:
https://graph.microsoft.com/v1.0/me/drive/items/{parentId}:/{filename}:/content
に対してPUT
リクエストでファイル内容を送信します。
// R2CloudManager::loadFile()
auto provider = getCurrentProvider();
if (!provider) { if (callback) callback(false, "", "No provider"); return; }
if (needsAuthentication()) { if (callback) callback(false, "", "Authentication required"); return; }
provider->downloadFileByPath(filePath, [callback](bool success, juce::MemoryBlock data, juce::String errorMessage) { /* ... */ });
5. コールバック: 操作の成否とエラーメッセージをコールバックで返します。
ファイル読み込み (Load File)
R2CloudManager::loadFile(const juce::String& filePath, FileContentCallback callback)
を呼び出すと、以下の順序で処理が行われます。
1. プロバイダの取得と認証チェック
現在選択されているクラウドサービスのプロバイダを取得し、認証済みかを確認します。未認証であればコールバックでエラーを返します。
// R2CloudManager::loadFile()
auto provider = getCurrentProvider();
if (!provider) { if (callback) callback(false, "", "No provider"); return; }
if (needsAuthentication()) { if (callback) callback(false, "", "Authentication required"); return; }
provider->downloadFileByPath(filePath, [callback](bool success, juce::MemoryBlock data, juce::String errorMessage) { /* ... */ });
2. パスの解析とファイル検索
-
filePath をスラッシュで分割し、各プロバイダの findFileByPath メソッドで再帰的にファイルIDを検索します。
-
各階層で listFiles API を呼び出し、現在のフォルダ内のエントリを検索します。
-
Google Drive:
files
API のGET
リクエストを使用します。
// R2GoogleDriveProvider::findFileByPath() および listFiles()
juce::String query = "'" + (folderId.isEmpty() ? "root" : folderId) + "' in parents and trashed=false";
juce::String endpoint = "https://www.googleapis.com/drive/v3/files?q=" + juce::URL::addEscapeChars(query, false) + "&fields=files(id,name,mimeType,modifiedTime,size)";
makeAPIRequest(endpoint, "GET", {}, "", [callback](bool success, const juce::String& response) { /* ... */ });
- OneDrive:
children
エンドポイントに対するGET
リクエストを使用します。
// R2OneDriveProvider::findFileByPath() および listFiles()
juce::String endpoint = "https://graph.microsoft.com/v1.0/me/drive/items/" + (folderId.isEmpty() ? "root" : folderId) + "/children";
makeAPIRequest(endpoint, "GET", {}, "", [callback](bool success, const juce::String& response) { /* ... */ });
3. ファイルのダウンロード
-
目的のファイルIDが見つかったら、
downloadFile
メソッドを呼び出します。 -
Google Drive:
https://www.googleapis.com/drive/v3/files/{fileId}?alt=media
に対してGET
リクエストを送信し、応答をjuce::MemoryBlock
として読み込みます。
// R2GoogleDriveProvider::downloadFile()
juce::String endpoint = "https://www.googleapis.com/drive/v3/files/" + fileId + "?alt=media";
// ...qq
juce::URL url(endpoint);
auto options = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress)
.withConnectionTimeoutMs(60000)
.withExtraHeaders("Authorization: Bearer " + accessToken);
if (auto stream = url.createInputStream(options))
{
juce::MemoryBlock data;
stream->readIntoMemoryBlock(data);
// ...
}
- OneDrive:
https://graph.microsoft.com/v1.0/me/drive/items/{itemId}/content
に対してGET
リクエストを送信し、応答をjuce::MemoryBlock
として読み込みます。
// R2OneDriveProvider::downloadFile()
juce::String endpoint = "https://graph.microsoft.com/v1.0/me/drive/items/" + fileId + "/content";
// ...
juce::URL url(endpoint);
auto options = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress)
.withConnectionTimeoutMs(60000)
.withExtraHeaders("Authorization: Bearer " + accessToken);
if (auto stream = url.createInputStream(options))
{
juce::MemoryBlock data;
stream->readIntoMemoryBlock(data);
// ...
}
4. コールバック
MemoryBlock から juce::String を作成し、操作の成否、コンテンツ、エラーメッセージをコールバックで返します。
HTTP リクエストの実行とエラー処理
すべての API リクエストは、プロバイダ内の makeAPIRequest
(Google Drive) または makeHttpRequest
(OneDrive) といったヘルパーメソッドによって非同期的に juce::Thread::launch
を介して実行されます。
-
HTTP クライアント: JUCE の
juce::URL::createInputStream
とjuce::WebInputStream
が使用されます。 -
ヘッダー: 認証トークン (
Authorization: Bearer <token>
) やContent-Type
など、API に必要なヘッダーが設定されます。
Google Drive:
// R2GoogleDriveProvider::makeAPIRequest() (一部)
juce::StringPairArray finalHeaders = headers;
finalHeaders.set("Authorization", "Bearer " + accessToken);
OneDrive:
// R2CloudAuthComponent::makeHttpRequest() (一部)
juce::StringPairArray headers;
headers.set("Content-Type", "application/x-www-form-urlencoded");
headers.set("Accept", "application/json");
headers.set("User-Agent", "R2JUCE CloudAuth/1.0");
POST/PATCH データ: juce::MemoryBlock
または juce::String
形式でペイロードが送信されます。
ステータスコードによる成功/失敗判定:
-
ステータスコードが 200番台の場合 (2xx) は成功と見なされます。
-
それ以外の場合、または接続エラーの場合は失敗と見なされ、エラーメッセージが返されます。
// R2CloudAuthComponent::makeHttpRequest() (一部)
int statusCode = webStream->getStatusCode();
bool success = (statusCode >= 200 && statusCode < 300);
-
JSON レスポンスのパース: ほとんどの API 応答は JSON 形式であり、
juce::JSON::parse
を使用してパースされ、結果のjuce::DynamicObject
から必要なデータが抽出されます。 -
UI スレッドへのディスパッチ: HTTP リクエストのコールバックは別スレッドで実行されるため、UI の更新(例:
showMessage
、setText
)が必要な場合は、juce::MessageManager::callAsync
を使用して JUCE のメッセージスレッドに処理をディスパッチしています。これにより、スレッドセーフティが保たれます。
タイムアウト処理
- HTTP リクエストには
withConnectionTimeoutMs(30000)
(30秒) が設定されており、接続がこの時間内に確立されない場合や応答がない場合にタイムアウトします。ファイルのダウンロード時には60000
(60秒) が設定されています。
// R2GoogleDriveProvider::makeAPIRequest() (一部)
auto options = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress)
.withConnectionTimeoutMs(30000) // 30秒
.withHttpRequestCmd(httpMethod)
.withExtraHeaders(headerString);
// R2GoogleDriveProvider::downloadFile() (一部)
auto options = juce::URL::InputStreamOptions(juce::URL::ParameterHandling::inAddress)
.withConnectionTimeoutMs(60000) // 60秒
.withExtraHeaders("Authorization: Bearer " + accessToken);
- デバイス認証フローのポーリングには
maxPolls
(120回) の上限があり、これを超えると認証タイムアウトとして処理されます。また、slow_down
エラーに応じてポーリング間隔interval
が動的に増加します。
// R2CloudAuthComponent::pollForAccessToken() (一部)
if (pollCount > maxPolls) // maxPolls = 120
{
showError(TRANS("Authentication timeout"));
return;
}
// ...
else if (error == "slow_down")
{
interval += 5;
stopTimer();
startTimer(interval * 1000);
return;
}
エラー処理の概要
認証エラー
- クライアント資格情報が設定されていない場合。
- デバイスコードのリクエスト失敗、または不正なレスポンス。
- ポーリング中の
error
レスポンス(例:invalid_grant
、access_denied
、unauthorized_client
など)。特にinvalid_grant
はリフレッシュトークンが無効になったことを示すため、サインアウト処理が行われます。 - 認証タイムアウト。
- これらのエラーは
R2CloudAuthComponent
内のshowError
メソッドで処理され、ユーザーにメッセージが表示されます。
ファイル操作エラー
- API リクエストの接続失敗、またはサーバーからのエラー応答。
- JSON レスポンスのパース失敗。
- ファイル/フォルダが見つからない場合。
- これらのエラーは、各ファイル操作コールバックの
errorMessage
引数で呼び出し元に伝えられ、最終的にMainComponent
のshowMessage
でユーザーに表示されます。
UI 更新のセーフティ
-
juce::Component::SafePointer
やjuce::MessageManager::callAsync
を積極的に使用することで、非同期処理中にコンポーネントが削除された場合のクラッシュを防ぎ、UI 更新が必ずメッセージスレッドで行われるようにしています。
このように、CloudDoc アプリケーションは、OAuth 2.0 Device Flow に基づく認証プロセスと、各クラウドサービスの RESTful API を利用したファイル操作を、JUCE のネットワーク機能と非同期処理を組み合わせて実装しています。
CloudDoc: R2JUCE Cloud Integration Sample Application
English follows Japanese
CloudDoc is a sample application that demonstrates how to interact with cloud storage (Google Drive, OneDrive) and local storage for file operations, built using the JUCE framework and the custom R2JUCE module. This application showcases R2JUCE's cloud integration features.
Key Features
- Cloud Service Selection: Switch between Google Drive, Microsoft OneDrive, and local storage for file operations.
- OAuth 2.0 Device Authorization Flow: Initial access to Google Drive and OneDrive requires user authentication via the OAuth 2.0 Device Authorization Flow. This process is handled within the application, allowing users to complete authentication by visiting a displayed URL and entering a code. This authentication method is typically used for devices like smart TVs where direct keyboard input is difficult. Therefore, when authenticating the CloudDoc application running on devices such as Raspberry Pi, it is possible to perform authentication using a smartphone or a Mac/PC.
- File Operations: Save and load text data displayed in the on-screen text editor to/from cloud or local storage.
- Drag & Drop File Upload: Drag and drop files onto a designated area within the application to read their content and upload them to the currently selected cloud service. This feature primarily supports files such as images and audio. Folder uploads are not supported, and file size is limited to 10MB.
- Settings Persistence: The last selected cloud service, file path, and file name are saved upon application exit and automatically restored on the next launch.
- Custom UI Components: Utilizes R2JUCE's custom UI components, including an alert with a progress bar and a text editor with on-screen keyboard support (for Raspberry Pi).
Getting Started
To build and run this project, you need a basic understanding of the JUCE framework and a C++ development environment set up.
Obtaining Google Drive / OneDrive Credentials
To integrate with Google Drive and OneDrive, you must register your application with their respective cloud platforms and obtain your Client ID and Client Secret. These are sensitive information and should not be committed to public repositories.
For Google Drive:
- Go to Google Cloud Console.
- Create a new project or select an existing one.
- Enable the "Google Drive API" under "APIs & Services" > "Library".
- Navigate to "APIs & Services" > "Credentials".
- Click "Create Credentials" and select "OAuth client ID".
- Choose "Desktop application" as the application type and create the client.
- Make a note of the "Client ID" and "Client Secret" displayed.
For Microsoft OneDrive:
- Go to Azure Portal and sign in.
- Navigate to "Azure Active Directory" > "App registrations".
- Click "New registration" and fill in the following details:
-
Name:
CloudDoc Desktop Client
(or any name you prefer) - Supported account types: Select "Personal Microsoft accounts only".
- Redirect URI: Leave this blank. Redirect URIs are not used for device authorization flow.
-
Name:
- After registering the app, go to "Certificates & secrets" in the left menu and create a "New client secret". Immediately copy the Value (secret) that is displayed. This value is shown only once and cannot be retrieved later. If you forget to copy it, you will need to delete the secret and create a new one.
- Go to "API permissions" in the left menu and click "Add a permission".
- Select "Microsoft Graph" and add the following delegated permissions:
Files.ReadWrite
offline_access
- After adding, click "Grant admin consent".
Setting up Credentials
The obtained Client ID and Client Secret should be set by renaming the Source/Credentials_template.h
file to Source/Credentials.h
and then replacing the following macros defined in that file:
WARNING: Credentials.h
file contains sensitive information and should NOT be committed to version control systems (like Git).
Build and Run
- Open the project file (
.jucer
) with Projucer. - Select the appropriate export target (Visual Studio, Xcode, Makefile, etc.) and generate the IDE project.
- Open the generated project in your IDE (Visual Studio, Xcode, etc.), then build and run the application.
Once the application starts, you can select a cloud service from the dropdown menu and try out the file operations.