Files and OPFS
Hooks and a ready-made component for listing, browsing and managing the files a Seed app stores in the browser's private file system (OPFS).
In the browser, the SDK keeps its SQLite database and its files (uploaded File and Image values and downloads from Arweave) in the browser's private file system (OPFS), under your config's filesDir (filesDir and endpoints says how it resolves). With filesDir: '.seed', the database is .seed/db/seed.db, and files go in subfolders such as .seed/files/, .seed/images/ and .seed/html/.
useFiles and useImageFiles list one folder of that storage through the Seed client and refresh when the SDK saves files there. useOPFSFiles and OPFSFilesManager read OPFS directly, without the Seed client; use them for debugging and for letting people see and clear what the app stores.
import {
useFiles,
FILES_QUERY_KEY_PREFIX,
useImageFiles,
useOPFSFiles,
OPFSFilesManager,
OPFS_FILES_MANAGER_CSS,
useStorageEstimate,
useThumbnail,
} from '@seedprotocol/react'
import type { OPFSFile, OPFSFilesManagerProps, FileEntry } from '@seedprotocol/react'useFiles
Lists the filenames in one folder of the SDK's file storage.
function useFiles(dir?: string): {
files: string[]
isLoading: boolean
error: Error | null
refetch: () => Promise<unknown>
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
dir | string | 'files' | A folder under the SDK's storage root, for example 'files' or 'images'. |
Returns
| Field | Type | Description |
|---|---|---|
files | string[] | Filenames (not paths) of the files directly in dir. Subfolders aren't listed or scanned. [] when the folder doesn't exist or the client isn't ready. |
isLoading | boolean | true during the first fetch. |
error | Error | null | The fetch error, if any. |
refetch | function | React Query's refetch. |
It waits for the client to be ready. Outside SeedProvider it uses a shared fallback query client and development builds log a warning once, so keep it inside the provider. The list refetches when the SDK saves a file into dir, when the SDK's download and resize workers write files there, and when another tab writes files into dir. Writes that bypass the SDK aren't noticed; call refetch after them.
The list gives filenames only. To show a File property's stored file, use the property's value: a blob: URL for the file, after a save and after a page reload. The SDK revokes that URL when the value changes to another file and when the property stops, so read the value again after a change rather than keeping the URL. See File, Image and Html properties.
import { useFiles } from '@seedprotocol/react'
function StoredFiles() {
const { files, isLoading, error, refetch } = useFiles('files')
if (isLoading) return <p>Loading…</p>
if (error) return <p>{error.message}</p>
return (
<>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{files.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
</>
)
}FILES_QUERY_KEY_PREFIX
The start of useFiles' React Query keys: ['seed', 'files']. Each folder's key is [...FILES_QUERY_KEY_PREFIX, dir]. Invalidate the prefix to refresh every useFiles list:
queryClient.invalidateQueries({ queryKey: FILES_QUERY_KEY_PREFIX })useImageFiles
Lists the original images in the SDK's images/ folder. The resized copies in images/<width>/ aren't listed.
function useImageFiles(): {
imageFiles: string[]
isLoading: boolean
error: Error | null
refetch: () => Promise<unknown>
}It returns the same list as useFiles('images'), refreshes on the same events, and behaves the same outside SeedProvider; imageFiles is [] until the client is ready. Its query key is ['seed', 'imageFiles'], which isn't exported and isn't under FILES_QUERY_KEY_PREFIX, so invalidating that prefix doesn't refresh it; call refetch instead.
import { useImageFiles } from '@seedprotocol/react'
function ImageCount() {
const { imageFiles, isLoading } = useImageFiles()
return <p>{isLoading ? '…' : `${imageFiles.length} image(s) stored`}</p>
}useOPFSFiles
Scans OPFS recursively and lists every file with its size and type. It uses the browser API directly and doesn't need the Seed client or SeedProvider.
function useOPFSFiles(options?: { rootPath?: string }): {
files: OPFSFile[]
isLoading: boolean
hasLoaded: boolean
error: string | null
errorName: string | null
refetch: () => Promise<void>
}
interface OPFSFile {
name: string
path: string
size: number
type: string
detectedType?: string
lastModified: number
}OPFSFile and the options type UseOPFSFilesOptions are exported.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
rootPath | string | the OPFS root | A folder to scan instead, relative to the OPFS root, for example '.seed'. |
Returns
| Field | Type | Description |
|---|---|---|
files | OPFSFile[] | Every file under the root, sorted by path. |
isLoading | boolean | true during every scan, including refetches. files keeps the previous result until a scan succeeds. |
hasLoaded | boolean | false until the first scan finishes. |
error | string | null | A message such as 'Failed to access OPFS: …'. A string, not an Error. |
errorName | string | null | The thrown error's name, for example 'NotFoundError' when rootPath doesn't exist. |
refetch | () => Promise<void> | Scan again. |
OPFSFile fields:
| Field | Description |
|---|---|
name | The filename. |
path | The path from the OPFS root, for example '.seed/images/photo.jpg'. |
size | Size in bytes. |
type | The MIME type the browser infers from the extension, or 'application/octet-stream'. |
detectedType | A type sniffed from the file's first bytes, set only when the browser gave no type, for example for images saved under an Arweave transaction ID with no extension. |
lastModified | Last modified time (ms since epoch). |
The hook scans once when it mounts and when rootPath changes. It doesn't watch for new files, so call refetch after writing. Files that can't be read are skipped with a console warning.
import { useOPFSFiles } from '@seedprotocol/react'
function StorageUsage() {
const { files, hasLoaded, error, refetch } = useOPFSFiles({ rootPath: '.seed' })
if (error) return <p>{error}</p>
if (!hasLoaded) return <p>Scanning…</p>
const total = files.reduce((sum, f) => sum + f.size, 0)
return (
<p>
{files.length} files, {(total / 1024 / 1024).toFixed(1)} MB{' '}
<button onClick={() => refetch()}>Rescan</button>
</p>
)
}OPFSFilesManager
A file browser for OPFS: a folder tree with grid and list views, previews, multi-select, download (several files as a .zip), copy path, and delete with confirmation. It shows the origin's storage usage, and works without the Seed client.
function OPFSFilesManager(props: OPFSFilesManagerProps): JSX.ElementProps
All props are optional.
| Name | Type | Default | Description |
|---|---|---|---|
rootPath | string | the OPFS root | Folder to show, relative to the OPFS root, for example '.seed'. |
filter | (file: OPFSFile) => boolean | Which files to include. | |
title | string | 'Files' | Heading text. |
description | string | a sentence about OPFS | Text under the heading. |
headingLevel | 1–6 | 2 | Heading level for title. |
defaultView | 'grid' | 'list' | by folder | Starting view. By default, folders that are mostly images open as a grid and others as a list, until the user picks a view. |
groupImageVariants | boolean | true | Show the SDK's resized copies (images/<width>/) together with their original instead of as separate files. |
theme | 'system' | 'light' | 'dark' | 'none' | 'system' | 'system' follows the OS. 'none' doesn't inject the stylesheet and renders unstyled markup with seed-fm-* classes. |
className | string | Class for the root element. | |
classNames | Partial<Record<OPFSFilesManagerSlot, string>> | {} | Extra classes per element. Slots: root, header, toolbar, body, folder, grid, tile, list, row, batchBar, panel, dialog, toast. |
onDownload | (file: OPFSFile, blob: Blob) => void | Promise<void> | browser download | Handle downloads yourself, for example in Electron. For several files, it's called once with a .zip blob and a synthetic file whose type is 'application/zip'. |
confirmDelete | (files: OPFSFile[]) => Promise<boolean> | built-in dialog | Replace the confirmation dialog. Receives every file that would be removed, including resized copies. Resolve true to delete. |
deleteWarning | (files: OPFSFile[]) => string | null | warns about the Seed database | Extra warning in the built-in dialog, or null for none. The default warns when the selection includes db/seed.db. |
deleteAction | (files: OPFSFile[]) => OPFSFilesManagerDeleteAction | Relabel delete (label) or make it non-destructive (destructive: false) for a set of files, for example ones that have a copy on Arweave. Runs on every render for every row, so keep it cheap and synchronous. | |
onBeforeDelete | (file: OPFSFile) => boolean | Promise<boolean> | Called for each file after the user confirms. Return false to keep that file. | |
onAfterDelete | (paths: string[]) => void | Promise<void> | Called with the deleted paths. Use it to update app state. | |
onNotify | (message: string, tone: 'success' | 'error', notice: OPFSFilesManagerNotice) => void | built-in toasts | Show results in your own toaster. When set, the built-in toasts aren't shown. notice says what happened (kind: 'delete' | 'download' | 'copy-path') so you can write your own wording. |
The prop and helper types are exported: OPFSFilesManagerProps, OPFSFilesManagerTheme, OPFSFilesManagerView, OPFSFilesManagerSlot, OPFSFilesManagerClassNames, OPFSFilesManagerDeleteAction, OPFSFilesManagerNotice and OPFSFilesManagerNotifyTone.
Deleting here removes files from OPFS only. The Seed database still refers to them, and deleting .seed/db/seed.db throws away every local draft that hasn't been published. Use onAfterDelete to clear or reload app state, and keep the default deleteWarning unless you replace it with something as clear.
The file list is scanned when the component mounts and after each delete. It has a refresh button for files written while it's open. In browsers without OPFS (some private windows, older browsers) it shows a message instead.
Example
import { OPFSFilesManager } from '@seedprotocol/react'
function StoragePage() {
return (
<OPFSFilesManager
rootPath=".seed"
title="Stored files"
theme="system"
onBeforeDelete={(file) => !file.path.endsWith('db/seed.db')}
onAfterDelete={(paths) => {
console.log('Deleted', paths)
}}
onNotify={(message, tone) => {
if (tone === 'error') console.error(message)
}}
/>
)
}Styling
Unless theme is 'none', the component injects its stylesheet into document.head once, inside the CSS layer seed-opfs-files-manager. Because it's in a layer, your own unlayered styles win over it. Every colour comes from a CSS custom property on .seed-fm, such as --seed-bg, --seed-surface, --seed-fg, --seed-border, --seed-accent and --seed-danger. Override them on .seed-fm or an ancestor to restyle:
.seed-fm {
--seed-accent: #6d28d9;
--seed-radius-md: 4px;
}OPFS_FILES_MANAGER_CSS
The component's stylesheet as a string, wrapped in @layer seed-opfs-files-manager { … }. Use it to include the styles in server-rendered HTML, so the component doesn't render unstyled before its effect runs. Give the element the id seed-opfs-files-manager-styles: the component skips injecting when an element with that id is already present.
import { OPFS_FILES_MANAGER_CSS } from '@seedprotocol/react'
export function FilesManagerStyles() {
return <style id="seed-opfs-files-manager-styles">{OPFS_FILES_MANAGER_CSS}</style>
}useStorageEstimate
The storage usage that OPFSFilesManager shows in its header: navigator.storage.estimate() as a hook.
function useStorageEstimate(revision: unknown): StorageEstimate | null
type StorageEstimate = {
usage: number // bytes this origin uses, across OPFS, IndexedDB, Cache Storage and so on
quota: number // bytes this origin may use
}It reads the estimate on mount and again whenever revision changes (compared by identity), so pass something that changes when files do, such as useOPFSFiles().files. It returns null until the first reading, and stays null where navigator.storage.estimate isn't available. Browsers round and pad these numbers. StorageEstimate is exported.
import { useOPFSFiles, useStorageEstimate } from '@seedprotocol/react'
function StorageUsage() {
const { files } = useOPFSFiles({ rootPath: '.seed' })
const estimate = useStorageEstimate(files)
if (!estimate) return null
const mb = (bytes: number) => (bytes / 1024 / 1024).toFixed(1)
return <p>{mb(estimate.usage)} MB of {mb(estimate.quota)} MB used</p>
}useThumbnail
Loads a thumbnail for an image file once its element scrolls near the viewport. OPFSFilesManager uses it for its tiles.
function useThumbnail(entry: FileEntry): {
ref: (el: Element | null) => void
url: string | null
status: ThumbnailStatus // 'idle' | 'loading' | 'ready' | 'error'
markError: () => void
}
type FileEntry = {
file: OPFSFile
variants: { file: OPFSFile; width: number }[] // resized copies, narrowest first
totalSize: number
}| Field | Description |
|---|---|
ref | Attach it to the element that shows the thumbnail. Nothing loads until that element is within 300 px of the viewport |
url | A blob: URL of the thumbnail once status is 'ready', otherwise null |
status | 'idle' until the element is near the viewport, and always for files that aren't images; then 'loading', 'ready' or 'error' |
markError | Sets status to 'error'. Call it from the <img>'s onError |
The thumbnail is the first entry of variants when there is one. Otherwise it's the original, downscaled to 400 px wide (as WebP) when it's 256 KB or larger. Thumbnails are shared between components and decoded at most four at a time. The hook owns the blob: URL and revokes it about 30 seconds after the last component using it unmounts, so don't keep url elsewhere. FileEntry and ThumbnailStatus are exported.
import { useThumbnail, type FileEntry, type OPFSFile } from '@seedprotocol/react'
function Thumbnail({ file }: { file: OPFSFile }) {
const entry: FileEntry = { file, variants: [], totalSize: file.size }
const { ref, url, status, markError } = useThumbnail(entry)
return (
<div ref={ref} style={{ width: 120, height: 120 }}>
{url && <img src={url} alt={file.name} width={120} onError={markError} />}
{status === 'error' && <span>No preview</span>}
</div>
)
}Related
- Images and media:
SeedImageand the media components - Working with file and image properties: how the SDK stores
FileandImagevalues - Multi-tab behavior: one database and file store shared by every tab
- Local-first and offline
Images and media
Components and hooks for showing Image properties stored on the device, and for resolving raw media strings from feeds into images, video, audio, file links, HTML and JSON.
@seedprotocol/vite
The Vite plugin for browser apps that use @seedprotocol/sdk: cross-origin isolation headers, ES module workers, in-order dev pre-bundles, and opt-in Node polyfills and fs aliases.