bknd-serve-files

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Serve 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
    bknd-file-upload
    skill
  • For CDN: storage provider with CDN support (S3/R2/Cloudinary)
  • Bknd配置中已启用媒体模块
  • 已配置存储适配器(S3、R2、Cloudinary或本地)
  • 已通过
    bknd-file-upload
    skill上传文件
  • 若使用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:
MethodUse CasePerformanceControl
API ProxySimple setup, private filesModerateFull (auth, permissions)
Direct URLHigh traffic, public filesBest (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
undefined
bash
undefined

Test 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..."

undefined
undefined

Step-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搭配自定义域名

  1. Create R2 bucket in Cloudflare dashboard
  2. Enable public access on bucket
  3. Configure custom domain (Cloudflare DNS):
    • Add CNAME:
      media.yourapp.com
      ->
      <bucket>.<account>.r2.dev
  4. 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}`,
      },
    },
  },
});
  1. Serve files via custom domain:
typescript
const publicUrl = `https://media.yourapp.com/${filename}`;
  1. 在Cloudflare控制台创建R2存储桶
  2. 为存储桶启用公开访问权限
  3. 配置自定义域名(通过Cloudflare DNS):
    • 添加CNAME记录:
      media.yourapp.com
      ->
      <bucket>.<account>.r2.dev
  4. 在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}`,
      },
    },
  },
});
  1. 通过自定义域名提供文件服务:
typescript
const publicUrl = `https://media.yourapp.com/${filename}`;

AWS S3 with CloudFront

AWS S3搭配CloudFront

  1. Create S3 bucket with public read (or CloudFront OAI)
  2. Create CloudFront distribution:
    • Origin: S3 bucket
    • Cache policy: CachingOptimized
    • Custom domain (optional)
  3. Use CloudFront URL:
typescript
const cdnUrl = `https://d123abc.cloudfront.net/${filename}`;
// Or with custom domain
const cdnUrl = `https://cdn.yourapp.com/${filename}`;
  1. 创建S3存储桶,设置公开读权限(或使用CloudFront OAI)
  2. 创建CloudFront分发:
    • 源站:S3存储桶
    • 缓存策略:CachingOptimized
    • 自定义域名(可选)
  3. 使用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
res.cloudinary.com
with global CDN.
Cloudinary默认包含全球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,
      },
    },
  },
});
文件将从
res.cloudinary.com
通过全球CDN提供服务。

Image 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 behaviors

Cloudflare R2 Cache Rules

Cloudflare R2缓存规则

In Cloudflare dashboard:
  1. Go to Caching > Cache Rules
  2. Create rule for your R2 subdomain
  3. Set Edge TTL (e.g., 1 year for immutable assets)
在Cloudflare控制台中:
  1. 进入「缓存」>「缓存规则」
  2. 为你的R2子域名创建规则
  3. 设置边缘TTL(例如,不可变资源设置为1年)

API Proxy Caching

API代理缓存

Bknd's API proxy supports standard HTTP caching:
bash
undefined
Bknd的API代理支持标准HTTP缓存:
bash
undefined

Client can use conditional requests

Client can use conditional requests

curl -H "If-None-Match: "abc123""
http://localhost:7654/api/media/file/image.png
curl -H "If-None-Match: "abc123""
http://localhost:7654/api/media/file/image.png

Returns 304 Not Modified if unchanged

Returns 304 Not Modified if unchanged

undefined
undefined

Access 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.read
权限:
typescript
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.read
权限:
typescript
export default defineConfig({
  auth: {
    guard: {
      roles: {
        user: {
          permissions: {
            "media.read": true,
            "media.create": true,
          },
        },
        // No anonymous role, or no media.read permission
      },
    },
  },
});
访问需要认证:
bash
undefined

Fails 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
undefined
curl -H "Authorization: Bearer $TOKEN"
http://localhost:7654/api/media/file/private.pdf
undefined

Signed 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参考

MethodEndpointDescription
GET
/api/media/file/:filename
Download/view file
GET
/api/media/files
List all files
方法端点描述
GET
/api/media/file/:filename
下载/查看文件
GET
/api/media/files
列出所有文件

Request Headers

请求标头

HeaderDescription
Authorization
Bearer token (if auth required)
If-None-Match
ETag for conditional request
Range
Byte range for partial download
标头描述
Authorization
Bearer令牌(若需要认证)
If-None-Match
用于条件请求的ETag
Range
用于分片下载的字节范围

Response Headers

响应标头

HeaderDescription
Content-Type
File MIME type
Content-Length
File size in bytes
ETag
File hash for caching
Accept-Ranges
Indicates range support
标头描述
Content-Type
文件MIME类型
Content-Length
文件大小(字节)
ETag
用于缓存的文件哈希值
Accept-Ranges
表示支持范围请求

Common Pitfalls

常见问题

404 File Not Found

404文件未找到

Problem: File URL returns 404.
Causes:
  1. Filename doesn't exist
  2. Wrong path/case sensitivity
  3. 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错误。
原因:
  1. 文件名不存在
  2. 路径错误/大小写不匹配
  3. 文件已被删除
解决方法: 验证文件是否存在:
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-fetching

Verification

验证

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 - 配置公开与认证访问