Skip to content

Commit eaaf551

Browse files
committed
fs/memory-provider: init the memory provider (stat, mkdir api)
1 parent 81ecb13 commit eaaf551

File tree

14 files changed

+310
-22
lines changed

14 files changed

+310
-22
lines changed

src/backend/src/api/APIError.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,9 @@ module.exports = class APIError {
518518
* is set to null. The first argument is used as the status code.
519519
*
520520
* @static
521-
* @param {number} status
522-
* @param {string|Error} message_or_source one of the following:
521+
* @param {number|string} status
522+
* @param {object} source
523+
* @param {string|Error|object} fields one of the following:
523524
* - a string to use as the error message
524525
* - an Error object to use as the source of the error
525526
* - an object with a message property to use as the error message

src/backend/src/filesystem/FilesystemService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ class FilesystemService extends BaseService {
346346
}
347347

348348
const svc_mountpoint = this.services.get('mountpoint');
349-
const provider = await svc_mountpoint.get_provider(selector);
349+
const { provider } = await svc_mountpoint.get_provider(selector);
350350

351351
let fsNode = new FSNodeContext({
352352
provider,

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ class HLMkdir extends HLFilesystemOperation {
279279
values.path = _path.basename(values.path);
280280
}
281281

282+
let { mountpoint } = await this.context.get('services').get('mountpoint').get_provider(values.parent.selector);
283+
282284
let parent_node = values.parent || await fs.node(new RootNodeSelector());
283285
console.log('USING PARENT', parent_node.selector.describe());
284286

@@ -287,7 +289,7 @@ class HLMkdir extends HLFilesystemOperation {
287289
// "top_parent" is the immediate parent of the target directory
288290
// (e.g: /home/foo/bar -> /home/foo)
289291
const top_parent = values.create_missing_parents
290-
? await this._create_top_parent({ top_parent: parent_node })
292+
? await this._create_dir({ dir: parent_node, mountpoint: mountpoint })
291293
: await this._get_existing_top_parent({ top_parent: parent_node })
292294
;
293295

@@ -468,28 +470,44 @@ 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 {FSNode} dir - The directory to create.
477+
* @returns {Promise<FSNode>} The created directory.
478+
*/
479+
async _create_dir ({ dir, mountpoint }) {
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');
484494
}
485495

486-
const path = maybe_path_selector.value;
496+
let path = maybe_path_selector.value;
487497

488498
const fs = this.context.get('services').get('filesystem');
489499

500+
let parent = await fs.node(new RootNodeSelector());
501+
if ( mountpoint && mountpoint !== '/' ) {
502+
parent = await fs.node(new NodePathSelector(mountpoint));
503+
504+
// remove the mountpoint from the path
505+
path = path.replace(mountpoint, '');
506+
}
507+
490508
const tree_op = new MkTree();
491509
await tree_op.run({
492-
parent: await fs.node(new RootNodeSelector()),
510+
parent: parent,
493511
tree: [path],
494512
});
495513

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

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

9090
setPropertiesKnownBySelector (node) {
9191
node.name = this.name;
92-
// no properties known
9392
}
9493

9594
describe () {

src/backend/src/helpers.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ async function is_user_signup_disabled() {
9797
}
9898

9999
const chkperm = spanify('chkperm', async (target_fsentry, requester_user_id, action) => {
100+
// TODO (xiaochen): remove this branch after the related ACL permission logic is implemented for "MemoryFSProvider".
101+
if (target_fsentry.is_public) {
102+
return true;
103+
}
104+
100105
// basic cases where false is the default response
101106
if(!target_fsentry)
102107
return false;

src/backend/src/modules/puterfs/MountpointService.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*/
2020
// const Mountpoint = o => ({ ...o });
2121

22-
const { RootNodeSelector, NodeUIDSelector } = require("../../filesystem/node/selectors");
22+
const { RootNodeSelector, NodeUIDSelector, NodePathSelector, NodeChildSelector } = require("../../filesystem/node/selectors");
2323
const BaseService = require("../../services/BaseService");
2424

2525
/**
@@ -66,6 +66,9 @@ class MountpointService extends BaseService {
6666
'/': {
6767
mounter: 'puterfs',
6868
},
69+
'/tmp': {
70+
mounter: 'memoryfs',
71+
},
6972
};
7073

7174
for ( const path of Object.keys(mountpoints) ) {
@@ -86,13 +89,25 @@ class MountpointService extends BaseService {
8689
});
8790
}
8891

92+
/**
93+
* Gets the provider for a given selector.
94+
*
95+
* @param {NodePathSelector|NodeUIDSelector|RootNodeSelector|NodeChildSelector} selector - The selector to get the provider for.
96+
* @returns {Promise<{provider: Provider, mountpoint: string}>} The provider for the given selector and the mountpoint.
97+
*/
8998
async get_provider (selector) {
9099
if ( selector instanceof RootNodeSelector ) {
91-
return this.mountpoints_['/'].provider;
100+
return {
101+
provider: this.mountpoints_['/'].provider,
102+
mountpoint: '/',
103+
};
92104
}
93105

94106
if ( selector instanceof NodeUIDSelector ) {
95-
return this.mountpoints_['/'].provider;
107+
return {
108+
provider: this.mountpoints_['/'].provider,
109+
mountpoint: '/',
110+
};
96111
}
97112

98113
const probe = {};
@@ -109,12 +124,18 @@ class MountpointService extends BaseService {
109124
}
110125

111126
if ( longest_mount_path ) {
112-
return this.mountpoints_[longest_mount_path].provider;
127+
return {
128+
provider: this.mountpoints_[longest_mount_path].provider,
129+
mountpoint: longest_mount_path,
130+
};
113131
}
114132
}
115133

116134
// Use root mountpoint as fallback
117-
return this.mountpoints_['/'].provider;
135+
return {
136+
provider: this.mountpoints_['/'].provider,
137+
mountpoint: '/',
138+
};
118139
}
119140

120141
// Temporary solution - we'll develop this incrementally

src/backend/src/modules/puterfs/PuterFSModule.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class PuterFSModule extends AdvancedBase {
4040

4141
const DatabaseFSEntryFetcher = require("./DatabaseFSEntryFetcher");
4242
services.registerService('fsEntryFetcher', DatabaseFSEntryFetcher);
43+
44+
const { MemoryFSService } = require('./customfs/MemoryFSService');
45+
services.registerService('memoryfs', MemoryFSService);
4346
}
4447
}
4548

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
20+
const FSNodeContext = require("../../../filesystem/FSNodeContext");
21+
const _path = require('path');
22+
const { Context } = require("../../../util/context");
23+
24+
class MemoryFSProvider {
25+
constructor(mountpoint) {
26+
this.mountpoint = mountpoint;
27+
28+
// key: relative path from the mountpoint, always starts with `/`
29+
// value: file content
30+
this.files = new Map();
31+
32+
// key: relative path from the mountpoint, always starts with `/`
33+
// value: directory content
34+
this.directories = new Set();
35+
36+
// The root directory (of the mountpoint) always exists.
37+
this.directories.add('/');
38+
}
39+
40+
/**
41+
* Normalize the path to be relative to the mountpoint. Returns `/` if the path is empty/undefined.
42+
*
43+
* @param {string} path - The path to normalize.
44+
* @returns {string} - The normalized path, always starts with `/`.
45+
*/
46+
_normalize_path (path) {
47+
if ( ! path ) {
48+
return '/';
49+
}
50+
51+
if ( path.startsWith(this.mountpoint) ) {
52+
path = path.slice(this.mountpoint.length);
53+
}
54+
55+
if ( ! path.startsWith('/') ) {
56+
path = '/' + path;
57+
}
58+
59+
return path;
60+
}
61+
62+
/**
63+
* Performs a stat operation on the given FSNode.
64+
*
65+
* @param {Object} param
66+
* @param {FSNodeContext} param.node - The node to stat.
67+
* @returns {Promise<Object|null>} - The result of the stat operation, or `null` if the node doesn't exist.
68+
*
69+
* If the result is not null, the returned object includes following fields:
70+
* - `is_dir` {boolean} — `true` if the node is a directory.
71+
* - `public` {boolean} — `true` if the node is public (read/write access for everyone).
72+
* - `user_id` {number} — The ID of the user who owns the node.
73+
*
74+
* (ref: https://github.com/HeyPuter/puter/blob/8e58fabb7156d02c0e396ad26788e25ab0138db8/src/backend/src/services/database/sqlite_setup/0001_create-tables.sql#L70-L99)
75+
*/
76+
async stat ({
77+
node,
78+
}) {
79+
const inner_path = this._normalize_path(node?.path);
80+
81+
// for now, assume the path is a dir
82+
if ( this.directories.has(inner_path) ) {
83+
const full_path = _path.join(this.mountpoint, inner_path);
84+
85+
return {
86+
is_public: true,
87+
88+
path: full_path,
89+
90+
name: _path.basename(full_path),
91+
92+
// TODO (xiaochen): get the user id from database, the `user_id` must be set no
93+
// matter what.
94+
user_id: 1,
95+
96+
is_dir: true,
97+
};
98+
}
99+
100+
return null;
101+
}
102+
103+
/**
104+
* Create a new directory.
105+
*
106+
* @param {Object} param
107+
* @param {Context} param.context - The context of the operation.
108+
* @param {FSNodeContext} param.parent - The parent node to create the directory in. Must exist and be a directory.
109+
* @param {string} param.name - The name of the new directory.
110+
* @returns {Promise<FSNodeContext>} - The new directory node.
111+
*/
112+
async mkdir({ context, parent, name }) {
113+
const inner_path = this._normalize_path(_path.join(parent.path, name));
114+
const full_path = _path.join(this.mountpoint, inner_path);
115+
116+
this.directories.add(inner_path);
117+
118+
// create the node
119+
const fs = context.get('services').get('filesystem');
120+
const node = await fs.node(full_path);
121+
return node;
122+
}
123+
}
124+
125+
module.exports = {
126+
MemoryFSProvider,
127+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
20+
const BaseService = require("../../../services/BaseService");
21+
const { MemoryFSProvider } = require("./MemoryFSProvider");
22+
23+
class MemoryFSService extends BaseService {
24+
async _init () {
25+
const svc_mountpoint = this.services.get('mountpoint');
26+
svc_mountpoint.register_mounter('memoryfs', this.as('mounter'));
27+
}
28+
29+
static IMPLEMENTS = {
30+
mounter: {
31+
async mount ({ path, options }) {
32+
const provider = new MemoryFSProvider(path);
33+
return provider;
34+
}
35+
}
36+
}
37+
}
38+
39+
module.exports = {
40+
MemoryFSService,
41+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Custom FS Providers
2+
3+
This directory contains custom FS providers that are not part of the core PuterFS.
4+
5+
## MemoryFSProvider
6+
7+
This is a demo FS provider that illustrates how to implement a custom FS provider.
8+
9+
### TODO
10+
11+
- Make all FS operations works.
12+
- [x] `stat`
13+
- [x] `mkdir`
14+
- [ ] `write`
15+
- [ ] `read`
16+
- [ ] `rename`
17+
- [ ] `copy`
18+
- [ ] `move`
19+
- [ ] Remove hardcoded permission checks, use standard ACL.
20+
- [ ] Store file path relative to the mountpoint, so remounting can be done easily.
21+
22+
## NullFSProvider
23+
24+
A FS provider that mimics `/dev/null`.
25+
26+
## LinuxFSProvider
27+
28+
Provide the ability to mount a Linux directory as a FS provider.

0 commit comments

Comments
 (0)