GCSのオブジェクトをダウンロードする方法の説明です。
おしながき
概要.
種類.
ダウンロードの方法も複数用意されています。ほとんどはアップロードと対になります。
最初の2つはプロジェクト管理者向けの方法であり、アプリケーションからの利用に向いていませんので、ここでは割愛します。残りの6つが一般的にアプリケーションソフトウェアから利用可能な手段です。
| 名称 | 概要 | 主な用途 |
|---|---|---|
| コンソール操作 | GCPコンソールで操作します。 ブラウザからGUIで操作できます。 |
GUIで操作したい管理者用。 |
| gsutil | gsutilツールを使用してコマンドラインで操作します。 公式ドキュメントはこちら。 |
CUIで操作したい管理者用。 |
| クライアントライブラリ | GAE/GCEのアプリケーションでクライアントライブラリを使用して、オブジェクトを取得します。 | サーバでオブジェクトに操作を加えてダウンロードしたい場合など。 小さいコンテンツのダウンロードに向く。 |
| JSON API | RESTful APIであるJSON APIを使用して、直接GCSからダウンロードします。 | 特定のユーザによる、ブラウザなどのユーザエージェントからのダウンロードに向く。 |
| XML API | RESTful APIであるXML APIを使用して、直接GCSからダウンロードします。 | |
| XML API + 署名付きURL | XML APIに署名付きURLを組わせて使用します。 | 一時的に任意のユーザからのダウンロードをしたい場合。 |
| Blobstore API | GAEのBlobstore APIを使用して、オブジェクトを生成します。 「Blobstore API for Java 8 の概要」を参照。 |
旧APIの互換性保持のため? |
| 認証済みブラウザでのダウンロード | Googleアカウントでログインしたユーザによる、ブラウザからのダウンロード。Cookieベースで認証する。 「認証済みのブラウザでのダウンロード」を参照。 |
特定のユーザによる、ブラウザからのダウンロード用。 |
Blobstore APIについては、他の方法で代用できるため、説明しません。公式ドキュメントにも「注: blobデータの保存には、BlobstoreではなくGoogle Cloud Storageの使用をご検討ください。」と注記されています。
認証済みブラウザでのダウンロードについては、未調査です。他の方法で代用できます。また「クロスオリジン リソース シェアリング(CORS)」の「Cloud Storage CORS のサポート」の章の注意書きにあるように、CORSリクエストが許可されません。ajaxで使えない点に注意が必要です。
クライアントライブラリ.
概要.
GAE/GCEなどに用意されたライブラリを利用して、アプリケーションからGCSオブジェクトを取得します。公式ドキュメントはこちら。
ユーザエージェントがブラウザやAndroidアプリの場合だと、ユーザエージェントとGCSの間にGAE/GCEのアプリケーションが挟まるので、ユーザエージェントの持っているファイルを単にダウンロードするだけにとどまらず、このアプリケーションで内容を操作したりできます。
その一方でGAE/GCEの制限を受けます。GAEの場合だと60秒、リクエスト/レスポンスとも最大32MBの制限の影響を受けますので、サイズの大きいファイルの扱いには向きません。
またGAE/GCEアプリケーションにオブジェクトの読み出し権限が設定されるので、GAE/GCEアプリケーションの該当機能をコールさえすれば誰でもダウンロードが可能になります。ダウンロードできるユーザの制限は、GAE/GCEアプリケーション側で実装が必要になります。
ライブラリの準備.
アップロードで準備済みなら、そのままでOkです。
公式ドキュメントに説明のとおりですが、ライブラリを開発環境にインストールする必要があります。Mavenを使用している場合は、pom.xmlに下記を追加します。
<dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-storage</artifactId> <version>1.87.0</version> </dependency>
ライブラリのAPIリファレンスはこちら。
肝になるクラスはざっと以下のとおりです。
| パッケージ | クラス | 説明 |
|---|---|---|
| com.google.cloud.storage | Storage (インターフェイス) | ストレージを直接あれこれするメソッドがいます。 |
| BlobId | オブジェクトのIDを表すクラス。 | |
| BlobInfo | オブジェクトのメタデータを表すクラス。 | |
| Blob | オブジェクトそのものを表すクラス。BlobInfoのサブクラスです。 | |
| com.google.cloud | WriteChannel | オブジェクトのバイナリデータの読み出しに使用します。 |
ダウンロード.
「オブジェクトのダウンロード」に超簡単なサンプルコードがあります。ただしダウンロード先が "/local/path/to/file.txt" となっていることからわかるように、GCEのローカルディスクに保存する前提になっています。GAEだと書き込み可能なファイルシステムが用意されないので、このサンプルは動作しません。Cloud Filestoreを利用すればできるのかもしれませんが、サンプルコードのまま動作するとは考えられません。
以下は、GCSのオブジェクトをのそのまま読み出し、ブラウザに送信する、GAEのサンプルです。
- ダウンロードしたいオブジェクトのIDを示す、BlobIdクラスのインスタンスを生成します。
- オブジェクトIDを渡してStorage.get()メソッドで、オブジェクトを表すBlobクラスのインスタンスを取得します。
- Blobクラスのインスタンスからコンテンツタイプとコンテンツエンコーディングを取得し、レスポンスに設定します。
- com.google.cloud.ReadChannelクラスを使用して、オブジェクトそのもののデータを読み出し、レスポンスのボディに出力します。
class DownloadSample {
// Storageクラスのインスタンスを取得
private Storage storageInstance = StorageOptions.getDefaultInstance().getService();
public void downloadObject(HttpServletRequest request, HttpServletResponse response) throws StorageException {
//読み出すオブジェクトのIDを用意
String bucket = request.getParameter("bucket");
String object = request.getParameger("object");
BlobId blobid = BlobId.of(bucket, object);
// GCSからオブジェクトを取得
Blob blob = storageInstance.get(blobid);
if (blob != null) {
// レスポンスにコンテンツタイプとコンテンツエンコーディングを設定
String type = blob.getContentType();
String encoding = blob.getContentEncoding();
response.setContentType(type);
response.setCharacterEncoding(encoding);
// レスポンスのボディに内容を出力
forwardObject(response, blob);
}
}
protected void forwardObject(HttpServletResponse response, Blob blob) throws StorageException {
// リードチャネルを取得して
ReadChannel readchnl = blob.reader();
if (readchnl != null) {
// リードチャネルから1MBずつ読み出し
ByteBuffer bytebuff = ByteBuffer.allocate(1024*1024); // 1MB
try {
int readsize;
while ((readsize = readchnl.read(bytebuff)) > 0) {
// レスポンスに書き出す
response.getOutputStream().write(bytebuff.array(), 0, readsize);
bytebuff.clear();
}
}
catch (IOException e) {
throw new StorageException(e);
}
finally {
readchnl.close();
}
}
}
}
Blob.downloadTo()メソッドを使ったほうがシンプルになりそうです...
注意点.
- GAE経由でダウンロードする場合、GAEの60秒/32MB制限を受けます。当然課金対象にもなります。そのためサイズの小さいコンテンツに向いている方法です。
- コンテンツタイプ/コンテンツエンコーディングは自分でレスポンスに設定しておかないと、ブラウザが受け取っても正しく処理できません。
JSON API.
概要.
GAE/GCEなどを経由することなく、直接GCSのバケットからダウンロードする方法の1つです。
公式ドキュメントでは「Google Cloud Storage JSON API の概要」で概要が説明されています。ここではユーザエージェントがブラウザである場合について説明します。
リクエストエンドポイント.
RESTful APIなので、URIのリクエストエンドポイントを持ちます。公式ドキュメントの説明どおりですが、下記のフォーマットです。
https://www.googleapis.com/storage/v1/操作対象のパス
- JSON APIはHTTPSリクエストのみの対応です。HTTPは使用できないとのことです。
- アップロードおよびバッチリクエストを除き、上記はダウンロード以外の機能でも使用できます。
ダウンロードついては、上記の「操作対象のパス」で、バケット名とオブジェクト名を指定します。
https://www.googleapis.com/storage/v1/b/バケット名/o/オブジェクト名
実際にリクエストする際は、この後にクエリーで追加のパラメータを付加します。またリクエストヘッダを使って渡すケースもあります。リクエストヘッダとクエリーの詳細は、「HTTP headers and common query string parameters for JSON」にリストされています。
CORS設定.
コンテンツのダウンロードでは、CORSポリシーが問題になることはあまりないと思います。
「クロスオリジン リソース シェアリング(CORS)」ページの「Cloud Storage CORS のサポート」の章の注意書きに説明されているとおりですが、JSON APIでは常にCORSリクエストが許可されます。バケットにCORSの設定をしていても無視されます。
そのため特にJSON APIでは、CORSについて意識する必要はありません。
ただしajaxでメタデータの取得/設定などのアクセスしていて、CORSではなくアクセスの仕方そのものに問題がある場合でも、リクエストエンドポイントはレスポンスにAccess-Control-Allow-Originヘッダを含めてくれません。そのため表向きCORS違反しているように見えてしまうことがあります。レスポンスのステータスコードが400番台なら、CORS以前に、アクセスの仕方に問題があると思われます。
バケットの権限設定.
バケットはデフォルトで非公開であり、プロジェクト管理者しかアクセスできません。管理者でないユーザがアップロードするには、そのユーザにバケットへのアクセス権を与える必要があります。
その手順はXML APIやアップロード/ダウンロードで共通ですので、別ページにまとめました。
アクセストークンの取得.
JSON APIを利用するには、基本的にアクセストークンが必要です。例外は公開オブジェクトに対するアクセスのみで、この場合はアクセストークンも必要ありません。
アクセストークンの取得は、その準備も含めて手順を踏む必要があります。またXML APIとも共通ですし、アップロード/ダウンロードに限らず、オブジェクトのリストや設定系などの他の機能でも必要になります。
準備も含めて手順が長いので、こちらのページにアクセストークンの取得方法をまとめてあります。
ダウンロード.
公式ドキュメントの「オブジェクトのダウンロード」にはcurlを使った例しかないので、ブラウザからアップロードするアプリケーションの参考としては不十分です。JSON APIの概要は「API reference」に、ダウンロードの詳細は「Objects: get」のページに書かれています。
やっとリクエストエンドポイントにアクセスします。次のコードは、最もシンプルなオブジェクト本体のみを、ブラウザからダウンロードするサンプルです。
- <input type="text">でバケット名とオブジェクト名を入力してもらって、
- "download"ボタン押下でリクエストエンドポイントへのURIを作っています。このURIにダウンロード先バケット名、オブジェクト名を含んでいます。
- リクエストURIのクエリーで、オブジェクトのダウンロードを示す"alt=media"を指定しています。
- アクセストークンは前記「アクセストークンの取得」 で取得したものです。ここではクエリーのaccess_tokenの値で渡しています。
- 別ウィンドウでダウンロードしています。 そのウィンドウのURIに、作成したリクエストURIを指定しています。
<html>
<body>
<p>
<input id="bucket_name" type="text"/><br/> // バケット名
<input id="object_name" type="text"/><br/> // オブジェクト名
<input type="button" value="download" onclick="downloadObjectByJsonApi();"/> // ボタン押下でダウンロード開始
</p>
<script type="text/javascript">
function downloadByJsonApi() {
// ダウンロードするオブジェクトの情報を得る
var bucket = $('#bucket_name').val();
var object = $('#object_name').val();
// リクエストURI
var path = 'https://www.googleapis.com/storage/v1/b/' +bucket +'/o/' +object +'?alt=media';
// アクセストークンはクエリーで渡す
var access_token = '取得したアクセストークン';
path += '&access_token=' +accessToken;
// 別ウィンドウでダウンロード
window.open(path, '_blank');
}
</script>
</body>
</html>
このサンプルではアクセストークンはwindow.openを使用するため、クエリーでaccess_tokenパラメータとして渡しましたが、Authorizationリクエストヘッダとして渡すこともできます。その他のリクエストヘッダとクエリーの仕様については「HTTP headers and common query string parameters for JSON」に説明があります。
XML API.
概要.
XML APIは、JSON APIと同等の機能を持っていて、GAE/GCEなどを経由せずに、直接GCSのバケットからダウンロードできます。
公式ドキュメントでは「XML API overview」で概要が説明されています。ここではユーザエージェントがブラウザである場合について説明します。
リクエストエンドポイント
RESTful APIなので、URIのリクエストエンドポイントを持ちます。公式ドキュメントの説明どおりですが、JSON APIと違って1つではありません。
- すべての機能を利用可能 (もちろんダウンロードも可能)
https://storage.googleapis.com/バケット名/オブジェクト名 https://バケット名.storage.googleapis.com/オブジェクト名
- ダウンロード専用 (パフォーマンス向上の可能性あり)
https://storage-download.googleapis.com/バケット名/オブジェクト名 https://バケット名.storage-download.googleapis.com/オブジェクト名
それぞれがまた2種類あって、1つはパス部分にバケット名を含むもの(上側)、もう1つはホスト名にバケット名を含むもの(下側)です。下側のホスト名にバケット名を含むパターンは、バケットごとに異なるCORSポリシーを設定したい場合に使用します。
実際にリクエストする際は、さらにクエリーやリクエストヘッダでパラメータを渡します。その詳細は「HTTP headers and query string parameters for XML API」にリストされています。
CORS設定.
ajaxを使用すると問題になるCORSについての設定です。JSON APIと違い、XML APIではCORS設定が有効になります。
署名付きURLを使用するケースと同じなので、別ページにまとめました。
ダウンロードの場合はCORSポリシーが影響する使い方はあまりないかもしれませんが、メタデータの取得や設定系の機能では影響を受けます。
バケットの権限設定.
バケットはデフォルトで非公開であり、プロジェクト管理者しかアクセスできません。管理者でないユーザがアップロードするには、そのユーザにバケットへのアクセス権を与える必要があります。
その手順はJSON APIやアップロード/ダウンロードで共通ですので、別ページにまとめました。
アクセストークンの取得.
XML APIを利用する際にも、基本的にアクセストークンが必要です。例外は、JSON API同様に公開オブジェクトに対するアクセスと、次章で説明する署名付きURLを使用する場合です。これら場合はアクセストークンは必要ありません。
アクセストークンの取得については、準備も手順も、アップロードおよびJSON APIと同じです。メタデータの取得や設定系の機能などの他の機能でも必要になる点も、JSON
APIと同じです。
こちらのページに、アクセストークンの取得方法をまとめてあります。
ダウンロード.
公式ドキュメントの「オブジェクトのダウンロード」には、JSON APIと同じく、curlを使った例しかありません。
XML APIの概要の説明ページ「Overview of request methods for the XML API」にあるとおり、GETメソッドでオブジェクトのダウンロードが可能です。ダウンロードの詳細は、「GET Object」および「Download an object」のページに書かれています。
次のコードはシンプルに、ブラウザからURI指定のページ遷移でダウンロードするサンプルです。
- <input type="text">でバケット名とオブジェクト名を入力してもらって、
- "download"ボタン押下でリクエストエンドポイントへのURIを作っています。このURIにダウンロード先バケット名、オブジェクト名を含んでいます。
- クエリーは付けていないので、オブジェクト本体の取得になります。
- アクセストークンは前記「アクセストークンの取得」 で取得したものです。Authorizationリクエストヘッダを用いて渡します。
- 取得したオブジェクトの中身をPRE要素で表示します。オブジェクトはプレーンテキスト限定です。
<html>
<body>
<p>
<input id="bucket_name" type="text"/><br/> // バケット名
<input id="boject_name" type="text"/><br/> // オブジェクト名
<input type="button" value="download" onclick="dwonloadObjectByXmlApi();"/> // ボタン押下でダウンロード開始
<div id="result"></div>
</p>
<script type="text/javascript">
function downloadObjectByXmlApi() {
// ダウンロードするオブジェクトの情報を得る
var bucket = $('#bucket_name').val();
var object = $('#object_name').val();
// リクエストURIとアクセストークン
var path = 'https://storage.googleapis.com/' +bucket +'/' +object;
var access_token = '取得したアクセストークン';
// ajaxで受信
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if ((this.readyState == 4) && (this.status == 200)) {
var elem = '<pre>' +this.response +'</pre>';
// 表示
$('#result').html(elem);
}
};
request.open('GET', path);
request.setRequestHeader('Authorization', 'Bearer ' +accessToken); // アクセストークンを渡す
request.responseType = 'text';
request.send();
}
</script>
</body>
</html>
先に説明したJSON APIを使ったサンプルと、前半はそっくりです。違いは、リクエストエンドポイントと、アクセストークンをAuthorizationヘッダで渡すためにXMLHttpRequestを使っていることです。
JSON APIを使ったサンプルでは、アクセストークンは、クエリーaccess_tokenの値として渡していました。XML APIでは、公式ドキュメントにはアクセストークンを渡せるクエリーは載っていません。試してみるとクエリーaccess_tokenがJSON
APIと同様に使えてしまいました。しかし公式にアナウンスされていないものを使うのもキモチワルイので、ここではAuthorizationリクエストヘッダを用いています。そのため無理矢理XMLHttpRequestを使っています。
また結果の表示にPRE要素を使っているので、オブジェクトのコンテンツタイプは"text/plain"限定でしか正しく表示できません。これはHTML/JavaScriptの問題です。
XML API + 署名付きURL.
概要.
アップロードと同じく、署名付きURLはアクセストークンの代わりに使用します。バケット/オブジェクトへのアクセス権を持たないユーザでも、またユーザがGoogleアカウントでログインしていなくても、アクセスすることができます。署名付きURLには有効期限があり、一時的にアクセス権を与えたい場合に使用します。
概要は公式ドキュメントの「署名付き URL」で説明されています。
署名付きURLの生成については、「独自のプログラムを使用した V4 署名プロセス」に説明がありますが、これよりも「Cloud Storage のツールを使用した V4 署名プロセス」で説明されているクライアントライブラリを使用するのが簡単です。
以下では、サービスアカウントで署名する方法を説明します。この方法はAPIリファレンスのサンプルコードで説明されています。
手順.
オブジェクトは、ブラウザからアクセスして、直接GCSのバケットからダウンロードするものとします。その手順は、以下のようになります。
- CORS設定を行います。XML APIおよびダウンロードで説明したのと同じです。 CORSポリシーに該当しないなら、必要ありません。
- 準備として、サービスアカウントキーを取得します。
- サービスアカウントキーを使用して、署名付きURLを生成します。GAE/GCEのフロントエンドに生成のAPIを実装すると良いでしょう。
- 生成した署名付きURLを使ってブラウザからダウンロードします。
バケット/オブジェクトのへの権限設定も必要ありません。設定してあってもそれらと無関係に一時的な権限を与えるのが、署名付きURLです。
アクセストークンも必要ありません。署名付きURLはアクセストークンと同様に認証情報を持っています。
CORS設定.
署名付きURLを使用する方法もXML APIの範疇なので、CORS設定が反映されます。設定の方法は同じなので、CORS設定のページを参照してください。既にXML APIの章に説明した方法で設定済みなら、そのままで良いはずです。
ダウンロードだとajaxを使用しなくてもできるので、CORSポリシーに該当しないケースもあると思います。その場合は、特にCORS設定は必要ありません。
サービスアカウントキーの取得.
サービスアカウントキーの取得手順は、アップロード/ダウンロードで共通ですので、別ページにまとめてあります。
署名付きURLの生成.
署名付きURLには有効期限がありますので、ダウンロードごとに署名付きURLを生成する必要があります。GAE/GCEでクライアントライブラリを使用したサンプルを示します。
- オブジェクトのメタデータを、バケット名/オブジェクト名から生成します。
- Storage.signUrl()メソッドで、署名付きURLを生成します。このメソッドはデフォルトでGETメソッドのURLを生成する仕様なので、ここではメソッドを指定していません。
- 生成した署名付きURLは、レスポンスにプレーンテキストとして書き出しています。
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
class DownloadSample {
// サービスアカウントキーを格納したファイルのパス
private final static String KEYPATH = "WEB-INF/ファイルのパス";
// Storageクラスのインスタンスを取得
private Storage storageInstance = StorageOptions.getDefaultInstance().getService();
public URL createDownloadUri(HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException, IOException {
// 生成するオブジェクトのメタデータを用意
String bucket = request.getParameter("bucket");
String object = request.getParameter("object");
BlobInfo.Builder blobinfobuilder = BlobInfo.newBuilder(bucket, object);
// 署名付きURLを生成
URL url = storageInstance.signUrl(blobinfobuilder.build(), 1L, TimeUnit.HOURS, // 有効期間は1時間
Storage.SignUrlOption.signWith(ServiceAccountCredentials.fromStream(new FileInputStream(keypath)))); // サービスアカウントで署名
// 生成した署名付きURLをレスポンスで返す
response.getWriter().write(url.toString());
}
}
アップロードのサンプルと比べると、以下の点で簡単になっています。
- デフォルトでGETメソッドが使われるので、あえてStorage.SignUrlOption.withSign()メソッドで指定していません。
- ダウンロードする際にコンテンツタイプを指定しないことを前提に、署名にコンテンツタイプを設定していません。
注意すべき点は、下記のとおりです。
- サービスアカウントで署名するためには、Storage.SignUrlOption.withSign()メソッドを使用します。引数のsignerには、「サービスアカウントキーの取得」で取得したキーからServiceAccountCredentials.fromStream()メソッドで生成した署名を指定します。これはStorage.signUrl()メソッドの例のとおりです。
- 公式ドキュメントの「GET Object」の記載のとおりダウンロードの際はGETメソッドを使用しますが、Storage.SignUrlOption.httpMethod()メソッドの説明どおり、指定しなければデフォルトのGETメソッドが使用されます。
- ダウンロード時にコンテンツタイプを指定するなら、署名にコンテンツタイプを含める必要があります。その方法はアップロードと同じで、コンテンツタイプの値はメタデータにBlobInfo.Builder.setContentType()メソッドで設定しますが、それを署名に含めるにはStorage.SignUrlOption.withContentType()メソッドのコールが必要です。またここで設定したコンテンツタイプは、ダウンロードのリクエストのContent-Typeヘッダの値と一致している必要があります。コンテンツタイプが必要な理由は、おそらく「CORS設定」と同じで、Content-Typeヘッダによりプリフライトが行われるためです。
ダウンロード.
ブラウザで、「署名付きURLの生成」で生成したURLから、ダウンロードします。
以下に、ダウンロードのサンプルを示します。
- <input type="text">でバケット名とオブジェクト名を入力してもらって、
- "download"ボタン押下でリクエストエンドポイントへのURIを作っています。このURIにダウンロード先バケット名、オブジェクト名を含んでいます。
- クエリーは付けていないので、オブジェクト本体の取得になります。
- 生成された署名付きURLをそのまま使用して、オブジェクトをダウンロードします。
- Content-Length/Content-Typeヘッダはブラウザが勝手につけてしまうので、このサンプルではあえてつけていません。
- リクエストのボディにはFileオブジェクトを渡しているので、コンテンツタイプはブラウザが認識しているファイルのコンテンツタイプになります。
<html>
<body>
<p>
<input id="bucket_name" type="text"/><br/> // バケット名
<input id="boject_name" type="text"/><br/> // オブジェクト名
<input type="button" value="download" onclick="getSignedUrl();"/> // ボタン押下でダウンロード開始
<div id="result"></div>
</p>
<script type="text/javascript">
function getSignedUrl() {
// ダウンロードしたいファイルの情報を得る
var bucket = $('#bucket_name').val();
var object = $('#object_name').val();
// 署名付きURLを取得
$.ajax({
type: 'GET', // GAE/GCEに実装したAPIに合わせる
url: GAE/GCEのAPIへのパス, // バケット名とオブジェクト名もここで渡す
})
.done(function(signedurl) {
// URLが得られたらオブジェクトをダウンロード
window.open(signedurl, "_blank");
});
}
</script>
</body>
</html>
注意すべき点をまとめておきます。
- 署名付きURLの取得でGETメソッドを使用していますが、GAE/GCEフロントエンドに実装したAPIがGETメソッドで実装されている前提です。
- バケット名/オブジェクト名は生成された署名付きURLに含まれていますので、その生成時に指定するのみです。
- オブジェクトダウンロード時のリクエストメソッドは、署名付きURL生成時にStorage.SignUrlOption.httpMethod()メソッドで指定したメソッドと一致している必要があります。XML APIでは「Overview of request methods for the XML API」のとおり、オブジェクトのダウンロードはGETメソッドを使用します。
- 署名付きURLの生成時にコンテンツタイプを指定していないので、オブジェクトダウンロード時のリクエストにもコンテンツタイプを指定しません。
Copyright 2005-2024, yosshie.
