Files
2025-11-30 09:06:02 +08:00

15 KiB

File Handling in PocketBase

Overview

PocketBase provides comprehensive file handling capabilities:

  • Single and multi-file uploads
  • Automatic image thumbnail generation
  • File type restrictions
  • Size limits
  • Public and private file access
  • CDN integration support
  • Image resizing and optimization

File Fields

Add file fields to collections via the Admin UI or API:

{
  "name": "avatar",
  "type": "file",
  "options": {
    "maxSelect": 1,
    "maxSize": 10485760,
    "mimeTypes": ["image/*"],
    "thumbs": ["100x100", "300x300"]
  }
}

File Field Options

maxSelect

Maximum number of files allowed:

  • 1 - Single file upload
  • null or 2+ - Multiple files
"maxSelect": 5  // Allow up to 5 files

maxSize

Maximum file size in bytes:

"maxSize": 10485760  // 10MB

// Common sizes:
5MB = 5242880
10MB = 10485760
50MB = 52428800
100MB = 104857600

mimeTypes

Allowed MIME types (array):

// Images only
"mimeTypes": ["image/jpeg", "image/png", "image/gif"]

// Images and videos
"mimeTypes": ["image/*", "video/*"]

// Any file type
"mimeTypes": ["*"]

// Specific types
"mimeTypes": [
  "image/jpeg",
  "image/png",
  "application/pdf",
  "text/csv"
]

thumbs

Auto-generate image thumbnails:

"thumbs": [
  "100x100",    // Small square
  "300x300",    // Medium square
  "800x600",    // Large thumbnail
  "1200x800"    // Extra large
]

// Formats:
// WIDTHxHEIGHT - exact size, may crop
// WIDTHx - width only, maintain aspect ratio
// xHEIGHT - height only, maintain aspect ratio

Uploading Files

Single File Upload

const formData = new FormData();
formData.append('avatar', fileInput.files[0]);

const user = await pb.collection('users').update('USER_ID', formData);

// Access file URL
const avatarUrl = pb.files.getURL(user, user.avatar);
console.log(avatarUrl);

Multiple File Upload

const formData = new FormData();

// Add multiple files
formData.append('images', fileInput.files[0]);
formData.append('images', fileInput.files[1]);
formData.append('images', fileInput.files[2]);

const post = await pb.collection('posts').update('POST_ID', formData);

// Access all files
post.images.forEach(image => {
  const url = pb.files.getURL(post, image);
  console.log(url);
});

Upload with Metadata

const formData = new FormData();
formData.append('document', fileInput.files[0], {
  filename: 'custom-name.pdf',  // Custom filename
  type: 'application/pdf',
  lastModified: Date.now()
});

const record = await pb.collection('documents').update('DOC_ID', formData);

File URLs

Get File URL

// Basic URL
const url = pb.files.getURL(record, record.avatar);

// With thumbnail
const thumbnailUrl = pb.files.getURL(
  record,
  record.avatar,
  { thumb: '300x300' }
);

// Custom options
const url = pb.files.getURL(
  record,
  record.avatar,
  {
    thumb: '100x100',
    expires: 3600  // URL expires in 1 hour (for private files)
  }
);

URL Parameters

For public files:

// Direct access (public files only)
const url = pb.files.getURL(record, record.avatar);
// Returns: http://localhost:8090/api/files/COLLECTION_ID/RECORD_ID/filename.jpg

For private files:

// Temporary signed URL (1 hour expiry)
const url = pb.files.getURL(record, record.avatar, { expires: 3600 });
// Returns: http://localhost:8090/api/files/COLLECTION_ID/RECORD_ID/filename.jpg?token=SIGNED_TOKEN

Thumbnail URLs:

// Automatic thumbnail
const thumbUrl = pb.files.getURL(record, record.avatar, {
  thumb: '300x300'
});
// Returns: thumbnail if available

File Access Control

Public Files

Default behavior - anyone with URL can access:

// File is publicly accessible
const url = pb.files.getURL(record, record.avatar);
// Can be shared and accessed by anyone

Private Files

Restrict access to authenticated users:

1. Configure in Admin UI

  • Go to Collection → File field options
  • Enable "Private files"
  • Set file rules (e.g., user_id = @request.auth.id)

2. Use signed URLs

// Generate signed URL (expires)
const signedUrl = pb.files.getURL(record, record.avatar, {
  expires: 3600  // Expires in 1 hour
});

// Use signed URL in frontend
<img src={signedUrl} alt="Avatar" />

3. Access files with auth token

// Include auth token in requests
const response = await fetch(signedUrl, {
  headers: {
    'Authorization': `Bearer ${pb.authStore.token}`
  }
});

File Rules

Control who can upload/view/delete files:

// Owner can only access their files
File Rule: user_id = @request.auth.id

// Public read, authenticated write
List Rule: true
View Rule: true
Create Rule: @request.auth.id != ""
Update Rule: user_id = @request.auth.id
Delete Rule: user_id = @request.auth.id

