跳转到内容

文件处理

PocketBase 提供了完整的文件管理功能,支持本地存储和对象存储(S3兼容)。

{
"name": "avatar",
"type": "file",
"options": {
"maxSelect": 1, // 最多选择 1 个文件
"maxSize": 5242880, // 单文件最大 5MB
"mimeTypes": [ // 限制文件类型
"image/jpeg",
"image/png",
"image/webp"
],
"thumbs": ["200x200"], // 生成缩略图
"protected": false // 公开访问
}
}
选项类型说明
maxSelectnumber最大文件数量,null 表示无限制
maxSizenumber单文件最大字节数
mimeTypesstring[]允许的 MIME 类型,* 表示全部
thumbsstring[]缩略图尺寸列表
protectedbool是否需要鉴权才能访问
{
"name": "gallery",
"type": "file",
"options": {
"maxSelect": 10,
"maxSize": 10485760,
"mimeTypes": ["image/*", "video/mp4"],
"thumbs": ["200x200", "800x600"]
}
}
{
"name": "document",
"type": "file",
"options": {
"maxSelect": 1,
"protected": true // 需要登录才能访问
}
}
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
// 单文件上传
const formData = new FormData();
formData.append("title", "My Post");
formData.append("file", fileInput.files[0]);
const record = await pb.collection("posts").create(formData);
const formData = new FormData();
formData.append("title", "Gallery");
// 多个文件使用相同的字段名
for (const file of fileInput.files) {
formData.append("gallery", file);
}
const record = await pb.collection("posts").create(formData);
function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("file", file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
onProgress(percent);
}
});
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
});
xhr.open("POST", `${pb.baseUrl}/api/collections/uploads/records`);
xhr.setRequestHeader("Authorization", `Bearer ${pb.authStore.token}`);
xhr.send(formData);
});
}
<script setup>
import { ref } from "vue";
import { pb } from "@/lib/pocketbase";
const uploading = ref(false);
const progress = ref(0);
const preview = ref(null);
async function uploadFile(file) {
uploading.value = true;
progress.value = 0;
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("file", file);
formData.append("title", "Uploaded file");
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
progress.value = Math.round((e.loaded / e.total) * 100);
}
});
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
uploading.value = false;
});
xhr.open("POST", `${pb.baseUrl}/api/collections/uploads/records`);
xhr.setRequestHeader("Authorization", `Bearer ${pb.authStore.token}`);
xhr.send(formData);
});
}
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
preview.value = URL.createObjectURL(file);
}
}
</script>
<template>
<div>
<input type="file" @change="handleFileSelect" :disabled="uploading" />
<img v-if="preview" :src="preview" class="preview" />
<p v-if="uploading">上传中: {{ progress }}%</p>
</div>
</template>
import { useState, useRef } from "react";
import pb from "@/lib/pocketbase";
export function FileUpload() {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const fileInput = useRef(null);
async function uploadFile(file) {
setUploading(true);
setProgress(0);
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("file", file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
setProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
setUploading(false);
});
xhr.open("POST", `${pb.baseUrl}/api/collections/uploads/records`);
xhr.setRequestHeader("Authorization", `Bearer ${pb.authStore.token}`);
xhr.send(formData);
});
}
return (
<div>
<input ref={fileInput} type="file" disabled={uploading} />
<p>{uploading ? `上传中: ${progress}%` : "选择文件"}</p>
</div>
);
}
// 获取原始文件 URL
const url = pb.files.getUrl(record, record.avatar);
// 获取缩略图 URL
const thumbUrl = pb.files.getUrl(record, record.avatar, {
thumb: "200x200",
});
// 完整示例
const post = await pb.collection("posts").getOne("record_id");
const imageUrl = pb.files.getUrl(post, post.cover);
const thumbnailUrl = pb.files.getUrl(post, post.cover, {
thumb: "200x200",
});

受保护的文件需要通过 API 访问:

// 使用 SDK 自动处理鉴权
const url = pb.files.getUrl(record, record.document);
// 手动添加 Token
const url = `${pb.baseUrl}/api/files/${record.collectionId}/${record.id}/${record.document}?token=${pb.authStore.token}`;
// Vue
<img :src="pb.files.getUrl(post, post.cover)" />
// React
<img src={pb.files.getUrl(post, post.cover)} />
// 缩略图
<img :src="pb.files.getUrl(post, post.cover, { thumb: '200x200' })" />

配置 S3 或兼容服务(如 MinIO、阿里云 OSS):

