> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pocketbase/pocketbase/llms.txt
> Use this file to discover all available pages before exploring further.

# File download

> Serve and download files from PocketBase records

PocketBase provides a built-in API endpoint for serving files from records. The system handles content type detection, range requests, caching, and security headers automatically.

## Download endpoint

Files are served through the following endpoint:

```
GET /api/files/{collection}/{recordId}/{filename}
```

### Path parameters

* `collection` - The collection name or ID
* `recordId` - The record ID
* `filename` - The exact filename stored in the record

### Example request

```bash theme={null}
curl https://your-app.com/api/files/users/abc123/avatar_kj3n4k5l6m.png
```

## Query parameters

You can modify the download behavior using query parameters:

### Thumbnail request

Request a specific thumbnail size for image files:

```bash theme={null}
# Get 100x100 thumbnail
GET /api/files/users/abc123/photo_x1y2z3.jpg?thumb=100x100

# Get 200x200 thumbnail cropped from top
GET /api/files/users/abc123/photo_x1y2z3.jpg?thumb=200x200t

# Fit inside 300x300 without cropping
GET /api/files/users/abc123/photo_x1y2z3.jpg?thumb=300x300f
```

<Info>
  Thumbnails are generated on-demand on the first request and cached for subsequent requests. They're stored at `{collection}/{recordId}/thumbs_{filename}/{thumbSize}_{filename}`.
</Info>

### Force download

Force the browser to download the file instead of displaying it inline:

```bash theme={null}
GET /api/files/users/abc123/report.pdf?download=1
```

This sets `Content-Disposition: attachment` instead of `inline`.

### Protected file access

For protected files, include the file token:

```bash theme={null}
GET /api/files/users/abc123/private_doc.pdf?token=eyJhbGc...
```

## Response headers

PocketBase sets appropriate headers for file serving:

```http theme={null}
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: inline; filename=avatar_kj3n4k5l6m.png
Content-Length: 45678
Cache-Control: max-age=2592000, stale-while-revalidate=86400
Content-Security-Policy: default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Last-Modified: Mon, 02 Jan 2006 15:04:05 GMT
```

### Header details

* `Content-Type` - Automatically detected from file content
* `Content-Disposition` - `inline` for viewable files, `attachment` for downloads
* `Cache-Control` - Valid for 30 days with background revalidation
* `Content-Security-Policy` - Restrictive CSP for security
* `ETag` - For cache validation
* `Last-Modified` - File modification timestamp

<Note>
  The `X-Frame-Options` header is removed for file endpoints to allow embedding files in iframes.
</Note>

## Protected files

When a file field has `Protected: true`, users must provide a valid file token to access files.

### Getting a file token

Authenticated users can request a file token:

```http theme={null}
POST /api/files/token
Authorization: Bearer {authToken}
```

Response:

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

### Using the file token

Append the token to file URLs:

```javascript theme={null}
const token = await pb.files.getToken();
const fileUrl = pb.files.getUrl(record, 'document', {'token': token});

// Result:
// https://your-app.com/api/files/docs/xyz789/file.pdf?token=eyJhbG...
```

<Warning>
  File tokens are time-limited. Protected files also respect collection view rules - even with a valid token, users can only access files from records they have permission to view.
</Warning>

## Implementation details

The download endpoint implementation (from apis/file.go:84):

```go theme={null}
func (api *fileApi) download(e *core.RequestEvent) error {
    // Find collection
    collection, err := e.App.FindCachedCollectionByNameOrId(
        e.Request.PathValue("collection"),
    )
    if err != nil {
        return e.NotFoundError("", nil)
    }
    
    // Find record
    recordId := e.Request.PathValue("recordId")
    record, err := e.App.FindRecordById(collection, recordId)
    if err != nil {
        return e.NotFoundError("", err)
    }
    
    // Find file field
    filename := e.Request.PathValue("filename")
    fileField := record.FindFileFieldByFile(filename)
    if fileField == nil {
        return e.NotFoundError("", nil)
    }
    
    // Check authorization for protected files
    if fileField.Protected {
        // Verify token and view permissions
        // ...
    }
    
    // Initialize filesystem
    fsys, err := e.App.NewFilesystem()
    if err != nil {
        return e.InternalServerError("Filesystem initialization failure.", err)
    }
    defer fsys.Close()
    
    // Serve file
    return fsys.Serve(e.Response, e.Request, originalPath, filename)
}
```

## Thumbnail generation

Thumbnails are generated using the filesystem.CreateThumb method (from filesystem.go:489):

