]> E/V Lambda - evlambda.git/commitdiff
streamline code accessing local files
authorRaphaël Van Dyck <raphael.vandyck@evlambda.org>
Sun, 2 Aug 2026 12:26:22 +0000 (14:26 +0200)
committerRaphaël Van Dyck <raphael.vandyck@evlambda.org>
Sun, 2 Aug 2026 12:26:22 +0000 (14:26 +0200)
file-systems/native.js
file-systems/router.js

index 116469913104ca14b31aa90eb99d55f141b46006..653e32b13c63e342c66a70a22008c40463dcf610 100644 (file)
@@ -4,31 +4,23 @@
 import path from 'path';
 import fs from 'fs';
 
-function pathnameToNativePathname(root, pathname) {
-  // pathname: [''], ['', <name>], ['', <name>, <name>], ...
-  // <name> !== ''
-  // <name> !== '.'
-  // <name> !== '..'
-  return path.join(root, ...pathname);
+function checkIsWritable(options) {
+  if (!options.writable) {
+    throw new Error('The file system is not writable.');
+  }
 }
 
-function checkIsDirectory(nativePathname) {
-  const stat = fs.statSync(nativePathname, {throwIfNoEntry: false});
-  if (stat === undefined) {
-    throw new Error('The file does not exist.');
-  }
-  if (!stat.isDirectory()) {
-    throw new Error('The file is not a directory.');
-  }
+function toNativePathname(pathnameSegments, options) {
+  return path.join(options.root, ...pathnameSegments);
 }
 
-function checkIsNotDirectory(nativePathname) {
+function checkIsRegularFile(nativePathname) {
   const stat = fs.statSync(nativePathname, {throwIfNoEntry: false});
   if (stat === undefined) {
     throw new Error('The file does not exist.');
   }
-  if (stat.isDirectory()) {
-    throw new Error('The file is a directory.');
+  if (!stat.isFile()) {
+    throw new Error('The file is not a regular file.');
   }
 }
 
@@ -36,17 +28,15 @@ export function getCapabilities(options) {
   return {writable: options.writable};
 }
 
-export function getFileContents(pathname, options) {
-  const nativePathname = pathnameToNativePathname(options.root, pathname);
-  checkIsNotDirectory(nativePathname);
+export function getFileContents(pathnameSegments, options) {
+  const nativePathname = toNativePathname(pathnameSegments, options);
+  checkIsRegularFile(nativePathname);
   return fs.readFileSync(nativePathname);
 }
 
-export function putFileContents(pathname, contents, options) {
-  if (!options.writable) {
-    throw new Error('The file system is not writable.');
-  }
-  const nativePathname = pathnameToNativePathname(options.root, pathname);
-  checkIsNotDirectory(nativePathname);
+export function putFileContents(pathnameSegments, contents, options) {
+  checkIsWritable(options);
+  const nativePathname = toNativePathname(pathnameSegments, options);
+  checkIsRegularFile(nativePathname);
   fs.writeFileSync(nativePathname, contents);
 }
index 2540000ed3a64f0d3660cb5c49526fb2965772db..75c2b8d8a6709066bbff539f3758007832ce5a3b 100644 (file)
 // SPDX-License-Identifier: BSD-3-Clause
 
 import express from 'express';
+import fs from 'fs';
+import { Buffer } from 'buffer';
 
 class ClientError extends Error {}
 
-function isString(value) {
-  return typeof value === 'string';
+function invalidPathname() {
+  throw new Error('Invalid pathname.');
 }
 
-function invalidStringParameter() {
-  throw new Error('Invalid string parameter.');
-}
-
-function validateStringParameter(value) {
-  if (!isString(value)) {
-    invalidStringParameter();
+function getPathnameSegments(req) {
+  const pathname = req.query.pathname;
+  if (typeof pathname !== 'string') {
+    invalidPathname();
   }
-  return value;
-}
-
-function invalidNonNegativeIntegerParameter() {
-  throw new Error('Invalid non-negative integer parameter.');
-}
-
-function validateNonNegativeIntegerParameter(value) {
-  if (!isString(value) || !/^(0|[1-9][0-9]*)$/.test(value)) {
-    invalidNonNegativeIntegerParameter();
+  const pathnameSegments = pathname.split('/');
+  if (pathnameSegments.length === 1) {
+    invalidPathname();
   }
-  return Number.parseInt(value);
-}
-
-function invalidBooleanParameter() {
-  throw new Error('Invalid boolean parameter.');
-}
-
-function validateBooleanParameter(value) {
-  if (!isString(value)) {
-    invalidBooleanParameter();
+  if (pathnameSegments[0] !== '') {
+    invalidPathname();
   }
-  switch (value) {
-    case 'true':
-      return true;
-    case 'false':
-      return false;
-    default:
-      invalidBooleanParameter();
+  for (let i = 1; i < pathnameSegments.length; i++) {
+    if (pathnameSegments[i] === '..') {
+      invalidPathname();
+    }
   }
+  return pathnameSegments.slice(1);
 }
 
-function isValidName(name) {
-  return !name.includes('\x00') &&
-         !name.includes('/') &&
-         name !== '' &&
-         name !== '.' &&
-         name !== '..';
+function invalidContents() {
+  throw new Error('Invalid contents.');
 }
 
-function invalidNameParameter() {
-  throw new Error('Invalid name parameter.');
-}
-
-function validateNameParameter(name) {
-  if (!isString(name) || !isValidName(name)) {
-    invalidNameParameter();
+function getContents(req) {
+  const contents = req.body;
+  if (!Buffer.isBuffer(contents)) {
+    invalidContents();
   }
-  return name;
-}
-
-function invalidNamesParameter() {
-  throw new Error('Invalid names parameter.');
+  return contents;
 }
 
-function validateNamesParameter(names) {
-  if (!Array.isArray(names)) {
-    invalidNamesParameter();
-  }
-  if (names.length === 0) {
-    invalidNamesParameter();
-  }
-  for (const name of names) {
-    if (!isString(name) || !isValidName(name)) {
-      invalidNamesParameter();
+function makeRouter(fileSystem, options) {
+  let root = null;
+  try {
+    root = fs.realpathSync.native(options.root);
+  } catch (exception) {
+    if (exception.code = 'ENOENT') {
+      throw new Error('The root does not exist.');
+    } else {
+      throw exception;
     }
   }
-  if (new Set(names).size !== names.length) {
-    invalidNamesParameter();
+  const stat = fs.statSync(root);
+  if (!stat.isDirectory()) {
+    throw new Error('The root is not a directory.');
   }
-  names.sort();
-  return names;
-}
-
-function invalidPathnameParameter() {
-  throw new Error('Invalid pathname parameter.');
-}
-
-function validatePathnameParameter(pathname) {
-  if (!isString(pathname)) {
-    invalidPathnameParameter();
-  }
-  const names = pathname.split('/');
-  if (names[0] !== '') {
-    invalidPathnameParameter();
-  }
-  for (let i = 1; i < names.length; i++) {
-    if (!isValidName(names[i])) {
-      invalidPathnameParameter();
-    }
-  }
-  return names;
-}
-
-function makeRouter(fileSystem, options) {
+  options.root = root;
   options.ClientError = ClientError;
   const router = express.Router();
   router.get('/get-capabilities', (req, res) => {
     res.json(fileSystem.getCapabilities(options));
   });
   router.get('/get-file-contents', (req, res) => {
-    res.send(fileSystem.getFileContents(validatePathnameParameter(req.query.pathname), options));
+    const pathnameSegments = getPathnameSegments(req);
+    res.send(fileSystem.getFileContents(pathnameSegments, options));
   });
-  router.put('/put-file-contents', express.raw({limit: '250kb'}), (req, res) => {
-    fileSystem.putFileContents(validatePathnameParameter(req.query.pathname), req.body, options);
+  router.put('/put-file-contents', express.raw({limit: '1mb'}), (req, res) => {
+    const pathnameSegments = getPathnameSegments(req);
+    const contents = getContents(req);
+    fileSystem.putFileContents(pathnameSegments, contents, options);
     res.end();
   });
   router.use((err, req, res, next) => {