// Admins only
Create Rule: @request.auth.role = "admin"
Update Rule: @request.auth.role = "admin"
Delete Rule: @request.auth.role = "admin"

Download Files

Browser Download

// Download via browser
const link = document.createElement('a');
link.href = pb.files.getURL(record, record.document);
link.download = record.document;
link.click();

Programmatic Download

// Fetch file as blob
const blob = await pb.files.download(record, record.document);

// Or with fetch
const response = await fetch(pb.files.getURL(record, record.document));
const blob = await response.blob();

// Save file
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = record.document;
a.click();

Deleting Files

Delete Single File

// Remove file from record
const updated = await pb.collection('users').update('USER_ID', {
  avatar: null  // Remove avatar
});

Delete Multiple Files

// Remove specific files from array
const updated = await pb.collection('posts').update('POST_ID', {
  images: record.images.filter(img => img !== imageToRemove)
});

Delete File on Record Delete

Files are automatically deleted when record is deleted:

await pb.collection('posts').delete('POST_ID');
// All associated files are removed automatically

Image Thumbnails

Automatic Thumbnails

Define in file field options:

{
  "name": "images",
  "type": "file",
  "options": {
    "maxSelect": 10,
    "maxSize": 10485760,
    "mimeTypes": ["image/*"],
    "thumbs": ["100x100", "300x300", "800x600"]
  }
}

Access Thumbnails

// Get specific thumbnail size
const smallThumb = pb.files.getURL(post, post.images[0], {
  thumb: '100x100'
});

const mediumThumb = pb.files.getURL(post, post.images[0], {
  thumb: '300x300'
});

// Auto-select best thumbnail
const thumb = pb.files.getURL(post, post.images[0], {
  thumb: '300x300'  // Returns thumbnail or original if not available
});

Thumbnail Formats

  • WxH - Crop to exact dimensions
  • Wx - Width only, maintain aspect ratio
  • xH - Height only, maintain aspect ratio
  • Wx0 - Width, no height limit
  • 0xH - Height, no width limit

Frontend Integration

React Image Component

import { useState } from 'react';

function ImageUpload() {
  const [file, setFile] = useState(null);
  const [uploadedUrl, setUploadedUrl] = useState('');

  const handleUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const formData = new FormData();
    formData.append('avatar', file);

    const updated = await pb.collection('users').update('USER_ID', formData);
    setUploadedUrl(pb.files.getURL(updated, updated.avatar));
  };

  return (
    <div>
      <input type="file" onChange={handleUpload} />
      {uploadedUrl && <img src={uploadedUrl} alt="Avatar" />}
    </div>
  );
}

Vue.js File Upload

<template>
  <div>
    <input type="file" @change="handleUpload" />
    <img v-if="uploadedUrl" :src="uploadedUrl" alt="Avatar" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      uploadedUrl: ''
    }
  },
  methods: {
    async handleUpload(e) {
      const file = e.target.files[0];
      if (!file) return;

      const formData = new FormData();
      formData.append('avatar', file);

      const updated = await pb.collection('users').update('USER_ID', formData);
      this.uploadedUrl = pb.files.getURL(updated, updated.avatar);
    }
  }
}
</script>

Vanilla JavaScript

<input type="file" id="fileInput" />
<img id="preview" />

<script>
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');

fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  const formData = new FormData();
  formData.append('avatar', file);

  const updated = await pb.collection('users').update('USER_ID', formData);
  const avatarUrl = pb.files.getURL(updated, updated.avatar);

  preview.src = avatarUrl;
});
</script>

File Validation

Client-Side Validation

function validateFile(file) {
  const maxSize = 10485760; // 10MB
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

  if (file.size > maxSize) {
    alert('File too large. Max size is 10MB.');
    return false;
  }

  if (!allowedTypes.includes(file.type)) {
    alert('Invalid file type. Only images allowed.');
    return false;
  }

  return true;
}

// Usage
fileInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (validateFile(file)) {
    // Proceed with upload
  }
});

Server-Side Validation

Configure in file field options:

  • Max file size
  • Allowed MIME types
  • File access rules

CDN Integration

Using External CDN

// PocketBase behind CDN
const pb = new PocketBase('https://cdn.yoursite.com');

// Or proxy files through CDN
const cdnUrl = `https://cdn.yoursite.com${pb.files.getURL(record, record.avatar)}`;

Cloudflare R2 / AWS S3

PocketBase can work with S3-compatible storage:

// In production config
export default {
  dataDir: '/path/to/data',
  // S3 configuration
  s3: {
    endpoint: 'https://s3.amazonaws.com',
    bucket: 'your-bucket',
    region: 'us-east-1',
    accessKey: 'YOUR_KEY',
    secretKey: 'YOUR_SECRET'
  }
}

File Storage Locations

Local Storage

