agalwood/Motrix · error · AppError

PluginRuntimeFault

PluginRuntimeFault

Error message

plugin.ffmpeg.staging_quota_exceeded

What it means

Thrown by FfmpegStaging.assertQuota() when the recursive sum of file sizes under the staging directory exceeds opts.quotaBytes. Enforces a per-task disk budget on ffmpeg output before chain commit. Tagged PluginRuntimeFault because the plugin produced more output than its configured allowance.

Source

Thrown at src/core/plugin/hooks/staging-dir.ts:66

  }

  async ensureDir(): Promise<void> {
    await mkdir(this.dir, { recursive: true })
  }

  async assertQuota(): Promise<void> {
    let total = 0
    const entries = await readdir(this.dir, {
      recursive: true,
      withFileTypes: true,
    })
    for (const e of entries) {
      if (e.isFile()) {
        total += (await stat(path.join(e.parentPath, e.name))).size
      }
    }
    if (total > this.opts.quotaBytes) {
      throw new AppError(
        ErrorCode.PluginRuntimeFault,
        'plugin.ffmpeg.staging_quota_exceeded'
      )
    }
  }

  /**
   * Chain commit: promote the file matching `finalFilePath` to saveDir; delete
   * all remaining staging contents afterward.
   *
   * `finalFilePath` may be absolute (resolving to saveDir) or relative to
   * saveDir. Returns the absolute path of the promoted file.
   */
  async promote(finalFilePath: string): Promise<string> {
    const saveDir = this.opts.saveDir.replace(/[/\\]+$/, '')
    const finalAbs = path.resolve(saveDir, finalFilePath)
    const stagedAbs = path.join(this.dir, path.relative(saveDir, finalAbs))
    const tmp = `${finalAbs}.tmp-${Date.now()}`

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Raise quotaBytes in the StagingOptions passed to FfmpegStaging (e.g. from 1 GiB to 4 * 1024 ** 3).
  2. Reduce ffmpeg output size: lower bitrate, shorter duration, fewer streams, or a more compact container.
  3. Ensure discard() is invoked on hook failure so staging does not accumulate across retries.
  4. Call FfmpegStaging.cleanupOrphans(pluginsDir) at host startup to reap leftover staging dirs from crashed runs.

Example fix

// before
new FfmpegStaging({ ..., quotaBytes: 1 * 1024 ** 3 })
// after
new FfmpegStaging({ ..., quotaBytes: 4 * 1024 ** 3 })
Defensive patterns

Strategy: try-catch

Validate before calling

import { stat } from 'node:fs/promises'
import path from 'node:path'
// Pre-check staging size before triggering assertQuota.
async function projectedBytes(stagingDir: string): Promise<number> {
  let total = 0
  for (const e of await readdir(stagingDir, { recursive: true, withFileTypes: true })) {
    if (e.isFile()) total += (await stat(path.join(e.parentPath, e.name))).size
  }
  return total
}

Try / catch

try {
  await staging.assertQuota()
} catch (e) {
  if (e instanceof AppError && e.message === 'plugin.ffmpeg.staging_quota_exceeded') {
    // lower ffmpeg quality / duration, or raise quotaBytes, then retry
    await staging.discard() // free the staging dir
    throw e
  }
  throw e
}

Prevention

When it happens

Trigger: A beforeFinalize plugin runs ffmpeg whose output lands in the staging dir; assertQuota() is called (pre-commit) and total bytes > quotaBytes. The loop at staging-dir.ts:54-64 stats every file recursively before the check at line 65.

Common situations: quotaBytes configured too low (default example is 4 GiB); plugin transcodes to a high bitrate/large format; multiple staged files accumulate because discard() was not called after a prior failed run; orphaned staging files from a crashed process (cleanupOrphans only runs at startup).

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/d27c893aadc0dea7. Report an issue: GitHub.