From 6f3bace4c40ae5453e279363f1cd41a895b67dc6 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 27 Aug 2025 14:26:03 -0400 Subject: [PATCH] Revert "dev: memory filesystem" This reverts commit 1f6bbe16723a541311d77699e379e198d34527b0. --- src/backend/exports.js | 2 - src/backend/src/MemoryStorageModule.js | 27 - src/backend/src/api/APIError.js | 6 +- src/backend/src/filesystem/FSNodeContext.js | 2 +- .../src/filesystem/hl_operations/hl_copy.js | 4 +- .../src/filesystem/hl_operations/hl_mkdir.js | 30 +- .../src/filesystem/ll_operations/ll_read.js | 14 +- .../src/filesystem/ll_operations/ll_rmdir.js | 22 +- src/backend/src/filesystem/node/selectors.js | 31 +- src/backend/src/helpers.js | 31 - .../modules/puterfs/DatabaseFSEntryFetcher.js | 8 - .../src/modules/puterfs/MountpointService.js | 38 +- .../src/modules/puterfs/PuterFSModule.js | 3 - .../puterfs/customfs/MemoryFSProvider.js | 603 ------------------ .../puterfs/customfs/MemoryFSService.js | 41 -- .../src/modules/puterfs/customfs/README.md | 15 - .../modules/puterfs/lib/PuterFSProvider.js | 54 +- .../modules/selfhosted/DefaultUserService.js | 2 - src/backend/src/modules/web/lib/eggspress.js | 22 +- .../src/services/LocalDiskStorageService.js | 3 +- .../src/services/MemoryStorageService.js | 42 -- tools/run-selfhosted.js | 2 - 22 files changed, 47 insertions(+), 955 deletions(-) delete mode 100644 src/backend/src/MemoryStorageModule.js delete mode 100644 src/backend/src/modules/puterfs/customfs/MemoryFSProvider.js delete mode 100644 src/backend/src/modules/puterfs/customfs/MemoryFSService.js delete mode 100644 src/backend/src/modules/puterfs/customfs/README.md delete mode 100644 src/backend/src/services/MemoryStorageService.js diff --git a/src/backend/exports.js b/src/backend/exports.js index 1766f7f1..9cc1f9f8 100644 --- a/src/backend/exports.js +++ b/src/backend/exports.js @@ -20,7 +20,6 @@ const CoreModule = require("./src/CoreModule.js"); const { Kernel } = require("./src/Kernel.js"); const DatabaseModule = require("./src/DatabaseModule.js"); const LocalDiskStorageModule = require("./src/LocalDiskStorageModule.js"); -const MemoryStorageModule = require("./src/MemoryStorageModule.js"); const SelfHostedModule = require("./src/modules/selfhosted/SelfHostedModule.js"); const { testlaunch } = require("./src/index.js"); const BaseService = require("./src/services/BaseService.js"); @@ -74,7 +73,6 @@ module.exports = { WebModule, DatabaseModule, LocalDiskStorageModule, - MemoryStorageModule, SelfHostedModule, TestDriversModule, PuterAIModule, diff --git a/src/backend/src/MemoryStorageModule.js b/src/backend/src/MemoryStorageModule.js deleted file mode 100644 index a8985460..00000000 --- a/src/backend/src/MemoryStorageModule.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -class MemoryStorageModule { - async install (context) { - const services = context.get('services'); - const MemoryStorageService = require("./services/MemoryStorageService"); - services.registerService('memory-storage', MemoryStorageService); - } -} - -module.exports = MemoryStorageModule; diff --git a/src/backend/src/api/APIError.js b/src/backend/src/api/APIError.js index 856c8f44..07755b67 100644 --- a/src/backend/src/api/APIError.js +++ b/src/backend/src/api/APIError.js @@ -17,7 +17,6 @@ * along with this program. If not, see . */ const { URLSearchParams } = require("node:url"); -const config = require("../config"); const { quot } = require('@heyputer/putility').libs.string; /** @@ -519,9 +518,8 @@ module.exports = class APIError { * is set to null. The first argument is used as the status code. * * @static - * @param {number|string} status - * @param {object} source - * @param {string|Error|object} fields one of the following: + * @param {number} status + * @param {string|Error} message_or_source one of the following: * - a string to use as the error message * - an Error object to use as the source of the error * - an object with a message property to use as the error message diff --git a/src/backend/src/filesystem/FSNodeContext.js b/src/backend/src/filesystem/FSNodeContext.js index cc062c94..fde3e20f 100644 --- a/src/backend/src/filesystem/FSNodeContext.js +++ b/src/backend/src/filesystem/FSNodeContext.js @@ -288,7 +288,7 @@ module.exports = class FSNodeContext { controls, }); - if ( ! entry ) { + if ( entry === null ) { this.found = false; this.entry = false; } else { diff --git a/src/backend/src/filesystem/hl_operations/hl_copy.js b/src/backend/src/filesystem/hl_operations/hl_copy.js index 000ba5f3..46c71af0 100644 --- a/src/backend/src/filesystem/hl_operations/hl_copy.js +++ b/src/backend/src/filesystem/hl_operations/hl_copy.js @@ -159,8 +159,8 @@ class HLCopy extends HLFilesystemOperation { throw APIError.create('source_and_dest_are_the_same'); } - if ( await is_ancestor_of(source.uid, parent.uid) ) { - throw APIError.create('cannot_copy_item_into_itself'); + if ( await is_ancestor_of(source.mysql_id, parent.mysql_id) ) { + throw APIError('cannot_copy_item_into_itself'); } let overwritten; diff --git a/src/backend/src/filesystem/hl_operations/hl_mkdir.js b/src/backend/src/filesystem/hl_operations/hl_mkdir.js index 0b612148..17cc12a2 100644 --- a/src/backend/src/filesystem/hl_operations/hl_mkdir.js +++ b/src/backend/src/filesystem/hl_operations/hl_mkdir.js @@ -287,7 +287,7 @@ class HLMkdir extends HLFilesystemOperation { // "top_parent" is the immediate parent of the target directory // (e.g: /home/foo/bar -> /home/foo) const top_parent = values.create_missing_parents - ? await this._create_dir(parent_node) + ? await this._create_top_parent({ top_parent: parent_node }) : await this._get_existing_top_parent({ top_parent: parent_node }) ; @@ -331,14 +331,12 @@ class HLMkdir extends HLFilesystemOperation { }); } else if ( dedupe_name ) { - const fs = context.get('services').get('filesystem'); - const parent_selector = parent_node.selector; + const fsEntryFetcher = context.get('services').get('fsEntryFetcher'); for ( let i=1 ;; i++ ) { let try_new_name = `${target_basename} (${i})`; - const selector = new NodeChildSelector(parent_selector, try_new_name); - const exists = await parent_node.provider.quick_check({ - selector, - }); + const exists = await fsEntryFetcher.nameExistsUnderParent( + existing.entry.parent_uid, try_new_name + ); if ( ! exists ) { target_basename = try_new_name; break; @@ -470,24 +468,16 @@ class HLMkdir extends HLFilesystemOperation { return node; } - /** - * Creates a directory and all its ancestors. - * - * @param {FSNodeContext} dir - The directory to create. - * @returns {Promise} The created directory. - */ - async _create_dir (dir) { - console.log('CREATING DIR', dir.selector.describe()); - - if ( await dir.exists() ) { - if ( ! dir.entry.is_dir ) { + async _create_top_parent ({ top_parent }) { + if ( await top_parent.exists() ) { + if ( ! top_parent.entry.is_dir ) { throw APIError.create('dest_is_not_a_directory'); } - return dir; + return top_parent; } const maybe_path_selector = - dir.get_selector_of_type(NodePathSelector); + top_parent.get_selector_of_type(NodePathSelector); if ( ! maybe_path_selector ) { throw APIError.create('dest_does_not_exist'); diff --git a/src/backend/src/filesystem/ll_operations/ll_read.js b/src/backend/src/filesystem/ll_operations/ll_read.js index 9ea9a049..1c51826d 100644 --- a/src/backend/src/filesystem/ll_operations/ll_read.js +++ b/src/backend/src/filesystem/ll_operations/ll_read.js @@ -18,7 +18,6 @@ */ const APIError = require("../../api/APIError"); const { Sequence } = require("../../codex/Sequence"); -const { MemoryFSProvider } = require("../../modules/puterfs/customfs/MemoryFSProvider"); const { DB_WRITE } = require("../../services/database/consts"); const { buffer_to_stream } = require("../../util/streamutil"); @@ -116,13 +115,10 @@ class LLRead extends LLFilesystemOperation { }, async function create_S3_read_stream (a) { const context = a.iget('context'); + const storage = context.get('storage'); const { fsNode, version_id, offset, length, has_range, range } = a.values(); - const svc_mountpoint = context.get('services').get('mountpoint'); - const provider = await svc_mountpoint.get_provider(fsNode.selector); - const storage = svc_mountpoint.get_storage(provider.constructor); - // Empty object here is in the case of local fiesystem, // where s3:location will return null. // TODO: storage interface shouldn't have S3-specific properties. @@ -134,7 +130,6 @@ class LLRead extends LLFilesystemOperation { bucket_region: location.bucket_region, version_id, key: location.key, - memory_file: fsNode.entry, ...(range? {range} : (has_range ? { range: `bytes=${offset}-${offset+length-1}` } : {})), @@ -149,11 +144,8 @@ class LLRead extends LLFilesystemOperation { const { fsNode, stream, has_range, range} = a.values(); if ( ! has_range ) { - // only cache for non-memoryfs providers - if ( ! (fsNode.provider instanceof MemoryFSProvider) ) { - const res = await svc_fileCache.maybe_store(fsNode, stream); - if ( res.stream ) a.set('stream', res.stream); - } + const res = await svc_fileCache.maybe_store(fsNode, stream); + if ( res.stream ) a.set('stream', res.stream); } }, async function return_stream (a) { diff --git a/src/backend/src/filesystem/ll_operations/ll_rmdir.js b/src/backend/src/filesystem/ll_operations/ll_rmdir.js index 30207074..08add7c7 100644 --- a/src/backend/src/filesystem/ll_operations/ll_rmdir.js +++ b/src/backend/src/filesystem/ll_operations/ll_rmdir.js @@ -17,7 +17,6 @@ * along with this program. If not, see . */ const APIError = require("../../api/APIError"); -const { MemoryFSProvider } = require("../../modules/puterfs/customfs/MemoryFSProvider"); const { ParallelTasks } = require("../../util/otelutil"); const FSNodeContext = require("../FSNodeContext"); const { NodeUIDSelector } = require("../node/selectors"); @@ -103,27 +102,14 @@ class LLRmDir extends LLFilesystemOperation { } await tasks.awaitAll(); - - // TODO (xiaochen): consolidate these two branches - if ( target.provider instanceof MemoryFSProvider ) { - await target.provider.rmdir( { + if ( ! descendants_only ) { + await target.provider.rmdir({ context, node: target, options: { - recursive, - descendants_only, + ignore_not_empty: true, }, - } ); - } else { - if ( ! descendants_only ) { - await target.provider.rmdir( { - context, - node: target, - options: { - ignore_not_empty: true, - }, - } ); - } + }); } } } diff --git a/src/backend/src/filesystem/node/selectors.js b/src/backend/src/filesystem/node/selectors.js index 717ecb17..1501a81f 100644 --- a/src/backend/src/filesystem/node/selectors.js +++ b/src/backend/src/filesystem/node/selectors.js @@ -89,11 +89,7 @@ class NodeChildSelector { setPropertiesKnownBySelector (node) { node.name = this.name; - - try_infer_attributes(this); - if ( this.path ) { - node.path = this.path; - } + // no properties known } describe () { @@ -149,30 +145,6 @@ class NodeRawEntrySelector { } } -/** - * Try to infer following attributes for a selector: - * - path - * - uid - * - * @param {NodePathSelector | NodeUIDSelector | NodeChildSelector | RootNodeSelector | NodeRawEntrySelector} selector - */ -function try_infer_attributes (selector) { - if ( selector instanceof NodePathSelector ) { - selector.path = selector.value; - } else if ( selector instanceof NodeUIDSelector ) { - selector.uid = selector.value; - } else if ( selector instanceof NodeChildSelector ) { - try_infer_attributes(selector.parent); - if ( selector.parent.path ) { - selector.path = _path.join(selector.parent.path, selector.name); - } - } else if ( selector instanceof RootNodeSelector ) { - selector.path = '/'; - } else { - // give up - } -} - const relativeSelector = (parent, path) => { if ( path === '.' ) return parent; if ( path.startsWith('..') ) { @@ -197,5 +169,4 @@ module.exports = { RootNodeSelector, NodeRawEntrySelector, relativeSelector, - try_infer_attributes, }; diff --git a/src/backend/src/helpers.js b/src/backend/src/helpers.js index 19f6477f..9abb815f 100644 --- a/src/backend/src/helpers.js +++ b/src/backend/src/helpers.js @@ -966,38 +966,7 @@ const body_parser_error_handler = (err, req, res, next) => { next(); } -/** - * Given a uid, returns a file node. - * - * TODO (xiaochen): It only works for MemoryFSProvider currently. - * - * @param {string} uid - The uid of the file to get. - * @returns {Promise} The file node, or null if the file does not exist. - */ -async function get_entry(uid) { - const svc_mountpoint = Context.get('services').get('mountpoint'); - const uid_selector = new NodeUIDSelector(uid); - const provider = await svc_mountpoint.get_provider(uid_selector); - - // NB: We cannot import MemoryFSProvider here because it will cause a circular dependency. - if ( provider.constructor.name !== 'MemoryFSProvider' ) { - return null; - } - - return provider.stat({ - selector: uid_selector, - }); -} - async function is_ancestor_of(ancestor_uid, descendant_uid){ - const ancestor = await get_entry(ancestor_uid); - const descendant = await get_entry(descendant_uid); - - if ( ancestor && descendant ) { - return descendant.path.startsWith(ancestor.path); - } - - /** @type BaseDatabaseAccessService */ const db = services.get('database').get(DB_READ, 'filesystem'); diff --git a/src/backend/src/modules/puterfs/DatabaseFSEntryFetcher.js b/src/backend/src/modules/puterfs/DatabaseFSEntryFetcher.js index df881265..0789a6df 100644 --- a/src/backend/src/modules/puterfs/DatabaseFSEntryFetcher.js +++ b/src/backend/src/modules/puterfs/DatabaseFSEntryFetcher.js @@ -222,12 +222,4 @@ module.exports = class DatabaseFSEntryFetcher extends BaseService { ); return !! check_dupe[0]; } - - async nameExistsUnderParentID (parent_id, name) { - const parent = await this.findByID(parent_id); - if ( ! parent ) { - return false; - } - return this.nameExistsUnderParent(parent.uuid, name); - } } diff --git a/src/backend/src/modules/puterfs/MountpointService.js b/src/backend/src/modules/puterfs/MountpointService.js index 68af55d7..48dddd8f 100644 --- a/src/backend/src/modules/puterfs/MountpointService.js +++ b/src/backend/src/modules/puterfs/MountpointService.js @@ -19,7 +19,7 @@ */ // const Mountpoint = o => ({ ...o }); -const { RootNodeSelector, NodeUIDSelector, NodeChildSelector, NodePathSelector, NodeInternalIDSelector, NodeSelector, try_infer_attributes } = require("../../filesystem/node/selectors"); +const { RootNodeSelector, NodeUIDSelector } = require("../../filesystem/node/selectors"); const BaseService = require("../../services/BaseService"); /** @@ -57,9 +57,8 @@ class MountpointService extends BaseService { * @returns {Promise} */ async _init () { - // key: provider class (e.g: PuterFSProvider, MemoryFSProvider) - // value: storage instance - this.storage_ = {}; + // Temporary solution - we'll develop this incrementally + this.storage_ = null; } async ['__on_boot.consolidation'] () { @@ -88,32 +87,12 @@ class MountpointService extends BaseService { } async get_provider (selector) { - try_infer_attributes(selector); - if ( selector instanceof RootNodeSelector ) { return this.mountpoints_['/'].provider; } if ( selector instanceof NodeUIDSelector ) { - for ( const [path, { provider }] of Object.entries(this.mountpoints_) ) { - const result = await provider.quick_check({ - selector, - }); - if ( result ) { - return provider; - } - } - - // No provider found, but we shouldn't throw an error here - // because it's a valid case for a node that doesn't exist. - } - - if ( selector instanceof NodeChildSelector ) { - if ( selector.path ) { - return this.get_provider(new NodePathSelector(selector.path)); - } else { - return this.get_provider(selector.parent); - } + return this.mountpoints_['/'].provider; } const probe = {}; @@ -139,16 +118,15 @@ class MountpointService extends BaseService { } // Temporary solution - we'll develop this incrementally - set_storage (provider, storage) { - this.storage_[provider] = storage; + set_storage (storage) { + this.storage_ = storage; } - /** * Gets the current storage backend instance * @returns {Object} The storage backend instance */ - get_storage (provider) { - return this.storage_[provider]; + get_storage () { + return this.storage_; } } diff --git a/src/backend/src/modules/puterfs/PuterFSModule.js b/src/backend/src/modules/puterfs/PuterFSModule.js index 234565fd..91b88fb5 100644 --- a/src/backend/src/modules/puterfs/PuterFSModule.js +++ b/src/backend/src/modules/puterfs/PuterFSModule.js @@ -40,9 +40,6 @@ class PuterFSModule extends AdvancedBase { const DatabaseFSEntryFetcher = require("./DatabaseFSEntryFetcher"); services.registerService('fsEntryFetcher', DatabaseFSEntryFetcher); - - const { MemoryFSService } = require('./customfs/MemoryFSService'); - services.registerService('memoryfs', MemoryFSService); } } diff --git a/src/backend/src/modules/puterfs/customfs/MemoryFSProvider.js b/src/backend/src/modules/puterfs/customfs/MemoryFSProvider.js deleted file mode 100644 index 09ec0d65..00000000 --- a/src/backend/src/modules/puterfs/customfs/MemoryFSProvider.js +++ /dev/null @@ -1,603 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const FSNodeContext = require('../../../filesystem/FSNodeContext'); -const _path = require('path'); -const { Context } = require('../../../util/context'); -const { v4: uuidv4 } = require('uuid'); -const config = require('../../../config'); -const { - NodeChildSelector, - NodePathSelector, - NodeUIDSelector, - NodeRawEntrySelector, - RootNodeSelector, - try_infer_attributes, -} = require('../../../filesystem/node/selectors'); -const fsCapabilities = require('../../../filesystem/definitions/capabilities'); -const APIError = require('../../../api/APIError'); - -class MemoryFile { - /** - * @param {Object} param - * @param {string} param.path - Relative path from the mountpoint. - * @param {boolean} param.is_dir - * @param {Buffer|null} param.content - The content of the file, `null` if the file is a directory. - * @param {string|null} [param.parent_uid] - UID of parent directory; null for root. - */ - constructor({ path, is_dir, content, parent_uid = null }) { - this.uuid = uuidv4(); - - this.is_public = true; - this.path = path; - this.name = _path.basename(path); - this.is_dir = is_dir; - - this.content = content; - - // parent_uid should reflect the actual parent's uid; null for root - this.parent_uid = parent_uid; - - // TODO (xiaochen): return sensible values for "user_id", currently - // it must be 2 (admin) to pass the test. - this.user_id = 2; - - // TODO (xiaochen): return sensible values for following fields - this.id = 123; - this.parent_id = 123; - this.immutable = 0; - this.is_shortcut = 0; - this.is_symlink = 0; - this.symlink_path = null; - this.created = Math.floor(Date.now() / 1000); - this.accessed = Math.floor(Date.now() / 1000); - this.modified = Math.floor(Date.now() / 1000); - this.size = is_dir ? 0 : content ? content.length : 0; - } -} - -class MemoryFSProvider { - constructor(mountpoint) { - this.mountpoint = mountpoint; - - // key: relative path from the mountpoint, always starts with `/` - // value: entry uuid - this.entriesByPath = new Map(); - - // key: entry uuid - // value: entry (MemoryFile) - // - // We declare 2 maps to support 2 lookup apis: by-path/by-uuid. - this.entriesByUUID = new Map(); - - const root = new MemoryFile({ - path: '/', - is_dir: true, - content: null, - parent_uid: null, - }); - this.entriesByPath.set('/', root.uuid); - this.entriesByUUID.set(root.uuid, root); - } - - /** - * Get the capabilities of this filesystem provider. - * - * @returns {Set} - Set of capabilities supported by this provider. - */ - get_capabilities() { - return new Set([ - fsCapabilities.READDIR_UUID_MODE, - fsCapabilities.UUID, - fsCapabilities.READ, - fsCapabilities.WRITE, - fsCapabilities.COPY_TREE, - ]); - } - - /** - * Normalize the path to be relative to the mountpoint. Returns `/` if the path is empty/undefined. - * - * @param {string} path - The path to normalize. - * @returns {string} - The normalized path, always starts with `/`. - */ - _inner_path(path) { - if (!path) { - return '/'; - } - - if (path.startsWith(this.mountpoint)) { - path = path.slice(this.mountpoint.length); - } - - if (!path.startsWith('/')) { - path = '/' + path; - } - - return path; - } - - /** - * Check the integrity of the whole memory filesystem. Throws error if any violation is found. - * - * @returns {Promise} - */ - _integrity_check() { - if (config.env !== 'dev') { - // only check in debug mode since it's expensive - return; - } - - // check the 2 maps are consistent - if (this.entriesByPath.size !== this.entriesByUUID.size) { - throw new Error('Path map and UUID map have different sizes'); - } - - for (const [inner_path, uuid] of this.entriesByPath) { - const entry = this.entriesByUUID.get(uuid); - - // entry should exist - if (!entry) { - throw new Error(`Entry ${uuid} does not exist`); - } - - // path should match - if (this._inner_path(entry.path) !== inner_path) { - throw new Error(`Path ${inner_path} does not match entry ${uuid}`); - } - - // uuid should match - if (entry.uuid !== uuid) { - throw new Error(`UUID ${uuid} does not match entry ${entry.uuid}`); - } - - // parent should exist - if (entry.parent_uid) { - const parent_entry = this.entriesByUUID.get(entry.parent_uid); - if (!parent_entry) { - throw new Error(`Parent ${entry.parent_uid} does not exist`); - } - } - - // parent's path should be a prefix of the entry's path - if (entry.parent_uid) { - const parent_entry = this.entriesByUUID.get(entry.parent_uid); - if (!entry.path.startsWith(parent_entry.path)) { - throw new Error( - `Parent ${entry.parent_uid} path ${parent_entry.path} is not a prefix of entry ${entry.path}`, - ); - } - } - - // parent should be a directory - if (entry.parent_uid) { - const parent_entry = this.entriesByUUID.get(entry.parent_uid); - if (!parent_entry.is_dir) { - throw new Error(`Parent ${entry.parent_uid} is not a directory`); - } - } - } - } - - /** - * Check if a given node exists. - * - * @param {Object} param - * @param {NodePathSelector | NodeUIDSelector | NodeChildSelector | RootNodeSelector | NodeRawEntrySelector} param.selector - The selector used for checking. - * @returns {Promise} - True if the node exists, false otherwise. - */ - async quick_check({ selector }) { - if (selector instanceof NodePathSelector) { - const inner_path = this._inner_path(selector.value); - return this.entriesByPath.has(inner_path); - } - - if (selector instanceof NodeUIDSelector) { - return this.entriesByUUID.has(selector.value); - } - - // fallback to stat - const entry = await this.stat({ selector }); - return !!entry; - } - - /** - * Performs a stat operation using the given selector. - * - * NB: Some returned fields currently contain placeholder values. And the - * `path` of the absolute path from the root. - * - * @param {Object} param - * @param {NodePathSelector | NodeUIDSelector | NodeChildSelector | RootNodeSelector | NodeRawEntrySelector} param.selector - The selector to stat. - * @returns {Promise} - The result of the stat operation, or `null` if the node doesn't exist. - */ - async stat({ selector }) { - try_infer_attributes(selector); - - let entry_uuid = null; - - if (selector instanceof NodePathSelector) { - // stat by path - const inner_path = this._inner_path(selector.value); - entry_uuid = this.entriesByPath.get(inner_path); - } else if (selector instanceof NodeUIDSelector) { - // stat by uid - entry_uuid = selector.value; - } else if (selector instanceof NodeChildSelector) { - if (selector.path) { - // Shouldn't care about about parent when the "path" is present - // since it might have different provider. - return await this.stat({ - selector: new NodePathSelector(selector.path), - }); - } else { - // recursively stat the parent and then stat the child - const parent_entry = await this.stat({ - selector: selector.parent, - }); - if (parent_entry) { - const full_path = _path.join(parent_entry.path, selector.name); - return await this.stat({ - selector: new NodePathSelector(full_path), - }); - } - } - } else { - // other selectors shouldn't reach here, i.e., it's an internal logic error - throw APIError.create('invalid_node'); - } - - const entry = this.entriesByUUID.get(entry_uuid); - if (!entry) { - return null; - } - - // Return a copied entry with `full_path`, since external code only cares - // about full path. - const copied_entry = { ...entry }; - copied_entry.path = _path.join(this.mountpoint, entry.path); - return copied_entry; - } - - /** - * Read directory contents. - * - * @param {Object} param - * @param {Context} param.context - The context of the operation. - * @param {FSNodeContext} param.node - The directory node to read. - * @returns {Promise} - Array of child UUIDs. - */ - async readdir({ context, node }) { - // prerequistes: get required path via stat - const entry = await this.stat({ selector: node.selector }); - if (!entry) { - throw APIError.create('invalid_node'); - } - - const inner_path = this._inner_path(entry.path); - const child_uuids = []; - - // Find all entries that are direct children of this directory - for (const [path, uuid] of this.entriesByPath) { - if (path === inner_path) { - continue; // Skip the directory itself - } - - const dirname = _path.dirname(path); - if (dirname === inner_path) { - child_uuids.push(uuid); - } - } - - return child_uuids; - } - - /** - * Create a new directory. - * - * @param {Object} param - * @param {Context} param.context - The context of the operation. - * @param {FSNodeContext} param.parent - The parent node to create the directory in. Must exist and be a directory. - * @param {string} param.name - The name of the new directory. - * @returns {Promise} - The new directory node. - */ - async mkdir({ context, parent, name }) { - // prerequistes: get required path via stat - const parent_entry = await this.stat({ selector: parent.selector }); - if (!parent_entry) { - throw APIError.create('invalid_node'); - } - - const full_path = _path.join(parent_entry.path, name); - const inner_path = this._inner_path(full_path); - - let entry = null; - if (this.entriesByPath.has(inner_path)) { - throw APIError.create('item_with_same_name_exists', null, { - entry_name: full_path, - }); - } else { - entry = new MemoryFile({ - path: inner_path, - is_dir: true, - content: null, - parent_uid: parent_entry.uuid, - }); - this.entriesByPath.set(inner_path, entry.uuid); - this.entriesByUUID.set(entry.uuid, entry); - } - - // create the node - const fs = context.get('services').get('filesystem'); - const node = await fs.node(entry.uuid); - await node.fetchEntry(); - - this._integrity_check(); - - return node; - } - - /** - * Remove a directory. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.node: The directory to remove. - * @param {Object} param.options: The options for the operation. - * @returns {Promise} - */ - async rmdir({ context, node, options = {} }) { - this._integrity_check(); - - // prerequistes: get required path via stat - const entry = await this.stat({ selector: node.selector }); - if (!entry) { - throw APIError.create('invalid_node'); - } - - const inner_path = this._inner_path(entry.path); - - // for mode: non-recursive - if (!options.recursive) { - const children = await this.readdir({ context, node }); - if (children.length > 0) { - throw APIError.create('not_empty'); - } - } - - // remove all descendants - for (const [other_inner_path, other_entry_uuid] of this.entriesByPath) { - if (other_entry_uuid === entry.uuid) { - // skip the directory itself - continue; - } - - if (other_inner_path.startsWith(inner_path)) { - this.entriesByPath.delete(other_inner_path); - this.entriesByUUID.delete(other_entry_uuid); - } - } - - // for mode: non-descendants-only - if (!options.descendants_only) { - // remove the directory itself - this.entriesByPath.delete(inner_path); - this.entriesByUUID.delete(entry.uuid); - } - - this._integrity_check(); - } - - /** - * Remove a file. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.node: The file to remove. - * @returns {Promise} - */ - async unlink({ context, node }) { - // prerequistes: get required path via stat - const entry = await this.stat({ selector: node.selector }); - if (!entry) { - throw APIError.create('invalid_node'); - } - - const inner_path = this._inner_path(entry.path); - this.entriesByPath.delete(inner_path); - this.entriesByUUID.delete(entry.uuid); - } - - /** - * Move a file. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.node: The file to move. - * @param {FSNodeContext} param.new_parent: The new parent directory of the file. - * @param {string} param.new_name: The new name of the file. - * @param {Object} param.metadata: The metadata of the file. - * @returns {Promise} - */ - async move({ context, node, new_parent, new_name, metadata }) { - // prerequistes: get required path via stat - const new_parent_entry = await this.stat({ selector: new_parent.selector }); - if (!new_parent_entry) { - throw APIError.create('invalid_node'); - } - - // create the new entry - const new_full_path = _path.join(new_parent_entry.path, new_name); - const new_inner_path = this._inner_path(new_full_path); - const entry = new MemoryFile({ - path: new_inner_path, - is_dir: node.entry.is_dir, - content: node.entry.content, - parent_uid: new_parent_entry.uuid, - }); - entry.uuid = node.entry.uuid; - this.entriesByPath.set(new_inner_path, entry.uuid); - this.entriesByUUID.set(entry.uuid, entry); - - // remove the old entry - const inner_path = this._inner_path(node.path); - this.entriesByPath.delete(inner_path); - // NB: should not delete the entry by uuid because uuid does not change - // after the move. - - this._integrity_check(); - - return entry; - } - - /** - * Copy a tree of files and directories. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.source - The source node to copy. - * @param {FSNodeContext} param.parent - The parent directory for the copy. - * @param {string} param.target_name - The name for the copied item. - * @returns {Promise} - The copied node. - */ - async copy_tree({ context, source, parent, target_name }) { - const fs = context.get('services').get('filesystem'); - - if (source.entry.is_dir) { - // Create the directory - const new_dir = await this.mkdir({ context, parent, name: target_name }); - - // Copy all children - const children = await this.readdir({ context, node: source }); - for (const child_uuid of children) { - const child_node = await fs.node(new NodeUIDSelector(child_uuid)); - await child_node.fetchEntry(); - const child_name = child_node.entry.name; - - await this.copy_tree({ - context, - source: child_node, - parent: new_dir, - target_name: child_name, - }); - } - - return new_dir; - } else { - // Copy the file - const new_file = await this.write_new({ - context, - parent, - name: target_name, - file: { stream: { read: () => source.entry.content } }, - }); - return new_file; - } - } - - /** - * Write a new file to the filesystem. Throws an error if the destination - * already exists. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.parent: The parent directory of the destination directory. - * @param {string} param.name: The name of the destination directory. - * @param {Object} param.file: The file to write. - * @returns {Promise} - */ - async write_new({ context, parent, name, file }) { - // prerequistes: get required path via stat - const parent_entry = await this.stat({ selector: parent.selector }); - if (!parent_entry) { - throw APIError.create('invalid_node'); - } - const full_path = _path.join(parent_entry.path, name); - const inner_path = this._inner_path(full_path); - - let entry = null; - if (this.entriesByPath.has(inner_path)) { - throw APIError.create('item_with_same_name_exists', null, { - entry_name: full_path, - }); - } else { - entry = new MemoryFile({ - path: inner_path, - is_dir: false, - content: file.stream.read(), - parent_uid: parent_entry.uuid, - }); - this.entriesByPath.set(inner_path, entry.uuid); - this.entriesByUUID.set(entry.uuid, entry); - } - - const fs = context.get('services').get('filesystem'); - const node = await fs.node(entry.uuid); - await node.fetchEntry(); - - this._integrity_check(); - - return node; - } - - /** - * Overwrite an existing file. Throws an error if the destination does not - * exist. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.node: The node to write to. - * @param {Object} param.file: The file to write. - * @returns {Promise} - */ - async write_overwrite({ context, node, file }) { - const entry = await this.stat({ selector: node.selector }); - if (!entry) { - throw APIError.create('invalid_node'); - } - const inner_path = this._inner_path(entry.path); - - this.entriesByPath.set(inner_path, entry.uuid); - let original_entry = this.entriesByUUID.get(entry.uuid); - if (!original_entry) { - throw new Error(`File ${entry.path} does not exist`); - } else { - if (original_entry.is_dir) { - throw new Error(`Cannot overwrite a directory`); - } - - original_entry.content = file.stream.read(); - original_entry.modified = Math.floor(Date.now() / 1000); - original_entry.size = original_entry.content ? original_entry.content.length : 0; - this.entriesByUUID.set(entry.uuid, original_entry); - } - - const fs = context.get('services').get('filesystem'); - node = await fs.node(original_entry.uuid); - await node.fetchEntry(); - - this._integrity_check(); - - return node; - } -} - -module.exports = { - MemoryFSProvider, -}; diff --git a/src/backend/src/modules/puterfs/customfs/MemoryFSService.js b/src/backend/src/modules/puterfs/customfs/MemoryFSService.js deleted file mode 100644 index 99397ecf..00000000 --- a/src/backend/src/modules/puterfs/customfs/MemoryFSService.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const BaseService = require("../../../services/BaseService"); -const { MemoryFSProvider } = require("./MemoryFSProvider"); - -class MemoryFSService extends BaseService { - async _init () { - const svc_mountpoint = this.services.get('mountpoint'); - svc_mountpoint.register_mounter('memoryfs', this.as('mounter')); - } - - static IMPLEMENTS = { - mounter: { - async mount ({ path, options }) { - const provider = new MemoryFSProvider(path); - return provider; - } - } - } -} - -module.exports = { - MemoryFSService, -}; \ No newline at end of file diff --git a/src/backend/src/modules/puterfs/customfs/README.md b/src/backend/src/modules/puterfs/customfs/README.md deleted file mode 100644 index bd66e79e..00000000 --- a/src/backend/src/modules/puterfs/customfs/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Custom FS Providers - -This directory contains custom FS providers that are not part of the core PuterFS. - -## MemoryFSProvider - -This is a demo FS provider that illustrates how to implement a custom FS provider. - -## NullFSProvider - -A FS provider that mimics `/dev/null`. - -## LinuxFSProvider - -Provide the ability to mount a Linux directory as a FS provider. \ No newline at end of file diff --git a/src/backend/src/modules/puterfs/lib/PuterFSProvider.js b/src/backend/src/modules/puterfs/lib/PuterFSProvider.js index 32ee4d06..67662d6b 100644 --- a/src/backend/src/modules/puterfs/lib/PuterFSProvider.js +++ b/src/backend/src/modules/puterfs/lib/PuterFSProvider.js @@ -22,7 +22,7 @@ const { MultiDetachable } = putility.libs.listener; const { TDetachable } = putility.traits; const { TeePromise } = putility.libs.promise; -const { NodeInternalIDSelector, NodeChildSelector, NodeUIDSelector, RootNodeSelector, NodePathSelector, NodeSelector } = require("../../../filesystem/node/selectors"); +const { NodeInternalIDSelector, NodeChildSelector, NodeUIDSelector, RootNodeSelector, NodePathSelector } = require("../../../filesystem/node/selectors"); const { Context } = require("../../../util/context"); const fsCapabilities = require('../../../filesystem/definitions/capabilities'); const { UploadProgressTracker } = require('../../../filesystem/storage/UploadProgressTracker'); @@ -66,52 +66,6 @@ class PuterFSProvider extends putility.AdvancedBase { ]); } - /** - * Check if a given node exists. - * - * @param {Object} param - * @param {NodeSelector} param.selector - The selector used for checking. - * @returns {Promise} - True if the node exists, false otherwise. - */ - async quick_check ({ - selector, - }) { - // a wrapper that access underlying database directly - const fsEntryFetcher = Context.get('services').get('fsEntryFetcher'); - - // shortcut: has full path - if ( selector?.path ) { - const entry = await fsEntryFetcher.findByPath(selector.path); - return Boolean(entry); - } - - // shortcut: has uid - if ( selector?.uid ) { - const entry = await fsEntryFetcher.findByUID(selector.uid); - return Boolean(entry); - } - - // shortcut: parent uid + child name - if ( selector instanceof NodeChildSelector && selector.parent instanceof NodeUIDSelector ) { - return await fsEntryFetcher.nameExistsUnderParent( - selector.parent.uid, - selector.name, - ); - } - - // shortcut: parent id + child name - if ( selector instanceof NodeChildSelector && selector.parent instanceof NodeInternalIDSelector ) { - return await fsEntryFetcher.nameExistsUnderParentID( - selector.parent.id, - selector.name, - ); - } - - // TODO (xiaochen): we should fallback to stat but we cannot at this moment - // since stat requires a valid `FSNodeContext` argument. - return false; - } - async stat ({ selector, options, @@ -702,9 +656,9 @@ class PuterFSProvider extends putility.AdvancedBase { * * @param {Object} param * @param {Context} param.context - * @param {FSNodeContext} param.node: The node to write to. + * @param {FSNode} param.node: The node to write to. * @param {File} param.file: The file to write. - * @returns {Promise} + * @returns {Promise} */ async write_overwrite({ context, node, file }) { const { @@ -810,7 +764,7 @@ class PuterFSProvider extends putility.AdvancedBase { const svc_event = svc.get('event'); const svc_mountpoint = svc.get('mountpoint'); - const storage = svc_mountpoint.get_storage(this.constructor); + const storage = svc_mountpoint.get_storage(); bucket ??= config.s3_bucket; bucket_region ??= config.s3_region ?? config.region; diff --git a/src/backend/src/modules/selfhosted/DefaultUserService.js b/src/backend/src/modules/selfhosted/DefaultUserService.js index b5c63368..ad4f6413 100644 --- a/src/backend/src/modules/selfhosted/DefaultUserService.js +++ b/src/backend/src/modules/selfhosted/DefaultUserService.js @@ -89,8 +89,6 @@ class DefaultUserService extends BaseService { ); if ( ! is_default_password ) return; - console.log(`password for admin is: ${tmp_password}`); - // show console widget this.default_user_widget = ({ is_docker }) => { if ( is_docker ) { diff --git a/src/backend/src/modules/web/lib/eggspress.js b/src/backend/src/modules/web/lib/eggspress.js index 32575d6e..2c81242f 100644 --- a/src/backend/src/modules/web/lib/eggspress.js +++ b/src/backend/src/modules/web/lib/eggspress.js @@ -24,7 +24,6 @@ const api_error_handler = require('./api_error_handler.js'); const APIError = require('../../../api/APIError.js'); const { Context } = require('../../../util/context.js'); const { subdomain } = require('../../../helpers.js'); -const config = require('../../../config.js'); /** * eggspress() is a factory function for creating express routers. @@ -170,9 +169,6 @@ module.exports = function eggspress (route, settings, handler) { return next(); } } - if ( config.env === 'dev' ) { - console.log(`request url: ${req.url}, body: ${JSON.stringify(req.body)}`); - } try { const expected_ctx = res.locals.ctx; const received_ctx = Context.get(undefined, { allow_fallback: true }); @@ -183,14 +179,18 @@ module.exports = function eggspress (route, settings, handler) { }); } else await handler(req, res, next); } catch (e) { - if ( config.env === 'dev' ) { - if (! (e instanceof APIError)) { - // Any non-APIError indicates an unhandled error (i.e. a bug) from the backend. - // We add a dedicated branch to facilitate debugging. - console.error(e); - } + if (e instanceof TypeError || e instanceof ReferenceError) { + // We add a dedicated branch for TypeError/ReferenceError since it usually + // indicates a bug in the backend. And it's pretty convenient to debug if we + // set a breakpoint here. + // + // Typical TypeError: + // - read properties of undefined + console.error(e); + api_error_handler(e, req, res, next); + } else { + api_error_handler(e, req, res, next); } - api_error_handler(e, req, res, next); } }; if (settings.allowedMethods.includes('GET')) { diff --git a/src/backend/src/services/LocalDiskStorageService.js b/src/backend/src/services/LocalDiskStorageService.js index e093f130..79434bde 100644 --- a/src/backend/src/services/LocalDiskStorageService.js +++ b/src/backend/src/services/LocalDiskStorageService.js @@ -18,7 +18,6 @@ * along with this program. If not, see . */ const { LocalDiskStorageStrategy } = require("../filesystem/strategies/storage_a/LocalDiskStorageStrategy"); -const { PuterFSProvider } = require("../modules/puterfs/lib/PuterFSProvider"); const { TeePromise } = require('@heyputer/putility').libs.promise; const { progress_stream, size_limit_stream } = require("../util/streamutil"); const BaseService = require("./BaseService"); @@ -53,7 +52,7 @@ class LocalDiskStorageService extends BaseService { svc_contextInit.register_value('storage', storage); const svc_mountpoint = this.services.get('mountpoint'); - svc_mountpoint.set_storage(PuterFSProvider, storage); + svc_mountpoint.set_storage(storage); } diff --git a/src/backend/src/services/MemoryStorageService.js b/src/backend/src/services/MemoryStorageService.js deleted file mode 100644 index 98b16ace..00000000 --- a/src/backend/src/services/MemoryStorageService.js +++ /dev/null @@ -1,42 +0,0 @@ -// METADATA // {"ai-commented":{"service":"mistral","model":"mistral-large-latest"}} -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -const BaseService = require("./BaseService"); -const { MemoryFSProvider } = require("../modules/puterfs/customfs/MemoryFSProvider"); -const { Readable } = require("stream"); - -class MemoryStorageService extends BaseService { - async _init () { - console.log('MemoryStorageService._init'); - - const svc_mountpoint = this.services.get('mountpoint'); - svc_mountpoint.set_storage(MemoryFSProvider, this); - } - - async create_read_stream (uuid, options) { - const memory_file = options?.memory_file; - if ( ! memory_file ) { - throw new Error('MemoryStorageService.create_read_stream: memory_file is required'); - } - - return Readable.from(memory_file.content); - } -} - -module.exports = MemoryStorageService; \ No newline at end of file diff --git a/tools/run-selfhosted.js b/tools/run-selfhosted.js index 5db748d7..9d3a33c8 100644 --- a/tools/run-selfhosted.js +++ b/tools/run-selfhosted.js @@ -83,7 +83,6 @@ const main = async () => { EssentialModules, DatabaseModule, LocalDiskStorageModule, - MemoryStorageModule, SelfHostedModule, BroadcastModule, TestDriversModule, @@ -101,7 +100,6 @@ const main = async () => { } k.add_module(new DatabaseModule()); k.add_module(new LocalDiskStorageModule()); - k.add_module(new MemoryStorageModule()); k.add_module(new SelfHostedModule()); k.add_module(new BroadcastModule()); k.add_module(new TestDriversModule());