Skip to main content
PocketBase provides comprehensive file system utilities through the $filesystem and $os namespaces for managing files, uploads, and storage.

Creating files

From local path

// Create file from local filesystem
const file = $filesystem.fileFromPath("/path/to/document.pdf")

// Attach to record
const record = $app.findRecordById("posts", "RECORD_ID")
record.set("attachment", file)
$app.save(record)

From bytes

// Create file from byte array
const content = toBytes("Hello, World!")
const file = $filesystem.fileFromBytes(content, "hello.txt")

const record = new Record($app.findCollectionByNameOrId("documents"))
record.set("file", file)
$app.save(record)

From URL

// Download and create file from URL
const file = $filesystem.fileFromURL("https://example.com/image.jpg")

// With custom timeout (default is 120 seconds)
const file = $filesystem.fileFromURL("https://example.com/large-file.zip", 300)

const record = new Record($app.findCollectionByNameOrId("downloads"))
record.set("file", file)
$app.save(record)

From multipart upload

routerAdd("POST", "/api/upload", (e) => {
  const form = e.request.formData
  const uploadedFile = form.file
  
  if (!uploadedFile) {
    throw new BadRequestError("No file uploaded")
  }
  
  const file = $filesystem.fileFromMultipart(uploadedFile)
  
  const record = new Record($app.findCollectionByNameOrId("uploads"))
  record.set("name", uploadedFile.name)
  record.set("file", file)
  $app.save(record)
  
  return e.json(200, {success: true, record: record})
}, $apis.requireAuth())

File storage backends

PocketBase supports local and S3-compatible storage backends.

Local storage

// Create local filesystem instance
const fs = $filesystem.local("/path/to/storage")

// Upload file
const file = $filesystem.fileFromPath("/path/to/source.jpg")
fs.upload(file, "images/photo.jpg")

// Check if file exists
const exists = fs.exists("images/photo.jpg")
console.log("File exists:", exists)

// Get file attributes
const attrs = fs.attributes("images/photo.jpg")
console.log("Size:", attrs.size)
console.log("Modified:", attrs.modTime)

// Delete file
fs.delete("images/photo.jpg")

S3 storage

// Create S3 filesystem instance
const fs = $filesystem.s3(
  "my-bucket",           // bucket name
  "us-east-1",           // region
  "access-key-id",       // access key
  "secret-access-key",   // secret key
  "https://s3.amazonaws.com", // endpoint
  false                  // force path style
)

// Upload to S3
const file = $filesystem.fileFromPath("/path/to/image.jpg")
fs.upload(file, "uploads/image.jpg")

// Generate public URL
const url = fs.url("uploads/image.jpg")
console.log("Public URL:", url)

Record file operations

Get file URLs

const record = $app.findRecordById("posts", "RECORD_ID")
const filename = record.getString("featured_image")

if (filename) {
  // Get original file URL
  const url = $app.fileUrl(record, filename)
  
  // Get thumbnail URL (if thumbs are configured)
  const thumbUrl = $app.fileUrl(record, filename, "200x200")
  
  console.log("Original:", url)
  console.log("Thumbnail:", thumbUrl)
}

Upload files in hooks

onRecordAfterCreateSuccess((e) => {
  if (e.collection.name === "posts") {
    const record = e.record
    
    // Download featured image from external source
    try {
      const imageUrl = record.getString("external_image_url")
      if (imageUrl) {
        const file = $filesystem.fileFromURL(imageUrl, 60)
        record.set("featured_image", file)
        $app.save(record)
      }
    } catch (err) {
      console.log("Failed to download image:", err.message)
    }
  }
  return e.next()
}, "posts")

Delete files

onRecordAfterDeleteSuccess((e) => {
  if (e.collection.name === "posts") {
    const record = e.record
    const filename = record.getString("featured_image")
    
    if (filename) {
      // Get collection filesystem
      const fs = $app.newFilesystem()
      const path = record.collection().id + "/" + record.id + "/" + filename
      
      try {
        fs.delete(path)
        console.log("Deleted file:", path)
      } catch (err) {
        console.log("Failed to delete file:", err.message)
      }
    }
  }
  return e.next()
}, "posts")

OS file operations

The $os namespace provides low-level file system operations.

Read files

// Read entire file
const content = $os.readFile("/path/to/config.json")
const config = JSON.parse(toString(content))

console.log("Config:", config)

Write files

// Write to file
const data = JSON.stringify({setting: "value"}, null, 2)
$os.writeFile("/path/to/output.json", toBytes(data), 0644)

Directory operations

// Read directory contents
const entries = $os.readDir("/path/to/directory")

for (let entry of entries) {
  console.log(entry.name(), entry.isDir() ? "[DIR]" : "[FILE]")
}

// Create directory
$os.mkdir("/path/to/newdir", 0755)

// Create directory with parents
$os.mkdirAll("/path/to/nested/dirs", 0755)

// Remove directory
$os.remove("/path/to/dir")

// Remove directory and contents
$os.removeAll("/path/to/dir")

File info

// Get file info
const info = $os.stat("/path/to/file.txt")

console.log("Name:", info.name())
console.log("Size:", info.size())
console.log("IsDir:", info.isDir())
console.log("Mode:", info.mode())
console.log("ModTime:", info.modTime())

File paths

Use $filepath for path manipulation:
// Join paths
const path = $filepath.join("uploads", "images", "photo.jpg")
console.log(path) // "uploads/images/photo.jpg"

// Get directory
const dir = $filepath.dir("/path/to/file.txt")
console.log(dir) // "/path/to"

