关于 firebase-storage
firebase-storage 是 Firebase Cloud Storage 技能指南,面向在客户端和服务器端集成文件存储的开发者,涵盖上传、下载、管理及部署配置。
主要功能
- 01客户端与服务器端初始化
- 02文件上传及进度跟踪
- 03文件下载与元数据读取
- 04删除和列出文件
- 05安全规则与 CORS 配置
- 06错误处理与排查建议
适用场景
- 01为 Web 应用上传用户文件
- 02显示大型文件上传进度
- 03生成临时签名下载链接
- 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 };
}
从 Data 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/unauthorized | 用户无权访问 | 检查安全规则,确保用户已通过身份验证 |
storage/canceled | 上传/下载已取消 | 妥善处理,并允许重试 |
storage/unknown | 未知错误 | 检查网络,并采用退避策略重试 |
storage/object-not-found | 文件不存在 | 验证路径,并处理文件缺失的情况 |
storage/bucket-not-found | Bucket 不存在 | 检查 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';
}
预防已知问题
此技能可预防 9 个有记录的 Firebase Storage 错误:
| 问题编号 | 错误/问题 | 描述 | 避免方法 | 来源 |
|---|---|---|---|---|
| #1 | storage/unauthorized | 安全规则阻止访问 | 测试规则,确保用户已通过身份验证 | 常见问题 |
| #2 | CORS 错误 | 浏览器阻止了跨源请求 | 使用 gsutil cors set 配置 CORS | 文档 |
| #3 | 大文件超时 | 上传超时 | 大文件使用 uploadBytesResumable | 常见问题 |
| #4 | 内存问题 | 将大文件加载到内存中 | 流式处理大文件,使用签名 URL | 常见问题 |
| #5 | 缺少内容类型 | 文件以错误的 MIME 类型提供 | 始终在元数据中设置 contentType | 常见问题 |
| #6 | URL 过期 | 下载 URL 不再有效 | 重新生成 URL,临时访问时使用签名 URL | 常见问题 |
| #7 | storage/quota-exceeded | 已达到免费套餐限额 | 监控使用量,升级方案 | 常见问题 |
| #8 | 私钥换行符问题 | Admin SDK 初始化失败 | 使用 .replace(/\\n/g, '\n') | 常见问题 |
| #9 | 重复上传 | 同一文件被多次上传 | 在文件名中添加时间戳/UUID | 最佳实践 |
最佳实践
始终要做
- 上传前验证文件类型和大小
- 对大文件使用
uploadBytesResumable(支持暂停和恢复) - 在元数据中设置内容类型
- 使用唯一文件名(添加时间戳或 UUID)
- 妥善处理上传错误并进行重试
- 在组件卸载时取消订阅上传任务
- 为 Web 应用配置 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