跳转到内容

Files API

PocketBase Files API 提供完整的文件管理功能,包括上传、下载、删除和访问控制。文件可以存储为公开或受保护类型,支持灵活的权限控制。

/api/files/{collection_id_or_name}/{record_id}/{filename}
# 使用集合名称
/api/files/posts/record_id_here/cover_image.jpg
# 使用集合 ID
/api/files/pbc_1234567890/record_id_here/document.pdf
# 缩略图
/api/files/posts/record_id_here/cover_image.jpg?thumb=100x100

公开文件无需认证即可访问,适用于用户头像、公开图片等。

字段配置: 在集合设置中不勾选 Protected 选项。

访问示例:

# 直接访问,无需认证
GET /api/files/posts/record_id/cover.jpg

受保护文件需要携带有效的认证 token 才能访问,适用于私密文档、用户上传的敏感文件等。

字段配置: 在集合设置中勾选 Protected 选项。

访问示例:

# 需要携带 token
GET /api/files/private_posts/record_id/document.pdf
Authorization: Bearer {token}
# 或使用 Cookie 认证
GET /api/files/private_posts/record_id/document.pdf
Cookie: pb_session={token}
const formData = new FormData();
formData.append("title", "Post with image");
formData.append("cover", fileInput.files[0]);
const record = await pb.collection("posts").create(formData);
const formData = new FormData();
formData.append("title", "Gallery post");
// 假设 gallery 是多选文件字段
for (let file of fileInput.files) {
formData.append("gallery", file);
}
const record = await pb.collection("posts").create(formData);
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
async function uploadPost(title, coverFile, attachments) {
const formData = new FormData();
formData.append("title", title);
formData.append("content", content);
formData.append("cover", coverFile);
// 多文件字段
for (let file of attachments) {
formData.append("attachments", file);
}
try {
const record = await pb.collection("posts").create(formData);
console.log("Uploaded:", record);
return record;
} catch (err) {
console.error("Upload failed:", err);
throw err;
}
}
POST /api/collections/posts/records
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="title"
My Post Title
------Boundary
Content-Disposition: form-data; name="cover"; filename="image.jpg"
Content-Type: image/jpeg
[binary image data]
------Boundary--
Terminal window
curl -X POST \
http://127.0.0.1:8090/api/collections/posts/records \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "title=My Post" \
-F "cover=@/path/to/image.jpg"
<a href="http://127.0.0.1:8090/api/files/posts/record_id/file.pdf" download>
Download PDF
</a>
// 获取下载 URL(自动附加 token)
const url = pb.files.getUrl(record, record.file);
// => "http://127.0.0.1:8090/api/files/.../file.pdf?token=..."
// 或手动构建
const url = `${pb.baseUrl}/api/files/${record.collectionId}/${record.id}/${record.file}?token=${pb.authStore.token}`;
// 在新窗口打开
window.open(url, "_blank");
// 或使用 fetch 下载
async function downloadFile(record) {
const url = pb.files.getUrl(record, record.file);
const response = await fetch(url);
const blob = await response.blob();
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = downloadUrl;
a.download = record.file;
a.click();
URL.revokeObjectURL(downloadUrl);
}

PocketBase 支持实时生成缩略图,通过 URL 参数控制。

# 指定宽高
/api/files/posts/record_id/image.jpg?thumb=200x200
# 指定宽度(高度按比例)
/api/files/posts/record_id/image.jpg?thumb=200x0
# 指定高度(宽度按比例)
/api/files/posts/record_id/image.jpg?thumb=0x200
# 保持原比例裁剪(默认)
?thumb=200x200
# 拉伸填充
?thumb=200x200&stretch
# 居中裁剪
?thumb=200x200&center
# 质量(0-100)
?thumb=200x200&quality=80
# 输出格式
?thumb=200x200&format=webp
// 原始尺寸
const originalUrl = pb.files.getUrl(record, record.cover);
// 缩略图
const thumbUrl = pb.files.getUrl(record, record.cover, {
thumb: "200x200",
});
// 高质量缩略图
const highQualityThumb = pb.files.getUrl(record, record.cover, {
thumb: "800x600",
quality: 90,
});
// WebP 格式
const webpThumb = pb.files.getUrl(record, record.cover, {
thumb: "400x300",
format: "webp",
});
const formData = new FormData();
formData.append("cover", newFile);
// 其他字段也需要传递,否则会被清空
formData.append("title", record.title);
const updated = await pb.collection("posts").update(record.id, formData);
const formData = new FormData();
// 保留现有文件(需要前端传递)
for (let file of record.attachments) {
formData.append("attachments", file);
}
// 添加新文件
formData.append("attachments", newFile);
const updated = await pb.collection("posts").update(record.id, formData);
// 清空所有文件
const formData = new FormData();
formData.append("cover", null); // 或 delete
await pb.collection("posts").update(record.id, formData);
// JS SDK 方式
await pb.collection("posts").update(record.id, {
cover: null,
});
// 删除记录会自动删除关联的所有文件
await pb.collection("posts").delete(record.id);

删除孤文件(清理未引用文件)

Section titled “删除孤文件(清理未引用文件)”

PocketBase 会在一定时间后自动清理未引用的文件。也可以手动触发:

Terminal window
# 使用 pocketbase clean 命令
./pocketbase clean --days 7
pb_data/
├── storage/
│ ├── pbc_1234567890/ # 集合 ID
│ │ ├── record_id_1/
│ │ │ ├── file1.jpg
│ │ │ └── file2.pdf
│ │ └── record_id_2/
│ │ └── document.pdf
│ └── pbc_9876543210/ # 另一个集合
│ └── ...
└── data.db # SQLite 数据库

