Skip to content

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.

typescript
import { createFileSystemAdapter } from '@lib/content/adapters/node/filesystem'

const adapter = createFileSystemAdapter({
  basePath: '/path/to/content',
})

API Reference

Creation

typescript
function createFileSystemAdapter(
  options?: FileSystemAdapterOptions
): ContentAdapter

Creates a new FileSystem adapter instance.

Options

typescript
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

typescript
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:

typescript
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

typescript
async write(uri: string, content: Content<string>): Promise<void>;

Writes content to the filesystem.

Parameters:

  • uri: Content URI
  • content: Content object to write

Example:

typescript
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

typescript
async delete(uri: string): Promise<void>;

Deletes content from the filesystem.

Parameters:

  • uri: Content URI

Example:

typescript
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

typescript
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:

typescript
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

typescript
async exists(uri: string): Promise<boolean>;

Checks if content exists.

Parameters:

  • uri: Content URI

Returns:

  • A Promise resolving to a boolean indicating existence

Example:

typescript
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

typescript
watch(pattern: string, listener: WatchListener): Unsubscribe;

Watches for content changes.

Parameters:

  • pattern: Glob pattern for content URIs to watch
  • listener: Callback function for change events

Returns:

  • An unsubscribe function to stop watching

Example:

typescript
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

typescript
async createDirectory(uri: string): Promise<void>;

Creates a directory.

Parameters:

  • uri: Content URI for the directory

Example:

typescript
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

typescript
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:

typescript
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

typescript
async dispose(): Promise<void>;

Cleans up resources used by the adapter.

Example:

typescript
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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

typescript
import { isNode } from '@lib/utils/env'
import {
  createFileSystemAdapter,
  createMemoryAdapter,
} from '@lib/content/adapters'

const adapter = isNode()
  ? createFileSystemAdapter({ basePath: '/content' })
  : createMemoryAdapter()

Released under the MIT License.