// Get filename
const base = $filepath.base("/path/to/file.txt")
console.log(base) // "file.txt"

// Get extension
const ext = $filepath.ext("/path/to/file.txt")
console.log(ext) // ".txt"

// Check if absolute
const isAbs = $filepath.isAbs("/path/to/file.txt")
console.log(isAbs) // true

// Get relative path
const rel = $filepath.rel("/a/b", "/a/c/d")
console.log(rel) // "../c/d"

Working with custom storage

Save files to custom location

routerAdd("POST", "/api/custom-upload", (e) => {
  const form = e.request.formData
  const uploadedFile = form.file
  
  if (!uploadedFile) {
    throw new BadRequestError("No file uploaded")
  }
  
  // Save to custom directory
  const customDir = $filepath.join($app.dataDir(), "custom_uploads")
  $os.mkdirAll(customDir, 0755)
  
  const filename = uploadedFile.name
  const filepath = $filepath.join(customDir, filename)
  
  // Read uploaded file content
  const content = toBytes(uploadedFile.reader, uploadedFile.size)
  
  // Write to custom location
  $os.writeFile(filepath, content, 0644)
  
  return e.json(200, {
    success: true,
    filename: filename,
    path: filepath,
  })
}, $apis.requireAuth())

Serve custom files

routerAdd("GET", "/custom/:filename", (e) => {
  const filename = e.request.pathValue("filename")
  
  // Validate filename to prevent directory traversal
  if (filename.includes("..") || filename.includes("/")) {
    throw new BadRequestError("Invalid filename")
  }
  
  const customDir = $filepath.join($app.dataDir(), "custom_uploads")
  const filepath = $filepath.join(customDir, filename)
  
  // Check if file exists
  try {
    $os.stat(filepath)
  } catch (err) {
    throw new NotFoundError("File not found")
  }
  
  // Read file content
  const content = $os.readFile(filepath)
  
  // Send file
  e.response.header().set("Content-Type", "application/octet-stream")
  return e.blob(200, "application/octet-stream", content)
})

Image processing

PocketBase automatically generates thumbnails for images when configured in file fields.

Configure thumbnails

const collection = new Collection({
  name: "photos",
  type: "base",
  fields: [
    {
      name: "image",
      type: "file",
      maxSelect: 1,
      maxSize: 10485760, // 10MB
      mimeTypes: ["image/jpeg", "image/png", "image/webp"],
      thumbs: [
        "100x100",   // Square thumbnail
        "500x500",   // Medium thumbnail
        "1000x0",    // Proportional width
        "0x800",     // Proportional height
      ],
    },
  ],
})

$app.save(collection)

Access thumbnails

const record = $app.findRecordById("photos", "RECORD_ID")
const filename = record.getString("image")

if (filename) {
  // Original image
  const original = $app.fileUrl(record, filename)
  
  // Thumbnails
  const small = $app.fileUrl(record, filename, "100x100")
  const medium = $app.fileUrl(record, filename, "500x500")
  const large = $app.fileUrl(record, filename, "1000x0")
  
  console.log("Original:", original)
  console.log("Small:", small)
  console.log("Medium:", medium)
  console.log("Large:", large)
}

File validation

Validate file type

onRecordCreate((e) => {
  if (e.collection.name === "documents") {
    const file = e.record.get("file")
    
    if (file) {
      const allowedTypes = ["application/pdf", "application/msword"]
      
      // Note: File type validation is typically handled by field configuration
      // This is for additional custom validation
    }
  }
  return e.next()
}, "documents")

Validate file size

onRecordCreate((e) => {
  if (e.collection.name === "uploads") {
    // File size validation is handled by field configuration
    // Additional validation can be added here
  }
  return e.next()
}, "uploads")

Examples

Batch file upload

routerAdd("POST", "/api/batch-upload", (e) => {
  const form = e.request.formData
  const files = form.files || []
  
  if (files.length === 0) {
    throw new BadRequestError("No files uploaded")
  }
  
  const collection = $app.findCollectionByNameOrId("uploads")
  const records = []
  
  $app.runInTransaction((txApp) => {
    for (let uploadedFile of files) {
      const file = $filesystem.fileFromMultipart(uploadedFile)
      
      const record = new Record(collection)
      record.set("name", uploadedFile.name)
      record.set("file", file)
      record.set("uploader", e.requestInfo().auth.id)
      
      txApp.save(record)
      records.push(record)
    }
    
    return null
  })
  
  return e.json(200, {
    success: true,
    count: records.length,
    records: records,
  })
}, $apis.requireAuth())

Generate file report

routerAdd("GET", "/api/reports/files", (e) => {
  const records = arrayOf(new Record())
  
  $app.recordQuery("uploads")
    .orderBy("created DESC")
    .limit(100)
    .all(records)
  
  const report = []
  let totalSize = 0
  
  for (let record of records) {
    const filename = record.getString("file")
    if (filename) {
      // Get file info from storage
      const collection = record.collection()
      const path = collection.id + "/" + record.id + "/" + filename
      
      try {
        const fs = $app.newFilesystem()
        const attrs = fs.attributes(path)
        
        totalSize += attrs.size
        
        report.push({
          id: record.id,
          filename: filename,
          size: attrs.size,
          created: record.getString("created"),
        })
      } catch (err) {
        console.log("File not found:", path)
      }
    }
  }
  
  return e.json(200, {
    files: report.length,
    totalSize: totalSize,
    totalSizeMB: Math.round(totalSize / 1024 / 1024 * 100) / 100,
    items: report,
  })
}, $apis.requireAuth())