AviatoAviato
DeveloperPlugins

Filesystem Capability

How a plugin discovers files for libraries to ingest, and optionally watches for changes and exposes local paths.

A filesystem plugin tells Aviato where files live and how to enumerate them. It is the only thing standing between "the user added a library" and "the pipeline has files to process."

The bundled aviato-fs-local plugin handles the local-disk case. Cloud and network filesystem providers (S3, Dropbox, SMB, etc.) are implemented as their own filesystem plugins. Anything you can list, fetch, and watch can be a filesystem plugin.

Manifest

{
  "id": "aviato-fs-local",
  "name": "Local Filesystem",
  "version": "1.0.0",
  "description": "Add local files to your libraries",
  "author": "Aviato",
  "license": "MIT",
  "engine": "bun",
  "entry": "src/index.ts",
  "aviato": { "minVersion": "0.1.0" },

  "capabilities": ["filesystem"],
  "mediaTypes": ["movies", "tv", "music", "photos", "books"],

  "configuration": [
    { "key": "excludePatterns", "label": "Exclude patterns", "input": "string-list" },
    { "key": "watchForChanges", "label": "Watch for changes", "input": "toggle", "default": true }
  ],

  "capabilityConfig": {
    "filesystem": {
      "supportsWatch": true,
      "supportsLocalFileAccess": true,
      "supportsWrite": true
    }
  }
}

mediaTypes lists the library types that can use this filesystem. Most filesystem plugins accept everything (the medium doesn't care what's on it); a niche provider could narrow this.

capabilityConfig.filesystem declares which optional behaviors the plugin implements:

FlagMeaningEffect
supportsWatchfilesystem.watch is implementedLibrary config exposes a "Watch for changes" toggle.
supportsLocalFileAccessfilesystem.getLocalPath returns a real path on diskLets Aviato's streaming pipeline use the file directly with FFmpeg/FFprobe instead of streaming bytes.
supportsWriteThe plugin can persist user uploadsLets users upload subtitles, posters, etc. to libraries backed by this filesystem.

A cloud provider like S3 would set supportsWatch: true (via S3 events), supportsLocalFileAccess: false, and supportsWrite: true. A read-only network share would set all three to false.

RPC contract

Methods Aviato calls on a filesystem plugin (filesystem.* namespace, types exported from @aviato/plugin-sdk):

MethodOptional?Purpose
validaterequiredCheck that the user-supplied config (paths, credentials) is valid before saving a library.
scanrequiredEnumerate every file under the configured location.
watchoptionalSubscribe to filesystem changes and emit events as they happen.
unwatchLibraryoptionalStop watching a single library.
unwatchoptionalStop watching everything (server shutdown).
getLocalPathoptionalTranslate a uri returned during scan into a real path on disk.

Optional methods are wired up only if the SDK's FilesystemHandlers object includes them. The SDK lazily registers each RPC handler.

validate

Quick correctness check. Called when the user creates or edits a library.

validate: async (config) => {
  const path = config.path as string
  if (!path) {
    return { valid: false, errors: ['Path is required'] }
  }
  try {
    await fs.access(path)
    return { valid: true }
  } catch {
    return { valid: false, errors: [`Cannot access ${path}`] }
  }
}

Returns { valid: boolean, errors?: string[] }. The wizard refuses to save a library with valid: false and surfaces the errors inline.

scan

Enumerate the configured location once. Aviato calls this when a library is first created and on every subsequent manual scan.

scan: async (config, extensionMap, emitters) => {
  const seen = new Set<string>()
  for await (const entry of walk(config.path as string)) {
    if (!matchesExtensionMap(entry, extensionMap)) continue
    seen.add(entry.path)
    emitters.emitFile({
      uri: `file://${entry.path}`,
      filename: entry.name,
      size: entry.size,
      mimeType: entry.mimeType,
      modifiedAt: entry.mtime.toISOString(),
    })
  }
  // Files that were previously known but didn't show up this scan
  await emitRemovedFiles(seen, emitters)
  emitters.emitScanComplete()

  return {
    totalFiles: seen.size,
    newFiles: 0,           // tracked by Aviato based on what it had before
    modifiedFiles: 0,
    removedFiles: 0,
    errors: [],
    durationMs: Date.now() - start,
  }
}

extensionMap is Aviato's compiled view of which extensions and glob patterns the active libraries care about, so the plugin can skip files it knows Aviato won't use:

