Skip to content

Commit 1f6bbe1

Browse files
authored
dev: memory filesystem
* fs/memory-provider: passed all apitests * test: write benchmark * fs: add benchmark for stat-intensive scenario * apitest: update duration * fs: remove "NodeSelector", add checks to memoryfs, passed simple test * test: update apitest * debug: remove a debug stmt
1 parent c34bd81 commit 1f6bbe1

22 files changed

+955
-47
lines changed

src/backend/exports.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const CoreModule = require("./src/CoreModule.js");
2020
const { Kernel } = require("./src/Kernel.js");
2121
const DatabaseModule = require("./src/DatabaseModule.js");
2222
const LocalDiskStorageModule = require("./src/LocalDiskStorageModule.js");
23+
const MemoryStorageModule = require("./src/MemoryStorageModule.js");
2324
const SelfHostedModule = require("./src/modules/selfhosted/SelfHostedModule.js");
2425
const { testlaunch } = require("./src/index.js");
2526
const BaseService = require("./src/services/BaseService.js");
@@ -73,6 +74,7 @@ module.exports = {
7374
WebModule,
7475
DatabaseModule,
7576
LocalDiskStorageModule,
77+
MemoryStorageModule,
7678
SelfHostedModule,
7779
TestDriversModule,
7880
PuterAIModule,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (C) 2024-present Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
class MemoryStorageModule {
20+
async install (context) {
21+
const services = context.get('services');
22+
const MemoryStorageService = require("./services/MemoryStorageService");
23+
services.registerService('memory-storage', MemoryStorageService);
24+
}
25+
}
26+
27+
module.exports = MemoryStorageModule;

src/backend/src/api/APIError.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919
const { URLSearchParams } = require("node:url");
20+
const config = require("../config");
2021
const { quot } = require('@heyputer/putility').libs.string;
2122

2223
/**
@@ -518,8 +519,9 @@ module.exports = class APIError {
518519
* is set to null. The first argument is used as the status code.
519520
*
520521
* @static
521-
* @param {number} status
522-
* @param {string|Error} message_or_source one of the following:
522+
* @param {number|string} status
523+
* @param {object} source
524+
* @param {string|Error|object} fields one of the following:
523525
* - a string to use as the error message
524526
* - an Error object to use as the source of the error
525527
* - an object with a message property to use as the error message

src/backend/src/filesystem/FSNodeContext.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ module.exports = class FSNodeContext {
288288
controls,
289289
});
290290

291-
if ( entry === null ) {
291+
if ( ! entry ) {
292292
this.found = false;
293293
this.entry = false;
294294
} else {

src/backend/src/filesystem/hl_operations/hl_copy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ class HLCopy extends HLFilesystemOperation {
159159
throw APIError.create('source_and_dest_are_the_same');
160160
}
161161

162-
if ( await is_ancestor_of(source.mysql_id, parent.mysql_id) ) {
163-
throw APIError('cannot_copy_item_into_itself');
162+
if ( await is_ancestor_of(source.uid, parent.uid) ) {
163+
throw APIError.create('cannot_copy_item_into_itself');
164164
}
165165

166166
let overwritten;

src/backend/src/filesystem/hl_operations/hl_mkdir.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ class HLMkdir extends HLFilesystemOperation {
287287
// "top_parent" is the immediate parent of the target directory
288288
// (e.g: /home/foo/bar -> /home/foo)
289289
const top_parent = values.create_missing_parents
290-
? await this._create_top_parent({ top_parent: parent_node })
290+
? await this._create_dir(parent_node)
291291
: await this._get_existing_top_parent({ top_parent: parent_node })
292292
;
293293

@@ -331,12 +331,14 @@ class HLMkdir extends HLFilesystemOperation {
331331
});
332332
}
333333
else if ( dedupe_name ) {
334-
const fsEntryFetcher = context.get('services').get('fsEntryFetcher');
334+
const fs = context.get('services').get('filesystem');
335+
const parent_selector = parent_node.selector;
335336
for ( let i=1 ;; i++ ) {
336337
let try_new_name = `${target_basename} (${i})`;
337-
const exists = await fsEntryFetcher.nameExistsUnderParent(
338-
existing.entry.parent_uid, try_new_name
339-
);
338+
const selector = new NodeChildSelector(parent_selector, try_new_name);
339+
const exists = await parent_node.provider.quick_check({
340+
selector,
341+
});
340342
if ( ! exists ) {
341343
target_basename = try_new_name;
342344
break;
@@ -468,16 +470,24 @@ class HLMkdir extends HLFilesystemOperation {
468470
return node;
469471
}
470472

471-
async _create_top_parent ({ top_parent }) {
472-
if ( await top_parent.exists() ) {
473-
if ( ! top_parent.entry.is_dir ) {
473+
/**
474+
* Creates a directory and all its ancestors.
475+
*
476+
* @param {FSNodeContext} dir - The directory to create.
477+
* @returns {Promise<FSNodeContext>} The created directory.
478+
*/
479+
async _create_dir (dir) {
480+
console.log('CREATING DIR', dir.selector.describe());
481+
482+
if ( await dir.exists() ) {
483+
if ( ! dir.entry.is_dir ) {
474484
throw APIError.create('dest_is_not_a_directory');
475485
}
476-
return top_parent;
486+
return dir;
477487
}
478488

479489
const maybe_path_selector =
480-
top_parent.get_selector_of_type(NodePathSelector);
490+
dir.get_selector_of_type(NodePathSelector);
481491

482492
if ( ! maybe_path_selector ) {
483493
throw APIError.create('dest_does_not_exist');

src/backend/src/filesystem/ll_operations/ll_read.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
const APIError = require("../../api/APIError");
2020
const { Sequence } = require("../../codex/Sequence");
21+
const { MemoryFSProvider } = require("../../modules/puterfs/customfs/MemoryFSProvider");
2122

2223
const { DB_WRITE } = require("../../services/database/consts");
2324
const { buffer_to_stream } = require("../../util/streamutil");
@@ -115,10 +116,13 @@ class LLRead extends LLFilesystemOperation {
115116
},
116117
async function create_S3_read_stream (a) {
117118
const context = a.iget('context');
118-
const storage = context.get('storage');
119119

120120
const { fsNode, version_id, offset, length, has_range, range } = a.values();
121121

122+
const svc_mountpoint = context.get('services').get('mountpoint');
123+
const provider = await svc_mountpoint.get_provider(fsNode.selector);
124+
const storage = svc_mountpoint.get_storage(provider.constructor);
125+
122126
// Empty object here is in the case of local fiesystem,
123127
// where s3:location will return null.
124128
// TODO: storage interface shouldn't have S3-specific properties.
@@ -130,6 +134,7 @@ class LLRead extends LLFilesystemOperation {
130134
bucket_region: location.bucket_region,
131135
version_id,
132136
key: location.key,
137+
memory_file: fsNode.entry,
133138
...(range? {range} : (has_range ? {
134139
range: `bytes=${offset}-${offset+length-1}`
135140
} : {})),
@@ -144,8 +149,11 @@ class LLRead extends LLFilesystemOperation {
144149
const { fsNode, stream, has_range, range} = a.values();
145150

146151
if ( ! has_range ) {
147-
const res = await svc_fileCache.maybe_store(fsNode, stream);
148-
if ( res.stream ) a.set('stream', res.stream);
152+
// only cache for non-memoryfs providers
153+
if ( ! (fsNode.provider instanceof MemoryFSProvider) ) {
154+
const res = await svc_fileCache.maybe_store(fsNode, stream);
155+
if ( res.stream ) a.set('stream', res.stream);
156+
}
149157
}
150158
},
151159
async function return_stream (a) {

src/backend/src/filesystem/ll_operations/ll_rmdir.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919
const APIError = require("../../api/APIError");
20+
const { MemoryFSProvider } = require("../../modules/puterfs/customfs/MemoryFSProvider");
2021
const { ParallelTasks } = require("../../util/otelutil");
2122
const FSNodeContext = require("../FSNodeContext");
2223
const { NodeUIDSelector } = require("../node/selectors");
@@ -102,14 +103,27 @@ class LLRmDir extends LLFilesystemOperation {
102103
}
103104

104105
await tasks.awaitAll();
105-
if ( ! descendants_only ) {
106-
await target.provider.rmdir({
106+
107+
// TODO (xiaochen): consolidate these two branches
108+
if ( target.provider instanceof MemoryFSProvider ) {
109+
await target.provider.rmdir( {
107110
context,
108111
node: target,
109112
options: {
110-
ignore_not_empty: true,
113+
recursive,
114+
descendants_only,
111115
},
112-
});
116+
} );
117+
} else {
118+
if ( ! descendants_only ) {
119+
await target.provider.rmdir( {
120+
context,
121+
node: target,
122+
options: {
123+
ignore_not_empty: true,
124+
},
125+
} );
126+
}
113127
}
114128
}
115129
}

src/backend/src/filesystem/node/selectors.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ class NodeChildSelector {
8989

9090
setPropertiesKnownBySelector (node) {
9191
node.name = this.name;
92-
// no properties known
92+
93+
try_infer_attributes(this);
94+
if ( this.path ) {
95+
node.path = this.path;
96+
}
9397
}
9498

9599
describe () {
@@ -145,6 +149,30 @@ class NodeRawEntrySelector {
145149
}
146150
}
147151

152+
/**
153+
* Try to infer following attributes for a selector:
154+
* - path
155+
* - uid
156+
*
157+
* @param {NodePathSelector | NodeUIDSelector | NodeChildSelector | RootNodeSelector | NodeRawEntrySelector} selector
158+
*/
159+
function try_infer_attributes (selector) {
160+
if ( selector instanceof NodePathSelector ) {
161+
selector.path = selector.value;
162+
} else if ( selector instanceof NodeUIDSelector ) {
163+
selector.uid = selector.value;
164+
} else if ( selector instanceof NodeChildSelector ) {
165+
try_infer_attributes(selector.parent);
166+
if ( selector.parent.path ) {
167+
selector.path = _path.join(selector.parent.path, selector.name);
168+
}
169+
} else if ( selector instanceof RootNodeSelector ) {
170+
selector.path = '/';
171+
} else {
172+
// give up
173+
}
174+
}
175+
148176
const relativeSelector = (parent, path) => {
149177
if ( path === '.' ) return parent;
150178
if ( path.startsWith('..') ) {
@@ -169,4 +197,5 @@ module.exports = {
169197
RootNodeSelector,
170198
NodeRawEntrySelector,
171199
relativeSelector,
200+
try_infer_attributes,
172201
};

src/backend/src/helpers.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,38 @@ const body_parser_error_handler = (err, req, res, next) => {
966966
next();
967967
}
968968

969+
/**
970+
* Given a uid, returns a file node.
971+
*
972+
* TODO (xiaochen): It only works for MemoryFSProvider currently.
973+
*
974+
* @param {string} uid - The uid of the file to get.
975+
* @returns {Promise<MemoryFile|null>} The file node, or null if the file does not exist.
976+
*/
977+
async function get_entry(uid) {
978+
const svc_mountpoint = Context.get('services').get('mountpoint');
979+
const uid_selector = new NodeUIDSelector(uid);
980+
const provider = await svc_mountpoint.get_provider(uid_selector);
981+
982+
// NB: We cannot import MemoryFSProvider here because it will cause a circular dependency.
983+
if ( provider.constructor.name !== 'MemoryFSProvider' ) {
984+
return null;
985+
}
986+
987+
return provider.stat({
988+
selector: uid_selector,
989+
});
990+
}
991+
969992
async function is_ancestor_of(ancestor_uid, descendant_uid){
993+
const ancestor = await get_entry(ancestor_uid);
994+
const descendant = await get_entry(descendant_uid);
995+
996+
if ( ancestor && descendant ) {
997+
return descendant.path.startsWith(ancestor.path);
998+
}
999+
1000+
9701001
/** @type BaseDatabaseAccessService */
9711002
const db = services.get('database').get(DB_READ, 'filesystem');
9721003

0 commit comments

Comments
 (0)