storageErrors.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * Map filesystem/storage errors into standardized ApiErrors.
  3. *
  4. * Problem:
  5. * - `fs.readdir()` throws ENOENT when a path does not exist.
  6. * - Without mapping, routes often return 500 with `error.message` (inconsistent).
  7. *
  8. * Goal:
  9. * - If a requested branch/year/month/day path does not exist => return 404.
  10. * - But if the NAS root itself is not available/misconfigured => return 500.
  11. *
  12. * This avoids:
  13. * - Incorrect 500s for normal “not found” cases.
  14. * - Misleading 404s when the whole NAS is down.
  15. */
  16. import fs from "node:fs/promises";
  17. import { ApiError } from "./errors.js";
  18. /**
  19. * Check whether an error looks like a “path not found” error from Node’s fs.
  20. * ENOENT: No such file or directory
  21. * ENOTDIR: A path segment is not a directory
  22. *
  23. * @param {unknown} err
  24. * @returns {boolean}
  25. */
  26. export function isFsNotFoundError(err) {
  27. return Boolean(
  28. err &&
  29. typeof err === "object" &&
  30. (err.code === "ENOENT" || err.code === "ENOTDIR")
  31. );
  32. }
  33. /**
  34. * Determine if the configured NAS root path is accessible.
  35. *
  36. * Rationale:
  37. * - If NAS_ROOT_PATH is missing or unreachable, “not found” below it
  38. * is likely a system issue => return 500 instead of 404.
  39. *
  40. * @returns {Promise<boolean>}
  41. */
  42. async function isNasRootAccessible() {
  43. const root = process.env.NAS_ROOT_PATH;
  44. if (!root) return false;
  45. try {
  46. // access() succeeds if the path exists and is accessible.
  47. await fs.access(root);
  48. return true;
  49. } catch {
  50. return false;
  51. }
  52. }
  53. /**
  54. * Convert errors from the storage layer into ApiErrors.
  55. *
  56. * Policy:
  57. * - ENOENT/ENOTDIR:
  58. * - If NAS root accessible => 404 FS_NOT_FOUND (requested resource missing)
  59. * - Else => 500 FS_STORAGE_ERROR (system/config issue)
  60. * - Anything else => 500 FS_STORAGE_ERROR
  61. *
  62. * @param {unknown} err
  63. * @param {{
  64. * notFoundCode?: string,
  65. * notFoundMessage?: string,
  66. * details?: any
  67. * }=} options
  68. * @returns {Promise<ApiError>}
  69. */
  70. export async function mapStorageReadError(
  71. err,
  72. { notFoundCode = "FS_NOT_FOUND", notFoundMessage = "Not found", details } = {}
  73. ) {
  74. if (isFsNotFoundError(err)) {
  75. const rootOk = await isNasRootAccessible();
  76. // If the NAS is not accessible, treat it as an internal failure.
  77. if (!rootOk) {
  78. return new ApiError({
  79. status: 500,
  80. code: "FS_STORAGE_ERROR",
  81. message: "Internal server error",
  82. cause: err,
  83. });
  84. }
  85. // NAS root exists => the specific requested path is missing => 404.
  86. return new ApiError({
  87. status: 404,
  88. code: notFoundCode,
  89. message: notFoundMessage,
  90. details,
  91. cause: err,
  92. });
  93. }
  94. // For all other filesystem errors, return a generic 500.
  95. return new ApiError({
  96. status: 500,
  97. code: "FS_STORAGE_ERROR",
  98. message: "Internal server error",
  99. cause: err,
  100. });
  101. }