extension-object-storage

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Object Storage

对象存储

Object storage extension for Caffeine AI.
Caffeine AI提供的对象存储扩展。

Overview

概述

This skill adds off-chain file/object storage with on-chain references. The
MixinObjectStorage
mixin provides infrastructure for file operations; you track uploaded files in your own data structures using
Storage.ExternalBlob
.
该技能添加了带有链上引用的链下文件/对象存储功能。
MixinObjectStorage
mixin提供了文件操作的基础设施;您可以使用
Storage.ExternalBlob
在自有数据结构中跟踪已上传的文件。

Required Setup Checklist

必备设置清单

All four steps are mandatory. Skipping any one causes
403 Forbidden: Invalid payload
at upload time.
  1. mops dependency — add
    caffeineai-object-storage
    to
    mops.toml
    under
    [dependencies]
    .
  2. Mixin invocation
    include MixinObjectStorage()
    in
    main.mo
    (imported from
    "mo:caffeineai-object-storage/Mixin"
    ).
  3. Storage.ExternalBlob types — every data field that represents a file MUST use
    Storage.ExternalBlob
    , never
    Text
    .
  4. Frontend npm package
    @caffeineai/object-storage
    installed and
    ExternalBlob.fromBytes()
    used at the call site.
CRITICAL: The frontend package (
@caffeineai/object-storage
) does NOT work without the backend mops package (
caffeineai-object-storage
). Installing only the npm package and not the mops package causes silent upload failures (403 from the storage gateway). You MUST install both together.
四个步骤均为必填项。跳过任何一步都会导致上传时出现
403 Forbidden: Invalid payload
错误。
  1. mops依赖 — 在
    mops.toml
    [dependencies]
    下添加
    caffeineai-object-storage
  2. Mixin调用 — 在
    main.mo
    中加入
    include MixinObjectStorage()
    (从
    "mo:caffeineai-object-storage/Mixin"
    导入)。
  3. Storage.ExternalBlob类型 — 所有表示文件的数据字段必须使用
    Storage.ExternalBlob
    ,绝不能使用
    Text
  4. 前端npm包 — 安装
    @caffeineai/object-storage
    并在调用处使用
    ExternalBlob.fromBytes()
重要提示:前端包(
@caffeineai/object-storage
)没有后端mops包(
caffeineai-object-storage
)无法正常工作。仅安装npm包而不安装mops包会导致静默上传失败(存储网关返回403)。您必须同时安装两者。

Backend

后端

File content is stored off-chain. The backend manages references to external files using the
Storage.ExternalBlob
type from
mo:caffeineai-object-storage/Storage
. The frontend handles the actual upload/download; the backend only stores the reference.
CRITICAL: ANY data field that represents a file, image, photo, document, or media MUST use
Storage.ExternalBlob
as its type -- NEVER
Text
. Using
Text
breaks the upload/download proxy. Method parameters that accept file uploads MUST also use
Storage.ExternalBlob
, not
Text
.
Correct:
blob : Storage.ExternalBlob
Wrong:
blobId : Text
imageUrl : Text
fileRef : Text
文件内容存储在链下。后端使用来自
mo:caffeineai-object-storage/Storage
Storage.ExternalBlob
类型管理外部文件的引用。前端处理实际的上传/下载操作;后端仅存储引用。
重要提示:任何表示文件、图片、照片、文档或媒体的数据字段必须使用
Storage.ExternalBlob
作为其类型——绝对不能使用
Text
。使用
Text
会破坏上传/下载代理。接受文件上传的方法参数也必须使用
Storage.ExternalBlob
,而非
Text
正确示例:
blob : Storage.ExternalBlob
错误示例:
blobId : Text
imageUrl : Text
fileRef : Text

Module API

模块API

The only type you use from
mo:caffeineai-object-storage/Storage
is
ExternalBlob
(which is
Blob
). All other functions in
Storage.mo
are internal infrastructure used by
MixinObjectStorage
-- do not call them directly.
您从
mo:caffeineai-object-storage/Storage
中使用的唯一类型是
ExternalBlob
(即
Blob
)。
Storage.mo
中的所有其他函数都是
MixinObjectStorage
使用的内部基础设施——请勿直接调用它们。

Setup in main.mo

在main.mo中设置

include MixinObjectStorage()
MUST be placed in
main.mo
, not in a custom mixin file. Your own file-tracking logic goes in a separate mixin.
motoko
import MixinObjectStorage "mo:caffeineai-object-storage/Mixin";
import Storage "mo:caffeineai-object-storage/Storage";

actor {
  include MixinObjectStorage();

   // Track file references
  type Data = {
        id: Text;
        blob: Storage.ExternalBlob;
        name: Text;
        // other metadata
    };
};
include MixinObjectStorage()
必须放在
main.mo
中,不能放在自定义mixin文件中。您自己的文件跟踪逻辑应放在单独的mixin中。
motoko
import MixinObjectStorage "mo:caffeineai-object-storage/Mixin";
import Storage "mo:caffeineai-object-storage/Storage";

actor {
  include MixinObjectStorage();

   // 跟踪文件引用
  type Data = {
        id: Text;
        blob: Storage.ExternalBlob;
        name: Text;
        // 其他元数据
    };
};

