{"record":{"id":"09b0760ef1aec4cc","repo":"agalwood/Motrix","slug":"plugin-fs-not-a-file","errorCode":"plugin.fs.not_a_file","errorMessage":"plugin.fs.not_a_file: ${relPath} is a directory","messagePattern":"plugin\\.fs\\.not_a_file: (.+?) is a directory","errorType":"exception","errorClass":"FsStorageError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/fs-storage.ts","lineNumber":128,"sourceCode":"  ): Promise<string | Uint8Array> {\n    const encoding = opts?.encoding ?? 'utf8'\n    const abs = await resolveInsideSandbox(this.root, relPath)\n    try {\n      if (encoding === 'utf8') {\n        return await fs.readFile(abs, 'utf8')\n      }\n      const buf = await fs.readFile(abs)\n      return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)\n    } catch (e: unknown) {\n      const err = e as NodeJS.ErrnoException\n      if (err.code === 'ENOENT') {\n        throw new FsStorageError(\n          'plugin.fs.not_found',\n          `plugin.fs.not_found: ${relPath}`\n        )\n      }\n      if (err.code === 'EISDIR') {\n        throw new FsStorageError(\n          'plugin.fs.not_a_file',\n          `plugin.fs.not_a_file: ${relPath} is a directory`\n        )\n      }\n      throw e\n    }\n  }\n\n  // -------------------------------------------------------------------------\n  // write (atomic)\n  // -------------------------------------------------------------------------\n\n  async write(\n    relPath: string,\n    data: string | Uint8Array,\n    opts?: { overwrite?: boolean; encoding?: 'utf8' | 'binary' }\n  ): Promise<void> {\n    const overwrite = opts?.overwrite ?? true","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/fs-storage.ts#L110-L146","documentation":"Thrown by FsStorage.read() when `fs.readFile` fails with EISDIR — the relPath resolves to a directory, not a regular file, so reading it as a file is invalid. Code is `plugin.fs.not_a_file`. Distinct from not_found (ENOENT) which is checked first in the same catch block.","triggerScenarios":"Calling `storage.read('somedir')` where somedir is a directory; passing a path that omits the filename and lands on the directory entry; using a directory path from listing output as a read target.","commonSituations":"Off-by-one in path construction drops the filename; plugin iterates entries from list() and forgets to skip directories; manifest/output path collides with an existing directory name.","solutions":["stat() the path first and branch on `isFile`/`isDirectory` before reading.","Filter directory entries out when iterating list() output before read().","Construct read paths with explicit filename suffixes to avoid landing on a directory.","If the caller expects either, dispatch on stat result rather than blindly reading."],"exampleFix":"// before\nconst data = await storage.read(entry) // entry may be a dir from list()\n\n// after\nconst s = await storage.stat(entry)\nif (!s.isFile) throw new Error(`${entry} is not a file`)\nconst data = await storage.read(entry)","handlingStrategy":"validation","validationCode":"async function assertIsFile(storage: FsStorage, rel: string): Promise<void> {\n  const s = await storage.stat(rel)\n  if (!s.isFile) throw new Error(`${rel} is not a regular file`)\n}","typeGuard":"function isNotAFile(e: unknown): boolean {\n  return e instanceof Error && (e as FsStorageError).code === 'plugin.fs.not_a_file'\n}","tryCatchPattern":"try {\n  const data = await storage.read(rel)\n} catch (e) {\n  if (isNotAFile(e)) { /* skip or route to a directory handler */ }\n  else throw e\n}","preventionTips":["stat() and check isFile before read().","Filter list() results by type before reading each entry.","Always include the filename segment in read paths."],"tags":["fs","storage","eisdir","read","type-mismatch"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}