From f906c1ebade39f778843e846ff2d1dfeac556a1f Mon Sep 17 00:00:00 2001 From: XiaochenCui Date: Mon, 21 Jul 2025 12:45:30 -0700 Subject: [PATCH] fs: adapt to different mkdir api, block write to root dir, add tests --- .../src/filesystem/hl_operations/hl_mkdir.js | 32 ++++-- tools/api-tester/lib/TestSDK.js | 10 ++ tools/api-tester/tests/mkdir.js | 102 ++++++++++++++++++ 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/src/backend/src/filesystem/hl_operations/hl_mkdir.js b/src/backend/src/filesystem/hl_operations/hl_mkdir.js index 2dc72716d..ce4ef64a9 100644 --- a/src/backend/src/filesystem/hl_operations/hl_mkdir.js +++ b/src/backend/src/filesystem/hl_operations/hl_mkdir.js @@ -271,8 +271,32 @@ class HLMkdir extends HLFilesystemOperation { }); } + // Unify the following formats: + // - full path: {"path":"/foo/bar", args...}, used by apitest (./tools/api-tester/apitest.js) + // - parent + path: {"parent": "/foo", "path":"bar", args...}, used by puter-js (puter.fs.mkdir("/foo/bar")) + if ( !values.parent && values.path ) { + values.parent = await fs.node(new NodePathSelector(_path.dirname(values.path))); + values.path = _path.basename(values.path); + } + let parent_node = values.parent || await fs.node(new RootNodeSelector()); + if ( parent_node.isRoot ) { + // root directory is read-only + throw APIError.create('forbidden', null, { + message: 'Cannot create directories in the root directory.' + }); + } console.log('USING PARENT', parent_node.selector.describe()); + + // TODO: this can be removed upon completion of: https://github.com/HeyPuter/puter/issues/1352 + if ( parent_node.isRoot ) { + // root directory is read-only + throw APIError.create('forbidden', null, { + message: 'Cannot create directories in the root directory.' + }); + } + + let target_basename = _path.basename(values.path); // "top_parent" is the immediate parent of the target directory @@ -282,14 +306,6 @@ class HLMkdir extends HLFilesystemOperation { : await this._get_existing_top_parent({ top_parent: parent_node }) ; - // TODO: this can be removed upon completion of: https://github.com/HeyPuter/puter/issues/1352 - if ( top_parent.isRoot ) { - // root directory is read-only - throw APIError.create('forbidden', null, { - message: 'Cannot create directories in the root directory.' - }); - } - // `parent_node` becomes the parent of the last directory name // specified under `path`. parent_node = await this._create_parents({ diff --git a/tools/api-tester/lib/TestSDK.js b/tools/api-tester/lib/TestSDK.js index 8236c7d81..9feb8dba1 100644 --- a/tools/api-tester/lib/TestSDK.js +++ b/tools/api-tester/lib/TestSDK.js @@ -188,6 +188,16 @@ module.exports = class TestSDK { }); return res.data; }; + // parent + path format: {"parent": "/foo", "path":"bar", args...} + // this is used by puter-js (puter.fs.mkdir("/foo/bar")) + this.mkdir_v2 = async (parent, path, opts) => { + const res = await this.post('mkdir', { + parent: p(parent), + path: p(path), + ...(opts ?? {}) + }); + return res.data; + } this.write = async (path, bin, params) => { path = p(path); params = params ?? {}; diff --git a/tools/api-tester/tests/mkdir.js b/tools/api-tester/tests/mkdir.js index 8d5442ad2..882a4ba57 100644 --- a/tools/api-tester/tests/mkdir.js +++ b/tools/api-tester/tests/mkdir.js @@ -65,5 +65,107 @@ module.exports = { expect(stat.name).equal(`a (${i})`); } }); + + await t.case('mkdir in root directory is prohibited', async () => { + const path = '/a'; + await t.case('throws 403', async () => { + try { + // full path format: {"path":"/foo/bar", args...} + await t.mkdir(path); + } catch (e) { + expect(e.response.status).equal(403); + } + + try { + // parent + path format: {"parent": "/foo", "path":"bar", args...} + const parent = '/'; + await t.mkdir(path, { + parent: parent, + }); + } catch (e) { + expect(e.response.status).equal(403); + } + }); + }); + + await t.case('create_missing_parents works (full path api)', async () => { + const path = 'a/b/c'; + + await t.case('parent directory does not exist', async () => { + try { + await t.stat('a'); + } catch (e) { + expect(e.response.status).equal(404); + } + }); + + await t.case('mkdir failed without create_missing_parents', async () => { + try { + await t.mkdir('a/b/c'); + } catch (e) { + expect(e.response.status).equal(409); + } + }); + + await t.case('mkdir succeeds with create_missing_parents', async () => { + const result = await t.mkdir('a/b/c', { + create_missing_parents: true, + }); + expect(result.name).equal('c'); + }); + + await t.case('can stat the directory', async () => { + const stat = await t.stat(path); + expect(stat.name).equal('c'); + }); + + await t.case('can stat the parent directory', async () => { + let stat = await t.stat('a'); + expect(stat.name).equal('a'); + + stat = await t.stat('a/b'); + expect(stat.name).equal('b'); + }); + }); + + await t.case('create_missing_parents works (parent + path api)', async () => { + const path = 'a/b/c'; + + await t.case('parent directory does not exist', async () => { + try { + await t.stat('a'); + } catch (e) { + expect(e.response.status).equal(404); + } + }); + + await t.case('mkdir failed without create_missing_parents', async () => { + try { + await t.mkdir_v2('a/b', 'c'); + } catch (e) { + expect(e.response.status).equal(409); + } + }); + + await t.case('mkdir succeeds with create_missing_parents', async () => { + const result = await t.mkdir_v2('a/b', 'c', { + create_missing_parents: true, + }); + expect(result.name).equal('c'); + }); + + await t.case('can stat the directory', async () => { + const stat = await t.stat(path); + expect(stat.name).equal('c'); + }); + + await t.case('can stat the parent directory', async () => { + let stat = await t.stat('a'); + expect(stat.name).equal('a'); + + stat = await t.stat('a/b'); + expect(stat.name).equal('b'); + }); + }); } }; \ No newline at end of file