|
|
@@ -0,0 +1,109 @@
|
|
|
+/**
|
|
|
+ * Map filesystem/storage errors into standardized ApiErrors.
|
|
|
+ *
|
|
|
+ * Problem:
|
|
|
+ * - `fs.readdir()` throws ENOENT when a path does not exist.
|
|
|
+ * - Without mapping, routes often return 500 with `error.message` (inconsistent).
|
|
|
+ *
|
|
|
+ * Goal:
|
|
|
+ * - If a requested branch/year/month/day path does not exist => return 404.
|
|
|
+ * - But if the NAS root itself is not available/misconfigured => return 500.
|
|
|
+ *
|
|
|
+ * This avoids:
|
|
|
+ * - Incorrect 500s for normal “not found” cases.
|
|
|
+ * - Misleading 404s when the whole NAS is down.
|
|
|
+ */
|
|
|
+
|
|
|
+import fs from "node:fs/promises";
|
|
|
+import { ApiError } from "./errors.js";
|
|
|
+
|
|
|
+/**
|
|
|
+ * Check whether an error looks like a “path not found” error from Node’s fs.
|
|
|
+ * ENOENT: No such file or directory
|
|
|
+ * ENOTDIR: A path segment is not a directory
|
|
|
+ *
|
|
|
+ * @param {unknown} err
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+export function isFsNotFoundError(err) {
|
|
|
+ return Boolean(
|
|
|
+ err &&
|
|
|
+ typeof err === "object" &&
|
|
|
+ (err.code === "ENOENT" || err.code === "ENOTDIR")
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Determine if the configured NAS root path is accessible.
|
|
|
+ *
|
|
|
+ * Rationale:
|
|
|
+ * - If NAS_ROOT_PATH is missing or unreachable, “not found” below it
|
|
|
+ * is likely a system issue => return 500 instead of 404.
|
|
|
+ *
|
|
|
+ * @returns {Promise<boolean>}
|
|
|
+ */
|
|
|
+async function isNasRootAccessible() {
|
|
|
+ const root = process.env.NAS_ROOT_PATH;
|
|
|
+ if (!root) return false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // access() succeeds if the path exists and is accessible.
|
|
|
+ await fs.access(root);
|
|
|
+ return true;
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Convert errors from the storage layer into ApiErrors.
|
|
|
+ *
|
|
|
+ * Policy:
|
|
|
+ * - ENOENT/ENOTDIR:
|
|
|
+ * - If NAS root accessible => 404 FS_NOT_FOUND (requested resource missing)
|
|
|
+ * - Else => 500 FS_STORAGE_ERROR (system/config issue)
|
|
|
+ * - Anything else => 500 FS_STORAGE_ERROR
|
|
|
+ *
|
|
|
+ * @param {unknown} err
|
|
|
+ * @param {{
|
|
|
+ * notFoundCode?: string,
|
|
|
+ * notFoundMessage?: string,
|
|
|
+ * details?: any
|
|
|
+ * }=} options
|
|
|
+ * @returns {Promise<ApiError>}
|
|
|
+ */
|
|
|
+export async function mapStorageReadError(
|
|
|
+ err,
|
|
|
+ { notFoundCode = "FS_NOT_FOUND", notFoundMessage = "Not found", details } = {}
|
|
|
+) {
|
|
|
+ if (isFsNotFoundError(err)) {
|
|
|
+ const rootOk = await isNasRootAccessible();
|
|
|
+
|
|
|
+ // If the NAS is not accessible, treat it as an internal failure.
|
|
|
+ if (!rootOk) {
|
|
|
+ return new ApiError({
|
|
|
+ status: 500,
|
|
|
+ code: "FS_STORAGE_ERROR",
|
|
|
+ message: "Internal server error",
|
|
|
+ cause: err,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // NAS root exists => the specific requested path is missing => 404.
|
|
|
+ return new ApiError({
|
|
|
+ status: 404,
|
|
|
+ code: notFoundCode,
|
|
|
+ message: notFoundMessage,
|
|
|
+ details,
|
|
|
+ cause: err,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // For all other filesystem errors, return a generic 500.
|
|
|
+ return new ApiError({
|
|
|
+ status: 500,
|
|
|
+ code: "FS_STORAGE_ERROR",
|
|
|
+ message: "Internal server error",
|
|
|
+ cause: err,
|
|
|
+ });
|
|
|
+}
|