Wrong: Do NOT Implement Storage Methods Yourself

错误做法:请勿自行实现存储方法

NEVER create your own implementation of
_immutableObjectStorageCreateCertificate
or any other
_immutableObjectStorage*
method. These are platform-reserved method names provided exclusively by the
MixinObjectStorage
mixin from the mops package. Hand-written implementations produce wrong return types and cause
403 Forbidden: Invalid payload
at upload time.
Wrong — inline stub in main.mo:
motoko
// WRONG: Do not write this yourself
public shared func _immutableObjectStorageCreateCertificate(fileHash : Text) : async Blob {
  CertifiedData.set(Blob.fromArray(hashBytes));
  Blob.fromArray([])
};
Wrong — custom mixin file mimicking the platform shape:
motoko
// WRONG: Do not create src/backend/mixins/object-storage-api.mo
import ObjectStorageMixin "mixins/object-storage-api";
include ObjectStorageMixin();
The correct import path is ALWAYS
"mo:caffeineai-object-storage/Mixin"
— a mops package, never a relative path. Any relative import like
"mixins/object-storage-api"
or
"./ObjectStorage"
is wrong.
The correct signature produced by the platform mixin is:
_immutableObjectStorageCreateCertificate : (blobHash : Text) -> async record { method : Text; blob_hash : Text }
Any other return type (
Blob
,
()
,
Text
, etc.) will fail gateway validation.
绝不要创建
_immutableObjectStorageCreateCertificate
或任何其他
_immutableObjectStorage*
方法的自定义实现。这些是平台保留的方法名称,仅由mops包中的
MixinObjectStorage
mixin提供。手写实现会产生错误的返回类型,导致上传时出现
403 Forbidden: Invalid payload
错误。
错误示例——在main.mo中编写内联存根:
motoko
// 错误:请勿自行编写此代码
public shared func _immutableObjectStorageCreateCertificate(fileHash : Text) : async Blob {
  CertifiedData.set(Blob.fromArray(hashBytes));
  Blob.fromArray([])
};
错误示例——模仿平台结构的自定义mixin文件:
motoko
// 错误:请勿创建src/backend/mixins/object-storage-api.mo
import ObjectStorageMixin "mixins/object-storage-api";
include ObjectStorageMixin();
正确的导入路径始终是
"mo:caffeineai-object-storage/Mixin"
——这是一个mops包,绝不能使用相对路径。任何类似
"mixins/object-storage-api"
"./ObjectStorage"
的相对导入都是错误的。
平台mixin生成的正确签名为:
_immutableObjectStorageCreateCertificate : (blobHash : Text) -> async record { method : Text; blob_hash : Text }
任何其他返回类型(
Blob
()
Text
等)都会导致网关验证失败。

Frontend

前端

Backend
Blob
fields are represented as
ExternalBlob
on the frontend.
typescript
import { ExternalBlob } from "@caffeineai/object-storage";
import type { FileRecord } from "@caffeineai/object-storage";
后端的
Blob
字段在前端表示为
ExternalBlob
typescript
import { ExternalBlob } from "@caffeineai/object-storage";
import type { FileRecord } from "@caffeineai/object-storage";

ExternalBlob API

ExternalBlob API

typescript
class ExternalBlob {
  getBytes(): Promise<Uint8Array<ArrayBuffer>>;
  getDirectURL(): string;
  static fromURL(url: string): ExternalBlob;
  static fromBytes(blob: Uint8Array<ArrayBuffer>): ExternalBlob;
  withUploadProgress(onProgress: (percentage: number) => void): ExternalBlob;
}
typescript
class ExternalBlob {
  getBytes(): Promise<Uint8Array<ArrayBuffer>>;
  getDirectURL(): string;
  static fromURL(url: string): ExternalBlob;
  static fromBytes(blob: Uint8Array<ArrayBuffer>): ExternalBlob;
  withUploadProgress(onProgress: (percentage: number) => void): ExternalBlob;
}

Uploading Files

上传文件

Convert the browser
File
object to
ExternalBlob
and pass the original filename alongside:
typescript
const handleUpload = async (file: File) => {
  const bytes = new Uint8Array(await file.arrayBuffer());
  const blob = ExternalBlob.fromBytes(bytes).withUploadProgress((pct) => {
    setProgress(pct);
  });

  await actor.uploadFile(file.name, blob);
};
Always send
file.name
so the backend stores the original filename.
将浏览器
File
对象转换为
ExternalBlob
,并同时传递原始文件名:
typescript
const handleUpload = async (file: File) => {
  const bytes = new Uint8Array(await file.arrayBuffer());
  const blob = ExternalBlob.fromBytes(bytes).withUploadProgress((pct) => {
    setProgress(pct);
  });

  await actor.uploadFile(file.name, blob);
};
务必发送
file.name
,以便后端存储原始文件名。

Displaying Files

显示文件

