Skip to content

Commit 1efc8e7

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

File tree

13 files changed

+303
-16
lines changed

13 files changed

+303
-16
lines changed

src/backend/src/api/APIError.js

Lines changed: 8 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,14 +519,19 @@ 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
526528
* @returns
527529
*/
528530
static create (status, source, fields = {}) {
531+
if ( config.env === 'dev' ) {
532+
console.trace('APIError.create', status, source, fields);
533+
}
534+
529535
// Just the error code
530536
if ( typeof status === 'string' ) {
531537
const code = this.codes[status];

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

Lines changed: 24 additions & 8 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

@@ -468,28 +468,44 @@ class HLMkdir extends HLFilesystemOperation {
468468
return node;
469469
}
470470

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

479487
const maybe_path_selector =
480-
top_parent.get_selector_of_type(NodePathSelector);
488+
dir.get_selector_of_type(NodePathSelector);
481489

482490
if ( ! maybe_path_selector ) {
483491
throw APIError.create('dest_does_not_exist');
484492
}
485493

486-
const path = maybe_path_selector.value;
494+
let path = maybe_path_selector.value;
487495

488496
const fs = this.context.get('services').get('filesystem');
489497

498+
let parent = await fs.node(new RootNodeSelector());
499+
// if ( mountpoint && mountpoint !== '/' ) {
500+
// parent = await fs.node(new NodePathSelector(mountpoint));
501+
502+
// // remove the mountpoint from the path
503+
// path = path.replace(mountpoint, '');
504+
// }
505+
490506
const tree_op = new MkTree();
491507
await tree_op.run({
492-
parent: await fs.node(new RootNodeSelector()),
508+
parent: parent,
493509
tree: [path],
494510
});
495511

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

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

9090
setPropertiesKnownBySelector (node) {
9191
node.name = this.name;
92-
// no properties known
92+
93+
// try to get the full path recursively
94+
//
95+
// TODO (xiaochen): this is a hack to get the absolute path and should be removed once
96+
// we have a better solution. We need the absolute path so that `MountpointService`
97+
// can determine the mountpoint and provider for the node.
98+
const stack = [this.name];
99+
let current = this.parent;
100+
while ( current ) {
101+
if ( current instanceof NodeChildSelector ) {
102+
stack.push(current.name);
103+
current = current.parent;
104+
} else if ( current instanceof NodePathSelector ) {
105+
stack.push(current.value);
106+
current = null;
107+
} else if ( current instanceof RootNodeSelector ) {
108+
current = null;
109+
} else {
110+
// for other selectors, we can't determine the absolute path
111+
return;
112+
}
113+
}
114+
115+
node.path = '/';
116+
stack.reverse().forEach(item => {
117+
node.path = _path.join(node.path, item);
118+
});
93119
}
94120

95121
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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) ) {

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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
## NullFSProvider
10+
11+
A FS provider that mimics `/dev/null`.
12+
13+
## LinuxFSProvider
14+
15+
Provide the ability to mount a Linux directory as a FS provider.

src/backend/src/services/auth/ACLService.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const FSNodeParam = require("../../api/filesystem/FSNodeParam");
2222
const { NodePathSelector } = require("../../filesystem/node/selectors");
2323
const { get_user } = require("../../helpers");
2424
const configurable_auth = require("../../middleware/configurable_auth");
25+
const { MemoryFSProvider } = require("../../modules/puterfs/customfs/MemoryFSProvider");
2526
const { Context } = require("../../util/context");
2627
const { Endpoint } = require("../../util/expressutil");
2728
const BaseService = require("../BaseService");
@@ -344,6 +345,14 @@ class ACLService extends BaseService {
344345
* - Explicit permissions in the ACL hierarchy
345346
*/
346347
async _check_fsNode (actor, fsNode, mode) {
348+
// We currently need this ad-hoc check for "MemoryFSProvider" since the related
349+
// ACL permission logic is not yet implemented for "MemoryFSProvider".
350+
//
351+
// TODO (xiaochen): remove this branch after the related ACL permission logic is implemented for "MemoryFSProvider".
352+
if ( fsNode.provider instanceof MemoryFSProvider && fsNode.entry.is_public ) {
353+
return true;
354+
}
355+
347356
const context = Context.get();
348357

349358
actor = Actor.adapt(actor);

0 commit comments

Comments
 (0)