Skip to main content
PocketBase provides a unified filesystem abstraction for managing files both locally and on S3-compatible storage.

Creating filesystem instances

NewFilesystem()

Create a filesystem instance based on app settings:
fs, err := app.NewFilesystem()
if err != nil {
    log.Fatal(err)
}
defer fs.Close()
Always call Close() on the filesystem when you’re done to release resources.

NewBackupsFilesystem()

Create a filesystem for managing backups:
fs, err := app.NewBackupsFilesystem()
if err != nil {
    log.Fatal(err)
}
defer fs.Close()

Local filesystem

Create a local filesystem instance directly:
import "github.com/pocketbase/pocketbase/tools/filesystem"

fs, err := filesystem.NewLocal("./storage")
if err != nil {
    log.Fatal(err)
}
defer fs.Close()

S3 filesystem

Create an S3-compatible filesystem:
import "github.com/pocketbase/pocketbase/tools/filesystem"

fs, err := filesystem.NewS3(
    "my-bucket",           // bucket name
    "us-east-1",           // region
    "s3.amazonaws.com",    // endpoint
    "ACCESS_KEY",          // access key
    "SECRET_KEY",          // secret key
    false,                 // force path style
)
if err != nil {
    log.Fatal(err)
}
defer fs.Close()

S3-compatible services

The S3 filesystem works with any S3-compatible service:
fs, err := filesystem.NewS3(
    "my-bucket",
    "us-east-1",
    "s3.amazonaws.com",
    "ACCESS_KEY",
    "SECRET_KEY",
    false,
)

File operations

Checking file existence

exists, err := fs.Exists("path/to/file.txt")
if err != nil {
    log.Fatal(err)
}

if exists {
    log.Println("File exists")
}

Getting file attributes

attrs, err := fs.Attributes("path/to/file.txt")
if err != nil {
    log.Fatal(err)
}

log.Printf("Size: %d bytes", attrs.Size)
log.Printf("Content-Type: %s", attrs.ContentType)
log.Printf("Modified: %v", attrs.ModTime)

Reading files

reader, err := fs.GetReader("path/to/file.txt")
if err != nil {
    log.Fatal(err)
}
defer reader.Close()

// Read file contents
content, err := io.ReadAll(reader)
if err != nil {
    log.Fatal(err)
}

log.Println("Content:", string(content))

Uploading files

Upload from bytes

import "github.com/pocketbase/pocketbase/tools/filesystem"

content := []byte("Hello, World!")
fileKey := "uploads/hello.txt"

err := fs.UploadFile(&filesystem.File{
    Reader: bytes.NewReader(content),
    Name:   "hello.txt",
    Size:   int64(len(content)),
}, fileKey)

if err != nil {
    log.Fatal(err)
}

Upload from multipart form

app.OnServe().BindFunc(func(e *core.ServeEvent) error {
    e.Router.POST("/upload", func(re *core.RequestEvent) error {
        // Parse multipart form
        file, err := re.Request.FormFile("file")
        if err != nil {
            return re.BadRequestError("", err)
        }
        defer file.Close()
        
        fs, err := re.App.NewFilesystem()
        if err != nil {
            return err
        }
        defer fs.Close()
        
        // Create File from multipart
        uploadedFile, err := filesystem.NewFileFromMultipart(file)
        if err != nil {
            return err
        }
        
        // Upload
        err = fs.UploadFile(uploadedFile, "uploads/"+uploadedFile.Name)
        if err != nil {
            return err
        }
        
        return re.JSON(200, map[string]string{
            "message": "File uploaded successfully",
        })
    })
    
    return e.Next()
})

Deleting files

Delete a single file

err := fs.Delete("path/to/file.txt")
if err != nil {
    log.Fatal(err)
}

Delete multiple files

fileKeys := []string{
    "uploads/file1.txt",
    "uploads/file2.txt",
    "uploads/file3.txt",
}

errors := fs.DeletePrefix("", fileKeys...)
for fileKey, err := range errors {
    log.Printf("Failed to delete %s: %v", fileKey, err)
}

Copying files

err := fs.Copy("source/file.txt", "destination/file.txt")
if err != nil {
    log.Fatal(err)
}

Listing files

attrs, err := fs.List("uploads/")
if err != nil {
    log.Fatal(err)
}

for _, attr := range attrs {
    log.Printf("File: %s (Size: %d)", attr.Key, attr.Size)
}

File struct

The File struct represents a file to be uploaded:
type File struct {
    Reader       io.Reader // File content reader
    Name         string    // File name
    OriginalName string    // Original uploaded name
    Size         int64     // File size in bytes
}

Creating File instances

From multipart form

multipartFile, err := request.FormFile("file")
if err != nil {
    log.Fatal(err)
}

file, err := filesystem.NewFileFromMultipart(multipartFile)
if err != nil {
    log.Fatal(err)
}

From bytes

import "github.com/pocketbase/pocketbase/tools/filesystem"

