cloud-storage-web について
Web SDKを介してブラウザーからCloudBaseストレージを操作するスキルです。アップロード、ダウンロード、URL生成、削除を扱い、実装前のバケットとアクセス制御の確認を重視します。
主な機能
- 01ブラウザーからのファイルアップロード
- 02一時URL・署名付きURLの処理
- 03ファイルの削除とダウンロード
- 04バケットとストレージモードの確認
- 05セキュリティドメインとRLSの指針
活用シーン
- 01Webアプリからのファイルアップロード
- 02保存オブジェクトのダウンロードリンク生成
- 03バケット未作成の問題の調査
- 04アップロード前のストレージ権限設定
MCP 対応
`@cloudbase/js-sdk`によるブラウザーストレージ操作を扱います。MCPツールによるストレージの直接管理とは区別し、その作業はCloudBaseプラットフォームスキルへ案内します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tencentcloudbase/ cloudbase-ai-toolkit --skill cloud-storage-webインストール後、Claude Code で/cloud-storage-webを入力して読み込まれたか確認します。
詳細
関連スキル(ローカルのみ)
関連する CloudBase スキルは、このスキルと同じ場所に配置されています。../auth-tool-cloudbase/SKILL.md のような相対パスを使用してください。
参照先の関連スキルファイルがこの環境に存在しない場合は、CloudBase プラグイン一式(または不足しているスキル)をインストールするようユーザーに依頼してください。リモートのスキルやプロトコルの Markdown を HTTP 経由で取得し、エージェントのコンテキストに読み込むことはしないでください。
Cloud Storage Web SDK
利用条件
まずこのスキルを使う場合
- ブラウザーまたは Web アプリから
@cloudbase/js-sdkを介して CloudBase ストレージのオブジェクトをアップロード、ダウンロード、または管理する必要がある場合。 - フロントエンドコードで
uploadFile、getTempFileURL、deleteFile、またはdownloadFileがリクエストに含まれている場合。
コードを書く前に読む場合
- ブラウザー側でストレージを扱うタスクで、Mini Program のストレージ、バックエンドでのストレージ管理、または静的ホスティングのデプロイと区別する必要がある場合。
- セキュリティドメインまたはフロントエンド認証が原因で、リクエストがブロックされる可能性がある場合。
続けて読むもの
- Web ログインと ID 管理 ->
../auth-web-cloudbase/SKILL.md - Web アプリの一般的なセットアップ ->
../web-development/SKILL.md - MCP ツールを介したストレージの直接管理 ->
../cloudbase-platform/SKILL.md
使用しない場合
- Mini Program のファイル API。
- MCP を介したバックエンドまたはエージェント側からのストレージの直接管理。
manageHosting(action="upload")を介した静的ウェブサイトのホスティングのデプロイ。- データベース操作。
よくある間違い/注意点
- セキュリティドメインを設定せずに、ブラウザーコードからアップロードする。
- ストレージオブジェクトの代わりに静的ホスティングでこのスキルを使用する。
- ブラウザー SDK のアップロードフローと、サーバー側のファイル管理タスクを混同する。
- 一時ダウンロード URL が永続的なリンクだと思い込む。
STORAGE_NOT_EXISTを無視する。これは対象のストレージバケット/リソースがまだ準備できていないことを意味します。ブラウザーのアップロードコードで URL を捏造すべきという意味ではありません。- ローカルの Vite または開発サーバーを使うタスクで、
app.uploadFile()をテストする前に、現在のブラウザーの正確なhost:portをホワイトリストに登録し忘れる。 - CloudBase PG/
pgstoreを従来の NoSQL CloudBase ストレージと同じものとして扱う。PG 環境では別のpgstoreバックエンドが使用され、以前の NoSQL バケットからバケットが自動作成されることはありません。pgstoreにバケットがない場合、すべてのアップロードでSTORAGE_BUCKET_NOT_FOUNDが返され、その後 SDK はPUT https://undefined/を発行します(DevTools ではnet::ERR_NAME_NOT_RESOLVEDと表示されます)。Supabase と同様、バケットの存在は必須条件です。Supabase Storage では、すべてのアップロードが作成済みのバケットを対象にする必要があります。CloudBase PG も同じモデルに従います。
最低限のチェックリスト
- 呼び出し元がブラウザー/Web アプリであることを確認します。
- Web SDK は一度だけ初期化します。
- アップロードをテストする前に、現在の環境に CloudBase ストレージが存在することを確認します。環境にデフォルトのバケットがない場合は、利用可能な MCP 管理/クエリツールを使ってストレージバケットを確認、作成、または選択してください。PG/pgstore 環境では、
DescribeEnvsに表示される従来の NoSQL バケットは、使用可能な pgstore バケットとは見なされません。ブラウザーからアップロードする前に、明示的にバケットを作成してください。従来の NoSQL バケット自体は、それを対象とする従来のapp.uploadFile()フローで引き続き使用できます。PG と NoSQL のストレージは共存し、このスキルは両方に適用されます。 - セキュリティドメイン/CORS の要件を確認します。
- コーディング前に、適切なストレージメソッドを選びます。
ローカル開発の手順
アプリをローカルのブラウザーオリジンで実行し、フロントエンドからファイルをアップロードする必要がある場合:
queryEnvをaction="domains"で使用し、現在のセキュリティドメインの許可リストを確認します。- ブラウザーオリジンを CloudBase の許可リストのエントリー形式に変換します。
- ブラウザーオリジン
http://127.0.0.1:4173-> 許可リストのエントリー127.0.0.1:4173 - ブラウザーオリジン
http://localhost:5173-> 許可リストのエントリーlocalhost:5173
- ブラウザーオリジン
- 現在使用しているホストのエントリーがない場合は、
envDomainManagementをaction="create"で呼び出し、app.uploadFile()に依存する前にそのホストのエントリーを追加します。 - 実行ごとにランタイムのポートが変わる可能性がある場合、固定のデフォルトポート一覧で十分だと思い込まないでください。テストまたは最終検証で実際に使用しているブラウザーオリジンを再確認し、その正確な
host:portを追加します。 - セキュリティドメインの変更が反映されるまで数分かかる場合があることをユーザーに伝えます。長時間の固定待機を行うのではなく、
queryEnv(action="domains")をポーリングします。 - その後にのみ、ブラウザー側の
app.uploadFile()のフローを実装してテストします。
app.uploadFile() が STORAGE_NOT_EXIST を返した場合は、フロントエンドコードの編集を中断し、まず環境側のストレージリソースを修正します。環境のストレージ一覧を再確認し、タスクで許可されている場合は利用可能なバケットを作成または選択してから、同じ SDK のアップロードフローを再試行します。
タスクでブラウザー側のファイルアップロードを使用する場合、これは任意の後処理ではなく必須の前提条件として扱います。
バケットの存在が必須(アップロードコードを書く前に必ず確認)
Supabase Storage と同様に、CloudBase Storage では、クライアント側からアップロードする前に対象のバケットが存在している必要があります。これは、従来の CloudBase NoSQL ストレージ(STORAGE_NOT_EXIST)と、新しい PG/pgstore バックエンド(STORAGE_BUCKET_NOT_FOUND)の両方に当てはまります。
Supabase と同じ考え方です:
| 手順 | Supabase | CloudBase |
|---|---|---|
| バケットを作成 | supabase.storage.createBucket('covers', { public: true })(サービスロールを使用して管理者側で実行) | PG モードでは、必要に応じて PG ストレージ HTTP API/CLI/コンソール/storage.buckets に対する SQL を使用して、storage.buckets のバケットを作成します。ブラウザー SDK では作成できません。 |
| アップロード | supabase.storage.from('covers').upload('a.png', file) | PG モード:app.storage.from('covers').upload('a.png', file) — from(bucketName) で pgstore バケットを指定します。非 PG モード: app.storage.from().upload('covers/a.png', file) — バケット名をパスの先頭の要素として指定します。 |
| バケットがない場合のエラー | Bucket not found | ブラウザーでは STORAGE_BUCKET_NOT_FOUND(PG)または STORAGE_NOT_EXIST(NoSQL)が表示され、その後 PUT https://undefined/ が発生します。SDK が metadata.url の欠落した値を使って PUT を試行するためです。 |
ブラウザーからのアップロードが必要なタスクでは、アップロード前に次の手順を必ず実行してください。
- まず既存のバケットを一覧表示します。PG/pgstore では、従来の NoSQL バケット(
DescribeEnvs.Storages[]が返す6d63-…-1409864723という形式のもの)は有効な pgstore バケットではありません。使えると決めつけないでください。 - アップロード先(例:
covers)に使えるバケットがない場合は、フロントエンドのアップロードコードを編集する前に、PG ストレージ管理機能を使ってバケットを作成します。コードにcoversをパスのプレフィックスとして追加しても、バケットは自動作成されません。 - バケットを作成した後のアップロード方法は、環境によって異なります。
- PG/pgstore:
app.storage.from('covers').upload('<file>', file)— バケット名をfrom()に渡します。 - 非 PG(NoSQL):
app.storage.from().upload('covers/<file>', file)— バケット名をパスの先頭の要素として指定します。
- PG/pgstore:
- DevTools で
https://undefined/に対するnet::ERR_NAME_NOT_RESOLVEDが表示される場合、これはmetadata.urlフィールドがないことに SDK が反応している状態です。ほとんどの場合、バケットが存在しないか、SDK のリクエストが上流で拒否されています。まず DevTools で失敗したPOST .../v1/storages/get-objects-upload-infoのレスポンスを確認してください。codeフィールド(例:STORAGE_BUCKET_NOT_FOUND、STORAGE_CONTENT_LENGTH_REQUIRED、INVALID_PARAM)から、修正すべき内容が分かります。
アップロードの失敗を黙って無視しないでください。uploadCoverImage() が reject された場合、親処理の createArticle() も MUST reject されなければなりません。作り物の URL やプレースホルダーを使って db.from(...).insert(...) に進んではいけません。また、UI に成功のトーストを表示してはいけません。
⚠️ PG モードでのアップロード:app.uploadFile() ではなく app.storage.from('bucket') を使用してください
PG/pgstore 環境では、アップロードには app.storage.from('covers').upload(key, file) を、アクセス URL の取得には app.storage.from('covers').createSignedUrl(path, expiresIn) を使用してください。
PG モードでは、旧式の NoSQL API を使用しないでください。
- ❌
app.uploadFile()— これは旧式 NoSQL のアップロード API です - ❌
app.getTempFileURL()— これは旧式 NoSQL の URL 取得方法です - ❌
app.storage.from().upload('covers/file', file)— バケット名が指定されていません
代わりに、次を使用してください。
- ✅
app.storage.from('covers').upload('file', file)— PG モードでのアップロード - ✅
app.storage.from('covers').createSignedUrl('file', 3600)— 署名付き URL を取得します(fullSignedURLフィールドを返します)
モードによって戻り値の形が異なります(v3 SDK)— 正しい列を参照してください。
| 呼び出し | 従来モード(from() 引数なし、cloud:// fileID) | PG モード(from('bucket')、バケット内のオブジェクト名) |
|---|---|---|
upload(path, file) | { data: { id, path, fullPath } };upsert のデフォルトは true | { data: { id, ... } };upsert のデフォルトは false |
createSignedUrl(path, expiresIn) | await → { data: { signedUrl } } | await → { data: { fullSignedURL } } |
getPublicUrl(path) | await → { data: { publicUrl } } | 同期呼び出し(await しません) → { data: { publicUrl } } |
出典:webv3/storage.md · webv3-pg/storage.md(raw markdown)。
PG モードの URL 解決:公開バケットの直リンクと署名付き URL
| バケットの種類 | URL 戦略 | コード |
|---|---|---|
公開バケット(storage.buckets.public = true) | 直リンク。ログイン状態は不要で、<img src> に直接指定できます | app.storage.from('covers').getPublicUrl('a.png') → { data: { publicUrl } } |
| プライベートバケット | 有効期限付きの署名 URL | app.storage.from('covers').createSignedUrl('a.png', 3600) |
- 公開バケットの直リンクでアクセスできるかどうかは、
storage.objectsの RLS SELECT ポリシーで anon が許可されているかによって決まります。バケット作成用 SQL とポリシーのテンプレートは、postgresql-development-cloudbase/references/storage-pg.mdの「Public-read bucket template」を参照してください。 - 表示レイヤーでは
onerrorによるフォールバックを実装してください(直リンクがポリシーで拒否された場合は、署名 URL に切り替えます)。単一の URL 取得手順に依存しないでください。 - 業務テーブルにはバケットとキー(または SDK が解析した最終 URL)のみを保存し、ブラウザーで URL を手動で組み立てないでください。
バケット作成後:ストレージ RLS(PG/pgstore 環境では必須)
PG/pgstore 環境では、ストレージのアクセス制御は storage.buckets/storage.objects に対する PostgreSQL Row Level Security(RLS) を通じて適用されます。Supabase Storage と同様です。これらのテーブルにはすでに anon、authenticated、service_role への権限が付与されており、RLS が権限のゲートとなります。従来のストレージ権限ラベル(READONLY/PRIVATE/CUSTOM)と JSON ストレージのセーフルールは適用されません。デフォルトの RLS ポリシーではすべて拒否されるため、バケットが存在していても、ポリシーを設定しない限り、ブラウザーからの app.storage.from('covers').upload() は STORAGE_PERMISSION_DENIED エラーで失敗します。
バケットを作成した後、managePgDatabase(action="execute", confirm=true) を使用して次の SQL を実行してください。
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
-- Allow authenticated users to upload files
CREATE POLICY "authenticated_upload" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (true);
-- Allow authenticated users to read/download files
CREATE POLICY "authenticated_read" ON storage.objects
FOR SELECT TO authenticated
USING (true);
-- Optional: allow users to update/delete their own files
CREATE POLICY "users_manage_own" ON storage.objects
FOR UPDATE TO authenticated
USING (auth.uid() = owner_id)
WITH CHECK (auth.uid() = owner_id);
重要な点:
storage.objectsの RLS は、CloudBase の従来の NoSQL ストレージセキュリティルール(managePermissions/ModifyStorageSafeRule)とは別のものです。PG モードでは、従来のセキュリティルール API ではなく、必ず PG SQL でストレージ RLS を設定してください。- これらのポリシーがない場合、PG モードでブラウザーから
app.storage.from('covers').upload()を呼び出すと、STORAGE_PERMISSION_DENIEDが返されます。 - 再適用時に「policy already exists」エラーが発生しないよう、
DO $$ブロック内でIF NOT EXISTSを使用してください。
概要
CloudBase Web SDK を通じたブラウザー側のクラウドストレージ操作には、このスキルを使用してください。
よくあるタスク:
- ブラウザーからファイルをアップロードする
- 一時的なダウンロード URL を生成する
- ファイルを削除する
- ブラウザーでのダウンロードを開始する
SDK の初期化
初期化のリファレンス:webv3/initialization.md
import cloudbase from "@cloudbase/js-sdk";
const app = cloudbase.init({
env: "your-env-id",
accessKey: import.meta.env.VITE_PUBLISHABLE_KEY, // publishable key — auto-provisioned, see below
});
初期化のルール:
- Publishable key の自動プロビジョニング(ユーザーに尋ねないでください): ブラウザー側からのストレージリクエストはデータプレーンの呼び出しであり、Publishable
accessKeyが必要です。クライアントコードを書く前に、queryAppAuth(action="getPublishableKey")を呼び出してください。値が空の場合は、manageAppAuth(action="ensurePublishableKey")を呼び出し、その後.env.localにVITE_PUBLISHABLE_KEYとしてキーを書き込み(ファイルがない場合は作成)、import.meta.env.VITE_PUBLISHABLE_KEYから読み取ります。ソースファイルにキーをハードコードしないでください。両方の MCP 呼び出しが失敗した場合に限り、コンソール(https://tcb.cloud.tencent.com/dev?envId={env}#/env/apikey)を使用してください。 - 共有アプリインスタンスを使って、同期的に初期化してください。
- すべてのコンポーネントで再初期化しないでください。
- 操作がユーザー ID に依存する場合は、ストレージ操作の前に認証を処理してください。
メソッドの使い分け
- ブラウザーからアップロードする場合 →
app.uploadFile() - 一時的なプレビュー/ダウンロード URL の場合 →
app.getTempFileURL() - 既存ファイルを削除する場合 →
app.deleteFile() - ブラウザーからダウンロードを開始する場合 →
app.downloadFile()
アップロード
const result = await app.uploadFile({
cloudPath: "uploads/avatar.jpg",
filePath: selectedFile
});
アップロードのルール
cloudPathにはファイル名を含める必要があります。/を使ってフォルダー構造を作成してください。- CloudBase PG/pgstore 環境では、
from(bucketName)引数はバケット名として使用されます(例:from('covers'))。また、upload(key, file)に渡すキーにはバケットのプレフィックスを含めません。バケットはあらかじめ作成しておく必要があります。Supabase Storage と同じモデルです。まだ作成されていないバケットにアップロードしないでください。 - アップロード前に、ファイルの種類とサイズを検証してください。
- UX 上重要な大きいファイルでは、アップロードの進行状況を表示してください。
- ローカル開発環境のオリジンでは、アップロード経路が利用可能だと判断する前に、環境のセキュリティドメインにフロントエンドの正確なオリジンがすでに登録されていることを確認してください。
- 通常、完全な
http://...URL ではなくhost:port形式で返されるqueryEnv(action="domains")のホワイトリスト項目の形式に合わせてください。 - 環境にストレージバケットがない場合、または SDK が
STORAGE_NOT_EXIST/STORAGE_BUCKET_NOT_FOUNDを返した場合は、再試行する前に CloudBase 管理ツール/MCP ストレージツールを使ってバケットを作成するか、既存のバケットを選択してください。任意のアップロードが成功したものとして扱わないでください。 app.uploadFile()の成功後、envId、バケットドメイン、cloudPathを連結して、公開 URL のような文字列を捏造しないでください。返されたfileIDをapp.getTempFileURL()に渡し、SDK が解決した URL を保存または表示してください。
進行状況の例
await app.uploadFile({
cloudPath: "uploads/avatar.jpg",
filePath: selectedFile,
onUploadProgress: ({ loaded, total }) => {
const percent = Math.round((loaded * 100) / total);
console.log(percent);
}
});
一時 URL
const result = await app.getTempFileURL({
fileList: [
{
fileID: "cloud://env-id/uploads/avatar.jpg",
maxAge: 3600
}
]
});
永続的な公開リンクを公開せずに、ブラウザーで非公開ファイルをプレビューまたはダウンロードする必要がある場合は、一時 URL を使用してください。
アップロードからプレビューまでの一般的なフロー:
const uploadResult = await app.uploadFile({
cloudPath: "uploads/avatar.jpg",
filePath: selectedFile
});
const tempUrlResult = await app.getTempFileURL({
fileList: [{ fileID: uploadResult.fileID, maxAge: 3600 }]
});
const previewUrl = tempUrlResult.fileList?.[0]?.tempFileURL || tempUrlResult.fileList?.[0]?.download_url;
if (!previewUrl) {
throw new Error("Failed to resolve temporary file URL after upload");
}
ファイルの削除
await app.deleteFile({
fileList: ["cloud://env-id/uploads/old-avatar.jpg"]
});
削除が成功したと判断する前に、ファイルごとの結果を必ず確認してください。
ファイルのダウンロード
await app.downloadFile({
fileID: "cloud://env-id/uploads/report.pdf"
});
ブラウザーからダウンロードを開始する場合に使用してください。プログラムによるレンダリングまたはプレビューには、getTempFileURL() を優先してください。
セキュリティドメインに関する注意
CORS の問題を避けるため、CloudBase のセキュリティドメインにフロントエンドのドメインを追加してください。MCP を有効にしたワークフローでは、ブラウザーからアップロードするコードを書く前に、ツールを使って確認し、更新することを推奨します。
{ "tool": "queryEnv", "action": "domains" }
追加するドメインを決める際は、実際のブラウザーオリジンを使用してください。ページがカスタムドメインまたはローカル開発用ポートで実行されている場合は、ハードコードされたリストから推測せず、その正確な host:port 値を追加してください。
{
"tool": "envDomainManagement",
"action": "create",
"domains": ["<actual-browser-host>:<actual-browser-port>"]
}
実際のブラウザーオリジンを、queryEnv(action="domains") が返すホワイトリスト項目の形式に合わせてください。ローカルの Vite サーバーやプレビューサーバーでは、実行ごとにポートが変わることがあるため、固定のデフォルトポートで十分だと決めつけないでください。
一般的な例:
<your-local-host>:<actual-port><your-custom-domain>
ベストプラクティス
uploads/、avatars/、documents/など、わかりやすいフォルダー構成を使用します。- アップロード前に、ブラウザーでファイルサイズと種類を検証します。
- 適切な有効期限を設定した一時 URL を使用します。
- 古くなったファイルをそのまま孤立したストレージオブジェクトとして残さず、削除します。
- 権限が必要な一括管理タスクは、ブラウザーから直接アクセスするのではなく、バックエンドまたは MCP のフローに振り分けます。
エラー処理
try {
const result = await app.uploadFile({
cloudPath: "uploads/file.jpg",
filePath: selectedFile
});
console.log(result.fileID);
} catch (error) {
console.error("Storage operation failed:", error);
}