Use
getDirectURL()
for inline display (images, videos). This returns an opaque proxy URL -- it has no file extension, so never inspect the URL to determine file type.
typescript
<img src={record.blob.getDirectURL()} alt={record.filename} />
使用
getDirectURL()
进行内联显示(图片、视频)。该方法返回一个不透明的代理URL——它没有文件扩展名,因此绝不要通过检查URL来确定文件类型。
typescript
<img src={record.blob.getDirectURL()} alt={record.filename} />

File Type Detection

文件类型检测

CRITICAL: Never detect file types by inspecting the URL from
getDirectURL()
. These are opaque proxy URLs with no extension. Instead use the
filename
field from the backend record:
typescript
const isImage = (filename: string) =>
  /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i.test(filename);

// Conditional rendering
{isImage(record.filename) ? (
  <img src={record.blob.getDirectURL()} alt={record.filename} />
) : (
  <div>{record.filename}</div>
)}
If the backend also returns a
mimeType
field, prefer that:
typescript
const isImage = (mimeType?: string) => mimeType?.startsWith("image/");
重要提示:绝不要通过检查
getDirectURL()
返回的URL来检测文件类型。这些是不透明的代理URL,没有扩展名。应改用后端记录中的
filename
字段:
typescript
const isImage = (filename: string) =>
  /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i.test(filename);

// 条件渲染
{isImage(record.filename) ? (
  <img src={record.blob.getDirectURL()} alt={record.filename} />
) : (
  <div>{record.filename}</div>
)}
如果后端还返回
mimeType
字段,则优先使用该字段:
typescript
const isImage = (mimeType?: string) => mimeType?.startsWith("image/");

Downloading Files

下载文件

For downloads with the original filename, use
getBytes()
to create a downloadable link:
typescript
const handleDownload = async (record: FileRecord) => {
  const bytes = await record.blob.getBytes();
  const blob = new Blob([bytes]);
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = record.filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
};
Use
getDirectURL()
for inline display,
getBytes()
for save-as downloads.
要以原始文件名下载文件,请使用
getBytes()
创建可下载链接:
typescript
const handleDownload = async (record: FileRecord) => {
  const bytes = await record.blob.getBytes();
  const blob = new Blob([bytes]);
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = record.filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
};
使用
getDirectURL()
进行内联显示,使用
getBytes()
进行“另存为”下载。

Summary

总结

Use caseMethodNotes
Display image/video
blob.getDirectURL()
Streaming, cached
Download with filename
blob.getBytes()
Wrap in Blob + anchor
Upload from browser
ExternalBlob.fromBytes(bytes)
Pair with
.withUploadProgress()
Detect file type
filename
or
mimeType
field
NEVER inspect the URL
使用场景方法说明
显示图片/视频
blob.getDirectURL()
流式传输、已缓存
带文件名下载
blob.getBytes()
包装为Blob + 锚点
从浏览器上传
ExternalBlob.fromBytes(bytes)
搭配
.withUploadProgress()
使用
检测文件类型
filename
mimeType
字段
绝不要检查URL

Verifying the Setup

验证设置

Confirm the backend has the mops dependency installed. Check
src/backend/mops.toml
:
toml
[dependencies]
caffeineai-object-storage = "0.1.2"
If
caffeineai-object-storage
is missing from
[dependencies]
, object storage will not work regardless of what the frontend does. Add it, run
mops install
, and rebuild.
确认后端已安装mops依赖。检查
src/backend/mops.toml
toml
[dependencies]
caffeineai-object-storage = "0.1.2"
如果
[dependencies]
中缺少
caffeineai-object-storage
,无论前端如何操作,对象存储都无法正常工作。添加该依赖,运行
mops install
并重新构建。

Troubleshooting

故障排除

ErrorCauseFix
403 Forbidden: Invalid payload
on
PUT /v1/blob-tree/
Backend canister missing
_immutableObjectStorageCreateCertificate
or returning wrong type
Install
caffeineai-object-storage
in mops.toml, add
include MixinObjectStorage()
in main.mo, redeploy
403 Forbidden: Invalid payload
(all files)
@caffeineai/object-storage
npm installed but
caffeineai-object-storage
mops NOT installed
Add the mops dependency and rebuild backend
Method exists but still 403Hand-written stub returns wrong type (e.g.
Blob
or
()
instead of
record { method; blob_hash }
)
Remove the custom implementation, use the platform mixin instead
Forbidden: Owner does not have an account with the cashier
Cashier registration issue (unrelated to this skill)Redeploy the backend canister to trigger self-healing registration
错误原因修复方法
PUT /v1/blob-tree/
时出现
403 Forbidden: Invalid payload
后端canister缺少
_immutableObjectStorageCreateCertificate
或返回错误类型
在mops.toml中安装
caffeineai-object-storage
,在main.mo中添加
include MixinObjectStorage()
,重新部署
(所有文件)
403 Forbidden: Invalid payload
已安装
@caffeineai/object-storage
npm包,但未安装
caffeineai-object-storage
mops包
添加mops依赖并重新构建后端
方法存在但仍返回403手写存根返回错误类型(例如
Blob
()
而非
record { method; blob_hash }
删除自定义实现,改用平台mixin
Forbidden: Owner does not have an account with the cashier
Cashier注册问题(与本技能无关)重新部署后端canister以触发自修复注册