// pocketbase.config.go 或使用环境变量
package main
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/plugins/s3storage"
)
func main() {
app := pocketbase.New()
s3storage.Register(app, s3storage.Config{
AuthToken: "your_s3_access_key",
AuthSecret: "your_s3_secret_key",
Bucket: "my-bucket",
Region: "us-east-1",
Endpoint: "https://s3.amazonaws.com",
ForcePathStyle: false,
})
app.Start()
}
s3storage.Register(app, s3storage.Config{
AuthToken: "OSS_ACCESS_KEY_ID",
AuthSecret: "OSS_ACCESS_KEY_SECRET",
Bucket: "my-bucket",
Region: "oss-cn-hangzhou",
Endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
ForcePathStyle: true,
});
s3storage.Register(app, s3storage.Config{
AuthToken: "SECRET_ID",
AuthSecret: "SECRET_KEY",
Bucket: "my-bucket-1234567890",
Region: "ap-guangzhou",
Endpoint: "https://cos.ap-guangzhou.myqcloud.com",
ForcePathStyle: false,
});
.env
S3_ACCESS_KEY=your_access_key
S3_SECRET_KEY=your_secret_key
S3_BUCKET=my-bucket
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.amazonaws.com
{
"name": "image",
"type": "file",
"options": {
"thumbs": ["200x200", "400x400", "800x800"]
}
}
// 获取指定尺寸缩略图
const thumb200 = pb.files.getUrl(record, record.image, {
thumb: "200x200",
});
const thumb400 = pb.files.getUrl(record, record.image, {
thumb: "400x400",
});
原始文件: abc123.jpg
200x200 缩略图: thumb_200x200_abc123.jpg
400x400 缩略图: thumb_400x400_abc123.jpg
function validateFile(file) {
const maxSize = 5 * 1024 * 1024; // 5MB
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
if (file.size > maxSize) {
throw new Error("文件大小不能超过 5MB");
}
if (!allowedTypes.includes(file.type)) {
throw new Error("只支持 JPG、PNG、WebP 格式");
}
return true;
}
// 使用
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
try {
validateFile(file);
} catch (err) {
alert(err.message);
}
});
onRecordBeforeCreateRequest((e) => {
const collection = e.record.collection();
// 检查上传的文件
collection.schema.forEach((field) => {
if (field.type === "file") {
const files = e.record.data[field.name];
// 自定义验证逻辑
}
});
});

使用 cascadeDelete 配置:

{
"name": "attachments",
"type": "file",
"options": {
"cascadeDelete": true // 删除记录时同时删除文件
}
}
// 删除记录会自动删除关联文件(如果配置了 cascadeDelete)
await pb.collection("posts").delete("record_id");
// 或通过 API 直接删除文件
await pb.send(`/api/files/posts/record_id/filename.jpg`, {
method: "DELETE",
});

生产环境推荐使用对象存储:

  • 可靠性高,有冗余备份
  • CDN 加速
  • 无限扩展性
  • 成本相对较低
{
"name": "avatar",
"type": "file",
"options": {
"maxSize": 2097152 // 2MB
}
}
{
"name": "document",
"type": "file",
"options": {
"mimeTypes": [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
]
}
}

将公开文件配置 CDN 加速:

location /api/files/ {
proxy_pass http://127.0.0.1:8090;
proxy_cache cdn_cache;
proxy_cache_valid 200 7d;
proxy_cache_key "$request_uri";
}
// 敏感文件使用 protected
{
"name": "idCard",
"type": "file",
"options": {
"protected": true // 需要登录才能访问
}
}
// 结合 API 规则
{
"view": "@request.auth.id != '' && id = @request.auth.id"
}

使用对象存储的路径前缀:

s3storage.Register(app, s3storage.Config{
// ...
Bucket: "my-bucket",
// 文件会存储在 uploads/ 前缀下
});

在上传前使用客户端压缩:

import Compressor from "compressorjs";
new Compressor(file, {
quality: 0.8,
maxWidth: 1920,
maxHeight: 1080,
success(result) {
// result 是压缩后的 Blob
uploadFile(result);
},
});

PocketBase 自动使用 UUID 作为文件名:

原始上传: photo.jpg
存储为: rxk2z3j1w34o8vj_photo.jpg

使用分块上传:

const CHUNK_SIZE = 1024 * 1024; // 1MB
async function uploadChunked(file) {
const chunks = Math.ceil(file.size / CHUNK_SIZE);
const uploadId = generateUUID();
for (let i = 0; i < chunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
await uploadChunk(uploadId, i, chunk);
}
await completeUpload(uploadId);
}

在 Hooks 中实现限流:

const uploadCounts = new Map();
onRecordBeforeCreateRequest((e) => {
const userId = e.authInfo.id;
const count = uploadCounts.get(userId) || 0;
const now = Date.now();
if (now - count.lastTime < 60000 && count.count >= 10) {
throw new BadRequestError("上传频率过高,请稍后再试");
}
uploadCounts.set(userId, {
count: count.count + 1,
lastTime: now,
});
});