$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())