Files API
PocketBase Files API 提供完整的文件管理功能,包括上传、下载、删除和访问控制。文件可以存储为公开或受保护类型,支持灵活的权限控制。
文件 URL 结构
Section titled “文件 URL 结构”/api/files/{collection_id_or_name}/{record_id}/{filename}URL 示例
Section titled “URL 示例”# 使用集合名称/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 选项。
访问示例:
# 需要携带 tokenGET /api/files/private_posts/record_id/document.pdfAuthorization: Bearer {token}
# 或使用 Cookie 认证GET /api/files/private_posts/record_id/document.pdfCookie: pb_session={token}单文件上传(FormData)
Section titled “单文件上传(FormData)”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);JavaScript SDK 完整示例
Section titled “JavaScript SDK 完整示例”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; }}原生 HTTP 上传
Section titled “原生 HTTP 上传”POST /api/collections/posts/recordsContent-Type: multipart/form-data; boundary=----Boundary
------BoundaryContent-Disposition: form-data; name="title"
My Post Title------BoundaryContent-Disposition: form-data; name="cover"; filename="image.jpg"Content-Type: image/jpeg
[binary image data]------Boundary--cURL 示例
Section titled “cURL 示例”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"直接下载(公开文件)
Section titled “直接下载(公开文件)”<a href="http://127.0.0.1:8090/api/files/posts/record_id/file.pdf" download> Download PDF</a>带认证下载(受保护文件)
Section titled “带认证下载(受保护文件)”// 获取下载 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¢er
# 质量(0-100)?thumb=200x200&quality=80
# 输出格式?thumb=200x200&format=webpSDK 生成缩略图 URL
Section titled “SDK 生成缩略图 URL”// 原始尺寸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",});替换现有文件
Section titled “替换现有文件”const formData = new FormData();formData.append("cover", newFile);// 其他字段也需要传递,否则会被清空formData.append("title", record.title);
const updated = await pb.collection("posts").update(record.id, formData);添加新文件到多文件字段
Section titled “添加新文件到多文件字段”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);删除特定文件
Section titled “删除特定文件”// 清空所有文件const formData = new FormData();formData.append("cover", null); // 或 deleteawait pb.collection("posts").update(record.id, formData);
// JS SDK 方式await pb.collection("posts").update(record.id, { cover: null,});删除记录时自动删除文件
Section titled “删除记录时自动删除文件”// 删除记录会自动删除关联的所有文件await pb.collection("posts").delete(record.id);删除孤文件(清理未引用文件)
Section titled “删除孤文件(清理未引用文件)”PocketBase 会在一定时间后自动清理未引用的文件。也可以手动触发:
# 使用 pocketbase clean 命令./pocketbase clean --days 7文件存储位置
Section titled “文件存储位置”本地存储结构
Section titled “本地存储结构”pb_data/├── storage/│ ├── pbc_1234567890/ # 集合 ID│ │ ├── record_id_1/│ │ │ ├── file1.jpg│ │ │ └── file2.pdf│ │ └── record_id_2/│ │ └── document.pdf│ └── pbc_9876543210/ # 另一个集合│ └── ...└── data.db # SQLite 数据库备份注意事项
Section titled “备份注意事项”备份时需要同时备份:
pb_data/data.db- 数据库文件pb_data/storage/- 所有存储的文件
# 创建完整备份tar -czf pocketbase_backup_$(date +%Y%m%d).tar.gz pb_data/文件大小限制
Section titled “文件大小限制”PocketBase 默认限制:
- 单文件最大:5MB
- 单次请求最大:10MB
修改启动命令的环境变量:
# 设置最大文件大小(字节)POCKETBASE_MAX_FILE_SIZE=10485760 ./pocketbase serve
# 设置最大请求体大小POCKETBASE_MAX_BODY_SIZE=104857600 ./pocketbase serve前端集成示例
Section titled “前端集成示例”Vue 3 文件上传组件
Section titled “Vue 3 文件上传组件”<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>React 图片上传预览
Section titled “React 图片上传预览”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> );}Q: 如何处理大文件上传?
Section titled “Q: 如何处理大文件上传?”解决方案:
- 分块上传(需要自定义实现)
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, });}- 使用外部存储:将大文件上传到 OSS/S3,在 PocketBase 中存储 URL
Q: 如何实现图片裁剪上传?
Section titled “Q: 如何实现图片裁剪上传?”// 使用 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);}Q: 如何实现拖拽上传?
Section titled “Q: 如何实现拖拽上传?”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); } });}Q: 如何处理文件上传错误?
Section titled “Q: 如何处理文件上传错误?”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); }}- 文件类型验证:在后端设置允许的文件类型
- 文件大小限制:根据业务需求设置合理的上限
- 病毒扫描:生产环境建议对上传文件进行扫描
- 敏感文件保护:使用 Protected 字段保护敏感文件
- URL 签名:对受保护文件使用临时签名 URL
- 定期清理:定期清理孤文件释放存储空间