备份时需要同时备份:

  1. pb_data/data.db - 数据库文件
  2. pb_data/storage/ - 所有存储的文件
Terminal window
# 创建完整备份
tar -czf pocketbase_backup_$(date +%Y%m%d).tar.gz pb_data/

PocketBase 默认限制:

  • 单文件最大:5MB
  • 单次请求最大:10MB

修改启动命令的环境变量:

Terminal window
# 设置最大文件大小(字节)
POCKETBASE_MAX_FILE_SIZE=10485760 ./pocketbase serve
# 设置最大请求体大小
POCKETBASE_MAX_BODY_SIZE=104857600 ./pocketbase serve
<script setup>
import { ref } from "vue";
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
const uploading = ref(false);
const progress = ref(0);
const fileInput = ref(null);
async function uploadFile() {
const file = fileInput.value.files[0];
if (!file) return;
uploading.value = true;
progress.value = 0;
const formData = new FormData();
formData.append("title", "Uploaded file");
formData.append("file", file);
try {
const record = await pb.collection("uploads").create(formData, {
// 上传进度回调
$autoCancel: false,
$fetch: (url, options) => {
return fetch(url, {
...options,
body: formData,
}).then((response) => {
// 注意:fetch API 不直接支持进度
// 如需进度,使用 XMLHttpRequest
return response;
});
},
});
console.log("Uploaded:", record);
} catch (err) {
console.error("Upload failed:", err);
} finally {
uploading.value = false;
}
}
// 带进度的上传
function uploadWithProgress(file) {
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) {
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));
}
});
xhr.addEventListener("error", () => reject(new Error("Upload failed")));
xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
xhr.open("POST", `${pb.baseUrl}/api/collections/uploads/records`);
xhr.setRequestHeader("Authorization", `Bearer ${pb.authStore.token}`);
xhr.send(formData);
});
}
</script>
<template>
<div>
<input ref="fileInput" type="file" />
<button @click="uploadFile" :disabled="uploading">
{{ uploading ? `Uploading ${progress}%` : "Upload" }}
</button>
</div>
</template>
import { useState } from "react";
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
function ImageUpload() {
const [preview, setPreview] = useState(null);
const [uploading, setUploading] = useState(false);
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
setPreview(URL.createObjectURL(file));
}
}
async function handleUpload() {
const file = fileInput.files[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append("image", file);
try {
const record = await pb.collection("images").create(formData);
console.log("Uploaded:", record);
setPreview(pb.files.getUrl(record, record.image));
} catch (err) {
console.error("Upload failed:", err);
} finally {
setUploading(false);
}
}
return (
<div>
<input type="file" accept="image/*" onChange={handleFileSelect} />
{preview && <img src={preview} alt="Preview" style={{ maxWidth: 200 }} />}
<button onClick={handleUpload} disabled={uploading}>
{uploading ? "Uploading..." : "Upload"}
</button>
</div>
);
}

解决方案:

  1. 分块上传(需要自定义实现)
async function uploadLargeFile(file, chunkSize = 5 * 1024 * 1024) {
const chunks = Math.ceil(file.size / chunkSize);
const uploadId = crypto.randomUUID();
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunkIndex", i);
formData.append("totalChunks", chunks);
formData.append("uploadId", uploadId);
formData.append("filename", file.name);
await pb.collection("file_chunks").create(formData);
}
// 最后合并分块
return pb.collection("file_chunks").create({
action: "merge",
uploadId: uploadId,
filename: file.name,
});
}
  1. 使用外部存储:将大文件上传到 OSS/S3,在 PocketBase 中存储 URL
// 使用 canvas 裁剪
function cropImage(file, x, y, width, height) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
canvas.toBlob(resolve, file.type);
};
img.src = URL.createObjectURL(file);
});
}
async function uploadCropped(file) {
const cropped = await cropImage(file, 0, 0, 200, 200);
const formData = new FormData();
formData.append("avatar", cropped);
return pb.collection("users").update(userId, formData);
}
function setupDropZone(zoneId) {
const zone = document.getElementById(zoneId);
zone.addEventListener("dragover", (e) => {
e.preventDefault();
zone.classList.add("drag-over");
});
zone.addEventListener("dragleave", () => {
zone.classList.remove("drag-over");
});
zone.addEventListener("drop", async (e) => {
e.preventDefault();
zone.classList.remove("drag-over");
const file = e.dataTransfer.files[0];
if (file) {
const formData = new FormData();
formData.append("file", file);
await pb.collection("uploads").create(formData);
}
});
}
try {
const record = await pb.collection("uploads").create(formData);
} catch (err) {
// 处理不同类型的错误
if (err.data?.file) {
// 文件字段验证错误
console.error("File error:", err.data.file.message);
} else if (err.status === 413) {
// 文件过大
console.error("File too large");
} else if (err.status === 422) {
// 验证失败
console.error("Validation failed:", err.data);
} else {
console.error("Upload failed:", err);
}
}
  1. 文件类型验证:在后端设置允许的文件类型
  2. 文件大小限制:根据业务需求设置合理的上限
  3. 病毒扫描:生产环境建议对上传文件进行扫描
  4. 敏感文件保护:使用 Protected 字段保护敏感文件
  5. URL 签名:对受保护文件使用临时签名 URL
  6. 定期清理:定期清理孤文件释放存储空间