firebase-storage について
firebase-storage は、Firebase Cloud Storage をクライアントとサーバーに組み込む開発者向けのガイドです。ファイル操作に加え、設定やセキュリティルール、一般的なエラーの対策を扱います。
主な機能
- 01クライアントとサーバーの初期化
- 02進捗表示付きアップロード
- 03ダウンロードとメタデータ取得
- 04ファイルの削除と一覧表示
- 05セキュリティルールと CORS 設定
- 06エラー処理とトラブルシューティング
活用シーン
- 01Web アプリでファイルを受け付ける
- 02大容量アップロードの進捗を表示する
- 03一時的な署名付きダウンロード URL を作成する
- 04ストレージのセキュリティルールをデプロイする
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill firebase-storageインストール後、Claude Code で/firebase-storageを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonrules/firebase-storage.mdtemplates/cors.json詳細
Firebase Cloud Storage
ステータス:本番環境で利用可能 最終更新日:2026-01-25 依存関係:なし(スタンドアロンのスキル) 最新バージョン:[email protected]、[email protected]
クイックスタート(5 分)
1. Firebase Storage を初期化する(クライアント)
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
// ... other config
};
const app = initializeApp(firebaseConfig);
export const storage = getStorage(app);
2. Firebase Admin Storage を初期化する(サーバー)
// src/lib/firebase-admin.ts
import { initializeApp, cert, getApps } from 'firebase-admin/app';
import { getStorage } from 'firebase-admin/storage';
if (!getApps().length) {
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
});
}
export const adminStorage = getStorage().bucket();
ファイルのアップロード(クライアント SDK)
基本的なアップロード
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import { storage } from './firebase';
async function uploadFile(file: File, path: string): Promise<string> {
const storageRef = ref(storage, path);
// Upload file
const snapshot = await uploadBytes(storageRef, file);
// Get download URL
const downloadURL = await getDownloadURL(snapshot.ref);
return downloadURL;
}
// Usage
const url = await uploadFile(file, `uploads/${userId}/${file.name}`);
進捗状況を表示してアップロードする
import { ref, uploadBytesResumable, getDownloadURL, UploadTask } from 'firebase/storage';
import { storage } from './firebase';
function uploadFileWithProgress(
file: File,
path: string,
onProgress: (progress: number) => void
): Promise<string> {
return new Promise((resolve, reject) => {
const storageRef = ref(storage, path);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
'state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
onProgress(progress);
},
(error) => {
reject(error);
},
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
resolve(downloadURL);
}
);
});
}
// Usage with React state
const [progress, setProgress] = useState(0);
const url = await uploadFileWithProgress(file, path, setProgress);
メタデータを指定してアップロードする
import { ref, uploadBytes, getDownloadURL, UploadMetadata } from 'firebase/storage';
import { storage } from './firebase';
async function uploadWithMetadata(file: File, path: string) {
const storageRef = ref(storage, path);
const metadata: UploadMetadata = {
contentType: file.type,
customMetadata: {
uploadedBy: userId,
originalName: file.name,
uploadTime: new Date().toISOString(),
},
};
const snapshot = await uploadBytes(storageRef, file, metadata);
const downloadURL = await getDownloadURL(snapshot.ref);
return { downloadURL, metadata: snapshot.metadata };
}
データ URL/Base64 からアップロードする
import { ref, uploadString, getDownloadURL } from 'firebase/storage';
import { storage } from './firebase';
// Upload base64 string
async function uploadBase64(base64String: string, path: string) {
const storageRef = ref(storage, path);
// For data URL (includes prefix like "data:image/png;base64,")
const snapshot = await uploadString(storageRef, base64String, 'data_url');
// For raw base64 (no prefix)
// const snapshot = await uploadString(storageRef, base64String, 'base64');
const downloadURL = await getDownloadURL(snapshot.ref);
return downloadURL;
}
ファイルのダウンロード
ダウンロード URL を取得する
import { ref, getDownloadURL } from 'firebase/storage';
import { storage } from './firebase';
async function getFileURL(path: string): Promise<string> {
const storageRef = ref(storage, path);
const downloadURL = await getDownloadURL(storageRef);
return downloadURL;
}
ファイルを Blob としてダウンロードする
import { ref, getBlob } from 'firebase/storage';
import { storage } from './firebase';
async function downloadFile(path: string): Promise<Blob> {
const storageRef = ref(storage, path);
const blob = await getBlob(storageRef);
return blob;
}
// Trigger browser download
async function downloadAndSave(path: string, filename: string) {
const blob = await downloadFile(path);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
ファイルのメタデータを取得する
import { ref, getMetadata } from 'firebase/storage';
import { storage } from './firebase';
async function getFileMetadata(path: string) {
const storageRef = ref(storage, path);
const metadata = await getMetadata(storageRef);
return {
name: metadata.name,
size: metadata.size,
contentType: metadata.contentType,
created: metadata.timeCreated,
updated: metadata.updated,
customMetadata: metadata.customMetadata,
};
}
ファイル管理
ファイルを削除する
import { ref, deleteObject } from 'firebase/storage';
import { storage } from './firebase';
async function deleteFile(path: string): Promise<void> {
const storageRef = ref(storage, path);
await deleteObject(storageRef);
}
ディレクトリ内のファイルを一覧表示する
import { ref, listAll, list, getDownloadURL } from 'firebase/storage';
import { storage } from './firebase';
// List all files in a directory
async function listAllFiles(directoryPath: string) {
const storageRef = ref(storage, directoryPath);
const result = await listAll(storageRef);
const files = await Promise.all(
result.items.map(async (itemRef) => ({
name: itemRef.name,
fullPath: itemRef.fullPath,
downloadURL: await getDownloadURL(itemRef),
}))
);
const folders = result.prefixes.map((folderRef) => ({
name: folderRef.name,
fullPath: folderRef.fullPath,
}));
return { files, folders };
}
// Paginated list (for large directories)
async function listFilesPaginated(directoryPath: string, pageSize = 100) {
const storageRef = ref(storage, directoryPath);
const result = await list(storageRef, { maxResults: pageSize });
// Get next page
if (result.nextPageToken) {
const nextPage = await list(storageRef, {
maxResults: pageSize,
pageToken: result.nextPageToken,
});
}
return result;
}
メタデータを更新する
import { ref, updateMetadata } from 'firebase/storage';
import { storage } from './firebase';
async function updateFileMetadata(path: string, newMetadata: object) {
const storageRef = ref(storage, path);
const updatedMetadata = await updateMetadata(storageRef, {
customMetadata: newMetadata,
});
return updatedMetadata;
}
サーバーサイドの操作(Admin SDK)
サーバーからアップロードする
import { adminStorage } from './firebase-admin';
async function uploadFromServer(
buffer: Buffer,
destination: string,
contentType: string
) {
const file = adminStorage.file(destination);
await file.save(buffer, {
contentType,
metadata: {
metadata: {
uploadedBy: 'server',
uploadTime: new Date().toISOString(),
},
},
});
// Make file publicly accessible (if needed)
await file.makePublic();
// Get public URL
const publicUrl = `https://storage.googleapis.com/${adminStorage.name}/${destination}`;
return publicUrl;
}
署名付き URL を生成する
import { adminStorage } from './firebase-admin';
async function generateSignedUrl(
path: string,
expiresInMinutes = 60
): Promise<string> {
const file = adminStorage.file(path);
const [signedUrl] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + expiresInMinutes * 60 * 1000,
});
return signedUrl;
}
// For uploads (write access)
async function generateUploadUrl(path: string): Promise<string> {
const file = adminStorage.file(path);
const [signedUrl] = await file.getSignedUrl({
action: 'write',
expires: Date.now() + 15 * 60 * 1000, // 15 minutes
contentType: 'application/octet-stream',
});
return signedUrl;
}
サーバーから削除する
import { adminStorage } from './firebase-admin';
async function deleteFromServer(path: string): Promise<void> {
const file = adminStorage.file(path);
await file.delete();
}
// Delete entire directory
async function deleteDirectory(directoryPath: string): Promise<void> {
await adminStorage.deleteFiles({
prefix: directoryPath,
});
}
セキュリティルール
基本的なルールの構成
// storage.rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function isValidImage() {
return request.resource.contentType.matches('image/.*')
&& request.resource.size < 5 * 1024 * 1024; // 5MB
}
function isValidDocument() {
return request.resource.contentType.matches('application/pdf')
&& request.resource.size < 10 * 1024 * 1024; // 10MB
}
// User uploads (private to user)
match /users/{userId}/{allPaths=**} {
allow read: if isOwner(userId);
allow write: if isOwner(userId)
&& (isValidImage() || isValidDocument());
}
// Public uploads (anyone can read)
match /public/{allPaths=**} {
allow read: if true;
allow write: if isAuthenticated() && isValidImage();
}
// Profile pictures
match /profiles/{userId}/avatar.{ext} {
allow read: if true;
allow write: if isOwner(userId)
&& request.resource.contentType.matches('image/.*')
&& request.resource.size < 2 * 1024 * 1024; // 2MB
}
// Deny all other access
match /{allPaths=**} {
allow read, write: if false;
}
}
}
ルールをデプロイする
firebase deploy --only storage
CORS の設定
CORS を設定する(Web では必須)
cors.json を作成します:
[
{
"origin": ["https://your-domain.com", "http://localhost:3000"],
"method": ["GET", "PUT", "POST", "DELETE"],
"maxAgeSeconds": 3600
}
]
CORS 設定を適用します:
gsutil cors set cors.json gs://your-bucket-name.appspot.com
重要: CORS を設定しないと、ブラウザーからのアップロードは CORS エラーで失敗します。
React コンポーネント
ファイルアップロードコンポーネント
// components/FileUpload.tsx
'use client';
import { useState, useRef } from 'react';
import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
import { storage } from '@/lib/firebase';
interface FileUploadProps {
path: string;
onUploadComplete: (url: string) => void;
accept?: string;
maxSize?: number; // in bytes
}
export function FileUpload({
path,
onUploadComplete,
accept = 'image/*',
maxSize = 5 * 1024 * 1024, // 5MB
}: FileUploadProps) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate file size
if (file.size > maxSize) {
setError(`File size must be less than ${maxSize / 1024 / 1024}MB`);
return;
}
setError(null);
setUploading(true);
setProgress(0);
try {
const storageRef = ref(storage, `${path}/${Date.now()}-${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
'state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
setProgress(progress);
},
(error) => {
setError(error.message);
setUploading(false);
},
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
onUploadComplete(downloadURL);
setUploading(false);
setProgress(100);
}
);
} catch (err: any) {
setError(err.message);
setUploading(false);
}
};
return (
<div>
<input
ref={inputRef}
type="file"
accept={accept}
onChange={handleFileChange}
disabled={uploading}
className="hidden"
/>
<button
onClick={() => inputRef.current?.click()}
disabled={uploading}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{uploading ? `Uploading... ${Math.round(progress)}%` : 'Upload File'}
</button>
{error && <p className="text-red-500 mt-2">{error}</p>}
</div>
);
}
アップロード機能付き画像プレビュー
// components/ImageUpload.tsx
'use client';
import { useState } from 'react';
import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
import { storage } from '@/lib/firebase';
import Image from 'next/image';
interface ImageUploadProps {
currentImage?: string;
path: string;
onUploadComplete: (url: string) => void;
}
export function ImageUpload({
currentImage,
path,
onUploadComplete,
}: ImageUploadProps) {
const [preview, setPreview] = useState<string | null>(currentImage || null);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Show preview immediately
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(file);
// Upload
setUploading(true);
const storageRef = ref(storage, `${path}/${Date.now()}-${file.name}`);
const uploadTask = uploadBytesResumable(storageRef, file);
uploadTask.on(
'state_changed',
(snapshot) => {
setProgress((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
},
(error) => {
console.error('Upload error:', error);
setUploading(false);
},
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
onUploadComplete(downloadURL);
setUploading(false);
}
);
};
return (
<div className="relative w-32 h-32">
{preview ? (
<Image
src={preview}
alt="Preview"
fill
className="object-cover rounded-full"
/>
) : (
<div className="w-full h-full bg-gray-200 rounded-full flex items-center justify-center">
<span className="text-gray-500">No image</span>
</div>
)}
<label className="absolute bottom-0 right-0 bg-blue-500 text-white p-2 rounded-full cursor-pointer">
<input
type="file"
accept="image/*"
onChange={handleFileChange}
disabled={uploading}
className="hidden"
/>
{uploading ? `${Math.round(progress)}%` : '+'}
</label>
</div>
);
}
エラー処理
よくある Storage エラー
| エラーコード | 説明 | 解決策 |
|---|---|---|
storage/unauthorized | ユーザーにアクセス権がありません | セキュリティルールを確認し、ユーザーが認証済みであることを確認します |
storage/canceled | アップロード/ダウンロードがキャンセルされました | 適切に処理し、再試行できるようにします |
storage/unknown | 不明なエラーです | ネットワークを確認し、間隔を空けて再試行します |
storage/object-not-found | ファイルが存在しません | パスを確認し、ファイルが見つからない場合の処理を行います |
storage/bucket-not-found | バケットが存在しません | storageBucket の設定を確認します |
storage/quota-exceeded | ストレージの割り当て容量を超過しました | プランをアップグレードするか、ファイルを削除します |
storage/unauthenticated | ユーザーが認証されていません | 先にユーザーをサインインさせます |
storage/invalid-checksum | アップロード中にファイルが破損しました | アップロードを再試行します |
storage/retry-limit-exceeded | 再試行回数が多すぎます | ネットワークを確認し、後でもう一度試します |
エラーハンドラーのユーティリティ
export function getStorageErrorMessage(error: any): string {
const messages: Record<string, string> = {
'storage/unauthorized': 'You do not have permission to access this file',
'storage/canceled': 'Upload was canceled',
'storage/object-not-found': 'File not found',
'storage/quota-exceeded': 'Storage quota exceeded',
'storage/unauthenticated': 'Please sign in to upload files',
'storage/invalid-checksum': 'Upload failed. Please try again',
'storage/retry-limit-exceeded': 'Upload failed after multiple retries',
};
return messages[error.code] || 'An unexpected error occurred';
}
既知の問題の予防
このスキルは、Firebase Storage で記録されている9 件のエラーを予防します:
| 問題 # | エラー/問題 | 説明 | 回避方法 | 出典 |
|---|---|---|---|---|
| #1 | storage/unauthorized | セキュリティルールによってアクセスがブロックされます | ルールをテストし、ユーザーが認証済みであることを確認します | 一般的 |
| #2 | CORS エラー | ブラウザーによってクロスオリジンリクエストがブロックされます | gsutil cors set で CORS を設定します | ドキュメント |
| #3 | 大容量ファイルのタイムアウト | アップロードがタイムアウトします | 大容量ファイルには uploadBytesResumable を使用します | 一般的 |
| #4 | メモリの問題 | 大容量ファイルをメモリに読み込んでいます | 大容量ファイルはストリーミングし、署名付き URL を使用します | 一般的 |
| #5 | Content-Type の欠落 | ファイルが誤った MIME タイプで配信されます | メタデータで必ず contentType を設定します | 一般的 |
| #6 | URL の有効期限切れ | ダウンロード URL が機能しなくなります | URL を再生成し、一時的なアクセスには署名付き URL を使用します | 一般的 |
| #7 | storage/quota-exceeded | 無料プランの上限に達しました | 使用量を監視し、プランをアップグレードします | 一般的 |
| #8 | 秘密鍵の改行文字の問題 | Admin SDK の初期化に失敗します | .replace(/\\n/g, '\n') を使用します | 一般的 |
| #9 | 重複アップロード | 同じファイルが複数回アップロードされます | ファイル名にタイムスタンプ/UUID を追加します | ベストプラクティス |
ベストプラクティス
常に行うこと
- アップロード前にファイルの種類とサイズを確認します
- 大きなファイルには**
uploadBytesResumableを使用**します(一時停止/再開に対応) - メタデータにコンテンツタイプを設定します
- 一意のファイル名を使用します(タイムスタンプまたは UUID を追加)
- アップロードエラーに対し、再試行を含めて適切に対処します
- コンポーネントのアンマウント時に、アップロードタスクの購読を解除します
- ウェブアプリケーション向けにCORS を設定します
- ファイルを保護するためにセキュリティルールを使用します
- 一時的なアクセス用に署名付き URL を生成します
絶対に行わないこと
- (本当に公開する場合を除き)認証なしでアップロードしないでください
- クライアントから提供されたファイル拡張子を決して信用しないでください(コンテンツタイプを確認します)
- 暗号化せずに機密データを保存しないでください
- クライアントコードで Admin SDK を公開しないでください
- ファイルサイズを無制限にしないでください(DoS のリスクがあります)
Firebase CLI コマンド
# Initialize Storage
firebase init storage
# Deploy security rules
firebase deploy --only storage
# Start emulator
firebase emulators:start --only storage
パッケージのバージョン(2026-01-25 確認済み)
{
"dependencies": {
"firebase": "^12.8.0"
},
"devDependencies": {
"firebase-admin": "^13.6.0"
}
}
公式ドキュメント
- Storage の概要: https://firebase.google.com/docs/storage
- Web の入門ガイド: https://firebase.google.com/docs/storage/web/start
- ファイルのアップロード: https://firebase.google.com/docs/storage/web/upload-files
- ファイルのダウンロード: https://firebase.google.com/docs/storage/web/download-files
- セキュリティルール: https://firebase.google.com/docs/storage/security
- Admin SDK: https://firebase.google.com/docs/storage/admin/start
最終確認日: 2026-01-25 | スキルのバージョン: 1.0.0
コメント
Markdown 対応