type ExtensionMap = {
  primary: string[]                              // primary file extensions
  auxiliaries: Record<string, {
    extensions: string[]
    patterns?: string[]
  }>                                             // companion files by role
}

emitters is how the plugin streams discoveries back to Aviato. The SDK creates these for you:

EmitterNotification it sendsUse when
emitFile(file)filesystem.fileA file was discovered (new or unchanged).
emitFileRemoved(uri)filesystem.file.removedA previously-known file is gone.
emitScanComplete()filesystem.scan.completeFinal notification, flushes Aviato's batch.

Streaming via emitters is preferred over returning a giant array. The ingestion pipeline can start probing files while the scan is still running, and a 2-million-file scan won't hold a 2-million-entry response in memory.

The return value is a summary used for the admin UI scan history.

watch

Optional. When supportsWatch is true, Aviato calls this once per library after the initial scan. The plugin keeps a watcher alive and emits events through the same emitters object as scan.

watch: async (config, extensionMap, libraryId, emitters) => {
  const watcher = chokidar.watch(config.path as string, { ignoreInitial: true })
  watcher.on('add',    p => emitters.emitFile(toDiscoveredFile(p)))
  watcher.on('change', p => emitters.emitFile(toDiscoveredFile(p)))
  watcher.on('unlink', p => emitters.emitFileRemoved(`file://${p}`))
  watchers.set(libraryId, watcher)
}

Watch is fire-and-forget from Aviato's perspective. The call returns immediately and the watcher runs until unwatchLibrary(libraryId) or unwatch() tears it down.

unwatchLibrary / unwatch

Stop watching a specific library, or every library. Called when a library is deleted, the plugin is being stopped, or Aviato is shutting down.

unwatchLibrary: async (libraryId) => {
  await watchers.get(libraryId)?.close()
  watchers.delete(libraryId)
},
unwatch: async () => {
  for (const w of watchers.values()) await w.close()
  watchers.clear()
}

getLocalPath

Optional. When supportsLocalFileAccess is true, Aviato's streaming pipeline calls this to convert a plugin-emitted uri into a real path on the local filesystem. The streaming server uses the path to spawn FFmpeg/FFprobe without buffering the file through the JSON-RPC channel.

getLocalPath: async (uri) => {
  // file://... -> /...
  return new URL(uri).pathname
}

A cloud provider that doesn't have a local copy should leave this method off and supportsLocalFileAccess: false. The streaming pipeline will fall back to streaming bytes through the plugin (when that path exists).

Discovered files

Every file the plugin emits is a DiscoveredFile:

{
  uri: string              // opaque, plugin-defined; must round-trip through getLocalPath
  filename: string         // basename only
  size: number             // bytes
  mimeType?: string        // optional, sniff if cheap
  modifiedAt: string       // ISO 8601
  metadata?: Record<string, unknown>  // free-form (e.g. ETag, version, region)
}

uri is opaque to Aviato. Use whatever scheme makes sense for your provider (file://..., s3://bucket/key, dropbox://...). The pipeline treats it as a stable key: if the same uri shows up across two scans, it's considered the same file (with modifiedAt deciding whether to re-probe).

SDK shape

import type { FilesystemHandlers } from '@aviato/plugin-sdk'
import { createPlugin } from '@aviato/plugin-sdk'

const filesystem: FilesystemHandlers = {
  validate: async (config) => { /* ... */ },
  scan: async (config, extensionMap, emitters) => { /* ... */ },
  watch: async (config, extensionMap, libraryId, emitters) => { /* ... */ },
  unwatchLibrary: async (libraryId) => { /* ... */ },
  unwatch: async () => { /* ... */ },
  getLocalPath: async (uri) => { /* ... */ },
}

createPlugin({ filesystem })

The SDK only registers an optional method (watch, unwatchLibrary, unwatch, getLocalPath) when the corresponding handler is present. Forgetting the matching capabilityConfig.filesystem.supports* flag is the difference between "Aviato knows it can call this" and "the plugin will respond to the call." Both sides need to agree.

See also

  • Plugin manifest
  • Plugin system overview
  • Bundle covers the data structure built from discovered files.
  • All filesystem types (DiscoveredFile, ScanResult, ValidationResult, FilesystemHandlers, FilesystemEmitters) are exported from @aviato/plugin-sdk.
  • aviato-fs-local: reference implementation for the local-disk case.

On this page