content := []byte("file content")

file := &filesystem.File{
    Reader: bytes.NewReader(content),
    Name:   "document.txt",
    Size:   int64(len(content)),
}

From existing file

f, err := os.Open("local-file.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

info, _ := f.Stat()

file := &filesystem.File{
    Reader: f,
    Name:   info.Name(),
    Size:   info.Size(),
}

Working with record files

Uploading files to records

app.OnServe().BindFunc(func(e *core.ServeEvent) error {
    e.Router.POST("/posts/:id/upload", func(re *core.RequestEvent) error {
        record, err := re.App.FindRecordById("posts", re.Request.PathValue("id"))
        if err != nil {
            return re.NotFoundError("", err)
        }
        
        // Get uploaded file
        multipartFile, err := re.Request.FormFile("document")
        if err != nil {
            return re.BadRequestError("", err)
        }
        defer multipartFile.Close()
        
        // Convert to filesystem.File
        file, err := filesystem.NewFileFromMultipart(multipartFile)
        if err != nil {
            return err
        }
        
        // Assign to record
        record.Set("document", file)
        
        // Save record (file is uploaded automatically)
        if err := re.App.Save(record); err != nil {
            return err
        }
        
        return re.JSON(200, record)
    })
    
    return e.Next()
})

Accessing record files

record, _ := app.FindRecordById("posts", "RECORD_ID")

// Get file names
filenames := record.GetStringSlice("documents")

for _, filename := range filenames {
    log.Println("File:", filename)
}

Deleting record files

Files are automatically deleted when you remove them from a record:
record, _ := app.FindRecordById("posts", "RECORD_ID")

// Remove all files
record.Set("documents", []string{})

// Remove specific file
filenames := record.GetStringSlice("documents")
filtered := []string{}
for _, name := range filenames {
    if name != "file-to-delete.pdf" {
        filtered = append(filtered, name)
    }
}
record.Set("documents", filtered)

app.Save(record) // Files are deleted automatically

File URLs

Generate URLs for accessing record files:
record, _ := app.FindRecordById("posts", "RECORD_ID")
filename := record.GetStringSlice("documents")[0]

// Build file URL
url := fmt.Sprintf(
    "%s/api/files/%s/%s/%s",
    app.Settings().Meta.AppURL,
    record.Collection().Name,
    record.Id,
    filename,
)

log.Println("File URL:", url)

Image transformations

PocketBase automatically generates thumbnails for images:
// Original image URL
originalURL := fmt.Sprintf(
    "%s/api/files/%s/%s/%s",
    baseURL, collectionName, recordId, filename,
)

// Thumbnail URL (100x100)
thumbURL := fmt.Sprintf(
    "%s/api/files/%s/%s/%s?thumb=100x100",
    baseURL, collectionName, recordId, filename,
)
Supported thumbnail formats:
  • 100x100 - Exact size with crop
  • 100x100f - Fit within bounds
  • 100x0 - Width only, auto height
  • 0x100 - Height only, auto width

File tokens

Generate temporary tokens for protected files:
record, _ := app.FindRecordById("posts", "RECORD_ID")

// Generate file token (valid for 2 minutes)
token, err := record.NewFileToken()
if err != nil {
    log.Fatal(err)
}

// URL with token
url := fmt.Sprintf(
    "%s/api/files/%s/%s/%s?token=%s",
    baseURL, collectionName, recordId, filename, token,
)

Context management

Set a custom context for filesystem operations:
import "context"

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

fs, _ := app.NewFilesystem()
defer fs.Close()

fs.SetContext(ctx)

// Operations will use the custom context
err := fs.UploadFile(file, "uploads/large-file.zip")

Best practices

Always close filesystems

fs, err := app.NewFilesystem()
if err != nil {
    return err
}
defer fs.Close() // Important!

Validate file types

file, _ := filesystem.NewFileFromMultipart(multipartFile)

allowedTypes := []string{"image/jpeg", "image/png", "image/gif"}
if !slices.Contains(allowedTypes, file.MimeType()) {
    return errors.New("invalid file type")
}

Handle large files

const maxFileSize = 10 * 1024 * 1024 // 10MB

file, _ := filesystem.NewFileFromMultipart(multipartFile)

if file.Size > maxFileSize {
    return errors.New("file too large")
}

Use transactions for file operations

err := app.RunInTransaction(func(txApp core.App) error {
    // Create record
    record := core.NewRecord(collection)
    record.Set("document", file)
    
    if err := txApp.Save(record); err != nil {
        return err // Rollback (file won't be uploaded)
    }
    
    return nil // Commit (file is uploaded)
})

Error handling

Check for not found errors

import "github.com/pocketbase/pocketbase/tools/filesystem"

reader, err := fs.GetReader("path/to/file.txt")
if err != nil {
    if errors.Is(err, filesystem.ErrNotFound) {
        log.Println("File not found")
    } else {
        log.Fatal(err)
    }
}