Default - files stored in pb_data/db/files/:

pb_data/
  db/
    files/
      collection_id/
        record_id/
          filename1.jpg
          filename2.png

Cloud Storage

Configure in pocketbase.js config:

import PocketBase from 'pocketbase';

const pb = new PocketBase('http://127.0.0.1:8090', {
  files: {
    // S3 or S3-compatible
    endpoint: 'https://your-s3-endpoint',
    bucket: 'your-bucket',
    region: 'your-region',
    accessKey: 'your-access-key',
    secretKey: 'your-secret-key'
  }
});

File Metadata

Access File Information

const post = await pb.collection('posts').getOne('POST_ID');

// File objects contain:
{
  "@collectionId": "...",
  "@collectionName": "...",
  "id": "file-id",
  "name": "filename.jpg",
  "title": "Original filename",
  "size": 1048576,           // File size in bytes
  "type": "image/jpeg",       // MIME type
  "width": 1920,              // Image width (if image)
  "height": 1080,             // Image height (if image)
  "created": "2024-01-01T00:00:00.000Z",
  "updated": "2024-01-01T00:00:00.000Z"
}

Custom File Metadata

Store additional file information:

// When uploading
const formData = new FormData();
formData.append('document', file);
formData.append('description', 'My document');  // Custom field

const record = await pb.collection('documents').create(formData);

// Access later
console.log(record.description);

Progress Tracking

Upload with Progress

function uploadWithProgress(file, onProgress) {
  const xhr = new XMLHttpRequest();

  xhr.upload.addEventListener('progress', (e) => {
    if (e.lengthComputable) {
      const percentComplete = (e.loaded / e.total) * 100;
      onProgress(percentComplete);
    }
  });

  xhr.addEventListener('load', async () => {
    if (xhr.status === 200) {
      const response = JSON.parse(xhr.responseText);
      // Handle success
    }
  });

  const formData = new FormData();
  formData.append('avatar', file);

  xhr.open('PATCH', `${pb.baseUrl}/api/collections/users/records/USER_ID`);
  xhr.send(formData);
}

// Usage
uploadWithProgress(file, (progress) => {
  console.log(`Upload progress: ${progress}%`);
});

Security Best Practices

1. Set File Size Limits

"maxSize": 10485760  // 10MB

2. Restrict MIME Types

"mimeTypes": ["image/jpeg", "image/png"]  // Specific types only

3. Use Private Files for Sensitive Data

  • Enable "Private files" option
  • Use signed URLs with expiration
  • Implement proper file rules

4. Validate File Content

// Check file type
if (!file.type.startsWith('image/')) {
  throw new Error('Only images allowed');
}

// Check file extension
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif'];
if (!validExtensions.some(ext => file.name.toLowerCase().endsWith(ext))) {
  throw new Error('Invalid file extension');
}

5. Sanitize Filenames

// Remove special characters
const sanitizedName = file.name.replace(/[^a-zA-Z0-9.]/g, '_');

// Generate unique filename
const uniqueName = `${Date.now()}_${sanitizedName}`;

6. Implement File Rules

// Only owners can upload
File Rule: user_id = @request.auth.id

// Public read, authenticated write
File Rule: @request.auth.id != ""

7. Monitor File Usage

  • Track storage usage
  • Monitor for abuse
  • Set up alerts for unusual activity

Common Use Cases

User Avatars

{
  "name": "avatar",
  "type": "file",
  "options": {
    "maxSelect": 1,
    "maxSize": 5242880,
    "mimeTypes": ["image/*"],
    "thumbs": ["100x100", "300x300"]
  }
}

Document Storage

{
  "name": "documents",
  "type": "file",
  "options": {
    "maxSelect": 10,
    "maxSize": 52428800,
    "mimeTypes": ["application/pdf", "text/*", "application/msword"]
  }
}

Product Images

{
  "name": "images",
  "type": "file",
  "options": {
    "maxSelect": 10,
    "maxSize": 10485760,
    "mimeTypes": ["image/*"],
    "thumbs": ["300x300", "800x800"]
  }
}
{
  "name": "media",
  "type": "file",
  "options": {
    "maxSelect": 50,
    "maxSize": 104857600,
    "mimeTypes": ["image/*", "video/*"]
  }
}

Troubleshooting

Upload fails with 413 (Payload Too Large)

  • File exceeds maxSize limit
  • Increase maxSize in field options
  • Or split large file into smaller chunks

File type rejected

  • Check mimeTypes in field options
  • Verify actual file type (not just extension)
  • Update allowed types

Private file returns 403

  • Ensure user is authenticated
  • Use signed URL with expiration
  • Check file rules allow access

Thumbnail not generating

  • Verify file is an image
  • Check thumbs array in field options
  • Ensure PocketBase has GD/ImageMagick extension

Slow file uploads

  • Check network connection
  • Reduce file size
  • Use CDN for large files
  • Enable compression