extension-object-storage
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseObject Storage
对象存储
Object storage extension for Caffeine AI.
为Caffeine AI提供的对象存储扩展。
Overview
概述
This skill adds off-chain file/object storage with on-chain references. The mixin provides infrastructure for file operations; you track uploaded files in your own data structures using .
MixinObjectStorageStorage.ExternalBlob该技能添加了带有链上引用的链下文件/对象存储功能。 mixin提供了文件操作的基础设施;您可以使用在自有数据结构中跟踪已上传的文件。
MixinObjectStorageStorage.ExternalBlobRequired Setup Checklist
必备设置清单
All four steps are mandatory. Skipping any one causes at upload time.
403 Forbidden: Invalid payload- mops dependency — add to
caffeineai-object-storageundermops.toml.[dependencies] - Mixin invocation — in
include MixinObjectStorage()(imported frommain.mo)."mo:caffeineai-object-storage/Mixin" - Storage.ExternalBlob types — every data field that represents a file MUST use , never
Storage.ExternalBlob.Text - Frontend npm package — installed and
@caffeineai/object-storageused at the call site.ExternalBlob.fromBytes()
CRITICAL: The frontend package () does NOT work without the backend mops package (). Installing only the npm package and not the mops package causes silent upload failures (403 from the storage gateway). You MUST install both together.
@caffeineai/object-storagecaffeineai-object-storage四个步骤均为必填项。跳过任何一步都会导致上传时出现错误。
403 Forbidden: Invalid payload- mops依赖 — 在的
mops.toml下添加[dependencies]。caffeineai-object-storage - Mixin调用 — 在中加入
main.mo(从include MixinObjectStorage()导入)。"mo:caffeineai-object-storage/Mixin" - Storage.ExternalBlob类型 — 所有表示文件的数据字段必须使用,绝不能使用
Storage.ExternalBlob。Text - 前端npm包 — 安装并在调用处使用
@caffeineai/object-storage。ExternalBlob.fromBytes()
重要提示:前端包()没有后端mops包()无法正常工作。仅安装npm包而不安装mops包会导致静默上传失败(存储网关返回403)。您必须同时安装两者。
@caffeineai/object-storagecaffeineai-object-storageBackend
后端
File content is stored off-chain. The backend manages references to external files using the type from . The frontend handles the actual upload/download; the backend only stores the reference.
Storage.ExternalBlobmo:caffeineai-object-storage/StorageCRITICAL: ANY data field that represents a file, image, photo, document, or media MUST use as its type -- NEVER . Using breaks the upload/download proxy. Method parameters that accept file uploads MUST also use , not .
Storage.ExternalBlobTextTextStorage.ExternalBlobTextCorrect:
blob : Storage.ExternalBlobWrong:
blobId : Text
imageUrl : Text
fileRef : Text文件内容存储在链下。后端使用来自的类型管理外部文件的引用。前端处理实际的上传/下载操作;后端仅存储引用。
mo:caffeineai-object-storage/StorageStorage.ExternalBlob重要提示:任何表示文件、图片、照片、文档或媒体的数据字段必须使用作为其类型——绝对不能使用。使用会破坏上传/下载代理。接受文件上传的方法参数也必须使用,而非。
Storage.ExternalBlobTextTextStorage.ExternalBlobText正确示例:
blob : Storage.ExternalBlob错误示例:
blobId : Text
imageUrl : Text
fileRef : TextModule API
模块API
The only type you use from is (which is ). All other functions in are internal infrastructure used by -- do not call them directly.
mo:caffeineai-object-storage/StorageExternalBlobBlobStorage.moMixinObjectStorage您从中使用的唯一类型是(即)。中的所有其他函数都是使用的内部基础设施——请勿直接调用它们。
mo:caffeineai-object-storage/StorageExternalBlobBlobStorage.moMixinObjectStorageSetup in main.mo
在main.mo中设置
include MixinObjectStorage()main.momotoko
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.momotoko
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 or any other method. These are platform-reserved method names provided exclusively by the mixin from the mops package. Hand-written implementations produce wrong return types and cause at upload time.
_immutableObjectStorageCreateCertificate_immutableObjectStorage*MixinObjectStorage403 Forbidden: Invalid payloadWrong — 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 — a mops package, never a relative path. Any relative import like or is wrong.
"mo:caffeineai-object-storage/Mixin""mixins/object-storage-api""./ObjectStorage"The correct signature produced by the platform mixin is:
_immutableObjectStorageCreateCertificate : (blobHash : Text) -> async record { method : Text; blob_hash : Text }Any other return type (, , , etc.) will fail gateway validation.
Blob()Text绝不要创建或任何其他方法的自定义实现。这些是平台保留的方法名称,仅由mops包中的 mixin提供。手写实现会产生错误的返回类型,导致上传时出现错误。
_immutableObjectStorageCreateCertificate_immutableObjectStorage*MixinObjectStorage403 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();正确的导入路径始终是——这是一个mops包,绝不能使用相对路径。任何类似或的相对导入都是错误的。
"mo:caffeineai-object-storage/Mixin""mixins/object-storage-api""./ObjectStorage"平台mixin生成的正确签名为:
_immutableObjectStorageCreateCertificate : (blobHash : Text) -> async record { method : Text; blob_hash : Text }任何其他返回类型(、、等)都会导致网关验证失败。
Blob()TextFrontend
前端
Backend fields are represented as on the frontend.
BlobExternalBlobtypescript
import { ExternalBlob } from "@caffeineai/object-storage";
import type { FileRecord } from "@caffeineai/object-storage";后端的字段在前端表示为。
BlobExternalBlobtypescript
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 object to and pass the original filename alongside:
FileExternalBlobtypescript
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 so the backend stores the original filename.
file.name将浏览器对象转换为,并同时传递原始文件名:
FileExternalBlobtypescript
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.nameDisplaying Files
显示文件
Use 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.
getDirectURL()typescript
<img src={record.blob.getDirectURL()} alt={record.filename} />使用进行内联显示(图片、视频)。该方法返回一个不透明的代理URL——它没有文件扩展名,因此绝不要通过检查URL来确定文件类型。
getDirectURL()typescript
<img src={record.blob.getDirectURL()} alt={record.filename} />File Type Detection
文件类型检测
CRITICAL: Never detect file types by inspecting the URL from . These are opaque proxy URLs with no extension. Instead use the field from the backend record:
getDirectURL()filenametypescript
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 field, prefer that:
mimeTypetypescript
const isImage = (mimeType?: string) => mimeType?.startsWith("image/");重要提示:绝不要通过检查返回的URL来检测文件类型。这些是不透明的代理URL,没有扩展名。应改用后端记录中的字段:
getDirectURL()filenametypescript
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>
)}如果后端还返回字段,则优先使用该字段:
mimeTypetypescript
const isImage = (mimeType?: string) => mimeType?.startsWith("image/");Downloading Files
下载文件
For downloads with the original filename, use to create a downloadable link:
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);
};Use for inline display, for save-as downloads.
getDirectURL()getBytes()要以原始文件名下载文件,请使用创建可下载链接:
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 case | Method | Notes |
|---|---|---|
| Display image/video | | Streaming, cached |
| Download with filename | | Wrap in Blob + anchor |
| Upload from browser | | Pair with |
| Detect file type | | NEVER inspect the URL |
| 使用场景 | 方法 | 说明 |
|---|---|---|
| 显示图片/视频 | | 流式传输、已缓存 |
| 带文件名下载 | | 包装为Blob + 锚点 |
| 从浏览器上传 | | 搭配 |
| 检测文件类型 | | 绝不要检查URL |
Verifying the Setup
验证设置
Confirm the backend has the mops dependency installed. Check :
src/backend/mops.tomltoml
[dependencies]
caffeineai-object-storage = "0.1.2"If is missing from , object storage will not work regardless of what the frontend does. Add it, run , and rebuild.
caffeineai-object-storage[dependencies]mops install确认后端已安装mops依赖。检查:
src/backend/mops.tomltoml
[dependencies]
caffeineai-object-storage = "0.1.2"如果中缺少,无论前端如何操作,对象存储都无法正常工作。添加该依赖,运行并重新构建。
[dependencies]caffeineai-object-storagemops installTroubleshooting
故障排除
| Error | Cause | Fix |
|---|---|---|
| Backend canister missing | Install |
| | Add the mops dependency and rebuild backend |
| Method exists but still 403 | Hand-written stub returns wrong type (e.g. | Remove the custom implementation, use the platform mixin instead |
| Cashier registration issue (unrelated to this skill) | Redeploy the backend canister to trigger self-healing registration |
| 错误 | 原因 | 修复方法 |
|---|---|---|
| 后端canister缺少 | 在mops.toml中安装 |
(所有文件) | 已安装 | 添加mops依赖并重新构建后端 |
| 方法存在但仍返回403 | 手写存根返回错误类型(例如 | 删除自定义实现,改用平台mixin |
| Cashier注册问题(与本技能无关) | 重新部署后端canister以触发自修复注册 |