FileSystem Adapter
The FileSystem adapter provides Node.js filesystem-based storage for the content system. It allows reading from and writing to the local filesystem with robust path handling and content transformation.
Overview
The FileSystem adapter is designed for server-side Node.js environments, providing a straightforward way to integrate with existing file-based content. It supports path normalization, directory creation, and watching for file changes.
import { createFileSystemAdapter } from '@lib/content/adapters/node/filesystem'
const adapter = createFileSystemAdapter({
basePath: '/path/to/content',
})
API Reference
Creation
function createFileSystemAdapter(
options?: FileSystemAdapterOptions
): ContentAdapter
Creates a new FileSystem adapter instance.
Options
interface FileSystemAdapterOptions {
/**
* Base directory for content files (required)
*/
basePath: string
/**
* File encoding for text files (default: 'utf8')
*/
encoding?: BufferEncoding
/**
* Whether to create directories automatically (default: true)
*/
createDirectories?: boolean
/**
* Normalize file paths (default: true)
*/
normalizePaths?: boolean
/**
* Override content type detection (optional)
*/
contentTypeDetector?: (path: string) => string
/**
* Watch for file changes (default: false)
*/
watch?: boolean
/**
* File watch options (optional)
*/
watchOptions?: WatchOptions
}
interface WatchOptions {
/**
* Polling interval in milliseconds (default: 1000)
*/
interval?: number
/**
* Whether to use polling instead of fs.watch (default: false)
*/
usePolling?: boolean
/**
* Ignore patterns (default: [])
*/
ignored?: string[] | RegExp
/**
* Whether to ignore initial scan (default: false)
*/
ignoreInitial?: boolean
}
Methods
read
async read(uri: string): Promise<Content<string>>;
Reads content from the filesystem.
Parameters:
uri
: Content URI
Returns:
- A Promise resolving to a Content object with string data
Example:
const content = await adapter.read('articles/welcome.md')
console.log(content.data) // File contents as string
console.log(content.metadata) // Extracted metadata
Implementation Details:
- Resolves URI to an absolute filesystem path
- Reads file contents using
fs.promises.readFile
- Determines content type based on file extension
- Extracts metadata from file attributes and content
write
async write(uri: string, content: Content<string>): Promise<void>;
Writes content to the filesystem.
Parameters:
uri
: Content URIcontent
: Content object to write
Example:
await adapter.write('articles/welcome.md', {
data: '# Welcome\n\nThis is a welcome article.',
contentType: 'text/markdown',
metadata: {
title: 'Welcome',
createdAt: new Date(),
},
})
Implementation Details:
- Resolves URI to an absolute filesystem path
- Creates parent directories if they don’t exist
- Writes file contents using
fs.promises.writeFile
- Handles metadata by storing it in the content or externally
delete
async delete(uri: string): Promise<void>;
Deletes content from the filesystem.
Parameters:
uri
: Content URI
Example:
await adapter.delete('articles/outdated-article.md')
Implementation Details:
- Resolves URI to an absolute filesystem path
- Deletes file using
fs.promises.unlink
- Handles errors for non-existent files
list
async list(pattern: string): Promise<string[]>;
Lists content URIs matching the pattern.
Parameters:
pattern
: Glob pattern for content URIs
Returns:
- A Promise resolving to an array of content URIs
Example:
const uris = await adapter.list('articles/**/*.md')
console.log(uris) // ['articles/welcome.md', 'articles/guide.md', ...]
Implementation Details:
- Converts pattern to filesystem glob
- Uses
glob
or similar libraries to find matching files - Converts absolute paths back to content URIs
exists
async exists(uri: string): Promise<boolean>;
Checks if content exists.
Parameters:
uri
: Content URI
Returns:
- A Promise resolving to a boolean indicating existence
Example:
const exists = await adapter.exists('articles/welcome.md')
if (exists) {
console.log('Article exists')
}
Implementation Details:
- Resolves URI to an absolute filesystem path
- Uses
fs.promises.access
to check file existence - Handles errors for permissions and non-existent files
watch
watch(pattern: string, listener: WatchListener): Unsubscribe;
Watches for content changes.
Parameters:
pattern
: Glob pattern for content URIs to watchlistener
: Callback function for change events
Returns:
- An unsubscribe function to stop watching
Example:
const unsubscribe = adapter.watch('articles/**/*.md', (uri, changeType) => {
console.log(`File ${uri} was ${changeType}`)
})
// Later, stop watching
unsubscribe()
Implementation Details:
- Uses
chokidar
or similar libraries for file watching - Normalizes file system events to content change types
- Provides a way to unsubscribe from events
createDirectory
async createDirectory(uri: string): Promise<void>;
Creates a directory.
Parameters:
uri
: Content URI for the directory
Example:
await adapter.createDirectory('articles/series-a')
Implementation Details:
- Resolves URI to an absolute filesystem path
- Creates directory using
fs.promises.mkdir
with recursive option - Handles existing directories gracefully
getMetadata
async getMetadata(uri: string): Promise<ContentMetadata>;
Gets content metadata without loading the full content.
Parameters:
uri
: Content URI
Returns:
- A Promise resolving to content metadata
Example:
const metadata = await adapter.getMetadata('articles/welcome.md')
console.log(metadata.updatedAt) // File modification time
Implementation Details:
- Resolves URI to an absolute filesystem path
- Uses
fs.promises.stat
to get file information - Maps file stats to content metadata properties
dispose
async dispose(): Promise<void>;
Cleans up resources used by the adapter.
Example:
await adapter.dispose()
Implementation Details:
- Closes file watchers if active
- Releases any other resources
Path Resolution
The FileSystem adapter normalizes paths between URIs and filesystem paths:
// URI to filesystem path
function uriToPath(uri: string, basePath: string): string {
// Remove scheme if present
const cleanUri = uri.replace(/^file:\/\//, '')
// Resolve relative to base path
return path.resolve(basePath, cleanUri)
}
// Filesystem path to URI
function pathToUri(fsPath: string, basePath: string): string {
// Convert absolute path to relative path from base
const relativePath = path.relative(basePath, fsPath)
// Normalize separators to forward slashes
return relativePath.replace(/\\/g, '/')
}
Content Type Detection
The adapter automatically detects content types based on file extensions:
function detectContentType(filePath: string): string {
const extension = path.extname(filePath).toLowerCase()
switch (extension) {
case '.md':
return 'text/markdown'
case '.mdx':
return 'text/mdx'
case '.json':
return 'application/json'
case '.html':
return 'text/html'
case '.txt':
return 'text/plain'
case '.css':
return 'text/css'
case '.js':
return 'application/javascript'
case '.ts':
return 'application/typescript'
case '.yaml':
case '.yml':
return 'application/yaml'
case '.xml':
return 'application/xml'
default:
return 'application/octet-stream'
}
}
Metadata Handling
The FileSystem adapter extracts metadata from various sources:
async function extractMetadata(
filePath: string,
data: string
): Promise<ContentMetadata> {
const stats = await fs.promises.stat(filePath)
// Base metadata from file stats
const metadata: ContentMetadata = {
createdAt: stats.birthtime,
updatedAt: stats.mtime,
size: stats.size,
}
// Extract content-specific metadata (e.g., frontmatter)
if (filePath.endsWith('.md') || filePath.endsWith('.mdx')) {
const frontmatter = extractFrontmatter(data)
Object.assign(metadata, frontmatter)
}
return metadata
}
Error Handling
The FileSystem adapter maps filesystem errors to content errors:
function handleError(error: NodeJS.ErrnoException, uri: string): ContentError {
if (error.code === 'ENOENT') {
return new ContentNotFoundError(uri, 'filesystem')
} else if (error.code === 'EACCES') {
return new ContentAccessError(uri, 'filesystem')
} else if (error.code === 'EISDIR') {
return new ContentError(`Path is a directory: ${uri}`, uri, 'filesystem')
} else {
return new ContentError(
`Filesystem error: ${error.message}`,
uri,
'filesystem',
false
)
}
}
Performance Considerations
The FileSystem adapter includes performance optimizations:
- Path Caching: Caches resolved paths to avoid repeated normalization
- Metadata Caching: Optionally caches file metadata to reduce stat calls
- Stream Support: Uses streams for large files to minimize memory usage
- Watch Optimization: Uses efficient file watching with debounced events
Environment Compatibility
This adapter is specifically designed for Node.js environments and will not work in browsers. For cross-environment code, use environment detection:
import { isNode } from '@lib/utils/env'
import {
createFileSystemAdapter,
createMemoryAdapter,
} from '@lib/content/adapters'
const adapter = isNode()
? createFileSystemAdapter({ basePath: '/content' })
: createMemoryAdapter()