Skip to content

Commit cd93021

Browse files
committed
fs/provider: keep memory fs's parent_uid consistent
1 parent c30bc76 commit cd93021

File tree

4 files changed

+63
-41
lines changed

4 files changed

+63
-41
lines changed

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

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

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

2525
/**
@@ -89,12 +89,7 @@ class MountpointService extends BaseService {
8989

9090
async get_provider (selector) {
9191
// type check
92-
if ( ! (selector instanceof RootNodeSelector) &&
93-
! (selector instanceof NodeUIDSelector) &&
94-
! (selector instanceof NodeChildSelector) &&
95-
! (selector instanceof NodePathSelector) &&
96-
! (selector instanceof NodeInternalIDSelector)
97-
) {
92+
if ( ! (selector instanceof NodeSelector) ) {
9893
throw new Error('Invalid selector type');
9994
}
10095

@@ -104,14 +99,16 @@ class MountpointService extends BaseService {
10499

105100
if ( selector instanceof NodeUIDSelector ) {
106101
for ( const [path, { provider }] of Object.entries(this.mountpoints_) ) {
107-
const result = await provider._stat_by_uid({
102+
const result = await provider.stat_by_uid({
108103
uid: selector.value,
109104
});
110105
if ( result ) {
111106
return provider;
112107
}
113108
}
114-
console.log('no provider found');
109+
110+
// No provider found, but we shouldn't throw an error here
111+
// because it's a valid case for a node that doesn't exist.
115112
}
116113

117114
if ( selector instanceof NodeChildSelector ) {

src/backend/src/modules/puterfs/customfs/MemoryFSProvider.js

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ class MemoryFile {
3333
* @param {Object} param
3434
* @param {string} param.full_path
3535
* @param {boolean} param.is_dir
36-
* @param {Buffer} param.content - The content of the file, `null` if the file is a directory.
36+
* @param {Buffer|null} param.content - The content of the file, `null` if the file is a directory.
37+
* @param {string|null} [param.parent_uid] - UID of parent directory; null for root.
3738
*/
3839
constructor({
3940
full_path,
4041
is_dir,
4142
content,
43+
parent_uid = null,
4244
}) {
4345
this.uuid = uuidv4();
4446

@@ -49,8 +51,8 @@ class MemoryFile {
4951

5052
this.content = content;
5153

52-
// TODO (xiaochen): return consistent parent_uid
53-
this.parent_uid = uuidv4();
54+
// parent_uid should reflect the actual parent's uid; null for root
55+
this.parent_uid = parent_uid;
5456

5557
// TODO (xiaochen): return sensible values for "user_id", currently
5658
// it must be 2 (admin) to pass the test.
@@ -89,6 +91,7 @@ class MemoryFSProvider {
8991
full_path: '/',
9092
is_dir: true,
9193
content: null,
94+
parent_uid: null,
9295
});
9396
this.entriesByPath.set('/', root.uuid);
9497
this.entriesByUUID.set(root.uuid, root);
@@ -132,40 +135,62 @@ class MemoryFSProvider {
132135
}
133136

134137
/**
135-
* Check the integrity of the whole memory filesystem and the input. Throws error if any violation is found.
138+
* Check the integrity of the whole memory filesystem. Throws error if any violation is found.
136139
*
137-
* @param {MemoryFile|FSNodeContext} entry - The entry to check.
138140
* @returns {Promise<void>}
139141
*/
140-
_integrity_check (entry) {
142+
_integrity_check () {
141143
if ( config.env !== 'dev' ) {
142144
// only check in debug mode since it's expensive
143145
return;
144146
}
145147

146-
if ( entry ) {
147-
// check all directories along the path are valid
148-
const path_to_check = 'entry' in entry ? entry.entry?.path : entry.path;
149-
const inner_path = this._inner_path(path_to_check);
150-
const path_components = inner_path.split('/');
151-
for ( let i = 2; i < path_components.length; i++ ) {
152-
const path_component = path_components.slice(0, i).join('/');
153-
const entry_uuid = this.entriesByPath.get(path_component);
154-
if ( ! entry_uuid ) {
155-
throw new Error(`Directory ${path_component} does not exist`);
156-
}
157-
}
158-
}
159-
160148
// check the 2 maps are consistent
161149
if ( this.entriesByPath.size !== this.entriesByUUID.size ) {
162150
throw new Error('Path map and UUID map have different sizes');
163151
}
152+
164153
for ( const [inner_path, uuid] of this.entriesByPath ) {
165154
const entry = this.entriesByUUID.get(uuid);
166-
if ( ! entry || this._inner_path(entry.path) !== inner_path ) {
155+
156+
// entry should exist
157+
if ( ! entry ) {
158+
throw new Error(`Entry ${uuid} does not exist`);
159+
}
160+
161+
// path should match
162+
if ( this._inner_path(entry.path) !== inner_path ) {
167163
throw new Error(`Path ${inner_path} does not match entry ${uuid}`);
168164
}
165+
166+
// uuid should match
167+
if ( entry.uuid !== uuid ) {
168+
throw new Error(`UUID ${uuid} does not match entry ${entry.uuid}`);
169+
}
170+
171+
// parent should exist
172+
if ( entry.parent_uid ) {
173+
const parent_entry = this.entriesByUUID.get(entry.parent_uid);
174+
if ( ! parent_entry ) {
175+
throw new Error(`Parent ${entry.parent_uid} does not exist`);
176+
}
177+
}
178+
179+
// parent's path should be a prefix of the entry's path
180+
if ( entry.parent_uid ) {
181+
const parent_entry = this.entriesByUUID.get(entry.parent_uid);
182+
if ( ! entry.path.startsWith(parent_entry.path) ) {
183+
throw new Error(`Parent ${entry.parent_uid} path ${parent_entry.path} is not a prefix of entry ${entry.path}`);
184+
}
185+
}
186+
187+
// parent should be a directory
188+
if ( entry.parent_uid ) {
189+
const parent_entry = this.entriesByUUID.get(entry.parent_uid);
190+
if ( ! parent_entry.is_dir ) {
191+
throw new Error(`Parent ${entry.parent_uid} is not a directory`);
192+
}
193+
}
169194
}
170195
}
171196

@@ -219,7 +244,7 @@ class MemoryFSProvider {
219244
* @param {string} param.uid
220245
* @returns {Promise<Object|null>} - The result of the stat operation, or `null` if the node doesn't exist.
221246
*/
222-
async _stat_by_uid ({ uid }) {
247+
async stat_by_uid ({ uid }) {
223248
const entry = this.entriesByUUID.get(uid);
224249
if ( ! entry ) {
225250
return null;
@@ -271,6 +296,7 @@ class MemoryFSProvider {
271296
full_path: full_path,
272297
is_dir: true,
273298
content: null,
299+
parent_uid: parent.uid,
274300
});
275301
this.entriesByPath.set(inner_path, entry.uuid);
276302
this.entriesByUUID.set(entry.uuid, entry);
@@ -279,7 +305,7 @@ class MemoryFSProvider {
279305
const fs = context.get('services').get('filesystem');
280306
const node = await fs.node(entry.path);
281307

282-
this._integrity_check(node);
308+
this._integrity_check();
283309

284310
return node;
285311
}
@@ -330,7 +356,7 @@ class MemoryFSProvider {
330356
this.entriesByUUID.delete(node.uid);
331357
}
332358

333-
this._integrity_check(node);
359+
this._integrity_check();
334360
}
335361

336362
/**
@@ -359,15 +385,14 @@ class MemoryFSProvider {
359385
* @returns {Promise<MemoryFile>}
360386
*/
361387
async move({ context, node, new_parent, new_name, metadata }) {
362-
this._integrity_check(null);
363-
364388
// create the new entry
365389
const new_full_path = _path.join(new_parent.path, new_name);
366390
const new_inner_path = this._inner_path(new_full_path);
367391
const entry = new MemoryFile({
368392
full_path: new_full_path,
369393
is_dir: node.entry.is_dir,
370394
content: node.entry.content,
395+
parent_uid: new_parent.uid,
371396
});
372397
entry.uuid = node.entry.uuid;
373398
this.entriesByPath.set(new_inner_path, entry.uuid);
@@ -378,7 +403,7 @@ class MemoryFSProvider {
378403
this.entriesByPath.delete(inner_path);
379404
// should not delete the entry by uuid because it's the same
380405

381-
this._integrity_check(entry);
406+
this._integrity_check();
382407

383408
return entry;
384409
}
@@ -456,6 +481,7 @@ class MemoryFSProvider {
456481
full_path: full_path,
457482
is_dir: false,
458483
content: file.stream.read(),
484+
parent_uid: parent.uid,
459485
});
460486
this.entriesByPath.set(inner_path, entry.uuid);
461487
this.entriesByUUID.set(entry.uuid, entry);
@@ -495,13 +521,12 @@ class MemoryFSProvider {
495521
this.entriesByUUID.set(node.uid, original_entry);
496522
}
497523

498-
this._integrity_check(node);
499-
500-
// return node;
501524
const fs = context.get('services').get('filesystem');
502525
node = await fs.node(original_entry.path);
503526
await node.fetchEntry();
504527

528+
this._integrity_check();
529+
505530
return node;
506531
}
507532
}

src/backend/src/modules/puterfs/lib/PuterFSProvider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,7 @@ class PuterFSProvider extends putility.AdvancedBase {
861861
* @param {string} param.uid
862862
* @returns {Promise<Object|null>} - The result of the stat operation, or `null` if the node doesn't exist.
863863
*/
864-
async _stat_by_uid ({ uid }) {
864+
async stat_by_uid ({ uid }) {
865865
const svc = Context.get('services');
866866
const svc_fsEntry = svc.get('fsEntryService');
867867
const entry = await svc_fsEntry.get(uid);

src/backend/src/modules/web/lib/eggspress.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ module.exports = function eggspress (route, settings, handler) {
181181
} else await handler(req, res, next);
182182
} catch (e) {
183183
if (! (e instanceof APIError)) {
184-
// Any non-APIError indicates an unhandled error from the backend.
184+
// Any non-APIError indicates an unhandled error (i.e. a bug) from the backend.
185185
// We add a dedicated branch to facilitate debugging.
186186
console.error(e);
187187
api_error_handler(e, req, res, next);

0 commit comments

Comments
 (0)