bknd-serve-files
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseServe Files
文件服务
Serve uploaded files from Bknd storage to users via API proxy or direct storage URLs.
通过API代理或直接存储URL,将Bknd存储中的已上传文件提供给用户。
Prerequisites
前置条件
- Media module enabled in Bknd config
- Storage adapter configured (S3, R2, Cloudinary, or local)
- Files uploaded via skill
bknd-file-upload - For CDN: storage provider with CDN support (S3/R2/Cloudinary)
- Bknd配置中已启用媒体模块
- 已配置存储适配器(S3、R2、Cloudinary或本地)
- 已通过skill上传文件
bknd-file-upload - 若使用CDN:需支持CDN的存储提供商(S3/R2/Cloudinary)
When to Use UI Mode
何时使用UI模式
- Admin panel > Media section > view/preview files
- Copy file URLs from admin interface
- Quick verification that files are accessible
- 管理面板 > 媒体板块 > 查看/预览文件
- 从管理界面复制文件URL
- 快速验证文件是否可访问
When to Use Code Mode
何时使用代码模式
- Build file URLs programmatically
- Configure CDN or custom domains
- Implement image optimization
- Access control for file downloads
- 以编程方式构建文件URL
- 配置CDN或自定义域名
- 实现图片优化
- 为文件下载设置访问控制
File Serving Methods
文件服务方式
Bknd supports two approaches to serve files:
| Method | Use Case | Performance | Control |
|---|---|---|---|
| API Proxy | Simple setup, private files | Moderate | Full (auth, permissions) |
| Direct URL | High traffic, public files | Best (CDN) | Limited (bucket ACLs) |
Bknd支持两种文件服务方式:
| 方式 | 使用场景 | 性能 | 可控性 |
|---|---|---|---|
| API代理 | 配置简单,适用于私有文件 | 中等 | 完全可控(认证、权限) |
| 直接URL | 高流量场景,适用于公开文件 | 最优(CDN加速) | 有限可控(存储桶ACL) |
Step-by-Step: API Proxy (via Bknd)
分步指南:API代理(通过Bknd)
Files served through Bknd API at .
/api/media/file/{filename}文件通过Bknd API的路径提供服务。
/api/media/file/{filename}Step 1: Get File URL
步骤1:获取文件URL
typescript
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// Build file URL
const fileUrl = `${api.host}/api/media/file/image.png`;
// "http://localhost:7654/api/media/file/image.png"typescript
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// Build file URL
const fileUrl = `${api.host}/api/media/file/image.png`;
// "http://localhost:7654/api/media/file/image.png"Step 2: Display in Frontend
步骤2:在前端展示
tsx
function Image({ filename }) {
const { api } = useApp();
const src = `${api.host}/api/media/file/${filename}`;
return <img src={src} alt="" />;
}tsx
function Image({ filename }) {
const { api } = useApp();
const src = `${api.host}/api/media/file/${filename}`;
return <img src={src} alt="" />;
}Step 3: Download File (SDK)
步骤3:下载文件(通过SDK)
typescript
// Get as File object
const file = await api.media.download("image.png");
// Get as stream (for large files)
const stream = await api.media.getFileStream("image.png");typescript
// Get as File object
const file = await api.media.download("image.png");
// Get as stream (for large files)
const stream = await api.media.getFileStream("image.png");Step 4: Verify Access
步骤4:验证访问权限
bash
undefinedbash
undefinedTest file access
Test file access
Response includes:
Response includes:
Content-Type: image/png
Content-Type: image/png
Content-Length: 12345
Content-Length: 12345
ETag: "abc123..."
ETag: "abc123..."
undefinedundefinedStep-by-Step: Direct Storage URLs
分步指南:直接存储URL
Serve files directly from S3/R2/Cloudinary for better performance.
直接从S3/R2/Cloudinary提供文件服务,以获得更好的性能。
S3/R2 Direct URLs
S3/R2直接URL
typescript
// S3 URL pattern
const s3Url = `https://${bucket}.s3.${region}.amazonaws.com/${filename}`;
// "https://mybucket.s3.us-east-1.amazonaws.com/image.png"
// R2 URL pattern (public bucket)
const r2Url = `https://${customDomain}/${filename}`;
// "https://media.myapp.com/image.png"typescript
// S3 URL pattern
const s3Url = `https://${bucket}.s3.${region}.amazonaws.com/${filename}`;
// "https://mybucket.s3.us-east-1.amazonaws.com/image.png"
// R2 URL pattern (public bucket)
const r2Url = `https://${customDomain}/${filename}`;
// "https://media.myapp.com/image.png"Cloudinary Direct URLs
Cloudinary直接URL
Cloudinary provides automatic CDN and transformations:
typescript
// Basic URL
const cloudinaryUrl = `https://res.cloudinary.com/${cloudName}/image/upload/${filename}`;
// With transformations
const optimizedUrl = `https://res.cloudinary.com/${cloudName}/image/upload/w_800,q_auto,f_auto/${filename}`;Cloudinary提供自动CDN和图片转换功能:
typescript
// Basic URL
const cloudinaryUrl = `https://res.cloudinary.com/${cloudName}/image/upload/${filename}`;
// With transformations
const optimizedUrl = `https://res.cloudinary.com/${cloudName}/image/upload/w_800,q_auto,f_auto/${filename}`;Building URLs in Code
代码中构建URL
typescript
// Helper to get direct URL based on adapter type
function getFileUrl(filename: string, config: MediaConfig): string {
const { adapter } = config;
switch (adapter.type) {
case "s3":
// S3/R2 URL from configured endpoint
return `${adapter.config.url}/${filename}`;
case "cloudinary":
return `https://res.cloudinary.com/${adapter.config.cloud_name}/image/upload/${filename}`;
case "local":
// Always use API proxy for local
return `/api/media/file/${filename}`;
default:
return `/api/media/file/${filename}`;
}
}typescript
// Helper to get direct URL based on adapter type
function getFileUrl(filename: string, config: MediaConfig): string {
const { adapter } = config;
switch (adapter.type) {
case "s3":
// S3/R2 URL from configured endpoint
return `${adapter.config.url}/${filename}`;
case "cloudinary":
return `https://res.cloudinary.com/${adapter.config.cloud_name}/image/upload/${filename}`;
case "local":
// Always use API proxy for local
return `/api/media/file/${filename}`;
default:
return `/api/media/file/${filename}`;
}
}CDN Configuration
CDN配置
Cloudflare R2 with Custom Domain
Cloudflare R2搭配自定义域名
-
Create R2 bucket in Cloudflare dashboard
-
Enable public access on bucket
-
Configure custom domain (Cloudflare DNS):
- Add CNAME: ->
media.yourapp.com<bucket>.<account>.r2.dev
- Add CNAME:
-
Use in Bknd config:
typescript
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "s3",
config: {
access_key: process.env.R2_ACCESS_KEY,
secret_access_key: process.env.R2_SECRET_KEY,
url: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}`,
},
},
},
});- Serve files via custom domain:
typescript
const publicUrl = `https://media.yourapp.com/${filename}`;-
在Cloudflare控制台创建R2存储桶
-
为存储桶启用公开访问权限
-
配置自定义域名(通过Cloudflare DNS):
- 添加CNAME记录:->
media.yourapp.com<bucket>.<account>.r2.dev
- 添加CNAME记录:
-
在Bknd配置中使用:
typescript
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "s3",
config: {
access_key: process.env.R2_ACCESS_KEY,
secret_access_key: process.env.R2_SECRET_KEY,
url: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}`,
},
},
},
});- 通过自定义域名提供文件服务:
typescript
const publicUrl = `https://media.yourapp.com/${filename}`;AWS S3 with CloudFront
AWS S3搭配CloudFront
-
Create S3 bucket with public read (or CloudFront OAI)
-
Create CloudFront distribution:
- Origin: S3 bucket
- Cache policy: CachingOptimized
- Custom domain (optional)
-
Use CloudFront URL:
typescript
const cdnUrl = `https://d123abc.cloudfront.net/${filename}`;
// Or with custom domain
const cdnUrl = `https://cdn.yourapp.com/${filename}`;-
创建S3存储桶,设置公开读权限(或使用CloudFront OAI)
-
创建CloudFront分发:
- 源站:S3存储桶
- 缓存策略:CachingOptimized
- 自定义域名(可选)
-
使用CloudFront URL:
typescript
const cdnUrl = `https://d123abc.cloudfront.net/${filename}`;
// Or with custom domain
const cdnUrl = `https://cdn.yourapp.com/${filename}`;Cloudinary (Built-in CDN)
Cloudinary(内置CDN)
Cloudinary includes global CDN automatically:
typescript
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "cloudinary",
config: {
cloud_name: "your-cloud-name",
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
},
},
},
});Files served from with global CDN.
res.cloudinary.comCloudinary默认包含全球CDN:
typescript
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "cloudinary",
config: {
cloud_name: "your-cloud-name",
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
},
},
},
});文件将从通过全球CDN提供服务。
res.cloudinary.comImage Optimization
图片优化
Cloudinary Transformations
Cloudinary图片转换
typescript
// Build optimized image URL
function getOptimizedImage(filename: string, options: {
width?: number;
height?: number;
quality?: "auto" | number;
format?: "auto" | "webp" | "avif" | "jpg" | "png";
crop?: "fill" | "fit" | "scale" | "thumb";
} = {}) {
const cloudName = process.env.CLOUDINARY_CLOUD_NAME;
const transforms: string[] = [];
if (options.width) transforms.push(`w_${options.width}`);
if (options.height) transforms.push(`h_${options.height}`);
if (options.quality) transforms.push(`q_${options.quality}`);
if (options.format) transforms.push(`f_${options.format}`);
if (options.crop) transforms.push(`c_${options.crop}`);
const transformStr = transforms.length > 0 ? transforms.join(",") + "/" : "";
return `https://res.cloudinary.com/${cloudName}/image/upload/${transformStr}${filename}`;
}
// Usage
const thumb = getOptimizedImage("avatar.png", {
width: 100,
height: 100,
crop: "fill",
quality: "auto",
format: "auto",
});
// "https://res.cloudinary.com/mycloud/image/upload/w_100,h_100,c_fill,q_auto,f_auto/avatar.png"typescript
// Build optimized image URL
function getOptimizedImage(filename: string, options: {
width?: number;
height?: number;
quality?: "auto" | number;
format?: "auto" | "webp" | "avif" | "jpg" | "png";
crop?: "fill" | "fit" | "scale" | "thumb";
} = {}) {
const cloudName = process.env.CLOUDINARY_CLOUD_NAME;
const transforms: string[] = [];
if (options.width) transforms.push(`w_${options.width}`);
if (options.height) transforms.push(`h_${options.height}`);
if (options.quality) transforms.push(`q_${options.quality}`);
if (options.format) transforms.push(`f_${options.format}`);
if (options.crop) transforms.push(`c_${options.crop}`);
const transformStr = transforms.length > 0 ? transforms.join(",") + "/" : "";
return `https://res.cloudinary.com/${cloudName}/image/upload/${transformStr}${filename}`;
}
// Usage
const thumb = getOptimizedImage("avatar.png", {
width: 100,
height: 100,
crop: "fill",
quality: "auto",
format: "auto",
});
// "https://res.cloudinary.com/mycloud/image/upload/w_100,h_100,c_fill,q_auto,f_auto/avatar.png"Common Transformation Patterns
常见转换模式
typescript
// Responsive images
const srcSet = [400, 800, 1200].map(w =>
`${getOptimizedImage(filename, { width: w, format: "auto" })} ${w}w`
).join(", ");
// Thumbnail generation
const thumb = getOptimizedImage(filename, {
width: 150,
height: 150,
crop: "thumb",
});
// Automatic format (WebP/AVIF when supported)
const optimized = getOptimizedImage(filename, {
quality: "auto",
format: "auto",
});typescript
// Responsive images
const srcSet = [400, 800, 1200].map(w =>
`${getOptimizedImage(filename, { width: w, format: "auto" })} ${w}w`
).join(", ");
// Thumbnail generation
const thumb = getOptimizedImage(filename, {
width: 150,
height: 150,
crop: "thumb",
});
// Automatic format (WebP/AVIF when supported)
const optimized = getOptimizedImage(filename, {
quality: "auto",
format: "auto",
});React Integration
React集成
Image Component with Fallback
带降级方案的图片组件
tsx
function StoredImage({ filename, alt, ...props }) {
const { api } = useApp();
const [error, setError] = useState(false);
// API proxy URL as fallback
const apiUrl = `${api.host}/api/media/file/${filename}`;
// Direct CDN URL (configure based on your adapter)
const cdnUrl = `https://media.yourapp.com/${filename}`;
return (
<img
src={error ? apiUrl : cdnUrl}
alt={alt}
onError={() => setError(true)}
{...props}
/>
);
}tsx
function StoredImage({ filename, alt, ...props }) {
const { api } = useApp();
const [error, setError] = useState(false);
// API proxy URL as fallback
const apiUrl = `${api.host}/api/media/file/${filename}`;
// Direct CDN URL (configure based on your adapter)
const cdnUrl = `https://media.yourapp.com/${filename}`;
return (
<img
src={error ? apiUrl : cdnUrl}
alt={alt}
onError={() => setError(true)}
{...props}
/>
);
}Responsive Image Component
响应式图片组件
tsx
function ResponsiveImage({ filename, alt, sizes = "100vw" }) {
const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
const base = `https://res.cloudinary.com/${cloudName}/image/upload`;
const srcSet = [400, 800, 1200, 1600].map(w =>
`${base}/w_${w},q_auto,f_auto/${filename} ${w}w`
).join(", ");
return (
<img
src={`${base}/w_800,q_auto,f_auto/${filename}`}
srcSet={srcSet}
sizes={sizes}
alt={alt}
loading="lazy"
/>
);
}tsx
function ResponsiveImage({ filename, alt, sizes = "100vw" }) {
const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
const base = `https://res.cloudinary.com/${cloudName}/image/upload`;
const srcSet = [400, 800, 1200, 1600].map(w =>
`${base}/w_${w},q_auto,f_auto/${filename} ${w}w`
).join(", ");
return (
<img
src={`${base}/w_800,q_auto,f_auto/${filename}`}
srcSet={srcSet}
sizes={sizes}
alt={alt}
loading="lazy"
/>
);
}File Download Button
文件下载按钮
tsx
function DownloadButton({ filename, label }) {
const { api } = useApp();
const [downloading, setDownloading] = useState(false);
const handleDownload = async () => {
setDownloading(true);
try {
const file = await api.media.download(filename);
// Create download link
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error("Download failed:", err);
} finally {
setDownloading(false);
}
};
return (
<button onClick={handleDownload} disabled={downloading}>
{downloading ? "Downloading..." : label || "Download"}
</button>
);
}tsx
function DownloadButton({ filename, label }) {
const { api } = useApp();
const [downloading, setDownloading] = useState(false);
const handleDownload = async () => {
setDownloading(true);
try {
const file = await api.media.download(filename);
// Create download link
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error("Download failed:", err);
} finally {
setDownloading(false);
}
};
return (
<button onClick={handleDownload} disabled={downloading}>
{downloading ? "Downloading..." : label || "Download"}
</button>
);
}Caching Configuration
缓存配置
S3/R2 Cache Headers
S3/R2缓存标头
Set cache headers when uploading:
typescript
// Custom adapter with cache headers (advanced)
// S3 adapter doesn't expose this directly; configure via bucket policy
// or CloudFront cache behaviors上传文件时设置缓存标头:
typescript
// Custom adapter with cache headers (advanced)
// S3 adapter doesn't expose this directly; configure via bucket policy
// or CloudFront cache behaviorsCloudflare R2 Cache Rules
Cloudflare R2缓存规则
In Cloudflare dashboard:
- Go to Caching > Cache Rules
- Create rule for your R2 subdomain
- Set Edge TTL (e.g., 1 year for immutable assets)
在Cloudflare控制台中:
- 进入「缓存」>「缓存规则」
- 为你的R2子域名创建规则
- 设置边缘TTL(例如,不可变资源设置为1年)
API Proxy Caching
API代理缓存
Bknd's API proxy supports standard HTTP caching:
bash
undefinedBknd的API代理支持标准HTTP缓存:
bash
undefinedClient can use conditional requests
Client can use conditional requests
curl -H "If-None-Match: "abc123""
http://localhost:7654/api/media/file/image.png
http://localhost:7654/api/media/file/image.png
curl -H "If-None-Match: "abc123""
http://localhost:7654/api/media/file/image.png
http://localhost:7654/api/media/file/image.png
Returns 304 Not Modified if unchanged
Returns 304 Not Modified if unchanged
undefinedundefinedAccess Control
访问控制
Public Files (No Auth)
公开文件(无需认证)
Configure default role with media.read permission:
typescript
export default defineConfig({
auth: {
guard: {
roles: {
anonymous: {
is_default: true,
permissions: {
"media.read": true, // Public read access
},
},
},
},
},
});为默认角色配置权限:
media.readtypescript
export default defineConfig({
auth: {
guard: {
roles: {
anonymous: {
is_default: true,
permissions: {
"media.read": true, // Public read access
},
},
},
},
},
});Private Files (Auth Required)
私有文件(需要认证)
Remove media.read from anonymous:
typescript
export default defineConfig({
auth: {
guard: {
roles: {
user: {
permissions: {
"media.read": true,
"media.create": true,
},
},
// No anonymous role, or no media.read permission
},
},
},
});Access requires auth:
bash
undefined从匿名角色中移除权限:
media.readtypescript
export default defineConfig({
auth: {
guard: {
roles: {
user: {
permissions: {
"media.read": true,
"media.create": true,
},
},
// No anonymous role, or no media.read permission
},
},
},
});访问需要认证:
bash
undefinedFails without auth
Fails without auth
401 Unauthorized
401 Unauthorized
Works with auth
Works with auth
curl -H "Authorization: Bearer $TOKEN"
http://localhost:7654/api/media/file/private.pdf
http://localhost:7654/api/media/file/private.pdf
undefinedcurl -H "Authorization: Bearer $TOKEN"
http://localhost:7654/api/media/file/private.pdf
http://localhost:7654/api/media/file/private.pdf
undefinedSigned URLs (Time-Limited Access)
签名URL(限时访问)
For S3/R2, generate presigned URLs:
typescript
// Custom endpoint for signed URLs (advanced)
// Requires S3 SDK directly, not through Bknd adapter
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
async function getSignedDownloadUrl(filename: string): Promise<string> {
const client = new S3Client({ /* config */ });
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: filename,
});
return getSignedUrl(client, command, { expiresIn: 3600 }); // 1 hour
}对于S3/R2,生成预签名URL:
typescript
// Custom endpoint for signed URLs (advanced)
// Requires S3 SDK directly, not through Bknd adapter
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
async function getSignedDownloadUrl(filename: string): Promise<string> {
const client = new S3Client({ /* config */ });
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: filename,
});
return getSignedUrl(client, command, { expiresIn: 3600 }); // 1 hour
}REST API Reference
REST API参考
| Method | Endpoint | Description |
|---|---|---|
| GET | | Download/view file |
| GET | | List all files |
| 方法 | 端点 | 描述 |
|---|---|---|
| GET | | 下载/查看文件 |
| GET | | 列出所有文件 |
Request Headers
请求标头
| Header | Description |
|---|---|
| Bearer token (if auth required) |
| ETag for conditional request |
| Byte range for partial download |
| 标头 | 描述 |
|---|---|
| Bearer令牌(若需要认证) |
| 用于条件请求的ETag |
| 用于分片下载的字节范围 |
Response Headers
响应标头
| Header | Description |
|---|---|
| File MIME type |
| File size in bytes |
| File hash for caching |
| Indicates range support |
| 标头 | 描述 |
|---|---|
| 文件MIME类型 |
| 文件大小(字节) |
| 用于缓存的文件哈希值 |
| 表示支持范围请求 |
Common Pitfalls
常见问题
404 File Not Found
404文件未找到
Problem: File URL returns 404.
Causes:
- Filename doesn't exist
- Wrong path/case sensitivity
- File was deleted
Fix: Verify file exists:
typescript
const { data: files } = await api.media.listFiles();
const exists = files.some(f => f.key === filename);问题: 文件URL返回404错误。
原因:
- 文件名不存在
- 路径错误/大小写不匹配
- 文件已被删除
解决方法: 验证文件是否存在:
typescript
const { data: files } = await api.media.listFiles();
const exists = files.some(f => f.key === filename);CORS Errors on Direct S3 URL
直接访问S3 URL时出现CORS错误
Problem: Browser blocks direct S3 access.
Fix: Configure CORS on S3 bucket:
json
{
"CORSRules": [{
"AllowedOrigins": ["https://yourapp.com"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}]
}问题: 浏览器阻止直接访问S3资源。
解决方法: 在S3存储桶上配置CORS:
json
{
"CORSRules": [{
"AllowedOrigins": ["https://yourapp.com"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}]
}Slow File Serving
文件服务速度慢
Problem: Files load slowly via API proxy.
Fix: Use direct storage URLs with CDN:
typescript
// Instead of API proxy
const slow = "/api/media/file/image.png";
// Use direct CDN URL
const fast = "https://cdn.yourapp.com/image.png";问题: 通过API代理提供的文件加载缓慢。
解决方法: 使用带CDN的直接存储URL:
typescript
// Instead of API proxy
const slow = "/api/media/file/image.png";
// Use direct CDN URL
const fast = "https://cdn.yourapp.com/image.png";Mixed Content (HTTP/HTTPS)
混合内容错误(HTTP/HTTPS)
Problem: HTTPS page loading HTTP file URLs.
Fix: Ensure storage URL uses HTTPS:
typescript
// WRONG
url: "http://bucket.s3.amazonaws.com",
// CORRECT
url: "https://bucket.s3.amazonaws.com",问题: HTTPS页面加载HTTP文件URL。
解决方法: 确保存储URL使用HTTPS:
typescript
// WRONG
url: "http://bucket.s3.amazonaws.com",
// CORRECT
url: "https://bucket.s3.amazonaws.com",Large File Download Fails
大文件下载失败
Problem: Download times out or memory error.
Fix: Use streaming for large files:
typescript
// Stream instead of loading into memory
const stream = await api.media.getFileStream("large-file.zip");
// Or direct download link
const downloadUrl = `${api.host}/api/media/file/large-file.zip`;
window.location.href = downloadUrl;问题: 下载大文件时超时或出现内存错误。
解决方法: 对大文件使用流处理:
typescript
// Stream instead of loading into memory
const stream = await api.media.getFileStream("large-file.zip");
// Or direct download link
const downloadUrl = `${api.host}/api/media/file/large-file.zip`;
window.location.href = downloadUrl;Cloudinary File Not Found
Cloudinary文件未找到
Problem: Cloudinary returns 404 for uploaded file.
Cause: Cloudinary uses eventual consistency; file not yet indexed.
Fix: Wait briefly or use upload response URL directly:
typescript
const { data } = await api.media.upload(file);
// Use data.name immediately rather than re-fetching问题: Cloudinary对已上传的文件返回404错误。
原因: Cloudinary使用最终一致性,文件尚未完成索引。
解决方法: 等待片刻,或直接使用上传响应中的URL:
typescript
const { data } = await api.media.upload(file);
// Use data.name immediately rather than re-fetchingVerification
验证
Test file serving setup:
typescript
async function testFileServing() {
const filename = "test-image.png";
// 1. Verify file exists
const { data: files } = await api.media.listFiles();
const file = files.find(f => f.key === filename);
console.log("File exists:", !!file);
// 2. Test API proxy
const apiUrl = `${api.host}/api/media/file/${filename}`;
const apiRes = await fetch(apiUrl);
console.log("API proxy status:", apiRes.status);
console.log("Content-Type:", apiRes.headers.get("content-type"));
// 3. Test conditional request
const etag = apiRes.headers.get("etag");
if (etag) {
const conditionalRes = await fetch(apiUrl, {
headers: { "If-None-Match": etag },
});
console.log("Conditional request:", conditionalRes.status === 304 ? "304 (cached)" : conditionalRes.status);
}
// 4. Test SDK download
const downloadedFile = await api.media.download(filename);
console.log("SDK download:", downloadedFile.name, downloadedFile.size);
}测试文件服务配置:
typescript
async function testFileServing() {
const filename = "test-image.png";
// 1. Verify file exists
const { data: files } = await api.media.listFiles();
const file = files.find(f => f.key === filename);
console.log("File exists:", !!file);
// 2. Test API proxy
const apiUrl = `${api.host}/api/media/file/${filename}`;
const apiRes = await fetch(apiUrl);
console.log("API proxy status:", apiRes.status);
console.log("Content-Type:", apiRes.headers.get("content-type"));
// 3. Test conditional request
const etag = apiRes.headers.get("etag");
if (etag) {
const conditionalRes = await fetch(apiUrl, {
headers: { "If-None-Match": etag },
});
console.log("Conditional request:", conditionalRes.status === 304 ? "304 (cached)" : conditionalRes.status);
}
// 4. Test SDK download
const downloadedFile = await api.media.download(filename);
console.log("SDK download:", downloadedFile.name, downloadedFile.size);
}DOs and DON'Ts
注意事项
DO:
- Use CDN (Cloudinary/R2/CloudFront) for public high-traffic files
- Set proper cache headers for static assets
- Use API proxy for private/auth-required files
- Implement lazy loading for images
- Use responsive images with srcSet
- Handle file not found gracefully
DON'T:
- Expose private files via public S3 URLs
- Serve large files without streaming
- Hardcode storage URLs (use config/env)
- Forget CORS configuration for direct access
- Use local adapter in production
- Skip error handling for missing files
建议:
- 对于高流量公开文件,使用CDN(Cloudinary/R2/CloudFront)
- 为静态资源设置正确的缓存标头
- 对于私有/需要认证的文件,使用API代理
- 为图片实现懒加载
- 使用带srcSet的响应式图片
- 优雅处理文件未找到的情况
禁止:
- 通过公开S3 URL暴露私有文件
- 不使用流处理提供大文件服务
- 硬编码存储URL(使用配置/环境变量)
- 直接访问时忘记配置CORS
- 在生产环境中使用本地适配器
- 忽略对缺失文件的错误处理
Related Skills
相关技能
- bknd-file-upload - Upload files to storage
- bknd-storage-config - Configure storage backends
- bknd-assign-permissions - Set media.read permission
- bknd-public-vs-auth - Configure public vs authenticated access
- bknd-file-upload - 将文件上传至存储
- bknd-storage-config - 配置存储后端
- bknd-assign-permissions - 设置权限
media.read - bknd-public-vs-auth - 配置公开与认证访问