name: bknd-storage-config description: 用于配置文件上传的存储后端。涵盖 S3 兼容存储(AWS S3、Cloudflare R2、DigitalOcean Spaces)、Cloudinary 媒体存储、用于开发的本地文件系统适配器、适配器配置选项、环境变量和生产存储设置。
存储配置
配置 Bknd 媒体模块的存储后端。
先决条件
- 已安装
bknd包 - 对于 S3:已创建具有适当权限的存储桶
- 对于 Cloudinary:具有 API 凭据的账户
- 对于 R2:具有 R2 绑定的 Cloudflare Workers 环境
何时使用 UI 模式
- 管理面板不支持存储配置
- 必须在代码中配置存储
何时使用代码模式
- 所有存储配置(必需)
- 为不同环境设置不同的适配器
- 使用凭据的生产存储
存储适配器概述
| 适配器 | 类型 | 使用案例 |
|---|---|---|
s3 |
S3 兼容 | AWS S3、Cloudflare R2(外部)、DigitalOcean Spaces、MinIO |
cloudinary |
媒体优化 | 图像/视频转换、CDN 交付 |
local |
文件系统 | 仅用于开发(Node.js/Bun 运行时) |
r2 |
Cloudflare R2 | 具有 R2 绑定的 Cloudflare Workers |
逐步指南:S3 适配器
步骤 1:创建 S3 存储桶
在 AWS 控制台或通过 CLI 创建存储桶:
aws s3 mb s3://my-app-uploads --region us-east-1
步骤 2:配置 CORS(如果浏览器上传)
{
"CORSRules": [{
"AllowedOrigins": ["https://yourapp.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"]
}]
}
步骤 3:获取访问凭据
创建具有 S3 访问权限的 IAM 用户并获取:
- 访问密钥 ID
- 秘密访问密钥
步骤 4:配置 Bknd
import { defineConfig } from "bknd";
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "s3",
config: {
access_key: process.env.S3_ACCESS_KEY,
secret_access_key: process.env.S3_SECRET_KEY,
url: "https://my-bucket.s3.us-east-1.amazonaws.com",
},
},
},
});
步骤 5:添加环境变量
# .env
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=wJalr...
S3 URL 格式
不同的 S3 兼容服务使用不同的 URL 格式:
// AWS S3
url: "https://{bucket}.s3.{region}.amazonaws.com"
// 示例:"https://my-bucket.s3.us-east-1.amazonaws.com"
// Cloudflare R2(通过 S3 API 外部访问)
url: "https://{account_id}.r2.cloudflarestorage.com/{bucket}"
// 示例:"https://abc123.r2.cloudflarestorage.com/my-bucket"
// DigitalOcean Spaces
url: "https://{bucket}.{region}.digitaloceanspaces.com"
// 示例:"https://my-bucket.nyc3.digitaloceanspaces.com"
// MinIO(自托管)
url: "http://localhost:9000/{bucket}"
逐步指南:Cloudinary 适配器
步骤 1:获取 Cloudinary 凭据
从 Cloudinary 仪表板复制:
- 云名称
- API 密钥
- API 秘密
步骤 2:配置 Bknd
import { defineConfig } from "bknd";
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "cloudinary",
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
},
},
},
});
步骤 3:添加环境变量
# .env
CLOUDINARY_CLOUD_NAME=my-cloud
CLOUDINARY_API_KEY=123456789
CLOUDINARY_API_SECRET=abcdef...
可选:上传预设
用于无符号上传或自定义转换:
adapter: {
type: "cloudinary",
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
upload_preset: "my-preset", // 可选
},
},
逐步指南:本地适配器(开发)
步骤 1:创建上传目录
mkdir -p ./uploads
步骤 2:注册和配置
import { defineConfig } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
// 注册本地适配器
const local = registerLocalMediaAdapter();
export default defineConfig({
media: {
enabled: true,
adapter: local({ path: "./uploads" }),
},
});
文件通过 /api/media/file/{filename} 提供服务。
运行时注意事项
本地适配器需要 Node.js 或 Bun 运行时(文件系统访问)。在以下环境中无法工作:
- Cloudflare Workers
- Vercel Edge Functions
- 浏览器环境
逐步指南:Cloudflare R2(Workers)
步骤 1:创建 R2 存储桶
wrangler r2 bucket create my-bucket
步骤 2:在 wrangler.toml 中添加 R2 绑定
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
步骤 3:配置 Bknd
import { serve, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
const config: CloudflareBkndConfig = {
app: (env) => ({
connection: { url: env.DB },
config: {
media: {
enabled: true,
adapter: {
type: "r2",
config: {
binding: "MY_BUCKET",
},
},
},
},
}),
};
export default serve(config);
R2 适配器直接使用 Cloudflare Workers 绑定,无需外部凭据。
媒体模块选项
大小限制
export default defineConfig({
media: {
enabled: true,
body_max_size: 10 * 1024 * 1024, // 最大上传 10MB
adapter: { ... },
},
});
默认大小行为
如果未设置 body_max_size,上传无大小限制。在生产中始终设置合理的限制。
基于环境的配置
为开发和生产使用不同的适配器:
import { defineConfig } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
const local = registerLocalMediaAdapter();
const isDev = process.env.NODE_ENV !== "production";
export default defineConfig({
media: {
enabled: true,
body_max_size: 25 * 1024 * 1024, // 25MB
adapter: isDev
? local({ path: "./uploads" })
: {
type: "s3",
config: {
access_key: process.env.S3_ACCESS_KEY,
secret_access_key: process.env.S3_SECRET_KEY,
url: process.env.S3_BUCKET_URL,
},
},
},
});
验证存储配置
检查媒体模块是否启用
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// 列出文件(如果还没有上传,则为空)
const { ok, data, error } = await api.media.listFiles();
if (ok) {
console.log("媒体模块正常工作,文件数:", data.length);
} else {
console.error("媒体错误:", error);
}
测试上传
async function testStorage() {
const testFile = new File(["测试内容"], "test.txt", {
type: "text/plain"
});
const { ok, data, error } = await api.media.upload(testFile);
if (ok) {
console.log("上传成功:", data.name);
// 清理
await api.media.deleteFile(data.name);
console.log("清理完成");
} else {
console.error("上传失败:", error);
}
}
通过 REST 检查
# 列出文件
curl http://localhost:7654/api/media/files
# 上传测试文件
echo "测试" | curl -X POST \
-H "Content-Type: text/plain" \
--data-binary @- \
http://localhost:7654/api/media/upload/test.txt
完整配置示例
AWS S3 生产环境
import { defineConfig } from "bknd";
export default defineConfig({
connection: {
url: process.env.DATABASE_URL,
},
config: {
media: {
enabled: true,
body_max_size: 50 * 1024 * 1024, // 50MB
adapter: {
type: "s3",
config: {
access_key: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
url: `https://${process.env.S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com`,
},
},
},
},
});
Cloudflare R2 + D1
import { serve, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
const config: CloudflareBkndConfig = {
app: (env) => ({
connection: { url: env.DB }, // D1 绑定
config: {
media: {
enabled: true,
body_max_size: 25 * 1024 * 1024,
adapter: {
type: "r2",
config: { binding: "UPLOADS" },
},
},
},
}),
};
export default serve(config);
开发环境热重载
import { defineConfig } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
const local = registerLocalMediaAdapter();
export default defineConfig({
connection: {
url: "file:data.db",
},
config: {
media: {
enabled: true,
adapter: local({ path: "./public/uploads" }),
},
},
});
常见陷阱
S3 403 禁止访问
问题: 上传失败并返回 403 错误。
原因:
- 无效凭据
- 存储桶策略阻止访问
- URL 格式不正确
修复:
// 检查 URL 格式 - 不能有尾部斜杠
url: "https://bucket.s3.region.amazonaws.com" // 正确
url: "https://bucket.s3.region.amazonaws.com/" // 错误
// 验证凭据
console.log("密钥:", process.env.S3_ACCESS_KEY?.substring(0, 8) + "...");
本地适配器 404
问题: 上传后文件找不到。
原因:
- 上传目录不存在
- 路径错误(绝对 vs 相对)
修复:
# 先创建目录
mkdir -p ./uploads
# 使用项目根目录的相对路径
adapter: local({ path: "./uploads" }) // 正确
adapter: local({ path: "/uploads" }) // 错误(绝对)
R2 绑定未找到
问题: 错误“未找到具有键的 R2Bucket”。
修复: 确保 wrangler.toml 有正确的绑定:
[[r2_buckets]]
binding = "MY_BUCKET" // 此名称用于配置
bucket_name = "actual-bucket"
Cloudinary 上传静默失败
问题: putObject 返回未定义。
原因:
- 无效凭据
- API 密钥没有上传权限
修复:
- 在 Cloudinary 仪表板验证凭据
- 确保 API 密钥具有写权限(非只读)
运行时适配器错误
问题: 本地适配器在无服务器环境中失败。
修复: 为无服务器环境使用 S3/R2/Cloudinary:
// Cloudflare Workers - 使用 r2
// Vercel - 使用 s3
// AWS Lambda - 使用 s3
// Node.js 服务器 - 本地适配器可用于开发
缺少环境变量
问题: 运行时凭据未定义。
修复:
// 添加验证
if (!process.env.S3_ACCESS_KEY) {
throw new Error("未设置 S3_ACCESS_KEY");
}
// 或为开发提供默认值
const config = {
access_key: process.env.S3_ACCESS_KEY ?? "dev-key",
// ...
};
CORS 未配置
问题: 浏览器上传被阻止。
修复: 在存储桶本身(AWS/R2 控制台)配置 CORS,而不是在 Bknd 中。
安全检查清单
- 从不提交凭据 - 使用环境变量
- 限制存储桶访问 - 除非需要,否则不要使存储桶公开
- 设置大小限制 - 始终配置
body_max_size - 使用 HTTPS - 特别是对于基于凭据的上传
- 轮换密钥 - 定期轮换访问密钥
- 最小权限原则 - IAM 用户只需对一个存储桶具有 S3 访问权限
注意事项
要做:
- 使用环境变量处理凭据
- 在生产中设置
body_max_size - 为生产使用 S3/R2/Cloudinary
- 为浏览器上传配置 CORS
- 部署前测试存储
不要做:
- 将凭据提交到 git
- 在生产中使用本地适配器
- 让上传大小无限制
- 忘记为本地适配器创建上传目录
- 在 S3 URL 中使用尾部斜杠
相关技能
- bknd-file-upload - 配置存储后上传文件
- bknd-serve-files - 服务和交付上传的文件
- bknd-env-config - 管理环境变量
- bknd-deploy-hosting - 部署生产存储
- bknd-assign-permissions - 设置媒体权限