--- title: Always Close the Filesystem Handle Returned by NewFilesystem impact: HIGH impactDescription: Leaked filesystem clients keep S3 connections and file descriptors open until the process exits tags: filesystem, extending, files, s3, NewFilesystem, close --- ## Always Close the Filesystem Handle Returned by NewFilesystem `app.NewFilesystem()` (Go) and `$app.newFilesystem()` (JS) return a filesystem client backed by either the local disk or S3, depending on the app settings. **The caller owns the handle** and must close it - there is no finalizer and no automatic pooling. Leaking handles leaks TCP connections to S3 and file descriptors on disk, and eventually the server will stop accepting uploads. PocketBase also ships a second client: `app.NewBackupsFilesystem()` for the backups bucket/directory, with the same ownership rules. **Incorrect (no close, raw bytes buffered in memory):** ```go // โŒ Forgets to close fs - connection leaks func downloadAvatar(app core.App, key string) ([]byte, error) { fs, err := app.NewFilesystem() if err != nil { return nil, err } // โŒ no defer fs.Close() // โŒ GetFile loads the whole file into a reader; reading it all into a // byte slice defeats streaming for large files r, err := fs.GetFile(key) if err != nil { return nil, err } defer r.Close() return io.ReadAll(r) } ``` **Correct (defer Close, stream to the HTTP response):** ```go func serveAvatar(app core.App, key string) echo.HandlerFunc { return func(e *core.RequestEvent) error { fs, err := app.NewFilesystem() if err != nil { return e.InternalServerError("filesystem init failed", err) } defer fs.Close() // REQUIRED // Serve directly from the filesystem - handles ranges, content-type, // and the X-Accel-Redirect / X-Sendfile headers when available return fs.Serve(e.Response, e.Request, key, "avatar.jpg") } } // Uploading a local file to the PocketBase-managed filesystem func importAvatar(app core.App, record *core.Record, path string) error { f, err := filesystem.NewFileFromPath(path) if err != nil { return err } record.Set("avatar", f) // assignment + app.Save() persist it return app.Save(record) } ``` ```javascript // JSVM - file factories live on the $filesystem global const file1 = $filesystem.fileFromPath("/tmp/import.jpg"); const file2 = $filesystem.fileFromBytes(new Uint8Array([0xff, 0xd8]), "logo.jpg"); const file3 = $filesystem.fileFromURL("https://example.com/a.jpg"); // Assigning to a record field triggers upload on save record.set("avatar", file1); $app.save(record); // Low-level client - MUST be closed const fs = $app.newFilesystem(); try { const list = fs.list("thumbs/"); for (const obj of list) { console.log(obj.key, obj.size); } } finally { fs.close(); // REQUIRED } ``` **Rules:** - `defer fs.Close()` **immediately** after a successful `NewFilesystem()` / `NewBackupsFilesystem()` call (Go). In JS, wrap in `try { ... } finally { fs.close() }`. - Prefer the high-level record-field API (`record.Set("field", file)` + `app.Save`) over direct `fs.Upload` calls - it handles thumbs regeneration, orphan cleanup, and hook integration. - File factory functions (`filesystem.NewFileFromPath`, `NewFileFromBytes`, `NewFileFromURL` / JS `$filesystem.fileFromPath|Bytes|URL`) capture their input; they do not stream until save. - `fileFromURL` performs an HTTP GET and loads the body into memory - not appropriate for large files. - Do not share a single long-lived `fs` across unrelated requests; the object is cheap to create per request. Reference: [Go Filesystem](https://pocketbase.io/docs/go-filesystem/) ยท [JS Filesystem](https://pocketbase.io/docs/js-filesystem/)