```go theme={null}
func (s *System) CreateThumb(
    originalKey string,
    thumbKey string,
    thumbSize string,
) error {
    // Parse thumb size (e.g., "100x100", "200x200t")
    sizeParts := ThumbSizeRegex.FindStringSubmatch(thumbSize)
    width, _ := strconv.Atoi(sizeParts[1])
    height, _ := strconv.Atoi(sizeParts[2])
    resizeType := sizeParts[3] // "", "t", "b", "f"
    
    // Load original image
    r, err := s.GetReader(originalKey)
    if err != nil {
        return err
    }
    defer r.Close()
    
    // Decode image
    img, err := imaging.Decode(r, imaging.AutoOrientation(true))
    if err != nil {
        return err
    }
    
    // Resize based on type
    var thumbImg *image.NRGBA
    switch resizeType {
    case "f": // fit
        thumbImg = imaging.Fit(img, width, height, imaging.Linear)
    case "t": // crop from top
        thumbImg = imaging.Fill(img, width, height, imaging.Top, imaging.Linear)
    case "b": // crop from bottom
        thumbImg = imaging.Fill(img, width, height, imaging.Bottom, imaging.Linear)
    default: // crop from center
        thumbImg = imaging.Fill(img, width, height, imaging.Center, imaging.Linear)
    }
    
    // Upload thumbnail
    // ...
}
```

### Supported image formats

Thumbnail generation works with:

* `image/png`
* `image/jpg` / `image/jpeg`
* `image/gif`
* `image/webp`

<Info>
  WebP images are decoded for thumbnail generation, but the thumbnails are saved as PNG to ensure broad compatibility.
</Info>

## Serving files programmatically

You can serve files in custom endpoints using the filesystem:

```go theme={null}
func customFileEndpoint(e *core.RequestEvent) error {
    // Initialize filesystem
    fsys, err := e.App.NewFilesystem()
    if err != nil {
        return err
    }
    defer fsys.Close()
    
    // Serve file
    fileKey := "custom/path/file.pdf"
    displayName := "document.pdf"
    
    return fsys.Serve(e.Response, e.Request, fileKey, displayName)
}
```

### Getting file reader

For more control, get a file reader:

```go theme={null}
fsys, err := app.NewFilesystem()
if err != nil {
    return err
}
defer fsys.Close()

// Get reader
reader, err := fsys.GetReader("path/to/file.pdf")
if err != nil {
    return err
}
defer reader.Close()

// Access file properties
contentType := reader.ContentType() // "application/pdf"
modTime := reader.ModTime()         // Last modified time
size := reader.Size()               // File size in bytes

// Read content
data, err := io.ReadAll(reader)
```

### Checking file existence

```go theme={null}
fsys, err := app.NewFilesystem()
if err != nil {
    return err
}
defer fsys.Close()

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

if exists {
    // File exists
}
```

### Getting file attributes

```go theme={null}
attrs, err := fsys.Attributes("path/to/file.txt")
if err != nil {
    return err
}

fmt.Println("Content-Type:", attrs.ContentType)
fmt.Println("Size:", attrs.Size)
fmt.Println("Modified:", attrs.ModTime)
fmt.Println("MD5:", attrs.MD5)
fmt.Println("Metadata:", attrs.Metadata)
```

## Range requests

PocketBase supports HTTP range requests for partial file downloads:

```bash theme={null}
# Request first 1024 bytes
curl -H "Range: bytes=0-1023" \
  https://your-app.com/api/files/videos/abc/video_xyz.mp4

# Request from byte 1024 to end
curl -H "Range: bytes=1024-" \
  https://your-app.com/api/files/videos/abc/video_xyz.mp4
```

Response:

```http theme={null}
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Content-Range: bytes 0-1023/45678
Content-Length: 1024
```

<Info>
  Range requests are handled automatically by `http.ServeContent`, enabling video streaming and resumable downloads.
</Info>

## Inline content types

These content types are served with `Content-Disposition: inline` by default:

```go theme={null}
var inlineServeContentTypes = []string{
    // Images
    "image/png", "image/jpg", "image/jpeg", "image/gif",
    "image/webp", "image/x-icon", "image/bmp",
    
    // Videos
    "video/webm", "video/mp4", "video/3gpp",
    "video/quicktime", "video/x-ms-wmv",
    
    // Audio
    "audio/basic", "audio/aiff", "audio/mpeg", "audio/midi",
    "audio/mp3", "audio/wave", "audio/wav", "audio/x-wav",
    "audio/x-mpeg", "audio/x-m4a", "audio/aac",
    
    // Documents
    "application/pdf", "application/x-pdf",
}
```

All other types are served as `attachment` (download) by default.

## Error handling

The download endpoint returns appropriate error responses:

```go theme={null}
// Collection not found
// 404 Not Found

// Record not found
// 404 Not Found

// File not in record
// 404 Not Found

// Protected file without valid token
// 404 Not Found (not 403 to avoid leaking file existence)

// Filesystem error
// 500 Internal Server Error
```

## Complete example

Here's a complete example with protected file download:

```go theme={null}
package main

import (
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()
    
    // Hook into file download to add custom logic
    app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error {
        // Log file access
        app.Logger().Info("File download",
            "collection", e.Collection.Name,
            "record", e.Record.Id,
            "file", e.ServedName,
        )
        
        // Add custom header
        e.Response.Header().Set("X-Custom-Header", "value")
        
        // Continue with default serving
        return e.Next()
    })
    
    if err := app.Start(); err != nil {
        panic(err)
    }
}
```

## Next steps

<CardGroup cols={2}>
  <Card title="File upload" icon="upload" href="/files/upload">
    Learn how to upload files to records
  </Card>

  <Card title="S3 storage" icon="cloud" href="/files/s3-storage">
    Configure cloud storage for scalability
  </Card>
</CardGroup>
